Goal

This tutorial shows you how you add support for business data to a Lucy application using a GXY view.

The custom data format used in this tutorial is the same waypoints format as introduced in the Support a custom vector format tutorial for LuciadLightspeed.

Adding support for a custom data format to Lucy requires a number of Lucy-specific steps that are not required in a non-Lucy application. In return, however, you get a lot of functionality for free. For example, if you follow these steps, you can:

  • Drag-and-drop the data on the map

  • Use the File | Open menu item to load your data

  • Open the table view for your data

  • Customize the styling through the layer properties panel

Once we can visualize the data, this tutorial will also cover the necessary steps to allow the user to edit the data:

  • 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. This allows users 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.

Adding support for decoding and visualizing the data

The recommended way to add support for decoding data is to:

  • Create an ALcyFormat class: the ALcyFormat is a class that groups everything Lucy needs to decode and handle models, and optionally to visualize them on a GXY view.

  • Create an ALcyAddOn that registers everything the ALcyFormat creates to the Lucy services: for example the File | Open action will query the services for all available model decoders and layer factories. By registering our instances as service, they will be picked up automatically.

Creating the ALcyFormat

As our data format is file-based, we start from one of the available ALcyFormat extensions for the creation of our ALcyFormat:

package com.luciad.lucy.addons.tutorial.staticgxydata;
final class GXYWayPointsModelFormat extends ALcyFileFormat {

  GXYWayPointsModelFormat(ILcyLucyEnv aLucyEnv,
                          String aLongPrefix,
                          String aShortPrefix,
                          ALcyProperties aProperties) {
    super(aLucyEnv, aLongPrefix, aShortPrefix, aProperties);
  }
}

The ALcyFileFormat allows us to configure certain items in the configuration file instead of in code. We will create the configuration file when we plug in our add-on.

Create the model decoder

As model decoder we re-use the model decoder created in the How to support a custom vector format. This model decoder is returned from the createModelDecoders method in our ALcyFormat:

@Override
protected ILcdModelDecoder[] createModelDecoders() {
  return new ILcdModelDecoder[]{new WayPointsModelDecoder()};
}

Creating the model content type provider

Lucy tries to determine the best initial position for a layer when adding new data to a map. This requires knowledge about what kind of data the layer and model represent.

One of the interfaces used in this process is the ILcyModelContentTypeProvider interface, which indicates what kind of data a model contains. See How to influence the initial layer position in the view for more information about this.

The ALcyFormat has a method to create this model content type provider:

@Override
protected ILcyModelContentTypeProvider createModelContentTypeProvider() {
  return aModel -> ILcyModelContentType.POINT;
}

As documented in the ILcyModelContentTypeProvider interface, the provider should return ILcyModelContentType.UNKNOWN for models it does not recognize.

Our implementation currently does not do this. We will take care of that when we introduce the safe guard format wrapper.

Creating the layer factory

Our model contains regular vector data, so in the layer factory we can create a standard TLcdGXYLayer which uses standard painters. The layer factory is created in the ALcyFormat#createGXYLayerFactory method:

@Override
protected ILcdGXYLayerFactory createGXYLayerFactory() {
  return new ILcdGXYLayerFactory() {
    @Override
    public ILcdGXYLayer createGXYLayer(ILcdModel aModel) {
      TLcdGXYLayer layer = TLcdGXYLayer.create(aModel);

      //Configure the styling for the bodies
      TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
      painter.setIcon(new TLcdSymbol(TLcdSymbol.FILLED_CIRCLE, 8, Color.GREEN));
      painter.setSelectedIcon(new TLcdSymbol(TLcdSymbol.FILLED_CIRCLE, 8, new Color(67, 157, 227)));
      layer.setGXYPainterProvider(painter);
      layer.setGXYEditorProvider(painter);

      //Activate labeling and configure the styling of those labels
      TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
      labelPainter.setExpressions("identifier");
      labelPainter.setForeground(Color.WHITE);
      labelPainter.setHaloEnabled(true);
      labelPainter.setHaloColor(Color.BLACK);

      layer.setGXYLabelPainterProvider(labelPainter);
      layer.setLabeled(true);

      return layer;
    }
  };
}

Similar to what we did in the model content type provider implementation, our layer factory assumes that the models it receives are waypoint models. This will be taken care of when we introduce the safe guard format wrapper.

The layer factory also does not create an asynchronous layer, which would improve the responsiveness of our view. This will be taken care of when we introduce the asynchronous format wrapper

Creating the layer type provider

Lucy tries to determine the best initial position for a layer when adding new data to a map. This requires knowledge about what kind of data the layer and model represent.

We already created an ILcyModelContentTypeProvider which determines the type of the model contents. We now do the same for the layer by creating an ILcyGXYLayerTypeProvider:

@Override
protected ILcyGXYLayerTypeProvider createGXYLayerTypeProvider() {
  return aGXYLayer -> ILcyGXYLayerTypeProvider.EDITABLE;
}

Adding workspace support for the layers

When the user saves a workspace containing a waypoint layer, the workspace manager will try to find an ALcyWorkspaceObjectCodec for the layers.

While the ALcyFileFormat adds workspace support for the models, the extension is still responsible for providing a workspace codec for the created layers. To provide a layer workspace codec, the createGXYLayerWorkspaceCodecs method returns an ALcyWorkspaceObjectCodec instance:

@Override
protected ALcyWorkspaceObjectCodec[] createGXYLayerWorkspaceCodecs() {
  return new ALcyWorkspaceObjectCodec[]{
      new GXYWayPointsLayerWorkspaceCodec(getLongPrefix(), getShortPrefix(), getGXYLayerFactory())
  };
}

Summary about the role of an ALcyWorkspaceObjectCodec

During workspace encoding, part of the Java object graph is stored to disk. The stored part consists of the most important objects such as the layers, models and views of the application. By storing and restoring the object graph, the workspace mechanism ensures that two objects that each have a reference to the same object when the workspace was saved, still each have a reference to the same object when the workspace is loaded.

