Layer styling

Introduction

When an ILcdModel is visualized in a view by means of an ILcdLayer, the visual appearance of the layer is determined by the elements in the model, but also by settings on the layer and painters. For example, when the model only specifies that a line must be drawn at a certain location, the line color and line width can be determined by the layer and/or painter settings.

The Lightspeed API introduces the concepts of ALspStyle and ILspStyler to represent and provide the style of a layer. The Lightspeed layers created by Lucy use the same mechanism, and allow you to make modifications to the style through this API. See Visualizing vector data in Lightspeed views for more information.

While almost anything is possible by using the Lightspeed styling API directly, Lucy provides extra utilities to support common style use cases out-of-the-box for both the developer and the end user. See the Lucy user guide for the possibilities available to the end users. This article illustrates the styling options for Lucy developers.

Lucy style integration

You can access the ILspStyler instances of each ILspStyledLayer through the ILspStyledLayer.getStyler method. At the interface level, ILspStyler does not reveal any information about the styles it provides. There is however a more specific extension of ILspStyler that exposes information about the styles it uses: ILspCustomizableStyler. The style information is accessible through its ILspCustomizableStyler.getStyles methods.

The Lucy style support is focused on those ILspCustomizableStyler instances, because it allows us to access and modify those styles. If a custom layer uses and exposes ILspCustomizableStyler instances, it will integrate with the Lucy style mechanism. In turn, most of the Lightspeed layers created by the Lucy formats use ILspCustomizableStyler instances, allowing the Lucy developer to easily access and modify the style of existing layers.

Adjust the default layer style

Most of the Lightspeed formats in Lucy allow you to configure a default style file. The settings in this file will be applied to each new layer created by the layer factory of that format. See the Javadoc of ALcyLspStyleFormat.DEFAULT_STYLE_FILE_SUFFIX for more information. Because the Lucy UI allows you to save the layer style through File | Save Layer Style, it is possible to adjust the style of a layer through the UI, save the layer style to file, and configure this file as the new default style in the configuration file of the corresponding format add-on.

Create UI to modify the layer style

Lucy provides a customizable UI which allows users to modify the general style of a layer: make it visible, selectable, and so on. If the layer uses one or more ILspCustomizableStyler instances, this UI also allows you to change the ALspStyle settings, for example change the color of a line. This UI is shown when the Layer properties action of the layer control panel is triggered.

This functionality is provided by the TLcyLspLayerCustomizerAddOn. This add-on registers a customizer panel factory, TLcyLspStyledLayerCustomizerPanelFactory, which creates a panel to modify the settings of a Lightspeed layer based on the ILspCustomizableStyler instances retrieved through the ILspStyledLayer.getStyler method.

Read on for an overview of how the factory constructs an ILcyCustomizerPanel and a discussion of the different customization options.

Construction mechanism of the panel

TLcyLspStyledLayerCustomizerPanelFactory sequence diagram
Figure 1. Inner workings of the TLcyLspStyledLayerCustomizerPanelFactory

The factory can create a customizer panel for a TLcyLayerContext, as illustrated in step 1. It first asks another factory to create a panel for the general layer settings by passing the TLcyLayerContext to this factory (step 2). The created panel typically contains checkboxes for visible, labeled, and so on.

Then it retrieves the ILspCustomizableStyler instances from the layer if any are available. Each of those stylers is asked for its TLspCustomizableStyle instances, and for those style instances a customizer panel is created (step 3). It does so by wrapping each TLspCustomizableStyle in a TLcyLspCustomizableStyleContext, which also provides the layer, the paint representation and the paint state of the style.

The customizer panels are created by the instances registered with the back-end for each TLcyLspCustomizableStyleContext (step 3). This context wrapper is useful when the panel requires layer information to know which settings should be displayed in the customizer panel, for instance. Examples of such panels are panels that allow users to modify the line, fill and icon style.

