Goal

At the end of this tutorial, you should understand the essential concepts of the data modeling API and be familiar with its basic use.

The data modeling API is a framework that allows access to the properties of domain objects, the elements in an ILcdModel, in a uniform way. This offers several benefits:

  • You need to learn just one API to read or modify the properties of all kinds of domain objects.

    For example, to get the name of a city from a SHP file, you can use the same code as you would use to find the name of an airport in the AIXM5 format.

    So, not only does this provide you with a single code path; it also allows you to write a generic UI, for example, to display the properties of domain objects of all formats.

  • Classes designed to work with this API work with data of all formats.

    For example, you can use the same label painter to visualize labels on the map, next to the geometry of the domain objects. The label painters use the data modeling API to retrieve the label text from the domain object.

  • If you need to support a custom format, you give LuciadLighspeed access to the properties of your domain objects by implementing this API. That way, you ensure that all classes, like the label painter from the previous example, can deal with your data.

Overview of the main classes

We introduce the main classes with an example. Suppose we have a model with airport data.

Each airport has:

  • A name represented with a String

  • A location. To keep things simple, we model the location as a point with a longitude and latitude.

  • A collection of runways

A runway has:

  • An identifier, represented with an integer

  • A length, represented with a double

The concepts in the data modeling API are very similar to Java concepts. If we wanted to model the above in Java code, we could use:

public class Airport{
  private String name;
  private TLcdLonLatPoint location;//TLcdLonLatPoint is a point with a longitude and latitude
  private List<Runway> runways;

  //getters and setters for the properties
}

public class Runway{
  private int identifier;
  private double length;

  //getters and setters for the properties
}

The data modeling API consists of:

The relationship between those 3 classes is:

  • Each TLcdDataType can have a number of properties, just like a Java class can have a number of fields. The TLcdDataType class offers API to list all the available properties.

  • Each TLcdDataProperty is of a certain TLcdDataType, just like a Java field is of a certain class. The TLcdDataProperty class offers API to discover the type of the property.

  • Each TLcdDataProperty can indicate whether the property has a single value, or a collection of values.

  • Each ILcdDataObject instance is of a certain TLcdDataType, just like a Java object is of a certain class.

Therefore, we represent the airport example with this TLcdDataType structure:

The airport data model
Airport: is a TLcdDataType
 |_ name: is a TLcdDataProperty
 |_ location: is as TLcdDataProperty
 |_ runways: is as TLcdDataProperty, indicating it is a collection

Runway: is a TLcdDataType
 |_ identifier: is a TLcdDataProperty
 |_ length: is a TLcdDataProperty

An ILcdModel containing airports is populated with ILcdDataObject instances of the Airport data type. The ILcdDataObject interface exposes that the object is of the Airport data type so that you know which properties are available for the object. On top of that, it provides access to those properties. This is the equivalent of providing getters and setters for the properties of a Java object. For example, obtaining the name of an airport looks like:

ILcdDataObject airport = (ILcdDataObject)model.elements().next();
String airportName = airport.getValue("name");

The usage of these getters and setters is illustrated in more detail later on in this tutorial.

Relationship between TLcdDataModel and ILcdModel

Before you can use this API to access the properties of the data in your ILcdModel, you need to know:

  • Whether the model implemented this API

  • The object type of each domain object

To find out, you need to check whether the ILcdModelDescriptor of the ILcdModel implements the com.luciad.model.ILcdDataModelDescriptor interface. If it does, you can:

  • Use ILcdDataModelDescriptor.getDataModel() to obtain a com.luciad.datamodel.TLcdDataModel. A TLcdDataModel is a container for all used TLcdDataType instances.

    In this example, the data model would include the Airport and Runway types. So, when the Airport type declares that it has a property which is a collection of Runway, you know what a Runway is, because it is declared in that same TLcdDataModel.

    If we compare this with Java code again: there could be multiple Runway class definitions in the code base, but by using an import in the Airport class, you define exactly which Runway your Airport has.

    The same principle applies to TLcdDataModel. It groups all used TLcdDataType instances so that there can be no confusion.

  • Safely cast each domain object in the model to com.luciad.datamodel.ILcdDataObject, which is the interface needed to know what properties the object has and to access the values of the properties.