For an illustration, consider the example of a layer that has a certain model. At the same time, the Lucy application shows a table view that shows the contents of the same model. As a result, a user modification of a layer element on the map is reflected in the table view. After all, both the layer and the table view use the same model.

That is why the workspace mechanism saves and restores the object graph. If the workspace stores the information that both the layer and the table were referring to the same object, it can restore that relation when the workspace is loaded.

During workspace encoding, the role of an ALcyWorkspaceObjectCodec is to save the state of a single node of the Java object graph. If the node refers to other nodes, like the layer refers to the model in the previous example, the codec must ask the workspace framework to generate a reference to the other node, and store that reference.

During workspace decoding, the workflow is reversed. The ALcyWorkspaceObjectCodec needs to create the object from scratch, based on the information it has stored in the workspace. If the newly created Java object requires references to other Java objects, the codec needs to ask the workspace framework for the object of which it has stored a reference.

The full code of the GXYWayPointsLayerWorkspaceCodec

package com.luciad.lucy.addons.tutorial.staticgxydata;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.lucy.util.properties.TLcyStringProperties;
import com.luciad.lucy.util.properties.codec.TLcyStringPropertiesCodec;
import com.luciad.lucy.workspace.ALcyWorkspaceCodec;
import com.luciad.lucy.workspace.ALcyWorkspaceObjectCodec;
import com.luciad.lucy.workspace.TLcyWorkspaceAbortedException;
import com.luciad.model.ILcdModel;
import com.luciad.view.gxy.ILcdGXYLayerFactory;
import com.luciad.view.gxy.TLcdGXYLayer;

final class GXYWayPointsLayerWorkspaceCodec extends ALcyWorkspaceObjectCodec {
  private static final String MODEL_REFERENCE_KEY = "model";

  private static final String VISIBLE_KEY = "layer.visible";
  private static final String SELECTABLE_KEY = "layer.selectable";
  private static final String LABELED_KEY = "layer.labeled";
  private static final String SELECTION_LABELED_KEY = "layer.selectionLabeled";
  private static final String EDITABLE_KEY = "layer.editable";
  private static final String LABEL_KEY = "layer.label";

  private final String fUID;
  private final String fShortPrefix;
  private final ILcdGXYLayerFactory fLayerFactory;

  GXYWayPointsLayerWorkspaceCodec(String aLongPrefix, String aShortPrefix, ILcdGXYLayerFactory aLayerFactory) {
    fLayerFactory = aLayerFactory;
    fShortPrefix = aShortPrefix;
    fUID = aLongPrefix + "layerCodec";
  }

  @Override
  public String getUID() {
    return fUID;
  }

  @Override
  public boolean canEncodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent) {
    //All checks are done by the safeguard format wrapper
    return true;
  }

  @Override
  public void encodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent, OutputStream aOut) throws IOException, TLcyWorkspaceAbortedException {
    TLcyStringProperties properties = new TLcyStringProperties();

    TLcdGXYLayer layer = (TLcdGXYLayer) aObject;
    ILcdModel model = layer.getModel();

    String referenceToModel = aWSCodec.encodeReference(model);
    properties.putString(fShortPrefix + MODEL_REFERENCE_KEY, referenceToModel);

    //Store the style related settings
    //We only store the settings which can be altered by the user in the UI
    properties.putBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible());
    properties.putBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable());
    properties.putBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled());
    properties.putBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled());
    properties.putBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable());
    properties.putString(fShortPrefix + LABEL_KEY, layer.getLabel());

    new TLcyStringPropertiesCodec().encode(properties, aOut);
  }

  @Override
  public Object createObject(ALcyWorkspaceCodec aWSCodec, Object aParent, InputStream aIn) throws IOException, TLcyWorkspaceAbortedException {
    ALcyProperties props = new TLcyStringPropertiesCodec().decode(aIn);

    String modelReference = props.getString(fShortPrefix + MODEL_REFERENCE_KEY, null);
    if (modelReference != null) {
      ILcdModel model = (ILcdModel) aWSCodec.decodeReference(modelReference);
      if (model != null) {
        TLcdGXYLayer layer = (TLcdGXYLayer) fLayerFactory.createGXYLayer(model);

        //restore the style related settings
        layer.setVisible(props.getBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible()));
        layer.setSelectable(props.getBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable()));
        layer.setLabeled(props.getBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled()));
        layer.setSelectionLabeled(props.getBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled()));
        layer.setEditable(props.getBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable()));
        layer.setLabel(props.getString(fShortPrefix + LABEL_KEY, layer.getLabel()));

        return layer;
      }
    }
    return null;
  }
}
Details of the GXYWayPointsLayerWorkspaceCodec implementation

During encoding, our codec needs to write information to the workspace OutputStream in the encodeObject method. We can get the same information back from the workspace by reading from an InputStream in the createObject method. One of the easiest ways to do that is using an ALcyProperties instance.

An ALcyProperties instance is created in the encodeObject method.

TLcyStringProperties properties = new TLcyStringProperties();

In the encodeObject method, the codec enters all the necessary information as key-value pairs in that ALcyProperties instance. Once it contains all the information, the information is stored in the OutputStream:

new TLcyStringPropertiesCodec().encode(properties, aOut);

During decoding, the information can be easily retrieved from the InputStream and converted back into an ALcyProperties instance:

ALcyProperties props = new TLcyStringPropertiesCodec().decode(aIn);

Because the codec is responsible for re-creating the object and restoring its state during workspace decoding, the codec needs to store two types of information in the encoding phase:

  • The information needed to re-create the layer: the creation of the layer can be delegated to the ILcdGXYLayerFactory. The layer factory only requires an ILcdModel as input, so that is all that needs to be stored.

    As explained in the previous section, when a codec needs to store a reference to another Java object, it must ask the workspace frame work to provide such a reference. To request a reference, you can use the ALcyWorkspaceCodec.encodeReference method.

    ILcdModel model = layer.getModel();
    
    String referenceToModel = aWSCodec.encodeReference(model);
    properties.putString(fShortPrefix + MODEL_REFERENCE_KEY, referenceToModel);

    Note how the codec does not try to insert the ILcdModel directly into the ALcyProperties. Instead, it stores the reference returned by the framework.

  • The information needed to restore the state of the layer: during the lifetime of the application, the user could have altered the state of the layer, by changing the label of the layer for example. Therefore, the codec stores all the settings of the layer that the user may have altered through the UI. It does not make sense to store information that an application user cannot change.

    //Store the style related settings
    //We only store the settings which can be altered by the user in the UI
    properties.putBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible());
    properties.putBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable());
    properties.putBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled());
    properties.putBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled());
    properties.putBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable());
    properties.putString(fShortPrefix + LABEL_KEY, layer.getLabel());

    All that information is used during the decoding of the workspace:

  • Re-creating the layer: first the codec must ask the framework to restore the model. The model is restored by retrieving the stored reference from the ALcyProperties and passing it to the ALcyWorkspaceCodec.decodeReference method. Once the model is available, the layer factory creates the layer:

    String modelReference = props.getString(fShortPrefix + MODEL_REFERENCE_KEY, null);
    if (modelReference != null) {
      ILcdModel model = (ILcdModel) aWSCodec.decodeReference(modelReference);
      if (model != null) {
        TLcdGXYLayer layer = (TLcdGXYLayer) fLayerFactory.createGXYLayer(model);
  • Restoring the state of the layer: restoring the state of the layer is as straightforward as reading out the settings the codec stored in the workspace, and applying them to the layer one-by-one.

    //restore the style related settings
    layer.setVisible(props.getBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible()));
    layer.setSelectable(props.getBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable()));
    layer.setLabeled(props.getBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled()));
    layer.setSelectionLabeled(props.getBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled()));
    layer.setEditable(props.getBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable()));
    layer.setLabel(props.getString(fShortPrefix + LABEL_KEY, layer.getLabel()));

Adding an ALcyAddOn to plug in the ALcyFormat

If you are coding along with this tutorial, you’ll note that at this point our ALcyFormat implementation still requires to implement an extra method (the isModelOfFormat method).

This missing method will be at the end of this section.

Creating the ALcyAddOn

Because we are creating our own functionality and adding it to Lucy, we need to create our own Lucy add-on. A Lucy add-on is an extension of ALcyAddOn.

One of the benefits of using an ALcyFormat in the previous step is that our add-on can be an extension of ALcyFormatAddOn. The format add-on:

  • Uses a configuration file for configurable settings.

  • Makes it easy to register all the objects created in the ALcyFormat, such as the model decoder, as a service.

package com.luciad.lucy.addons.tutorial.staticgxydata;
public final class GXYWayPointsAddOn extends ALcyFormatAddOn {

  public GXYWayPointsAddOn() {
    super(ALcyTool.getLongPrefix(GXYWayPointsAddOn.class),
          ALcyTool.getShortPrefix(GXYWayPointsAddOn.class));
  }
}

The prefixes we need to pass to the super constructor are:

  • A long prefix, which is used to generate unique IDs.

  • A short prefix, which is the prefix used in the configuration file of the add-on

The add-on needs to create the ALcyFormat so that it can plug in everything, including the model decoder and model content type provider we created:

@Override
protected ALcyFormat createBaseFormat() {
  return new GXYWayPointsModelFormat(getLucyEnv(), getLongPrefix(), getShortPrefix(), getPreferences());
}

We also pass the parsed version of the configuration file (getPreferences()) to the format, and the prefix used in the configuration file (getShortPrefix) to the format. This allows to format to read settings from the configuration file.

The last method we need to implement is the createFormatWrapper method.

The Lucy API contains a number of decorators or wrappers for an ALcyFormat which makes it easy to re-use functionality. You can find all available decorators in the Javadoc by looking for the available extensions of ALcyFormatWrapper.

In this tutorial we are going to use the TLcySafeGuardFormatWrapper: this wrapper ensures that the instances created in the base ALcyFormat are only called with the correct models and layers.

For example the ILcyModelContentTypeProvider we created currently violates the contract because it indicates that all models contain point data. Instead, it should indicate that our waypoint models contain point data, and that it does not know what kind of data other models contain.

By decorating our format with a TLcySafeGuardFormatWrapper, our ILcyModelContentTypeProvider will only be called with waypoint models.

In order for this to work, the TLcySafeGuardFormatWrapper needs to know which models belong to our format and which do not. This information is provided by the last method we still need to implement in our ALcyFormat:

@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());
}

Adding the safe guard wrapper is done in the add-on:

@Override
protected ALcyFormat createFormatWrapper(ALcyFormat aBaseFormat) {
  return new TLcySafeGuardFormatWrapper(new TLcyAsynchronousFormatWrapper(aBaseFormat));
}

In this method, we also decorate the format with a TLcyAsynchronousFormatWrapper instance. This wrapper will ensure that the layers created by our ALcyFormat are painted asynchronously (= on a background thread).

Creating the config file for the add-on

Our add-on requires a configuration file, and the ALcyFileFormat we used reads some properties from that configuration (as documented in the class Javadoc). The same applies to the TLcyAsynchronousFormatWrapper.

GXYWayPointsAddOn.fileTypeDescriptor.displayName=Way Point files
GXYWayPointsAddOn.fileTypeDescriptor.defaultExtension=cwp
GXYWayPointsAddOn.fileTypeDescriptor.filters=*.cwp
GXYWayPointsAddOn.fileTypeDescriptor.groupIDs=All Vector Files

# Configuration options for asynchronous painting
# Consult the class javadoc of TLcyAsynchronousFormatWrapper for more info
GXYWayPointsAddOn.asynchronous=true
GXYWayPointsAddOn.asynchronous.bodiesOnly=false

Plugging in the add-ons

To plug in our add-on, we add it to a custom add-ons file:

The GXYWayPointAddOns.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_gxy.xml</include>

    <!-- Add our own add-ons for waypoint data -->
    <addon>
      <priority>data_producer</priority>
      <name>Waypoint format</name>
      <class>com.luciad.lucy.addons.tutorial.staticgxydata.GXYWayPointsAddOn</class>
      <configFile>docs/articles/tutorial/lucy/customdata/gxy/GXYWayPointsAddOn.cfg</configFile>
    </addon>
  </addons>
</addonConfiguration>

See How to customize the add-ons used by your application for more information about using custom add-ons.

Profiting from other functionality that just works

Lucy comes bundled with a lot of default functionality. Now that we added support for the waypoint data files, we get a lot of extra functionality for free:

  • Integration with the table view: you can view the waypoint data in a tabular view. Just right-click on the layer and choose the Table view option. This functionality is provided by the TLcyToteAddOn.

  • Object properties: double-click on a waypoint to open an object properties view. It displays properties that are based on the data model of the waypoint.

This tutorial assumes the model data is loaded from file. Consult the "Generated data" sample samples.lucy.format.generated.Main for an illustration of handling models created in code.

Examples of such models are models that are the result of some computation, the result of some web service request, or a connection to a live feed of tracks.

Adding support for editing the data

The layer factory created in our ALcyFormat already creates editable layers (TLcdGXYLayer is editable). The selection and editing controller available on a standard Lucy GXY map can edit standard shapes, so at this point our waypoint data is already editable. However, the changes made by the user cannot be saved, the user cannot create new waypoints, …​ .

If your data should not be editable, you can safely skip the remainder of this tutorial.

The only things you need to do are:

  • Making sure your layer factory does return layers where ILcdLayer#isEditableSupported returns false

  • The ILcyGXYLayerTypeProvider does not return EDITABLE as layer type

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.

While an ILcdModelDecoder is used 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 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 have to do two things:

  • Create an ILcdModelEncoder: for this purpose, we reuse 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.

Just like the ILcdModelDecoder, this is achieved 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 of a way point 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 reuse that customizer panel and create it in an ILcyCustomizerPanelFactory which we register as 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 TLcySafeGuardFormatWrapper 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 to make changes to the waypoint.

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

  • When the controller is activated but there is no waypoint layer, the controller should ask the user whether it needs to create a new layer

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:

  • Should 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 should be created.

  • The position of the button should 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 for example group them in sub-groups.

To achieve this, we will use a number of utility classes and methods:

Our ALcyFormatBar implementation looks like:

package com.luciad.lucy.addons.tutorial.staticgxydata;

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.ILcyMapComponent;
import com.luciad.lucy.map.action.TLcyCreateGXYLayerAction;
import com.luciad.lucy.model.ALcyDefaultModelDescriptorFactory;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.gxy.ILcdGXYLayer;

import samples.lucy.util.LayerUtil;

final class GXYWayPointsFormatBar extends ALcyFormatBar {

  /**
   * The actual Swing component representing the format bar
   */
  private final TLcyToolBar fToolBar = new TLcyToolBar();
  private final GXYWayPointsNewControllerModel fControllerModel;

  GXYWayPointsFormatBar(ILcyMapComponent aMapComponent,
                        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,
                                                 GXYWayPointsFormatBarFactory.TOOLBAR_ID,
                                                 aMapComponent,
                                                 aProperties,
                                                 aShortPrefix,
                                                 (JComponent) aMapComponent.getComponent(),
                                                 actionBarManager);

    TLcyCreateGXYLayerAction createGXYLayerAction = new TLcyCreateGXYLayerAction(aLucyEnv, aMapComponent);
    createGXYLayerAction.setDefaultModelDescriptorFactory(aDefaultModelDescriptorFactory);

    fControllerModel = new GXYWayPointsNewControllerModel(createGXYLayerAction, aMapComponent);
    LayerUtil.insertCreateShapeActiveSettable(aProperties, aShortPrefix, aLucyEnv, aMapComponent, fControllerModel);
  }

  @Override
  protected void updateForLayer(ILcdLayer aPreviousLayer, ILcdLayer aLayer) {
    fControllerModel.setCurrentLayer((ILcdGXYLayer) aLayer);
  }

  @Override
  public boolean canSetLayer(ILcdLayer aLayer) {
    // TLcySafeGuardFormatWrapper already checks the layer
    return true;
  }

  @Override
  public Component getComponent() {
    return fToolBar.getComponent();
  }
}

and the GXYWayPointsNewControllerModel

package com.luciad.lucy.addons.tutorial.staticgxydata;

import java.awt.Graphics;
import java.awt.event.MouseEvent;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.map.ILcyMapComponent;
import com.luciad.lucy.map.action.TLcyCreateGXYLayerAction;
import com.luciad.lucy.map.controller.ALcyGXYNewControllerModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.view.gxy.ILcdGXYContext;
import com.luciad.view.gxy.ILcdGXYLayerSubsetList;

final class GXYWayPointsNewControllerModel extends ALcyGXYNewControllerModel {

  GXYWayPointsNewControllerModel(TLcyCreateGXYLayerAction aCreateLayerAction, ILcyMapComponent aMapComponent) {
    super(aCreateLayerAction, aMapComponent);
  }

  @Override
  public Object create(int aEditCount, Graphics aGraphics, MouseEvent aMouseEvent, ILcdGXYLayerSubsetList aSnappables, ILcdGXYContext aContext) {
    //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 GXYWayPointsFormatBar 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 GXYWayPointsAddOn.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.
GXYWayPointsAddOn.gxyWayPointsToolBar.groupPriorities=\
  LayerGroup,\
  CreateGroup,\
  DefaultGroup

The button for the controller is inserted using TLcyActionBarUtil#insertInConfiguredActionBars. As such, we need to configure its location in the GXYWayPointsAddOn.cfg configuration file as well:

GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.item=Way Point
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.groups=CreateGroup
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.shortDescription=Create a new way point
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.smallIcon=draw_point

Creating and registering the format bar factory

You can create the format bar factory in the ALcyFormat in the ALcyFormat#createFormatBarFactory method, and register it automatically:

@Override
protected ALcyFormatBarFactory createFormatBarFactory() {
  return new GXYWayPointsFormatBarFactory(getLucyEnv(), getProperties(), getShortPrefix());
}

The implementation of the factory creates new GXYWayPointsFormatBar instances:

package com.luciad.lucy.addons.tutorial.staticgxydata;

import com.luciad.lucy.ILcyLucyEnv;
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.ILcyMapComponent;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;

final class GXYWayPointsFormatBarFactory extends ALcyFormatBarFactory {

  static final String TOOLBAR_ID = "gxyWayPointsToolBar";

  private final ILcyLucyEnv fLucyEnv;
  private final ALcyProperties fProperties;
  private final String fShortPrefix;

  GXYWayPointsFormatBarFactory(ILcyLucyEnv aLucyEnv, ALcyProperties aProperties, String aShortPrefix) {
    fLucyEnv = aLucyEnv;
    fProperties = aProperties;
    fShortPrefix = aShortPrefix;
  }

  @Override
  public boolean canCreateFormatBar(ILcdView aView, ILcdLayer aLayer) {
    // TLcySafeGuardFormatWrapper already checks the layer
    return findMapComponent(aView) != null;
  }

  @Override
  public ALcyFormatBar createFormatBar(ILcdView aView, ILcdLayer aLayer) {
    GXYWayPointsAddOn wayPointsModelAddOn = fLucyEnv.retrieveAddOnByClass(GXYWayPointsAddOn.class);
    return new GXYWayPointsFormatBar(findMapComponent(aView),
                                     fProperties,
                                     fShortPrefix,
                                     wayPointsModelAddOn.getFormat().getDefaultModelDescriptorFactories()[0],
                                     fLucyEnv);
  }

  private ILcyMapComponent findMapComponent(ILcdView aView) {
    ILcyGenericMapComponent mapComponent = fLucyEnv.getCombinedMapManager().findMapComponent(aView);
    return mapComponent instanceof ILcyMapComponent ? (ILcyMapComponent) mapComponent : null;
  }
}

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: a ALcyDefaultModelDescriptorFactory to create the ILcdModelDescriptor, and a 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);
    }
  };
}

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. Since 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. This is done in the plugInto method of our ALcyFormatAddOn:

@Override
public void plugInto(ILcyLucyEnv aLucyEnv) {
  super.plugInto(aLucyEnv);

  TLcyCombinedMapManager mapManager = aLucyEnv.getCombinedMapManager();
  ILcyGenericMapManagerListener<ILcdView, ILcdLayer> listener =
      ILcyGenericMapManagerListener.onMapAdded(ILcdGXYView.class, mapComponent ->
      {
        ILcyLucyEnv lucyEnv = getFormat().getLucyEnv();
        TLcyCreateGXYLayerAction action = new TLcyCreateGXYLayerAction(getFormat(), (ILcyMapComponent) mapComponent);
        //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,
            mapComponent,
            lucyEnv.getUserInterfaceManager().getActionBarManager(),
            getPreferences());
      });
  mapManager.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 every 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 GXYWayPointsAddOn.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 "gxyWayPointsToolBar":

GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.item=New way point layer
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.groups=LayerGroup
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.shortDescription=Create a new way point layer
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.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. The layer selection transfer handler is created in the ALcyFormat:

@Override
protected ALcyLayerSelectionTransferHandler[] createGXYLayerSelectionTransferHandlers() {
  return new ALcyLayerSelectionTransferHandler[]{
      new GXYWayPointsLayerSelectionTransferHandler()
  };
}

For the GXYWayPointsLayerSelectionTransferHandler 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: this is used when copying data between 2 waypoint layers.

  • Convert a shape from another format into a waypoint: this is used when copying a point from a non-waypoint layer into our waypoint layer.

  • Create a copy of a waypoint shape: this is used when copying a waypoint to a layer of another format. In that case, only the shape is copied.