Once all the panels are created, the factory creates a ILcyCompositeCustomizerPanel (step 4) to which all panels created in steps 2 and 3 are added. The panels from steps 2 and 3 are not added directly to the composite panel, but are wrapped. To allow the composite panel to recognize the different panels, a Map is passed to the createCompositeCustomizerPanel method containing the mapping between the customizer panels which will be added, and the object for which the panels were created in step 2 and 3.

Step 5 and 6 show how the general layer settings panel from step 2 and the style related panels from step 3 are added to the composite panel.

Once all panels are added, the factory returns the composite panel, which now contains the panels created for the general layer settings and the style-related panels (step 7).

This mechanism allows you to customize the resulting UI:

  • By overriding the createCompositeCustomizerPanel method in TLcyLspStyledLayerCustomizerPanelFactory, you have full control of how the different subpanels are combined into one UI. A possible use case is to switch from the default tabbed UI to UI that combines all panels into one panel. This is illustrated in Program: Override the createCompositeCustomizerPanel method to modify how the panels are combined..

    Program: Override the createCompositeCustomizerPanel method to modify how the panels are combined. (from samples/lucy/lightspeed/style/newiconstyle/SHPCustomizerPanelFactory)
    @Override
    protected ILcyCompositeCustomizerPanel createCompositeCustomizerPanel(Map<ILcyCustomizerPanel, Object> aCustomizerPanelObjectMap) {
      TLcyCompositeCustomizerPanel result = new TLcyCompositeCustomizerPanel() {
        @Override
        public void addCustomizerPanel(ILcyCustomizerPanel aCustomizerPanel) {
          //do not add the panel to the UI since we want to combine all customizer panels on one panel
          super.addCustomizerPanel(aCustomizerPanel, false);
          //all customizer panels are components, so it is save to perform the cast
          add(((Component) aCustomizerPanel));
        }
      };
      result.setLayout(new BoxLayout(result, BoxLayout.Y_AXIS));
      return result;
    }
  • You can replace the general layer settings UI by passing another ILcyCustomizerPanelFactory instance to the constructor of TLcyLspStyledLayerCustomizerPanelFactory.

  • You can replace the UI for the customizable style contexts by registering ILcyCustomizerPanelFactory instances as a service that accepts the TLcyLspCustomizableStyleContext instances. The lucy.lightspeed.style.modifiedlinestylecustomizer sample illustrates this by registering a custom factory for line styles. See the CustomizableLineStyleCustomizerPanelFactory class for more information.

Because the UI to customize the layer style depends only on the presence of an ILspCustomizableStyler on the layer, it can be re-used by any Lightspeed layer on condition that the layer exposes the correct styler.

Replace for all formats

Most of the default Lightspeed formats do not register an ILcyCustomizerPanelFactory to alter the layer settings. Instead they rely on the presence of the TLcyLspLayerCustomizerAddOn to register such an ILcyCustomizerPanelFactory. If you want to replace the UI for all formats, you can simply remove that add-on and plug in your own ILcyCustomizerPanelFactory. All those formats will then use that factory.

There are a few formats that provide their own ILcyCustomizerPanelFactory and do not rely on the TLcyLspLayerCustomizerAddOn. If you want to change those panels as well, you must adjust the format-specific panel as well.

Obtain a consistent layout across multiple panels

As explained in the first part of Construction mechanism of the panel, the layer properties panel in Lucy is constructed from individual panels for each of the exposed styles. By using the TLcyTwoColumnLayoutBuilder for each of the individual style panels, it is possible to have a consistent layout for the whole panel. That is, the columns will be aligned across all panels. Although the columns have the same width, it is still derived from the preferred sizes of the containing components.

If you want to customize a panel and keep this consistent layout, it is sufficient to use the layout builder for your panel. This is illustrated in Program: Using the two-column layout builder to add components to the UI, which shows the code responsible for adding the UI components to a modified line style customer panel. See CustomizableLineStyleCustomizerPanel in the samples for an illustration. When this custom line style panel is added to the composite panel, the composite panel makes sure that the layout of this custom panel is consistent with that of the other panels in the composite panel.

