What are the parts of a data model?
If you’ve written object-oriented code, the core of a data model maps onto ideas you already know: a data type is like a class, a data property like a field, and a data object like an instance. The full set of parts is:
- DataType
-
Like a class, a data type describes an object through the set of properties it has. It’s a blueprint, not the data itself.
- DataProperty
-
Like a field, a data property is one named value within a data type. The value has a type of its own, and can be a single value or a list of values.
- DataObject
-
Like an instance of a class, a data object is a self-contained value that holds the values described by a data type. It’s the value of a composite property: a property with a value type that is itself a data type.
- DataModel
-
A data model groups related data types, in the way a namespace or package groups related classes.
- DataAnnotation
-
Extra information attached to a data type, or property, much like a Java annotation or a C# attribute.
- DataPropertyPath
-
An address that points to a value inside a data type, like writing
address.cityto reach a nested field. It can step through composites and list indices to arrive at a potentially nested value.
Together, these allow you to describe the structure of the FeatureFeatureFeature objects provided by an IFeatureModelIFeatureModelIFeatureModel.
A FeatureFeatureFeature is such an instance: it has a DataTypeDataTypeDataType that describes what values can be retrieved from it.
Next to the full DataModelDataModelDataModel, IFeatureModelIFeatureModelIFeatureModel objects also provide
accessaccessaccess
to the list of DataTypes of all features that actually appear in the model.
Creating a data model
You use the builder pattern to create data models, types, and properties. Data annotations are simple objects. You can create them like any other regular object type.
For example:
return DataProperty::newBuilder().name("geometry").valueType(DataType::getGeometryType()).build();
return DataProperty.NewBuilder().Name("geometry").ValueType(DataType.GeometryType).Build();
return DataProperty.newBuilder()
.name("geometry")
.valueType(DataType.getGeometryType())
.build();
return DataType::newBuilder()
.name("TestDataType")
.addProperty(someProperty)
.addProperty(geometryProperty)
.addAnnotation(IDataAnnotationFactory::create([&](const auto& dataType) {
return std::make_shared<GeometryDataAnnotation>(DataPropertyPath::newBuilder().originType(dataType).property(geometryProperty).build());
}))
.build();
return DataType.NewBuilder()
.Name("TestDataType")
.AddProperty(someProperty)
.AddProperty(geometryProperty)
.AddAnnotation(new GeometryDataAnnotationFactory(geometryProperty))
.Build();
return DataType.newBuilder()
.name("TestDataType")
.addProperty(TestFeatureModelFactory.SomeProperty)
.addProperty(geometryProperty)
.addAnnotation(new GeometryDataAnnotationFactory(geometryProperty))
.build();
return DataModel::newBuilder().name("http://www.mydomain.com/datamodel/TestFeatureModel").addDataType(dataType).build();
return DataModel.NewBuilder()
.Name("http://www.mydomain.com/datamodel/TestFeatureModel")
.AddDataType(dataType)
.Build();
return DataModel.newBuilder()
.name("http://www.mydomain.com/datamodel/TestFeatureModel")
.addDataType(dataType)
.build();
Accessing values in a feature
A FeatureFeatureFeature holds its values according to its DataTypeDataTypeDataType. You set values on a
Feature::BuilderFeature::BuilderFeature::Builder and read them back with
Feature::getValueFeature::getValueFeature::getValue, addressing each property with a
DataPropertyPathDataPropertyPathDataPropertyPath. A property holds a single value, a list of
values, or a nested composite value. The same API serves all three.
Scalar values
A scalar property holds one value of a primitive type (boolean, integer, floating point or string) or a geometry. Set it by passing the value, and read it back as an optional that is empty when the property is unset.
featureBuilder.value<bool>(boolPropertyPath, boolValue);
featureBuilder.value<int32_t>(intPropertyPath, intValue);
featureBuilder.value<int64_t>(longPropertyPath, longValue);
featureBuilder.value<float>(floatPropertyPath, floatValue);
featureBuilder.value<double>(doublePropertyPath, doubleValue);
featureBuilder.value<std::string>(stringPropertyPath, stringValue);
featureBuilder.value<std::shared_ptr<Geometry>>(geometryPropertyPath, geometryValue);
featureBuilder.Value<bool>(boolPropertyPath, boolValue);
featureBuilder.Value<int>(intPropertyPath, intValue);
featureBuilder.Value<long>(longPropertyPath, longValue);
featureBuilder.Value<float>(floatPropertyPath, floatValue);
featureBuilder.Value<double>(doublePropertyPath, doubleValue);
featureBuilder.Value<string>(stringPropertyPath, stringValue);
featureBuilder.Value<Geometry>(geometryPropertyPath, geometryValue);
featureBuilder.value(booleanPropertyPath, booleanValue);
featureBuilder.value(intPropertyPath, intValue);
featureBuilder.value(longPropertyPath, longValue);
featureBuilder.value(floatPropertyPath, floatValue);
featureBuilder.value(doublePropertyPath, doubleValue);
featureBuilder.value(stringPropertyPath, stringValue);
featureBuilder.value(geometryPropertyPath, geometry);
auto optBool = feature.getValue<bool>(boolPropertyPath);
auto optInt = feature.getValue<int32_t>(intPropertyPath);
auto optLong = feature.getValue<int64_t>(longPropertyPath);
auto optFloat = feature.getValue<float>(floatPropertyPath);
auto optDouble = feature.getValue<double>(doublePropertyPath);
auto optString = feature.getValue<std::string>(stringPropertyPath);
auto optGeometry = feature.getValue<std::shared_ptr<Geometry>>(geometryPropertyPath);
auto unsetValue = feature.getValue<std::string>(unsetPropertyPath); // Unset property -> empty optional
var optBool = feature.GetValue<bool?>(boolPropertyPath);
var optInt = feature.GetValue<int?>(intPropertyPath);
var optLong = feature.GetValue<long?>(longPropertyPath);
var optFloat = feature.GetValue<float?>(floatPropertyPath);
var optDouble = feature.GetValue<double?>(doublePropertyPath);
var optString = feature.GetValue<string>(stringPropertyPath);
var optGeometry = feature.GetValue<Geometry>(geometryPropertyPath);
var unsetValue = feature.GetValue<string>(unsetPropertyPath); // Unset property -> null
Boolean optBoolean = feature.<Boolean>getValue(booleanPropertyPath);
Integer optInteger = feature.<Integer>getValue(intPropertyPath);
Long optLong = feature.<Long>getValue(longPropertyPath);
Float optFloat = feature.<Float>getValue(floatPropertyPath);
Double optDouble = feature.<Double>getValue(doublePropertyPath);
String optString = feature.<String>getValue(stringPropertyPath);
Geometry optGeometry = feature.<Geometry>getValue(geometryPropertyPath);
String unsetValue = feature.<String>getValue(unsetPropertyPath); // Unset property -> null
Values by name
You can also address a property by its name, without building a
DataPropertyPathDataPropertyPathDataPropertyPath yourself: the name is resolved against the
feature’s data type. This is convenient for occasional access. When you read or write the same property across
many features in a loop, it is recommended to resolve the name once with
DataType::getPropertyPathForDataType::getPropertyPathForDataType::getPropertyPathFor and reuse the returned
DataPropertyPathDataPropertyPathDataPropertyPath with the path-based overloads, which avoids resolving the name on every call.
// Address a property by its name instead of building a DataPropertyPath.
auto poiType = DataType::newBuilder()
.name("POI")
.addProperty(DataProperty::newBuilder().name("name").valueType(DataType::getStringType()).build())
.addProperty(DataProperty::newBuilder().name("visitors").valueType(DataType::getLongType()).build())
.build();
auto feature = Feature::newBuilder() //
.id(1)
.dataType(poiType)
.value<std::string>("name", "Brussels")
.value<int64_t>("visitors", 1800000)
.build();
std::optional<std::string> name = feature.getValue<std::string>("name"); // "Brussels"
std::optional<int64_t> visitors = feature.getValue<int64_t>("visitors"); // 1800000
// Address a property by its name instead of building a DataPropertyPath.
var poiType = DataType.NewBuilder()
.Name("POI")
.AddProperty(DataProperty.NewBuilder().Name("name").ValueType(DataType.StringType).Build())
.AddProperty(DataProperty.NewBuilder().Name("visitors").ValueType(DataType.LongType).Build())
.Build();
var feature = Feature.NewBuilder()
.Id(1)
.DataType(poiType)
.Value<string>("name", "Brussels")
.Value<long>("visitors", 1800000)
.Build();
string name = feature.GetValue<string>("name"); // "Brussels"
long? visitors = feature.GetValue<long?>("visitors"); // 1800000
// Address a property by its name instead of building a DataPropertyPath.
DataType poiType = DataType.newBuilder()
.name("POI")
.addProperty(DataProperty.newBuilder().name("name").valueType(DataType.getStringType()).build())
.addProperty(DataProperty.newBuilder().name("visitors").valueType(DataType.getLongType()).build())
.build();
Feature feature = Feature.newBuilder()
.id(1)
.dataType(poiType)
.value("name", "Brussels")
.value("visitors", 1800000L)
.build();
String name = feature.<String>getValue("name"); // "Brussels"
Long visitors = feature.<Long>getValue("visitors"); // 1800000
List values
A DataPropertyDataPropertyDataProperty can hold a list of values instead of a single value.
Set its collection typecollection typecollection type to
ListListList, and build a
DataPropertyPathDataPropertyPathDataPropertyPath that ends in an index to address a single element.
// A list-typed property holds an ordered list of values of its element type.
auto trackType = DataType::newBuilder()
.name("Track")
.addProperty(DataProperty::newBuilder() //
.name("time")
.valueType(DataType::getLongType())
.collectionKind(DataCollectionKind::List)
.build())
.build();
auto timePath = DataPropertyPath::newBuilder().originType(trackType).property("time").build();
auto firstTimePath = DataPropertyPath::newBuilder().originType(trackType).property("time").index(0).build();
// A list-typed property holds an ordered list of values of its element type.
var time = DataProperty.NewBuilder()
.Name("time")
.ValueType(DataType.LongType)
.CollectionKind(DataCollectionKind.List)
.Build();
var trackType = DataType.NewBuilder().Name("Track").AddProperty(time).Build();
var timePath = DataPropertyPath.NewBuilder().OriginType(trackType).Property(time).Build();
var firstTimePath = DataPropertyPath.NewBuilder().OriginType(trackType).Property(time).Index(0).Build();
// A list-typed property holds an ordered list of values of its element type.
DataProperty time = DataProperty.newBuilder()
.name("time")
.valueType(DataType.getLongType())
.collectionKind(com.luciad.datamodels.DataCollectionKind.List)
.build();
DataType trackType = DataType.newBuilder().name("Track").addProperty(time).build();
DataPropertyPath timePath = DataPropertyPath.newBuilder().originType(trackType).property(time).build();
DataPropertyPath firstTimePath = DataPropertyPath.newBuilder().originType(trackType).property(time).index(0).build();
You populate a list with a single bulk set. A null element marks an unknown value. Once the list is sized by a bulk set, you can overwrite an individual element through its indexed path.
// Bulk-set the list (a null element marks an unknown value), then overwrite one element through its indexed path.
auto feature = Feature::newBuilder()
.id(1)
.dataType(trackType)
.value<std::vector<std::optional<int64_t>>>(timePath, {std::optional<int64_t>{100}, std::nullopt, std::optional<int64_t>{300}})
.value<int64_t>(firstTimePath, 150)
.build();
// Bulk-set the list (a null element marks an unknown value), then overwrite one element through its indexed path.
var feature = Feature.NewBuilder().Id(1).DataType(trackType)
.Value(timePath, new List<long?> { 100L, null, 300L })
.Value(firstTimePath, 150L)
.Build();
// Bulk-set the list (a null element marks an unknown value), then overwrite one element through its indexed path.
Feature feature = Feature.newBuilder().id(1L).dataType(trackType)
.value(timePath, Arrays.asList(100L, null, 300L))
.value(firstTimePath, 150L)
.build();
Use getListSizegetListSizegetListSize to query the element count, and read individual
elements through their indexed path. An unset list returns an empty optional for both its size and its elements.
getListSize also accepts a property name, like the value accessors described in Values by name. Addressing an
individual element still requires an indexed DataPropertyPathDataPropertyPathDataPropertyPath.
std::optional<size_t> size = feature.getListSize(timePath); // 3
std::optional<int64_t> first = feature.getValue<int64_t>(firstTimePath); // 150
var size = feature.GetListSize(timePath); // 3
long? first = feature.GetValue<long?>(firstTimePath); // 150
Long size = feature.getListSize(timePath); // 3
Long first = feature.getValue(firstTimePath); // 150
Composite values and DataObject
A DataPropertyDataPropertyDataProperty whose value type is itself a non-primitive DataTypeDataTypeDataType is a composite property. Its value is a
DataObjectDataObjectDataObject: a self-contained value with its own properties, built with its
own builderbuilderbuilder.
// A composite property holds a DataObject: build the nested object, then set it on the Feature.
auto address = DataObject::newBuilder().dataType(addressType).value<std::string>(streetPath, "Main St").build();
auto person = Feature::newBuilder().id(1).dataType(personType).value<DataObject>(addressPath, address).build();
// A composite property holds a DataObject: build the nested object, then set it on the Feature.
var address = DataObject.NewBuilder().DataType(addressType).Value(streetPath, "Main St").Build();
var person = Feature.NewBuilder().Id(1).DataType(personType).Value(addressPath, address).Build();
// A composite property holds a DataObject: build the nested object, then set it on the Feature.
DataObject address = DataObject.newBuilder().dataType(addressType).value(streetPath, "Main St").build();
Feature person = Feature.newBuilder().id(1L).dataType(personType).value(addressPath, address).build();
You can read the whole DataObjectDataObjectDataObject back, or descend through the composite to one of its leaves.
// Read the whole composite, then descend to one of its leaves.
std::optional<DataObject> retrieved = person.getValue<DataObject>(addressPath);
std::optional<std::string> street = retrieved->getValue<std::string>(streetPath);
// Read the whole composite, then descend to one of its leaves.
var retrieved = person.GetValue<DataObject>(addressPath);
var street = retrieved.GetValue<string>(streetPath);
// Read the whole composite, then descend to one of its leaves.
DataObject retrieved = person.getValue(addressPath);
String street = retrieved.getValue(streetPath);
Composite values combine with lists: a list-typed composite property holds a list of DataObjectDataObjectDataObject elements.
// A list of composites: bulk-set the list, then navigate from an element to its inner leaf.
auto addressesPath = DataPropertyPath::newBuilder().originType(teamType).property("addresses").build();
auto mainSt = DataObject::newBuilder().dataType(addressType).value<std::string>(streetPath, "Main St").build();
auto highSt = DataObject::newBuilder().dataType(addressType).value<std::string>(streetPath, "High St").build();
std::vector<std::optional<DataObject>> addresses = {mainSt, highSt};
auto team = Feature::newBuilder().id(2).dataType(teamType).value<std::vector<std::optional<DataObject>>>(addressesPath, addresses).build();
auto secondPath = DataPropertyPath::newBuilder().originType(teamType).property("addresses").index(1).build();
std::optional<std::string> secondStreet = team.getValue<DataObject>(secondPath)->getValue<std::string>(streetPath);
// A list of composites: bulk-set the list, then navigate from an element to its inner leaf.
var addressesPath = DataPropertyPath.NewBuilder().OriginType(teamType).Property(addressesProperty).Build();
var mainSt = DataObject.NewBuilder().DataType(addressType).Value(streetPath, "Main St").Build();
var highSt = DataObject.NewBuilder().DataType(addressType).Value(streetPath, "High St").Build();
var team = Feature.NewBuilder().Id(2).DataType(teamType)
.Value(addressesPath, new List<DataObject> { mainSt, highSt })
.Build();
var secondPath = DataPropertyPath.NewBuilder().OriginType(teamType).Property(addressesProperty).Index(1)
.Build();
var secondStreet = team.GetValue<DataObject>(secondPath).GetValue<string>(streetPath);
// A list of composites: bulk-set the list, then navigate from an element to its inner leaf.
DataPropertyPath addressesPath = DataPropertyPath.newBuilder().originType(teamType).property(addressesProperty).build();
DataObject mainSt = DataObject.newBuilder().dataType(addressType).value(streetPath, "Main St").build();
DataObject highSt = DataObject.newBuilder().dataType(addressType).value(streetPath, "High St").build();
Feature team = Feature.newBuilder().id(2L).dataType(teamType)
.value(addressesPath, Arrays.asList(mainSt, highSt))
.build();
DataPropertyPath secondPath = DataPropertyPath.newBuilder().originType(teamType).property(addressesProperty).index(1).build();
DataObject secondElement = team.getValue(secondPath);
String secondStreet = secondElement.getValue(streetPath);