Prerequisites
In this tutorial, it is assumed that the reader is familiar with the LuciadLightspeed concepts introduced in the Create your first Lightspeed application tutorial:
-
Lightspeed views:
ILspView
-
Lightspeed layers:
ILspLayer
-
Model and model decoders:
ILcdModel
andILcdModelDecoder
Goal
This tutorial teaches you the basic concepts of styling vector data on a Lightspeed view with the LuciadLightspeed API.
We start off by creating a JFrame
containing a Lightspeed view.
We add 3 different layers to this view:
-
One background layer with country data. This layer will apply the same styling to all objects.
-
One layer with line data, representing some of the major rivers in the USA. This layer will apply a different style to selected objects.
-
One layer with point data, representing some of the major cities in the USA. This layer will apply distinct styles to distinct objects.
You can find the complete, runnable code at the end of this tutorial.
Initial setup
This tutorial starts from an application that shows a Lightspeed view in a JFrame
:
public class BasicBodyStylerTutorial {
final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();
public JFrame createUI() {
JFrame frame = new JFrame("Vector painting 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 BasicBodyStylerTutorial().createUI();
frame.setVisible(true);
});
}
}
Background layer: the same styling for all objects
To make the rest of the code more compact, we start by introducing a utility method to decode the data:
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);
}
}
On the background layer representing all the countries in the world, we want to style each country exactly the same:
-
A white outline
-
A gray, semi-transparent interior
To do so, we create two ALspStyle
instances:
-
A
TLspLineStyle
for the outline: line styles can be used for lines and for the outlines of areas. -
A
TLspFillStyle
for the country interior: fill styles are used to fill areas.
ALspStyle
— and any extension — is a container object grouping a set of styling settings.
It is a definition of how the data must look.
During painting, LuciadLightspeed will convert these styles into OpenGL state.
To pass this styling information to the layer, we use the TLspShapeLayerBuilder
class.
This class serves to create layers for vector data.
In this case, we want to use the same styling for all objects in the layer.
We pass our ALspStyle
objects to the bodyStyles
method of the TLspShapeLayerBuilder
.
private void addWorldLayer() {
//Define the styles we want to use for the layer
//The line style is used for the outline of the polygons, the fill style for the interior
TLspFillStyle fillStyle = TLspFillStyle.newBuilder()
.color(new Color(192, 192, 192, 128))
.build();
TLspLineStyle lineStyle = TLspLineStyle.newBuilder()
.color(Color.white)
.build();
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel(countriesFile))
.bodyStyles(TLspPaintState.REGULAR, lineStyle, fillStyle)
.selectable(false)
.build();
fView.addLayer(layer);
}
As you can see in Program: Creating a vector layer with the same styling for all objects, the creation of a style requires a builder. Once the style has been built, it becomes an immutable object, to prevent threading problems during painting.
Rivers layer: different styling for selected objects
On the layer with river data, we will be setting two styles:
-
If the river is not selected, we visualize it as a blue line.
-
If the river is selected, we use a red line.
private void addRiversLayer() {
//Define the styles we want to use
TLspLineStyle regularLineStyle = TLspLineStyle.newBuilder()
.color(Color.BLUE)
.width(2.0)
.build();
//For the selected line style, we call the asBuilder method on the regularLineStyle
//This creates a builder initialized with all settings of regularLineStyle
//As such, we only need to set the diff between the two styles
TLspLineStyle selectedLineStyle = regularLineStyle.asBuilder()
.color(Color.RED)
.build();
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel("Data/Shp/Usa/rivers.shp"))
.bodyStyles(TLspPaintState.REGULAR, regularLineStyle)
.bodyStyles(TLspPaintState.SELECTED, selectedLineStyle)
.selectable(true)
.build();
fView.addLayer(layer);
}
We specify distinct body styles for the TLspPaintState.REGULAR
and TLspPaintState.SELECTED
.
Program: Creating a layer with different styling for selected objects also illustrates that you can take an existing ALspStyle
instance and call asBuilder
on it to
get a builder that is initialized with all the settings of the existing style object.
That way you don’t need to repeat all the settings, but you only need to define the settings that you want to modify.
Cities layer: distinct icons for distinct cities
If we want to apply distinct styling to distinct objects, we can no longer use the bodyStyles
method of the TLspShapeLayerBuilder
.
Instead, we need to create an ILspStyler
implementation.
An ILspStyler
is responsible for applying styles (ALspStyle
) to objects.
The main method of the styler is:
void style(Collection<?> aObjects, ALspStyleCollector aStyleCollector, TLspContext aContext)
In this method, you receive a collection of objects.
All those objects are part of the ILcdModel
of the visualized layer.
For each of those objects, you need to specify on the ALspStyleCollector
which styles LuciadLightspeed must use to visualize the object.
In this example, we are going to style the cities based on their population size.
-
For cities with large populations, we use a large orange circle with a white outline. We use the
TLcdSymbol
class to create the circle.Other frequently used
ILcdIcon
implementations areTLcdImageIcon
andTLcdSVGIcon
. These can be used to create icons from a PNG/GIF image, or from an SVG image. -
For small cities, we use a similar, but smaller circle.
ILspStyler
that uses a distinct icon for small and large cities
private void addCitiesLayer() {
//Define the styles we want to use
TLcdSymbol largeCityIcon = new TLcdSymbol(FILLED_CIRCLE, 20, Color.WHITE, Color.ORANGE);
TLcdSymbol smallCityIcon = new TLcdSymbol(FILLED_CIRCLE, 10, Color.WHITE, Color.ORANGE);
TLspIconStyle largeCityStyle = TLspIconStyle.newBuilder()
.icon(largeCityIcon)
.build();
TLspIconStyle smallCityStyle = TLspIconStyle.newBuilder()
.icon(smallCityIcon)
.build();
//Create the ILspStyler
//We start from the abstract class ALspStyler which already implements all the boiler plate code for us
ILspStyler styler = new ALspStyler() {
@Override
public void style(Collection<?> aObjects, ALspStyleCollector aStyleCollector, TLspContext aContext) {
for (Object city : aObjects) {
if (isLargeCity(city)) {
aStyleCollector.object(city).style(largeCityStyle).submit();
} else {
aStyleCollector.object(city).style(smallCityStyle).submit();
}
}
}
};
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
.bodyStyler(TLspPaintState.REGULAR, styler)
.build();
fView.addLayer(layer);
}
private static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
To decide whether a city is a large or small city, we check the |
The full code
import static com.luciad.gui.TLcdSymbol.FILLED_CIRCLE;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.io.IOException;
import java.util.Collection;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.gui.TLcdSymbol;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.TLcdCompositeModelDecoder;
import com.luciad.util.service.TLcdServiceLoader;
import com.luciad.view.lightspeed.ILspAWTView;
import com.luciad.view.lightspeed.TLspContext;
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.TLspFillStyle;
import com.luciad.view.lightspeed.style.TLspIconStyle;
import com.luciad.view.lightspeed.style.TLspLineStyle;
import com.luciad.view.lightspeed.style.styler.ALspStyleCollector;
import com.luciad.view.lightspeed.style.styler.ALspStyler;
import com.luciad.view.lightspeed.style.styler.ILspStyler;
import com.luciad.view.swing.TLcdLayerTree;
public class BasicBodyStylerTutorial {
final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();
public JFrame createUI() {
JFrame frame = new JFrame("Vector painting tutorial");
addWorldLayer();
addRiversLayer();
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 addWorldLayer() {
//Define the styles we want to use for the layer
//The line style is used for the outline of the polygons, the fill style for the interior
TLspFillStyle fillStyle = TLspFillStyle.newBuilder()
.color(new Color(192, 192, 192, 128))
.build();
TLspLineStyle lineStyle = TLspLineStyle.newBuilder()
.color(Color.white)
.build();
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel(countriesFile))
.bodyStyles(TLspPaintState.REGULAR, lineStyle, fillStyle)
.selectable(false)
.build();
fView.addLayer(layer);
}
private void addRiversLayer() {
//Define the styles we want to use
TLspLineStyle regularLineStyle = TLspLineStyle.newBuilder()
.color(Color.BLUE)
.width(2.0)
.build();
//For the selected line style, we call the asBuilder method on the regularLineStyle
//This creates a builder initialized with all settings of regularLineStyle
//As such, we only need to set the diff between the two styles
TLspLineStyle selectedLineStyle = regularLineStyle.asBuilder()
.color(Color.RED)
.build();
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel("Data/Shp/Usa/rivers.shp"))
.bodyStyles(TLspPaintState.REGULAR, regularLineStyle)
.bodyStyles(TLspPaintState.SELECTED, selectedLineStyle)
.selectable(true)
.build();
fView.addLayer(layer);
}
private void addCitiesLayer() {
//Define the styles we want to use
TLcdSymbol largeCityIcon = new TLcdSymbol(FILLED_CIRCLE, 20, Color.WHITE, Color.ORANGE);
TLcdSymbol smallCityIcon = new TLcdSymbol(FILLED_CIRCLE, 10, Color.WHITE, Color.ORANGE);
TLspIconStyle largeCityStyle = TLspIconStyle.newBuilder()
.icon(largeCityIcon)
.build();
TLspIconStyle smallCityStyle = TLspIconStyle.newBuilder()
.icon(smallCityIcon)
.build();
//Create the ILspStyler
//We start from the abstract class ALspStyler which already implements all the boiler plate code for us
ILspStyler styler = new ALspStyler() {
@Override
public void style(Collection<?> aObjects, ALspStyleCollector aStyleCollector, TLspContext aContext) {
for (Object city : aObjects) {
if (isLargeCity(city)) {
aStyleCollector.object(city).style(largeCityStyle).submit();
} else {
aStyleCollector.object(city).style(smallCityStyle).submit();
}
}
}
};
TLspLayer layer =
TLspShapeLayerBuilder.newBuilder()
.model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
.bodyStyler(TLspPaintState.REGULAR, styler)
.build();
fView.addLayer(layer);
}
private static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new BasicBodyStylerTutorial().createUI();
frame.setVisible(true);
});
}
}