This tutorial uses a step-by-step approach to explain how you can load and visualize a static vector data set. It covers:

  • Limiting the amount of data loaded based on the map scale

  • Changing the feature visualization based on its state

  • Several options for styling features based on their properties

Scenario

In this tutorial, we visualize OpenStreetMap road data. The features in this dataset represent road segments. Each feature has a type property that describes how the specific road segment is classified, covering the range from motorways to residential streets. When the map is zoomed out, we want to show major roads only, such as motorways. We do not want to show less important roads, because they clutter up the map. Showing minor roads would also force us to load and visualize too much data in the case of large large datasets. As the map user zooms in on the map, it makes much more sense to load and visualize minor roads as well.

To visually distinguish between the road types, we also want to apply a distinct color to each type. Furthermore, we want the width of the roads to indicate their type. Finally, we want those widths to depend on the zoom level as well, so that major roads become even wider when a user zooms in on the map.

The end result we want to achieve is:

roads screenshot
Figure 1. The final result with major roads distinguished from minor roads through color and width

Loading and visualizing feature data

Limiting the data to be loaded based on map scale

To limit the amount of data that is loaded from the model for display in a map, the FeatureLayerFeatureLayerFeatureLayer builder offers a method addConditionaddConditionaddCondition. This method takes a minimum MapScaleMapScaleMapScale, a maximum map scale, and an ExpressionExpressionExpression as arguments. When the scale of the map enters the range between the minimum and the maximum map scale, all features accepted by the relevant expression are loaded. If this method is invoked multiple times with overlapping scale ranges, the map layer visualizes all features accepted by any expression that is linked to a scale range containing the current map scale.

Creating expressions

You can create expressions using the ExpressionFactoryExpressionFactoryExpressionFactory. In this case, we want an expression that matches features with specific values for the type property.

We start by creating equalequalequal expressions. An equal expression evaluates to true if the two expressions used to initialize it evaluate to the same value. To get an equal expression that checks if a feature is of a certain type, we use a valueReferencevalueReferencevalueReference expression for the property type, and a literalliteralliteral expression with the desired value for the type property.

To combine multiple equal expressions, we can use an ororor expression. An or expression evaluates to true if at least one of its component expressions evaluates to true.

To find road features of specific types, we create an expression that evaluates to true for features that have unclassified or residential as type. It identifies features that should be shown only when the user has zoomed in enough.

Program: Creating expressions
return ExpressionFactory::orOp({
    ExpressionFactory::equal(ExpressionFactory::valueReference(roadTypePropertyName), ExpressionFactory::literal("unclassified")),
    ExpressionFactory::equal(ExpressionFactory::valueReference(roadTypePropertyName), ExpressionFactory::literal("residential")),
});
return ExpressionFactory.OrOp(new List<Expression>()
{
    ExpressionFactory.Equal(ExpressionFactory.ValueReference(roadTypePropertyName),
        ExpressionFactory.Literal(new ExpressionValue("unclassified"))),
    ExpressionFactory.Equal(ExpressionFactory.ValueReference(roadTypePropertyName),
        ExpressionFactory.Literal(new ExpressionValue("residential")))
});
return ExpressionFactory.orOp(
    listOf(
        ExpressionFactory.equal(
            ExpressionFactory.valueReference(roadTypePropertyName),
            ExpressionFactory.literal(ExpressionValue("unclassified"))
        ),
        ExpressionFactory.equal(
            ExpressionFactory.valueReference(roadTypePropertyName),
            ExpressionFactory.literal(ExpressionValue("residential"))
        )
    )
)

Accessing properties in a Feature

We want to base the styling of a road feature on the type of the road. To do so, we must be able to retrieve the value of the type property.

A feature model offers information about the data types of the features it contains. Features have a DataTypeDataTypeDataType, which defines the properties of that feature. To retrieveretrieveretrieve the value of a property, we need to create a DataPropertyPathDataPropertyPathDataPropertyPath. We do not want to create such a path for each feature, so we create it once, based on the data types exposed by the feature model.

In this case, we expect a single feature type, which should have a type property. Based on that property, we create the data property path we require.

Program: Getting the type data property path
std::vector<DataType> dataTypes = featureModel->getFeatureModelMetadata().getFeatureTypes();
if (dataTypes.size() != 1) {
  std::cerr << "Expected model containing a single data type." << std::endl;
  return {};
}

