LuciadLightspeed provides a framework to access domain objects in a unified way. With this framework it is possible to access all domain objects of a LuciadLightspeed model, independent of their specific domain. Additionally, the framework provides information about the static structure of the domain objects as is typically provided by a UML diagram.

This article uses the term domain model to refer to the set of domain objects and their relationship as used in a specific domain. A LuciadLightspeed model refers to an ILcdModel, the container for a set of domain objects as described in LuciadLightspeed models.

Introducing the framework

LuciadLightspeed supports a multitude of domain models, for example AIXM51, SHP, DAFIF, and VPF. All these domain models have their own types of domain objects that represent the different concepts in their domains. LuciadLightspeed provides a framework for these specific domain objects which makes it easy to interact with the model and allows for example to retrieve the name of an airport, to create a new airspace, or to change the designator of a certain procedure.

In some cases, you might need to access the different domain models in a unified way. Consider for example the case of a label painter that is responsible for drawing a label containing the name of a domain object on the map. This label painter should work both for an AIXM51 airspace and for a DAFIF airport. For these use cases, LuciadLightspeed provides a framework that defines unified access to domain objects.

The com.luciad.datamodel package defines a common meta model that describes the static structure of the different domain models. The meta model structures domain objects using types, each with a different set of properties. TLcdDataType and TLcdDataProperty explicitly represent types and properties as objects at runtime, which makes the static structure of a domain model visible. The package also defines the ILcdDataObject interface that enables unified access to domain objects.

The java.lang.reflect package provides similar introspection capabilities for Java classes and Java objects. One advantage of the com.luciad.datamodel package is that you can use it in situations where new data models are created at run-time. In such cases, different types often share the same instance class (for example TLcdDataObject). You will not be able to extract all necessary type information using Java reflection. Also, the com.luciad.datamodel package provides these capabilities at a higher level of abstraction, much closer to the actual data model.

A model is an abstraction of objects in the real world; a meta model is yet another abstraction, highlighting properties of the model itself. A well-known meta model is the Unified Modeling Language (UML) that is used to specify, visualize, modify, construct, and document the artifacts of an object-oriented software system.

Getting started

Handling models

The elements contained in an ILcdModel are domain objects. In case these domain objects implement ILcdDataObject, the model should have an ILcdModelDescriptor that implements ILcdDataModelDescriptor. This interface gives access to the data model on which the model’s elements are based.

A data model, represented using the TLcdDataModel class, is defined as a collection of types. A type is always declared by exactly one data model. Data models allow you to group types in logical units, much like Java classes are grouped in packages or XML schema types are grouped in XML schema documents.

Program: Displaying all element types that are defined for a certain model shows how you can use an ILcdDataModelDescriptor to print out all data types that are defined for elements within a certain model. Note that this does not require iteration over the elements of the model.

Program: Displaying all element types that are defined for a certain model
  public void print(ILcdModel model) {
    if (model.getModelDescriptor() instanceof ILcdDataModelDescriptor) {
      ILcdDataModelDescriptor desc = (ILcdDataModelDescriptor) model.getModelDescriptor();
      for (TLcdDataType t : desc.getModelElementTypes()) {
        System.out.println(t.getDataModel() + ":" + t.getName());
      }
    }
  }

For a more elaborate code sample refer to samples\util\dataModelDisplayTree. The sample shows how to display the information of a data model in a tree model.

Handling domain objects

The interface ILcdDataObject is the unified representation of a domain object. A data object holds a set of named properties. Each property contains either a simple primitive-type value or a reference to another data object. The interface ILcdDataObject provides methods to manipulate these properties. A data object also provides access to its type. The type defines which properties an object can have. Figure 1, “ILcdDataObject” shows ILcdDataObject in a diagram.

ILcdDataObject
Figure 1. ILcdDataObject

Using ILcdDataObject allows you to access the data stored inside a domain object. For example, Program: Printing all properties of an object shows how you can print the values of all properties of an object on the console.

