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

git clone --recursive https://github.com/geosolutions-it/MapStoreExtension

create a new branch where we can start to modify the template

cd MapStoreExtension
git checkout -b training-extension

switch the MapStore2 submodule to the latest stable release

cd MapStore2
fetch origin
git checkout <stable-branch>
cd ..

install all the npm dependencies with

npm install

rename the name of the package.json to ensure a new dev environment eg: from MapStoreExtension to training-extension

select 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

Start development

we could start to develop the extension by running two commands, the first one initialize an instance of MapStore that will be used to host the Extension and the second command will run the extension in developer mode

npm run ext:startapp
npm run ext: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/Extension.jsx component inside the MapStoreExtension repository. The main difference between a plugin and an extension file is the export structure where usually the plugin uses the createPlugin function helper. The extension file cannot use the createPlugin function helper and must be exported as an object.

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';

function MapViewInfo({
   center
}) {
   return (
      <div
            style={{
               position: 'absolute',
               zIndex: 100,
               bottom: 35,
               margin: 8,
               left: '50%',
               backgroundColor: '#ffffff',
               padding: 8,
               textAlign: 'center',
               transform: 'translateX(-50%)'
            }}
      >
            <div><small><Message msgId="extension.mapCenter"/> ({center.crs})</small></div>
            <div>x: <strong>{center?.x?.toFixed(6)}</strong> - y: <strong>{center?.y?.toFixed(6)}</strong></div>
      </div>
   );
}

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: {}
};

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 ssets/translation/data.en-US.json file this content

{
   "locale": "en-US",
   "messages": {
      "extension": {
         "mapCenter": "Map Center"
      }
   }
}

Replace the content of the assets/translation/data.it-IT.json file this content

{
   "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

.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/Extension.jsx file as follow

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 (
      <div
-            style={{
-               position: 'absolute',
-               zIndex: 100,
-               bottom: 35,
-               margin: 8,
-               left: '50%',
-               backgroundColor: '#ffffff',
-               padding: 8,
-               textAlign: 'center',
-               transform: 'translateX(-50%)'
-            }}
+            className="map-view-info"
      >
            <div><small><Message msgId="extension.mapCenter"/> ({center.crs})</small></div>
            <div>x: <strong>{center?.x?.toFixed(6)}</strong> - y: <strong>{center?.y?.toFixed(6)}</strong></div>
      </div>
   );
}

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

.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

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

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

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. 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

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/mapviewinfo';
+ import mapinfoview from '../reducers/mapinfoview';

function MapViewInfo({
   center,
+   timeZoneFeature
}) {
+   const timeZone = timeZoneFeature?.properties?.time_zone;
   return (
      <div
            className="map-view-info"
      >
            <div><small><Message msgId="extension.mapCenter" /> ({center.crs})</small></div>
            <div>x: <strong>{center?.x?.toFixed(6)}</strong> - y: <strong>{center?.y?.toFixed(6)}</strong></div>
+            {timeZone && <div>Time zone: {timeZone}</div>}
      </div>
   );
}

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

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