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
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
implementation to show the result the way we want by overriding the 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
implementation and add it to the map’s 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"; }
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
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(); }
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();
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
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
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()); }
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());
For the full source code, used in a functional application, check the selection sample.