Domain object editing deals with the modification of the properties of domain objects triggered by user interaction. If a user drags one or more corners of a shape to make the shape bigger, for example, the result of the size modification needs to be stored with the domain object represented by the shape. Dragging a particular object handle to another location may move the object itself to that location.

In addition, the functional domain of LuciadLightspeed editing covers the creation of new domain objects as a result of user interaction. A user could click several points on the screen to create the vertices of a new polygon, for instance.

If you want to let input events trigger the creation or modification of one or more objects painted in the view, you can make use of the LuciadLightspeed editor and controller classes to initiate or modify the object’s graphical representation.

Working with editors and controllers

What is editing?

In LuciadLightspeed, an editor instance is able to create and edit a certain type of object. To allow users to use those capabilities, it provides sets of object handles. This means that when users start creating a new object, or select an editable object, the associated object editor provides the users with the user interface (UI) elements that allow them to create or edit that object. For example, when a user selects a bounds object (ILcdBounds), the corresponding bounds editor (TLspBoundsEditor) provides five UI handles: four point handles that allow the user to edit the corner points, and one translation handle that allows the user to translate the bounds object as a whole.

editing handles
Figure 1. Shapes can be edited by interacting with edit handles

The controller coordinates the interaction between the object handles, the editor and the domain model. The editor needs to interpret the information stored about the domain object, as well as the information generated from the input event, to initiate or modify the object appropriately.

Making the objects in a layer editable

To make a Lightspeed layer editable, the following steps are required:

  • Choose an editor

  • Configure your layer for editing

  • Register the chosen editor with the correct paint representation

  • Set the create or edit controller on the view

Choosing an editor

LuciadLightspeed provides a number of pre-defined editors for all the shapes in the API. These editors implement the basic creation and editing behavior and are sufficient in most cases. To work with shapes such as circles, arcs, polygons, and so on, you can use tailored editors such as TLspCircleEditor, TLspArcEditor, TLsp2DPointListEditor respectively.

Some of the other ILspEditor implementations allow you to:

  • Define and edit the base shape as well as the minimum width and maximum height of an extruded shape. TLspExtrudedShapeEditor takes the editor of the base shape as an argument.

  • Use multiple editors in one editor instance: TLspCompositeEditor

For a full list and detailed descriptions, see the API reference documentation of the editor classes. Note that most of the editors that edit shape objects also allow the user to translate the shapes.

For convenience, TLspShapeEditor aggregates all the editors in the API, making it the preferred editor for a large number of use cases.

In cases where specialized editing behavior is required, you can create your own ILspEditor implementations or customize existing implementations. For more information about custom implementations, see Implementing new editors.

Configuring a layer for editing

To configure a layer as editable, use the method call layer.setEditable(true). This sets the layer’s editable flag to true. If you are using a layer builder, the layer is automatically configured as editable when you call the methods for registering body and label editors with the layer. For more information about layer builders, see Setting up your layers.

Registering an editor with the layer

In the Lightspeed view and layer framework, editors are linked to the paint representations of objects. If you want to add creation and editing capabilities for an object, you need to register an editor with the layer, and link it to the appropriate paint representation.

To register an editor for a TLspPaintRepresentation with the layer, either use the setEditor(TLspPaintRepresentation, ILspEditor) method on the layer, or use the available layer builder methods for setting body and label editors.

Note that labels can be edited as well, by calling labelEditor().

Program: Making the shape layer builder body editable is enough to add the TLspShapeEditor to the layer. (from samples/lightspeed/editing/EditableLayerFactory)
return TLspShapeLayerBuilder
    .newBuilder()
    .model(aModel)
    .bodyEditable(true)
    .build();

Setting the controller

The editor needs to receive input events from its UI handles. For this, you need to set a create or edit controller on the view. This controller passes the input events on to the editor. To set a TLspEditController on the view, for example, call view.setController(new TLspEditController()).

The editing process in a Lightspeed view

The editing process involves four main actors:

The following diagram shows the flow of interactions between these classes:

editing sequence
Figure 2. Sequence diagram illustrating the editing process

