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.
Rename the
name
of thepackage.json
to ensure a new dev environment e.g.: fromMapStoreExtension
totraining-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.
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