Program: Using the TLcyTwoColumnLayoutBuilder to add components to the UI (from samples/lucy/lightspeed/style/modifiedlinestylecustomizer/CustomizableLineStyleCustomizerPanel)
/**
 * Add all components to the panel by using the TLcyTwoColumnLayoutBuilder, which makes sure
 * all components are aligned in the same way as the other style panels
 */
private void initUI() {
  TLcyTwoColumnLayoutBuilder builder = TLcyTwoColumnLayoutBuilder.newBuilder();
  builder.
             row().columnOne(new JLabel(TLcyLang.getString("Width")), fSpinner).
             build();
  builder.populate(this);
}

Consult the class Javadoc of TLcyTwoColumnLayoutBuilder for more details on how to use this layout builder.

Using the TLcyTwoColumnLayoutBuilder allows you to have a consistent layout across the different panels, but it is also a convenient way to rapidly create a good-looking UI.

Replace for a specific format

When the add-on responsible for a specific format exposes the creation of its ALcyLspFormat, you can override the ALcyLspFormat.createLayerCustomizerPanelFactories method and return your own ILcyCustomizerPanelFactory. This will only affect the UI for the layers of that specific format. The lucy.lightspeed.style.newiconstyle sample illustrates this:

Program: Providing a format-specific layer properties UI (from samples/lucy/lightspeed/style/newiconstyle/SHPFormat)
@Override
protected ILcyCustomizerPanelFactory[] createLayerCustomizerPanelFactories() {
  return new ILcyCustomizerPanelFactory[]{new SHPCustomizerPanelFactory(getLucyEnv())};
}

Use OGC SLD styling

The majority of the Lightspeed layers in Lucy automatically pick up an SLD file located next to the source file of the model. If such an SLD file is present, the created layer is styled according to the styling rules defined in that SLD file.

Currently, Lucy does not offer any UI to customize the SLD styling. When SLD styling is used, the layer properties panel only shows UI to change the general layer properties such as the visibility, the label, and so on.

The use of an SLD file in conjunction with the model source file does not just allow you to set up more complicated styling than you could set up with ILspCustomizableStyler. It also allows you to define scale ranges and filters for your dataset. By setting up well-chosen filters and scale ranges in the SLD file, you can ensure that Lucy loads large data files without a decrease in performance.

Managing layer styles

Lucy has a more general layer style concept that groups all style settings of a layer into one object: the ILcyLayerStyle interface. This concept allows to save and load layer styles, copy styles between layers and keep track of changes to the style in a uniform way for all layers.

The ILcyLayerStyle is a black-box object at the interface level, so it can only be used between similar layers that are aware of their type of style. It only allows to register ILcdChangeListener instances that will be warned when the style changes. For example, for a Lightspeed layer the actual ILcyLayerStyle implementation could contain references to the different ILspStyler instances of the layer.

To retrieve the ILcyLayerStyle of a layer, the concept of an ILcyLayerStyleProvider has been introduced. This provider can be asked to provide the ILcyLayerStyle of a layer, as shown in Program: Retrieving an ILcyLayerStyle by using an ILcyLayerStyleProvider. Making changes to this layer style will change the corresponding settings on the layer and/or painter. Typically an ILcyLayerStyleProvider is created in an ALcyFormat or ALcyLspFormat, because the implementation of such a style provider depends on the implementation of the layer, and it is registered as a service with the Lucy back-end. For more information, see the Lucy services mechanism documentation).

Program: Retrieving an ILcyLayerStyle by using an ILcyLayerStyleProvider (from samples/lucy/loadstyle/ChangeStyleAction)
//retrieve the style from the new layer
TLcyCompositeLayerStyleProvider layerStyleProvider = new TLcyCompositeLayerStyleProvider(fLucyEnv);
if (layerStyleProvider.canGetStyle(layerWithNewStyle)) {
  layerStyle = layerStyleProvider.getStyle(layerWithNewStyle);
}

