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:
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 FeatureLayer
FeatureLayer
FeatureLayer
builder offers a method addCondition
addCondition
addCondition
.
This method takes a minimum MapScale
MapScale
MapScale
, a maximum map scale, and an Expression
Expression
Expression
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 ExpressionFactory
ExpressionFactory
ExpressionFactory
.
In this case, we want an expression that matches features with specific values for the type
property.
We start by creating equal
equal
equal
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 valueReference
valueReference
valueReference
expression for the property type
, and a literal
literal
literal
expression with the desired value for the type
property.
To combine multiple equal
expressions, we can use an or
or
or
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.
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 DataType
DataType
DataType
, which defines the properties of that feature.
To retrieve
retrieve
retrieve
the value of a property, we need to create a DataPropertyPath
DataPropertyPath
DataPropertyPath
.
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.
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 FeaturePainter
FeaturePainter
FeaturePainter
.
In many cases, implementing a feature painter requires these steps:
-
Defining triggers for a visualization change
-
Creating the necessary styles
-
Implementing the paint logic
Defining triggers for a visualization change
You can handle the first step by implementing the configureMetadata
configureMetadata
configureMetadata
method.
By updating the submitted FeaturePainterMetadata
FeaturePainterMetadata
FeaturePainterMetadata
instance, you can control which changes result in a re-execution of the paint logic.
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:
-
The selected or on-hover state of a feature is updated.
-
The feature itself is updated. The type of the feature changes, for example.
-
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.
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
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 FeaturePainterContext
FeaturePainterContext
FeaturePainterContext
, 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 drawGeometry
drawGeometry
drawGeometry
method on the FeatureCanvas
FeatureCanvas
FeatureCanvas
.
We use the resulting object to instruct LuciadCPillar to visualize the current feature with the geometry and style of our
choice.
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 FeatureLayer
FeatureLayer
FeatureLayer
to visualize the roads data.
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: