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:

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.

gxy 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 GXY view in a JFrame:

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

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 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:

Program: Creating a vector layer with the same styling for all objects
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));
}
gxy vectorpainting worldlayer
Figure 2. The background world layer with the white outlines and gray fill

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.

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

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

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 ILcdGXYPainter implementations in the API also implement the ILcdGXYPainterProvider interface, and simply return themselves when asked to provide a painter.

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 are TLcdImageIcon and TLcdSVGIcon. 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.

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

gxy vectorpainting citieslayer
Figure 4. The cities layer with different icons for big 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 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);
    });
  }
}