Suppose that a user clicks a polygon object with the intention of moving one of the polygon points.

  1. When the polygon object is selected, the edit controller requests edit handles for the selected object from the editor set up on the layer for this object type.

  2. The editor returns a list of polygon handles to the controller. The handles are displayed on-screen.

  3. When the user clicks and drags one of the handles, the edit controller issues a user input event to the touched handle.

  4. The touched handle interprets the user input event, and moves to the new position on screen.

  5. The handle confirms that it has been moved by returning an edit operation to the edit controller.

  6. The edit controller contacts the snapper to check if there is a suitable object in the vicinity that the moved handle can be snapped to. If there is a suitable object, the snapping location is marked by a visual indicator. If the handle continues to move closer to the snapping location, it is snapped.

  7. A snap operation containing a list of edit operations is returned to the edit controller.

  8. The edit controller confirms that an edit has been performed on the polygon object by sending the edit operation to the editor. The editor modifies the polygon domain object itself, and returns the result of this operation to the edit controller.

  9. The edit controller notifies the model that one of its objects has changed.

To deactivate the snapping function while they are dragging a handle around, users must press the CTRL (CMD on Mac OS) key.

The following sections discuss the responsibilities of each participant in the editing process in more detail. Afterwards, the possibilities for customization of this process are explained using the samples.lightspeed.customization.hippodrome sample as a guide.

The editor

An ILspEditor is the key component of the editing functionality in LuciadLightspeed. It has two main responsibilities:

  • Creating edit handles. The method getEditHandles() creates a list of ALspEditHandle instances for a given editable domain object. Each edit handle is a small GUI element that users can interact with. It interprets user input and converts it into a higher-level description of the user input, called an edit operation. In the case of a polygon, for instance, an editor typically creates a handle for each vertex. When these handles are dragged around, they report the geographic coordinates to which they have been moved. See The handles for more details.

  • Editing domain objects. The edit() method applies edit operations, represented by the class TLspEditOperation, to domain objects. If we resume the example of the polygon, the edit() method is responsible for actually setting the point of the polygon to its new coordinates. The edit handle itself only reports that it has been moved; it never modifies the underlying domain object.

The edit controller

The coordination between editors and edit handles is performed by TLspEditController. When you activate an edit controller, it retrieves editing candidates, objects that are currently editable, from the view. By default, these correspond to the currently selected objects. For these objects, edit handles are obtained from each corresponding editor. From then on, the controller forwards the input events it receives to the edit handles. The edit operations received from the handles are then routed back to the editor.

The controller is also responsible for visualizing the edit handles. To this effect, it calls the getHandleGeometry() method of the handles, and draws the resulting geometry using an internally created layer.

The only aspect of the edit controller you can customize, is the getEditingCandidates() method. If you want to customize the edit controllers any further, you may want to create a new ILspController implementation. Keep in mind, though, that the interaction with editors relies on a correct setup, so be careful when you decide to take this approach.

The handles

Handles take input events as input and produce edit operations as output. To this effect, handles have a handleAWTEvent() method, which takes an AWTEvent and returns a TLspEditHandleResult. The latter is a container for one or more TLspEditOperation objects.

Using the handle API

There are two main categories of handles:

Figure 3, “Main edit handle implementations” shows the main handle implementations available in LuciadLightspeed.

editing handles classes
Figure 3. Main edit handle implementations

The APIs of both handle types are mostly identical. In the next few sections, we focus on the general properties of handles and on the regular edit handles in particular.

The most important handle classes are:

  • TLspPointTranslationHandle allows the user to drag a point to a new location. The point can be an ILcdPoint object, but also a derived property of another object, such as the center point of a circle, or the start and end points of an arc.

  • TLspPointSetHandle is the counterpart of TLspPointTranslationHandle, used for the creation of points . It allows the user to position a point by clicking on the map, instead of by dragging an existing point to a new location.

  • ALspOutlineResizeHandle can be used for shapes which can be resized by dragging their contours in and out. Examples include the radius of a circle or the width of a buffer.

  • TLspObjectTranslationHandle allows the user to translate an object. This means moving an object to a new location as a whole by clicking anywhere on the object and then dragging it to the new location.

  • ALspCreateHandle is a list of edit handles. It is used only when new objects are created. Create handles are discussed in more detail in Create handles.

Working with handles

Defining properties

The limited set of edit handles listed in Using the handle API can enable a very wide range of editing functionality. Many edit operations can be expressed as dragging some form of object control point to a new location, for instance. However, the editor obviously needs some way to associate each of its edit handles with such a control point. To this effect, each handle has a set of properties. These properties are key/value pairs that can be set by the editor inside its getEditHandles() method.

For instance, an editor may set a value for the key HANDLE_IDENTIFIER. This key indicates that it identifies the role of the handle in a set of handles. If the handle concerned is the one that allows users to expand the radius of a circular object, the value that is paired with the handle identifier key could be RADIUS.

Properties allow the editor to freely attach any kind of semantic information to a handle. When the handle creates a TLspEditOperation object, the latter contains a copy of these properties. This allows the editor to recover this semantic information in its edit() method.

Visualizing handles

Most edit handles require some form of visual representation in the view to be useful. A point translation handle, for instance, is typically visualized as a small icon, so that the application user can actually see the control points that are available to manipulate an object. ALspHandle uses the ILspStyler API to create the visual representation of the handle. Each handle has a getStyleTargetProviders() method, which creates the ALspStyleTargetProvider objects used to visualize the handles.

TLspEditController and TLspCreateController internally add edit handles to a layer that is painted in the view. This layer uses a TLspEditHandleStyler (which can be accessed via the controller). This styler, in turn, uses the style target providers obtained from the handles. The edit controller makes it possible to access the handle layer in the editors and handles using TLspEditContext.getHandleContext(). This makes it for example possible to do isTouched queries on the projected base shape of a 3D object.

The getStyleTargetProviders() method takes a TLspHandleGeometryType as input. This is an enumeration type which splits handle representations into a few broad categories, such as points and visual aid lines. Each of these types has its own style target provider, and TLspEditHandleStyler allows you to register a list of styles for each type.

For more information about style target providers, see Deriving geometry from objects with a style target provider.

How does a handle become active?

To decide which handle to activate, LuciadLightspeed relies on the concepts of handle activation, priority and focus.

Handle activation

Depending on the current view, edit handles may overlap with each other on the screen. In such cases, it is undesirable for user input to be handled by more than one handle simultaneously. To this effect, handles have an activation mechanism, which is implemented by the simple method ALspHandle.isActive().

A handle can choose whether or not it wants to become active when an input event comes in. If the handle does become active, the edit controller only forwards input events to that handle from then on, until the handle advertises that it deactivated.

Handle priorities

To make the handle activation mechanism reliable and predictable for the user, each handle has a priority. When no handle is active, the controller forwards input events to all its handles in descending priority order. This means that the highest-priority handle is always the first one that gets a chance to activate itself. ALspHandle predefines a number of priority levels you can use as a reference. In addition, most edit handles have an appropriate priority by default. Editors can always choose to modify the priorities when they create handles.

Handle focus

Edit handles support a concept of focus, which is closely tied to the activation and priority mechanisms. A handle can request focus if the cursor hovers over it, for example. If it is the highest-priority handle at that location on the screen, it becomes focused. If the mouse is clicked at that same location, the focused handle is also the one that becomes active from then on.

The focus mechanism exists primarily to give visual feedback to the user: the focused handle can be styled differently than the others. To this effect, TLspEditHandleStyler has two sets of styles: one for regular handles and one for focused handles. The default behavior is that point handles turn red when they are in focus.

Working with multi object handles

Edit and create handles are created by an ILspEditor. Multi object handles, on the other hand, are created by controllers. The edit and create controllers may create handles of their own, which perform operations on multiple objects simultaneously. The most typical example of such an operation is selecting and moving a whole group of objects at once.

Multi object handles are represented by the class ALspMultiObjectHandle and are created by the getMultiObjectHandle() method of the controller. The main available implementation is TLspMultiObjectTranslationHandle, which performs the previously mentioned translation of a group of objects.

The multi object handle API differs slightly from the regular handle API because multi object handles work on a collection of objects. Other than that, their functionality is identical to that of other handles. Editors do need to be aware, however, that they may receive edit operations that did not originate from one of their own handles. The discussion of the hippodrome editor below shows how an editor can support the multi-object translation feature.

Snapping

When the TLspEditController receives a TLspEditOperation from an edit handle, it does not forward it to the ILspEditor directly. Instead, the controller has an ILspSnapper which receives the edit operation first. The snapper is given the opportunity to modify the edit operation before it is passed on. To this effect, ILspSnapper defines a snap() method which takes a single TLspEditOperation as input and produces zero, one or more new operations as output.

