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:

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.

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

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:

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

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 style each country exactly the same:

  • A white outline

  • A gray, semi-transparent interior

To do so, we create two ALspStyle instances:

  • A TLspLineStyle for the outline: line styles can be used for lines and for the outlines of areas.

  • A TLspFillStyle for 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.

Program: Creating a vector layer with the same styling for all objects
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.

lls vectorpainting worldlayer
Figure 2. The background world layer with the white outlines and gray fill

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.

Program: Creating a layer with different styling for selected objects
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.

lls vectorpainting riverslayer
Figure 3. The rivers layer with different styling for selected rivers

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 TLcdSymbol class to create the circle.

    Other frequently used ILcdIcon implementations are TLcdImageIcon and TLcdSVGIcon. 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.

Program: Creating an 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 TOT_POP property, which represents the total population number. We access that property through the ILcdDataObject interface.

lls vectorpainting citieslayer
Figure 4. The cities layer with distinct icons for large and small 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.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);
    });
  }
}