Prerequisites
In this tutorial, it is assumed that the reader is familiar with the LuciadLightspeed concepts introduced in the Create your first 2D map application tutorial:
-
GXY views:
ILcdGXYView
-
GXY layers:
ILcdGXYLayer
-
Model and model decoders:
ILcdModel
andILcdModelDecoder
Goal
This tutorial teaches you the basic concepts of the LuciadLightspeed API for styling vector data on a GXY view.
This tutorial creates a JFrame
containing a GXY view.
We will add 3 layers to this view:
-
One background layer with country data. This layer will use the same styling for all objects.
-
One layer with line data, representing some of the major rivers in the USA. This layer will apply a distinct style for selected objects.
-
One layer with point data, representing some of the major cities in the USA. This layer will apply a distinct style based on object properties.

You can find the complete, runnable code at the end of this tutorial.
Initial setup
This tutorial starts from an application that shows a GXY view in a JFrame
:
public class BasicPainterProviderTutorial {
final TLcdMapJPanel fView = new TLcdMapJPanel();
public JFrame createUI() {
JFrame frame = new JFrame("Vector painting tutorial");
frame.getContentPane().add(fView, 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 BasicPainterProviderTutorial().createUI();
frame.setVisible(true);
});
}
}
Background layer: same styling for all objects
To make the rest of the code a bit more compact, we start by introducing a utility method for decoding 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 shape exactly the same:
-
A white outline
-
A gray, semi-transparent interior
We do that by creating two ILcdGXYPainterStyle
instances.
It is the responsibility of the painter style to correctly configure the java.awt.Graphics
object which the ILcdGXYPainter
uses for painting.
In this case, we only want to change the color so we use the TLcdGXYPainterColorStyle
implementation.
We set that painter style on the TLcdGXYShapePainter
instance, which in turn is installed on the TLcdGXYLayer
:
private void addWorldLayer() {
//Define the styles we want to use for painting the layer
//The line style is used for the outline of the polygons, the fill style for the interior
ILcdGXYPainterStyle fillStyle = new TLcdGXYPainterColorStyle(new Color(192, 192, 192, 128));
ILcdGXYPainterStyle lineStyle = new TLcdGXYPainterColorStyle(Color.WHITE);
//Create a painter and configure the styles on that painter
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setFillStyle(fillStyle);
painter.setLineStyle(lineStyle);
//Indicate that the painter should paint both outlines and fills
painter.setMode(ALcdGXYAreaPainter.OUTLINED_FILLED);
//Create the layer
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel(countriesFile));
//Disable selection for this layer, as it only serves as background data
layer.setSelectableSupported(false);
//Install the painter on the layer
layer.setGXYPainterProvider(painter);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(layer));
}

Rivers layer: different styling for selected objects
For the layer with river data, we will be using 2 distinct styles:
-
If the river is not selected, we visualize it with a blue, single-pixel-wide line.
-
If the river is selected, we use a red line of 2 pixels wide.
private void addRiversLayer() {
//Define the styles we want to use
TLcdStrokeLineStyle lineStyle =
TLcdStrokeLineStyle.newBuilder().color(Color.BLUE)
.selectionColor(Color.RED)
.lineWidth(2f)
.build();
//Create a painter and configure the styles on that painter
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setLineStyle(lineStyle);
//Create the layer
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/rivers.shp"));
//Enable selection for the layer
layer.setSelectable(true);
//Install the painter on the layer
layer.setGXYPainterProvider(painter);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(layer));
}
This time, we use a TLcdStrokeLineStyle
for our ILcdGXYPainterStyle
implementation.
That style allows us to specify distinct colors for regular lines and selected lines among other things,
and is constructed using a TLcdStrokeLineStyleBuilder
.

