Prerequisites

This tutorial requires no prior LuciadLightspeed knowledge, but you do need some basic familiarity with Java Swing or JavaFX.

Goal

This tutorial provides a step-by-step guide to building a basic LuciadLightspeed application with a hardware-accelerated map. The application built in this tutorial allows users to view major USA cities on a raster image of the world in either 2D or 3D. The tutorial discusses the setup of the basic View, Model and Controller components of the application, before expanding that basic configuration with additional functions.

More specifically, the application contains the following functionality:

  • A map displayed inside a window

  • A raster image of the world used as background data

  • The major cities of the USA, read from a .SHP file

  • The ability to navigate through the view

  • The ability to switch between 2D and 3D visualization

  • Check boxes to toggle the visibility of the data on the map

lls basic app end result
Figure 1. The window with the components of a basic LuciadLightspeed application with a Lightspeed view

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

This tutorial uses the hardware-accelerated Lightspeed view. A similar tutorial for the non-hardware-accelerated GXY view is available here.

Read High-level overview of the different technologies if you are unsure which view is most suitable for you.

Overview of the main API concepts

A standard LuciadLightspeed application follows the M(odel)-V(iew)-C(ontroller) architectural pattern. The idea behind the MVC architecture is to separate the data (model), the representation of the data (view), and the user interaction (controller) from each other. That separation results in a simpler design of the application and a higher flexibility and re-usability of code.

The MVC parts of the LuciadLightspeed API are defined as follows:

  • A LuciadLightspeed model stores and describes geographical data regardless of how the data is visualized and interacted with. For example: a model contains the location of a number of hospitals and additional hospital information such as capacity.

    In the LuciadLightspeed API, the model is an ILcdModel instance.

  • A LuciadLightspeed view contains all information needed for the visual representation of the data in the LuciadLightspeed models. A view does not contain data. For example, in the case of the hospitals, the view uses a red cross to represent the location of a hospital.

    In the LuciadLightspeed API, an ILspLayer contains the information about how to visualize a single ILcdModel. All those layers are added to an ILspView, which can be visualized in a Swing or JavaFX container.

  • A LuciadLightspeed controller interprets user interaction and performs the required action on LuciadLightspeed models and views, regardless of the type of model and view. For example, in the case of the hospitals, a mouse right-click on a red cross results in an information pop-up with the hospital location and capacity.

    In the LuciadLightspeed API, these are ILspController instances.

Separating the different parts of the application allows you to re-use objects for different purposes, and to re-define objects without changing other objects. You can, for example, change a view without making changes to the models represented in the view. You can also re-define the user interaction with a view without changing the view itself. Object re-use shortens the time for writing an application. In addition, it promotes a consistent design and functionality for all your applications.

Creating the Swing application UI

Before we can turn to the LuciadLightspeed API to create our map, we need a JFrame to display the map in, and a main method to start the application:

Program: Creating the Swing application UI
public class FirstApplicationTutorial {
  public JFrame createUI() {
    JFrame frame = new JFrame("First Lightspeed application");
    frame.setSize(2000, 1500);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    return frame;
  }
  public static void main(String[] args) {
    //Swing components must be created on the Event Dispatch Thread
    EventQueue.invokeLater(() -> {
      JFrame frame = new FirstApplicationTutorial().createUI();
      frame.setVisible(true);
    });
  }
}

Creating the JavaFX application UI

In JavaFX, we need to set a Scene to display the map in.

Program: Creating the JavaFX application UI
public class FirstApplicationFXTutorial extends Application {
  @Override
  public void start(Stage primaryStage) {
    primaryStage.setTitle("First Lightspeed application");
    BorderPane borderPane = new BorderPane();
    Scene scene = new Scene(borderPane, 800, 600);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

Creating the map

Now that we have a window, we need to create the map and add it to the UI. In LuciadLightspeed terminology, a map is known as a view. It is defined by the interface ILspView.

We use the builder TLspViewBuilder to instantiate an ILspView.

For Swing we’ll use a TLspAWTView, for JavaFX we’ll use a TLspFXView.

Program: Creating a view for a Swing application
ILspAWTView createView() {
  return TLspViewBuilder.newBuilder().buildAWTView();
}
Program: Creating a view for a JavaFX application
private TLspFXView createView() {
  return TLspViewBuilder.newBuilder().buildFXView();
}

Once the view is created, we can add its host component to our UI.

Program: Adding the view’s component to the Swing frame
ILspAWTView view = createView();
frame.add(view.getHostComponent(), BorderLayout.CENTER);
Program: Adding the view’s component to the JavaFX BorderPane
TLspFXView view = createView();
borderPane.setCenter(view.getHostNode());

Running the program at this point results in an empty window. Because the view does not contain any data yet, it only displays a window background.

Adding data to the view

Now that we have set up the view, we can add some data to the view. Adding the data is a two-step process:

  1. First we create an ILcdModel that contains the data.

  2. Then we create an ILspLayer for the model, which we can add to the view. The ILcdModel contains the data only. The ILspLayer defines how that data is visualized on the view.

Creating a model from a file

The LuciadLightspeed API supports a number of geospatial formats out-of-the-box. It offers ILcdModelDecoder implementations to create an ILcdModel for data of those formats.

In this tutorial, we use a TLcdCompositeModelDecoder to decode the data. As the class name suggests, this is a composite version of the ILcdModelDecoder interface.

This composite implementation is populated with model decoders for all supported formats.

Program: The creation of the composite model decoder
ILcdModelDecoder decoder =
    new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));

The mechanism that makes this work is the Java ServiceLoader: LuciadLightspeed registers ILcdModelDecoder implementations for each of the supported formats with the service registry. We request all those implementations using the TLcdServiceLoader utility class, and pass them all to the constructor of the TLcdCompositeModelDecoder.

The resulting model decoder is capable of decoding data of all supported formats.

Once we have the decoder, we pass the path to the data file to the decode method of the model decoder. The model decoder reads the data in the file and creates an ILcdModel for it.

Program: Creating an ILcdModel for a SHP file
private static ILcdModel createSHPModel() throws IOException {
  // This composite decoder can decode all supported formats
  ILcdModelDecoder decoder =
      new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));

  // Decode city_125.shp to create an ILcdModel
  ILcdModel shpModel = decoder.decode("Data/Shp/Usa/city_125.shp");

  return shpModel;
}

We can do the same for some raster data:

Program: Creating an ILcdModel for raster data
private static ILcdModel createRasterModel() throws IOException {
  // This composite decoder can decode all supported formats
  ILcdModelDecoder decoder =
      new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));

  // Decode a sample data set (imagery data)
  ILcdModel geopackageModel = decoder.decode("Data/GeoPackage/bluemarble.gpkg");

  return geopackageModel;
}

These snippets only work from your IDE if you activate its annotation processing settings.

See the Installation documentation for more details on how to set up your IDE, and the Fixing service registry errors article for specific tips related to annotation processing.

Creating a layer using a layer factory

The layer defines how the data from the ILcdModel is visualized on the view. The LuciadLightspeed API uses the concept of an ILspLayerFactory to create layers for an ILcdModel.

Just like we did for the model decoder, we use a composite ILspLayerFactory implementation. We make use of the TLcdServiceLoader class once more to populate the composite instance with layer factories for all supported formats.

Program: Creating the composite layer factory
TLspCompositeLayerFactory layerFactory =
    new TLspCompositeLayerFactory(TLcdServiceLoader.getInstance(ILspLayerFactory.class));

We can use the createLayers method of the layer factory to create our layer.

Program Creating a layer using the composite layer factory
private static ILspLayer createLayer(ILcdModel aModel) {
  TLspCompositeLayerFactory layerFactory =
      new TLspCompositeLayerFactory(TLcdServiceLoader.getInstance(ILspLayerFactory.class));

  if (layerFactory.canCreateLayers(aModel)) {
    Collection<ILspLayer> layers = layerFactory.createLayers(aModel);
    //We only expect a single layer for our data
    return layers.iterator().next();
  }
  throw new RuntimeException("Could not create a layer for " + aModel.getModelDescriptor().getDisplayName());
}

Now we can add the model to the view using ILspView.addLayer:

Program: Adding the created layers to the view
ILcdModel shpModel = createSHPModel();
view.addLayer(createLayer(shpModel));

