Using IFeatureModel
To give access to feature data, you must useuseuse an
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 many 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.
This article provides more details on these topics.
Working with Feature data
Application data can come in many different forms. Examples are:
-
A list of in-memory business objects
-
A database containing business objects
-
A file containing feature data, such as a SHP file or a GeoPackage file
-
A network stream with feature updates
To handle any kind of business data, and focus on the information needed for rendering or editing purposes, the LuciadCPillar
API offers IFeatureModel
IFeatureModel
IFeatureModel
. IFeatureModel
IFeatureModel
IFeatureModel
is responsible for converting business data of any form to Feature
Feature
Feature
instances, which LuciadCPillar can use.
IFeatureModel
IFeatureModel
IFeatureModel
exposes Feature
Feature
Feature
instances,
which have a uniform structure and have a description of the data propertiesdata propertiesdata properties
and geometriesgeometriesgeometries they contain. They’re a partial copy of the application data
that’s of interest for the Map.
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’s important that no two features have the same ID. When this happens, LuciadCPillar considers them the same feature.
Adding properties to features
Features that have just an ID aren’t 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.
Query (pull) mechanism to access application data
When the Map wants to render feature data, it wants to retrieve only those 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 back end
retrieves the wanted features through the IFeatureModel::query
IFeatureModel::query
IFeatureModel::query
method.
This method allows you to use filters by configuring them on FeatureQuery
FeatureQuery
FeatureQuery
.
The Map uses these filters to get only the feature data it needs at a certain view point. For example:
-
BoundsBoundsBounds: the feature query must only return features within these bounds.
-
ConditionConditionCondition: typically a property-based filter. This filter allows the caller of the query method — typically the Map rendering back end — to retrieve only a subset of the features in the model. It can return only the highways in a set of roads features, for example.
-
Feature IDsFeature IDsFeature IDs: 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 and smaller roads. You want to visualize this data in a scalable way.
-
At a zoomed-out level, you want to see only the main features in the dataset. Loading all small roads features for the entire world wouldn’t fit into memory or would take a huge amount of processing time to display them.
-
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 limits the number of returned features, even when the bounds on the
FeatureQuery
FeatureQuery
FeatureQuery
object cover 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, a pull mechanism may also be required.
If models don’t
change, such as static roads data, you only need the pull mechanism. This changes when the data back end — wrapped by IFeatureModel
IFeatureModel
IFeatureModel
— is dynamic in nature. Examples are:
-
Purely dynamic data: for example, the
CustomerTrackModel
that’s used in the Tracks sample. This model’s data back end is a stream of location updates for tracks. -
Semi-dynamic data: the data is mostly static, but the user can also edit it. See the editing sample for an example of such a use case.
In both cases, the Map’s rendering back end still uses the pull mechanism to retrieve the data that it’s interested
in. However, the detection of changes to the back end isn’t handled by IFeatureModel::query
IFeatureModel::query
IFeatureModel::query
. This would require the Map to call the query function continuously, which would be highly inefficient.
To resolve this, IFeatureModel
IFeatureModel
IFeatureModel
also offers an observer mechanism: 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 an update.
-
Additions: new features were added, so they need to be added to the Map.
-
Removals: a feature was removed, so it needs to be removed from the Map.
IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
is the core API that implements the
push mechanism. This API is complementary with the queryqueryquery API.
You must implement both in a correct and consistent way to render a model correctly.
One important note is that IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
is decoupled
from IFeatureModel::query
IFeatureModel::query
IFeatureModel::query
: IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
must transmit
all 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 back end filters out the changes that it isn’t interested in.
Making changes to the application data
So far, this article only discussed feature information going from the application’s data back end to the Map, using both
the
query (pull) and observer (push) mechanisms. However, IFeatureModel
IFeatureModel
IFeatureModel
also allows making changes to the application
data, through the use of IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
.
You make changes to the data 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 modifies the underlying
application data based on these changes.
How does this tie in with the Map?
The Map’s rendering back end isn’t interested in IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
itself. It doesn’t use it. It’s only interested in calls to IFeatureModelObserver
IFeatureModelObserver
IFeatureModelObserver
.
The following happens when editing features on the Map:
-
The editoreditoreditor makes a new version of a Feature, with a modified geometry for example, and calls
IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
. -
IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
applies this change to the application data. -
The application data changed, so
IFeatureModel
IFeatureModel
IFeatureModel
must to notifynotifynotify that this happened. -
The Map’s rendering back end retrieves this notification and updates the visualization on the Map.
Persisting data
IFeatureModel
IFeatureModel
IFeatureModel
gives you control over when to persist the edited data. For more information on saving changes
to feature models, see Working with feature models with save support.
Choosing a suitable IFeatureModel
Which IFeatureModel
IFeatureModel
IFeatureModel
you need depends on the data back end you
are using, and the static or dynamic nature of the data. This section goes over a few
cases and gives advice on the type of model to use.
- Decode feature data from files
-
You can decode feature data from file formats such as 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’s easy to use and doesn’t require much customization. It isn’t suited for large amounts of features however: 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 back end
-
You must implement your own
IFeatureModel
IFeatureModel
IFeatureModel
instance that can connect to the database you are using. - Adapt your own application data
-
You must implement your own
IFeatureModel
IFeatureModel
IFeatureModel
instance that can query the application data, send notifications for changes, and that converts the application data toFeature
Feature
Feature
instances. - A (network) stream of data
-
You must implement your own
IFeatureModel
IFeatureModel
IFeatureModel
instance that can capture the data stream and convert it toFeature
Feature
Feature
instances. Because a stream can’t be queried, you may find it challenging to implement thequery
query
query
method. To resolve this issue, you can build up a local cache of the streamed data so that you can query it efficiently.
How to implement an IFeatureModel
In some scenarios, 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
When you’re implementing or creating an IFeatureModel
IFeatureModel
IFeatureModel
, the first step is defining what a Feature
Feature
Feature
contains and how
it’s structured. You do so by creating a DataModel
DataModel
DataModel
and
DataType
DataType
DataType
. For most use cases, the DataType
DataType
DataType
must at least contain:
-
A Feature ID: to uniquely identify the feature. Possible ways to implement feature ID generation are:
-
Using a counter: in-memory for example, or in the database you connect to
-
Using an existing ID like a UUID or a string, and converting it to a 64-bits integer using hashing. Make sure to prevent hash collisions though.
-
For an example, see the Tracks 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 trajectoryProperty = DataProperty::newBuilder().name("Trajectory").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(trajectoryProperty)
.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 TrajectoryPropertyPath = DataPropertyPath::newBuilder().originType(TrackType).property("Trajectory").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());
std::shared_ptr<Geometry> trajectory = GeometryFactory::createPolyline(modelReference,
{{track->getStartLon(), track->getStartLat()},
{track->getEndLon(), track->getEndLat()}},
LineInterpolationType::Geodesic);
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<std::shared_ptr<Geometry>>(TrajectoryPropertyPath, std::move(trajectory))
.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 trajectoryProperty = DataProperty.NewBuilder().Name("Trajectory")
.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(trajectoryProperty)
.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);
Polyline trajectory = GeometryFactory.CreatePolyline(modelReference,
new List<Coordinate>() { new Coordinate(track.StartLon, track.StartLat), new Coordinate(track.EndLon, track.EndLat) }
, LineInterpolationType.Geodesic);
Feature.Builder featureBuilder = Feature.NewBuilder().Id(track.Id).DataType(TrackType);
featureBuilder.Value<string>(CallSignPropertyPath, track.CallSign);
featureBuilder.Value<Geometry>(LocationPropertyPath, trackLocation);
featureBuilder.Value<Geometry>(TrajectoryPropertyPath, trajectory);
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 considerations
- IFeatureQueryCallback
-
The
IFeatureQueryCallback
IFeatureQueryCallback
IFeatureQueryCallback
argument is used to return the result of the query. You must pass everyFeature
Feature
Feature
that the filters inFeatureQuery
FeatureQuery
FeatureQuery
include to theIFeatureQueryCallback::handleFeature
IFeatureQueryCallback::handleFeature
IFeatureQueryCallback::handleFeature
method.In turn,
IFeatureQueryCallback::handleFeature
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.Program: Calling IFeatureQueryCallbackfor (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 toIFeatureQueryCallback
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 possible in many databases, for example.
If there is no other way to filter on the given condition using the data back end 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 back end 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 you’re using
FeatureExpressionEvaluatorFactory
FeatureExpressionEvaluatorFactory
FeatureExpressionEvaluatorFactory
, as in the
previous subsection.
Order
You must order returned features according to the query sort operatorquery sort operatorquery sort operator. Ideally, the data back end supports that and takes care of it, like a database. If back end support isn’t 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 IDs
If the query has a feature id filterfeature id filterfeature id filter, you must only return the features with an ID that’s in this list.
Adding support for observing changes to the data
You can add support for observing changes to the data by registering observers and calling them whenever a feature in the data back end 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");
}
}
}
Adding support for editing and creation
You can add support for editing and creation 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 back end -
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.