Map Support Plugin ******************** This section shows how to implement a plugin able to access the native api of the mapping library to create custom supports. MapStore provides the most common map events and supports out of the box but in some project there could be the need to add more functionalities to the map. Below an example on how to create a simple animated marker using the plugin system Setup a plugin ----------------- Create a new plugin that will allows us to select two different map type by selecting the value from a dropdown, the options will be: OpenLayers and Cesium The plugin will be included in the ``js/plugins/`` directory with the name ``AnimatedMarker.jsx`` .. code-block:: javascript // js/plugins/AnimatedMarker.jsx import React from 'react'; import { createPlugin } from '@mapstore/utils/PluginsUtils'; import Select from 'react-select'; import { changeMapType } from '@mapstore/actions/maptype'; import { mapTypeSelector } from '@mapstore/selectors/maptype'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; function AnimatedMarkerPanel({ mapType, onChangeMapType }) { return (
onChangeMapType(value)} options={[ { value: 'openlayers', label: 'Openlayers' }, { value: 'cesium', label: 'Cesium' } ]} />
); } const ConnectedAnimatedMarkerPanel = connect( createSelector( [ mapTypeSelector ], (mapType) => ({ mapType })), { onChangeMapType: changeMapType } )(AnimatedMarkerPanel); export default createPlugin('AnimatedMarker', { component: ConnectedAnimatedMarkerPanel, containers: { Map: { Tool: AnimatedMarkerSupport, name: 'AnimatedMarker' } } }); The Map container configuration accepts two keys: - name, the name of the support - Tool, a react component used for the support implementation. This component will get two special props: - mapType, the current map type in use - map, the native library instance With this configuration we are now able to implement the support we need Implement a support in react ----------------------------- This example will show an animated marker similar to the one in the OpenLayers examples, `Marker Animation `__ . Create a component at ``js/components/map/openlayers/AnimatedMarkerSupport.jsx`` with the following content for the OpenLayers library: .. code-block:: javascript // js/components/map/openlayers/AnimatedMarkerSupport.jsx import { useEffect } from 'react'; import GeoJSON from 'ol/format/GeoJSON'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import Feature from 'ol/Feature'; import Point from 'ol/geom/Point'; import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'; function AnimatedMarkerSupport({ map, speed = 60, feature }) { useEffect(() => { let coordinates; let now = new Date().getTime(); let vectorLayer; let positionStyle; function movePoint(event) { const vectorContext = event.vectorContext; const frameState = event.frameState; const elapsedTime = frameState.time - now; const index = Math.round(speed * elapsedTime / 1000); if (index >= coordinates.length) { now = new Date().getTime(); } const currentPoint = new Point(coordinates[index % coordinates.length]); const currentFeature = new Feature(currentPoint); vectorContext.drawFeature(currentFeature, positionStyle); map.render(); } if (feature) { const route = new GeoJSON().readFeature(feature, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }); const position = new Feature({ type: 'marker', geometry: new Point(route.getGeometry().getFirstCoordinate()) }); positionStyle = new Style({ image: new CircleStyle({ radius: 7, fill: new Fill({ color: 'black' }), stroke: new Stroke({ color: 'white', width: 2 }) }) }); vectorLayer = new VectorLayer({ source: new VectorSource({ features: [route, position] }), style: (olFeature) => { if (olFeature.get('type') === 'marker') { return positionStyle; } return new Style({ stroke: new Stroke({ width: 6, color: 'red' }) }); } }); map.addLayer(vectorLayer); coordinates = route.getGeometry().getCoordinates(); position.setGeometry(null); vectorLayer.on('postcompose', movePoint); } return () => { if (vectorLayer) { vectorLayer.un('postcompose', movePoint); map.removeLayer(vectorLayer); } }; }, [feature, speed]); return null; } export default AnimatedMarkerSupport; In the snippet above we are initialize the map events on every feature or speed change Create another component at ``js/components/map/cesium/AnimatedMarkerSupport.jsx`` with the following content for the Cesium library: .. code-block:: javascript // js/components/map/cesium/AnimatedMarkerSupport.jsx import { useEffect } from 'react'; import * as Cesium from 'cesium'; function AnimatedMarkerSupport({ map, speed = 60, feature }) { useEffect(() => { let dataSource; let entities; let startTime = new Date().getTime(); let lastTime = startTime; let removing = false; const clock = map.clock; function movePoint() { const elapsedTime = lastTime - startTime; const index = Math.round(speed * elapsedTime / 1000); const coordinates = entities[0].polyline.positions.getValue(); if (index >= coordinates.length) { startTime = new Date().getTime(); } const currentPosition = coordinates[index % coordinates.length]; entities[1].position = currentPosition; lastTime = new Date().getTime(); map.scene.requestRender(); } if (feature) { Cesium.GeoJsonDataSource.load({ type: 'FeatureCollection', features: [feature, { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: feature?.geometry?.coordinates[0] } }] }, { stroke: Cesium.Color.RED, strokeWidth: 6 }).then((newDataSource) => { if (!removing) { dataSource = newDataSource; entities = dataSource?.entities?.values; entities[1].billboard = new Cesium.BillboardGraphics({ image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAJlJREFUOE+tk7sNAjEQRN+0QiNcBlUQIEFRIAioAjIohGtl0J7OyDgB22y2Xs3zfkVhtpfABhiAxRwegTtwkfTIJcod2wdgV0IL/yhpn97eANtXYPVFnMI3SetwJsCPP5fsKRPNNUd9LTYE4ARsW9TAOQDPrNu1nDEArlV9jPEfgO4SupsYq9s+xu5FSh3tWuUM0n5MGaTqnF8jHk9pS1JsJgAAAABJRU5ErkJggg==', color: Cesium.Color.BLACK, disableDepthTestDistance: Number.POSITIVE_INFINITY }); map.dataSources.add(dataSource); clock.onTick.addEventListener(movePoint); } }); } return () => { removing = true; if (dataSource && map?.dataSources) { map.dataSources.remove(dataSource); clock.onTick.removeEventListener(movePoint); } }; }, [feature, speed]); return null; } export default AnimatedMarkerSupport; Now we have similar implementation that show a point moving on a line string feature Once we created this two component we could include them in the plugin support using the ``lazy`` functionality from react. This allow to reduce the main bundle size adding the mapping libraries dependencies only when requested .. code-block:: javascript // js/plugins/AnimatedMarker.jsx import React, { lazy, Suspense } from 'react'; import { createPlugin } from '@mapstore/utils/PluginsUtils'; import Select from 'react-select'; import { changeMapType } from '@mapstore/actions/maptype'; import { mapTypeSelector } from '@mapstore/selectors/maptype'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; const animatedMarkerSupports = { openlayers: lazy(() => import('@js/components/map/openlayers/AnimatedMarkerSupport.jsx')), cesium: lazy(() => import('@js/components/map/cesium/AnimatedMarkerSupport.jsx')) }; function AnimatedMarkerSupport({ mapType, ...props }) { const Support = animatedMarkerSupports[mapType]; return Support ? }> : null; } function AnimatedMarkerPanel({ mapType, onChangeMapType }) { return (
onChangeMapType(value)} options={[ { value: 'openlayers', label: 'Openlayers' }, { value: 'cesium', label: 'Cesium' } ]} />
); } const ConnectedAnimatedMarkerPanel = connect( createSelector( [ mapTypeSelector ], (mapType) => ({ mapType })), { onChangeMapType: changeMapType } )(AnimatedMarkerPanel); export default createPlugin('AnimatedMarker', { component: ConnectedAnimatedMarkerPanel, containers: { Map: { Tool: AnimatedMarkerSupport, name: 'AnimatedMarker' } } }); The final plugin will show a read line with a black point moving over it for the OpenLayers and Cesium map types.