Goal

In this tutorial, we show you how you can use an icon style to visualize points.

In the previous tutorials, we already explained that LuciadRIA models provide data and that LuciadRIA layers are responsible for displaying data.

Now it’s time to introduce a new concept: the painter.

Each Feature Layer has a painter that paints all the features that are visible on the map. If we don’t set a painter, the FeatureLayer uses the default painter. In the previous tutorial, the feature layer uses the default painter as well. It paints the city features as blue dots.

You use painters to style features. Styling includes applying color, but you can also change other style attributes, such as line width.

The principle is simple: a painter draws one feature at a time. This means that LuciadRIA calls the same painter once for each feature that’s visible in the map.

Each painter derives from FeaturePainter. Whenever we create a painter, we must also derive it from that class. We must overwrite two methods in particular:

  • paintBody: paints the body of our feature

  • paintLabel: paints a label for our feature

In this tutorial, we show you how to create a painter and assign it to a layer. We focus on the basic concepts. The next tutorial deals with the painting of feature labels.

Starting point

We take the code written in the Load and display vector data in your application tutorial as our starting point, and expand from there.

Step 1 - Create the painter

The key class responsible for styling vector data is the FeaturePainter. To visualize the objects in the model, the layer automatically passes each object individually to its painter by calling the paintBody() function. The painter actually draws the object on the screen, also referred to as the canvas. The GeoCanvas is a geographic feature-rendering canvas extension in the LuciadRIA API. It helps you draw icons, shapes, and text in a geographical context.

In this tutorial:

  • We create a new feature painter objects called CityPainter, which extends the LuciadRIA FeaturePainter.

  • We create a new class that encapsulates all the logic for drawing and styling city features.

  • We overwrite the default implementation of paintBody() function as well.

  • We create the CityPainter.ts file that defines the painter class.

CityPainter.ts file
import {FeaturePainter, PaintState} from "@luciad/ria/view/feature/FeaturePainter.js";
import {GeoCanvas} from "@luciad/ria/view/style/GeoCanvas.js";
import {Feature} from "@luciad/ria/model/feature/Feature.js";
import {Shape} from "@luciad/ria/shape/Shape.js";
import {Layer} from "@luciad/ria/view/Layer.js";
import {Map} from "@luciad/ria/view/Map.js";

export class CityPainter extends FeaturePainter {
  paintBody(geoCanvas: GeoCanvas, feature: Feature, shape: Shape, layer: Layer, map: Map, paintState: PaintState) {
    // Draw and style a feature with
  };
}

Step 2 - Configure the style per object

To implement the paintBody() function, we first of all need style instances, describing the look of an object on the map. For the cities, which are 2D points, we use IconStyle instances, more specifically one IconStyle instance for big cities, and another IconStyle instance for smaller cities. Because PNG icons are available as local resources, we can pass the URL referring to the PNG icons to the url parameter:

import {IconStyle} from "@luciad/ria/view/style/IconStyle.js";
// ...
  export class CityPainter extends FeaturePainter {
    _bigCityStyle: IconStyle = {
      url: "./resources/bigCity.png",
      width: "32px",
      height: "32px"
    };
    _cityStyle: IconStyle = {
      url: "./resources/city.png",
      width: "32px",
      height: "32px"
    };
    // ...
  }

For performance reasons, define the style instances in the constructor of the painter whenever possible. That way, they can be re-used among objects and even between paint cycles.

We must place the bigCity.png and city.png icon files in the dist/resources folder. Our application can access that folder. You can use these images or any other images you like:

city
Figure 1. City Icon
bigCity
Figure 2. Big City Icon.

Secondly, inside the paintBody() function, we assign styles to the domain objects. To do so, the paintBody() function gives access to these arguments:

  • geoCanvas - the GeoCanvas to draw on, supporting icons, 3D icons, shapes and text

  • feature - the rendered Feature

  • shape - the Shape representing our feature

  • layer - the Layerto which the feature belongs

  • map - the Map containing the layer

  • paintState - find out whether a user selected the rendered feature or not, and what the current level-of-detail is — defined by FeaturePainter.getDetailLevelScales()

In short, the geoCanvas argument serves to draw the shape argument on with a certain style, as this code snippet shows:

export class CityPainter extends FeaturePainter {
  _cityStyle: IconStyle = {
    url: "./resources/city.png",
    // ...
  };
  paintBody(geoCanvas: GeoCanvas, feature: Feature, shape: Shape, layer: Layer, map: Map, paintState: PaintState) {
    geoCanvas.drawIcon(shape, this._cityStyle);
  };
}

In this example, we use the GeoCanvas.drawIcon() function because the points of interest are 2D points. The GeoCanvas has dedicated functions for other types of geometries, though, such as 3D points, polyline, and polygons. Have a look at the API reference documentation for more information.

Because you can access the rendered feature with the paintBody() function, the implementation of feature-dependent styling is straightforward. In our example, we use distinct icons depending on the city population property associated with the city feature:

