Prerequisites

In this tutorial, it is assumed that the reader is familiar with:

Goal

This tutorial teaches you the basic concepts of the LuciadLightspeed API for expressions and parameterized styling. The API allows you to base the styling of an object on the values of its properties, in a hardware-accelerated manner. It also allows you to filter the data and change the styling instantly, without re-processing the data.

This tutorial creates a JFrame containing a Lightspeed view. We will add 2 different layers to this view:

  • One background layer with country data. The objects in this layer will be styled based on their properties.

  • One layer with point data, representing the major cities in the USA. For this layer, we will use a JSlider to adjust the icon size.

You can also use the regular styling API to style objects based on their properties. The regular styling API is introduced in Introduction to styling vector data.

The Differences between parameterized styling and regular styling article explains the differences between the two styling approaches.

lls parameterizedstyling result
Figure 1. The finished application

The complete, runnable code of this tutorial can be found at the end.

Introduction to expressions

In regular styles like TLspIconStyle, TLspLineStyle, …​ , you create a definition of a specific style. For example a blue line of 10 pixels wide.

This is not the case when working with parameterized styles. Here you use an ILcdExpression to define the styling.

An ILcdExpression can be seen as a little program which gets evaluated for each domain object. The result of the evaluation of the ILcdExpression is the actual styling of the object.

For example, if you have data of all countries in the world and you want to style:

  • The countries with a population larger that 50 million as red polygons

  • All other countries as blue polygons

Using regular styling, you would need to create two TLspFillStyle instances (one for the red style and one for the blue style). In your ILspStyler, you would retrieve the countries which a sufficiently high population and submit them with the red style. All other countries would be submitted using the blue style.

When working with parameterized styles and ILcdExpression instances, you would create only a single style. The style would be configured with an expression which contains the logic of checking the population, and selecting the correct color.

The ILcdExpression API provides the following constructions:

  • Boolean operations: AND, OR, NOT, …​

  • Numerical operations: find the minimum or maximum, addition, multiplication, …​

  • Control operations: if-else, switch, …​

  • Trigonometric operations: sinus, cosinus, …​

  • Vector operations: cross product, dot product, …​

  • Distance operations: distance between two points, view position, …​

  • Type conversion operations

  • Value expressions: using a constant value

The TLcdExpressionFactory javadoc contains a complete list.

There are also two value expressions available which allow to dynamically retrieve a value:

  • Attributes: an attribute expression is used to retrieve the value of a property of an ILcdDataObject, and use that value in the ILcdExpression. For example when you want to style cities based on their population, you would use an attribute expression to retrieve the population property.

  • Parameters: often, you want to define an expression based on a value that changes many times. For example, you want to visualize only the objects within a time range that is constantly moving, or you want to color objects based on a height threshold backed by a slider UI component.

    To deal with changing values, the parameterized styling API offers parameters. These allow you to build your expression once, and vary the parameters later. Each time the value of a parameter is changed, the visualization on the map will be updated immediately.

Initial setup

This tutorial starts from an application which shows a Lightspeed view in a JFrame:

Program: Basic setup for this tutorial
public class ExpressionStylingTutorial {
  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();

  public JFrame createUI() {
    JFrame frame = new JFrame("Parameterized styling tutorial");

    frame.getContentPane().add(fView.getHostComponent(), BorderLayout.CENTER);
    frame.getContentPane().add(new TLcdLayerTree(fView), BorderLayout.EAST);

    frame.setSize(800, 600);
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    return frame;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame frame = new ExpressionStylingTutorial().createUI();
      frame.setVisible(true);
    });
  }
}

To improve the readability of the code snippets, we use a static import for TLcdExpressionFactory:

import static com.luciad.util.expression.TLcdExpressionFactory.*;

That factory class is used for the creation of all ILcdExpression objects.

We also use a utility method to decode the SHP data that we will use in this tutorial:

Program: Utility method to decode SHP data into a model
private ILcdModel decodeSHPModel(String aSourceName) {
  try {
    TLcdCompositeModelDecoder modelDecoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
    return modelDecoder.decode(aSourceName);
  } catch (IOException e) {
    //In a real application, we would need proper error handling
    throw new RuntimeException(e);
  }
}

Background layer: styling based on object properties

As background layer we use country data of the whole world. For each country, we will:

  • Use a constant, white outline style. This is done with a regular TLspLineStyle.

  • The fill color must depend on the population of the country. We create an ILcdExpression<Color> for this which we put on a TLspParameterizedFillStyle instance:

    • Retrieving the population is done with an attribute expression.

    • Mapping the population value to a color is done with a mapping expression and a TLcdColorMap.

The result is that, during painting, for each country the population will be requested. That population value is used to retrieve the correct color from the color mapping.

private void addCountriesLayer() {
  String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
  ILcdModel model = decodeSHPModel(countriesFile);
  //Retrieve the TLcdDataType for all domain objects
  //We can use the following construct because we know that all domain objects in a SHP model have the same data type
  TLcdDataType dataType =
      ((ILcdDataModelDescriptor) model.getModelDescriptor())
          .getModelElementTypes()
          .iterator()
          .next();

  //Use an attribute expression to retrieve the population
  //The population is stored in the POP_EST property
  ILcdExpression<Double> populationAttribute = attribute(Double.class, dataType.getProperty("POP_EST"));
  //Define which color we want to use for each population value
  //We use a TLcdColorMap for this, which makes it easy to assign a color to a certain numeric range
  TLcdColorMap colorMap = new TLcdColorMap(new TLcdInterval(0, 2e9),
                                           //population interval values
                                           new double[]{1e5, 1e6, 1e7, 1e8, 1e9},
                                           //the corresponding colors for each interval
                                           new Color[]{new Color(179, 226, 205),
                                                       new Color(253, 205, 172),
                                                       new Color(203, 213, 232),
                                                       new Color(244, 202, 228),
                                                       new Color(230, 245, 201)});
  ILcdExpression<Color> colorExpression = mixmap(toFloat(populationAttribute), colorMap);

  //Set the color expression on the parameterized fill style
  TLspParameterizedFillStyle fillStyle = TLspParameterizedFillStyle.newBuilder()
                                                                   .color(colorExpression)
                                                                   .build();

  //Use regular styling for the outline of the country
  TLspLineStyle outlineStyle = TLspLineStyle.newBuilder().color(Color.WHITE).build();

  //Create the layer
  TLspLayer worldLayer = TLspShapeLayerBuilder.newBuilder()
                                              .model(model)
                                              .bodyStyles(TLspPaintState.REGULAR, fillStyle, outlineStyle)
                                              .selectable(false)
                                              .build();
  fView.addLayer(worldLayer);
}

The result is shown in the following screenshot

World layer with countries colored based on their population

lls parameterizedstyling worldlayer

Cities layer: use parameter to control the icon size

For the cities layer, we add a JSlider to the UI which controls the scale of the icon. This requires the introduction of a parameter expression: ILcdParameter.

  • The ILcdParameter is set on the TLspParameterizedIconStyle. Each time the ILcdParameter#setValue method is called, the styling will be updated automatically.

  • We couple the JSlider with the ILcdParameter so that when the user changes the JSlider value in the UI, the ILcdParameter#setValue method is called.

First, we create the layer and the ILcdParameter which controls the scale of the icon.

private void addCitiesLayer() {
  //First create a parameter for the scale of the icon
  ILcdParameter<Float> iconScale = parameter("iconScale", 1f);

  ILcdIcon icon = new TLcdSymbol(TLcdSymbol.FILLED_TRIANGLE, 16, Color.BLACK, Color.ORANGE);
  TLspParameterizedIconStyle iconStyle =
      TLspParameterizedIconStyle.newBuilder()
                                .icon(constant(icon))//all cities are visualized using the same icon
                                .scale(iconScale)//use the parameter to control the scale
                                .build();

  TLspLayer layer = TLspShapeLayerBuilder.newBuilder()
                                         .model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
                                         .bodyStyles(TLspPaintState.REGULAR, iconStyle)
                                         .selectable(false)
                                         .build();
  fView.addLayer(layer);
}

Then, we create a JSlider and link it with that ILcdParameter.

JSlider slider = new JSlider(1, 20);
slider.setValue((int) (iconScale.getValue() * 10));
//Update the parameter each time the user changes the value on the slider
slider.addChangeListener(e -> iconScale.setValue(slider.getValue() / 10f));
//Add the slider to the UI
fView.getOverlayComponent().add(slider, TLcdOverlayLayout.Location.SOUTH);

When you now change the slider in the UI, the styling on the map is updated immediately.

lls parameterizedstyling citieslayer
Figure 2. Adjusting the slider updates the icon scale immediately

Full code

Program: The full code
import static com.luciad.util.expression.TLcdExpressionFactory.*;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.io.IOException;

import javax.swing.JFrame;
import javax.swing.JSlider;
import javax.swing.WindowConstants;