Furthermore, the ILcyLayerStyleProvider allows you to apply a certain ILcyLayerStyle to a layer, after which the layer settings will match those contained in the ILcyLayerStyle. Applying a style to a layer is illustrated in Program: Applying an ILcyLayerStyle by using an ILcyLayerStyleProvider.

Program: Applying an ILcyLayerStyle by using an ILcyLayerStyleProvider (from samples/lucy/loadstyle/ChangeStyleAction)
if (layerStyleProvider.canApplyStyle(layerStyle, countriesLayer)) {
  layerStyleProvider.applyStyle(layerStyle, countriesLayer);
}

The combination of both these mechanisms allows you to copy the layer style from one layer to another, for example, as shown in Program: Copy the style from one layer to another layer.

Program: Copy the style from one layer to another layer (from samples/lucy/syncstyle/LayerStyleSynchronizer)
private void copyLayerStyle(ILcdLayer aLayerToCopyFrom, ILcdLayer aLayerToCopyToSFCT) {
  TLcyCompositeLayerStyleProvider layerStyleProvider = new TLcyCompositeLayerStyleProvider(fLucyEnv);
  if (layerStyleProvider.canGetStyle(aLayerToCopyFrom)) {
    ILcyLayerStyle layerStyle = layerStyleProvider.getStyle(aLayerToCopyFrom);
    if (layerStyleProvider.canApplyStyle(layerStyle, aLayerToCopyToSFCT)) {
      layerStyleProvider.applyStyle(layerStyle, aLayerToCopyToSFCT);
    }
  }
}

The layers of the default Lucy formats allow copying the layer style from a GXY layer to a Lightspeed layer, and the other way round.

Although this section illustrates the power of the ILcyLayerStyle and ILcyLayerStyleProvider concepts, there is no need to use these concepts. You can achieve all the functionality provided by these interfaces and their implementations by working directly against the Lightspeed API. This will have no effect on how Lucy behaves. If you are integrating an existing Lightspeed application that already has a similar mechanism into Lucy, for example, there is no need to convert this mechanism to start using ILcyLayerStyle and ILcyLayerStyleProvider. If you do want to use these concepts and integrate them in your own formats, see Integrate styling in your own format. That section discusses the best approach to provide style support in your format, and what functionality is already available.

Saving/loading a layer style

You can make layer styles persistent with ILcyLayerStyleCodec instances. Such a codec has the necessary methods to encode and decode a layer style to/from a stream. Possible use cases are saving the style of a layer to disk and load it afterwards, or encoding/decoding the layer settings in/from a workspace. This is illustrated in Program: Loading a layer style and applying it to an existing layer..

Program: Loading a layer style and applying it to an existing layer. (from samples/lucy/lightspeed/style/loading/LoadStyleAction)
private void loadStyle(ILspLayer aLayer) throws IOException {
  TLcyCompositeLayerStyleProvider layerStyleProvider = new TLcyCompositeLayerStyleProvider(fLucyEnv);
  if (!layerStyleProvider.canGetStyle(aLayer)) {
    return;
  }
  ILcyLayerStyle style = layerStyleProvider.getStyle(aLayer);

  TLcyCompositeLayerStyleCodec layerStyleCodec = new TLcyCompositeLayerStyleCodec(fLucyEnv);
  if (layerStyleCodec.canDecode(aLayer, style)) {
    try (InputStream inputStream = fInputStreamFactory.createInputStream(fStyleFile)) {
      layerStyleCodec.decode(aLayer, style, inputStream);
    }
  }
}

Currently this functionality only works for Lightspeed layers. The lucy.style sample illustrates how to achieve similar functionality with GXY layers.

Integrate styling in your own format