DataType& dataType = dataTypes.at(0);
std::optional<DataProperty> typeProperty = dataType.findDataProperty(dataProperty);
if (!typeProperty) {
  std::cerr << "Expected data type containing a property named '" << dataProperty << "'" << std::endl;
  return {};
}

return DataPropertyPath::newBuilder().originType(dataType).property(*typeProperty).build();
if (featureModel.GetFeatureModelMetadata().FeatureTypes.Count != 1)
{
    MessageBox.Show("Expected model with a single feature type.");
    return null;
}

DataType dataType = featureModel.GetFeatureModelMetadata().FeatureTypes[0];
DataProperty roadTypeProperty = dataType.FindDataProperty(roadTypePropertyName);
DataProperty nameProperty = dataType.FindDataProperty(namePropertyName);
if (roadTypeProperty == null || nameProperty == null)
{
    MessageBox.Show("Expected model with a single feature type, containing a '" + roadTypePropertyName +
                    "' property and a '" + namePropertyName + "' property.");
    return null;
}

DataPropertyPath typePropertyPath =
    DataPropertyPath.NewBuilder().OriginType(dataType).Property(roadTypeProperty).Build();
DataPropertyPath namePropertyPath =
    DataPropertyPath.NewBuilder().OriginType(dataType).Property(nameProperty).Build();
if (featureModel.featureModelMetadata.featureTypes.size != 1) {
    throw RuntimeException("Expected model with a single feature type")
}

val dataType = featureModel.featureModelMetadata.featureTypes[0]
val roadTypeProperty = dataType.findDataProperty(roadTypePropertyName)
val nameProperty = dataType.findDataProperty(namePropertyName)
if (roadTypeProperty == null || nameProperty == null) {
    throw RuntimeException(
        "Expected model with a single feature type, containing a '" + roadTypePropertyName +
                "' property and a '" + namePropertyName + "' property."
    )
}
val typePropertyPath =
    DataPropertyPath.newBuilder().originType(dataType).property(roadTypeProperty).build()
val namePropertyPath =
    DataPropertyPath.newBuilder().originType(dataType).property(nameProperty).build()

Implementing the painter

To associate certain styles with certain features, scale levels, and so on, you need an implementation of a FeaturePainterFeaturePainterFeaturePainter. In many cases, implementing a feature painter requires these steps:

  1. Defining triggers for a visualization change

  2. Creating the necessary styles

  3. Implementing the paint logic

Defining triggers for a visualization change

You can handle the first step by implementing the configureMetadataconfigureMetadataconfigureMetadata method. By updating the submitted FeaturePainterMetadataFeaturePainterMetadataFeaturePainterMetadata instance, you can control which changes result in a re-execution of the paint logic.

Program: Feature painter metadata
void RoadsPainter::configureMetadata(FeaturePainterMetadata& metadata) const {
  metadata.painterDependsOnFeatureState(FeatureState::hover(), true);
  metadata.detailLevelScales(std::vector<MapScale>{HighToMedium, MediumToLow, LowToMin});
}
public void ConfigureMetadata(FeaturePainterMetadata metadata)
{
    metadata.PainterDependsOnFeatureState(FeatureState.Hover, true);
    metadata.PainterDependsOnFeature(true);
    metadata.DetailLevelScales(new List<MapScale>()
    {
        MapScale.FromDenominator(ScaleHighToMediumDenominator),
        MapScale.FromDenominator(ScaleMediumToLowDenominator),
        MapScale.FromDenominator(ScaleLowToMinDenominator)
    });
}
override fun configureMetadata(metadata: FeaturePainterMetadata) {
    metadata.painterDependsOnFeature(true)
    metadata.detailLevelScales(
        listOf(
            MapScale.fromDenominator(ScaleHighToMediumDenominator),
            MapScale.fromDenominator(ScaleMediumToLowDenominator),
            MapScale.fromDenominator(ScaleLowToMinDenominator)
        )
    )
}

In this case, we re-invoke the painter when:

  1. The selected or on-hover state of a feature is updated.

  2. The feature itself is updated. The type of the feature changes, for example.

  3. The map scale crosses one of the two thresholds we define. This results in three detail levels:

    • A detail level that is smaller than the first scale. This is detail level 0.

    • A detail level in between the first and the second scale. This is detail level 1.

    • A detail level that is larger than the second scale. This is detail level 2.

Creating the necessary styles

In this step, we create the styles for the road data. Our main goal is to create the styles only once. Creating a new style instance whenever a feature is painted would result in a lot of unnecessary overhead. It is much more efficient to create the styles statically, or when the painter is constructed, and then storing the styles.