export class CityPainter extends FeaturePainter {
  _bigCityStyle: IconStyle = {
    url: "./resources/bigCity.png",
    width: "32px",
    height: "32px"
  };
  _cityStyle: IconStyle = {
    url: "./resources/city.png",
    width: "32px",
    height: "32px"
  };

  paintBody(geoCanvas: GeoCanvas, feature: Feature, shape: Shape, layer: Layer, map: Map, paintState: PaintState) {
    const style = this.isBigCity(feature) ? this._bigCityStyle : this._cityStyle;
    style.zOrder = feature.properties.TOT_POP;
    geoCanvas.drawIcon(shape, style);
  };

  isBigCity(feature: Feature) {
    return !!feature.properties.TOT_POP && feature.properties.TOT_POP > 1000000;
  }
}

The style applied to each city has a zOrder property. LuciadRIA paints shapes from the lowest Z-order to the highest Z-order, so that shapes with a higher Z-order end up on top of shapes with a lower Z-order. In our case, the application draws bigger cities on top of smaller cities.

Step 3 - Activate the painter on the layer

Because the layer needs a painter to style the model objects, we must set the painter on the layer. To set the painter on the layer, we use the painter option parameter in the layer constructor:

Revised .vectorData.ts that sets a CityPainter instance on the layer.
import {CityPainter} from "./CityPainter.js";
// ...
  const featureLayer = new FeatureLayer(featureModel, {
    label: "US Cities",
    layerType: LayerType.STATIC,
    painter: new CityPainter()
  });

Step 4 - Style selected features

The paintState argument in the paintBody() function describes whether a rendered feature is selected or not. We can use this property to apply a dedicated style to selected features. We use a utility function, addSelection from FeaturePainterUtil, to add automatic selection styling behavior to an existing painter. In our case, we change the style used by CityPainter for a selected feature so that the icon image becomes a little larger.

import {addSelection} from "@luciad/ria/view/feature/FeaturePainterUtil.js";
// ...
const featureLayer = new FeatureLayer(featureModel, {
  label: "US Cities",
  layerType: LayerType.STATIC,
  painter: addSelection(new CityPainter()),
  selectable: true
});

To allow the user to select objects on the map, you need to set the selectable flag on the layer.

This results in:

vector styling
Figure 3. Selected city drawn larger

Final code

The CityPainter.ts file.
import {FeaturePainter, PaintState} from "@luciad/ria/view/feature/FeaturePainter.js";
import {IconStyle} from "@luciad/ria/view/style/IconStyle.js";
import {GeoCanvas} from "@luciad/ria/view/style/GeoCanvas.js";
import {Feature} from "@luciad/ria/model/feature/Feature.js";
import {Shape} from "@luciad/ria/shape/Shape.js";
import {Layer} from "@luciad/ria/view/Layer.js";
import {Map} from "@luciad/ria/view/Map.js";

export class CityPainter extends FeaturePainter {
  _bigCityStyle: IconStyle = {
    url: "./resources/bigCity.png",
    width: "32px",
    height: "32px"
  };
  _cityStyle: IconStyle = {
    url: "./resources/city.png",
    width: "32px",
    height: "32px"
  };

  paintBody(geoCanvas: GeoCanvas, feature: Feature, shape: Shape, layer: Layer, map: Map, paintState: PaintState) {
    const style = this.isBigCity(feature) ? this._bigCityStyle : this._cityStyle;
    style.zOrder = feature.properties.TOT_POP;
    geoCanvas.drawIcon(shape, style);
  };

  isBigCity(feature: Feature) {
    return !!feature.properties.TOT_POP && feature.properties.TOT_POP > 1000000;
  }
}
vectorData.ts code after modification
import {FeatureLayer} from "@luciad/ria/view/feature/FeatureLayer.js";
import {FeatureModel} from "@luciad/ria/model/feature/FeatureModel.js";
import {LayerType} from "@luciad/ria/view/LayerType.js";
import {Map} from "@luciad/ria/view/Map.js";
import {WFSFeatureStore} from "@luciad/ria/model/store/WFSFeatureStore.js";
import {CityPainter} from "./CityPainter.js";
import {addSelection} from "@luciad/ria/view/feature/FeaturePainterUtil.js";

export function createCitiesLayer(map: Map) {
  const url = "https://sampleservices.luciad.com/wfs";
  const featureTypeName = "cities";
  // Create a WFS store
  WFSFeatureStore.createFromURL(url, featureTypeName).then(
      (wfsSore: WFSFeatureStore) => {
        // Create a model based on the created store
        const featureModel = new FeatureModel(wfsSore);
        // Create a feature layer with a feature painter
        const featureLayer = new FeatureLayer(featureModel, {
          label: "US Cities",
          layerType: LayerType.STATIC,
          painter: addSelection(new CityPainter()),
          selectable: true
        });

        // Add a layer to the map
        map.layerTree.addChild(featureLayer);
      });
}