Lucy provides support for integrating styling in your own format when the layers of your format can use a ILspCustomizableStyler. Such a styler uses the same set of styles as the base to determine the styling of each object, so it is suited for models/layers where the same set of styles is applicable for all the objects, for example. In this case, it is recommended to extend from ALcyLspStyleFormat when you are implementing your own format.

This format provides an ILcyLayerStyleProvider, saves the layer style in the workspace, and registers a ILcyLayerStyleCodec for the layers of your formats. The only thing left to the user of this class is to provide a layer factory by implementing the ALcyLspStyleFormat#createLayerFactoryImpl method.

Program: Example implementation of the ALcyLspStyleFormat#createLayerFactoryImpl method. (from samples/lucy/lightspeed/style/newiconstyle/SHPFormat)
@Override
protected ILspLayerFactory createLayerFactoryImpl() {
  return new ALspSingleLayerFactory() {
    @Override
    public ILspLayer createLayer(ILcdModel aModel) {
      //create a customizable styler containing one icon style
      TLspIconStyle iconStyle = TLspIconStyle.newBuilder().icon(new TLcdImageIcon(CustomizableIconStyleCustomizerPanel.ICON_SOURCE_NAMES.get(0))).build();
      TLspCustomizableStyler styler = new TLspCustomizableStyler(iconStyle);

      //use the TLspShapeLayerBuilder to construct the layer
      TLspShapeLayerBuilder builder = TLspShapeLayerBuilder.newBuilder();
      builder.model(aModel).bodyStyler(TLspPaintState.REGULAR, styler);

      //apply the application wide selection style when available
      ALcyLspStyleRepository styleRepository = ALcyLspStyleRepository.getInstance(getLucyEnv());
      ILspStyler decoratedStyler = styleRepository.createSelectionStyler(styler);
      builder.bodyStyler(TLspPaintState.SELECTED, decoratedStyler);
      //make the layer selectable by default
      builder.selectable(true);

      return builder.build();
    }

    @Override
    public boolean canCreateLayers(ILcdModel aModel) {
      //the format is wrapped with a TLcyLspSafeGuardFormatWrapper so we can just return true
      return true;
    }
  };
}

There are two extensions of the ALcyLspStyleFormat classes that already contain a layer factory: TLcyLspVectorFormat and TLcyLspRasterFormat. These extensions are suitable for simple vector data and raster formats respectively. They both feature a customizeLayer method that allows you to make minor changes to the created layers, such as changing the label or the icon. Consult the Javadoc of these classes to determine which one is the most suited for your format.

If your format cannot use ILspCustomizableStyler instances, it is recommended to extend from ALcyLspStyleFileFormat, ALcyLspGeneralFormat or ALcyLspFormat and provide the style support yourself.

Use ALcyLspStyleFormat or one of its extensions in combination with a layer with a ILspCustomizableStyler whenever it is possible. Your layer style will be fully integrated with the Lucy style mechanism without you having to write extra code.

Integrate SLD styling in your own format

The automatic pick-up functionality for SLD files located next to source data of the model is provided by the ALcyLspStyleFormat class. Extensions of this class, such as TLcyLspVectorFormat, are recommended to provide a layer factory that applies SLD styling when available. An example of such a layer factory is available in the Javadoc of the ALcyLspStyleFormat.createLayerFactoryImpl method.

Application-wide selection style

A common use case consists of having distinct styling for selected objects and unselected objects. Lucy supports storing the selection style settings in one location, and lets you access this location. This allows you to offer consistent styling for selected objects for all layers in the whole application, instead of on a per-layer basis. The default Lucy layer factories make sure that newly created layers respect these selection style settings.

Those settings are stored in the ALcyLspStyleRepository container. By default, this style repository is registered as a service with the back-end by the TLcyLspStyleRepositoryAddOn. The style repository add-on also provides a UI for changing the selection style, and workspace support for storing those settings. See the Lucy user’s guide for more information about the selection style UI.

Adjust the application-wide selection style