Program: Initializing styles
void RoadsPainter::createStyles() {
  registerStyle(0, "motorway", 4, "#e990a0ff");
  registerStyle(1, "motorway", 6, "#e990a0ff");
  registerStyle(2, "motorway", 7, "#e990a0ff");
  registerStyle(3, "motorway", 7, "#e990a0ff");
  registerLabelStyle(2, "motorway", 14);
  registerLabelStyle(3, "motorway", 16);
}

void RoadsPainter::registerStyle(size_t detailLevel, const std::string& roadType, double width, Color color) {
  _regularStyles.emplace(std::make_pair(detailLevel, roadType), LineStyle::newBuilder().width(width).color(color).build());
  _selectedStyles.emplace(std::make_pair(detailLevel, roadType), LineStyle::newBuilder().width(width + 2).color(Color(255, 140, 0, 250)).build());
  _hoverStyles.emplace(std::make_pair(detailLevel, roadType), LineStyle::newBuilder().width(width + 2).color(color).build());
}

void RoadsPainter::registerLabelStyle(size_t detailLevel, const std::string& roadType, size_t fontSize) {
  auto regularTextStyle = TextStyle::newBuilder().fontSize(fontSize).build();
  auto selectedTextStyle = TextStyle::newBuilder().fontSize(fontSize).textColor(Color(196, 75, 0, 255)).build();
  auto hoverTextStyle = TextStyle::newBuilder().fontSize(fontSize).textColor(Color(255, 140, 0, 255)).build();
  _regularLabelStyles.emplace(std::make_pair(detailLevel, roadType), regularTextStyle);
  _selectedLabelStyles.emplace(std::make_pair(detailLevel, roadType), selectedTextStyle);
  _hoverLabelStyles.emplace(std::make_pair(detailLevel, roadType), hoverTextStyle);
}
static RoadsPainter()
{
    RegisterStyles(new StyleKey(0, "motorway"), MotorwayColor, 4);
    RegisterStyles(new StyleKey(1, "motorway"), MotorwayColor, 6);
    RegisterStyles(new StyleKey(2, "motorway"), MotorwayColor, 8);
    RegisterStyles(new StyleKey(3, "motorway"), MotorwayColor, 8);
    RegisterLabelStyles(new StyleKey(2, "motorway"), 14);
    RegisterLabelStyles(new StyleKey(3, "motorway"), 16);
}

private static void RegisterStyles(StyleKey key, Color color, double width)
{
    RegularStylesMap.Add(key, LineStyle.NewBuilder().Color(color).Width(width).Build());
    HoverStylesMap.Add(key, LineStyle.NewBuilder().Color(color).Width(width + 2).Build());
    SelectedStylesMap.Add(key,
        LineStyle.NewBuilder().Color(SelectionColor).Width(width + 2).Build());
}

private static void RegisterLabelStyles(StyleKey key, uint fontSize)
{
    var regularStyle = TextStyle.NewBuilder().FontSize(fontSize).Build();
    var selectedStyle = TextStyle.NewBuilder().FontSize(fontSize)
        .TextColor(Color.FromArgb(255, 255, 140, 0)).Build();
    var hoverStyle = TextStyle.NewBuilder().FontSize(fontSize)
        .TextColor(Color.FromArgb(255, 196, 75, 0)).Build();
    RegularLabelStylesMap.Add(key, regularStyle);
    HoverLabelStylesMap.Add(key, selectedStyle);
    SelectedLabelStylesMap.Add(key, hoverStyle);
}
init {
    registerStyles(StyleKey(0, "motorway"), MotorwayColor, 4.0)
    registerStyles(StyleKey(1, "motorway"), MotorwayColor, 6.0)
    registerStyles(StyleKey(2, "motorway"), MotorwayColor, 8.0)
    registerStyles(StyleKey(3, "motorway"), MotorwayColor, 8.0)
    registerLabelStyles(StyleKey(2, "motorway"), 14)
    registerLabelStyles(StyleKey(3, "motorway"), 16)
}

private fun registerStyles(key: StyleKey, color: Color, width: Double) {
    RegularStylesMap[key] = LineStyle.newBuilder().color(color).width(width).build()
    SelectedStylesMap[key] =
        LineStyle.newBuilder().color(SelectionColor).width(width + 2).build()
}

