Prerequisites
In this tutorial, it is assumed that the reader is familiar with:
-
The fundamental LuciadLightspeed concepts introduced in the Create your first Lightspeed application tutorial:
ILspView
,ILspLayer
,ILcdModel
, andILcdModelDecoder
. -
The fundamentals of vector styling, as introduced in the Introduction to styling vector data tutorial.
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. |
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 theILcdExpression
. 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
:
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:
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 aTLspParameterizedFillStyle
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
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 theTLspParameterizedIconStyle
. Each time theILcdParameter#setValue
method is called, the styling will be updated automatically. -
We couple the
JSlider
with theILcdParameter
so that when the user changes theJSlider
value in the UI, theILcdParameter#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.
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);
});
}
}