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
cd MapStoreExtension

switch the MapStore2 submodule to the latest stable branch

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

git checkout -b training-extension

install all the npm dependencies with

npm install

The first things we have to change are the name of the npm package and the name of the extension.

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.

npm run be:start
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.

// 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 (
      <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 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

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

Replace the content of the assets/translations/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/Module.jsx file as follow

// 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 (
      <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

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

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

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

// 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 (
      <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