This tutorial uses a step-by-step approach to explain how you can change the selection of features on the map by customizing
a Controller
.
It covers:
-
Creating a custom event handler to change the default selection order
-
Showing some feature properties of a selected feature
-
Making a controller visualize something on the map
Scenario
In this tutorial, we visualize GeoPackage data representing states, rivers, and cities in the United States of America. We want to select features from those models, but this isn’t always possible. LuciadCPillar displays each model in a distinct layer, and displays some features on top of other features. That makes it difficult for users to select the features underneath other features.
We fix that problem by customizing the selection order. For that, we use a custom event handler in our IController
IController
IController
implementation. We create a context menu to let users choose the feature they want to select.
We also want to make it possible for users to select several features by drawing a rectangle over those features on the map.
These are the results we’re aiming for:

Create a custom event handler
The API already manages the Alt + click sequence and allows us to inject our own ISelectionCandidateChooser
ISelectionCandidateChooser
ISelectionCandidateChooser
implementation to show the result the way we want by overriding the chooseCandidates
chooseCandidates
chooseCandidates
method.
Get the properties of a feature
We want to show information about a feature when users select it. For that, we use an IFeatureStateObserver
IFeatureStateObserver
IFeatureStateObserver
implementation and add it to the map’s FeatureStateManager
FeatureStateManager
FeatureStateManager
.
When the user selects a feature, we loop over its properties to show them on the screen.
std::vector<DataProperty> propertiesWithoutGeometries;
for (const auto& property : _feature.value().getDataType().getDataProperties()) {
if (property.getValueType() != DataType::getGeometryType()) {
propertiesWithoutGeometries.emplace_back(property);
}
}
auto property = propertiesWithoutGeometries[index.row()];
if (index.column() == 0) {
return QString::fromStdString(property.getName());
}
auto path = DataPropertyPath::newBuilder().originType(_feature.value().getDataType()).property(property).build();
if (property.getValueType() == DataType::getStringType()) {
return QString::fromStdString(_feature.value().getValue<std::string>(path).value_or("Undefined"));
}
if (property.getValueType() == DataType::getDoubleType()) {
if (auto value = _feature.value().getValue<double>(path)) {
return QVariant::fromValue(value.value());
}
return "Undefined";
}
if (property.getValueType() == DataType::getIntType()) {
if (auto value = _feature.value().getValue<int32_t>(path)) {
return QVariant::fromValue(value.value());
}
return "Undefined";
}
if (property.getValueType() == DataType::getLongType()) {
if (auto value = _feature.value().getValue<int64_t>(path)) {
return QVariant::fromValue(value.value());
}
return "Undefined";
}
var dataType = feature.DataType;
foreach (var dataProperty in dataType.DataProperties)
{
string name = dataProperty.Name;
string value = null;
if (Equals(dataProperty.ValueType, DataType.StringType))
{
value = feature.GetValue<string>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build());
}
if (Equals(dataProperty.ValueType, DataType.IntType))
{
value = feature.GetValue<int?>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build())?.ToString();
}
if (Equals(dataProperty.ValueType, DataType.LongType))
{
value = feature.GetValue<long?>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build())?.ToString();
}
if (Equals(dataProperty.ValueType, DataType.DoubleType))
{
value = feature.GetValue<double?>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build())?.ToString(CultureInfo.InvariantCulture);
}
if (Equals(dataProperty.ValueType, DataType.FloatType))
{
value = feature.GetValue<float?>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build())?.ToString(CultureInfo.InvariantCulture);
}
if (Equals(dataProperty.ValueType, DataType.BooleanType))
{
value = feature.GetValue<bool?>(DataPropertyPath.NewBuilder().OriginType(dataType).Property(dataProperty).Build())?.ToString();
}
if (value != null)
{
_viewModel._propertyEntries.Add(new PropertyEntry() {Name = name, Value = value});
}
}
Select features with a rectangle
To draw a rectangle on the Map, we create a model and its associated layer. In doing so, we add the layer to the controller’s layer list, and paint it on top of everything on the map. Currently, the API doesn’t allow us to draw the rectangle in the screen (pixel) reference. Because of this limitation, we restrict this feature to a 2D map.
Create the model
We create a simple model for this rectangle, using a feature with a geometry DataType
DataType
DataType
to represent the rectangle.
namespace {
const DataProperty GeometryProperty = DataProperty::newBuilder().name("Geometry").valueType(DataType::getGeometryType()).build();
const DataType RectangleDataType = DataType::newBuilder()
.name("RectangleDataType")
.addProperty(GeometryProperty)
.addAnnotation(IDataAnnotationFactory::create([](const DataType& dataType) {
return std::make_shared<GeometryDataAnnotation>(
DataPropertyPath::newBuilder().originType(dataType).property(GeometryProperty).build());
}))
.build();
const DataPropertyPath RectanglePropertyPath = DataPropertyPath::newBuilder().originType(RectangleDataType).property("Geometry").build();
const DataModel RectangleDataModel = DataModel::newBuilder().name("http://www.mydomain.com/datamodel/Rectangle").addDataType(RectangleDataType).build();
}
const DataType& RectangleModel::getDataType() {
return RectangleDataType;
}
const DataPropertyPath& RectangleModel::getGeometryPropertyPath() {
return RectanglePropertyPath;
}
std::shared_ptr<IFeatureModel> RectangleModel::createModel() {
auto featureModelMetadata = FeatureModelMetadata::newBuilder().dataModel(RectangleDataModel).featureTypes({RectangleDataType}).build();
return FeatureModelBuilder::newBuilder()
.modelMetadata(ModelMetadata::newBuilder().title("EditHandleModel").build())
.featureModelMetadata(featureModelMetadata)
.features({})
.editable(true)
.build();
}
private static DataModel DataModel { get; } = CreateRectangleDataModel();
public static DataType RectangleType { get; } = DataModel.FindDataType("RectangleType");
private static DataProperty GeometryProperty { get; } = RectangleType.FindDataProperty("Geometry");
public static DataPropertyPath GeometryPropertyPath { get; } = DataPropertyPath.NewBuilder().OriginType(RectangleType).Property(GeometryProperty).Build();
private static DataModel CreateRectangleDataModel()
{
var geometryProperty = DataProperty.NewBuilder().Name("Geometry")
.ValueType(DataType.GeometryType)
.Build();
return DataModel.NewBuilder().Name("http://www.mydomain.com/datamodel/Rectangle")
.AddDataType(DataType.NewBuilder().Name("RectangleType")
.AddProperty(geometryProperty)
.AddAnnotation(new GeometryDataAnnotationFactory(geometryProperty))
.Build())
.Build();
}
public static IFeatureModel CreateRectangleModel()
{
var modelMetadata = ModelMetadata.NewBuilder().Title("Rectangle").Build();
IList<DataType> featureTypes = new List<DataType>();
featureTypes.Add(RectangleType);
var featureModelMetadata = FeatureModelMetadata.NewBuilder()
.DataModel(DataModel)
.FeatureTypes(featureTypes)
.Build();
var feature = Feature.NewBuilder()
.Id(0)
.DataType(RectangleType)
.Build();
var features = new List<Feature> {feature};
return FeatureModelBuilder.NewBuilder()
.ModelMetadata(modelMetadata)
.FeatureModelMetadata(featureModelMetadata)
.Features(features)
.Editable(true)
.Build();
}
Create the layer
We create a layer for the model containing the rectangle and include it in the controller layer list. As a result, the rectangle layer is painted on top of everything on the map.
auto painter = std::make_shared<SelectionRectanglePainter>();
_rectangleLayer = FeatureLayer::newBuilder().model(_rectangleModel).painter(std::move(painter)).build();
RectangleLayer = FeatureLayer.NewBuilder()
.Model(_rectangleModel)
.Painter(new SelectionRectanglePainter())
.Build();
Update the model using input events
We use the controller’s onEvent
method to direct drag events to another handler when the user presses the Shift key.
When that handler receives an event, we use the map’s ViewMapTransformation
ViewMapTransformation
ViewMapTransformation
to transform the four corners of the rectangle from screen coordinates to the map reference.
Once the transformations are complete, we use the model’s IFeatureModelUpdater
IFeatureModelUpdater
IFeatureModelUpdater
to update the feature in the rectangle model.
We use a polyline to represent the rectangle. As a result, the rectangle follows the window orientation even if the map rotates.
Coordinate extraCorner1{dragEvent->getStartLocation().x, dragEvent->getLocation().y};
Coordinate extraCorner2{dragEvent->getLocation().x, dragEvent->getStartLocation().y};
auto p0 = map->getViewMapTransformation().viewToMap(Map::LocationMode::ClosestSurface, dragEvent->getStartLocation());
auto p1 = map->getViewMapTransformation().viewToMap(Map::LocationMode::ClosestSurface, extraCorner1);
auto p2 = map->getViewMapTransformation().viewToMap(Map::LocationMode::ClosestSurface, dragEvent->getLocation());
auto p3 = map->getViewMapTransformation().viewToMap(Map::LocationMode::ClosestSurface, extraCorner2);
if (p0 && p1 && p2 && p3) {
std::vector<luciad::Coordinate> corners{p0->getLocation(), p1->getLocation(), p2->getLocation(), p3->getLocation()};
std::shared_ptr<Geometry> rectangle = GeometryFactory::createPolylineRing(map->getReference(), corners, LineInterpolationType::Linear);
auto updater = _rectangleModel->getUpdater();
if (!updater) {
throw luciad::RuntimeException(" Model must have an updater.");
}
auto feature = Feature::newBuilder()
.id(RectangleFeatureId)
.dataType(RectangleModel::getDataType())
.value(RectangleModel::getGeometryPropertyPath(), std::move(rectangle))
.build();
updater->update(FeatureModelUpdate::newBuilder().updateFeature(feature).build());
}
private void UpdateRectangle(DragEvent dragEvent, Map map)
{
Coordinate extraCorner1 = new Coordinate(dragEvent.StartLocation.X, dragEvent.Location.Y);
Coordinate extraCorner2 = new Coordinate(dragEvent.Location.X, dragEvent.StartLocation.Y);
var locationMode = Map.LocationMode.ClosestSurface;
var p0 = map.GetViewMapTransformation().ViewToMap(locationMode, dragEvent.StartLocation);
var p1 = map.GetViewMapTransformation().ViewToMap(locationMode, extraCorner1);
var p2 = map.GetViewMapTransformation().ViewToMap(locationMode, dragEvent.Location);
var p3 = map.GetViewMapTransformation().ViewToMap(locationMode, extraCorner2);
if (p0 == null || p1 == null || p2 == null || p3 == null) return;
IList<Coordinate> coordinates = new List<Coordinate> {p0.Location, p1.Location, p2.Location, p3.Location};
var rectangle =
GeometryFactory.CreatePolylineRing(map.Reference, coordinates, LineInterpolationType.Linear);
var feature = Feature.NewBuilder()
.Id(0)
.DataType(SelectionRectangleUtil.RectangleType)
.Value(SelectionRectangleUtil.GeometryPropertyPath, rectangle)
.Build();
_rectangleModel.GetUpdater().Update(FeatureModelUpdate.NewBuilder().UpdateFeature(feature).Build());
}
When the user stops dragging to complete the rectangle, we use the start and end position of the event to query the map and select all features.
auto queryFeaturesCallback = IMapQueryFeaturesCallback::create([map](const std::vector<MapQueryFeaturesResult>& features) {
auto& featureStateManager = map->getFeatureStateManager();
auto stateChange = FeatureStateManager::Change::create();
for (auto feature : features) {
stateChange->setState(feature.featureId, FeatureState::selected(), !featureStateManager.isStateEnabled(feature.featureId, FeatureState::selected()));
}
featureStateManager.applyStateChange(*stateChange);
});
Rectangle rectangle{dragEvent->getStartLocation(), dragEvent->getLocation()};
auto query = Map::FeatureQuery::newBuilder().rectangle(rectangle).build();
map->queryFeatures(query, queryFeaturesCallback);
auto feature = Feature::newBuilder().id(RectangleFeatureId).dataType(RectangleModel::getDataType()).build();
auto updater = _rectangleModel->getUpdater();
if (!updater) {
throw luciad::RuntimeException(" Model must have an updater.");
}
updater->update(FeatureModelUpdate::newBuilder().updateFeature(feature).build());
private void PerformSelection(DragEvent dragEvent, Map map)
{
var rectangle = new Luciad.Cartesian.Rectangle(dragEvent.StartLocation, dragEvent.Location);
var query = Map.FeatureQuery.NewBuilder().Rectangle(rectangle).Build();
map.QueryFeatures(query, new RectangleQueryHandler(map));
HideRectangle();
}
internal class RectangleQueryHandler : IMapQueryFeaturesCallback
{
private readonly Map _map;
public RectangleQueryHandler(Map map)
{
_map = map;
}
void IMapQueryFeaturesCallback.HandleFeatures(IList<MapQueryFeaturesResult> results)
{
var featureStateManager = _map.FeatureStateManager;
var change = FeatureStateManager.Change.Create();
foreach (var result in results)
{
var layerFeatureId = result.FeatureId;
change.SetState(layerFeatureId, FeatureState.Selected,
!featureStateManager.IsStateEnabled(layerFeatureId, FeatureState.Selected));
}
featureStateManager.ApplyStateChange(change);
}
}
For the full source code, used in a functional application, check the selection sample.