The ILspSnapper implementation TLspPointToPointSnapper specifically looks for move operations, like a vertex of a polygon being dragged around. When it sees such an operation, it tries to find other domain objects in the vicinity of the dragged point. If the dragged point comes within a certain pixel distance of a nearby vertex of another object, the snapper overrides the incoming edit operation so that its coordinates correspond exactly to this nearby point.

By convention, if the snap() method returns null, the editor forwards the unmodified incoming edit operation to the editor. Otherwise, it uses the operations returned by the snapper.

LuciadLightspeed does not manage snapped objects internally. You can override TLspPointToPointSnapper.snap to get TLspEditOperations, and use the ALspPointSnapper.OBJECT_PROPERTY_KEY property on incoming TLspEditOperations. This property can be used to manage snapped objects.

Program: Retrieving the snapped object using \java{TLspPointToPointSnapper}. (from samples/lightspeed/editing/MainPanel)
TLspPointToPointSnapper pointToPointSnapper = new TLspPointToPointSnapper() {
  @Override
  public TLspSnapOperation snap(TLspEditOperation aIncomingOperation, TLspEditContext aContext) {
    TLspSnapOperation operation = super.snap(aIncomingOperation, aContext);
    for (TLspEditOperation editOperation : operation.getEditOperations()) {
      Object obj = editOperation.getProperties().get(TLspPointToPointSnapper.OBJECT_PROPERTY_KEY);
      if (obj != null) {
        // manage this objects. Actual snapped object is obj.
      }
    }
    return operation;
  }
};

ILspController candidateEditController = defaultController;
while (candidateEditController != null &&
       !(candidateEditController instanceof TLspEditController)) {
  candidateEditController = candidateEditController.getNextController();
}
((TLspEditController) candidateEditController).setSnapperProvider(pointToPointSnapper);

The creation process in a Lightspeed view

Until now, we have only discussed the editing of existing shapes. It is also possible to draw new shapes on the map. The actors in the shape creation process are similar to those required in the shape editing process: editor, controllers and handles. To create new shapes, you need to work with a create controller instead of an edit controller, however, and with create handles instead of editing handles.

The create controller

To draw new shapes on the map, use TLspCreateController instead of TLspEditController. The inner workings of the create and edit controllers are largely identical, although ILspEditor defines a separate getCreateHandle() method that is used instead of getEditHandles(). Many editors require subtly different handles for editing and creation.

Create handles

TLspCreateController invokes the editor’s getCreateHandle() method instead of the getEditHandles() method. The getCreateHandle() method returns an ALspCreateHandle, which is an extension of the regular ALspEditHandle. A create handle is actually a list of ALspEditHandle instances. Since this is a creation process rather than an editing process, the handles are activated in a fixed sequential order to guide a user through the creation of an object. The handles are responsible for initializing the various properties of the object one by one, until the object is ready to be committed to the model.

LuciadLightspeed offers the choice between two types of create handles, depending on whether you know up front how many delegate handles are required to create a shape:

  • If you know the number of delegate handles beforehand, use TLspStaticCreateHandle. For example, it is possible to draw a circle on a map with two mouse clicks: one that sets the center and another that sets the radius of the circle. In such cases, the editor would use a static create handle containing handles for these two properties.

  • If you do not know the number of delegate handles up front, use ALspDynamicCreateHandle. A dynamic create handle instantiates new delegate handles on the fly, and continues to do so until some stop condition is met. A polygon, for instance, is typically drawn with a dynamic create handle. The dynamic handle continues to create new point handles, each of which add an extra vertex to the polygon. The stop condition is a double-click, for example, through which users indicates that they have finished drawing the polygon.

Customizing the editing behavior

This section lists a number of areas in which the editing functionality of Lightspeed views is customizable. This ranges from tweaks to the edit handle visualization to tailor-made editors for custom domain objects.

Visualization and styling of edit handles

Simple tweaks to the handle visualization, such as changes to colors, line widths or icons, can be made by setting different styles on the TLspEditHandleStyler. More advanced customization, such as adding additional visual aid lines, can be performed by overriding the getStyleTargetProviders() method in the handles and/or extending the handle styler.

