Plugin Examples

If you want to develop and test your custom renderer plugin locally, you should first install the Web App Generator with the option Custom data using custom JavaScript translators to GeoJSON. Then, extend the renderer.plugin.template.js file that becomes available in the generated example application.

Follow the examples below to practise your renderer plugin development skills.

Topology Geometry Plugin Example

This example includes a custom renderer for HERE Map Content (HMC) Topology partitions. The example demonstrates various plugin features by visualizing road segments and nodes.

Topology Geometry Plugin Example
Figure 1. Topology Geometry Plugin Example

Because the raw data from topology partitions contains all the necessary geometry information, the synchronous getGeoJSON function is used. In this case, the plugin only converts the source data from the HERE Map Content Topology Geometry to GeoJSON format.

Example Code

Before pasting the code below into the renderer.plugin.template.js file (that can be found in the project's root directory), open the index.ts file and make sure that the configuration of the DataInspector class is set to the following:

  • The value of the externalRendererPluginName parameter matches your renderer plugin filename without the .js extension. In our case, it is renderer.plugin.template.

  • The HERE Resource Name (HRN) of the catalog that is used for this example is set to hrn:here-cn:data::olp-cn-here:here-map-content-china-2.

  • The layer ID that is used for this example is set to topology-geometry.

    ...
    interactiveDataSource: {
        hrn: "hrn:here-cn:data::olp-cn-here:here-map-content-china-2",
        layer: "topology-geometry",
        levelWithData: 12,
        externalRendererPluginName: "renderer.plugin.template"
    },
    ...
    

renderer.plugin.template.js

/**
 * Renderer plugin to convert JSON data to GeoJSON.
 *
 * Generated GeoJSON must be created according to GeoJSON Specification:
 * https://tools.ietf.org/html/rfc7946
 *
 * This template is an example of plugin to generate the GeoJSON object based on decoded partition
 * content (the same data structure as in Decoded Data panel).
 *
 * In order to use renderer plugin this file must be placed into project root directory and
 * parameter `externalRendererPluginName` must be passed into constructor:
 *
 * ```
 * const geoJsonDataSource = new GeoJsonDataSource({
 *    dataStore: {
 *        hrn: HRN.fromString("Your HRN string"),
 *        getBearerToken: tokenResolver.getBearerToken.bind(tokenResolver),
 *        layer: "Layer name"
 *    },
 *    externalRendererPluginName: "renderer.plugin.template"
 * });
 * ```
 *
 * Renderer plugin must return an object with one of these functions in it:
 *
 * ```
 * getGeoJSON: function (params) { ... }
 * getGeoJSONAsync: async function (params) { ... }
 * ```
 *
 * Both synchronous \`getGeoJSON\` and async \`getGeoJSONAsync\` functions return an object with
 * GeoJSON data.
 *
 * Only one of these functions must be declared, if both are present plugin will not be used.
 */
(() => {
    "use strict";

    /** @type {RendererPlugin} */
    const plugin = {
        /**
         * Generates GeoJSON from RIB topology layer data.
         *
         * @param {object} params.logger Logger to log information to console.
         * @param {object} params.layer All properties as well as dataUrl, name, partitioning and
         * originalConfiguration.
         * @param {object} params.decodeResult Input RIB Navigation Attributes data.
         * @param {object} params.mapUtils Various converters like `mercatorConverter`,
         * `heretileToQuad`, `hdmapCoordinateToLonLat`, etc.
         * @param {object} params.geometryDataProviders Various geometry data providers to map data.
         * @returns {object} An object with generated GeoJSON data.
         */
        getGeoJSON(params) {
            // Destructuring of `params` object.
            const { logger, decodeResult } = params;

            const data = decodeResult.data;

            if (!data.node && !data.segment) {
                throw new Error("Source data does not have node and segment lists. Make sure you are passing correct data to the renderer.");
            }

            var result = {
                "type": "FeatureCollection",
                "features": []
            };

            // Process segments in the tile if any exist.
            result.features = result.features.concat(processSegments(data.segment));
            logger.info("Segments successfully processed: " + data.segment.length);

            // Process nodes in the tile.  They will be rendered on top of the segments since they were added to the
            // FeatureCollection afterwards.
            result.features = result.features.concat(processNodes(data.node));
            logger.info("Nodes successfully processed: " + data.node.length);

            // return object with GeoJSON
            return {
                contentType: "application/vnd.geo+json; charset=utf-8",
                body: result
            };
        }
    };

    return plugin;

    function processNodes(nodes) {
        var list = [];

        for (var i = 0; i < nodes.length; i++) {
            list.push(createNodeFeature(nodes[i], i));
        }

        return list;
    }

    function createNodeFeature(node, index) {
        return {
            type: "Feature",
            properties: {
                protobufRef: `node[${index}]`,
                tooltip: `<b>Node Identifier:</b><br>${node.identifier}<br><br><b>Geometry:</b><br>Latitude: ${node.geometry.latitude}<br>Longitude: ${node.geometry.longitude}`,
                style: { color: "#FAD87A" }
            },
            geometry: {
                type: "Polygon",
                coordinates: convertCoordinateToShape(node.geometry.longitude, node.geometry.latitude)
            }
        };
    }

    function convertCoordinateToShape(lon, lat) {
        var coordinateList = [];
        var distance = 0.000025;

        // Creates rectangle from point
        coordinateList.push([lon - distance, lat + distance]);  // Upper-left corner.
        coordinateList.push([lon + distance, lat + distance]);  // Upper-right corner.
        coordinateList.push([lon + distance, lat - distance]);  // Lower-right corner.
        coordinateList.push([lon - distance, lat - distance]);  // Lower-left corner.
        coordinateList.push([lon - distance, lat + distance]);  // End ring with upper-left corner.

        return [coordinateList];
    }

    function processSegments(segments) {
        var list = [];

        for (var i = 0; i < segments.length; i++) {
            list.push(processLineStringArray(segments[i], i));
        }

        return list;
    }

    function processLineStringArray(segment, index) {
        var feature = {
            type: "Feature",
            properties: {
                protobufRef: `segment[${index}]`,
                identifier: segment.identifier,
                length: segment.length,
                style: {
                    color: "#1EE0FF"
                },
                width: 1
            },
            geometry: {
                type: "LineString",
                coordinates: []
            }
        };

        for (var i = 0; i < segment.geometry.point.length; i++) {
            var point = segment.geometry.point[i];
            feature.geometry.coordinates.push([point.longitude, point.latitude]);
        }

        return feature;
    }
})();

Speed Limit Attribute Plugin Example

This renderer plugin generates GeoJSON output with speed limit lines. Speed limits information comes from the HERE Map Content Navigation Attributes layer, and road geometries are retrieved from the HERE Map Content Topology Geometry layer.

Speed Limit Attribute Plugin Example
Figure 2. Speed Limit Attribute Plugin Example

This is a more complex example compared to the previous synchronous one. Since Navigation Attributes data lacks road geometry information, it has to be referenced to road segments using SegmentAnchors instead.

That is why the async getGeoJSONAsync function is implemented to retrieve road geometries from SegmentAnchorGeometryDataProvider. Road geometries are retrieved by calling the getSegmentAnchorCoordinates method of the data provider. The data provider returns geometry information in a format that is ready to be used as an array of GeoJSON coordinates. This reduces the effort required to generate GeoJSON Feature objects. For more information, see the 'SegmentAnchor' and SegmentAnchorGeometryDataProvider API reference documentation.

Note

Calls to the geometry data provider's getSegmentAnchorCoordinates function are async, as well as getting a data provider instance.

Example Code

Before pasting the code below into the renderer.plugin.template.js file (that can be found in the project's root directory), open the index.ts file and make sure that the configuration of the DataInspector class is set to the following:

  • The value of the externalRendererPluginName parameter matches your renderer plugin filename without the .js extension. In our case, it is renderer.plugin.template.

  • The HERE Resource Name (HRN) of the catalog that is used for this example is set to hrn:here-cn:data::olp-cn-here:here-map-content-china-2.

  • The layer ID that is used for this example is set to navigation-attributes.

    ...
    interactiveDataSource: {
        hrn: "hrn:here-cn:data::olp-cn-here:here-map-content-china-2",
        layer: "navigation-attributes",
        levelWithData: 12,
        externalRendererPluginName: "renderer.plugin.template"
    },
    ...
    

renderer.plugin.template.js

/**
 * Renderer plugin to convert JSON data to GeoJSON.
 *
 * Generated GeoJSON must be created according to GeoJSON Specification:
 * https://tools.ietf.org/html/rfc7946
 *
 * This template is an example of plugin to generate the GeoJSON object based on decoded partition
 * content (the same data structure as in Decoded Data panel).
 *
 * In order to use renderer plugin this file must be placed into project root directory and
 * parameter `externalRendererPluginName` must be passed into constructor:
 *
 * ```
 * const geoJsonDataSource = new GeoJsonDataSource({
 *    dataStore: {
 *        hrn: HRN.fromString("Your HRN string"),
 *        getBearerToken: tokenResolver.getBearerToken.bind(tokenResolver),
 *        layer: "Layer name"
 *    },
 *    externalRendererPluginName: "renderer.plugin.template"
 * });
 * ```
 *
 * Renderer plugin must return an object with one of these functions in it:
 *
 * ```
 * getGeoJSON: function (params) { ... }
 * getGeoJSONAsync: async function (params) { ... }
 * ```
 *
 * Both synchronous \`getGeoJSON\` and async \`getGeoJSONAsync\` functions return an object with
 * GeoJSON data.
 *
 * Only one of these functions must be declared, if both are present plugin will not be used.
 */
(() => {
    "use strict";

    let geometryDataProvider; // An instance of geometry data provider.
    let loggerInstance; // An instance of logger.

    // Thresholds and colors for different speed limit values.
    const MEDIUM_LIMIT_LOW_VALUE = 50;
    const MEDIUM_LIMIT_HIGH_VALUE = 80;
    const COLOR_SPEEDLIMIT_LOW = "#bf212f";
    const COLOR_SPEEDLIMIT_MEDIUM = "#e18c1e";
    const COLOR_SPEEDLIMIT_HIGH = "#27b376";
    const HOVER_SPEEDLIMIT_LOW = "#e52838";
    const HOVER_SPEEDLIMIT_MEDIUM = "#ffa423";
    const HOVER_SPEEDLIMIT_HIGH = "#2fd98f";

    /** @type {RendererPlugin} */
    const plugin = {
        /**
         * Asynchronously generates GeoJSON from RIB Navigation Attributes data.
         *
         * @param {object} params.logger Logger to log information to console.
         * @param {object} params.layer All properties as well as dataUrl, name, partitioning and
         * originalConfiguration.
         * @param {object} params.decodeResult Input RIB Navigation Attributes data.
         * @param {object} params.mapUtils Various converters like `mercatorConverter`,
         * `heretileToQuad`, `hdmapCoordinateToLonLat`, etc.
         * @param {object} params.geometryDataProviders Various geometry data providers to map data.
         * @returns {Promise} A promise which resolves with generated GeoJSON data.
         */
        async getGeoJSONAsync(params) {
            // Destructuring of `params` object.
            const { logger, decodeResult, geometryDataProviders } = params;

            // This is an async function so it must return a Promise.
            // Save an instance of logger so it can be used by other functions within the scope of
            // this plugin.
            loggerInstance = logger;

            // Get an instance of geometry data provider from `geometryDataProviders`.
            // Please note that this is an asynchronous call.
            geometryDataProvider = await geometryDataProviders.getSegmentAnchorDataProvider();

            // Validate input data.
            if (!decodeResult.data.speed_limit && !decodeResult.data.segment_anchor) {
                loggerInstance.error("Source data does not have speed limits data. Make sure you are passing correct data to the renderer.");
                throw new Error();
            }

            // Create result object.
            const result = {
                "type": "FeatureCollection",
                "features": await processSpeedLimits(decodeResult.data)
            };

            // Resolve promise with generated GeoJSON.
            return {
                contentType: "application/vnd.geo+json; charset=utf-8",
                body: result
            };
        }
    };

    return plugin;

    /**
     * Creates speed limits geometry.
     * Please note that this function is async.
     */
    async function processSpeedLimits(data) {
        const result = [];

        // Preliminary sort source data by speed limit value.
        data.speed_limit.sort((item1, item2) => item1.value < item2.value);

        // Iterate over speed limit items.
        let speedLimitItemIndex = 0;
        for (const speedLimitItem of data.speed_limit) {
            const style = getLineStyle(speedLimitItem);

            // Iterate over `SegmentAnchor`s of speed limit item.
            let segmentAnchorIndex = 0;
            for (const anchorIndex of speedLimitItem.segment_anchor_index) {
                // First, get actual `SegmentAnchor` from pool.
                const anchor = data.segment_anchor[anchorIndex];
                // Then, retrieve line coordinates from geometry data provider.
                const coordinates = await geometryDataProvider.getSegmentAnchorCoordinates(anchor);

                if (coordinates.length === 0) {
                    loggerInstance.warn(`Can't retrieve geometry for SegmentAnchor #${anchorIndex}`);
                }

                // Create a `LineString` feature for speed limit segment
                result.push({
                    type: "Feature",
                    properties: {
                        // `protobufRef` is a path pointing to original data property
                        protobufRef: `speed_limit[${speedLimitItemIndex}].segment_anchor_index[${segmentAnchorIndex}]`,
                        speedLimit: speedLimitItem.value,
                        style, // apply visual styles to feature
                        width: 4,
                        // `title` will be shown in tooltip when mouse is moved over the feature
                        title: speedLimitItem.value + " km/h"
                    },
                    geometry: {
                        type: "LineString",
                        coordinates
                    }
                });

                segmentAnchorIndex++;
            }
            speedLimitItemIndex++;
        };

        return result;
    }

    /**
     * Returns line style based on speed limit value.
     *
     * @param {object} speedLimitItem Speed limit item.
     * @returns {string} String with line color.
     */
    function getLineStyle(speedLimitItem) {
        if (speedLimitItem.value < MEDIUM_LIMIT_LOW_VALUE) {
            return { color: COLOR_SPEEDLIMIT_LOW, hoverColor: HOVER_SPEEDLIMIT_LOW };
        } else if (speedLimitItem.value < MEDIUM_LIMIT_HIGH_VALUE) {
            return { color: COLOR_SPEEDLIMIT_MEDIUM, hoverColor: HOVER_SPEEDLIMIT_MEDIUM };
        } else {
            return { color: COLOR_SPEEDLIMIT_HIGH, hoverColor: HOVER_SPEEDLIMIT_HIGH };
        }
    }
})();

If your renderer plugin needs all the road segments for a certain road topology partition, you can obtain the segment data from an instance of the road segment data provider by implementing the following:

getGeoJSONAsync: function (params) {
    ...
    // Get an instance of geometry data provider.
    const roadSegmentsProvider = await params.geometryDataProviders.getRoadSegmentsDataProvider();
    // Get an array of all road segments from data provider for current partition.
    const roadSegments = await roadSegmentsProvider.getRoadSegmentsForTile(params.decodeResult.partitionId);
    ...
}

Note

This example plugin uses params.decodeResult.partitionId to refer to the same partition as the HERE Map Content partition with the source data.

For more information on the road segment data format, see BaseRoadSegmentsDataProvider reference documentation.

results matching ""

    No results matching ""