Prerequisites

Goal

In this tutorial, you will learn how to:

  • Allow the user to select objects on the map

  • Receive notifications when the user selects or deselects an object on the map

  • Change the selection through the LuciadLightspeed API

lls selection
Figure 1. The finished application with some selected objects

Initial setup

This tutorial starts from an application which shows a Lightspeed view in a JFrame, and adds a SHP layer to it.

Program: Basic setup for this tutorial
public class SelectionTutorial {

  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();
  private ILspLayer fSelectableLayer;

  public JFrame createUI() throws IOException {
    JFrame frame = new JFrame("Selection tutorial");
    fSelectableLayer = createSelectableLayer();
    fView.addLayer(fSelectableLayer);
    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 ILspLayer createSelectableLayer() throws IOException {
    ILcdModelDecoder decoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
    String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
    ILcdModel model = decoder.decode(countriesFile);
    TLspLayer layer = TLspShapeLayerBuilder.newBuilder()
                                           .model(model)
                                           .label("Countries")
                                           .build();
    return layer;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      try {
        JFrame frame = new SelectionTutorial().createUI();
        frame.setVisible(true);
      } catch (IOException aE) {
        //In a real application, we would need proper error handling
        throw new RuntimeException(aE);
      }
    });
  }
}

Manually selecting objects on the map

Like all user interaction with the map, selecting objects happens through a controller. For Lightspeed maps, the selection controller is the TLspSelectController.

This controller allows for the selection of an object on the map when the object is in an ILcdLayer which:

  • Supports selection through ILcdLayer.isSelectableSupported

  • Is configured to be selectable, through ILcdLayer.isSelectable

You can use the controllers in combination with modifier keys, such as the shift key to select multiple objects.

When wou create a Lightspeed view using the TLspViewBuilder, the default controller installed on the view allows the user to select objects. If you run the initial code of this tutorial, you can:

  • Left-click on a country to select it

  • Left-click again outside a selected country to de-select it

  • Hold the shift button and drag your mouse to select multiple countries

  • Hold the shift button and left-click on a country to add it to the selection

Listening to changes in the selection

LuciadLightspeed has the ILcdSelection interface to work with selection. This interface allows you to:

  • Retrieve the selected objects

  • Checking if a certain object is selected

  • Register ILcdSelectionListener instances that will be informed when the selection changes

The ILcdSelectionListener receives a TLcdSelectionChangedEvent each time the selection changes. The event identifies the objects that were selected and de-selected, and provides access to the ILcdSelection, which is the source of the event.

In this tutorial, we write a listener which prints out some information about the selected elements and the event.

ILcdSelectionListener<Object> selectionListener = new ILcdSelectionListener<Object>() {
  @Override
  public void selectionChanged(TLcdSelectionChangedEvent<Object> aSelectionEvent) {
    ILcdSelection<Object> layer = aSelectionEvent.getSelection();
    //Print out some general information about the currently selected elements
    System.out.println(String.format("Detected selection change in layer %s", ((ILcdLayer) layer).getLabel()));
    System.out.println(String.format("There are currently %d elements selected in the layer:", layer.getSelectionCount()));

    List<Object> selectedObjects = layer.getSelectedObjects();
    selectedObjects.stream()
                   .map(ILcdDataObject.class::cast)//a SHP layer only contains ILcdDataObject instances
                   .map(dataObject -> "  " + dataObject.getValue("ADMIN"))
                   .forEach(System.out::println);

    //Print out how many elements got selected and deselected in this event
    long selectedElementCount = aSelectionEvent.getChangedObjects()
                                               .stream()
                                               .filter(aSelectionEvent::retrieveChange)
                                               .count();
    long deselectedElementCount = aSelectionEvent.getChangedObjects()
                                                 .stream()
                                                 .filter(object -> !aSelectionEvent.retrieveChange(object))
                                                 .count();
    System.out.println(String.format("This selection change selected %d elements and de-selected %d elements.",
                                     selectedElementCount, deselectedElementCount));
  }
};

Once we have the listener, we attach it to the ILspLayer, which implements ILcdSelection, using the ILcdSelection#addSelectionListener method.

fSelectableLayer.addSelectionListener(selectionListener);

As a result, the listener will print out what we changed after a selection change on the map. For example, if we select Zimbabwe and Zambia in one go, the output is:

Detected selection change in layer Countries
There are currently 2 elements selected in the layer:
  Zimbabwe
  Zambia
This selection change selected 2 elements and de-selected 0 elements.

In this example, we attached the listener directly to the ILspLayer. The listener will only be informed about changes in that specific layer.

An ILspView has a method to register an ILcdSelectionListener which will be informed about selection changes in all layers: ILspView.addLayerSelectionListener.

Changing the selection with code

It is also possible to alter the selection programmatically. This functionality is not provided by the ILcdSelection interface itself. That interface provides read-only access to the selection.

However, most implementations of the ILcdSelection interface expose extra methods that allow you to adjust the selection. For example, all layers have the com.luciad.view.ILcdLayer.selectObject method.

We will use this method to select 2 elements in the layer:

private void selectObjects() {
  ILcdModel model = fSelectableLayer.getModel();
  //Accessing the model elements requires a read lock
  try (TLcdLockUtil.Lock readLock = TLcdLockUtil.readLock(model)) {
    //Query 2 random elements from the model
    try (Stream<Object> elementsToSelect = model.query(ILcdModel.all().limit(2))) {
      //For each of those elements, use the selectObject method to select it
      //Do not fire events yet
      elementsToSelect.forEach(element -> fSelectableLayer.selectObject(element, true, ILcdFireEventMode.FIRE_LATER));
    } finally {
      //Now that we have adjusted the selection, fire the event which will inform the listeners about this change
      fSelectableLayer.fireCollectedSelectionChanges();
    }
  }
}

Full code

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;

import javax.swing.JFrame;
import javax.swing.WindowConstants;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.TLcdCompositeModelDecoder;
import com.luciad.util.ILcdFireEventMode;
import com.luciad.util.ILcdSelection;
import com.luciad.util.ILcdSelectionListener;
import com.luciad.util.TLcdSelectionChangedEvent;
import com.luciad.util.concurrent.TLcdLockUtil;
import com.luciad.util.service.TLcdServiceLoader;
import com.luciad.view.ILcdLayer;
import com.luciad.view.lightspeed.ILspAWTView;
import com.luciad.view.lightspeed.TLspViewBuilder;
import com.luciad.view.lightspeed.layer.ILspLayer;
import com.luciad.view.lightspeed.layer.TLspLayer;
import com.luciad.view.lightspeed.layer.shape.TLspShapeLayerBuilder;
import com.luciad.view.swing.TLcdLayerTree;

public class SelectionTutorial {

  final ILspAWTView fView = TLspViewBuilder.newBuilder().buildAWTView();
  private ILspLayer fSelectableLayer;

  public JFrame createUI() throws IOException {
    JFrame frame = new JFrame("Selection tutorial");
    fSelectableLayer = createSelectableLayer();
    fView.addLayer(fSelectableLayer);

    listenForSelectionChanges();
    selectObjects();

    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 ILspLayer createSelectableLayer() throws IOException {
    ILcdModelDecoder decoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
    String countriesFile = "Data/Shp/NaturalEarth/50m_cultural/ne_50m_admin_0_countries_lakes.shp";
    ILcdModel model = decoder.decode(countriesFile);
    TLspLayer layer = TLspShapeLayerBuilder.newBuilder()
                                           .model(model)
                                           .label("Countries")
                                           .build();
    return layer;
  }

  private void listenForSelectionChanges() {
    ILcdSelectionListener<Object> selectionListener = new ILcdSelectionListener<Object>() {
      @Override
      public void selectionChanged(TLcdSelectionChangedEvent<Object> aSelectionEvent) {
        ILcdSelection<Object> layer = aSelectionEvent.getSelection();
        //Print out some general information about the currently selected elements
        System.out.println(String.format("Detected selection change in layer %s", ((ILcdLayer) layer).getLabel()));
        System.out.println(String.format("There are currently %d elements selected in the layer:", layer.getSelectionCount()));

        List<Object> selectedObjects = layer.getSelectedObjects();
        selectedObjects.stream()
                       .map(ILcdDataObject.class::cast)//a SHP layer only contains ILcdDataObject instances
                       .map(dataObject -> "  " + dataObject.getValue("ADMIN"))
                       .forEach(System.out::println);

        //Print out how many elements got selected and deselected in this event
        long selectedElementCount = aSelectionEvent.getChangedObjects()
                                                   .stream()
                                                   .filter(aSelectionEvent::retrieveChange)
                                                   .count();
        long deselectedElementCount = aSelectionEvent.getChangedObjects()
                                                     .stream()
                                                     .filter(object -> !aSelectionEvent.retrieveChange(object))
                                                     .count();
        System.out.println(String.format("This selection change selected %d elements and de-selected %d elements.",
                                         selectedElementCount, deselectedElementCount));
      }
    };
    fSelectableLayer.addSelectionListener(selectionListener);
  }

  private void selectObjects() {
    ILcdModel model = fSelectableLayer.getModel();
    //Accessing the model elements requires a read lock
    try (TLcdLockUtil.Lock readLock = TLcdLockUtil.readLock(model)) {
      //Query 2 random elements from the model
      try (Stream<Object> elementsToSelect = model.query(ILcdModel.all().limit(2))) {
        //For each of those elements, use the selectObject method to select it
        //Do not fire events yet
        elementsToSelect.forEach(element -> fSelectableLayer.selectObject(element, true, ILcdFireEventMode.FIRE_LATER));
      } finally {
        //Now that we have adjusted the selection, fire the event which will inform the listeners about this change
        fSelectableLayer.fireCollectedSelectionChanges();
      }
    }
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      try {
        JFrame frame = new SelectionTutorial().createUI();
        frame.setVisible(true);
      } catch (IOException aE) {
        //In a real application, we would need proper error handling
        throw new RuntimeException(aE);
      }
    });
  }
}