Omitting edit handles or adding new ones

It is possible to customize the set of edit handles created by an editor by removing certain handles or by adding new ones. For instance, to constrain a circle editor so that it does not allow users to change the radius of the circle, you could leave out the edit handle responsible for defining the radius. Analogously, to allow users to rotate a polygon around its center, for instance, you could add an extra edit handle to the polygon editor.

The obvious way to approach this is by overriding either getEditHandles() or getCreateHandle(). The editor implementations supplied with LuciadLightspeed, however, also follow the convention that each edit handle is created by a dedicated protected method, TLspCircleEditor.createRadiusHandle() for example. This enables you to override these creation methods individually. You can make these methods return null if you want to remove the handle in question, or you can decorate or replace the handle to customize its behavior, to add modifier keys to certain functions for instance.

When customizing the edit handles, it is important to keep in mind that the editor may expect that certain properties are set, to guarantee that a particular edit operation will work. A polygon editor, for instance, expects the handles of the polygon’s vertices to have a property linking each handle to its corresponding vertex.

The properties set by the default editor implementations are described in the API reference documentation. Each editor has an internal enumeration class named PropertyKeys, the values of which are used as the keys for properties. The documentation of each key lists the expected type of the property associated with that key. For non-intrusive changes to edit handles, you should ensure that the editor finds all the properties it expects. If you deviate from these expectations, it is likely that you need to override ILspEditor.edit() as well to handle the deviation.

Editing custom domain objects

LuciadLightspeed’s ILcdModel interface and implementations do not impose any restrictions on the type of elements that are added to them. On the visualization side, the ILspStyler API allows developers to convert model elements of any type into ILcdShape objects that can be painted in a view. Similarly, the edit and create controllers call on the same styler that is used during painting to convert model elements into editable geometry. Since the editor picks up the same geometry that is drawn in the view, in many cases editing for custom domain objects comes "for free" if you have already implemented visualization support for these objects.

It is only in cases where your domain object cannot be decomposed into one or more existing ILcdShape objects, that you will need to develop a fully customized editor. The following section provides more information on how to approach this task.

It is important to note that in order for an object to be editable in a Lightspeed view, it must implement the ILcdCloneable interface.

Implementing new editors

This section discusses the implementation of a custom editor using the samples.lightspeed.customization.hippodrome sample as a guide. This sample contains a class HippodromeEditor, which will be our main focus.

The sample builds on the GXY view-based hippodrome sample which is discussed at length in Implementing a painter and editor in a GXY view. Please refer to this article first for a definition of the hippodrome shape with which we are working.

The new hippodrome editor needs to perform the following tasks:

  • Create and provide edit handles for the hippodrome shape

  • Use the information provided by the edit controllers to apply edit information to the domain objects

  • Support the creation of new hippodrome shapes by providing create handles to the create controller

Creating edit handles

The first task of an editor is to create handles for the domain object being edited. It is generally recommended to reuse the existing ALspEditHandle implementations in LuciadLightspeed where possible. HippodromeEditor follows this recommendation: it uses TLspPointTranslationHandle, TLspObjectTranslationHandle and ALspOutlineResizeHandle.

Semantic information is attached to each handle by setting properties on it. Like the editors in the LuciadLightspeed API, HippodromeEditor defines an enumeration with possible property keys, as shown in Program: Defining handle property keys and values.

