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 IControllerIControllerIController 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:

context menu
Figure 1. Using a context menu to select the right feature and show some information.
selection rectangle
Figure 2. Using a rectangle to select several features at once.

Create a custom event handler

The API already manages the Alt + click sequence and allows us to inject our own ISelectionCandidateChooserISelectionCandidateChooserISelectionCandidateChooser implementation to show the result the way we want by overriding the chooseCandidateschooseCandidateschooseCandidates method.

Get the properties of a feature

We want to show information about a feature when users select it. For that, we use an IFeatureStateObserverIFeatureStateObserverIFeatureStateObserver implementation and add it to the map’s FeatureStateManagerFeatureStateManagerFeatureStateManager. When the user selects a feature, we loop over its properties to show them on the screen.

Program: Getting all the feature properties
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 DataTypeDataTypeDataType to represent the rectangle.

Program: Create the rectangle model
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.

Program: Create the rectangle layer
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 ViewMapTransformationViewMapTransformationViewMapTransformation 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 IFeatureModelUpdaterIFeatureModelUpdaterIFeatureModelUpdater 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.

Program: Update the rectangle model
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.

Program: 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.