To make this all sound a bit less abstract, we will use all these concepts to create an ILcdModel with airports in the next section, and then use the ILcdDataObject API to print the properties of all the objects.

Bringing it into practice

As an exercise, we will create an ILcdModel with two airports:

  • Chicago O’Hare airport:

    • Name: Chicago O’Hare

    • Location: 41.9742° N, 87.9073° W

    • Runways:

      • Runway 1 with length 1829 metres

      • Runway 2 with length 2438 metres

  • LaGuardia airport:

    • Name: LaGuardia

    • Location: 40.7769° N, 73.8740° W

    • Runways:

      • Runway 1 with length 1957 metres

      • Runway 2 with length 2367 metres

For the TLcdDataModel, we use the one described in The airport data model.

This tutorial also contains the code to create the ILcdModel and its TLcdDataModel in Creating the TLcdDataModel and ILcdModel.

Creating your own ILcdModel and TLcdDataModel is something you typically only need if you want to support a custom data format. If you are decoding data from a supported format such as SHP, GeoPackage, or GeoJSON, the decoded model will already have a TLcdDataModel.

Querying the data model of an ILcdModel

Querying the TLcdDataModel of an ILcdModel allows you to learn about:

  • What types of domain objects the ILcdModel contains

  • What properties are available in the domain objects

  • What the type of those properties is

This is done through the ILcdDataModelDescriptor. You can use this descriptor to access the data model and check which types are used in the model. These snippets illustrate a few possible usages of the API:

Program: Checking what the type is of the domain objects in the model
//Check what the type of the domain objects is
Set<TLcdDataType> modelElementTypes = aDataModelDescriptor.getModelElementTypes();
System.out.println("The model contains elements of the following types:");
modelElementTypes.stream()
                 .map(TLcdDataType::getDisplayName)
                 .sorted()
                 .forEach(System.out::println);

resulting for our example data model in

The model contains elements of the following types:
Airport
Program: Checking what the TLcdDataType instances are that are defined in the TLcdDataModel
//Check what types the data model contains
Set<TLcdDataType> allDefinedTypes = dataModel.getDeclaredTypes();
System.out.println(String.format("The TLcdDataModel [%s] defines the following types", dataModel.getDisplayName()));
allDefinedTypes.stream()
               .map(TLcdDataType::getDisplayName)
               .sorted()
               .forEach(System.out::println);

resulting in

The TLcdDataModel [http://www.luciad.com/datamodel/tutorial/Airport] defines the following types
Airport
Runway
Program: Looping over all the properties of a certain TLcdDataType
//Check which properties the Airport type contains
TLcdDataType airportType = dataModel.getDeclaredType("Airport");
System.out.println("The Airport type has the following properties:");
airportType.getProperties().stream()
           .map(property -> String.format("Property [%s] of type [%s] (collection: [%s])",
                                          property.getDisplayName(),
                                          property.getType().getDisplayName(),
                                          property.isCollection()))
           .sorted()
           .forEach(System.out::println);

resulting in

The Airport type has the following properties:
Property [location] of type [Shape] (collection: [false])
Property [name] of type [String] (collection: [false])
Property [runways] of type [Runway] (collection: [true])

Accessing the property values of a domain object

All domain objects in our model are ILcdDataObject instances. This interface allows you to:

  • Determine the type of domain object, using ILcdDataObject.getDataType()

  • Read the value of a property using ILcdDataObject.getValue

  • Change the value of a property using ILcdDataObject.setValue

Combine that with what we learned in the previous section about looping over the properties of a TLcdDataType and we can now print all the properties of our domain objects in a generic way.

Program: printing the information about our domain objects
public static void printDomainObjectProperties(ILcdModel aModel) {
  try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(aModel)) {
    Enumeration elements = aModel.elements();
    while (elements.hasMoreElements()) {
      ILcdDataObject airport = (ILcdDataObject) elements.nextElement();

      String name = (String) airport.getValue("name");//if you know the property names, you can access them directly
      System.out.println(name);
      //We can also use the information contained in TLcdDataType
      //to loop over the properties
      //
      //This can for example be used to create a tree UI
      //displaying all properties of an object
      //
      //In this example, we limit ourself to printing
      //out some information about the airport
      printProperties(airport);
      System.out.println();

    }
  }
}