private fun registerLabelStyles(key: StyleKey, fontSize: Long) {
    val regularStyle = TextStyle.newBuilder().fontSize(fontSize).build()
    val selectedStyle = TextStyle.newBuilder().fontSize(fontSize).textColor(
        Color.valueOf(Color.rgb(255, 140, 0))
    ).build()
    RegularLabelStylesMap[key] = regularStyle
    SelectedLabelStylesMap[key] = selectedStyle
}

In this specific case, we define line colors and widths for several combinations of road type and detail level. We increase the line width for styles assigned to features in the selected and on-hover state. To selected features, we assign a different color as well. The styles are stored in three distinct maps that can be accessed when painting.

Implementing the paint logic

Finally, we implement the paint logic itself, by implementing the paintpaintpaint method.

Program: Feature painter painting
void RoadsPainter::paint(const Feature& feature, const FeaturePainterContext& context, FeatureCanvas& canvas) const {
  const std::optional<std::shared_ptr<Geometry>>& geometry = feature.findGeometry();
  if (!geometry) {
    std::cerr << "Geometry not found for the given feature: " << feature.getId() << std::endl;
    return;
  }

  std::string roadType = feature.getValue<std::string>(_roadTypePropertyPath).value_or("unknown");
  size_t detailLevel = context.getDetailLevel();

  std::map<StyleKey, LineStyle> styleMap;
  std::map<StyleKey, TextStyle> labelStyleMap;
  if (context.isFeatureStateEnabled(FeatureState::selected())) {
    styleMap = _selectedStyles;
    labelStyleMap = _selectedLabelStyles;
  } else if (context.isFeatureStateEnabled(FeatureState::hover())) {
    styleMap = _hoverStyles;
    labelStyleMap = _hoverLabelStyles;
  } else {
    styleMap = _regularStyles;
    labelStyleMap = _regularLabelStyles;
  }

  StyleKey key = std::make_pair(detailLevel, roadType);
  auto iterator = styleMap.find(key);
  LineStyle style = iterator != styleMap.end() ? iterator->second : _fallbackStyle;

  canvas.drawGeometry().geometry(*geometry).stroke(style).submit();

  auto labelIterator = labelStyleMap.find(key);
  bool foundLabel = labelIterator != labelStyleMap.end();
  std::optional<std::string> name = feature.getValue<std::string>(_namePropertyPath);
  if (foundLabel && name) {
    TextStyle labelStyle = labelIterator->second;
    canvas.drawLabel().anchor(*geometry).textStyle(labelStyle).text(*name).submit();
  }
}
public void Paint(Feature feature, FeaturePainterContext context, FeatureCanvas canvas)
{
    Geometry geometry = feature.FindGeometry();
    if (geometry == null)
    {
        Console.WriteLine("Feature [" + feature.Id + "] without geometry!");
        return;
    }

    Dictionary<StyleKey, LineStyle> styleMap;
    Dictionary<StyleKey, TextStyle> labelStyleMap;

    if (context.IsFeatureStateEnabled(FeatureState.Selected))
    {
        styleMap = SelectedStylesMap;
        labelStyleMap = SelectedLabelStylesMap;
    }
    else if (context.IsFeatureStateEnabled(FeatureState.Hover))
    {
        styleMap = HoverStylesMap;
        labelStyleMap = HoverLabelStylesMap;
    }
    else
    {
        styleMap = RegularStylesMap;
        labelStyleMap = RegularLabelStylesMap;
    }

    string type = feature.GetValue<string>(_typePropertyPath);
    var key = new StyleKey(context.DetailLevel, type);
    if (!styleMap.TryGetValue(key, out var style))
    {
        style = FallbackStyle;
    }

    canvas.DrawGeometry().Geometry(geometry).Stroke(style).Draped(true).Submit();

    if (labelStyleMap.TryGetValue(key, out var textStyle))
    {
        var name = feature.GetValue<string>(_namePropertyPath);
        if (!string.IsNullOrEmpty(name))
        {
            canvas.DrawLabel().Anchor(geometry).TextStyle(textStyle).Text(name).Submit();
        }
    }
}
override fun paint(feature: Feature, context: FeaturePainterContext, canvas: FeatureCanvas) {
    val geometry = feature.findGeometry()
    if (geometry == null) {
        Log.e("LUCIAD", "Feature [" + feature.id + "] without geometry!")
        return
    }
    val styleMap: Map<StyleKey, LineStyle>
    val labelStyleMap: Map<StyleKey, TextStyle>
    if (context.isFeatureStateEnabled(FeatureState.Selected)) {
        styleMap = SelectedStylesMap
        labelStyleMap = SelectedLabelStylesMap
    } else {
        styleMap = RegularStylesMap
        labelStyleMap = RegularLabelStylesMap
    }
    val type = feature.getValue<String>(_typePropertyPath)
    val key = StyleKey(context.detailLevel, type!!)
    val lineStyle = styleMap[key] ?: FallbackStyle
    canvas.drawGeometry().geometry(geometry).stroke(lineStyle).draped(true).submit()
    val name = feature.getValue<String>(_namePropertyPath)
    name?.let { labelName ->
        labelStyleMap[key]?.let { style ->
            canvas.drawLabel().anchor(geometry).textStyle(style).text(labelName).submit()
        }
    }
}

