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:

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

Program (C++): 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";
}
Program (C#): Getting all the feature properties
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 to represent the rectangle.

Program (C++): 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();
}
Program (C#): Create the rectangle model
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 (C++): Create the rectangle layer
auto painter = std::make_shared<SelectionRectanglePainter>();
_rectangleLayer = FeatureLayer::newBuilder().model(_rectangleModel).painter(std::move(painter)).build();
Program (C#): Create the rectangle layer
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 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.

Program (C++): 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());
}
Program (C#): Update the rectangle model
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 (C++): 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());
Program (C#): Select all features
    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 these samples:

  • C++: cpillar_sample_selection

  • C#: csharp_sample_selection