Goal

This tutorial introduces you to the basic operations you can perform on an ILcdModel.

We will:

  • Create an ILcdModel containing the major US cities by decoding a SHP file

  • Query that model for some cities, and print out the properties of those cities

  • Modify the properties of a city

  • Make sure we are informed about changes to the elements of a model

Most operations on a model require you to take a lock. This tutorial illustrates how to take the right type of lock, but does not go into detail.

Consult the Threading and locking documentation for more information about threads and locks in a LuciadLightspeed application.

Creating models by decoding data

Most often, an ILcdModel is created by asking an ILcdModelDecoder to decode a source of data. In this tutorial, we create a model by decoding a SHP file through an ILcdModelDecoder instance.

ILcdModelDecoder modelDecoder =
    new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
ILcdModel model = modelDecoder.decode("Data/Shp/Usa/city_125.shp");

The model decoder in this example is a composite instance that can handle all formats supported by our product.

If your data is stored in a custom format, you can always write your own model decoder to decode the data. See Supporting custom formats for more information and tutorials.

Reading data from a model

We want to find the first 5 cities in the model, and print the city name and population of each of those cities.

Accessing the data of a model requires you to take a read lock on the model. All model reading code should therefore be surrounded by:

try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(model)) {
}

You can read a model from any thread. See Threading and locking in LuciadLightspeed for more information about the threading rules in a LuciadLightspeed application.

To access the first 5 cities, we use the query method. For our query, we specify that we want the first 5 elements, which results in a java.util.Stream with those 5 domain objects.

Because our model is a SHP model, we know that the domain objects are ILcdDataObject instances, and that we can use the ILcdDataObject API to access the city name and population:

//Accessing the model data requires a read lock
//Reading, and taking the read lock, can be done on any thread
try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(model)) {
  //Loop over the domain objects.
  //Here we limit us to 5 elements
  try (Stream<ILcdDataObject> domainObjects = model.query(ILcdModel.all().limit(5))) {
    domainObjects
        .forEach(city -> {
          String cityName = (String) city.getValue("CITY");
          Integer population = (Integer) city.getValue("TOT_POP");
          System.out.println(String.format("%s: population %d",
                                           cityName,
                                           population));
        });
  }
}

Running this results in the following output:

Akron: population 223019
Albuquerque: population 384736
Amarillo: population 157615
Anaheim: population 266406
Anchorage: population 226338

Operations that require a read lock include:

  • Model operations like elements, query or applyOnInteract2DBounds

  • Element operations such as looping over points of a polyline when the polyline is part of an ILcdModel

Simply looping over all the elements of a model might trigger the loading of a lot of data. For example, in the case of a database model, the request might start a loop over the whole database.

Therefore, it is recommended never to request all elements from a model, unless you know that the number of elements is limited.

Changing data in a model

The ILcdModel interface allows you to add and remove elements to and from a model using the addElement and removeElement methods. The interface also provides access to the domain objects, allowing you to modify the domain objects.

In this case, we plan to change the population of the first city. To do so, we are going to access the model data, and use the ILcdDataObject API.

When you are modifying the contents of a model, you need to take a write lock on the model. All such code must be surrounded by:

try (TLcdLockUtil.Lock lock = TLcdLockUtil.writeLock(model)) {
} finally {
  model.fireCollectedModelChanges();
}

It is also required to make the model inform all interested parties about the changes to its content. That is why there is a call to fireCollectedModelChanges in the finally block. See the next section in this tutorial for more details.

You can take read locks on any thread, but you can take write locks on one thread only. In most applications, that thread is the toolkit thread: the Event Dispatch Thread (EDT) for Swing or the JavaFX Application Thread. See Threading and locking in LuciadLightspeed for more information about the threading rules in a LuciadLightspeed application.

To modify the population of the first city, we:

  • Take the write lock

  • Access the first city, using the query method

  • Update the population using the ILcdDatObject API

  • Inform the model that we have changed that particular domain object by calling the elementChanged method. We do that so that the model can inform its listeners when fireCollectedModelChanged is called in the finally block.

//Modifying a model requires a write lock.
//In a real application this can only be done on a specific thread.
//See the threading guidelines for more information
try (TLcdLockUtil.Lock lock = TLcdLockUtil.writeLock(model)) {
  //Dummy operation for illustration purposes:
  //Take the first city from the model, and increase the population by one
  try (Stream<ILcdDataObject> cities = model.query(ILcdModel.all().limit(1))) {
    cities.forEach(city -> {
      String cityName = (String) city.getValue("CITY");
      Integer population = (Integer) city.getValue("TOT_POP");
      city.setValue("TOT_POP", population + 1);

      System.out.println(String.format("Population of city %s has been updated in the model",
                                       cityName));

      //We need to inform the ILcdModel that the element has been changed
      //Do not yet fire an event to the listener as we are still locking the model
      //Firing the event from inside the lock will result in deadlocks
      model.elementChanged(city, ILcdModel.FIRE_LATER);
    });
  }
} finally {
  System.out.println("Informing the listeners about the changes in the model");
  //Right after releasing the lock, let the model inform its listeners about the changes
  model.fireCollectedModelChanges();
}

Operations that require a write lock include:

  • Model operations, such as addElement or elementChanged

  • Element operations, such as adding points to a polyline

Always call the fireCollectedModelChanges method as soon as you release a write lock. For instance, if you take two model write locks in a row, call fireCollectedModelChanges right after you release the first lock, and before you take the second lock. Next, call fireCollectedModelChanges again right after you release the second lock.

