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:

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)