Before we paint, we verify that the feature has a geometry. If that is the case, we retrieve the value of the type property, using the retrieved data property path. Using that type and the detail level in the FeaturePainterContextFeaturePainterContextFeaturePainterContext, we create a key for our style maps. Based on the state of the feature — whether it is selected, or in the on-hover state — we select which map to retrieve the style from.

Once we have a geometry and a style, we can invoke the drawGeometrydrawGeometrydrawGeometry method on the FeatureCanvasFeatureCanvasFeatureCanvas. We use the resulting object to instruct LuciadCPillar to visualize the current feature with the geometry and style of our choice.

Labels are added by invoking the drawLabeldrawLabeldrawLabel method.

Putting it all together

So far, we have created expressions that we want to tie to specific map scales, and that identify which features we want to visualize at those scales. We also created a painter that visualizes features based on their type, their state, and the map scale. Using all of the above, we now create a FeatureLayerFeatureLayerFeatureLayer to visualize the roads data.

Program: Creating the layer
std::shared_ptr<FeatureLayer> featureLayer = FeatureLayer::newBuilder()
                                                 .model(featureModel)
                                                 .painter(std::make_shared<RoadsPainter>(*roadTypePropertyPath, *namePropertyPath))
                                                 .queryConfiguration(FeatureQueryConfiguration::newBuilder()
                                                                         .addCondition(MapScale::maxZoomedOut(), MapScale::maxZoomedIn(), highPriority)
                                                                         .addCondition(RoadsPainter::HighToMedium, MapScale::maxZoomedIn(), mediumPriority)
                                                                         .addCondition(RoadsPainter::MediumToLow, MapScale::maxZoomedIn(), lowPriority)
                                                                         .loadNothingForNonDefinedScales()
                                                                         .build())
                                                 .build();
IFeaturePainter roadsPainter = new RoadsPainter(typePropertyPath, namePropertyPath);
FeatureLayer featureLayer = FeatureLayer.NewBuilder().Model(featureModel)
    .QueryConfiguration(FeatureQueryConfiguration.NewBuilder()
        .AddCondition(MapScale.MaxZoomedOut, MapScale.MaxZoomedIn, high)
        .AddCondition(1 / RoadsPainter.ScaleHighToMediumDenominator, MapScale.MaxZoomedIn, medium)
        .AddCondition(1 / RoadsPainter.ScaleMediumToLowDenominator, MapScale.MaxZoomedIn, low)
        .Build())
    .Painter(roadsPainter)
    .Build();
val roadsPainter: IFeaturePainter = RoadsPainter(typePropertyPath, namePropertyPath)
return FeatureLayer.newBuilder()
    .model(featureModel)
    .painter(roadsPainter)
    .queryConfiguration(
        FeatureQueryConfiguration.newBuilder()
            .addCondition(MapScale.MaxZoomedOut, MapScale.MaxZoomedIn, high)
            .addCondition(
                MapScale.fromDenominator(RoadsPainter.ScaleHighToMediumDenominator),
                MapScale.MaxZoomedIn,
                medium
            )
            .addCondition(
                MapScale.fromDenominator(RoadsPainter.ScaleMediumToLowDenominator),
                MapScale.MaxZoomedIn,
                low
            )
            .build()
    )
    .build()

We define model query conditions for three different scale ranges:

  • Everything

  • When the map is zoomed in past the first threshold

  • When the map is zoomed in past the second threshold

We progressively load more detailed data as users are zooming in on the map.

We also initialize a roads painter with the type data property path, and pass that painter to the layer builder.

For the full source code, used in functional applications, check out the feature styling samples: