Prerequisites
This tutorial assumes that you are familiar with:
-
The concepts introduced in the Adding support for custom static data to a Lightspeed view tutorial:
ALcyFormat
,ALcyLspFormat
,ALcyFormatAddOn
andALcyLspFormatAddOn
. -
Model encoding and the concept of an
ILcdModelEncoder
Goal
This tutorial shows you how you make the waypoint format, introduced in the Adding support for custom static data to a Lightspeed view tutorial, editable:
-
The user must be able to add new waypoints to an existing waypoint file.
-
The user must be able to create a new waypoints layer.
-
The user must be able to edit and delete existing waypoints on the map.
-
The user must be able to edit a waypoint through a textual UI. A textual UI allows a user to precisely enter the coordinates, and adjust the name of the waypoint.
-
The user must be able to save the waypoint data to a waypoint file.
Initial setup
The initial setup for this tutorial is almost identical to the end result of the Adding support for custom static data to a Lightspeed view tutorial:
-
An
ALcyFormatAddOn
that plugs in anALcyFormat
for decoding the waypoint models. -
An
ALcyLspFormatAddOn
that plugs in anALcyLspFormat
for visualizing the waypoint models. The only difference is that we use theALcyLspStyleFormat
and not theTLcyLspVectorFormat
. -
A custom
addons.xml
file to include those add-ons
The WayPointModelAddOn
code
public class WayPointModelAddOn extends ALcyFormatAddOn {
public WayPointModelAddOn() {
super(ALcyTool.getLongPrefix(WayPointModelAddOn.class),
ALcyTool.getShortPrefix(WayPointModelAddOn.class));
}
@Override
protected ALcyFormat createBaseFormat() {
return new WayPointModelFormat(getLucyEnv(),
getLongPrefix(),
getShortPrefix(),
getPreferences());
}
@Override
protected ALcyFormat createFormatWrapper(ALcyFormat aBaseFormat) {
return new TLcySafeGuardFormatWrapper(aBaseFormat);
}
}
The WayPointModelFormat
code
class WayPointModelFormat extends ALcyFileFormat {
WayPointModelFormat(ILcyLucyEnv aLucyEnv, String aLongPrefix, String aShortPrefix, ALcyProperties aProperties) {
super(aLucyEnv, aLongPrefix, aShortPrefix, aProperties);
}
@Override
protected ILcdModelDecoder[] createModelDecoders() {
return new ILcdModelDecoder[]{new WayPointsModelDecoder()};
}
@Override
protected ILcyModelContentTypeProvider createModelContentTypeProvider() {
//All our models only contain point data, so we can return a fixed type
//No need to check the contents of the model
return aModel -> ILcyModelContentType.POINT;
}
@Override
protected ILcyGXYLayerTypeProvider createGXYLayerTypeProvider() {
return null;
}
@Override
protected ILcdGXYLayerFactory createGXYLayerFactory() {
return null;
}
@Override
public boolean isModelOfFormat(ILcdModel aModel) {
//All the waypoint models created by our model decoder have CWP as type name
//We assume here that this typename is unique over all supported formats
return "CWP".equals(aModel.getModelDescriptor().getTypeName());
}
}
The configuration file for the WayPointModelAddOn
WayPointModelAddOn.fileTypeDescriptor.displayName=Waypoint files
WayPointModelAddOn.fileTypeDescriptor.defaultExtension=cwp
WayPointModelAddOn.fileTypeDescriptor.filters=*.cwp
WayPointModelAddOn.fileTypeDescriptor.groupIDs=All Vector Files
The WayPointAddOn
code
public class WayPointAddOn extends ALcyLspFormatAddOn {
public WayPointAddOn() {
super(ALcyTool.getLongPrefix(WayPointAddOn.class),
ALcyTool.getShortPrefix(WayPointAddOn.class));
}
@Override
protected ALcyLspFormat createBaseFormat() {
ILcdFilter<ILcdModel> modelFilter =
(ILcdFilter<ILcdModel>) aModel -> "CWP".equals(aModel.getModelDescriptor().getTypeName());
return new WayPointFormat(getLucyEnv(),
getLongPrefix(),
getShortPrefix(),
getPreferences(),
modelFilter);
}
@Override
protected ALcyLspFormat createFormatWrapper(ALcyLspFormat aBaseFormat) {
return new TLcyLspSafeGuardFormatWrapper(aBaseFormat);
}
}
The WayPointFormat
code
public class WayPointFormat extends ALcyLspStyleFormat {
public WayPointFormat(ILcyLucyEnv aLucyEnv, String aLongPrefix, String aShortPrefix, ALcyProperties aPreferences, ILcdFilter<ILcdModel> aModelFilter) {
super(aLucyEnv, aLongPrefix, aShortPrefix, aPreferences, aModelFilter);
}
@Override
protected ILspLayerFactory createLayerFactoryImpl() {
return new ALspSingleLayerFactory() {
@Override
public ILspLayer createLayer(ILcdModel aModel) {
//Ensure that the layer is editable
//Also make sure the layer has labels
return TLspShapeLayerBuilder.newBuilder()
.model(aModel)
.bodyEditable(true)
.labelStyler(TLspPaintState.REGULAR, createLabelStyler())
.build();
}
private TLspCustomizableStyler createLabelStyler() {
TLspDataObjectLabelTextProviderStyle labelContentsStyle =
TLspDataObjectLabelTextProviderStyle.newBuilder()
.expressions("identifier")
.build();
return new TLspCustomizableStyler(TLspTextStyle.newBuilder().build(),
labelContentsStyle);
}
@Override
public boolean canCreateLayers(ILcdModel aModel) {
//The TLcyLspSafeGuardFormatWrapper only passes waypoint models
//to this factory, so no need to check anything here
return true;
}
};
}
}
The configuration file for the WayPointAddOn
WayPointAddOn.style.fileTypeDescriptor.displayName=Style Files
WayPointAddOn.style.fileTypeDescriptor.defaultExtension=sty
WayPointAddOn.style.fileTypeDescriptor.filters=*.sty
WayPointAddOn.style.fileTypeDescriptor.groupIDs=All Style Files
WayPointAddOn.defaultStyleFile=docs/articles/tutorial/lucy/customdata/defaultWaypointStyle.sty
The custom addons.xml
file
<?xml version="1.0" encoding="UTF-8"?>
<addonConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="config/lucy/addons.xsd">
<addons>
<!-- Include all the original add-ons -->
<include>lucy/addons.xml</include>
<!-- Add our own add-ons for waypoint data -->
<addon>
<priority>data_producer</priority>
<name>Waypoint model format</name>
<class>com.luciad.lucy.addons.tutorial.editabledata.model.WayPointModelAddOn</class>
<configFile>docs/articles/tutorial/lucy/customdata/editable/WayPointModelAddOn.cfg</configFile>
</addon>
<addon>
<priority>data_producer</priority>
<name>Waypoint format</name>
<class>com.luciad.lucy.addons.tutorial.editabledata.WayPointAddOn</class>
<configFile>docs/articles/tutorial/lucy/customdata/editable/WayPointAddOn.cfg</configFile>
</addon>
</addons>
</addonConfiguration>
Note how, compared to the Adding support for custom static data to a Lightspeed view tutorial, we
now use an ALcyLspStyleFormat
for the ALcyLspFormat
.
This gives us control over the layer factory and allows to create editable layers:
return new ALspSingleLayerFactory() {
@Override
public ILspLayer createLayer(ILcdModel aModel) {
//Ensure that the layer is editable
//Also make sure the layer has labels
return TLspShapeLayerBuilder.newBuilder()
.model(aModel)
.bodyEditable(true)
.labelStyler(TLspPaintState.REGULAR, createLabelStyler())
.build();
}
private TLspCustomizableStyler createLabelStyler() {
TLspDataObjectLabelTextProviderStyle labelContentsStyle =
TLspDataObjectLabelTextProviderStyle.newBuilder()
.expressions("identifier")
.build();
return new TLspCustomizableStyler(TLspTextStyle.newBuilder().build(),
labelContentsStyle);
}
@Override
public boolean canCreateLayers(ILcdModel aModel) {
//The TLcyLspSafeGuardFormatWrapper only passes waypoint models
//to this factory, so no need to check anything here
return true;
}
};
At this point, we have waypoint data that we can edit on a Lightspeed view with the default controller of Lucy, which supports editing.
Saving the data using an ILcdModelEncoder
When users modify the data, they probably want to save the modified data back to file.
In a LuciadLightspeed application, you can use an ILcdModelEncoder
to save data.
Whereas you use an ILcdModelDecoder
to read input data and convert it into an ILcdModel
,
the ILcdModelEncoder
does the opposite. It takes an ILcdModel
as input, and writes it to a destination, to a file on-disk for example.
Lucy already offers UI to save data that is present on the map through the File | Save and File | Save as menu items. Those menu items work in exactly the same way as the File | Open item:
-
Lucy asks the user which data it needs to be save, and to which destination.
-
Lucy loops over all known
ILcdModelEncoder
instances. -
If a model encoder indicates it can save the data, Lucy delegates the actual saving to that model encoder.
-
If none of the registered model encoders can save the data, the data cannot be saved.
That means that we must do two things:
-
Create an
ILcdModelEncoder
: for this purpose, we re-use the encoder created in the Model encoding tutorial. -
Register the model encoder as a Lucy service: because Lucy loops over all known model encoders, it is sufficient to register our model encoder as a service to ensure that it is available when the user wants to save way point data.
Similar to the ILcdModelDecoder
, we achieve that by creating the ILcdModelEncoder
in our ALcyFormat
class:
@Override
protected ILcdModelEncoder[] createModelEncoders() {
return new ILcdModelEncoder[]{new WayPointsModelEncoder()};
}
Now we can use the File | Save menu item to save waypoint data.
Editing way points through a panel
At this point, users can only edit the location and, in 3D, the height of a waypoint on the map. There are no options to edit the name of the way point, nor to enter precise coordinates for the location. That kind of input requires a panel where the user can enter textual information.
Lucy already has a mechanism to show the properties of an object. For example, when you double-click on a flight plan or way point object on the map, the Object Properties UI opens, and shows a generic panel with the properties of the object. This section illustrates how you can change the object properties panel for specific objects.
Like all the functionality offered by the Lucy framework, the Object Properties action relies on the Lucy services. When that action is triggered, it:
-
Retrieves the selected object: the action first determines which object is selected on the map, and to which layer and model it belongs. All that information is grouped into a
TLcyDomainObjectContext
instance. -
Finds a factory to create the UI: Lucy uses the
TLcyDomainObjectContext
to ask each of the registeredILcyCustomizerPanelFactory
instances whether they can create anILcyCustomizerPanel
UI panel for thatTLcyDomainObjectContext
. -
Shows the UI: the UI created by the factory is shown by the Object Properties action.
The creation of an ILcyCustomizerPanel
for waypoints is illustrated in the Implementing an ILcyCustomizerPanel tutorial.
For this tutorial, we are going to re-use that customizer panel and create it in an ILcyCustomizerPanelFactory
which we register as a service.
The ILcyCustomizerPanelFactory
for domain objects is created in the ALcyFormat.createDomainObjectCustomizerPanelFactories
method:
@Override
protected ILcyCustomizerPanelFactory[] createDomainObjectCustomizerPanelFactories() {
return new ILcyCustomizerPanelFactory[]{
new ILcyCustomizerPanelFactory() {
@Override
public boolean canCreateCustomizerPanel(Object aObject) {
//The TLcyLspSafeGuardFormatWrapper takes care of this
return true;
}
@Override
public ILcyCustomizerPanel createCustomizerPanel(Object aObject) {
return new WayPointCustomizerPanel(getLucyEnv());
}
}
};
}
When we now open the object properties of a waypoint, we have a custom panel that allows us to make changes to the waypoint.
Creating an empty way points layer
The user can create new layers from the File | New | Layer menu item. When the new layer action is triggered, Lucy shows the users UI to indicate what kind of layer they want to create.
Currently, the UI does not yet offer the option to create a new way points layer. We need to register a few more services before the action picks up the way point format.
The action needs to be able to create a new way point model from scratch. Once the model has been created, the action can use the already available layer factory to create and add the layer to the map.
Creating a model requires two new services: an ALcyDefaultModelDescriptorFactory
to create the ILcdModelDescriptor
,
and an ILcdModelFactory
to create the actual model with that model descriptor.
Both those instances can be created in the ALcyFormat
, ensuring that they are registered as a service:
@Override
protected ALcyDefaultModelDescriptorFactory[] createDefaultModelDescriptorFactories() {
return new ALcyDefaultModelDescriptorFactory[]{
new ALcyDefaultModelDescriptorFactory() {
@Override
public ILcdModelDescriptor createDefaultModelDescriptor() {
//Return the same model descriptor as the model decoder is creating
return new TLcdDataModelDescriptor(null,
"CWP",
"Way Points",
WayPointsModelDecoder.DATA_MODEL,
Collections.singleton(WayPointsModelDecoder.WAYPOINT_TYPE),
WayPointsModelDecoder.DATA_MODEL.getTypes());
}
}
};
}
@Override
protected ILcdModelFactory createModelFactory() {
return new ILcdModelFactory() {
@Override
public ILcdModel createModel(ILcdModelDescriptor aModelDescriptor, ILcdModelReference aModelReference) throws IllegalArgumentException {
//First check whether the model descriptor is one we recognize
//The TLcySafeGuardFormatWrapper does not help us, because that wrapper can only check whether
//a model is valid for a format.
//It has no knowledge of model descriptors
if (!"CWP".equals(aModelDescriptor.getTypeName())) {
throw new IllegalArgumentException("Cannot create model for model descriptor [" + aModelDescriptor + "]");
}
//Create a model the same was as done in the WayPointsModelDecoder
return new TLcd2DBoundsIndexedModel(new TLcdGeodeticReference(), aModelDescriptor);
}
};
}
Creating new way points on the map
In the previous sections, we added the necessary controls and UI elements to edit existing way points and create new layers. Typically, when you are working with editable data, you also want to allow the users to create new instances.
Just like any other operation that interacts with the map, creating new objects on the map requires an ILspController
instance.
Although Lucy offers a default controller that allows editing and navigation, it does not allow creating new shapes.
The reason for that is that it is the responsibility of the controller to create the new domain object, but Lucy has no idea what kind of domain object the user wants to create.
Therefore, we need to create our own ILspController
.
In addition, the user must be able to select the controller in the UI.
In this tutorial, we:
-
Create a tool bar that contains the UI to activate the controller
-
Show the tool bar on the map
-
Create and add a button to activate the controller on the tool bar
-
Ensure that the controller asks the user if it needs to create a new layer when it is activated but there is no waypoint layer yet
The Lucy framework already contains a mechanism to show a tool bar on the map for the selected layer. As always, the framework looks through the Lucy services for certain interfaces and classes to create the tool bar, and shows the tool bar on the map if one is available.
The creation of the tool bar is delegated to an ALcyFormatBarFactory
, which is a factory class
for ALcyFormatBar
instances.
An ALcyFormatBar
instance is a tool bar for a certain data format.
Creating the format bar
Our format bar must meet these requirements:
-
It must contain a button to activate the controller to create a new waypoint.
-
When the button is pressed and there is no waypoint layer, a waypoint layer must be created.
-
The position of the button must be configurable. In a real application, it is often the case that the user can create different objects or different geometries. As such, it is convenient if the location of the buttons can be configured to make it easy to group them in sub-groups, for example.
We will use a number of utility classes and methods:
-
The
TLcyLspCreateLayerAction
is an action that can create a new layer and add it to the map based on anALcyDefaultModelDescriptorFactory
. -
insertCreateShapeActiveSettable
is a utility method in the samplesamples.lucy.util.LayerUtil
that creates anILspController
for anALspCreateControllerModel
, and inserts a button to activate that controller in the UI. -
The
TLcyActionBarUtil#setupAsConfiguredActionBar
method ensures that the contents of anILcyActionBar
can be configured. -
The
ALcyLspCreateControllerModel
class, which is an extension ofALspCreateControllerModel
. This controller model uses anTLcyLspCreateLayerAction
to create a layer when there is no layer to draw on.
Our ALcyFormatBar
implementation looks like this:
package com.luciad.lucy.addons.tutorial.editabledata;
import java.awt.Component;
import javax.swing.JComponent;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.gui.TLcyActionBarManager;
import com.luciad.lucy.gui.TLcyActionBarUtil;
import com.luciad.lucy.gui.TLcyToolBar;
import com.luciad.lucy.gui.formatbar.ALcyFormatBar;
import com.luciad.lucy.map.action.lightspeed.TLcyLspCreateLayerAction;
import com.luciad.lucy.map.lightspeed.TLcyLspMapComponent;
import com.luciad.lucy.model.ALcyDefaultModelDescriptorFactory;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.lightspeed.layer.ILspInteractivePaintableLayer;
import samples.lucy.util.LayerUtil;
class WayPointFormatBar extends ALcyFormatBar {
/**
* The actual Swing component representing the format bar
*/
private final TLcyToolBar fToolBar = new TLcyToolBar();
private final WayPointCreateControllerModel fControllerModel;
WayPointFormatBar(TLcyLspMapComponent aLspMapComponent,
ALcyProperties aProperties,
String aShortPrefix,
ALcyDefaultModelDescriptorFactory aDefaultModelDescriptorFactory,
ILcyLucyEnv aLucyEnv) {
putValue(ALcyFormatBar.NAME, "Way Points");
putValue(ALcyFormatBar.SHORT_DESCRIPTION, "Create way points");
//Allow TLcyActionBarUtil (and other add-ons) to contribute to our tool bar
TLcyActionBarManager actionBarManager = aLucyEnv.getUserInterfaceManager().getActionBarManager();
TLcyActionBarUtil.setupAsConfiguredActionBar(fToolBar,
WayPointFormatBarFactory.TOOLBAR_ID,
aLspMapComponent,
aProperties,
aShortPrefix,
(JComponent) aLspMapComponent.getComponent(),
actionBarManager);
//Create and insert a button for the controller
TLcyLspCreateLayerAction createLayerAction = new TLcyLspCreateLayerAction(aLucyEnv, aLspMapComponent);
createLayerAction.setDefaultModelDescriptorFactory(aDefaultModelDescriptorFactory);
fControllerModel = new WayPointCreateControllerModel(aLspMapComponent, createLayerAction);
LayerUtil.insertCreateShapeActiveSettable(aProperties, aShortPrefix, aLucyEnv, aLspMapComponent, fControllerModel);
}
@Override
protected void updateForLayer(ILcdLayer aPreviousLayer, ILcdLayer aLayer) {
fControllerModel.setCurrentLayer((ILspInteractivePaintableLayer) aLayer);
}
@Override
public boolean canSetLayer(ILcdLayer aLayer) {
// TLcyLspSafeGuardFormatWrapper already checks the layer
return true;
}
@Override
public Component getComponent() {
return fToolBar.getComponent();
}
}
and here is the WayPointCreateControllerModel
:
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.map.action.lightspeed.TLcyLspCreateLayerAction;
import com.luciad.lucy.map.lightspeed.ILcyLspMapComponent;
import com.luciad.lucy.map.lightspeed.controller.ALcyLspCreateControllerModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.view.lightspeed.ILspView;
import com.luciad.view.lightspeed.layer.ILspLayer;
final class WayPointCreateControllerModel extends ALcyLspCreateControllerModel {
WayPointCreateControllerModel(ILcyLspMapComponent aMapComponent, TLcyLspCreateLayerAction aCreateLayerAction) {
super(aMapComponent, aCreateLayerAction);
}
@Override
public Object create(ILspView aView, ILspLayer aLayer) {
//Create the waypoint domain object, and initiate it with dummy values
ILcdDataObject wayPoint = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", "WayPoint (no name)");
wayPoint.setValue("location", new TLcdLonLatHeightPoint(0, 0, 0));
return wayPoint;
}
}
The TLcyActionBarUtil.setupAsConfiguredActionBar
method, which we call in the WayPointFormatBar
class, will read
some settings for the tool bar from the configuration file of our add-on.
So we add the following lines to the WayPointAddOn.cfg
file:
# This property defines the order of the groups in which the toolbar items are contained. For more
# information, please refer to the lucy.cfg configuration file, more specifically to the property
# TLcyMain.menuBar.groupPriorities and its comments.
WayPointAddOn.wayPointsToolBar.groupPriorities=\
LayerGroup,\
CreateGroup,\
DefaultGroup
The button for the controller is inserted using TLcyActionBarUtil.insertInConfiguredActionBars
.
As such, we need to configure its location in the WayPointAddOn.cfg
configuration file as well:
# Insert the button to activate the create controller in the format bar
WayPointAddOn.newActiveSettable.wayPointsToolBar.item=Way Point
WayPointAddOn.newActiveSettable.wayPointsToolBar.groups=CreateGroup
WayPointAddOn.newActiveSettable.wayPointsToolBar.shortDescription=Create a new way point
WayPointAddOn.newActiveSettable.wayPointsToolBar.smallIcon=draw_point
Creating and registering the format bar factory
You can create the format bar factory in the ALcyLspFormat
with the ALcyLspFormat.createFormatBarFactory
method, and register it automatically:
@Override
protected ALcyFormatBarFactory createFormatBarFactory() {
return new WayPointFormatBarFactory(getLucyEnv(), getProperties(), getShortPrefix());
}
The Therefore, we create the format bar in the |
The implementation of the factory creates new WayPointFormatBar
instances:
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.addons.tutorial.editabledata.model.WayPointModelAddOn;
import com.luciad.lucy.gui.formatbar.ALcyFormatBar;
import com.luciad.lucy.gui.formatbar.ALcyFormatBarFactory;
import com.luciad.lucy.map.ILcyGenericMapComponent;
import com.luciad.lucy.map.lightspeed.TLcyLspMapComponent;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;
class WayPointFormatBarFactory extends ALcyFormatBarFactory {
static String TOOLBAR_ID = "wayPointsToolBar";
private final ILcyLucyEnv fLucyEnv;
private final ALcyProperties fProperties;
private final String fShortPrefix;
WayPointFormatBarFactory(ILcyLucyEnv aLucyEnv, ALcyProperties aProperties, String aShortPrefix) {
fLucyEnv = aLucyEnv;
fProperties = aProperties;
fShortPrefix = aShortPrefix;
}
@Override
public boolean canCreateFormatBar(ILcdView aView, ILcdLayer aLayer) {
// TLcyLspSafeGuardFormatWrapper already checks the layer
return findLspMapComponent(aView) != null;
}
@Override
public ALcyFormatBar createFormatBar(ILcdView aView, ILcdLayer aLayer) {
WayPointModelAddOn wayPointsModelAddOn = fLucyEnv.retrieveAddOnByClass(WayPointModelAddOn.class);
return new WayPointFormatBar(findLspMapComponent(aView),
fProperties,
fShortPrefix,
wayPointsModelAddOn.getFormat().getDefaultModelDescriptorFactories()[0],
fLucyEnv);
}
private TLcyLspMapComponent findLspMapComponent(ILcdView aView) {
ILcyGenericMapComponent mapComponent = fLucyEnv.getCombinedMapManager().findMapComponent(aView);
return mapComponent instanceof TLcyLspMapComponent ? (TLcyLspMapComponent) mapComponent : null;
}
}
Adding a create layer action to the tool bar
Our on-map toolbar currently contains just a controller to create new way points.
From a usability perspective, it makes sense to add a tool bar action that create a new way points layer on the map.
Because our tool bar already populates itself with all the actions registered for it in the TLcyActionBarManager
,
we can add the new action by creating it, and then registering it with that manager.
The layer creation action is specific to a map, and is inserted in a map-specific action bar. We need to
register an instance of this action for each existing map component, and all future map components.
To do so, we use the plugInto
method of our ALcyLspFormatAddOn
:
@Override
public void plugInto(ILcyLucyEnv aLucyEnv) {
super.plugInto(aLucyEnv);
//Add an action to create a new waypoints layer to each format bar
WayPointModelAddOn wayPointModelAddOn = aLucyEnv.retrieveAddOnByClass(WayPointModelAddOn.class);
ILcyGenericMapManagerListener<ILcdView, ILcdLayer> listener =
ILcyGenericMapManagerListener.onMapAdded(ILspView.class,
aMapComponent -> {
ILcyLucyEnv lucyEnv = wayPointModelAddOn.getLucyEnv();
TLcyLspCreateLayerAction action = new TLcyLspCreateLayerAction(wayPointModelAddOn.getFormat(), (ILcyGenericMapComponent<ILspView, ILspLayer>) aMapComponent);
//Allow the user to undo the creation of the new layer
action.addUndoableListener(lucyEnv.getUndoManager());
//Configure the ID of the action, and insert it in all configured action bars
action.putValue(TLcyActionBarUtil.ID_KEY, getShortPrefix() + "newLayerAction");
TLcyActionBarUtil.insertInConfiguredActionBars(
action,
aMapComponent,
lucyEnv.getUserInterfaceManager().getActionBarManager(),
getPreferences());
});
aLucyEnv.getCombinedMapManager().addMapManagerListener(listener, true);
}
See How to insert an action on each map for more information about adding an action to each map. |
The important things to note in this snippet are:
-
It uses the
TLcyCombinedMapManager.addMapManagerListener(xxx, true)
method call. By setting the boolean totrue
, you ensure that the listener is not just called when new maps are created, but also once for each map that already exists. -
In the listener, we create the action and insert it in the action bars.
-
The snippet does not show in which action bars the action is inserted. This information is configured in the configuration file of the add-on.
In the WayPointAddOn.cfg
configuration file, we add the necessary settings to insert the "create a new waypoint layer" action in the format bar.
Remember we gave the format bar the ID "wayPointsToolBar"
:
# Insert the button to create a new layer in the format bar
WayPointAddOn.newLayerAction.wayPointsToolBar.item=New way point layer
WayPointAddOn.newLayerAction.wayPointsToolBar.groups=LayerGroup
WayPointAddOn.newLayerAction.wayPointsToolBar.shortDescription=Create a new way point layer
WayPointAddOn.newLayerAction.wayPointsToolBar.smallIcon=add_empty_layer
Adding copy-paste functionality for the way points
During editing, it is convenient to be able to copy-paste way points, or even copy points from another format into the way points layer.
Lucy already contains cut, copy, and paste actions in the Edit menu.
To integrate with these actions, the format must register an ALcyLayerSelectionTransferHandler
.
The functionality of a ALcyLayerSelectionTransferHandler
is comparable to that of a javax.swing.TransferHandler
.
When a cut or copy operation is initiated, it is up to the transfer handler to create a java.awt.datatransfer.Transferable
.
The Transferable
exposes what kind of data it contains by supporting certain java.awt.datatransfer.DataFlavor
instances.
When the paste action is executed, the Transferable
is handed to a transfer handler.
The transfer handler must inspect which DataFlavor
instances the Transferable
contains,
and decide whether it accepts the Transferable
based on that information.
When it accepts the Transferable
, it is up to the handler to retrieve the data from the Transferable
and perform the actual paste of the data.
While a javax.swing.TransferHandler
handles transfers for a Swing component, the ALcyLayerSelectionTransferHandler
handles transfers for the selected elements of a layer.
Similar to all other services that work on the Lightspeed layer, it is created in the ALcyLspFormat
:
@Override
protected ALcyLayerSelectionTransferHandler[] createLayerSelectionTransferHandlers() {
return new ALcyLayerSelectionTransferHandler[]{
new WayPointLayerSelectionTransferHandler()
};
}
For the WayPointLayerSelectionTransferHandler
implementation, we start from the base class ALcyDefaultLayerSelectionTransferHandler
.
The ALcyDefaultLayerSelectionTransferHandler
class allows copy-pasting way points between way point models, and importing shapes from other formats into a way points
model.
Our extension only needs to provide the logic to:
-
Create a copy of a waypoint for copying data between 2 waypoint layers.
-
Convert a shape from another format into a waypoint for copying a point from a non-waypoint layer into our waypoint layer.
-
Create a copy of a waypoint shape for copying a waypoint to a layer of another format. In that case, only the shape is copied.
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.datatransfer.ALcyDefaultLayerSelectionTransferHandler;
import com.luciad.model.ILcdModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.ALcdShape;
import com.luciad.shape.ILcdPoint;
import com.luciad.shape.ILcdShape;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.transformation.TLcdGeoReference2GeoReference;
import com.luciad.util.TLcdOutOfBoundsException;
final class WayPointLayerSelectionTransferHandler extends ALcyDefaultLayerSelectionTransferHandler<ILcdDataObject> {
private final TLcdGeoReference2GeoReference fTransformer = new TLcdGeoReference2GeoReference();
WayPointLayerSelectionTransferHandler() {
super(null);
}
@Override
protected ILcdDataObject createDomainObjectCopy(ILcdDataObject aDomainObject, ILcdModel aSourceModel, ILcdModel aTargetModel) {
if (aSourceModel.getModelReference().equals(aTargetModel.getModelReference())) {
//no transformation needed as we only copy between models of the same reference
ILcdDataObject copy = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
copy.setValue("identifier", aDomainObject.getValue("identifier"));
copy.setValue("location", new TLcdLonLatHeightPoint((TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(aDomainObject)));
return copy;
}
return null;
}
@Override
protected ILcdDataObject createDomainObjectForShape(ILcdShape aShape, ILcdModel aSourceModel, ILcdModel aTargetModel) {
if (aShape instanceof ILcdPoint) {
fTransformer.setSourceReference(aSourceModel.getModelReference());
fTransformer.setDestinationReference(aTargetModel.getModelReference());
ILcdDataObject wayPoint = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", "WayPoint (no name)");
TLcdLonLatHeightPoint location = new TLcdLonLatHeightPoint(0, 0, 0);
wayPoint.setValue("location", location);
try {
fTransformer.sourcePoint2destinationSFCT((ILcdPoint) aShape, location);
return wayPoint;
} catch (TLcdOutOfBoundsException aE) {
getLogListener().fail("Could not copy shape from source to destination reference");
return null;
}
}
getLogListener().fail("Only point shapes can be pasted into a way points layer.");
return null;
}
@Override
protected ILcdShape createShapeCopy(ILcdShape aShape, ILcdModel aSourceModel) {
ILcdPoint originalLocation = (ILcdPoint) aShape;
TLcdLonLatHeightPoint copy = new TLcdLonLatHeightPoint();
copy.move3D(originalLocation);
return copy;
}
}
Adding workspace support
When we added the static, non-editable data in the Adding support for custom static data to a Lightspeed view tutorial, we got workspace support for free. This time, we need to take a few more steps before the way point data can be stored inside a workspace.
-
Keep the source name in-sync: each model has a source name in its model descriptor — see
ILcdModelDescriptor.getSourceName
. The workspace support for the models, as provided by theALcyFileFormat
, relies on the assumption that the source name is set in the model descriptor, and that it is correct. This assumption is documented in the class Javadoc of theALcyGeneralFormat
class, from which theALcyFileFormat
extends.The workspace does not contain the actual data, but simply a reference to the source file by storing only the source name. When the workspace is loaded, the source file is decoded to restore the model. To keep that mechanism intact, our
ILcdModelEncoder
must ensure that the source name matches the last location where the data was saved. This is already the case for ourWayPointsModelEncoder
class. -
Save all changes to file before saving the workspace: the above mechanism of storing the source name still fails when the user made changes but did not yet save them. Another scenario where this approach fails is when the user created a new layer, but did not save the data to disk yet. At that point, the model will not even have a source name.
We can remedy cases like that by decorating our
ALcyFormat
with aTLcyMutableFileFormatWrapper
. As stated in the Javadoc of that class, the decorator provides workspace support for models that can be modified by the user.@Override protected ALcyFormat createFormatWrapper(ALcyFormat aBaseFormat) { //Ensure that the user is asked to save the modified waypoint models //before saving a workspace aBaseFormat = new TLcyMutableFileFormatWrapper(aBaseFormat); return new TLcySafeGuardFormatWrapper(aBaseFormat); }
Full code
The WayPointModelAddOn
code
package com.luciad.lucy.addons.tutorial.editabledata.model;
import com.luciad.lucy.addons.ALcyFormatAddOn;
import com.luciad.lucy.format.ALcyFormat;
import com.luciad.lucy.format.TLcyMutableFileFormatWrapper;
import com.luciad.lucy.format.TLcySafeGuardFormatWrapper;
import com.luciad.lucy.util.ALcyTool;
public class WayPointModelAddOn extends ALcyFormatAddOn {
public WayPointModelAddOn() {
super(ALcyTool.getLongPrefix(WayPointModelAddOn.class),
ALcyTool.getShortPrefix(WayPointModelAddOn.class));
}
@Override
protected ALcyFormat createBaseFormat() {
return new WayPointModelFormat(getLucyEnv(),
getLongPrefix(),
getShortPrefix(),
getPreferences());
}
@Override
protected ALcyFormat createFormatWrapper(ALcyFormat aBaseFormat) {
//Ensure that the user is asked to save the modified waypoint models
//before saving a workspace
aBaseFormat = new TLcyMutableFileFormatWrapper(aBaseFormat);
return new TLcySafeGuardFormatWrapper(aBaseFormat);
}
}
The WayPointModelFormat
code
package com.luciad.lucy.addons.tutorial.editabledata.model;
import java.util.Collections;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.format.ALcyFileFormat;
import com.luciad.lucy.gui.customizer.ILcyCustomizerPanel;
import com.luciad.lucy.gui.customizer.ILcyCustomizerPanelFactory;
import com.luciad.lucy.map.ILcyGXYLayerTypeProvider;
import com.luciad.lucy.model.ALcyDefaultModelDescriptorFactory;
import com.luciad.lucy.model.ILcyModelContentType;
import com.luciad.lucy.model.ILcyModelContentTypeProvider;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.ILcdModelDescriptor;
import com.luciad.model.ILcdModelEncoder;
import com.luciad.model.ILcdModelFactory;
import com.luciad.model.ILcdModelReference;
import com.luciad.model.TLcd2DBoundsIndexedModel;
import com.luciad.model.TLcdDataModelDescriptor;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.model.tutorial.customvector.WayPointsModelEncoder;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.view.gxy.ILcdGXYLayerFactory;
class WayPointModelFormat extends ALcyFileFormat {
WayPointModelFormat(ILcyLucyEnv aLucyEnv, String aLongPrefix, String aShortPrefix, ALcyProperties aProperties) {
super(aLucyEnv, aLongPrefix, aShortPrefix, aProperties);
}
@Override
protected ILcdModelDecoder[] createModelDecoders() {
return new ILcdModelDecoder[]{new WayPointsModelDecoder()};
}
@Override
protected ILcyModelContentTypeProvider createModelContentTypeProvider() {
//All our models only contain point data, so we can return a fixed type
//No need to check the contents of the model
return aModel -> ILcyModelContentType.POINT;
}
@Override
protected ILcyGXYLayerTypeProvider createGXYLayerTypeProvider() {
return null;
}
@Override
protected ILcdGXYLayerFactory createGXYLayerFactory() {
return null;
}
@Override
public boolean isModelOfFormat(ILcdModel aModel) {
//All the waypoint models created by our model decoder have CWP as type name
//We assume here that this typename is unique over all supported formats
return "CWP".equals(aModel.getModelDescriptor().getTypeName());
}
@Override
protected ILcdModelEncoder[] createModelEncoders() {
return new ILcdModelEncoder[]{new WayPointsModelEncoder()};
}
@Override
protected ILcyCustomizerPanelFactory[] createDomainObjectCustomizerPanelFactories() {
return new ILcyCustomizerPanelFactory[]{
new ILcyCustomizerPanelFactory() {
@Override
public boolean canCreateCustomizerPanel(Object aObject) {
//The TLcyLspSafeGuardFormatWrapper takes care of this
return true;
}
@Override
public ILcyCustomizerPanel createCustomizerPanel(Object aObject) {
return new WayPointCustomizerPanel(getLucyEnv());
}
}
};
}
@Override
protected ALcyDefaultModelDescriptorFactory[] createDefaultModelDescriptorFactories() {
return new ALcyDefaultModelDescriptorFactory[]{
new ALcyDefaultModelDescriptorFactory() {
@Override
public ILcdModelDescriptor createDefaultModelDescriptor() {
//Return the same model descriptor as the model decoder is creating
return new TLcdDataModelDescriptor(null,
"CWP",
"Way Points",
WayPointsModelDecoder.DATA_MODEL,
Collections.singleton(WayPointsModelDecoder.WAYPOINT_TYPE),
WayPointsModelDecoder.DATA_MODEL.getTypes());
}
}
};
}
@Override
protected ILcdModelFactory createModelFactory() {
return new ILcdModelFactory() {
@Override
public ILcdModel createModel(ILcdModelDescriptor aModelDescriptor, ILcdModelReference aModelReference) throws IllegalArgumentException {
//First check whether the model descriptor is one we recognize
//The TLcySafeGuardFormatWrapper does not help us, because that wrapper can only check whether
//a model is valid for a format.
//It has no knowledge of model descriptors
if (!"CWP".equals(aModelDescriptor.getTypeName())) {
throw new IllegalArgumentException("Cannot create model for model descriptor [" + aModelDescriptor + "]");
}
//Create a model the same was as done in the WayPointsModelDecoder
return new TLcd2DBoundsIndexedModel(new TLcdGeodeticReference(), aModelDescriptor);
}
};
}
}
The WayPointCustomizerPanel
code
package com.luciad.lucy.addons.tutorial.editabledata.model;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.Format;
import javax.swing.JLabel;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.gui.TLcyTwoColumnLayoutBuilder;
import com.luciad.lucy.gui.customizer.ALcyDomainObjectCustomizerPanel;
import com.luciad.lucy.util.context.TLcyDomainObjectContext;
import com.luciad.model.ILcdModel;
import com.luciad.shape.ALcdShape;
import com.luciad.shape.ILcdPoint;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.util.ALcdWeakPropertyChangeListener;
import com.luciad.util.ILcdFilter;
import com.luciad.util.concurrent.TLcdLockUtil;
import samples.lucy.text.StringFormat;
import samples.lucy.util.ValidatingTextField;
public class WayPointCustomizerPanel extends ALcyDomainObjectCustomizerPanel {
private static final ILcdFilter WAYPOINT_DOMAIN_OBJECT_FILTER = new ILcdFilter() {
@Override
public boolean accept(Object aObject) {
if (aObject instanceof TLcyDomainObjectContext) {
TLcyDomainObjectContext context = (TLcyDomainObjectContext) aObject;
//Check if the model of the domain object is a waypoint model
return "CWP".equals(context.getModel().getModelDescriptor().getTypeName());
}
return false;
}
};
private ValidatingTextField fIdentifierField;
private ValidatingTextField fLocationField;
private ValidatingTextField fHeightField;
/**
* This boolean field is used to avoid loops:
* <ul>
* <li>When the user updates the waypoint on the map,
* the customizer panel will update the contents of the text fields.</li>
* <li>The listener attached to the text fields would detect this change,
* and indicate that the user made a change in the text fields (which isn't the case).</li>
* </ul>
* This boolean is used to indicate when the customizer panel itself is updating the text fields,
* allowing the text field listener to distinguish between user-made changes
* and changes made by the panel itself.
*/
boolean fUpdatingUI = false;
public WayPointCustomizerPanel(ILcyLucyEnv aLucyEnv) {
super(WAYPOINT_DOMAIN_OBJECT_FILTER, "Way points");
//Create the text fields and add them to this panel
initUI(aLucyEnv);
//Create a listener to detect changes made by the user in the text fields
PropertyChangeListener textFieldListener = evt -> {
if (!fUpdatingUI) {
setChangesPending(true);
}
};
//Install the listener on the text fields
fIdentifierField.addPropertyChangeListener("value", textFieldListener);
fLocationField.addPropertyChangeListener("value", textFieldListener);
fHeightField.addPropertyChangeListener("value", textFieldListener);
aLucyEnv.addPropertyChangeListener(new PointFormatListener(this));
aLucyEnv.addPropertyChangeListener(new AltitudeFormatListener(this));
}
private void initUI(ILcyLucyEnv aLucyEnv) {
fIdentifierField = new ValidatingTextField(new StringFormat(), aLucyEnv);
fLocationField = new ValidatingTextField(aLucyEnv.getDefaultLonLatPointFormat(), aLucyEnv);
fHeightField = new ValidatingTextField(aLucyEnv.getDefaultAltitudeFormat(), aLucyEnv);
TLcyTwoColumnLayoutBuilder.newBuilder()
.addTitledSeparator("Way point")
.row()
.columnOne(new JLabel("Identifier"), fIdentifierField)
.build()
.row()
.columnOne(new JLabel("Location"), fLocationField)
.build()
.row()
.columnOne(new JLabel("Height"), fHeightField)
.build()
.populate(this);
}
@Override
protected void updateCustomizerPanelFromObject(boolean aPanelEditable) {
fIdentifierField.setEditable(aPanelEditable);
fLocationField.setEditable(aPanelEditable);
fHeightField.setEditable(aPanelEditable);
boolean old = fUpdatingUI;
try {
//Switch the flag to indicate we are currently updating the panel
//and the changes to the text fields are not made by the user
fUpdatingUI = true;
ILcdDataObject waypoint = (ILcdDataObject) getDomainObject();
if (waypoint != null) {
//Take a read lock so that we can safely read the values from the domain object
try (TLcdLockUtil.Lock autoUnlock = TLcdLockUtil.readLock(getModel())) {
fIdentifierField.setValue(waypoint.getValue("identifier"));
TLcdLonLatHeightPoint location = (TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(waypoint);
fLocationField.setValue(location);
fHeightField.setValue(location.getZ());
}
} else {
fIdentifierField.setValue("");
fLocationField.setValue(null);
fHeightField.setValue(0);
}
} finally {
//Restore the state of the flag
fUpdatingUI = old;
}
}
@Override
protected boolean applyChangesImpl() {
ILcdDataObject waypoint = (ILcdDataObject) getDomainObject();
if (waypoint != null) {
ILcdModel model = getModel();
//Changing a model element requires a write lock
try (TLcdLockUtil.Lock autoUnlock = TLcdLockUtil.writeLock(model)) {
waypoint.setValue("identifier", fIdentifierField.getValue());
TLcdLonLatHeightPoint location = (TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(waypoint);
ILcdPoint updatedLocation = (ILcdPoint) fLocationField.getValue();
double height = (double) fHeightField.getValue();
location.move3D(updatedLocation.getX(), updatedLocation.getY(), height);
model.elementChanged(waypoint, ILcdModel.FIRE_LATER);
} finally {
model.fireCollectedModelChanges();
}
}
return true;
}
private void updatePointFormat(Format aPointFormat) {
boolean old = fUpdatingUI;
try {
fUpdatingUI = true;
fLocationField.setFormat(aPointFormat, fLocationField.getValue());
} finally {
fUpdatingUI = old;
}
}
private void updateAltitudeFormat(Format aAltitudeFormat) {
boolean old = fUpdatingUI;
try {
fUpdatingUI = true;
fHeightField.setFormat(aAltitudeFormat, fHeightField.getValue());
} finally {
fUpdatingUI = old;
}
}
/**
* The location field should be formatted using the point format exposed on the Lucy back-end.
* When this format changes, the UI must be updated.
*/
private static class PointFormatListener extends
ALcdWeakPropertyChangeListener<WayPointCustomizerPanel> {
private PointFormatListener(WayPointCustomizerPanel aObjectToModify) {
super(aObjectToModify);
}
@Override
protected void propertyChangeImpl(WayPointCustomizerPanel aWayPointCustomizerPanel, PropertyChangeEvent aPropertyChangeEvent) {
String propertyName = aPropertyChangeEvent.getPropertyName();
if ("defaultLonLatPointFormat".equals(propertyName)) {
aWayPointCustomizerPanel.updatePointFormat((Format) aPropertyChangeEvent.getNewValue());
}
}
}
/**
* The altitude field should be formatted using the altitude format exposed on the Lucy back-end.
* When this format changes, the UI must be updated
*/
private static class AltitudeFormatListener extends ALcdWeakPropertyChangeListener<WayPointCustomizerPanel> {
private AltitudeFormatListener(WayPointCustomizerPanel aObjectToModify) {
super(aObjectToModify);
}
@Override
protected void propertyChangeImpl(WayPointCustomizerPanel aWayPointCustomizerPanel, PropertyChangeEvent aPropertyChangeEvent) {
String propertyName = aPropertyChangeEvent.getPropertyName();
if ("defaultAltitudeFormat".equals(propertyName) || "defaultUserAltitudeUnit".equals(propertyName)) {
aWayPointCustomizerPanel.updateAltitudeFormat(((ILcyLucyEnv) aPropertyChangeEvent.getSource()).getDefaultAltitudeFormat());
}
}
}
}
The configuration file for the WayPointModelAddOn
WayPointModelAddOn.fileTypeDescriptor.displayName=Waypoint files
WayPointModelAddOn.fileTypeDescriptor.defaultExtension=cwp
WayPointModelAddOn.fileTypeDescriptor.filters=*.cwp
WayPointModelAddOn.fileTypeDescriptor.groupIDs=All Vector Files
The WayPointAddOn
code
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.addons.lightspeed.ALcyLspFormatAddOn;
import com.luciad.lucy.addons.tutorial.editabledata.model.WayPointModelAddOn;
import com.luciad.lucy.format.lightspeed.ALcyLspFormat;
import com.luciad.lucy.format.lightspeed.TLcyLspSafeGuardFormatWrapper;
import com.luciad.lucy.gui.TLcyActionBarUtil;
import com.luciad.lucy.map.ILcyGenericMapComponent;
import com.luciad.lucy.map.ILcyGenericMapManagerListener;
import com.luciad.lucy.map.action.lightspeed.TLcyLspCreateLayerAction;
import com.luciad.lucy.util.ALcyTool;
import com.luciad.model.ILcdModel;
import com.luciad.util.ILcdFilter;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;
import com.luciad.view.lightspeed.ILspView;
import com.luciad.view.lightspeed.layer.ILspLayer;
public class WayPointAddOn extends ALcyLspFormatAddOn {
public WayPointAddOn() {
super(ALcyTool.getLongPrefix(WayPointAddOn.class),
ALcyTool.getShortPrefix(WayPointAddOn.class));
}
@Override
protected ALcyLspFormat createBaseFormat() {
ILcdFilter<ILcdModel> modelFilter =
(ILcdFilter<ILcdModel>) aModel -> "CWP".equals(aModel.getModelDescriptor().getTypeName());
return new WayPointFormat(getLucyEnv(),
getLongPrefix(),
getShortPrefix(),
getPreferences(),
modelFilter);
}
@Override
protected ALcyLspFormat createFormatWrapper(ALcyLspFormat aBaseFormat) {
return new TLcyLspSafeGuardFormatWrapper(aBaseFormat);
}
@Override
public void plugInto(ILcyLucyEnv aLucyEnv) {
super.plugInto(aLucyEnv);
//Add an action to create a new waypoints layer to each format bar
WayPointModelAddOn wayPointModelAddOn = aLucyEnv.retrieveAddOnByClass(WayPointModelAddOn.class);
ILcyGenericMapManagerListener<ILcdView, ILcdLayer> listener =
ILcyGenericMapManagerListener.onMapAdded(ILspView.class,
aMapComponent -> {
ILcyLucyEnv lucyEnv = wayPointModelAddOn.getLucyEnv();
TLcyLspCreateLayerAction action = new TLcyLspCreateLayerAction(wayPointModelAddOn.getFormat(), (ILcyGenericMapComponent<ILspView, ILspLayer>) aMapComponent);
//Allow the user to undo the creation of the new layer
action.addUndoableListener(lucyEnv.getUndoManager());
//Configure the ID of the action, and insert it in all configured action bars
action.putValue(TLcyActionBarUtil.ID_KEY, getShortPrefix() + "newLayerAction");
TLcyActionBarUtil.insertInConfiguredActionBars(
action,
aMapComponent,
lucyEnv.getUserInterfaceManager().getActionBarManager(),
getPreferences());
});
aLucyEnv.getCombinedMapManager().addMapManagerListener(listener, true);
}
}
The WayPointFormat
code
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.datatransfer.ALcyLayerSelectionTransferHandler;
import com.luciad.lucy.format.lightspeed.ALcyLspStyleFormat;
import com.luciad.lucy.gui.formatbar.ALcyFormatBarFactory;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.model.ILcdModel;
import com.luciad.util.ILcdFilter;
import com.luciad.view.lightspeed.layer.ALspSingleLayerFactory;
import com.luciad.view.lightspeed.layer.ILspLayer;
import com.luciad.view.lightspeed.layer.ILspLayerFactory;
import com.luciad.view.lightspeed.layer.TLspPaintState;
import com.luciad.view.lightspeed.layer.shape.TLspShapeLayerBuilder;
import com.luciad.view.lightspeed.painter.label.style.TLspDataObjectLabelTextProviderStyle;
import com.luciad.view.lightspeed.style.TLspTextStyle;
import com.luciad.view.lightspeed.style.styler.TLspCustomizableStyler;
public class WayPointFormat extends ALcyLspStyleFormat {
public WayPointFormat(ILcyLucyEnv aLucyEnv, String aLongPrefix, String aShortPrefix, ALcyProperties aPreferences, ILcdFilter<ILcdModel> aModelFilter) {
super(aLucyEnv, aLongPrefix, aShortPrefix, aPreferences, aModelFilter);
}
@Override
protected ILspLayerFactory createLayerFactoryImpl() {
return new ALspSingleLayerFactory() {
@Override
public ILspLayer createLayer(ILcdModel aModel) {
//Ensure that the layer is editable
//Also make sure the layer has labels
return TLspShapeLayerBuilder.newBuilder()
.model(aModel)
.bodyEditable(true)
.labelStyler(TLspPaintState.REGULAR, createLabelStyler())
.build();
}
private TLspCustomizableStyler createLabelStyler() {
TLspDataObjectLabelTextProviderStyle labelContentsStyle =
TLspDataObjectLabelTextProviderStyle.newBuilder()
.expressions("identifier")
.build();
return new TLspCustomizableStyler(TLspTextStyle.newBuilder().build(),
labelContentsStyle);
}
@Override
public boolean canCreateLayers(ILcdModel aModel) {
//The TLcyLspSafeGuardFormatWrapper only passes waypoint models
//to this factory, so no need to check anything here
return true;
}
};
}
@Override
protected ALcyFormatBarFactory createFormatBarFactory() {
return new WayPointFormatBarFactory(getLucyEnv(), getProperties(), getShortPrefix());
}
@Override
protected ALcyLayerSelectionTransferHandler[] createLayerSelectionTransferHandlers() {
return new ALcyLayerSelectionTransferHandler[]{
new WayPointLayerSelectionTransferHandler()
};
}
}
The WayPointLayerSelectionTransferHandler
code
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.datatransfer.ALcyDefaultLayerSelectionTransferHandler;
import com.luciad.model.ILcdModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.ALcdShape;
import com.luciad.shape.ILcdPoint;
import com.luciad.shape.ILcdShape;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.transformation.TLcdGeoReference2GeoReference;
import com.luciad.util.TLcdOutOfBoundsException;
final class WayPointLayerSelectionTransferHandler extends ALcyDefaultLayerSelectionTransferHandler<ILcdDataObject> {
private final TLcdGeoReference2GeoReference fTransformer = new TLcdGeoReference2GeoReference();
WayPointLayerSelectionTransferHandler() {
super(null);
}
@Override
protected ILcdDataObject createDomainObjectCopy(ILcdDataObject aDomainObject, ILcdModel aSourceModel, ILcdModel aTargetModel) {
if (aSourceModel.getModelReference().equals(aTargetModel.getModelReference())) {
//no transformation needed as we only copy between models of the same reference
ILcdDataObject copy = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
copy.setValue("identifier", aDomainObject.getValue("identifier"));
copy.setValue("location", new TLcdLonLatHeightPoint((TLcdLonLatHeightPoint) ALcdShape.fromDomainObject(aDomainObject)));
return copy;
}
return null;
}
@Override
protected ILcdDataObject createDomainObjectForShape(ILcdShape aShape, ILcdModel aSourceModel, ILcdModel aTargetModel) {
if (aShape instanceof ILcdPoint) {
fTransformer.setSourceReference(aSourceModel.getModelReference());
fTransformer.setDestinationReference(aTargetModel.getModelReference());
ILcdDataObject wayPoint = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", "WayPoint (no name)");
TLcdLonLatHeightPoint location = new TLcdLonLatHeightPoint(0, 0, 0);
wayPoint.setValue("location", location);
try {
fTransformer.sourcePoint2destinationSFCT((ILcdPoint) aShape, location);
return wayPoint;
} catch (TLcdOutOfBoundsException aE) {
getLogListener().fail("Could not copy shape from source to destination reference");
return null;
}
}
getLogListener().fail("Only point shapes can be pasted into a way points layer.");
return null;
}
@Override
protected ILcdShape createShapeCopy(ILcdShape aShape, ILcdModel aSourceModel) {
ILcdPoint originalLocation = (ILcdPoint) aShape;
TLcdLonLatHeightPoint copy = new TLcdLonLatHeightPoint();
copy.move3D(originalLocation);
return copy;
}
}
The WayPointCreateControllerModel
code
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.map.action.lightspeed.TLcyLspCreateLayerAction;
import com.luciad.lucy.map.lightspeed.ILcyLspMapComponent;
import com.luciad.lucy.map.lightspeed.controller.ALcyLspCreateControllerModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.view.lightspeed.ILspView;
import com.luciad.view.lightspeed.layer.ILspLayer;
final class WayPointCreateControllerModel extends ALcyLspCreateControllerModel {
WayPointCreateControllerModel(ILcyLspMapComponent aMapComponent, TLcyLspCreateLayerAction aCreateLayerAction) {
super(aMapComponent, aCreateLayerAction);
}
@Override
public Object create(ILspView aView, ILspLayer aLayer) {
//Create the waypoint domain object, and initiate it with dummy values
ILcdDataObject wayPoint = WayPointsModelDecoder.WAYPOINT_TYPE.newInstance();
wayPoint.setValue("identifier", "WayPoint (no name)");
wayPoint.setValue("location", new TLcdLonLatHeightPoint(0, 0, 0));
return wayPoint;
}
}
The WayPointFormatBar
code
package com.luciad.lucy.addons.tutorial.editabledata;
import java.awt.Component;
import javax.swing.JComponent;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.gui.TLcyActionBarManager;
import com.luciad.lucy.gui.TLcyActionBarUtil;
import com.luciad.lucy.gui.TLcyToolBar;
import com.luciad.lucy.gui.formatbar.ALcyFormatBar;
import com.luciad.lucy.map.action.lightspeed.TLcyLspCreateLayerAction;
import com.luciad.lucy.map.lightspeed.TLcyLspMapComponent;
import com.luciad.lucy.model.ALcyDefaultModelDescriptorFactory;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.lightspeed.layer.ILspInteractivePaintableLayer;
import samples.lucy.util.LayerUtil;
class WayPointFormatBar extends ALcyFormatBar {
/**
* The actual Swing component representing the format bar
*/
private final TLcyToolBar fToolBar = new TLcyToolBar();
private final WayPointCreateControllerModel fControllerModel;
WayPointFormatBar(TLcyLspMapComponent aLspMapComponent,
ALcyProperties aProperties,
String aShortPrefix,
ALcyDefaultModelDescriptorFactory aDefaultModelDescriptorFactory,
ILcyLucyEnv aLucyEnv) {
putValue(ALcyFormatBar.NAME, "Way Points");
putValue(ALcyFormatBar.SHORT_DESCRIPTION, "Create way points");
//Allow TLcyActionBarUtil (and other add-ons) to contribute to our tool bar
TLcyActionBarManager actionBarManager = aLucyEnv.getUserInterfaceManager().getActionBarManager();
TLcyActionBarUtil.setupAsConfiguredActionBar(fToolBar,
WayPointFormatBarFactory.TOOLBAR_ID,
aLspMapComponent,
aProperties,
aShortPrefix,
(JComponent) aLspMapComponent.getComponent(),
actionBarManager);
//Create and insert a button for the controller
TLcyLspCreateLayerAction createLayerAction = new TLcyLspCreateLayerAction(aLucyEnv, aLspMapComponent);
createLayerAction.setDefaultModelDescriptorFactory(aDefaultModelDescriptorFactory);
fControllerModel = new WayPointCreateControllerModel(aLspMapComponent, createLayerAction);
LayerUtil.insertCreateShapeActiveSettable(aProperties, aShortPrefix, aLucyEnv, aLspMapComponent, fControllerModel);
}
@Override
protected void updateForLayer(ILcdLayer aPreviousLayer, ILcdLayer aLayer) {
fControllerModel.setCurrentLayer((ILspInteractivePaintableLayer) aLayer);
}
@Override
public boolean canSetLayer(ILcdLayer aLayer) {
// TLcyLspSafeGuardFormatWrapper already checks the layer
return true;
}
@Override
public Component getComponent() {
return fToolBar.getComponent();
}
}
The WayPointFormatBarFactory
code
package com.luciad.lucy.addons.tutorial.editabledata;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.addons.tutorial.editabledata.model.WayPointModelAddOn;
import com.luciad.lucy.gui.formatbar.ALcyFormatBar;
import com.luciad.lucy.gui.formatbar.ALcyFormatBarFactory;
import com.luciad.lucy.map.ILcyGenericMapComponent;
import com.luciad.lucy.map.lightspeed.TLcyLspMapComponent;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;
class WayPointFormatBarFactory extends ALcyFormatBarFactory {
static String TOOLBAR_ID = "wayPointsToolBar";
private final ILcyLucyEnv fLucyEnv;
private final ALcyProperties fProperties;
private final String fShortPrefix;
WayPointFormatBarFactory(ILcyLucyEnv aLucyEnv, ALcyProperties aProperties, String aShortPrefix) {
fLucyEnv = aLucyEnv;
fProperties = aProperties;
fShortPrefix = aShortPrefix;
}
@Override
public boolean canCreateFormatBar(ILcdView aView, ILcdLayer aLayer) {
// TLcyLspSafeGuardFormatWrapper already checks the layer
return findLspMapComponent(aView) != null;
}
@Override
public ALcyFormatBar createFormatBar(ILcdView aView, ILcdLayer aLayer) {
WayPointModelAddOn wayPointsModelAddOn = fLucyEnv.retrieveAddOnByClass(WayPointModelAddOn.class);
return new WayPointFormatBar(findLspMapComponent(aView),
fProperties,
fShortPrefix,
wayPointsModelAddOn.getFormat().getDefaultModelDescriptorFactories()[0],
fLucyEnv);
}
private TLcyLspMapComponent findLspMapComponent(ILcdView aView) {
ILcyGenericMapComponent mapComponent = fLucyEnv.getCombinedMapManager().findMapComponent(aView);
return mapComponent instanceof TLcyLspMapComponent ? (TLcyLspMapComponent) mapComponent : null;
}
}
The configuration file for the WayPointAddOn
WayPointAddOn.style.fileTypeDescriptor.displayName=Style Files
WayPointAddOn.style.fileTypeDescriptor.defaultExtension=sty
WayPointAddOn.style.fileTypeDescriptor.filters=*.sty
WayPointAddOn.style.fileTypeDescriptor.groupIDs=All Style Files
WayPointAddOn.defaultStyleFile=docs/articles/tutorial/lucy/customdata/editable/defaultWaypointStyle.sty
# This property defines the order of the groups in which the toolbar items are contained. For more
# information, please refer to the lucy.cfg configuration file, more specifically to the property
# TLcyMain.menuBar.groupPriorities and its comments.
WayPointAddOn.wayPointsToolBar.groupPriorities=\
LayerGroup,\
CreateGroup,\
DefaultGroup
# Insert the button to activate the create controller in the format bar
WayPointAddOn.newActiveSettable.wayPointsToolBar.item=Way Point
WayPointAddOn.newActiveSettable.wayPointsToolBar.groups=CreateGroup
WayPointAddOn.newActiveSettable.wayPointsToolBar.shortDescription=Create a new way point
WayPointAddOn.newActiveSettable.wayPointsToolBar.smallIcon=draw_point
# Insert the button to create a new layer in the format bar
WayPointAddOn.newLayerAction.wayPointsToolBar.item=New way point layer
WayPointAddOn.newLayerAction.wayPointsToolBar.groups=LayerGroup
WayPointAddOn.newLayerAction.wayPointsToolBar.shortDescription=Create a new way point layer
WayPointAddOn.newLayerAction.wayPointsToolBar.smallIcon=add_empty_layer
The custom addons.xml
file
<?xml version="1.0" encoding="UTF-8"?>
<addonConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="config/lucy/addons.xsd">
<addons>
<!-- Include all the original add-ons -->
<include>lucy/addons.xml</include>
<!-- Add our own add-ons for waypoint data -->
<addon>
<priority>data_producer</priority>
<name>Waypoint model format</name>
<class>com.luciad.lucy.addons.tutorial.editabledata.model.WayPointModelAddOn</class>
<configFile>docs/articles/tutorial/lucy/customdata/editable/WayPointModelAddOn.cfg</configFile>
</addon>
<addon>
<priority>data_producer</priority>
<name>Waypoint format</name>
<class>com.luciad.lucy.addons.tutorial.editabledata.WayPointAddOn</class>
<configFile>docs/articles/tutorial/lucy/customdata/editable/WayPointAddOn.cfg</configFile>
</addon>
</addons>
</addonConfiguration>