Prerequisites

In this tutorial, it is assumed that you are familiar with the:

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.

gxy vectorpainting labeling result
Figure 1. The finished application with the 3 layers

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:

  • It uses the same data

  • It uses the same visualization methods for the domain objects

  • It adds labels to the domain objects

Initial setup

This tutorial starts from an application which shows a GXY view in a JFrame:

Program: Basic setup for this tutorial
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:

Program: Utility method to decode SHP data into a model
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.

Using a property as label for all domain objects
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.

gxy vectorpainting labeling worldlayer
Figure 2. The background world layer with labels

When the user zooms out on the map, the labels automatically declutter to prevent overlap:

gxy vectorpainting labeling worldlayer decluttered

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.

Creating the rivers layer
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.

gxy vectorpainting labeling riverslayer
Figure 3. River labels following the same path as the rivers. The label of the selected river is colored red.

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 the TLcdGXYDataObjectLabelPainter

  • Connect the label and city with a so-called pin, through the setWithPin method on the TLcdGXYDataObjectLabelPainter. A pin is the terminology used in the LuciadLightspeed API for the line between the label and the domain object.

Creating an 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);
    }
  }
}
gxy vectorpainting labeling citieslayer
Figure 4. The cities layer with different labels for small and large cities

The full code

Program: 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);
    });
  }
}