private static void printProperties(ILcdDataObject aDataObject) {
  TLcdDataType dataType = aDataObject.getDataType();
  List<TLcdDataProperty> properties = dataType.getProperties();
  for (TLcdDataProperty property : properties) {
    TLcdDataType propertyType = property.getType();

    //When the property is a primitive (e.g. a string, number, ...) we can
    //print it directly
    if (propertyType.isPrimitive()) {
      System.out.println(String.format("%s: %s", property.getDisplayName(), aDataObject.getValue(property)));
    }
    //If the property is an ILcdDataObject itself, we use recursion
    else if (propertyType.isDataObjectType()) {
      System.out.println(String.format("%s (Type: %s)", property.getDisplayName(), propertyType.getDisplayName()));
      if (!property.isCollection()) {
        printProperties((ILcdDataObject) aDataObject.getValue(property));
      } else {
        Collection<ILcdDataObject> collection = (Collection<ILcdDataObject>) aDataObject.getValue(property);
        int counter = 0;
        for (ILcdDataObject dataObject : collection) {
          System.out.println("Index: [" + counter++ + "]");
          printProperties(dataObject);
        }
      }
    }
  }
}

resulting in

Chicago O'Hare
name: Chicago O'Hare
location: com.luciad.shape.shape2D.TLcdLonLatPoint(87.9073, 41.9742)
runways (Type: Runway)
Index: [0]
identifier: 1
length: 1829.0
Index: [1]
identifier: 2
length: 2438.0

LaGuardia
name: LaGuardia
location: com.luciad.shape.shape2D.TLcdLonLatPoint(73.874, 40.7769)
runways (Type: Runway)
Index: [0]
identifier: 1
length: 1957.0
Index: [1]
identifier: 2
length: 2367.0

Creating the TLcdDataModel and ILcdModel

This section is only relevant if you need to create your own TLcdDataModel, for example when writing a custom model decoder for your own format.

We start by defining the TLcdDataModel including a TLcdDataType for the airports and runways. Creating a data model requires the use of the TLcdDataModelBuilder class:

Program: Creating the TLcdDataModel
//Use the builder to create the TLcdDataModel
//The builder is created with a unique name for the TLcdDataModel
//You can compare this with a Java package name
TLcdDataModelBuilder builder =
    new TLcdDataModelBuilder("http://www.luciad.com/datamodel/tutorial/Airport");

//We start with defining the runway type
TLcdDataTypeBuilder runwayTypeBuilder = builder.typeBuilder("Runway");
//Add the properties and specify their type
//Primitive types like strings, integers, doubles, ... are available in TLcdCoreDataTypes
runwayTypeBuilder.addProperty("identifier", TLcdCoreDataTypes.INTEGER_TYPE);
runwayTypeBuilder.addProperty("length", TLcdCoreDataTypes.DOUBLE_TYPE);

//Define the Airport Type
TLcdDataTypeBuilder airportTypeBuilder = builder.typeBuilder("Airport");
airportTypeBuilder.addProperty("name", TLcdCoreDataTypes.STRING_TYPE);
airportTypeBuilder.addProperty("location", TLcdShapeDataTypes.SHAPE_TYPE);//indicate location is an ILcdShape
airportTypeBuilder.addProperty("runways", runwayTypeBuilder)//same for runways
                  .collectionType(TLcdDataProperty.CollectionType.LIST);//indicate this property is actually a List of runways

//Create the TLcdDataModel instance
TLcdDataModel dataModel = builder.createDataModel();

Once we have the TLcdDataModel, we need to:

  1. Create the ILcdModel.

  2. Create and install the ILcdModelDescriptor. Because we want to indicate to LuciadLightspeed that our model uses the data modeling API, our model descriptor must be an implementation of ILcdDataModelDescriptor.

  3. Create the two airports, and add them to the model.

Program Creating and populating the ILcdModel
//Retrieve the types from the created data model
//These are needed to create the actual domain objects
TLcdDataType airportType = dataModel.getDeclaredType("Airport");
TLcdDataType runwayType = dataModel.getDeclaredType("Runway");

//Create the two airports domain objects
//The domain objects must be ILcdDataObject instances of the correct type,
//so we use the available method on the data type.
//Afterwards, we use the ILcdDataObject API to populate the properties
ILcdDataObject chicagoAirport = airportType.newInstance();//this creates an airport ILcdDataObject
chicagoAirport.setValue("name", "Chicago O'Hare");//fill in the values for each of the properties
chicagoAirport.setValue("location", new TLcdLonLatPoint(87.9073, 41.9742));

ILcdDataObject newYorkAirport = airportType.newInstance();
newYorkAirport.setValue("name", "LaGuardia");
newYorkAirport.setValue("location", new TLcdLonLatPoint(73.8740, 40.7769));

//The runways are ILcdDataObjects themselfs, but of the runwayType
//Create them, and add them to the airports
ILcdDataObject chicagoRunway1 = runwayType.newInstance();
chicagoRunway1.setValue("identifier", 1);
chicagoRunway1.setValue("length", 1829d);

ILcdDataObject chicagoRunway2 = runwayType.newInstance();
chicagoRunway2.setValue("identifier", 2);
chicagoRunway2.setValue("length", 2438d);

ILcdDataObject newYorkRunway1 = runwayType.newInstance();
newYorkRunway1.setValue("identifier", 1);
newYorkRunway1.setValue("length", 1957d);

ILcdDataObject newYorkRunway2 = runwayType.newInstance();
newYorkRunway2.setValue("identifier", 2);
newYorkRunway2.setValue("length", 2367d);

chicagoAirport.setValue("runways", Arrays.asList(chicagoRunway1, chicagoRunway2));
newYorkAirport.setValue("runways", Arrays.asList(newYorkRunway1, newYorkRunway2));

//Create the ILcdModel with the two airports
TLcdVectorModel model = new TLcdVectorModel(new TLcdGeodeticReference());
model.addElement(chicagoAirport, ILcdModel.NO_EVENT);
model.addElement(newYorkAirport, ILcdModel.NO_EVENT);

//Create the model descriptor and install it on the model
ILcdDataModelDescriptor modelDescriptor =
    new TLcdDataModelDescriptor(null,//No source name, as the model is created in code
                                "AIRPORT_TYPE", //Invent a type name
                                "Airports", //A nice display name
                                dataModel,//Specify what the datamodel is of the model
                                Collections.singleton(airportType),//specify that all domain objects are airports
                                null);
model.setModelDescriptor(modelDescriptor);

Our ILcdModel is as good as done. We just need to tweak it slightly to show the data on the map.

If we want to show the data on a map, we need to tell LuciadLightspeed where it can find the geometry of the domain object. In this example, we need to point it to the location property of the airport.

We do so by adding a com.luciad.datamodel.ILcdAnnotation to the airport TLcdDataType, telling LuciadLightspeed that it can find the geometry for the airport type in the location property:

Program: Specifying which property holds the geometry
//If we want to visualize our model on a map,
//we need to indicate to LuciadLightspeed what the main geometry is of our domain object
//This is done by adding an annotation on the airport type (=the type of our domain objects)
//that specifies that the location property holds the geometry of the airport that should
//be used when visualizing the data on the map
TLcdDataProperty locationProperty = airportType.getProperty("location");
airportType.addAnnotation(new TLcdHasGeometryAnnotation(locationProperty));

The TLcdHasGeometryAnnotation is the most commonly used annotation. To find out the other available annotations, see the Javadoc with the implementations of the ILcdAnnotation interface.

If necessary, you can also create and use custom annotations, as explained in the reference guide.

Full code

Program: the full code
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;

import com.luciad.datamodel.ILcdDataObject;
import com.luciad.datamodel.TLcdCoreDataTypes;
import com.luciad.datamodel.TLcdDataModel;
import com.luciad.datamodel.TLcdDataModelBuilder;
import com.luciad.datamodel.TLcdDataProperty;
import com.luciad.datamodel.TLcdDataType;
import com.luciad.datamodel.TLcdDataTypeBuilder;
import com.luciad.model.ILcdDataModelDescriptor;
import com.luciad.model.ILcdModel;
import com.luciad.model.TLcdDataModelDescriptor;
import com.luciad.model.TLcdVectorModel;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.shape.TLcdShapeDataTypes;
import com.luciad.shape.shape2D.TLcdLonLatPoint;
import com.luciad.util.TLcdHasGeometryAnnotation;
import com.luciad.util.concurrent.TLcdLockUtil;

public class DataModelTutorial {

  public static ILcdModel createModel() {
    //Use the builder to create the TLcdDataModel
    //The builder is created with a unique name for the TLcdDataModel
    //You can compare this with a Java package name
    TLcdDataModelBuilder builder =
        new TLcdDataModelBuilder("http://www.luciad.com/datamodel/tutorial/Airport");

    //We start with defining the runway type
    TLcdDataTypeBuilder runwayTypeBuilder = builder.typeBuilder("Runway");
    //Add the properties and specify their type
    //Primitive types like strings, integers, doubles, ... are available in TLcdCoreDataTypes
    runwayTypeBuilder.addProperty("identifier", TLcdCoreDataTypes.INTEGER_TYPE);
    runwayTypeBuilder.addProperty("length", TLcdCoreDataTypes.DOUBLE_TYPE);

    //Define the Airport Type
    TLcdDataTypeBuilder airportTypeBuilder = builder.typeBuilder("Airport");
    airportTypeBuilder.addProperty("name", TLcdCoreDataTypes.STRING_TYPE);
    airportTypeBuilder.addProperty("location", TLcdShapeDataTypes.SHAPE_TYPE);//indicate location is an ILcdShape
    airportTypeBuilder.addProperty("runways", runwayTypeBuilder)//same for runways
                      .collectionType(TLcdDataProperty.CollectionType.LIST);//indicate this property is actually a List of runways

    //Create the TLcdDataModel instance
    TLcdDataModel dataModel = builder.createDataModel();

    //Retrieve the types from the created data model
    //These are needed to create the actual domain objects
    TLcdDataType airportType = dataModel.getDeclaredType("Airport");
    TLcdDataType runwayType = dataModel.getDeclaredType("Runway");

    //Create the two airports domain objects
    //The domain objects must be ILcdDataObject instances of the correct type,
    //so we use the available method on the data type.
    //Afterwards, we use the ILcdDataObject API to populate the properties
    ILcdDataObject chicagoAirport = airportType.newInstance();//this creates an airport ILcdDataObject
    chicagoAirport.setValue("name", "Chicago O'Hare");//fill in the values for each of the properties
    chicagoAirport.setValue("location", new TLcdLonLatPoint(87.9073, 41.9742));

    ILcdDataObject newYorkAirport = airportType.newInstance();
    newYorkAirport.setValue("name", "LaGuardia");
    newYorkAirport.setValue("location", new TLcdLonLatPoint(73.8740, 40.7769));

    //The runways are ILcdDataObjects themselfs, but of the runwayType
    //Create them, and add them to the airports
    ILcdDataObject chicagoRunway1 = runwayType.newInstance();
    chicagoRunway1.setValue("identifier", 1);
    chicagoRunway1.setValue("length", 1829d);

    ILcdDataObject chicagoRunway2 = runwayType.newInstance();
    chicagoRunway2.setValue("identifier", 2);
    chicagoRunway2.setValue("length", 2438d);

    ILcdDataObject newYorkRunway1 = runwayType.newInstance();
    newYorkRunway1.setValue("identifier", 1);
    newYorkRunway1.setValue("length", 1957d);

    ILcdDataObject newYorkRunway2 = runwayType.newInstance();
    newYorkRunway2.setValue("identifier", 2);
    newYorkRunway2.setValue("length", 2367d);

    chicagoAirport.setValue("runways", Arrays.asList(chicagoRunway1, chicagoRunway2));
    newYorkAirport.setValue("runways", Arrays.asList(newYorkRunway1, newYorkRunway2));

    //Create the ILcdModel with the two airports
    TLcdVectorModel model = new TLcdVectorModel(new TLcdGeodeticReference());
    model.addElement(chicagoAirport, ILcdModel.NO_EVENT);
    model.addElement(newYorkAirport, ILcdModel.NO_EVENT);

    //Create the model descriptor and install it on the model
    ILcdDataModelDescriptor modelDescriptor =
        new TLcdDataModelDescriptor(null,//No source name, as the model is created in code
                                    "AIRPORT_TYPE", //Invent a type name
                                    "Airports", //A nice display name
                                    dataModel,//Specify what the datamodel is of the model
                                    Collections.singleton(airportType),//specify that all domain objects are airports
                                    null);
    model.setModelDescriptor(modelDescriptor);

    //If we want to visualize our model on a map,
    //we need to indicate to LuciadLightspeed what the main geometry is of our domain object
    //This is done by adding an annotation on the airport type (=the type of our domain objects)
    //that specifies that the location property holds the geometry of the airport that should
    //be used when visualizing the data on the map
    TLcdDataProperty locationProperty = airportType.getProperty("location");
    airportType.addAnnotation(new TLcdHasGeometryAnnotation(locationProperty));

    return model;
  }

