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.
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, |
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()
.
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:
Suppose that a user clicks a polygon object with the intention of moving one of the polygon points.
-
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.
-
The editor returns a list of polygon handles to the controller. The handles are displayed on-screen.
-
When the user clicks and drags one of the handles, the edit controller issues a user input event to the touched handle.
-
The touched handle interprets the user input event, and moves to the new position on screen.
-
The handle confirms that it has been moved by returning an edit operation to the edit controller.
-
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.
-
A snap operation containing a list of edit operations is returned to the edit controller.
-
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.
-
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 ofALspEditHandle
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 classTLspEditOperation
, to domain objects. If we resume the example of the polygon, theedit()
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:
-
To edit single domain objects, choose the regular edit handle type
ALspEditHandle
. -
To edit collections of domain objects, start from multi object handles,
ALspMultiObjectHandle
. To learn more about multi object handles, see Working with multi object handles.
Figure 3, “Main edit handle implementations” shows the main handle implementations available in LuciadLightspeed.
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 anILcdPoint
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 ofTLspPointTranslationHandle
, 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.
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 |
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.
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.
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.
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 includeMOVE
,INSERT_POINT
,PROPERTY_CHANGE
. The type may also beNO_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. ForMOVE
operations, for instance, the descriptor is aTLspMoveDescriptor
. 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 asFINISHED
, not for ones that are marked asIN_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.
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 aTLspMoveDescriptor
. -
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 aTLspPropertyChangeDescriptor
.
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.
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.
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.
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
.