Prerequisites

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

Goal

This tutorial teaches you the basic concepts of the LuciadLightspeed API for adding labels to vector data on a Lightspeed view.

This tutorial creates a JFrame containing a Lightspeed view. We 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 rivers and non-selected rivers. The labels will also be oriented 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 labels for distinct objects.

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

You can find the complete, runnable code at the end of this tutorial.

The LuciadLightspeed API for visualizing and styling labels is almost identical to the API used to style the domain object itself. As a result, this tutorial is very similar to the Visualizing vector data tutorial. It:

  • Uses the same data

  • Uses the same visualization for the domain objects

  • Adds labels to the domain objects

Initial setup

This tutorial starts from an application that shows a Lightspeed view in a JFrame:

Program: Basic setup for this tutorial
public class BasicLabelingTutorial {

  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();

  public JFrame createUI() {
    JFrame frame = new JFrame("Vector labeling 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 BasicLabelingTutorial().createUI();
      frame.setVisible(true);
    });
  }
}

Background layer: use a fixed property as label

To make the rest of the code more compact, we start by introducing a utility method to decode 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. LuciadLightspeed requires this information:

  • An ALspLabelTextProviderStyle instance defining the contents of the label. This is a specialized extension of ALspStyle that has an extra method.

    public String[] getText(Object aDomainObject, Object aSubLabelID, TLspContext aContext)

    The method allows you to specify the labels for a specific domain object.

  • An ALspStyle instance defining the styling of the label: which font to use, font size, color, positioning, and so on.

We provide the information by creating:

To pass the information to the layer, we use the TLspShapeLayerBuilder class. In this case, we want to apply the same label styling for all objects in the layer. We pass our ALspStyle instance objects to the labelStyles method of the TLspShapeLayerBuilder.

Using a property as a label for all domain objects
private void addWorldLayer() {
  //Specify that the label should contain the COUNTRY name
  ALspStyle labelContentsStyle = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                     .expressions("ADMIN")
                                                                     .build();
  //Specify the visual appearance of the label:
  //We add a halo to the text to improve readability
  ALspStyle textStyle = TLspTextStyle.newBuilder()
                                     .font(Font.decode("SansSerif-PLAIN-12"))
                                     .textColor(Color.BLACK)
                                     .haloColor(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, createWorldLayerBodyStyles())
                           .labelStyles(TLspPaintState.REGULAR, labelContentsStyle, textStyle)
                           .selectable(false)
                           .build();

  fView.addLayer(layer);
}

To focus on the labeling-related code, the snippets in this tutorial do not include the code for styling the bodies of the vector data. The body styling code is the code shown in the Visualizing vector data tutorial.

The full code at the end of this tutorial does contain the code for the body styling.

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

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

lls vectorpainting labeling worldlayer decluttered
Figure 3. Decluttered labels when zooming out

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 need a red one.

Creating the rivers layer
private void addRiversLayer() {
  //Specify that the label should contain the name of the river
  ALspStyle labelContentsStyle = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                     .expressions("NAME")
                                                                     .build();
  //Specify the styling for regular labels and selected labels
  TLspTextStyle regularTextStyle = TLspTextStyle.newBuilder()
                                                .textColor(Color.BLUE)
                                                .haloColor(Color.WHITE)
                                                .build();
  TLspTextStyle selectedTextStyle = regularTextStyle.asBuilder()
                                                    .textColor(Color.RED)
                                                    .build();
  //In order for the labels to follow the rivers, we need to specify that we want to use an on-path label placement algorithm.
  //This is done with the TLspOnPathLabelingAlgorithm class.
  //The LuciadLightspeed API contains multiple placement algorithms.
  //Search for the ILspLabelingAlgorithm implementations in the API.
  //
  //The label placement algorithm is not stored in an ALspStyle, but rather on the TLspLabelStyler.
  //So we create a styler for both the regular and selected style.
  TLspLabelStyler regularLabelStyler = TLspLabelStyler.newBuilder()
                                                      .algorithm(new TLspOnPathLabelingAlgorithm())
                                                      .styles(labelContentsStyle, regularTextStyle)
                                                      .build();
  TLspLabelStyler selectedLabelStyler = regularLabelStyler.asBuilder()
                                                          .styles(labelContentsStyle, selectedTextStyle)
                                                          .build();

  TLspLayer layer =
      TLspShapeLayerBuilder.newBuilder()
                           .model(decodeSHPModel("Data/Shp/Usa/rivers.shp"))
                           .bodyStyles(TLspPaintState.REGULAR, createRiversRegularBodyStyle())
                           .bodyStyles(TLspPaintState.SELECTED, createRiversSelectedBodyStyle())
                           .labelStyler(TLspPaintState.REGULAR, regularLabelStyler)
                           .labelStyler(TLspPaintState.SELECTED, selectedLabelStyler)
                           .selectable(true)
                           .build();
  fView.addLayer(layer);
}

To set this up, we specify a different styler for the TLspPaintState.REGULAR and TLspPaintState.SELECTED.

To ensure that the labels have the same orientation as the rivers, we specify the ILspLabelingAlgorithm. 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 the same direction as our line domain objects, the rivers. We use the TLspOnPathLabelingAlgorithm to take care of that.

The algorithm is not specified on an ALspStyle, but on the styler. As we have no other requirements, we use the default label styler class TLspLabelStyler.

lls vectorpainting labeling riverslayer
Figure 4. River labels oriented like the rivers. The label of the selected river receives a red color.

Cities layer: distinct labels for distinct cities at a custom location

If we want to use distinct styling for distinct objects, we need to create our own ILspStyler instance. This approach is identical to the one we used for the visualization of the bodies of the vector data. The Visualizing vector data tutorial explains the code for the body styling.

In this example, we are going to style the cities based on their population size.

  • For cities with large populations, the label shows the name of the city and the population.

  • For small cities, the label only shows the name of the city.

For large cities, we also:

  • Force the label to a location north of the city, by means of the locations method on the ALspLabelStyleCollector.

  • Connect the label and city with a so-called pin, using a TLspPinLineStyle. A pin is the term used in the LuciadLightspeed API for the line between the label and the domain object.

Creating an ILspStylerthat uses distinct labels for small and large cities
private void addCitiesLayer() {
  //Define the styles we want to use
  ALspStyle largeCityLabelContents = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                         .expressions("CITY", "TOT_POP")
                                                                         .build();
  ALspStyle smallCityLabelContents = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                         .expressions("CITY")
                                                                         .build();
  TLspTextStyle textStyle = TLspTextStyle.newBuilder().build();
  TLspPinLineStyle pinLineStyle = TLspPinLineStyle.newBuilder()
                                                  .pinEndPosition(TLspPinLineStyle.PinEndPosition.MIDDLE_OF_BOUNDS_ON_EDGE)
                                                  .build();

  ILspStyler styler = new ALspLabelStyler() {
    @Override
    public void style(Collection<?> aObjects, ALspLabelStyleCollector aStyleCollector, TLspContext aContext) {
      for (Object city : aObjects) {
        if (isLargeCity(city)) {
          aStyleCollector.object(city)
                         .styles(largeCityLabelContents, textStyle, pinLineStyle)
                         .locations(20, TLspLabelLocationProvider.Location.NORTH)
                         .submit();
        } else {
          aStyleCollector.object(city).styles(smallCityLabelContents, textStyle).submit();
        }
      }
    }
  };

  TLspLayer layer =
      TLspShapeLayerBuilder.newBuilder()
                           .model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
                           .bodyStyler(TLspPaintState.REGULAR, createCitiesBodyStyler())
                           .labelStyler(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;
}

Because this styler is used for labeling, the ALspStyleCollector is an ALspLabelStyleCollector that offers some label-specific methods.

For example, it offers a method to specify the label placement algorithm.

lls vectorpainting labeling citieslayer
Figure 5. The cities layer with distinct 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 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.label.algorithm.TLspLabelLocationProvider;
import com.luciad.view.lightspeed.label.algorithm.discrete.TLspOnPathLabelingAlgorithm;
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.painter.label.style.TLspDataObjectLabelTextProviderStyle;
import com.luciad.view.lightspeed.style.ALspStyle;
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.TLspPinLineStyle;
import com.luciad.view.lightspeed.style.TLspTextStyle;
import com.luciad.view.lightspeed.style.styler.ALspLabelStyleCollector;
import com.luciad.view.lightspeed.style.styler.ALspLabelStyler;
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.lightspeed.style.styler.TLspLabelStyler;
import com.luciad.view.swing.TLcdLayerTree;

public class BasicLabelingTutorial {

  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();

  public JFrame createUI() {
    JFrame frame = new JFrame("Vector labeling 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() {
    //Specify that the label should contain the COUNTRY name
    ALspStyle labelContentsStyle = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                       .expressions("ADMIN")
                                                                       .build();
    //Specify the visual appearance of the label:
    //We add a halo to the text to improve readability
    ALspStyle textStyle = TLspTextStyle.newBuilder()
                                       .font(Font.decode("SansSerif-PLAIN-12"))
                                       .textColor(Color.BLACK)
                                       .haloColor(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, createWorldLayerBodyStyles())
                             .labelStyles(TLspPaintState.REGULAR, labelContentsStyle, textStyle)
                             .selectable(false)
                             .build();

    fView.addLayer(layer);
  }

  private ALspStyle[] createWorldLayerBodyStyles() {
    TLspFillStyle fillStyle = TLspFillStyle.newBuilder()
                                           .color(new Color(192, 192, 192, 128))
                                           .build();
    TLspLineStyle lineStyle = TLspLineStyle.newBuilder()
                                           .color(Color.white)
                                           .build();
    return new ALspStyle[]{lineStyle, fillStyle};
  }

  private void addRiversLayer() {
    //Specify that the label should contain the name of the river
    ALspStyle labelContentsStyle = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                       .expressions("NAME")
                                                                       .build();
    //Specify the styling for regular labels and selected labels
    TLspTextStyle regularTextStyle = TLspTextStyle.newBuilder()
                                                  .textColor(Color.BLUE)
                                                  .haloColor(Color.WHITE)
                                                  .build();
    TLspTextStyle selectedTextStyle = regularTextStyle.asBuilder()
                                                      .textColor(Color.RED)
                                                      .build();
    //In order for the labels to follow the rivers, we need to specify that we want to use an on-path label placement algorithm.
    //This is done with the TLspOnPathLabelingAlgorithm class.
    //The LuciadLightspeed API contains multiple placement algorithms.
    //Search for the ILspLabelingAlgorithm implementations in the API.
    //
    //The label placement algorithm is not stored in an ALspStyle, but rather on the TLspLabelStyler.
    //So we create a styler for both the regular and selected style.
    TLspLabelStyler regularLabelStyler = TLspLabelStyler.newBuilder()
                                                        .algorithm(new TLspOnPathLabelingAlgorithm())
                                                        .styles(labelContentsStyle, regularTextStyle)
                                                        .build();
    TLspLabelStyler selectedLabelStyler = regularLabelStyler.asBuilder()
                                                            .styles(labelContentsStyle, selectedTextStyle)
                                                            .build();

    TLspLayer layer =
        TLspShapeLayerBuilder.newBuilder()
                             .model(decodeSHPModel("Data/Shp/Usa/rivers.shp"))
                             .bodyStyles(TLspPaintState.REGULAR, createRiversRegularBodyStyle())
                             .bodyStyles(TLspPaintState.SELECTED, createRiversSelectedBodyStyle())
                             .labelStyler(TLspPaintState.REGULAR, regularLabelStyler)
                             .labelStyler(TLspPaintState.SELECTED, selectedLabelStyler)
                             .selectable(true)
                             .build();
    fView.addLayer(layer);
  }

  private TLspLineStyle createRiversRegularBodyStyle() {
    return TLspLineStyle.newBuilder()
                        .color(Color.BLUE)
                        .width(2.0)
                        .build();
  }

  private TLspLineStyle createRiversSelectedBodyStyle() {
    return TLspLineStyle.newBuilder()
                        .color(Color.RED)
                        .width(2.0)
                        .build();
  }

  private void addCitiesLayer() {
    //Define the styles we want to use
    ALspStyle largeCityLabelContents = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                           .expressions("CITY", "TOT_POP")
                                                                           .build();
    ALspStyle smallCityLabelContents = TLspDataObjectLabelTextProviderStyle.newBuilder()
                                                                           .expressions("CITY")
                                                                           .build();
    TLspTextStyle textStyle = TLspTextStyle.newBuilder().build();
    TLspPinLineStyle pinLineStyle = TLspPinLineStyle.newBuilder()
                                                    .pinEndPosition(TLspPinLineStyle.PinEndPosition.MIDDLE_OF_BOUNDS_ON_EDGE)
                                                    .build();

    ILspStyler styler = new ALspLabelStyler() {
      @Override
      public void style(Collection<?> aObjects, ALspLabelStyleCollector aStyleCollector, TLspContext aContext) {
        for (Object city : aObjects) {
          if (isLargeCity(city)) {
            aStyleCollector.object(city)
                           .styles(largeCityLabelContents, textStyle, pinLineStyle)
                           .locations(20, TLspLabelLocationProvider.Location.NORTH)
                           .submit();
          } else {
            aStyleCollector.object(city).styles(smallCityLabelContents, textStyle).submit();
          }
        }
      }
    };

    TLspLayer layer =
        TLspShapeLayerBuilder.newBuilder()
                             .model(decodeSHPModel("Data/Shp/Usa/city_125.shp"))
                             .bodyStyler(TLspPaintState.REGULAR, createCitiesBodyStyler())
                             .labelStyler(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;
  }


  private ILspStyler createCitiesBodyStyler() {
    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();

    return 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();
          }
        }
      }
    };
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame frame = new BasicLabelingTutorial().createUI();
      frame.setVisible(true);
    });
  }
}