package com.luciad.lucy.addons.tutorial.staticgxydata;

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 GXYWayPointsLayerSelectionTransferHandler extends ALcyDefaultLayerSelectionTransferHandler<ILcdDataObject> {

  private final TLcdGeoReference2GeoReference fTransformer = new TLcdGeoReference2GeoReference();

  GXYWayPointsLayerSelectionTransferHandler() {
    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 for the edited data

At the start of the tutorial, we already added workspace support for our layers, and we got support for the model data from the ALcyFileFormat. However, now that we allowed users to edit the data we need to take a few more steps before the edited 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) {
      aBaseFormat = new TLcyMutableFileFormatWrapper(aBaseFormat);
      return new TLcySafeGuardFormatWrapper(new TLcyAsynchronousFormatWrapper(aBaseFormat));
    }

Making use of optional GXY format settings

There are a number of features that you get out-of-the-box when you work with Lightspeed formats. If you are working with GXY formats, you have to implement them yourself though. Those features are all optional. They have been left out of this tutorial for the sake of brevity.

Allow snapping to way points

When users are editing or creating shapes on the map, they often want points to automatically connect with other nearby points or shapes on the map. In the LuciadLightspeed products, that functionality is called snapping: when the user moves a point on the map close to another point, the location of the edited point is adjusted automatically to match the location of the existing point.

If you want to add the option to snap to a way point, you must make sure that the ILcdGXYLayer created by the ILcdGXYLayerFactory returns a layer instance that also implements the ILcySnappable interface.

Adding undo and redo support

By default, there is no undo and redo support for the changes a user makes to way points on the map. To add this functionality, use an ILcdGXYEditor that also implements the ILcdUndoableSource interface on the layer.

An example of such an editor is available in the LuciadLightspeed samples: see the samples.gxy.undo.UndoablePointListPainter class.

Adding a custom layer properties panel

The sample does not register a custom layer properties panel, but relies on the standard Lucy layer properties panel. If you want to give the user the possibility to adjust extra properties, you need to create your own layer properties panel.

To create such a custom properties, the ALcyFormat must plug in a ILcyCustomizerPanelFactory that can create ILcyCustomizerPanel implementations for a TLcyLayerContext object. The creation of an ILcyCustomizerPanel is illustrated in a dedicated tutorial. You can create that factory in the ALcyFormat.createGXYLayerCustomizerPanelFactories method.

When you replace the layer properties panel and offer the user more customization options for the layer, consider storing and restoring those extra settings in the workspace codec for the layer as well.

Full code

The GXYWayPointsAddOn code

package com.luciad.lucy.addons.tutorial.staticgxydata;

import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.addons.ALcyFormatAddOn;
import com.luciad.lucy.format.ALcyFormat;
import com.luciad.lucy.format.TLcyAsynchronousFormatWrapper;
import com.luciad.lucy.format.TLcyMutableFileFormatWrapper;
import com.luciad.lucy.format.TLcySafeGuardFormatWrapper;
import com.luciad.lucy.gui.TLcyActionBarUtil;
import com.luciad.lucy.map.ILcyGenericMapManagerListener;
import com.luciad.lucy.map.ILcyMapComponent;
import com.luciad.lucy.map.TLcyCombinedMapManager;
import com.luciad.lucy.map.action.TLcyCreateGXYLayerAction;
import com.luciad.lucy.util.ALcyTool;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;
import com.luciad.view.gxy.ILcdGXYView;

public final class GXYWayPointsAddOn extends ALcyFormatAddOn {

  public GXYWayPointsAddOn() {
    super(ALcyTool.getLongPrefix(GXYWayPointsAddOn.class),
          ALcyTool.getShortPrefix(GXYWayPointsAddOn.class));
  }

  @Override
  public void plugInto(ILcyLucyEnv aLucyEnv) {
    super.plugInto(aLucyEnv);

    TLcyCombinedMapManager mapManager = aLucyEnv.getCombinedMapManager();
    ILcyGenericMapManagerListener<ILcdView, ILcdLayer> listener =
        ILcyGenericMapManagerListener.onMapAdded(ILcdGXYView.class, mapComponent ->
        {
          ILcyLucyEnv lucyEnv = getFormat().getLucyEnv();
          TLcyCreateGXYLayerAction action = new TLcyCreateGXYLayerAction(getFormat(), (ILcyMapComponent) mapComponent);
          //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,
              mapComponent,
              lucyEnv.getUserInterfaceManager().getActionBarManager(),
              getPreferences());
        });
    mapManager.addMapManagerListener(listener, true);
  }

  @Override
  protected ALcyFormat createBaseFormat() {
    return new GXYWayPointsModelFormat(getLucyEnv(), getLongPrefix(), getShortPrefix(), getPreferences());
  }

  @Override
  protected ALcyFormat createFormatWrapper(ALcyFormat aBaseFormat) {
    aBaseFormat = new TLcyMutableFileFormatWrapper(aBaseFormat);
    return new TLcySafeGuardFormatWrapper(new TLcyAsynchronousFormatWrapper(aBaseFormat));
  }
}

The GXYWayPointsModelFormat code

package com.luciad.lucy.addons.tutorial.staticgxydata;

import java.awt.Color;
import java.util.Collections;

import com.luciad.gui.TLcdSymbol;
import com.luciad.lucy.ILcyLucyEnv;
import com.luciad.lucy.addons.tutorial.editabledata.model.WayPointCustomizerPanel;
import com.luciad.lucy.datatransfer.ALcyLayerSelectionTransferHandler;
import com.luciad.lucy.format.ALcyFileFormat;
import com.luciad.lucy.gui.customizer.ILcyCustomizerPanel;
import com.luciad.lucy.gui.customizer.ILcyCustomizerPanelFactory;
import com.luciad.lucy.gui.formatbar.ALcyFormatBarFactory;
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.lucy.workspace.ALcyWorkspaceObjectCodec;
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.ILcdGXYLayer;
import com.luciad.view.gxy.ILcdGXYLayerFactory;
import com.luciad.view.gxy.TLcdGXYDataObjectLabelPainter;
import com.luciad.view.gxy.TLcdGXYLayer;
import com.luciad.view.gxy.TLcdGXYShapePainter;

final class GXYWayPointsModelFormat extends ALcyFileFormat {