ILcdModel rasterModel = createRasterModel();
view.addLayer(createLayer(rasterModel));

Creating layers directly

The usage of an ILspLayerFactory is optional. For the view, it is irrelevant how the layer was constructed.

To illustrate this, we create a grid layer directly:

Program: Creating the grid layer using the TLspLonLatGridLayerBuilder
static ILspLayer createGridLayer() {
  return TLspLonLatGridLayerBuilder.newBuilder().build();
}

and add it to the view:

Program: Adding the grid layer to the view
view.addLayer(createGridLayer());

Currently, our application looks like:

lls basic app with layers
Figure 2. The map displaying raster imagery, a SHP model and a longitude/latitude grid

Using a controller to make the view interactive

Essentially, a controller interprets input events and translates them into actions that are performed on the view, or on the models it contains.

For the creation of the view, we used the TLspViewBuilder. That builder already installs a default ILspController on the view for us.

This default controller is a chained controller that combines the behavior of several other LuciadLightspeed controllers. It offers view navigation, selection and editing with this navigation and selection configuration:

  • Dragging the left mouse button pans the view, unless a selected editable object is under the cursor.

  • Dragging the middle mouse button pans the view.

  • Dragging the right mouse button rotates the view.

  • Scrolling the mouse wheel zooms in and out on the cursor location.

  • Double-clicking the left mouse button starts a smooth, animated fly-to on the cursor location.

  • Clicking the left mouse button selects a selectable object.

  • Dragging the left mouse button over one or more selectable objects while holding the Shift key selects the objects that are fully covered by the resulting selection rectangle.

  • Clicking the left mouse button over one or more selectable objects while holding the Alt key displays a pop-up menu. The menu allows you to indicate which object you want to select.

  • Clicking the left mouse button over a selectable object while holding the Shift key inverts the selection state of the object. An unselected object is selected, a selected object is deselected.

  • Dragging the left mouse button while a selected object is under the cursor, moves the selected object.

  • Dragging the left mouse button on a handle of a selected object edits the object.

In 3D, the controller allows you to navigate around tall or airborne objects like air tracks easily, by moving around and toward the object that is under the mouse.

For this tutorial, we stick to the default controller. To learn how the chaining works in this controller, see Working with controllers.

See the documentation about Interacting with the view for more information about using controllers in general.

Adding a UI widget with available layers

To keep the user of our application informed about the layers that are available on the view, we will add a widget showing the available layers: a TLcdLayerTree. The widget also includes check boxes to toggle the visibility of the individual layers.

Program: Creating the TLcdLayerTree widget
private JComponent createLayerControl(ILspView aView) {
  return new TLcdLayerTree(aView);
}

Once the widget is created, we can add it to our JFrame:

Program: Creating the TLcdLayerTree widget
JComponent layerControl = createLayerControl(view);
frame.add(layerControl, BorderLayout.EAST);
lls basic app layer control
Figure 3. Adding the layer control

Switching between 2D and 3D views

LuciadLightspeed supports two-dimensional (2D) as well as three-dimensional (3D) visualization of the same data. You can switch between the two using the TLspViewTransformationUtil class, which has methods to configure a view as either 2D or 3D.

Program: Creating the actions to switch between 2D and 3D
static Action createSwitchTo2DAction(ILspView aView) {
  AbstractAction action = new AbstractAction("2D") {
    @Override
    public void actionPerformed(ActionEvent e) {
      TLspViewTransformationUtil.setup2DView(
          aView,
          new TLcdGridReference(new TLcdGeodeticDatum(),
                                new TLcdEquidistantCylindrical()),
          true
      );
    }
  };
  action.putValue(Action.SHORT_DESCRIPTION, "Switch the view to 2D");
  return action;
}

private Action createSwitchTo3DAction(ILspView aView) {
  AbstractAction action = new AbstractAction("3D") {
    @Override
    public void actionPerformed(ActionEvent e) {
      TLspViewTransformationUtil.setup3DView(aView, true);
    }
  };
  action.putValue(Action.SHORT_DESCRIPTION, "Switch the view to 3D");
  return action;
}

Note that the 2D action requires us to specify what kind of projection the map needs to use. In this example, we opted for an equidistant cylindrical projection. Another example of a frequently used projection is the (pseudo-)Mercator projection, also used by Bing Maps for instance.

To add these actions to the UI, we place them in a JToolBar:

Program: Adding the actions to the UI
JToolBar toolBar = new JToolBar();

JRadioButton b2d = new JRadioButton(createSwitchTo2DAction(view));
b2d.setSelected(true);//start with a 2D view
JRadioButton b3d = new JRadioButton(createSwitchTo3DAction(view));

//Place the buttons in a ButtonGroup.
//This ensures that only one of them can be selected at the same time
ButtonGroup group = new ButtonGroup();
group.add(b2d);
group.add(b3d);

toolBar.add(b2d);
toolBar.add(b3d);

frame.add(toolBar, BorderLayout.NORTH);

In JavaFX, we do just the same thing using an FX ToolBar. Refer to the full code for the details.

lls basic app end result
Figure 4. Using the 3D button to switch the map to a 3D view

The full code

Program: The full Swing code
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.util.Collection;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRadioButton;
import javax.swing.JToolBar;

import com.luciad.geodesy.TLcdGeodeticDatum;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.TLcdCompositeModelDecoder;
import com.luciad.projection.TLcdEquidistantCylindrical;
import com.luciad.reference.TLcdGridReference;
import com.luciad.util.service.TLcdServiceLoader;
import com.luciad.view.lightspeed.ILspAWTView;
import com.luciad.view.lightspeed.ILspView;
import com.luciad.view.lightspeed.TLspViewBuilder;
import com.luciad.view.lightspeed.layer.ILspLayer;
import com.luciad.view.lightspeed.layer.ILspLayerFactory;
import com.luciad.view.lightspeed.layer.TLspCompositeLayerFactory;
import com.luciad.view.lightspeed.painter.grid.TLspLonLatGridLayerBuilder;
import com.luciad.view.lightspeed.util.TLspViewTransformationUtil;
import com.luciad.view.swing.TLcdLayerTree;

public class FirstApplicationTutorial {


  public JFrame createUI() {
    JFrame frame = new JFrame("First Lightspeed application");

    ILspAWTView view = createView();
    frame.add(view.getHostComponent(), BorderLayout.CENTER);

    addData(view);

    JComponent layerControl = createLayerControl(view);
    frame.add(layerControl, BorderLayout.EAST);

    view.addLayer(createGridLayer());

    JToolBar toolBar = new JToolBar();

    JRadioButton b2d = new JRadioButton(createSwitchTo2DAction(view));
    b2d.setSelected(true);//start with a 2D view
    JRadioButton b3d = new JRadioButton(createSwitchTo3DAction(view));

    //Place the buttons in a ButtonGroup.
    //This ensures that only one of them can be selected at the same time
    ButtonGroup group = new ButtonGroup();
    group.add(b2d);
    group.add(b3d);

    toolBar.add(b2d);
    toolBar.add(b3d);

    frame.add(toolBar, BorderLayout.NORTH);

    frame.setSize(2000, 1500);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    return frame;
  }

  ILspAWTView createView() {
    return TLspViewBuilder.newBuilder().buildAWTView();
  }

  static void addData(ILspView view) {
    try {
      ILcdModel shpModel = createSHPModel();
      view.addLayer(createLayer(shpModel));

      ILcdModel rasterModel = createRasterModel();
      view.addLayer(createLayer(rasterModel));
    } catch (IOException e) {
      throw new RuntimeException("Problem during data decoding", e);
    }
  }