Program: Printing all properties of an object
  public void print(ILcdDataObject dataObject) {
    for (TLcdDataProperty p : dataObject.getDataType().getProperties()) {
      System.out.println(p.getName() + " => " + dataObject.getValue(p));
    }
  }

For a more elaborate code sample refer to the samples.common.dataObjectDisplayTree.DataObjectDisplayTree class. This sample class shows how to display the properties of a data object in a tree model.

To change an object, you can use ILcdDataObject as shown in Program: Translating the data inside an object. This code snippet shows how to translate all string typed properties for a given data object.

Program: Translating the data inside an object
  public void translateDataObject(ILcdDataObject dataObject) {
    for (TLcdDataProperty p : dataObject.getDataType().getProperties()) {
      if (p.getType() == TLcdCoreDataTypes.STRING_TYPE) {
        dataObject.setValue(p, translate((String) dataObject.getValue(p)));
      }
    }
  }

  public String translate(String aString) {
    //return a translated version of the string
  }

Finally, you can create new instances with a TLcdDataType. Program: Creating instances with TLcdDataType shows how to create an instance for a given type. The code also sets the value of the name property.

Program: Creating instances with TLcdDataType
  public ILcdDataObject createObject(TLcdDataType type, String name) {
    ILcdDataObject result = type.newInstance();
    TLcdDataProperty p = type.getProperty("name");
    if (p != null) {
      result.setValue(p, name);
    }
    return result;
  }

Browsing type information

Given a data model, it is possible to browse for all kinds of type information. Program: Finding child types shows for example how to find all types that are declared in a given data model and that are a subtype of a given type.

Program: Finding child types
  public List<TLcdDataType> findSubTypes(TLcdDataModel dataModel, TLcdDataType type) {
    List<TLcdDataType> result = new ArrayList<TLcdDataType>();
    for (TLcdDataType t : dataModel.getDeclaredTypes()) {
      if (type.isAssignableFrom(t)) {
        result.add(t);
      }
    }
    return result;
  }

Defining a data model

Suppose that we have a simple model defining a flight plan with a name. Figure 2, “Extending the Flight Plan model” shows how to extend this flight plan with an association to a way point.

domainModelSample
Figure 2. Extending the Flight Plan model

You can create a data model for this extension using a TLcdDataModelBuilder. With such a builder, you create the WayPointBasedFlightPlan type and the WayPoint type as shown in Program: Creating the data model.

Program: Creating the data model
  public static TLcdDataModel createWayPointBasedFlightPlanModel() {
    TLcdDataModelBuilder builder = new TLcdDataModelBuilder("http://www.mydomain.com/datamodel/WayPointBasedFlightPlanModel");
    TLcdDataTypeBuilder typeBuilder = builder.typeBuilder("WayPointBasedFlightPlanType");
    typeBuilder.superType(FLIGHT_PLAN_DATA_TYPE);
    typeBuilder.addProperty("wayPoints", "WayPointType").collectionType(TLcdDataProperty.CollectionType.LIST);
    typeBuilder = builder.typeBuilder("WayPointType");
    typeBuilder.addProperty("name", TLcdCoreDataTypes.STRING_TYPE);
    typeBuilder.addProperty("identifier", TLcdCoreDataTypes.STRING_TYPE);
    return builder.createDataModel();
  }

Note how the wayPoints property on WayPointBasedFlightPlanType is created. Unlike the other properties, the type of this property has not yet been created. Therefore, you cannot use a type object to set the property’s type. Instead, you need to refer to that type using its name (WayPointType). Note that it does not matter if WayPointType is defined before or after the WayPointBasedFlightPlanType. The CollectionType.LIST marks the property as a list, this is an ordered collection implemented at runtime by instances of java.util.List.

