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:
ILcdModelandILcdModelDecoder
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
TLspLineStylefor the outline: line styles can be used for lines and for the outlines of areas. -
A
TLspFillStylefor 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
TLcdSymbolclass to create the circle.Other frequently used
ILcdIconimplementations areTLcdImageIconandTLcdSVGIcon. 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);
});
}
}