  public static void printTypes(ILcdDataModelDescriptor aDataModelDescriptor) {
    TLcdDataModel dataModel = aDataModelDescriptor.getDataModel();

    //Check what the type of the domain objects is
    Set<TLcdDataType> modelElementTypes = aDataModelDescriptor.getModelElementTypes();
    System.out.println("The model contains elements of the following types:");
    modelElementTypes.stream()
                     .map(TLcdDataType::getDisplayName)
                     .sorted()
                     .forEach(System.out::println);

    System.out.println();

    //Check what types the data model contains
    Set<TLcdDataType> allDefinedTypes = dataModel.getDeclaredTypes();
    System.out.println(String.format("The TLcdDataModel [%s] defines the following types", dataModel.getDisplayName()));
    allDefinedTypes.stream()
                   .map(TLcdDataType::getDisplayName)
                   .sorted()
                   .forEach(System.out::println);

    System.out.println();

    //Check which properties the Airport type contains
    TLcdDataType airportType = dataModel.getDeclaredType("Airport");
    System.out.println("The Airport type has the following properties:");
    airportType.getProperties().stream()
               .map(property -> String.format("Property [%s] of type [%s] (collection: [%s])",
                                              property.getDisplayName(),
                                              property.getType().getDisplayName(),
                                              property.isCollection()))
               .sorted()
               .forEach(System.out::println);

    System.out.println();
  }

  public static void printDomainObjectProperties(ILcdModel aModel) {
    try (TLcdLockUtil.Lock lock = TLcdLockUtil.readLock(aModel)) {
      Enumeration elements = aModel.elements();
      while (elements.hasMoreElements()) {
        ILcdDataObject airport = (ILcdDataObject) elements.nextElement();

        String name = (String) airport.getValue("name");//if you know the property names, you can access them directly
        System.out.println(name);
        //We can also use the information contained in TLcdDataType
        //to loop over the properties
        //
        //This can for example be used to create a tree UI
        //displaying all properties of an object
        //
        //In this example, we limit ourself to printing
        //out some information about the airport
        printProperties(airport);
        System.out.println();

      }
    }
  }

  private static void printProperties(ILcdDataObject aDataObject) {
    TLcdDataType dataType = aDataObject.getDataType();
    List<TLcdDataProperty> properties = dataType.getProperties();
    for (TLcdDataProperty property : properties) {
      TLcdDataType propertyType = property.getType();

      //When the property is a primitive (e.g. a string, number, ...) we can
      //print it directly
      if (propertyType.isPrimitive()) {
        System.out.println(String.format("%s: %s", property.getDisplayName(), aDataObject.getValue(property)));
      }
      //If the property is an ILcdDataObject itself, we use recursion
      else if (propertyType.isDataObjectType()) {
        System.out.println(String.format("%s (Type: %s)", property.getDisplayName(), propertyType.getDisplayName()));
        if (!property.isCollection()) {
          printProperties((ILcdDataObject) aDataObject.getValue(property));
        } else {
          Collection<ILcdDataObject> collection = (Collection<ILcdDataObject>) aDataObject.getValue(property);
          int counter = 0;
          for (ILcdDataObject dataObject : collection) {
            System.out.println("Index: [" + counter++ + "]");
            printProperties(dataObject);
          }
        }
      }
    }
  }

  public static void main(String[] args) {
    ILcdModel model = createModel();
    printTypes((ILcdDataModelDescriptor) model.getModelDescriptor());
    printDomainObjectProperties(model);
  }
}