The super type of WayPointBasedFlightPlanType is FlightPlanType. As such, WayPointBasedFlightPlanType inherits the name and geometry properties of FlightPlanType. Also, because of this relation, the WayPointBasedFlightPlanModel data model depends on the original FlightPlanModel data model.

Program: Creating the data model does not assign instance classes for the two new types. As a result, new instances will be instances of TLcdDataObject. You can change this by setting appropriate instance classes.

Explaining the meta model

Figure 3, “The meta model” shows a UML diagram of the LuciadLightspeed meta model. The three main classes defined by the meta model are TLcdDataModel, TLcdDataType, and TLcdDataProperty. The following sections provide more information on each of the main classes.

Metamodel
Figure 3. The meta model

Describing the static model

TLcdDataModel

A TLcdDataModel is a collection of types that forms a logical entity. As such you can compare it to a Java or UML package or an XML schema. A data model is identified by a unique name and can have a number of dependencies. A data model depends on another data model when domain model objects of the data model need to refer to domain model objects of another data model. This is for example the case when a type of the data model declares a property of a type of another data model. This is actually the most common case of dependency. As such, these dependencies are automatically defined by the framework. Dependencies can be cyclic.

A special kind of data model is an anonymous data model. By definition, this is a data model that has no name and declares no types. It only has dependencies. These data models are typically used as a simple way to represent a group of data models as a single data model.

TLcdDataType

A TLcdDataType represents the type of a data object. A data type describes the structure of a data object as a list of properties. Each of these properties themselves are of a certain type. A type always has a super type. The only exception to this rule is the Object type, which has no super type. A type (recursively) inherits all properties from its super type. Note that a type cannot redefine these inherited properties, they are always inherited as is.

A type is either a primitive type or a data object type. Primitive types are types which have no internal structure (no properties) and typically represent simple objects such as strings, numbers, dates, and so on. All primitive types either extend from another primitive type or directly from the Object type. A data object type is structured as a list of properties. All data object types either extend from another data object type or from the DataObject type. Instances of data object types implement ILcdDataObject.

Types are always defined in the context of a TLcdDataModel. Within a certain data model, types are uniquely defined by their name. As such, two types are equal if their data models are equal and they have the same name.

Types are mapped on Java classes. This is a many-to-one mapping; each type maps on one Java class, but different types can map on the same Java class. This class is called the type’s instance class. Instances of a type are always instances of the type’s instance class.

A type can be defined as an enumeration. This means that there are only a fixed set of possible instances. This set is directly available from the TLcdDataType interface. A prime example of such a type is a type of which the instance class is a Java enumeration.

You can create an instance of a data object type using the newInstance method. Note that primitive types do not support this method.

TLcdDataProperty

A TLcdDataProperty represents a property of a type. A property always belongs to a certain type (called its declaring type) and also has a certain type (called its type).

A property either has a single value or multiple values. In case of a single value, the value of the property for a certain data object is an instance of the type of the property. In case of multiple values, the property has a CollectionType that defines if property values are represented as a set, a list, or a map. Values of the property will be instances of which the class implements one of the Java Set, List or Map interfaces. The elements of these Set and List objects and the values of these Map objects are instances of the property’s type. The keys in the Map objects are instances of the property’s map key type.

By default, a property is defined as contained. This means that, by default, the values for properties are not shared among data objects. In other words, the graph of data objects of which the types only have contained properties is a tree. Cycles are only possible if a property is not contained.

Properties which are not nullable should always have a value that is different from null.

Creating data models

You create a data model using a TLcdDataModelBuilder. With such a builder, you create a TLcdDataTypeBuilder for each type you need to create. With the TLcdDataTypeBuilder you can build a data type. When you add a property to a data type builder, a TLcdDataPropertyBuilder is returned. That allows you to configure the properties.

Once all types and properties are built, you can ask the TLcdDataModelBuilder to create a data model. At the same time, this creates all types and properties. From that point on, the data model builder and all type and property builders depending on it will no longer accept any interaction and throw exceptions to indicate that the data model has been built. This ensures that you cannot modify a data model once it has been created.

The TLcdDataModelBuilder allows the creation of other TLcdDataModelBuilder instances. This is required to create data models that have a cyclic dependency.

Advanced topics

Getting started gives a quick introduction to the meta model and describes a simple use case. This section covers some more advanced topics.

Designing a data model

The following subsections describe the considerations to take when modeling domain objects.

Single or multiple data models

You have to decide whether you want to model your domain with one or more data models. Using a single model is the simplest. Using multiple data models allows partitioning of types into different groups, which can benefit larger models. For example, suppose that the sample described in Getting started is extended so that it includes multiple different types of way points to cater for nation-specific differences. In that case, you might decide to use two data models: one for the WayPointsBasedFlightPlanType, and one for the different way point types.

Choosing the right super type

The meta model supports single inheritance. Each type has exactly one super type. The only exception is the predefined TLcdCoreDataTypes.OBJECT_TYPE that serves as the root of all types. This type has no super type. Another essential core type is TLcdCoreDataTypes.DATA_OBJECT_TYPE. This type is the root of all data object types. By definition, only data object types can have properties defined on them. Types that do not extend from DATA_OBJECT_TYPE are called primitive types. These types are typically used for data of which the internal structure is a black box. Typical examples are simple types like String, Number, and Date of which the values are immutable. But that is not a hard constraint. In the end, it is up to you to decide which types are data object types and which are primitives. For example, it is a perfectly good practice to define a primitive type with instance class ILcd2DEditablePoint (which obviously is not immutable) if you do not want to expose the internal structure of the point (its x and y coordinate) using the data model API.

Many-valued properties

The meta model also supports many-valued properties. These properties are typically represented in the domain class as some form of collection. For example, in the Program: Creating the data model the wayPoints property is represented by a List. Note that if you provide an instance class for the WayPointsBasedFlightPlanType, then that class does not have to provide an accessor to set the value of the wayPoints property. Instead, modifications are done by changing the list object returned by the getter. Note that ILcdDataObject requires that implementations provide an initial (empty) list object.

Program: A list property
    ((List<ILcdDataObject>) flightPlan.getValue("wayPoints")).add(wayPoint);

Data model dependencies

Data models can depend on other data models. You can define dependencies either implicitly or explicitly. An implicit dependency is created when one of the types of a data model has a property of a type from another data model. These dependencies are automatically detected and added by the framework. This is the typical use case. However, the TLcdDataModelBuilder also allows to explicitly add dependencies. This is, for example, used to create an anonymous data model that merely serves as a container of other data models. Note that graphs with cyclic dependencies are supported.

Traversing an object graph

An often occurring pattern when dealing with data objects is traversal through the data contained in a graph of data objects. The ILcdDataObject interface allows you to traverse the object graph. Program: Traversing a data object traverses an ILcdDataObject by looking at the values of all its properties. If the value is not null, then depending on whether the property is a map, a collection, or just a single value, traversal continues on the value.

Program: Traversing a data object (from samples/common/dataObjectTraversal/DataObjectTraversalUtil)
public static void traverseDataObject(ILcdDataObject aObject) {
  for (TLcdDataProperty property : aObject.getDataType().getProperties()) {
    Object value = aObject.getValue(property);
    if (value != null) {
      if (property.getCollectionType() == null) {
        // Single-valued property
        traverseChild(property.getType(), value);
      } else {
        switch (property.getCollectionType()) {
        case MAP:
          Map<Object, Object> map = (Map<Object, Object>) value;
          for (Map.Entry<Object, Object> entry : map.entrySet()) {
            traverseChild(property.getMapKeyType(), entry.getKey());
            traverseChild(property.getType(), entry.getValue());
          }
          break;
        case LIST:
        case SET:
          for (Object element : (Collection<?>) value) {
            traverseChild(property.getType(), element);
          }
          break;
        }
      }
    }
  }
}

Program: Traversing an object traverses a single object. The type of the object is passed as argument. Note that this is merely the declared type. The object itself may be of a specialized type. This is in accordance with the standard rules on polymorphism.

  • If the type is primitive, then no further (generic) introspection is possible.

  • If the type is a data object type, then the object has to be an ILcdDataObject and traversal can continue as shown in Program: Traversing a data object.

  • Otherwise the type is TLcdCoreDataTypes.OBJECT_TYPE. This is an exceptional case where you do not have enough type information available. You have to examine the object that is passed as parameter. The method traverseObject uses the Java instanceof operator to determine how to continue the traversal.

Program: Traversing an object (from samples/common/dataObjectTraversal/DataObjectTraversalUtil)
public static void traverseChild(TLcdDataType aType, Object aObject) {
  if (aType.isPrimitive()) {
    traversePrimitive(aType, aObject);
  } else if (aType.isDataObjectType()) {
    traverseDataObject((ILcdDataObject) aObject);
  } else {
    traverseObject(aObject);
  }
}

public static void traverseObject(Object aObject) {
  if (aObject instanceof Map<?, ?>) {
    for (Map.Entry entry : ((Map<?, ?>) aObject).entrySet()) {
      // ...
    }
  } else if (aObject instanceof Collection<?>) {
    for (Object element : (Collection<?>) aObject) {
      traverseObject(element);
    }
  } else if (aObject instanceof ILcdDataObject) {
    traverseDataObject((ILcdDataObject) aObject);
  } else {
    traversePrimitive(null, aObject);
  }
}

public static void traversePrimitive(TLcdDataType aType, Object aValue) {
  // ...
}

A practical example with properties of type TLcdCoreDataTypes.OBJECT_TYPE is the modeling of a choice from XML schema or a union from C. Both a choice and a union property can have values of different types. You can model this by representing the choice or union by a single property. The type of that property is the common super type of the choice or union types. If the choice or union types contain both data object types and primitive types, this common super type is TLcdCoreDataTypes.OBJECT_TYPE.

Traversal as shown in the programs above works fine for graphs of objects in which there are no cycles. This is the case for most of the supported domain models in LuciadMap. In case of cycles, you need to add a check when a data object is traversed to see if the object has not already been traversed before.

Implementing ILcdDataObject

In some cases, you might not want your domain classes to extend from TLcdDataObject. In that case you have to implement ILcdDataObject yourself. Probably the easiest way to do this is to use delegation. There are cases, however, where you may not want to keep the state in a delegate TLcdDataObject. Program: Implementing ILcdDataObject shows an alternative implementation for the simple WayPoint type with two properties we used above. The getValue and setValue operations can be implemented easily based on a switch on the index of the property. This index is the position of this property in its declaring type’s properties. Because this index is fixed by the data model, you can use it as an efficient way of resolving properties. The getDataType operation can easily be implemented using a static reference to the type. In cases where more than one type uses the same domain class, you may need to add the data type as an instance variable.