  GXYWayPointsModelFormat(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() {
    return aModel -> ILcyModelContentType.POINT;
  }

  @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 ILcyGXYLayerTypeProvider createGXYLayerTypeProvider() {
    return aGXYLayer -> ILcyGXYLayerTypeProvider.EDITABLE;
  }

  @Override
  protected ILcdGXYLayerFactory createGXYLayerFactory() {
    return new ILcdGXYLayerFactory() {
      @Override
      public ILcdGXYLayer createGXYLayer(ILcdModel aModel) {
        TLcdGXYLayer layer = TLcdGXYLayer.create(aModel);

        //Configure the styling for the bodies
        TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
        painter.setIcon(new TLcdSymbol(TLcdSymbol.FILLED_CIRCLE, 8, Color.GREEN));
        painter.setSelectedIcon(new TLcdSymbol(TLcdSymbol.FILLED_CIRCLE, 8, new Color(67, 157, 227)));
        layer.setGXYPainterProvider(painter);
        layer.setGXYEditorProvider(painter);

        //Activate labeling and configure the styling of those labels
        TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
        labelPainter.setExpressions("identifier");
        labelPainter.setForeground(Color.WHITE);
        labelPainter.setHaloEnabled(true);
        labelPainter.setHaloColor(Color.BLACK);

        layer.setGXYLabelPainterProvider(labelPainter);
        layer.setLabeled(true);

        return layer;
      }
    };
  }

  @Override
  protected ALcyLayerSelectionTransferHandler[] createGXYLayerSelectionTransferHandlers() {
    return new ALcyLayerSelectionTransferHandler[]{
        new GXYWayPointsLayerSelectionTransferHandler()
    };
  }

  @Override
  protected ALcyWorkspaceObjectCodec[] createGXYLayerWorkspaceCodecs() {
    return new ALcyWorkspaceObjectCodec[]{
        new GXYWayPointsLayerWorkspaceCodec(getLongPrefix(), getShortPrefix(), getGXYLayerFactory())
    };
  }

  @Override
  protected ALcyFormatBarFactory createFormatBarFactory() {
    return new GXYWayPointsFormatBarFactory(getLucyEnv(), getProperties(), getShortPrefix());
  }

  @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 TLcySafeGuardFormatWrapper 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 GXYWayPointsAddOn

GXYWayPointsAddOn.fileTypeDescriptor.displayName=Way Point files
GXYWayPointsAddOn.fileTypeDescriptor.defaultExtension=cwp
GXYWayPointsAddOn.fileTypeDescriptor.filters=*.cwp
GXYWayPointsAddOn.fileTypeDescriptor.groupIDs=All Vector Files

# Configuration options for asynchronous painting
# Consult the class javadoc of TLcyAsynchronousFormatWrapper for more info
GXYWayPointsAddOn.asynchronous=true
GXYWayPointsAddOn.asynchronous.bodiesOnly=false

# 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.
GXYWayPointsAddOn.gxyWayPointsToolBar.groupPriorities=\
  LayerGroup,\
  CreateGroup,\
  DefaultGroup

GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.item=Way Point
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.groups=CreateGroup
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.shortDescription=Create a new way point
GXYWayPointsAddOn.newActiveSettable.gxyWayPointsToolBar.smallIcon=draw_point

GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.item=New way point layer
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.groups=LayerGroup
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.shortDescription=Create a new way point layer
GXYWayPointsAddOn.newLayerAction.gxyWayPointsToolBar.smallIcon=add_empty_layer

The GXYWayPointsFormatBar code

package com.luciad.lucy.addons.tutorial.staticgxydata;

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.ILcyMapComponent;
import com.luciad.lucy.map.action.TLcyCreateGXYLayerAction;
import com.luciad.lucy.model.ALcyDefaultModelDescriptorFactory;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.gxy.ILcdGXYLayer;

import samples.lucy.util.LayerUtil;

final class GXYWayPointsFormatBar extends ALcyFormatBar {

  /**
   * The actual Swing component representing the format bar
   */
  private final TLcyToolBar fToolBar = new TLcyToolBar();
  private final GXYWayPointsNewControllerModel fControllerModel;

  GXYWayPointsFormatBar(ILcyMapComponent aMapComponent,
                        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,
                                                 GXYWayPointsFormatBarFactory.TOOLBAR_ID,
                                                 aMapComponent,
                                                 aProperties,
                                                 aShortPrefix,
                                                 (JComponent) aMapComponent.getComponent(),
                                                 actionBarManager);

    TLcyCreateGXYLayerAction createGXYLayerAction = new TLcyCreateGXYLayerAction(aLucyEnv, aMapComponent);
    createGXYLayerAction.setDefaultModelDescriptorFactory(aDefaultModelDescriptorFactory);

    fControllerModel = new GXYWayPointsNewControllerModel(createGXYLayerAction, aMapComponent);
    LayerUtil.insertCreateShapeActiveSettable(aProperties, aShortPrefix, aLucyEnv, aMapComponent, fControllerModel);
  }

  @Override
  protected void updateForLayer(ILcdLayer aPreviousLayer, ILcdLayer aLayer) {
    fControllerModel.setCurrentLayer((ILcdGXYLayer) aLayer);
  }

  @Override
  public boolean canSetLayer(ILcdLayer aLayer) {
    // TLcySafeGuardFormatWrapper already checks the layer
    return true;
  }

  @Override
  public Component getComponent() {
    return fToolBar.getComponent();
  }
}

The GXYWayPointsFormatBarFactory code

package com.luciad.lucy.addons.tutorial.staticgxydata;

import com.luciad.lucy.ILcyLucyEnv;
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.ILcyMapComponent;
import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.view.ILcdLayer;
import com.luciad.view.ILcdView;

final class GXYWayPointsFormatBarFactory extends ALcyFormatBarFactory {

  static final String TOOLBAR_ID = "gxyWayPointsToolBar";

  private final ILcyLucyEnv fLucyEnv;
  private final ALcyProperties fProperties;
  private final String fShortPrefix;

  GXYWayPointsFormatBarFactory(ILcyLucyEnv aLucyEnv, ALcyProperties aProperties, String aShortPrefix) {
    fLucyEnv = aLucyEnv;
    fProperties = aProperties;
    fShortPrefix = aShortPrefix;
  }

  @Override
  public boolean canCreateFormatBar(ILcdView aView, ILcdLayer aLayer) {
    // TLcySafeGuardFormatWrapper already checks the layer
    return findMapComponent(aView) != null;
  }

  @Override
  public ALcyFormatBar createFormatBar(ILcdView aView, ILcdLayer aLayer) {
    GXYWayPointsAddOn wayPointsModelAddOn = fLucyEnv.retrieveAddOnByClass(GXYWayPointsAddOn.class);
    return new GXYWayPointsFormatBar(findMapComponent(aView),
                                     fProperties,
                                     fShortPrefix,
                                     wayPointsModelAddOn.getFormat().getDefaultModelDescriptorFactories()[0],
                                     fLucyEnv);
  }

