Goal
This tutorial teaches you the basic concepts of the LuciadLightspeed API for adding labels to 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 display the country name as label.
-
One layer with line data, representing some of the major rivers in the USA. This layer will use distinct labels for selected and non-selected rivers. The labels will also be pointed in a specific direction so that they follow the orientation of the river.
-
One layer with point data, representing some of the major cities in the USA. This layer will use distinct styles for objects depending on their properties.
The complete, runnable code of this tutorial can be found at the end.
The LuciadLightspeed API for visualizing and styling labels is almost identical to the API used for styling the domain object itself. As a result, this tutorial is very similar to the Visualizing vector data tutorial:
|
Initial setup
This tutorial starts from an application which shows a GXY view in a JFrame
:
class BasicLabelingGXYTutorial {
final TLcdMapJPanel fView = new TLcdMapJPanel();
public JFrame createUI() {
JFrame frame = new JFrame("Vector labeling 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 BasicLabelingGXYTutorial().createUI();
frame.setVisible(true);
});
}
}
Background layer: use a fixed property as label
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 display the country name for each domain object.
Because our domain objects are ILcdDataObject
instances, we can use the TLcdGXYDataObjectLabelPainter
for this.
On this label painter, we configure:
-
Which properties we want to use as contents for the label
-
The styling for the label
We install the label painter on the TLcdGXYLayer
with the setGXYLabelPainterProvider
method.
private void addWorldLayer() {
TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
//Specify that the label should contain the ADMIN property
labelPainter.setExpressions("ADMIN");
//Specify the visual appearance of the label:
labelPainter.setFont(Font.decode("SansSerif-PLAIN-12"));
labelPainter.setForeground(Color.BLACK);
//Add a halo to the text to improve readability
labelPainter.setHaloColor(Color.WHITE);
labelPainter.setHaloEnabled(true);
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel(countriesFile));
layer.setGXYPainterProvider(createWorldBodyPainter());
//Disable selection for this layer, as it only serves as background data
layer.setSelectableSupported(false);
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainter);
// Make sure the labels are nicely placed inside the countries.
ILcdGXYLabelingAlgorithm labelingAlgorithm = new TLcdGXYInPathLabelingAlgorithm();
layer.setGXYLabelingAlgorithmProvider(l -> labelingAlgorithm);
//Enable labels
layer.setLabeled(true);
//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));
}
To be able to focus on the labeling-related code, the short snippets in this tutorial do not include the code for the body styling. The body styling code is identical to the code used in the Visualizing vector data tutorial. The full code at the end of this tutorial does contain the code for the body styling. |
When the user zooms out on the map, the labels automatically declutter to prevent overlap:
Rivers layer: labels along a line
For the layer with river data, we have two requirements:
-
The labels must be oriented in the same direction as the rivers.
-
Unselected rivers need a blue label, and selected rivers a red label.
We can set up the distinct styling for selected and regular objects by configuring different selection colors on the TLcdGXYDataObjectLabelPainter
.
private void addRiversLayer() {
TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
//Specify that the label should contain the NAME property
labelPainter.setExpressions("NAME");
//Specify the styling for regular labels
labelPainter.setForeground(Color.BLUE);
labelPainter.setHaloColor(Color.WHITE);
labelPainter.setHaloEnabled(true);
// and for selected labels
labelPainter.setSelectionColor(Color.RED);
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/rivers.shp"));
layer.setGXYPainterProvider(createRiversBodyPainter());
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainter);
//Make sure the labels follow the rivers
TLcdGXYOnPathLabelingAlgorithm labelingAlgorithm = new TLcdGXYOnPathLabelingAlgorithm();
layer.setGXYLabelingAlgorithmProvider(l -> labelingAlgorithm);
//Enable labels
layer.setLabeled(true);
//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));
}
We can influence the label orientation through an ILcdGXYLabelingAlgorithm
.
It is the responsibility of this algorithm to position the label.
In this case, we want labels placed in such a way that they are oriented in the same direction as our line domain objects,
the rivers.
We use the TLcdGXYOnPathLabelingAlgorithm
to take care of that.
Cities layer: distinct labels for distinct cities at custom locations
If we want to use distinct label styling for objects, we need to create our own ILcdGXYLabelPainterProvider
instance.
This is identical to what we did for the visualization of the bodies.
In this example, we are going to style the cities based on their population.
-
For big cities, the label shows the name of the city and the population.
-
For small cities, the label only shows the name of the city.
For big cities, we also:
-
Force the label to a location north of the city, through the
setPositionList
method on theTLcdGXYDataObjectLabelPainter
-
Connect the label and city with a so-called pin, through the
setWithPin
method on theTLcdGXYDataObjectLabelPainter
. A pin is the terminology used in the LuciadLightspeed API for the line between the label and the domain object.
ILcdGXYLabelPainterProvider
which uses a different label for small and large cities
private void addCitiesLayer() {
//Define the label painters we want to use
TLcdGXYDataObjectLabelPainter largeCityLabelPainter = new TLcdGXYDataObjectLabelPainter();
largeCityLabelPainter.setExpressions("CITY", "TOT_POP");
largeCityLabelPainter.setShiftLabelPosition(12);
largeCityLabelPainter.setWithPin(true); // Use a pin because the label is moved a bit further from the icon
largeCityLabelPainter.setPositionList(new int[] {TLcdGXYDataObjectLabelPainter.NORTH});
TLcdGXYDataObjectLabelPainter smallCityLabelPainter = new TLcdGXYDataObjectLabelPainter();
smallCityLabelPainter.setExpressions("CITY");
//Create a label painter provider which selects the correct label painter based on the population of the city
ILcdGXYLabelPainterProvider labelPainterProvider = new LabelPainterProvider(largeCityLabelPainter, smallCityLabelPainter);
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/city_125.shp"));
layer.setGXYPainterProvider(createCitiesBodyPainter());
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainterProvider);
//Enable labels
layer.setLabeled(true);
//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 static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
private static class LabelPainterProvider implements ILcdGXYLabelPainterProvider {
private ILcdGXYLabelPainter fLargeCitiesLabelPainter;
private ILcdGXYLabelPainter fSmallCitiesLabelPainter;
private LabelPainterProvider(ILcdGXYLabelPainter aLargeCitiesLabelPainter, ILcdGXYLabelPainter aSmallCitiesLabelPainter) {
fLargeCitiesLabelPainter = aLargeCitiesLabelPainter;
fSmallCitiesLabelPainter = aSmallCitiesLabelPainter;
}
@Override
public ILcdGXYLabelPainter getGXYLabelPainter(Object aObject) {
if (isLargeCity(aObject)) {
fLargeCitiesLabelPainter.setObject(aObject);
return fLargeCitiesLabelPainter;
} else {
fSmallCitiesLabelPainter.setObject(aObject);
return fSmallCitiesLabelPainter;
}
}
@Override
public Object clone() {
try {
LabelPainterProvider clone = (LabelPainterProvider) super.clone();
clone.fLargeCitiesLabelPainter = (ILcdGXYLabelPainter) fLargeCitiesLabelPainter.clone();
clone.fSmallCitiesLabelPainter = (ILcdGXYLabelPainter) fSmallCitiesLabelPainter.clone();
return clone;
} catch (CloneNotSupportedException aE) {
throw new RuntimeException(aE);
}
}
}
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.awt.Font;
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.ILcdObjectIconProvider;
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.ILcdGXYLabelPainter;
import com.luciad.view.gxy.ILcdGXYLabelPainterProvider;
import com.luciad.view.gxy.ILcdGXYPainterProvider;
import com.luciad.view.gxy.ILcdGXYPainterStyle;
import com.luciad.view.gxy.TLcdGXYDataObjectLabelPainter;
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.labeling.algorithm.ILcdGXYLabelingAlgorithm;
import com.luciad.view.gxy.labeling.algorithm.discrete.TLcdGXYInPathLabelingAlgorithm;
import com.luciad.view.gxy.labeling.algorithm.discrete.TLcdGXYOnPathLabelingAlgorithm;
import com.luciad.view.gxy.painter.ALcdGXYAreaPainter;
import com.luciad.view.map.TLcdMapJPanel;
import com.luciad.view.swing.TLcdLayerTree;
class BasicLabelingGXYTutorial {
final TLcdMapJPanel fView = new TLcdMapJPanel();
public JFrame createUI() {
JFrame frame = new JFrame("Vector labeling 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() {
TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
//Specify that the label should contain the ADMIN property
labelPainter.setExpressions("ADMIN");
//Specify the visual appearance of the label:
labelPainter.setFont(Font.decode("SansSerif-PLAIN-12"));
labelPainter.setForeground(Color.BLACK);
//Add a halo to the text to improve readability
labelPainter.setHaloColor(Color.WHITE);
labelPainter.setHaloEnabled(true);
String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel(countriesFile));
layer.setGXYPainterProvider(createWorldBodyPainter());
//Disable selection for this layer, as it only serves as background data
layer.setSelectableSupported(false);
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainter);
// Make sure the labels are nicely placed inside the countries.
ILcdGXYLabelingAlgorithm labelingAlgorithm = new TLcdGXYInPathLabelingAlgorithm();
layer.setGXYLabelingAlgorithmProvider(l -> labelingAlgorithm);
//Enable labels
layer.setLabeled(true);
//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 ILcdGXYPainterProvider createWorldBodyPainter() {
ILcdGXYPainterStyle fillStyle = new TLcdGXYPainterColorStyle(new Color(192, 192, 192, 128));
ILcdGXYPainterStyle lineStyle = new TLcdGXYPainterColorStyle(Color.WHITE);
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setFillStyle(fillStyle);
painter.setLineStyle(lineStyle);
painter.setMode(ALcdGXYAreaPainter.OUTLINED_FILLED);
return painter;
}
private void addRiversLayer() {
TLcdGXYDataObjectLabelPainter labelPainter = new TLcdGXYDataObjectLabelPainter();
//Specify that the label should contain the NAME property
labelPainter.setExpressions("NAME");
//Specify the styling for regular labels
labelPainter.setForeground(Color.BLUE);
labelPainter.setHaloColor(Color.WHITE);
labelPainter.setHaloEnabled(true);
// and for selected labels
labelPainter.setSelectionColor(Color.RED);
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/rivers.shp"));
layer.setGXYPainterProvider(createRiversBodyPainter());
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainter);
//Make sure the labels follow the rivers
TLcdGXYOnPathLabelingAlgorithm labelingAlgorithm = new TLcdGXYOnPathLabelingAlgorithm();
layer.setGXYLabelingAlgorithmProvider(l -> labelingAlgorithm);
//Enable labels
layer.setLabeled(true);
//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 ILcdGXYPainterProvider createRiversBodyPainter() {
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
painter.setLineStyle(TLcdStrokeLineStyle.newBuilder()
.color(Color.BLUE)
.selectionColor(Color.RED)
.lineWidth(2f)
.build());
return painter;
}
private void addCitiesLayer() {
//Define the label painters we want to use
TLcdGXYDataObjectLabelPainter largeCityLabelPainter = new TLcdGXYDataObjectLabelPainter();
largeCityLabelPainter.setExpressions("CITY", "TOT_POP");
largeCityLabelPainter.setShiftLabelPosition(12);
largeCityLabelPainter.setWithPin(true); // Use a pin because the label is moved a bit further from the icon
largeCityLabelPainter.setPositionList(new int[] {TLcdGXYDataObjectLabelPainter.NORTH});
TLcdGXYDataObjectLabelPainter smallCityLabelPainter = new TLcdGXYDataObjectLabelPainter();
smallCityLabelPainter.setExpressions("CITY");
//Create a label painter provider which selects the correct label painter based on the population of the city
ILcdGXYLabelPainterProvider labelPainterProvider = new LabelPainterProvider(largeCityLabelPainter, smallCityLabelPainter);
TLcdGXYLayer layer = TLcdGXYLayer.create(decodeSHPModel("Data/Shp/Usa/city_125.shp"));
layer.setGXYPainterProvider(createCitiesBodyPainter());
//Install the label painter on the layer
layer.setGXYLabelPainterProvider(labelPainterProvider);
//Enable labels
layer.setLabeled(true);
//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 static boolean isLargeCity(Object aObject) {
int population = ((Number) ((ILcdDataObject) aObject).getValue("TOT_POP")).intValue();
return population > 500000;
}
private static class LabelPainterProvider implements ILcdGXYLabelPainterProvider {
private ILcdGXYLabelPainter fLargeCitiesLabelPainter;
private ILcdGXYLabelPainter fSmallCitiesLabelPainter;
private LabelPainterProvider(ILcdGXYLabelPainter aLargeCitiesLabelPainter, ILcdGXYLabelPainter aSmallCitiesLabelPainter) {
fLargeCitiesLabelPainter = aLargeCitiesLabelPainter;
fSmallCitiesLabelPainter = aSmallCitiesLabelPainter;
}
@Override
public ILcdGXYLabelPainter getGXYLabelPainter(Object aObject) {
if (isLargeCity(aObject)) {
fLargeCitiesLabelPainter.setObject(aObject);
return fLargeCitiesLabelPainter;
} else {
fSmallCitiesLabelPainter.setObject(aObject);
return fSmallCitiesLabelPainter;
}
}
@Override
public Object clone() {
try {
LabelPainterProvider clone = (LabelPainterProvider) super.clone();
clone.fLargeCitiesLabelPainter = (ILcdGXYLabelPainter) fLargeCitiesLabelPainter.clone();
clone.fSmallCitiesLabelPainter = (ILcdGXYLabelPainter) fSmallCitiesLabelPainter.clone();
return clone;
} catch (CloneNotSupportedException aE) {
throw new RuntimeException(aE);
}
}
}
private ILcdGXYPainterProvider createCitiesBodyPainter() {
TLcdSymbol largeCityIcon = new TLcdSymbol(FILLED_CIRCLE, 20, Color.WHITE, Color.ORANGE);
TLcdSymbol smallCityIcon = new TLcdSymbol(FILLED_CIRCLE, 10, Color.WHITE, Color.ORANGE);
TLcdGXYShapePainter painter = new TLcdGXYShapePainter();
ILcdObjectIconProvider iconProvider = new ILcdObjectIconProvider() {
@Override
public ILcdIcon getIcon(Object aObject) throws IllegalArgumentException {
if (isLargeCity(aObject)) {
return largeCityIcon;
}
return smallCityIcon;
}
@Override
public boolean canGetIcon(Object aObject) {
return true;
}
};
painter.setIconProvider(iconProvider);
painter.setSelectedIconProvider(new ILcdObjectIconProvider() {
@Override
public ILcdIcon getIcon(Object aObject) throws IllegalArgumentException {
return wrapWithSelectionRectangle(iconProvider.getIcon(aObject));
}
@Override
public boolean canGetIcon(Object aObject) {
return iconProvider.canGetIcon(aObject);
}
});
return painter;
}
/**
* 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();
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new BasicLabelingGXYTutorial().createUI();
frame.setVisible(true);
});
}
}