At runtime, you can change the application-wide selection style through the UI and the API. In the UI, the action is located under the Map | Style settings…​ menu item. To change the style through the API, you can adjust the ALcyProperties of the TLcyLspStyleRepositoryAddOn, as shown below:

Program: Update the application wide selection style (from samples/lucy/lightspeed/style/loading/LoadStyleAction)
private void updateSelectionStyleColors() {
  TLcyLspStyleRepositoryAddOn styleRepositoryAddOn = fLucyEnv.retrieveAddOnByClass(TLcyLspStyleRepositoryAddOn.class);
  if (styleRepositoryAddOn != null) {
    ALcyProperties preferences = styleRepositoryAddOn.getPreferences();
    preferences.putColor("TLcyLspStyleRepositoryAddOn.selection.fillColor", fSelectionFillColor);
    preferences.putColor("TLcyLspStyleRepositoryAddOn.selection.lineColor", fSelectionLineColor);
    preferences.putColor("TLcyLspStyleRepositoryAddOn.selection.textColor", fSelectionTextColor);
  }
}

You can adjust the default selection colors in the TLcyLspStyleRepositoryAddOn.cfg configuration file of the TLcyLspStyleRepositoryAddOn.

Integrate the application-wide selection style into your own format

To integrate new formats with the application-wide selection mechanism, decorate the ILspStyler for the regular style, and use the decorated version as ILspStyler for the selected objects. Program: Decorating the styler to use the application-wide selection style colors shows you how.

Program: Decorating the styler to use the application-wide selection style colors. (from samples/lucy/lightspeed/style/newiconstyle/SHPFormat)
//apply the application wide selection style when available
ALcyLspStyleRepository styleRepository = ALcyLspStyleRepository.getInstance(getLucyEnv());
ILspStyler decoratedStyler = styleRepository.createSelectionStyler(styler);
builder.bodyStyler(TLspPaintState.SELECTED, decoratedStyler);

The layers of this new format will then pick up any change made to the application-wide selection style.

Application-wide elevation color mapping

When you are representing elevation data, each elevation is typically visualized with a certain color. If you are visualizing multiple elevation data sets, it is recommended to apply the same elevation-to-color mapping to all elevation data sets. Mixing distinct color mappings makes no sense as it would become impossible for the user to learn which color corresponds to which elevation value.

Lucy allows storing such an elevation-value-to-color mapping in one location, and lets you access this location. This allows you to offer consistent styling for all elevation layers in the whole application. The default Lucy elevation layer factories ensure that newly created layers respect this color mapping.

The color mapping is stored in the ALcyLspStyleRepository container. By default, this style repository is registered as a service with the back-end by the TLcyLspStyleRepositoryAddOn. The style repository add-on also provides a UI for changing the elevation color mapping, and workspace support for storing those settings. See the Lucy user’s guide for more information about the color mapping UI.

Adjust the application-wide elevation color mapping

At runtime, you can change the application-wide elevation color mapping through the UI and the API. In the UI, the action is located under the Map | Style settings…​ menu item. To change the color mapping through the API, call the ALcyLspStyleRepository.setElevationColorMap method:

Program: Update the application wide elevation color mapping
ALcyLspStyleRepository repo = lucyEnv.getService( ALcyLspStyleRepository.class );
TLcdColorMap customColorMap = ... ;

repo.setElevationColorMap( customColorMap );

You can adjust the default color mapping in the configuration file TLcyLspStyleRepositoryAddOn.cfg of the TLcyLspStyleRepositoryAddOn.

Integrate application-wide elevation color mapping into your own format

To integrate new formats with the application-wide elevation color mapping mechanism, use an elevation styler created by the style repository. This is illustrated below:

Program: Creating an elevation styler which uses the application-wide elevation color mapping.
ALcyLspStyleRepository repo = lucyEnv.getService( ALcyLspStyleRepository.class );
ILcdModel elevationModel = ...
ILspStyler elevationStyler = repo.createElevationStyler( elevationModel );

The layers of this new format will then pick up any change made to the application-wide elevation color mapping.