  private ILcyMapComponent findMapComponent(ILcdView aView) {
    ILcyGenericMapComponent mapComponent = fLucyEnv.getCombinedMapManager().findMapComponent(aView);
    return mapComponent instanceof ILcyMapComponent ? (ILcyMapComponent) mapComponent : null;
  }
}

The GXYWayPointsLayerSelectionTransferHandler code

package com.luciad.lucy.addons.tutorial.staticgxydata;

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 GXYWayPointsLayerSelectionTransferHandler extends ALcyDefaultLayerSelectionTransferHandler<ILcdDataObject> {

  private final TLcdGeoReference2GeoReference fTransformer = new TLcdGeoReference2GeoReference();

  GXYWayPointsLayerSelectionTransferHandler() {
    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 GXYWayPointsLayerWorkspaceCodec code

package com.luciad.lucy.addons.tutorial.staticgxydata;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.luciad.lucy.util.properties.ALcyProperties;
import com.luciad.lucy.util.properties.TLcyStringProperties;
import com.luciad.lucy.util.properties.codec.TLcyStringPropertiesCodec;
import com.luciad.lucy.workspace.ALcyWorkspaceCodec;
import com.luciad.lucy.workspace.ALcyWorkspaceObjectCodec;
import com.luciad.lucy.workspace.TLcyWorkspaceAbortedException;
import com.luciad.model.ILcdModel;
import com.luciad.view.gxy.ILcdGXYLayerFactory;
import com.luciad.view.gxy.TLcdGXYLayer;

final class GXYWayPointsLayerWorkspaceCodec extends ALcyWorkspaceObjectCodec {
  private static final String MODEL_REFERENCE_KEY = "model";

  private static final String VISIBLE_KEY = "layer.visible";
  private static final String SELECTABLE_KEY = "layer.selectable";
  private static final String LABELED_KEY = "layer.labeled";
  private static final String SELECTION_LABELED_KEY = "layer.selectionLabeled";
  private static final String EDITABLE_KEY = "layer.editable";
  private static final String LABEL_KEY = "layer.label";

  private final String fUID;
  private final String fShortPrefix;
  private final ILcdGXYLayerFactory fLayerFactory;

  GXYWayPointsLayerWorkspaceCodec(String aLongPrefix, String aShortPrefix, ILcdGXYLayerFactory aLayerFactory) {
    fLayerFactory = aLayerFactory;
    fShortPrefix = aShortPrefix;
    fUID = aLongPrefix + "layerCodec";
  }

  @Override
  public String getUID() {
    return fUID;
  }

  @Override
  public boolean canEncodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent) {
    //All checks are done by the safeguard format wrapper
    return true;
  }

  @Override
  public void encodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent, OutputStream aOut) throws IOException, TLcyWorkspaceAbortedException {
    TLcyStringProperties properties = new TLcyStringProperties();

    TLcdGXYLayer layer = (TLcdGXYLayer) aObject;
    ILcdModel model = layer.getModel();

    String referenceToModel = aWSCodec.encodeReference(model);
    properties.putString(fShortPrefix + MODEL_REFERENCE_KEY, referenceToModel);

    //Store the style related settings
    //We only store the settings which can be altered by the user in the UI
    properties.putBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible());
    properties.putBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable());
    properties.putBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled());
    properties.putBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled());
    properties.putBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable());
    properties.putString(fShortPrefix + LABEL_KEY, layer.getLabel());

    new TLcyStringPropertiesCodec().encode(properties, aOut);
  }

  @Override
  public Object createObject(ALcyWorkspaceCodec aWSCodec, Object aParent, InputStream aIn) throws IOException, TLcyWorkspaceAbortedException {
    ALcyProperties props = new TLcyStringPropertiesCodec().decode(aIn);

    String modelReference = props.getString(fShortPrefix + MODEL_REFERENCE_KEY, null);
    if (modelReference != null) {
      ILcdModel model = (ILcdModel) aWSCodec.decodeReference(modelReference);
      if (model != null) {
        TLcdGXYLayer layer = (TLcdGXYLayer) fLayerFactory.createGXYLayer(model);

        //restore the style related settings
        layer.setVisible(props.getBoolean(fShortPrefix + VISIBLE_KEY, layer.isVisible()));
        layer.setSelectable(props.getBoolean(fShortPrefix + SELECTABLE_KEY, layer.isSelectable()));
        layer.setLabeled(props.getBoolean(fShortPrefix + LABELED_KEY, layer.isLabeled()));
        layer.setSelectionLabeled(props.getBoolean(fShortPrefix + SELECTION_LABELED_KEY, layer.isSelectionLabeled()));
        layer.setEditable(props.getBoolean(fShortPrefix + EDITABLE_KEY, layer.isEditable()));
        layer.setLabel(props.getString(fShortPrefix + LABEL_KEY, layer.getLabel()));

        return layer;
      }
    }
    return null;
  }
}

The GXYWayPointsNewControllerModel code

package com.luciad.lucy.addons.tutorial.staticgxydata;

import java.awt.Graphics;
import java.awt.event.MouseEvent;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.lucy.map.ILcyMapComponent;
import com.luciad.lucy.map.action.TLcyCreateGXYLayerAction;
import com.luciad.lucy.map.controller.ALcyGXYNewControllerModel;
import com.luciad.model.tutorial.customvector.WayPointsModelDecoder;
import com.luciad.shape.shape3D.TLcdLonLatHeightPoint;
import com.luciad.view.gxy.ILcdGXYContext;
import com.luciad.view.gxy.ILcdGXYLayerSubsetList;

final class GXYWayPointsNewControllerModel extends ALcyGXYNewControllerModel {

  GXYWayPointsNewControllerModel(TLcyCreateGXYLayerAction aCreateLayerAction, ILcyMapComponent aMapComponent) {
    super(aCreateLayerAction, aMapComponent);
  }

  @Override
  public Object create(int aEditCount, Graphics aGraphics, MouseEvent aMouseEvent, ILcdGXYLayerSubsetList aSnappables, ILcdGXYContext aContext) {
    //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 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_gxy.xml</include>

    <!-- Add our own add-ons for waypoint data -->
    <addon>
      <priority>data_producer</priority>
      <name>Waypoint format</name>
      <class>com.luciad.lucy.addons.tutorial.staticgxydata.GXYWayPointsAddOn</class>
      <configFile>docs/articles/tutorial/lucy/customdata/gxy/GXYWayPointsAddOn.cfg</configFile>
    </addon>
  </addons>
</addonConfiguration>