What is IFeatureModel
To provide access to feature data, you must useuseuse a
feature modelfeature modelfeature model. IFeatureModel
IFeatureModel
IFeatureModel
acts as an adapter between your
application data and the LuciadCPillar Map.
As an adapter to your application data, IFeatureModel
IFeatureModel
IFeatureModel
serves multiple purposes:
-
Add a uniform representation for any possible application data:
Feature
Feature
Feature
.IFeatureModel
IFeatureModel
IFeatureModel
exposes application data asFeature
Feature
Feature
instances so that they can be used in a uniform way by the LuciadCPillar Map. -
Provide the LuciadCPillar Map with a pull mechanism to access application data.
-
Provide the LuciadCPillar Map with a push mechanism to observe changes to the application data.
-
Offer a mechanism for LuciadCPillar to make changes to the application data, for example using editingeditingediting or creationcreationcreation.
The next subsections will provide more details on these topics.
Feature
Application data can come in many different forms. Examples are:
-
A list of in-memory business objects
-
A database containing business objects
-
A file (SHP/GeoPackage/…​) containing feature data
-
A network stream with feature updates
-
…​
LuciadCPillar can not handle any kind of business data that could possibly exist. It is also mostly interested in
information that is needed for rendering or editing purposes. To accomplish this, IFeatureModel
IFeatureModel
IFeatureModel
exposes Feature
Feature
Feature
instances,
which have a uniform structure and have a descriptions of the data propertiesdata propertiesdata properties
and geometriesgeometriesgeometries they contain. They are a partial copy of the application data
that is of interest to the Map.
IFeatureModel
IFeatureModel
IFeatureModel
is responsible for converting business data (which can have any form) to Feature
Feature
Feature
instances
(which can be used by LuciadCPillar).
Uniquely identifying features
You can use feature IDs to identify features. Feature IDs are defined as unsigned 64-bit integers, and must be unique across the model. It is important that no 2 features have the same ID. When this happens, LuciadCPillar will consider them the same feature.
Adding properties to features
Features that have just an ID are not that interesting. Most likely, you want to associate some business data with each feature that you can then use during painting.
Each feature has an associated DataTypeDataTypeDataType that describes its property structure. During the creation of the feature, you can assignassignassign values to the various properties.
For more information on data types, see Describing a feature structure with a data model.
Features are read-only
Features can not be modified once they have been created. The main advantage of making Feature instances read-only is that they can be used on multiple threads (by LuciadCPillar) in a thread-safe way. If you want to modify a Feature, you can do so by making a copy that has the same ID.
Query (pull) mechanism to access application data
When the Map wants to render feature data, it is interested in only retrieving the features that are visible at a certain
camera location or at a certain Map scale. The main mechanism for this is a pull mechanism: the Map’s rendering backend
retrieves the features it is interested in using the IFeatureModel::query
IFeatureModel::query
IFeatureModel::query
method.
This method allows using filters by configuring them on FeatureQuery
FeatureQuery
FeatureQuery
.
These filters are used by the Map to only get the feature data it needs at a certain view point. For example:
-
BoundsBoundsBounds: the feature query should only return features within these bounds.
-
ConditionConditionCondition: typically a property-based filter. This for example allows the caller of the query method (typically the Map rendering backend) to only retrieve a subset of the features in the model. For example: only return the highways in a set of roads features.
-
Feature ID’sFeature ID’sFeature ID’s: Request
Feature
Feature
Feature
instances for specific feature IDs.
How are feature queries used by the Map ?
Imagine a feature data set with a large amount of roads data across the world: highways + smaller roads. The user is interested in visualizing this data in a scalable way.
-
At a zoomed-out level, you only want to see the main features in the dataset. Loading all small roads features for the entire world would not fit into memory or would take a huge amount of processing time before they can be displayed.
-
At a zoomed-in level, you want to see both highways and small roads, but only the ones that are visible in the region of the Map you’re looking at.
The Map accomplishes this by using feature queries with bounds and conditions on it.
-
At a zoomed-out level, a condition is used to limit the amount of features that is returned, even when the bounds on the
FeatureQuery
FeatureQuery
FeatureQuery
object covers a large part of the world. -
At a zoomed-in level, the used condition allows returning more (or all) types of features, but the size of the bounds on the
FeatureQuery
FeatureQuery
FeatureQuery
object is small. The end result is again, a relatively small set of features.
See the visualizing feature data article for more information on how to configure the Map to display feature data in a scalable way.
Observer (push) mechanism to observe changes to the application data
Next to the pull mechanism that was explained before, there is also the need for a push mechanism. When models don’t change (like is for example the case for static roads data), there is only a need for the pull mechanism.
This changes when the data backend (that is wrapped by IFeatureModel) is dynamic in nature. Examples are:
-
pure dynamic data: for example
TrackFeatureModel
that is used in the data formats sample. This model’s data backend is a stream of location updates for tracks. -
semi-dynamic data: the data is mostly static, but the user can also edit it. See for example the editing sample for such a use case.
In both cases, the Map’s rendering backend will still use the pull mechanism to retrieve the data that it’s interested
in. Detecting changes to the backend, is not handled by IFeatureModel::query
IFeatureModel::query
IFeatureModel::query
though. This would require the Map to call the query function continuously, which would be highly inefficient.
To resolve this, an observer mechanism was added to IFeatureModel: IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
.
This mechanism allows the Map (or anyone else) to get notifications when the application data changes. These changes can be
-
Updates. The feature location or data properties have changed, so the visualization needs to be updated.
-
Additions. A new features was added, so it needs to be added to the Map.
-
Removals: A feature was removed, so it needs to be removed from the Map.
So IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
is the core API that implements the
push mechanism. This API is complementary with the queryqueryquery API.
Both need to be implemented in a correct and consistent way for a model to be rendered correctly.
One important note is that IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
is decoupled
from IFeatureModel::queryIFeatureModel::queryIFeatureModel::query: `IFeatureModelObserver´ should notify
of any data changes, not only the ones that are currently visualized by the Map, or that were previously returned
by the query method. The Map rendering backend filters out the changes that it is not interested in.
Make changes to the application data
So far, we’ve only discussed feature information going from the application’s data backend to the Map, using both the
query (pull) and observer (push) mechanisms. IFeatureModel
IFeatureModel
IFeatureModel
however also allows making changes to the application
data: using IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
.
This is for example done when editingeditingediting or creatingcreatingcreating
features on the Map. Editing or creation generates new or modified Feature
Feature
Feature
instances, that are then sent to
IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
, which then modifies the underlying
application data based on these changes.
How does this tie in with the Map
The Map’s rendering backend is not interested in IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
itself. It doesn’t use it. It is only interested in calls to IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
.
The following happens when editing features on the Map:
-
The editoreditoreditor will make a new version of a Feature (for example with a modified geometry) and call
IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
. -
IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
applies this change to the application data. -
The application data changed, so
IFeatureModel
IFeatureModel
IFeatureModel
needs to notifynotifynotify that this happened. -
The Map’s rendering backend retrieves this notification and updates the visualization on the Map.
Persisting data
IFeatureModel
IFeatureModel
IFeatureModel
gives you control over when to persist the data that is being edited. For more information on saving changes
to feature models, see Working with feature models with save support.
Which IFeatureModel should I use
Which IFeatureModel
IFeatureModel
IFeatureModel
you need depends on the data backend you
are using, and the nature of the data: is it static data ? Or dynamic data ? This section will go over a few
cases and give advice on the type of model to use.
Decode feature data from files
You can decode feature data from file formats like for example GeoPackage, or SHP. Each supported vector format
offers a model decoder in the API. That model decoder returns a IFeatureModel
IFeatureModel
IFeatureModel
, based on a path to the data file.
A model based on a list of Feature instances
When you have a list of Feature instances, you can use FeatureModelBuilder
FeatureModelBuilder
FeatureModelBuilder
.
This is a convenience model implementation that is easy to use and doesn’t require much customization. It is however
not suited for large amounts of features: it doesn’t have a spatial index to efficiently filter on
boundsboundsbounds and it doesn’t have other indices to efficiently
filter based on conditionsconditionsconditions.
Only use this model when your data set is relatively small.
Connect to a database backend
You will need to implement your own IFeatureModel
IFeatureModel
IFeatureModel
instance that can connect to the database you are using.
Adapt your own application data
You will need to implement your own IFeatureModel
IFeatureModel
IFeatureModel
instance that can query the application data, send notifications
for changes and that converts the application data to Feature
Feature
Feature
instances.
A (network) stream of data
You will need to implement your own IFeatureModel
IFeatureModel
IFeatureModel
instance that can capture the data stream and convert it to
Feature
Feature
Feature
instances. A difficulty here is implementing the query
query
query
method: a stream cannot be queried. You can solve this by building up a local cache of the streamed data in
a way that it can be queried efficiently.
How can I implement an IFeatureModel
In some scenario’s, you need your own IFeatureModel
IFeatureModel
IFeatureModel
implementation. This section gives some guidance on how to
create your own implementation.
Use FeatureModelBuilder if possible
Implementing an |
Define the structure of Feature and its DataType
The first step when implementing (or creating) an IFeatureModel
IFeatureModel
IFeatureModel
is to define what a Feature
Feature
Feature
contains and how
it is structured. This is done by creating a DataModel
DataModel
DataModel
and
DataType
DataType
DataType
. For most use cases, the DataType
DataType
DataType
must at minimum contain
-
a Feature ID: to uniquely identify the feature. Possible ways to implement feature ID generation are:
-
A counter: For example in-memory, or in the database you connect to
-
Use an existing ID like a UUID or a string, and convert it to a 64-bits integer using hashing. Make sure to avoid hash collisions though.
-
An example of this can be found in the data formats sample:
DataModel createCustomerTrackDataModel() {
auto speedValueProperty = DataProperty::newBuilder().name("SpeedValue").valueType(DataType::getDoubleType()).build();
auto speedUnitProperty = DataProperty::newBuilder().name("SpeedUnit").valueType(DataType::getStringType()).build();
auto speedType = DataType::newBuilder().name("SpeedType").addProperty(speedValueProperty).addProperty(speedUnitProperty).build();
auto callSignProperty = DataProperty::newBuilder().name("CallSign").valueType(DataType::getStringType()).build();
auto locationProperty = DataProperty::newBuilder().name("Location").valueType(DataType::getGeometryType()).build();
auto azimuthProperty = DataProperty::newBuilder().name("CourseOverGround").valueType(DataType::getDoubleType()).build();
auto groundSpeedProperty = DataProperty::newBuilder().name("GroundSpeed").valueType(speedType).build();
auto trackType = DataType::newBuilder()
.name("TrackType")
.addProperty(callSignProperty)
.addProperty(locationProperty)
.addProperty(azimuthProperty)
.addProperty(groundSpeedProperty)
.addAnnotation(IDataAnnotationFactory::create([&](const DataType& dataType) {
auto path = DataPropertyPath::newBuilder().originType(dataType).property(locationProperty).build();
return std::make_shared<GeometryDataAnnotation>(path);
}))
.build();
return DataModel::newBuilder().name("http://www.mydomain.com/datamodel/TrackModel").addDataType(speedType).addDataType(trackType).build();
}
DataModel TrackDataModel = createCustomerTrackDataModel();
DataType TrackType = TrackDataModel.findDataType("TrackType").value();
ModelMetadata TrackModelMetadata = ModelMetadata::newBuilder().title("Tracks").build();
DataPropertyPath CallSignPropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("CallSign").build();
DataPropertyPath LocationPropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("Location").build();
DataPropertyPath AzimuthPropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("CourseOverGround").build();
DataPropertyPath GroundSpeedValuePropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("GroundSpeed").property("SpeedValue").build();
DataPropertyPath GroundSpeedUnitPropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("GroundSpeed").property("SpeedUnit").build();
Feature createFeature(CustomerTrack* track, const std::shared_ptr<CoordinateReference>& modelReference) {
std::shared_ptr<Point> trackLocation = GeometryFactory::createPoint(modelReference, track->getLon(), track->getLat(), track->getHeight());
return Feature::newBuilder()
.id(track->getId())
.dataType(CustomerTrackModel::getTrackType())
.value<std::string>(CallSignPropertyPath, track->getCallSign())
.value<std::shared_ptr<Geometry>>(LocationPropertyPath, std::move(trackLocation))
.value<double>(AzimuthPropertyPath, track->getAzimuth().getDegrees())
.value<double>(GroundSpeedValuePropertyPath, track->getGroundSpeed())
.value<std::string>(GroundSpeedUnitPropertyPath, "m/s")
.build();
}
private static DataModel CreateCustomerTrackDataModel()
{
var dataModelBuilder = DataModel.NewBuilder().Name("http://www.mydomain.com/datamodel/TrackModel");
var speedType = DataType.NewBuilder().Name("SpeedType")
.AddProperty(DataProperty.NewBuilder().Name("SpeedValue")
.ValueType(DataType.DoubleType)
.Build())
.AddProperty(DataProperty.NewBuilder().Name("SpeedUnit")
.ValueType(DataType.StringType)
.Build())
.Build();
dataModelBuilder.AddDataType(speedType);
var locationProperty = DataProperty.NewBuilder().Name("Location")
.ValueType(DataType.GeometryType)
.Build();
var azimuthProperty = DataProperty.NewBuilder().Name("CourseOverGround")
.ValueType(DataType.DoubleType)
.Build();
var trackType = DataType.NewBuilder().Name("TrackType")
.AddProperty(DataProperty.NewBuilder().Name("CallSign")
.ValueType(DataType.StringType)
.Build())
.AddProperty(locationProperty)
.AddProperty(azimuthProperty)
.AddProperty(DataProperty.NewBuilder().Name("GroundSpeed")
.ValueType(speedType)
.Build())
.AddAnnotation(new GeometryDataAnnotationFactory(locationProperty))
.Build();
dataModelBuilder.AddDataType(trackType);
return dataModelBuilder.Build();
}
private static Feature CreateFeature(CustomerTrack track, CoordinateReference modelReference)
{
Point trackLocation = GeometryFactory.CreatePoint(modelReference, track.Lon, track.Lat, track.Height);
Feature.Builder featureBuilder = Feature.NewBuilder().Id(track.Id).DataType(TrackType);
featureBuilder.Value<string>(CallSignPropertyPath, track.CallSign);
featureBuilder.Value<Geometry>(LocationPropertyPath, trackLocation);
featureBuilder.Value<double>(AzimuthPropertyPath, track.Azimuth.Degrees);
featureBuilder.Value<double>(GroundSpeedValuePropertyPath, track.GroundSpeed);
featureBuilder.Value<string>(GroundSpeedUnitPropertyPath, "m/s");
return featureBuilder.Build();
}
private static DataModel createCustomerTrackDataModel() {
var dataModelBuilder = DataModel.newBuilder().name("http://www.mydomain.com/datamodel/TrackModel");
var speedType = DataType.newBuilder().name("SpeedType")
.addProperty(DataProperty.newBuilder().name("SpeedValue")
.valueType(DataType.getDoubleType())
.build())
.addProperty(DataProperty.newBuilder().name("SpeedUnit")
.valueType(DataType.getStringType())
.build())
.build();
dataModelBuilder.addDataType(speedType);
var locationProperty = DataProperty.newBuilder().name("Location")
.valueType(DataType.getGeometryType())
.build();
var azimuthProperty = DataProperty.newBuilder().name("CourseOverGround")
.valueType(DataType.getDoubleType())
.build();
var trackType = DataType.newBuilder().name("TrackType")
.addProperty(DataProperty.newBuilder().name("CallSign")
.valueType(DataType.getStringType())
.build())
.addProperty(locationProperty)
.addProperty(azimuthProperty)
.addProperty(DataProperty.newBuilder().name("GroundSpeed")
.valueType(speedType)
.build())
.addAnnotation(new GeometryDataAnnotationFactory(locationProperty))
.build();
dataModelBuilder.addDataType(trackType);
return dataModelBuilder.build();
}
private static Feature createFeature(CustomerTrack track, CoordinateReference modelReference) {
Point trackLocation = GeometryFactory.createPoint(modelReference, track.getLon(), track.getLat(), track.getHeight());
Feature.Builder featureBuilder = Feature.newBuilder().id(track.getId()).dataType(_trackType);
featureBuilder.value(_callSignPropertyPath, track.getCallSign());
featureBuilder.<Geometry>value(_locationPropertyPath, trackLocation);
featureBuilder.value(_azimuthPropertyPath, track.getAzimuth().getDegrees());
featureBuilder.value(_groundSpeedValuePropertyPath, track.getGroundSpeed());
featureBuilder.value(_groundSpeedUnitPropertyPath, "m/s");
return featureBuilder.build();
}
Implement the query method
All IFeatureModel
IFeatureModel
IFeatureModel
implementations must implement the query
query
query
method. Such an implementation must take a couple of things into account.
General
IFeatureQueryCallback
The IFeatureQueryCallback
IFeatureQueryCallback
IFeatureQueryCallback
argument is used to return the result
of the query. Every Feature
Feature
Feature
that is included by the filters in FeatureQuery
FeatureQuery
FeatureQuery
must be passed to the IFeatureQueryCallback::handleFeature
IFeatureQueryCallback::handleFeature
IFeatureQueryCallback::handleFeature
method.
IFeatureQueryCallback::handleFeature
on its turn returns a boolean to indicate that the current query should stop.
Implementations of the query method must take this into account. Not doing so may have an impact on feature loading
performance.
for (const Feature& feature : featuresToReturn) {
if (!callback.handleFeature(feature)) {
break;
}
}
foreach (var feature in features)
{
if (!callback.HandleFeature(feature))
{
break;
}
}
for (Feature feature : featuresToReturn) {
if (!callback.handleFeature(feature)) {
break;
}
}
Threading
The implementation of query
query
query
must be implemented in a synchronous
and blocking way. When the query method returns, no more features should be passed to IFeatureQueryCallback
IFeatureQueryCallback
IFeatureQueryCallback
.
The query method can be called from any thread, so implementations must be thread-safe.
Condition
All Feature
Feature
Feature
instances that you return must be accepted by the query conditionquery conditionquery condition,
if available. Ideally, your IFeatureModel
IFeatureModel
IFeatureModel
uses some kind of indexing to make this as efficient as possible. This is for
example possible in many databases.
If there is no other way to filter on the given condition using the data backend itself, you can still loop over all features and evaluate the filter.
bool evaluateCondition = query.getCondition() || query.getBounds();
auto conditionEvaluator = FeatureExpressionEvaluatorFactory::createEvaluator(query);
for (auto& feature : features) {
if (!evaluateCondition || FeatureExpressionEvaluator::accept(feature, conditionEvaluator)) {
featuresToReturn.emplace_back(feature);
if (limit && *limit == featuresToReturn.size()) {
break;
}
}
}
var conditionEvaluator = FeatureExpressionEvaluatorFactory.CreateEvaluator(query);
features = features
.Where(f => FeatureExpressionEvaluator.Accept(f, conditionEvaluator))
.Take((int)limit.Value)
.ToList();
var conditionEvaluator = FeatureExpressionEvaluatorFactory.createEvaluator(query);
List<Feature> featuresToReturn = new ArrayList<>();
for (Feature feature : features) {
if (!FeatureExpressionEvaluator.accept(feature, conditionEvaluator)) {
continue;
}
featuresToReturn.add(feature);
if (limit != null && featuresToReturn.size() >= limit) {
break;
}
}
For more detail on using expressions, see Filtering data with expressions and conditions.
Bounds filter
All Feature
Feature
Feature
instances that you return must overlap with the query boundsquery boundsquery bounds,
if available. Ideally, your IFeatureModel
IFeatureModel
IFeatureModel
uses a spatial index to check this in an efficient way. You can:
-
use the spatial index of the data backend if it exists, for example in a spatial database.
-
implement a spatial index yourself.
-
don’t use a spatial filter. Beware, for larger models this can have a severe performance impact.
Note that spatial filtering is also implemented when using
FeatureExpressionEvaluatorFactory
FeatureExpressionEvaluatorFactory
FeatureExpressionEvaluatorFactory
, as in the
previous subsection.
Order
Returned features must be ordered according to the query sort operatorquery sort operatorquery sort operator. Ideally, this is also done by the data backend that supports this, for example a database. If this is not available, you can sort the list of features to return as follows:
std::optional<FeatureQuery::SortOperator> order = query.getSortBy();
if (order) {
std::sort(std::begin(features), std::end(features), order->getComparer());
}
var order = query.SortBy;
if (order != null)
{
features.Sort(new FeatureComparer(order));
}
var order = query.getSortBy();
if (order != null) {
features.sort(new FeatureComparator(order));
}
Feature ID’s
When the query has a feature id filterfeature id filterfeature id filter, you must only return the features with an id that is in this list.
Add support for observing changes to the data
Adding support for observing changes to the data can be done by registering observers and calling them whenever a feature in the data backend changes.
void CustomerTrackModel::addObserver(std::shared_ptr<IFeatureModelObserver> modelObserver) {
std::lock_guard<std::mutex> lockGuard(_observerMutex);
auto it = std::find_if(_modelObservers.begin(), _modelObservers.end(),
[&](const std::shared_ptr<IFeatureModelObserver>& observer) { return observer.get() == modelObserver.get(); });
if (it != _modelObservers.end()) {
throw InvalidArgumentException("CustomerTrackModel::addObserver: it is not allowed to add the same observer twice");
}
_modelObservers.emplace_back(std::move(modelObserver));
}
void CustomerTrackModel::removeObserver(const std::shared_ptr<IFeatureModelObserver>& modelObserver) {
std::lock_guard<std::mutex> lockGuard(_observerMutex);
auto it = std::find_if(_modelObservers.begin(), _modelObservers.end(),
[&](const std::shared_ptr<IFeatureModelObserver>& observer) { return observer.get() == modelObserver.get(); });
if (it != _modelObservers.end()) {
_modelObservers.erase(it);
} else {
throw InvalidArgumentException("CustomerTrackModel::removeObserver: attempting to remove an observer that was never added");
}
}
void CustomerTrackModel::fireChangeEvent(const FeatureModelEvent& changeEvent) {
// Fire change events outside the lock to make the code less prone to deadlocks
std::unique_lock<std::mutex> lockGuard(_observerMutex);
auto observersCopy = _modelObservers;
lockGuard.unlock();
for (const auto& observer : observersCopy) {
observer->onFeatureModelChanged(changeEvent);
}
}
private readonly IList<IFeatureModelObserver> _modelObservers = new List<IFeatureModelObserver>();
private void FireChangeEvent(FeatureModelEvent changeEvent)
{
lock (_modelObservers)
{
foreach (IFeatureModelObserver observer in _modelObservers)
{
observer.OnFeatureModelChanged(changeEvent);
}
}
}
public void AddObserver(IFeatureModelObserver modelObserver)
{
lock (_modelObservers)
{
_modelObservers.Add(modelObserver);
}
}
public void RemoveObserver(IFeatureModelObserver modelObserver)
{
lock (_modelObservers)
{
if (!_modelObservers.Remove(modelObserver))
{
throw new ArgumentException("Attempted to remove model observer that wasn't added by the AddObserver method", "modelObserver");
}
}
}
private final List<IFeatureModelObserver> _modelObservers = new ArrayList<>();
private void fireChangeEvent(FeatureModelEvent changeEvent) {
synchronized (_modelObservers) {
for (IFeatureModelObserver observer : _modelObservers) {
observer.onFeatureModelChanged(changeEvent);
}
}
}
@Override
public void addObserver(IFeatureModelObserver modelObserver) {
synchronized (_modelObservers) {
_modelObservers.add(modelObserver);
}
}
@Override
public void removeObserver(IFeatureModelObserver modelObserver) {
synchronized (_modelObservers) {
if (!_modelObservers.remove(modelObserver)) {
throw new IllegalArgumentException("Attempted to remove model observer that wasn't added by the AddObserver method");
}
}
}
Add support for editing and creation
Adding support for editing and creation can be done by implementing an IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
and returning an instance in the IFeatureModel::getUpdater
IFeatureModel::getUpdater
IFeatureModel::getUpdater
method.
-
apply the changes that are passed to its
update
method in the data backend -
fire a notification to all registered observersregistered observersregistered observers that a feature has changed.
For more information on feature updates and saving changes to feature models, see Working with feature models with save support.