Program: Implementing ILcdDataObject
  public final class WayPoint implements ILcdDataObject {
    private final TLcdDataType wayPointDataType = FLIGHT_PLAN_DATA_MODEL.getDeclaredType("WayPointType");

    private String name;
    private String identifier;

    @Override
    public TLcdDataType getDataType() {
      return wayPointDataType;
    }

    @Override
    public boolean hasValue(String propertyName) {
      return true;
    }

    @Override
    public boolean hasValue(TLcdDataProperty aProperty) {
      return true;
    }

    @Override
    public Object getValue(String propertyName) {
      TLcdDataProperty property = getDataType().getDeclaredProperty(propertyName);
      if (property == null) {
        throw new IllegalArgumentException("Property " + propertyName + " does not exist in data type " + getDataType());
      }
      return getValue(property);
    }

    @Override
    public Object getValue(TLcdDataProperty property) {
      if (property.getDeclaringType() != wayPointDataType) {
        throw new IllegalArgumentException("Invalid property " + property);
      }
      switch (property.getIndex()) {
      case 0:
        return name;
      case 1:
        return identifier;
      }
      throw new IllegalStateException("Unknown property " + property);
    }

    @Override
    public void setValue(String propertyName, Object value) {
      TLcdDataProperty property = getDataType().getDeclaredProperty(propertyName);
      if (property == null) {
        throw new IllegalArgumentException("Property " + propertyName + " does not exist in data type " + getDataType());
      }
      setValue(property, value);
    }

    @Override
    public void setValue(TLcdDataProperty property, Object value) {
      if (property.getDeclaringType() != wayPointDataType) {
        throw new IllegalArgumentException("Invalid property " + property);
      }
      switch (property.getIndex()) {
      case 0:
        name = (String) value;
        break;
      case 1:
        identifier = (String) value;
        break;
      }
      throw new IllegalStateException("Unknown property " + property);
    }
  }

Adding custom metadata

In some cases, you might want to attach additional information to a TLcdDataModel. The meta model supports this through the use of ILcdAnnotation. You can attach implementations of this tag to a data model. As data models are immutable, all annotations that are or will be attached to them should be immutable as well.

Program: Annotating a data model shows how to do this for a simple example. Suppose you have a data model that is derived from an XML schema. You want to add the location of this schema to the data model. First you define the SchemaLocation class that implements ILcdAnnotation and that keeps track of the location. Then, when your data model is being built, you can annotate the data model with an appropriate SchemaLocation instance.

First you create the custom annotation:

public class SchemaLocation implements ILcdAnnotation {
  private final String location;

  public String getLocation() {
    return location;
  }

  public SchemaLocation(String location) {
    this.location = location;
  }
}

and use it to annotate the data model:

Program: Annotating a data model
    dataModelBuilder.annotate(new SchemaLocation("http://www.luciad.com/samples/model/1.1"));

TLcdDataModel provides methods to retrieve all annotations that are attached to it. Program: Accessing annotations shows how to retrieve the schema location annotation from the data model.

Program: Accessing annotations
    SchemaLocation location = dataModel.getAnnotation(SchemaLocation.class);
    if (location != null) {
      System.out.println("Data model " + dataModel.getName() + " is derived from " + location.getLocation());
    }

Note that exactly the same annotation functionality is also defined on TLcdDataType and TLcdDataProperty.

Representing geometries

Some data types are directly linked to a geometry. For example, a FlightPlanType may be represented by a polyline. In most cases, you should not add the information about the geometry to the data type. Instead, the data type should be given an appropriate instance class that implements an appropriate ILcdShape interface. This keeps the data type focused on the data while the implementation of the interface ensures integration with LuciadLightspeed.

There are two main exceptions to this general rule:

  • Sometimes you may want to use a certain instance class for different geometries. The common practice in LuciadLightspeed for such cases is to have the instance class implement ILcdShapeList and put the geometry that needs to be supported inside the shape list. Most parts of LuciadLightspeed can handle such composite shapes. However, some information is lost. Given a data type with such an instance class, it is no longer possible to determine which type of shape it represents. You should use the TLcdShapeListAnnotation class to annotate the data type and prevent this loss of information. Another, less common, option is to use an ILcdShape with TLcdShapeAnnotation.

  • In some cases, it does make sense to represent geometry information also in the data type through properties. An example of such a data model is GML. This data model closely resembles the GML XML schema. As such, all data (whether it corresponds to a geometry or not) is modeled using properties. In such cases, you should annotate these data types with the TLcdHasAShapeAnnotation. This asserts that the state exposed by the ILcdShape interface is also exposed by one or more of these types' properties.

You can consult the API reference for a more detailed explanation on these annotation classes.