Cities layer: different icons for different cities
As you can see in the snippets, we don’t install an ILcdGXYPainter
on the layer, but rather an ILcdGXYPainterProvider
.
It is the responsibility of the painter provider to return a correctly configured painter for each object that must be painted.
For reasons of convenience, the majority of the We used this in the previous sections to install the painter as a painter provider on the layer. |
On the cities layer, we want to style the cities based on their population.
-
For big cities, we use a large orange circle with a white outline. The circle is created using the
TLcdSymbol
class.Other frequently used
ILcdIcon
implementations areTLcdImageIcon
andTLcdSVGIcon
. You can use those to create icons from PNG/GIF images or SVG images. -
For small cities, we use a similar but smaller circle.
This means we will use distinct styles for small and big cities. Therefore, we implement an ILcdGXYPainterProvider
:
-
If it receives a request for for a painter for a big city, our implementation should return a painter configured to use a large icon to visualize the city.
-
If it receives a request for a painter for a small city, the returned painter should use a small icon.
When we select a city, we want to use the same icon but we want to surround the icon with a rectangle to indicate selection.
We create this icon using the TLcdBoxIcon
class, and configure it on the painter.
ILcdGXYPainterProvider
which uses a different painter 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);
//Create a painter provider which selects the correct icon based on the population of the city
ILcdGXYPainterProvider painterProvider = new CityPainterProvider(largeCityIcon, smallCityIcon);
//Create the layer
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/city_125.shp"));
//Install the painter provider on the layer
layer.setGXYPainterProvider(painterProvider);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(layer));
}
/**
* Add a border around an icon to indicate the icon is selected
*/
private static ILcdIcon wrapWithSelectionRectangle(ILcdIcon aIcon) {
return TLcdBoxIcon.newBuilder()
.icon(aIcon)
.frameColor(Color.RED)
.frame(true)
.filled(false)
.frameLineWidth(1)
.padding(2)
.build();
}
private static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
private static class CityPainterProvider implements ILcdGXYPainterProvider<TLcdGXYIconPainter> {
private TLcdGXYIconPainter fLargeCityPainter = new TLcdGXYIconPainter();
private TLcdGXYIconPainter fSmallCityPainter = new TLcdGXYIconPainter();
CityPainterProvider(ILcdIcon aLargeCityIcon, ILcdIcon aSmallCityIcon) {
//Configure the painters with the regular and selected icon
fLargeCityPainter.setIcon(aLargeCityIcon);
fLargeCityPainter.setSelectionIcon(wrapWithSelectionRectangle(aLargeCityIcon));
fSmallCityPainter.setIcon(aSmallCityIcon);
fSmallCityPainter.setSelectionIcon(wrapWithSelectionRectangle(aSmallCityIcon));
}
@Override
public TLcdGXYIconPainter getGXYPainter(Object aObject) {
//The painter provider needs to return the correct painter,
//depending on the object
if (isLargeCity(aObject)) {
fLargeCityPainter.setObject(aObject);
return fLargeCityPainter;
} else {
fSmallCityPainter.setObject(aObject);
return fSmallCityPainter;
}
}
@Override
public Object clone() {
try {
CityPainterProvider clone = (CityPainterProvider) super.clone();
clone.fLargeCityPainter = (TLcdGXYIconPainter) fLargeCityPainter.clone();
clone.fSmallCityPainter = (TLcdGXYIconPainter) fSmallCityPainter.clone();
return clone;
} catch (CloneNotSupportedException aE) {
throw new RuntimeException(aE);
}
}
}
To decide whether a city is a big 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 javax.swing.JFrame;
import javax.swing.WindowConstants;
import com.luciad.datamodel.ILcdDataObject;
import com.luciad.gui.ILcdIcon;
import com.luciad.gui.TLcdBoxIcon;
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.gxy.ILcdGXYPainterProvider;
import com.luciad.view.gxy.ILcdGXYPainterStyle;
import com.luciad.view.gxy.TLcdGXYLayer;
import com.luciad.view.gxy.TLcdGXYPainterColorStyle;
import com.luciad.view.gxy.TLcdGXYShapePainter;
import com.luciad.view.gxy.TLcdStrokeLineStyle;
import com.luciad.view.gxy.asynchronous.ILcdGXYAsynchronousLayerWrapper;
import com.luciad.view.gxy.painter.ALcdGXYAreaPainter;
import com.luciad.view.gxy.painter.TLcdGXYIconPainter;
import com.luciad.view.map.TLcdMapJPanel;
import com.luciad.view.swing.TLcdLayerTree;
public class BasicPainterProviderTutorial {
final TLcdMapJPanel fView = new TLcdMapJPanel();
public JFrame createUI() {
JFrame frame = new JFrame("Vector painting tutorial");
addWorldLayer();
addRiversLayer();
addCitiesLayer();
frame.getContentPane().add(fView, 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 painting the layer
//The line style is used for the outline of the polygons, the fill style for the interior
ILcdGXYPainterStyle fillStyle = new TLcdGXYPainterColorStyle(new Color(192, 192, 192, 128));
ILcdGXYPainterStyle lineStyle = new TLcdGXYPainterColorStyle(Color.WHITE);
//Create a painter and configure the styles on that painter
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setFillStyle(fillStyle);
painter.setLineStyle(lineStyle);
//Indicate that the painter should paint both outlines and fills
painter.setMode(ALcdGXYAreaPainter.OUTLINED_FILLED);
//Create the layer
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel(countriesFile));
//Disable selection for this layer, as it only serves as background data
layer.setSelectableSupported(false);
//Install the painter on the layer
layer.setGXYPainterProvider(painter);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(layer));
}
private void addRiversLayer() {
//Define the styles we want to use
TLcdStrokeLineStyle lineStyle =
TLcdStrokeLineStyle.newBuilder().color(Color.BLUE)
.selectionColor(Color.RED)
.lineWidth(2f)
.build();
//Create a painter and configure the styles on that painter
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setLineStyle(lineStyle);
//Create the layer
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/rivers.shp"));
//Enable selection for the layer
layer.setSelectable(true);
//Install the painter on the layer
layer.setGXYPainterProvider(painter);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(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);
//Create a painter provider which selects the correct icon based on the population of the city
ILcdGXYPainterProvider painterProvider = new CityPainterProvider(largeCityIcon, smallCityIcon);
//Create the layer
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/city_125.shp"));
//Install the painter provider on the layer
layer.setGXYPainterProvider(painterProvider);
//Add an asynchronous version of the layer to the view
//This ensures that the view remains responsive while the layer is being painted
fView.addGXYLayer(ILcdGXYAsynchronousLayerWrapper.create(layer));
}
/**
* Add a border around an icon to indicate the icon is selected
*/
private static ILcdIcon wrapWithSelectionRectangle(ILcdIcon aIcon) {
return TLcdBoxIcon.newBuilder()
.icon(aIcon)
.frameColor(Color.RED)
.frame(true)
.filled(false)
.frameLineWidth(1)
.padding(2)
.build();
}
private static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
private static class CityPainterProvider implements ILcdGXYPainterProvider<TLcdGXYIconPainter> {
private TLcdGXYIconPainter fLargeCityPainter = new TLcdGXYIconPainter();
private TLcdGXYIconPainter fSmallCityPainter = new TLcdGXYIconPainter();
CityPainterProvider(ILcdIcon aLargeCityIcon, ILcdIcon aSmallCityIcon) {
//Configure the painters with the regular and selected icon
fLargeCityPainter.setIcon(aLargeCityIcon);
fLargeCityPainter.setSelectionIcon(wrapWithSelectionRectangle(aLargeCityIcon));
fSmallCityPainter.setIcon(aSmallCityIcon);
fSmallCityPainter.setSelectionIcon(wrapWithSelectionRectangle(aSmallCityIcon));
}
@Override
public TLcdGXYIconPainter getGXYPainter(Object aObject) {
//The painter provider needs to return the correct painter,
//depending on the object
if (isLargeCity(aObject)) {
fLargeCityPainter.setObject(aObject);
return fLargeCityPainter;
} else {
fSmallCityPainter.setObject(aObject);
return fSmallCityPainter;
}
}
@Override
public Object clone() {
try {
CityPainterProvider clone = (CityPainterProvider) super.clone();
clone.fLargeCityPainter = (TLcdGXYIconPainter) fLargeCityPainter.clone();
clone.fSmallCityPainter = (TLcdGXYIconPainter) fSmallCityPainter.clone();
return clone;
} catch (CloneNotSupportedException aE) {
throw new RuntimeException(aE);
}
}
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new BasicPainterProviderTutorial().createUI();
frame.setVisible(true);
});
}
}