Extensions ********** From 2021.01.00 MapStore provides the possibility to develop your own plugin in a separated repository and install them in an existing instance of MapStore ( or of a MapStore project) . These are called “Extensions”. Plugins installed can be used in MapStore contexts or in the main pages, editing ``localConfig.json``. To develop an extension, you can start for a sample project: https://github.com/geosolutions-it/MapStoreExtension This is a standard MapStore project and follows almost the same guidelines, with additional scripts for build the extension ZIP file to install. Follow the readme of the project to learn how to develop the extension *Example* This example shows how to transform a simple MapStore plugin to an extension to make it loaded a runtime. This example does not include the setup of a development environments so please follow the MapStoreExtension readme for more information https://github.com/geosolutions-it/MapStoreExtension/blob/master/README.md. These are the step to follow to create this extension: Setup ----- clone the extension template repository locally .. code-block:: bash git clone --recursive https://github.com/geosolutions-it/MapStoreExtension .. code-block:: bash cd MapStoreExtension switch the MapStore2 submodule to the latest stable branch .. code-block:: bash git checkout 2023.01.xx # or latest branch git submodule update Prepare branch and name the extension ------------------------------------- Now that we are aligned to the latest stable branch we can create a new branch where we can start to modify the template .. code-block:: bash git checkout -b training-extension install all the npm dependencies with .. code-block:: bash npm install The first things we have to change are the name of the npm package and the name of the extension. - Rename the ``name`` of the ``package.json`` to ensure a new dev environment e.g.: from ``MapStoreExtension`` to ``training-extension`` - Choose a name for this extension and change the file as described here https://github.com/geosolutions-it/MapStoreExtension#naming-the-plugin. We can use the name ``MapViewExtension`` for this example Development Environment ----------------------- We could start to develop the extension by running two commands in two separated terminals. The first command will start the backend while the second command will start the frontend part, that is the real extension development environment. .. code-block:: bash npm run be:start .. code-block:: bash npm run fe:start The default extension plugin included in the template repository is providing all the available functionality such as actions, reducers and epics setups. We are going to create a different plugin for this example and it will be a floating box that displays information about the current map view center and it can be extended to show other properties of the map state object. This plugin described in the snippet below should replace the content of the ``js/extension/plugins/Module.jsx`` component inside the MapStoreExtension repository. .. code-block:: javascript // js/extension/plugins/Module.jsx import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import {createPlugin} from "@mapstore/utils/PluginsUtils"; import { mapSelector } from '@mapstore/selectors/map'; import Message from '@mapstore/components/I18N/Message'; import { name } from '../../../config'; function MapViewInfo({ center }) { return (
({center.crs})
x: {center?.x?.toFixed(6)} - y: {center?.y?.toFixed(6)}
); } MapViewInfo.propTypes = { center: PropTypes.object }; MapViewInfo.defaultProps = { center: {} }; const MapViewInfoPlugin = connect( createSelector([ mapSelector ], (map) => ({ center: map?.center })), {} )(MapViewInfo); export default createPlugin(name, { name, component: MapViewInfoPlugin, containers: {}, epics: {}, reducers: {} }); If we open a map viewer page we should see the extension rendered in the page Translations ------------------- We can also apply custom translations by including new message ids inside the json files of the ``assets/translations`` folder Replace the content of the ``assets/translations/data.en-US.json`` file this content .. code-block:: javascript { "locale": "en-US", "messages": { "extension": { "mapCenter": "Map Center" } } } Replace the content of the ``assets/translations/data.it-IT.json`` file this content .. code-block:: javascript { "locale": "it-IT", "messages": { "extension": { "mapCenter": "Centro della Mappa" } } } Style and assets ---------------- The style for the component could be moved inside a separated css file and applied as classes. The repository contains already a css file that can be used for the extension Replace the content of the ``js/extension/assets/style.css`` with .. code-block:: css .map-view-info { position: absolute; z-index: 100; bottom: 35px; margin: 8px; left: 50%; background-color: #ffffff; padding: 8px; text-align: center; transform: translateX(-50%); } Then change the ``js/extension/plugins/Module.jsx`` file as follow .. code-block:: diff // js/extension/plugins/Module.jsx import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { mapSelector } from '@mapstore/selectors/map'; import Message from '@mapstore/components/I18N/Message'; import { name } from '../../../config'; + import '../assets/style.css'; function MapViewInfo({ center }) { return (
({center.crs})
x: {center?.x?.toFixed(6)} - y: {center?.y?.toFixed(6)}
); } MapViewInfo.propTypes = { center: PropTypes.object }; MapViewInfo.defaultProps = { center: {} }; const MapViewInfoPlugin = connect( createSelector([ mapSelector ], (map) => ({ center: map?.center })), {} )(MapViewInfo); export default { name, component: MapViewInfoPlugin, containers: {}, epics: {}, reducers: {} }; MapStore app expose also css variable to it is possible to use those variables inside the style. This change allows to inherits custom style variable in the extension .. code-block:: css .map-view-info { position: absolute; z-index: 100; bottom: 35px; margin: 8px; left: 50%; background-color: #ffffff; + background-color: var(--ms-main-bg, #ffffff); padding: 8px; text-align: center; transform: translateX(-50%); } Also other assets such images can be included inside the ``js/extension/assets/`` folder and imported in the components Interactions with the State: actions, reducers and epics --------------------------------------------------------- We could enhance the plugin by requesting additional information using external data source. For the scope of this exercise we could simulate a service that provide information of the time zone given a point coordinates. We will add the information about the time zone within the current map info view panel based on the current center. First step is to create a new folders where to store our actions, reducers and epics in the ``js/extension/`` directory. Create a new ``js/extension/actions`` folder with inside a ``mapinfoview.js`` file where to add our actions. The action will allow us to store a feature in the state .. code-block:: javascript // js/extension/actions/mapinfoview.js export const SET_CURRENT_TIME_ZONE_FEATURE = 'MAP_INFO_VIEW:SET_CURRENT_TIME_ZONE_FEATURE'; export const setCurrentTimeZoneFeature = (feature) => ({ type: SET_CURRENT_TIME_ZONE_FEATURE, feature }); Create a new ``js/extension/reducers`` folder with inside a ``mapinfoview.js`` file where to add our reducer. This reducer will manage the action received by the mapinfoview actions .. code-block:: javascript // js/extension/reducers/mapinfoview.js import { SET_CURRENT_TIME_ZONE_FEATURE } from '../actions/mapinfoview'; function mapinfoview(state = [], action) { switch (action.type) { case SET_CURRENT_TIME_ZONE_FEATURE: { return { ...state, timeZoneFeature: action.feature }; } default: return state; } } export default mapinfoview; Create a new ``js/extension/epics`` folder with inside a ``mapinfoview.js`` file where to add our epic. This epic will simulate a request to a mock service that provide a feature with time zone information .. code-block:: javascript // js/extension/epics/mapinfoview.js import { Observable } from 'rxjs'; import { CHANGE_MAP_VIEW } from '@mapstore/actions/map'; import { setCurrentTimeZoneFeature } from '../actions/mapinfoview'; import turfInside from '@turf/inside'; // this is a mock function to simulate an external service that takes // a position and return a time zone feature // this is only used to explain how an epic could interact with external service // this could alternatively obtained with a Get Features Info request to a layer uploaded to const mockServiceGetTimezonesGeoJSON = (position) => { return import(/* webpackChunkName: 'json/ne_10m_time_zones_simplified_0.1degrees' */ '../assets/ne_10m_time_zones_simplified_0.1degrees.json') .then((mod) => mod.default) .then((featuresCollection) => { const centerFeature = { type: 'Feature', properties: {}, geometry: { coordinates: [position.x, position.y], type: 'Point' } }; const currentTimeZoneFeature = featuresCollection.features.find(feature => turfInside(centerFeature, feature)); return currentTimeZoneFeature; }); }; export const extMapViewInfoGetTimezoneGivenCenter = (action$) => action$.ofType(CHANGE_MAP_VIEW) .switchMap((action) => { return Observable.defer(() => mockServiceGetTimezonesGeoJSON(action.center)) .switchMap((currentTimeZoneFeature) => { return Observable.of(setCurrentTimeZoneFeature(currentTimeZoneFeature)); }); }); export default { extMapViewInfoGetTimezoneGivenCenter }; To make this epic work we need to add to the ``js/extension/assets/`` folder the following data file `ne_10m_time_zones_simplified_0.1degrees.json <../../_static/ne_10m_time_zones_simplified_0.1degrees.json>`_. This file is a feature collection of time zone geometries downloaded from the `Natural Earth `__ page. The data has been simplified to speed up the import for the scope of this exercise. Finally include reducers and epic inside the Extension passing to the component the updated time zone feature that include information about the center time zone .. code-block:: diff // js/extension/plugins/Module.jsx import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { mapSelector } from '@mapstore/selectors/map'; import Message from '@mapstore/components/I18N/Message'; import { name } from '../../../config'; import '../assets/style.css'; + import epics from '../epics/mapinfoview'; + import mapinfoview from '../reducers/mapinfoview'; function MapViewInfo({ center, + timeZoneFeature }) { + const timeZone = timeZoneFeature?.properties?.time_zone; return (
({center.crs})
x: {center?.x?.toFixed(6)} - y: {center?.y?.toFixed(6)}
+ {timeZone &&
Time zone: {timeZone}
}
); } MapViewInfo.propTypes = { center: PropTypes.object }; MapViewInfo.defaultProps = { center: {} }; const MapViewInfoPlugin = connect( createSelector([ - mapSelector, + mapSelector, + state => state?.mapinfoview?.timeZoneFeature - ], (map) => ({ + ], (map, timeZoneFeature) => ({ - center: map?.center + center: map?.center, + timeZoneFeature })), {} )(MapViewInfo); export default { name, component: MapViewInfoPlugin, containers: {}, - epics: {}, + epics, - reducers: {} + reducers: { + mapinfoview + } }; Build the extension --------------------- Now that everything is in place it is possible to run the build script to get the extension zip file .. code-block:: bash npm run ext:build The MapViewInfo extension zip file is generated in the dist/ folder after the build script is completed. Now the extension can be added via UI in the dedicated context step .. toctree:: :maxdepth: 1 :caption: Contents: