Prerequisites

This tutorial assumes that you are familiar with:

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.

editable waypoints

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:

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:

  1. Lucy asks the user which data it needs to be save, and to which destination.

  2. Lucy loops over all known ILcdModelEncoder instances.

  3. If a model encoder indicates it can save the data, Lucy delegates the actual saving to that model encoder.

  4. 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:

  1. 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.

  2. Finds a factory to create the UI: Lucy uses the TLcyDomainObjectContext to ask each of the registered ILcyCustomizerPanelFactory instances whether they can create an ILcyCustomizerPanel UI panel for that TLcyDomainObjectContext.

  3. 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:

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 ALcyFormat also has a method to create a format bar factory, ALcyFormat.createFormatBarFactory. Those format bars work on a GXY view, but not on a Lightspeed view.

Therefore, we create the format bar in the ALcyLspFormat.

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 to true, 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 the ALcyFileFormat, 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 the ALcyGeneralFormat class, from which the ALcyFileFormat 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 our WayPointsModelEncoder 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 a TLcyMutableFileFormatWrapper. 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>