  private static ILcdModel createSHPModel() throws IOException {
    // This composite decoder can decode all supported formats
    ILcdModelDecoder decoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));

    // Decode city_125.shp to create an ILcdModel
    ILcdModel shpModel = decoder.decode("Data/Shp/Usa/city_125.shp");

    return shpModel;
  }

  private static ILcdModel createRasterModel() throws IOException {
    // This composite decoder can decode all supported formats
    ILcdModelDecoder decoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));

    // Decode a sample data set (imagery data)
    ILcdModel geopackageModel = decoder.decode("Data/GeoPackage/bluemarble.gpkg");

    return geopackageModel;
  }

  private static ILspLayer createLayer(ILcdModel aModel) {
    TLspCompositeLayerFactory layerFactory =
        new TLspCompositeLayerFactory(TLcdServiceLoader.getInstance(ILspLayerFactory.class));

    if (layerFactory.canCreateLayers(aModel)) {
      Collection<ILspLayer> layers = layerFactory.createLayers(aModel);
      //We only expect a single layer for our data
      return layers.iterator().next();
    }
    throw new RuntimeException("Could not create a layer for " + aModel.getModelDescriptor().getDisplayName());
  }

  static ILspLayer createGridLayer() {
    return TLspLonLatGridLayerBuilder.newBuilder().build();
  }

  private JComponent createLayerControl(ILspView aView) {
    return new TLcdLayerTree(aView);
  }

  static Action createSwitchTo2DAction(ILspView aView) {
    AbstractAction action = new AbstractAction("2D") {
      @Override
      public void actionPerformed(ActionEvent e) {
        TLspViewTransformationUtil.setup2DView(
            aView,
            new TLcdGridReference(new TLcdGeodeticDatum(),
                                  new TLcdEquidistantCylindrical()),
            true
        );
      }
    };
    action.putValue(Action.SHORT_DESCRIPTION, "Switch the view to 2D");
    return action;
  }

  private Action createSwitchTo3DAction(ILspView aView) {
    AbstractAction action = new AbstractAction("3D") {
      @Override
      public void actionPerformed(ActionEvent e) {
        TLspViewTransformationUtil.setup3DView(aView, true);
      }
    };
    action.putValue(Action.SHORT_DESCRIPTION, "Switch the view to 3D");
    return action;
  }

  public static void main(String[] args) {
    //Swing components must be created on the Event Dispatch Thread
    EventQueue.invokeLater(() -> {
      JFrame frame = new FirstApplicationTutorial().createUI();
      frame.setVisible(true);
    });
  }
}
Program: The JavaFX code
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import com.luciad.geodesy.TLcdGeodeticDatum;
import com.luciad.projection.TLcdEquidistantCylindrical;
import com.luciad.reference.TLcdGridReference;
import com.luciad.view.lightspeed.ILspView;
import com.luciad.view.lightspeed.TLspFXView;
import com.luciad.view.lightspeed.TLspViewBuilder;
import com.luciad.view.lightspeed.util.TLspViewTransformationUtil;

import samples.lightspeed.javafx.common.layercontrols.FXLayerControl;

public class FirstApplicationFXTutorial extends Application {


  @Override
  public void start(Stage primaryStage) {
    primaryStage.setTitle("First Lightspeed application");
    BorderPane borderPane = new BorderPane();

    TLspFXView view = createView();
    borderPane.setCenter(view.getHostNode());


    FirstApplicationTutorial.addData(view);

    Node layerControl = createLayerControl(view);
    borderPane.setRight(layerControl);

    view.addLayer(FirstApplicationTutorial.createGridLayer());

    ToolBar toolBar = new ToolBar();
    borderPane.setTop(toolBar);
    RadioButton b2d = new RadioButton("2D");
    RadioButton b3d = new RadioButton("3D");

    toolBar.getItems().add(b2d);
    toolBar.getItems().add(b3d);

    //Place the buttons in a group.
    //This ensures that only one of them can be selected at the same time
    ToggleGroup group = new ToggleGroup();
    b2d.setToggleGroup(group);
    b3d.setToggleGroup(group);

    group.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue == b2d) {
        TLspViewTransformationUtil.setup2DView(
            view,
            new TLcdGridReference(new TLcdGeodeticDatum(), new TLcdEquidistantCylindrical()), true);
      } else {
        TLspViewTransformationUtil.setup3DView(view, true);
      }
    });

    b2d.setSelected(true);//start with a 2D view


    Scene scene = new Scene(borderPane, 800, 600);
    primaryStage.setScene(scene);
    primaryStage.show();
  }

  private TLspFXView createView() {
    return TLspViewBuilder.newBuilder().buildFXView();
  }

  private Node createLayerControl(ILspView aView) {
    return new FXLayerControl(aView);
  }
}