MapStore Framework - Frontend Only Project (Experimental)¶
This session focus on the use of MapStore as a frontend framework where the GeoStore backend is excluded. This approach could be useful when we need to connect MapStore to a different backend or when we want create a viewer/dashboard that extract data from static configuration files.
The exercise in this session will allow to create a map viewer using an experimental project creation process, the result can be viewed here. Some useful links:
@mapstore/project an experimental library to create mapstore project
mapstore static examples a mapstore viewer that uses static files created with the @mapstore/project scripts
Install a MapStore project frontend only¶
navigate in your workspace folder and run the creation script using npx
npx @mapstore/project create
the script will prompt some options before to create and these are the value selected for this custom project
- Name of project (default mapstore-project): static-map
- Include backend (yes/no default yes): no
- Optional features (printing, ldap):
- Run npm install after creation setup (yes/no default yes): no
navigate inside the static-map
folder
cd static-map/
change the mapstore dependencies inside the package.json
to point to the latest stable branch instead of master
touch package.json
replace the mapstore dependencies with the needed branch version
"dependencies": {
"mapstore": "git+https://github.com/geosolutions-it/MapStore2.git#<branch|tag>"
}
add configuration to package.json to target specific folder of this project instead of the default one
"mapstore": {
"apps": [
"js/apps"
],
"html": [
""
],
"themes": [
"themes/"
]
}
remove all the default apps inside the static-map/js/apps
directory
create a new entry called static-map/js/apps/static-map.js
with the following content
/*
* Copyright 2023, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
// add this binding to ensure all the streams inside components are working
import '@mapstore/framework/libs/bindings/rxjsRecompose';
import url from 'url';
import main from '@mapstore/framework/product/main';
import pluginsDef from '@js/plugins/def';
import {
setLocalConfigurationFile,
setConfigProp
} from '@mapstore/framework/utils/ConfigUtils';
import axios from '@mapstore/framework/libs/ajax';
import MapViewer from '@mapstore/framework/containers/MapViewer';
import { configureMap } from '@mapstore/framework/actions/config';
import { setControlProperty } from '@mapstore/framework/actions/controls';
import security from '@mapstore/framework/reducers/security';
import omit from 'lodash/omit';
setLocalConfigurationFile('configs/localConfig.json');
setConfigProp('translationsPath', ['translations', 'ms-translations']);
setConfigProp('extensionsRegistry', 'configs/extensions.json');
// list of path that need version parameter
const pathsNeedVersion = [
'configs/',
'assets/',
'translations/',
'ms-translations/',
'print.json'
];
const version = __MAPSTORE_PROJECT_CONFIG__.version || 'dev';
axios.interceptors.request.use(
config => {
if (config.url && version && pathsNeedVersion.filter(urlEntry => config.url.match(urlEntry))[0]) {
return {
...config,
params: {
...config.params,
v: version
}
};
}
return config;
}
);
const pages = [{
name: 'home',
path: '/',
component: MapViewer
}];
const MAP_TYPE = 'openlayers';
document.addEventListener('DOMContentLoaded', function() {
// example of initial security state
// with null this state is not initialized
const user = null;
const securityState = user && {
security: {
user: user,
token: '' // this token is applied to the request defined in the localConfig authenticationRules properties
}
};
// this is an example of dynamic map loading via query param
// there are other possibilities such use injected map data in the page
// or use the internal react routing
// the important steps is to populate the configureMap action with a valid map config
const params = url.parse(window.location.href, true).query || {};
const mapName = params.map || 'new';
// load a base map configuration
axios.get(`configs/${mapName}.json`)
.then(({ data }) => {
// initialize the mapstore app
main(
{
targetId: 'container',
pages,
initialState: {
defaultState: {
...securityState,
maptype: {
mapType: MAP_TYPE,
last2dMapType: 'openlayers'
}
}
},
appReducers: {
security
},
appEpics: {},
printingEnabled: false
},
pluginsDef,
// TODO: use default main import to avoid override
(cfg) => ({
...cfg,
// remove epics that manage the map type for the standard product
appEpics: omit(cfg.appEpics, [
'syncMapType',
'updateLast2dMapTypeOnChangeEvents',
'restore2DMapTypeOnLocationChange'
]),
initialActions: [
setControlProperty.bind(null, 'toolbar', 'expanded', false),
configureMap.bind(null, data, 1, true)
]
}));
});
});
add a new static-map/index.ejs
template where to load the new app entry and theme with following content
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Static Map - MapStore</title>
<link rel="icon" type="image/x-icon" href="assets/img/favicon.ico"/>
<link rel="stylesheet" id="theme_stylesheet" href="<%= htmlWebpackPlugin.files.css.find((fileName) => fileName.indexOf('default') !== -1) %>?v=<%= version %>" type='text/css'>
<style>
body {
margin: 0;
}
._ms2_init_center {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: show;
margin: auto;
display: flex;
align-items: center;
}
._ms2_init_spinner {
height: 176px;
width: 176px;
}
._ms2_init_spinner > div,
._ms2_init_spinner > div:after {
border-radius: 50%;
width: 176px;
height: 176px;
}
._ms2_init_spinner > div {
box-sizing: border-box;
text-indent: -9999em;
border: 16px solid rgba(119,119,119, 0.2);
border-left: 16px solid #777777;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: _ms2_init_anim 1.1s infinite linear;
animation: _ms2_init_anim 1.1s infinite linear;
}
@-webkit-keyframes _ms2_init_anim {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes _ms2_init_anim {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
._ms2_init_text {
-webkit-animation: _ms2_init_text_anim 2s linear 0s infinite normal;
animation: _ms2_init_text_anim 2s linear 0s infinite normal;
color: #6F6F6f;
font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
font-size: 20px;
font-weight: bold;
height: 0.75em;
width: 6em;
text-align: center;
margin: auto;
z-index: 1000;
}
@keyframes _ms2_init_text_anim {
0% {opacity: 0}
20% {opacity: 0}
50% {opacity: 1}
70% {opacity: .75}
100%{opacity: 0}
}
.static-map,
.viewer,
#container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
#mapstore-burger-menu .dropdown-menu {
z-index: 1020;
}
</style>
</head>
<body class="<%= name %>" data-ms2-container="<%= name %>">
<div id="container">
<div class="_ms2_init_spinner _ms2_init_center"><div></div></div>
<div class="_ms2_init_text _ms2_init_center">Loading MapStore</div>
</div>
<script src="<%= htmlWebpackPlugin.files.js.find((fileName) => fileName.indexOf('static-map') !== -1) %>?v=<%= version %>"></script>
</body>
</html>
add a static-map/js/plugins/def.js
where to include all the plugins available in the app
/*
* Copyright 2023, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import MapPlugin from '@mapstore/framework/plugins/Map';
export default {
plugins: {
// framework plugins
MapPlugin
},
requires: {}
};
add a static-map/configs/localConfig.json
where to configure all the plugins inside the viewer or simple-viewer modes with following content
{
"proxyUrl": {
"url": "proxy/?url=",
"autoDetectCORS": true,
"useCORS": []
},
"monitorState": [
{
"name": "mapType",
"path": "maptype.mapType"
}
],
"projectionDefs": [],
"authenticationRules": [],
"plugins": [
{
"name": "Map",
"cfg": {
"mapOptions": {
"openlayers": {
"interactions": {
"pinchRotate": false,
"altShiftDragRotate": false
},
"attribution": {
"container": "#footer-attribution-container"
}
},
"leaflet": {
"attribution": {
"container": "#footer-attribution-container"
}
}
},
"toolsOptions": {
"scalebar": {
"container": "#footer-scalebar-container"
}
}
}
}
]
}
install all the dependencies with
npm install
Development¶
Run the application with the following command
npm start
app runs at http://localhost:8081/
npm install is needed only once at first setup or if the dependencies have been updated. If the installation does not solve correctly the dependencies use npm install –legacy-peer-deps (eg latest version of node/npm)
Build the app¶
You can run the build script to generate the client static application in the dist folder
./build.sh
the compiled app will be copied to dist/ folder
Best practices¶
Here some suggestions on how to improve the setup of a custom project:
ensure all the configuration path are initialized correctly. An example are the symbols for annotation and their location in the repository.
add a custom plugins definition and import only the plugins used by the app. This will reduce the size of final js bundle
use the alias @mapstore/framework/ to have access to the components inside the folder MapStore2/web/client/ of MapStore
use the alias @js/ to have access to the components inside the js folder of the custom project
use the options available in the main entries to initialize the state of the app with particular conditions (an example is the security state)