import com.luciad.datamodel.TLcdDataType;
import com.luciad.gui.ILcdIcon;
import com.luciad.gui.TLcdSymbol;
import com.luciad.gui.swing.TLcdOverlayLayout;
import com.luciad.model.ILcdDataModelDescriptor;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.TLcdCompositeModelDecoder;
import com.luciad.util.TLcdColorMap;
import com.luciad.util.TLcdInterval;
import com.luciad.util.expression.ILcdExpression;
import com.luciad.util.expression.ILcdParameter;
import com.luciad.util.service.TLcdServiceLoader;
import com.luciad.view.lightspeed.ILspAWTView;
import com.luciad.view.lightspeed.TLspViewBuilder;
import com.luciad.view.lightspeed.layer.TLspLayer;
import com.luciad.view.lightspeed.layer.TLspPaintState;
import com.luciad.view.lightspeed.layer.shape.TLspShapeLayerBuilder;
import com.luciad.view.lightspeed.style.TLspLineStyle;
import com.luciad.view.lightspeed.style.TLspParameterizedFillStyle;
import com.luciad.view.lightspeed.style.TLspParameterizedIconStyle;
import com.luciad.view.swing.TLcdLayerTree;

public class ExpressionStylingTutorial {
  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();

  public JFrame createUI() {
    JFrame frame = new JFrame("Parameterized styling tutorial");


    addCountriesLayer();
    addCitiesLayer();

    frame.getContentPane().add(fView.getHostComponent(), BorderLayout.CENTER);
    frame.getContentPane().add(new TLcdLayerTree(fView), BorderLayout.EAST);

    frame.setSize(800, 600);
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    return frame;
  }

  private ILcdModel decodeSHPModel(String aSourceName) {
    try {
      TLcdCompositeModelDecoder modelDecoder =
          new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
      return modelDecoder.decode(aSourceName);
    } catch (IOException e) {
      //In a real application, we would need proper error handling
      throw new RuntimeException(e);
    }
  }

  private void addCountriesLayer() {
    String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
    ILcdModel model = decodeSHPModel(countriesFile);
    //Retrieve the TLcdDataType for all domain objects
    //We can use the following construct because we know that all domain objects in a SHP model have the same data type
    TLcdDataType dataType =
        ((ILcdDataModelDescriptor) model.getModelDescriptor())
            .getModelElementTypes()
            .iterator()
            .next();

    //Use an attribute expression to retrieve the population
    //The population is stored in the POP_EST property
    ILcdExpression<Double> populationAttribute = attribute(Double.class, dataType.getProperty("POP_EST"));
    //Define which color we want to use for each population value
    //We use a TLcdColorMap for this, which makes it easy to assign a color to a certain numeric range
    TLcdColorMap colorMap = new TLcdColorMap(new TLcdInterval(0, 2e9),
                                             //population interval values
                                             new double[]{1e5, 1e6, 1e7, 1e8, 1e9},
                                             //the corresponding colors for each interval
                                             new Color[]{new Color(179, 226, 205),
                                                         new Color(253, 205, 172),
                                                         new Color(203, 213, 232),
                                                         new Color(244, 202, 228),
                                                         new Color(230, 245, 201)});
    ILcdExpression<Color> colorExpression = mixmap(toFloat(populationAttribute), colorMap);

    //Set the color expression on the parameterized fill style
    TLspParameterizedFillStyle fillStyle = TLspParameterizedFillStyle.newBuilder()
                                                                     .color(colorExpression)
                                                                     .build();

    //Use regular styling for the outline of the country
    TLspLineStyle outlineStyle = TLspLineStyle.newBuilder().color(Color.WHITE).build();

    //Create the layer
    TLspLayer worldLayer = TLspShapeLayerBuilder.newBuilder()
                                                .model(model)
                                                .bodyStyles(TLspPaintState.REGULAR, fillStyle, outlineStyle)
                                                .selectable(false)
                                                .build();
    fView.addLayer(worldLayer);
  }

  private void addCitiesLayer() {
    //First create a parameter for the scale of the icon
    ILcdParameter<Float> iconScale = parameter("iconScale", 1f);

    ILcdIcon icon = new TLcdSymbol(TLcdSymbol.FILLED_TRIANGLE, 16, Color.BLACK, Color.ORANGE);
    TLspParameterizedIconStyle iconStyle =
        TLspParameterizedIconStyle.newBuilder()
                                  .icon(constant(icon))//all cities are visualized using the same icon
                                  .scale(iconScale)//use the parameter to control the scale
                                  .build();

    TLspLayer layer = TLspShapeLayerBuilder.newBuilder()
                                           .model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
                                           .bodyStyles(TLspPaintState.REGULAR, iconStyle)
                                           .selectable(false)
                                           .build();
    fView.addLayer(layer);

    JSlider slider = new JSlider(1, 20);
    slider.setValue((int) (iconScale.getValue() * 10));
    //Update the parameter each time the user changes the value on the slider
    slider.addChangeListener(e -> iconScale.setValue(slider.getValue() / 10f));
    //Add the slider to the UI
    fView.getOverlayComponent().add(slider, TLcdOverlayLayout.Location.SOUTH);

  }

  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame frame = new ExpressionStylingTutorial().createUI();
      frame.setVisible(true);
    });
  }
}