Program: Defining handle property keys and values (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
/**
 * Keys used for properties on edit handles.
 */
public static enum PropertyKeys {
  /**
   * Maps to a {@link HandleIdentifier}, which indicates the purpose of the edit handle.
   */
  HANDLE_IDENTIFIER,
}

/**
 * Describes the type of an edit handle created by the enclosing editor implementation.
 *
 * @since 2012.0
 */
public static enum HandleIdentifier {
  /**
   * Identifies the handle at the hippodrome's start point.
   */
  START_POINT,
  /**
   * Identifies the handle at the hippodrome's end point.
   */
  END_POINT,
  /**
   * Identifies the whole-object translation handle.
   */
  TRANSLATE,
  /**
   * Identifies the hippodrome radius (or width) handle.
   */
  RADIUS
}

The getEditHandles() method is implemented as shown in Program: Creating edit handles for a hippodrome. It applies the convention of creating each handle through a separate method.

Program: Creating edit handles for a hippodrome (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
@Override
public List<ALspEditHandle> getEditHandles(TLspEditContext aContext) {
  // Don't edit if the object is not a hippodrome
  Object object = aContext.getGeometry();
  if (!(object instanceof IHippodrome)) {
    return Collections.emptyList();
  }
  final IHippodrome hippodrome = (IHippodrome) object;

  // Create handles and add them to a list
  ArrayList<ALspEditHandle> handles = new ArrayList<ALspEditHandle>(4);

  ALspEditHandle start = createStartPointHandle(hippodrome, aContext, true);
  handles.add(start);

  ALspEditHandle end = createEndPointHandle(hippodrome, aContext, true);
  handles.add(end);

  ALspEditHandle outline = createWidthHandle(hippodrome, true);
  handles.add(outline);

  ALspEditHandle translate = createTranslationHandle(hippodrome);
  handles.add(translate);

  return handles;
}

Program: Creating a point translation handle is an example of the creation of a handle through a separate method. It shows the code that creates a point translation handle for the end point of the hippodrome. Other handles are created in a similar fashion.

Program: Creating a point translation handle (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
private ALspEditHandle createEndPointHandle(final IHippodrome aHippodrome, TLspEditContext aEditContext, boolean aEditing) {
  final ILcdModelReference modelReference = aEditContext.getObjectContext().getModelReference();
  TLspPointTranslationHandle end = new TLspPointTranslationHandle(
      aHippodrome, aHippodrome.getEndPoint(), modelReference
  );
  end.getProperties().put(PropertyKeys.HANDLE_IDENTIFIER, HandleIdentifier.END_POINT);
  end.setTranslateOnDrag(aEditing);
  return end;
}

Applying edit operations

The second main task of an editor is to apply edit operations to the underlying domain object. Edit operations are represented by the class TLspEditOperation. You can use its properties to specify:

  • A general indication of what the user is doing with the object: TLspEditOperationType describes in general terms what kind of operation the user performed. Possible values include MOVE, INSERT_POINT, PROPERTY_CHANGE. The type may also be NO_EDIT, indicating that the domain object does not need to be modified at this point.

  • Additional information as a set of properties, in the form of a key/value map. These properties always include all the properties that were set on the handle that triggered the edit operation.

  • Details of the edit operation, in the form of an operation descriptor. The descriptor is stored in the operation’s properties, using a key which is given by TLspEditOperationType.getPropertyKey(). The type of the descriptor depends on the edit operation type. For MOVE operations, for instance, the descriptor is a TLspMoveDescriptor. The API reference documentation for each predefined operation type also lists the corresponding operation descriptor class.

  • The model reference in which the operation is described. This normally is the reference of the model that contains the domain object.

  • An indication of whether the operation is considered complete or not. The InteractionStatus indicates whether the edit operation leaves the edited object in a committable state or not. One of the uses for this property is undo/redo functionality: the edit or create controller only registers an undoable step for operations that are marked as FINISHED, not for ones that are marked as IN_PROGRESS.

The combined information in TLspEditOperation should give the editor sufficient information to make changes to the edited domain object. Program: Implementing the edit() method for hippodromes shows the implementation of the edit() method for hippodromes.

Program: Implementing the edit() method for hippodromes (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
private static final String RADIUS_PROPERTY_NAME = "width";

@Override
protected TLspEditOperationResult editImpl(TLspEditOperation aOperation, ELspInteractionStatus aInteractionStatus, TLspEditContext aContext) {
  Object object = aContext.getGeometry();
  if (!(object instanceof IHippodrome)) {
    return TLspEditOperationResult.FAILED;
  }
  IHippodrome hippodrome = (IHippodrome) object;

  TLspEditOperationType type = aOperation.getType();
  if (type == TLspEditOperationType.MOVE) {
    TLspMoveDescriptor descriptor =
        (TLspMoveDescriptor) aOperation.getProperties().get(type.getPropertyKey());
    applyMove(hippodrome, aOperation, descriptor);
    return TLspEditOperationResult.SUCCESS;
  } else if (type == TLspEditOperationType.PROPERTY_CHANGE) {
    TLspPropertyChangeDescriptor descriptor =
        (TLspPropertyChangeDescriptor) aOperation.getProperties().get(type.getPropertyKey());
    if (RADIUS_PROPERTY_NAME.equals(descriptor.getPropertyName()) &&
        descriptor.getNewValue() != null) {
      hippodrome.setWidth(Math.abs((Double) descriptor.getNewValue()));
      return TLspEditOperationResult.SUCCESS;
    }
  }
  return TLspEditOperationResult.FAILED;
}

The method first performs a quick sanity check by testing if the edited object is effectively a hippodrome. Next, it looks at the edit operation type and extracts the corresponding operation descriptor from the operation’s properties. The hippodrome supports two operation types:

  • MOVE is used for changes to the start and end point of the hippodrome, or for a translation of the object as a whole. These changes are described by a TLspMoveDescriptor.

  • PROPERTY_CHANGE is used for changes to the radius of the hippodrome. Since the radius of the arcs is equal to the width of the hippodrome, it is referred to as the width. This operation is described by a TLspPropertyChangeDescriptor.

The PROPERTY_CHANGE case is the simplest: if the property change descriptor reports "width" as the name of the property being changed, the editor knows that the value stored in the descriptor is a double that it can pass in a call to IHippodrome.setWidth().

MOVE operations are dealt with in a separate method, which is shown in Program: Applying a move operation to a hippodrome.

Program: Applying a move operation to a hippodrome (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
private void applyMove(
    IHippodrome aHippodrome,
    TLspEditOperation aOperation,
    TLspMoveDescriptor aDescriptor
) {
  ILcdPoint startPoint = aDescriptor.getStartPoint();
  ILcdPoint targetPoint = aDescriptor.getTargetPoint();
  HandleIdentifier handleIdentifier = (HandleIdentifier) aOperation.getProperties().get(
      PropertyKeys.HANDLE_IDENTIFIER
  );

  if (handleIdentifier == null) {
    // This can happen for multi-object translation handles, which are created
    // by the controller rather than the editor.
    if (startPoint != null) {
      aHippodrome.translate2D(
          targetPoint.getX() - startPoint.getX(),
          targetPoint.getY() - startPoint.getY()
      );
    }
  } else {
    switch (handleIdentifier) {
    case START_POINT:
      aHippodrome.moveReferencePoint(targetPoint, IHippodrome.START_POINT);
      break;
    case END_POINT:
      aHippodrome.moveReferencePoint(targetPoint, IHippodrome.END_POINT);
      break;
    case TRANSLATE:
      if (startPoint != null) {
        aHippodrome.translate2D(
            targetPoint.getX() - startPoint.getX(),
            targetPoint.getY() - startPoint.getY()
        );
      }
      break;
    }
  }
}

To perform the move operation on a hippodrome, the editor first retrieves the properties from the edit operations that allow it to identify which handle triggered the edit operation. Based on these properties, the editor moves either the start point, the end point or the entire hippodrome.

The editor also specifically checks for the case in which properties are missing from the operation. This can happen when the operation was fired by a multi-object handle instead of a regular edit handle. In practice, this means that the user selected and dragged multiple shapes simultaneously, so the editor applies the whole-object translation.

Creating create handles

The hippodrome editor also supports the drawing of new hippodrome objects in the view. Program: Create handle for a hippodrome shows the implementation of the getCreateHandle() method.

Program: Create handle for a hippodrome (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
@Override
public ALspEditHandle getCreateHandle(TLspEditContext aContext) {
  // Don't edit if the object is not a hippodrome
  Object object = aContext.getGeometry();
  if (!(object instanceof IHippodrome)) {
    return null;
  }
  final IHippodrome hippodrome = (IHippodrome) object;

  // We use a static create handle, since we know beforehand
  // how many handles are needed to initialize the hippodrome
  Collection<ALspEditHandle> handles = new ArrayList<ALspEditHandle>();

  ALspEditHandle start = createStartPointHandle(hippodrome, aContext, false);
  handles.add(start);

  ALspEditHandle end = createEndPointHandle(hippodrome, aContext, false);
  handles.add(end);

  ALspEditHandle width = createWidthHandle(hippodrome, false);
  handles.add(width);

  return new TLspStaticCreateHandle(hippodrome, handles);
}

This method uses a static create handle, because a hippodrome can be drawn with a fixed number of mouse clicks:

  • The first click to set the start point

  • The second click to set the end point

  • The last click to set the width of the hippodrome

Each of these corresponds to an edit handle that is added to the TLspStaticCreateHandle. These handles are the same as the ones returned by getEditHandles(), with one exception. The first handle, which sets the hippodrome’s start point, is created differently in creation mode. Program: Differentiating between editing and creation modes shows how.

Program: Differentiating between editing and creation modes (from samples/lightspeed/customization/hippodrome/HippodromeEditor)
private ALspEditHandle createStartPointHandle(
    final IHippodrome aHippodrome,
    TLspEditContext aContext,
    boolean aEditing
) {
  ALspEditHandle start;
  // When editing, use a TLspPointTranslationHandle. This handle allows a point to
  // be dragged using the mouse.
  final ILcdModelReference modelReference = aContext.getObjectContext().getModelReference();
  if (aEditing) {
    start = new TLspPointTranslationHandle(
        aHippodrome, aHippodrome.getStartPoint(), modelReference
    );
  }
  // When creating, use an TLspPointSetHandle instead. This handle allows the
  // point to be positioned using a mouse click.
  else {
    start = new TLspPointSetHandle(aHippodrome, aHippodrome.getStartPoint(), modelReference) {
      @Override
      protected TLspEditHandleResult createEditHandleResult(ILcdPoint aViewPoint,
                                                            AWTEvent aOriginalEvent,
                                                            AWTEvent aProcessedEvent,
                                                            ELspInteractionStatus aInteractionStatus,
                                                            TLspEditContext aEditContext) {
        // Add operations to move the end point, and initialize the width
        // This results in a better visualization during creation
        TLspEditHandleResult editHandleResult = super.createEditHandleResult(aViewPoint,
                                                                             aOriginalEvent,
                                                                             aProcessedEvent,
                                                                             aInteractionStatus,
                                                                             aEditContext);
        List<TLspEditOperation> operations = new ArrayList<TLspEditOperation>();
        for (TLspEditOperation operation : editHandleResult.getEditOperations()) {
          // Add the original operation
          operations.add(operation);

          if (operation.getType() == TLspEditOperationType.MOVE) {
            String movePropertyKey = TLspEditOperationType.MOVE.getPropertyKey();
            TLspMoveDescriptor moveDescriptor =
                (TLspMoveDescriptor) operation.getProperties().get(movePropertyKey);

            // Add an operation to move the end point to the same location
            Map<Object, Object> properties1 = new HashMap<Object, Object>();
            properties1.put(PropertyKeys.HANDLE_IDENTIFIER, HandleIdentifier.END_POINT);
            properties1.put(movePropertyKey, moveDescriptor);
            operations.add(new TLspEditOperation(TLspEditOperationType.MOVE, properties1));

            // Add an operation to set the width to an initial value
            Map<Object, Object> properties2 = new HashMap<Object, Object>();
            properties2.put(PropertyKeys.HANDLE_IDENTIFIER, HandleIdentifier.RADIUS);
            properties2.put(TLspEditOperationType.PROPERTY_CHANGE.getPropertyKey(),
                            new TLspPropertyChangeDescriptor<Double>(RADIUS_PROPERTY_NAME,
                                                                     aHippodrome.getWidth(),
                                                                     0.001)
            );
            operations.add(new TLspEditOperation(TLspEditOperationType.PROPERTY_CHANGE, properties2));
          }
        }

        // Return a new handle result with the extra operations
        return new TLspEditHandleResult(operations,
                                        editHandleResult.getProcessedEvent(),
                                        editHandleResult.getInteractionStatus());
      }
    };
  }
  start.getProperties().put(PropertyKeys.HANDLE_IDENTIFIER, HandleIdentifier.START_POINT);
  return start;
}

To create a start handle for hippodrome editing purposes, a standard TLspPointTranslationHandle is used. This handle allows the user to drag an existing point to a new location. However, when a new hippodrome is being drawn in creation mode, the start point has not been defined yet. Therefore, an TLspPointSetHandle is used instead of an TLspPointTranslationHandle for creation purposes. An TLspPointSetHandle allows the user to position the point by clicking on the map. When the user places the start point, the editor puts the end point at the same coordinates. This means that the subsequent end point edit handle does not need to be an TLspPointSetHandle.