Listening for changes in a model

Each time the data in an ILcdModel is changed, the ILcdModel informs its ILcdModelListener instances about the changes.

This can be useful when you have a UI that shows the properties of a domain object, for example, and the user changes the position of that domain object on the map. You need to update the UI with the new position, and you can do this in an ILcdModelListener implementation that you register with the ILcdModel.

In this tutorial, we implement an ILcdModelListener that listens for changes made to the cities in our model. When a city is changed, we print the city name and population.

Our implementation could look like:

//Creating an ILcdModelListener which will be informed when changes are made
ILcdModelListener modelListener = new ILcdModelListener() {
  @Override
  public void modelChanged(TLcdModelChangedEvent aEvent) {
    //The TLcdModelChangedEvent is a container object describing which elements were added/removed/changed
    //
    //Here we are only interested in changes to an element
    //See the class javadoc of TLcdModelChangedEvent for code which handles all cases
    //
    //As we are going to access properties of the domain objects, we need to take a lock
    try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(aEvent.getModel())) {
      Enumeration elements = aEvent.elements();
      while (elements.hasMoreElements()) {
        ILcdDataObject city = (ILcdDataObject) elements.nextElement();
        int change = aEvent.retrieveChange(city);
        if (change == TLcdModelChangedEvent.OBJECT_CHANGED) {
          String cityName = (String) city.getValue("CITY");
          Integer population = (Integer) city.getValue("TOT_POP");
          System.out.println(String.format("Listener has been informed about change to city %s. Population: %d",
                                           cityName,
                                           population));
        }
      }
    }
  }
};

and you register it with the ILcdModel using the addModelListener method:

model.addModelListener(modelListener);

If we now make a change to the model, as described in the Changing data in a model section, the output is:

Population of city Akron has been updated in the model
Informing the listeners about the changes in the model
Listener has been informed about change to city Akron. Population: 223020

Full code

import java.io.IOException;
import java.util.Enumeration;
import java.util.stream.Stream;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.model.ILcdModel;
import com.luciad.model.ILcdModelDecoder;
import com.luciad.model.ILcdModelListener;
import com.luciad.model.TLcdCompositeModelDecoder;
import com.luciad.model.TLcdModelChangedEvent;
import com.luciad.util.concurrent.TLcdLockUtil;
import com.luciad.util.service.TLcdServiceLoader;

public class WorkingWithModelsTutorial {

  public static void main(String[] args) throws IOException {
    ILcdModelDecoder modelDecoder =
        new TLcdCompositeModelDecoder(TLcdServiceLoader.getInstance(ILcdModelDecoder.class));
    ILcdModel model = modelDecoder.decode("Data/Shp/Usa/city_125.shp");

    //Accessing the model data requires a read lock
    //Reading, and taking the read lock, can be done on any thread
    try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(model)) {
      //Loop over the domain objects.
      //Here we limit us to 5 elements
      try (Stream<ILcdDataObject> domainObjects = model.query(ILcdModel.all().limit(5))) {
        domainObjects
            .forEach(city -> {
              String cityName = (String) city.getValue("CITY");
              Integer population = (Integer) city.getValue("TOT_POP");
              System.out.println(String.format("%s: population %d",
                                               cityName,
                                               population));
            });
      }
    }

    System.out.println();

    //Creating an ILcdModelListener which will be informed when changes are made
    ILcdModelListener modelListener = new ILcdModelListener() {
      @Override
      public void modelChanged(TLcdModelChangedEvent aEvent) {
        //The TLcdModelChangedEvent is a container object describing which elements were added/removed/changed
        //
        //Here we are only interested in changes to an element
        //See the class javadoc of TLcdModelChangedEvent for code which handles all cases
        //
        //As we are going to access properties of the domain objects, we need to take a lock
        try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(aEvent.getModel())) {
          Enumeration elements = aEvent.elements();
          while (elements.hasMoreElements()) {
            ILcdDataObject city = (ILcdDataObject) elements.nextElement();
            int change = aEvent.retrieveChange(city);
            if (change == TLcdModelChangedEvent.OBJECT_CHANGED) {
              String cityName = (String) city.getValue("CITY");
              Integer population = (Integer) city.getValue("TOT_POP");
              System.out.println(String.format("Listener has been informed about change to city %s. Population: %d",
                                               cityName,
                                               population));
            }
          }
        }
      }
    };
    model.addModelListener(modelListener);

    //Modifying a model requires a write lock.
    //In a real application this can only be done on a specific thread.
    //See the threading guidelines for more information
    try (TLcdLockUtil.Lock lock = TLcdLockUtil.writeLock(model)) {
      //Dummy operation for illustration purposes:
      //Take the first city from the model, and increase the population by one
      try (Stream<ILcdDataObject> cities = model.query(ILcdModel.all().limit(1))) {
        cities.forEach(city -> {
          String cityName = (String) city.getValue("CITY");
          Integer population = (Integer) city.getValue("TOT_POP");
          city.setValue("TOT_POP", population + 1);

          System.out.println(String.format("Population of city %s has been updated in the model",
                                           cityName));

          //We need to inform the ILcdModel that the element has been changed
          //Do not yet fire an event to the listener as we are still locking the model
          //Firing the event from inside the lock will result in deadlocks
          model.elementChanged(city, ILcdModel.FIRE_LATER);
        });
      }
    } finally {
      System.out.println("Informing the listeners about the changes in the model");
      //Right after releasing the lock, let the model inform its listeners about the changes
      model.fireCollectedModelChanges();
    }
  }
}