• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 14262787831

04 Apr 2025 09:55AM UTC coverage: 76.701% (-0.009%) from 76.71%
14262787831

Pull #10974

github

web-flow
Merge 2df0a8058 into 2c8a1d2bf
Pull Request #10974: [c027-2024.01.xx] #10970: Add configuration support to open resource in target from homepage

30685 of 48027 branches covered (63.89%)

3 of 16 new or added lines in 5 files covered. (18.75%)

34 existing lines in 8 files now uncovered.

38332 of 49976 relevant lines covered (76.7%)

32.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.39
/web/client/plugins/Map.jsx
1
/*
2
 * Copyright 2017, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
*/
8

9
import PropTypes from 'prop-types';
10
import React from 'react';
11
import { connect, createPlugin } from '../utils/PluginsUtils';
12
import Spinner from 'react-spinkit';
13
import './map/css/map.css';
14
import Message from '../components/I18N/Message';
15
import ConfigUtils from '../utils/ConfigUtils';
16
import { setMapResolutions, mapPluginLoad } from '../actions/map';
17
import { isString } from 'lodash';
18
import selector from './map/selector';
19
import MapSettings from './map/mapsettings/MapSettings';
20
import mapReducer from "../reducers/map";
21
import layersReducer from "../reducers/layers";
22
import drawReducer from "../reducers/draw";
23
import boxReducer from '../reducers/box';
24
import highlightReducer from "../reducers/highlight";
25
import mapTypeReducer from "../reducers/maptype";
26
import additionalLayersReducer from "../reducers/additionallayers";
27
import mapEpics from "../epics/map";
28
import pluginsCreator from "./map/index";
29
import withScalesDenominators from "../components/map/enhancers/withScalesDenominators";
30
import { createFeatureFilter } from '../utils/FilterUtils';
31
import ErrorPanel from '../components/map/ErrorPanel';
32
import catalog from "../epics/catalog";
33
import backgroundSelector from "../epics/backgroundselector";
34
import API from '../api/catalog';
35
import { MapLibraries } from '../utils/MapTypeUtils';
36
import {getHighlightLayerOptions} from "../utils/HighlightUtils";
37

38
/**
39
 * The Map plugin allows adding mapping library dependent functionality using support tools.
40
 * Some are already available for the supported mapping libraries (openlayers, leaflet, cesium), but it's possible to develop new ones.
41
 * The list of enabled tools can be configured using the tools property, as in the following example:
42
 *
43
 * ```
44
 * {
45
 * "name": "Map",
46
 * "cfg": {
47
 *     "tools": ["overview", "scalebar", "draw", "highlight"]
48
 *   ...
49
 *  }
50
 * }
51
 * ```
52
 * // Each tool can be configured using the toolsOptions. Tool configuration can be mapping library dependent:
53
 * ```
54
 * "toolsOptions": {
55
 *        "scalebar": {
56
 *            "leaflet": {
57
 *                "position": "bottomright"
58
 *            }
59
 *            ...
60
 *        }
61
 *        ...
62
 *    }
63
 *
64
 * ```
65
 * or not
66
 * ```
67
 * "toolsOptions": {
68
 * "scalebar": {
69
 *        "position": "bottomright"
70
 *        ...
71
 *    }
72
 *    ...
73
 * }
74
 * ```
75
 * In addition to standard tools, you can also develop your own, ad configure them to be used.
76
 *
77
 * To do that you need to:
78
 *  - develop a tool Component, in JSX (e.g. TestSupport), for each supported mapping library
79
 * ```
80
 * const React = require('react');
81
 *    class TestSupport extends React.Component {
82
 *     static propTypes = {
83
 *            label: PropTypes.string
84
 *        }
85
 *        render() {
86
 *            alert(this.props.label);
87
 *            return null;
88
 *        }
89
 *    }
90
 *    module.exports = TestSupport;
91
 * ```
92
 *  - include the tool(s) in the requires section of plugins.js amd give it a name:
93
 * ```
94
 *    module.exports = {
95
 *        plugins: {
96
 *            MapPlugin: require('../plugins/Map'),
97
 *            ...
98
 *        },
99
 *        requires: {
100
 *            ...
101
 *            TestSupportLeaflet: require('../components/map/leaflet/TestSupport')
102
 *        }
103
 *    };
104
 * ```
105
 *  - configure the Map plugin including the new tool and related options. You can configure the tool to be used for each mapping library, giving it a name and impl attributes, where:
106
 * ```
107
 *    {
108
 *      "name": "Map",
109
 *      "cfg": {
110
 *        "tools": ["overview", "scalebar", "draw", {
111
 *          "leaflet": {
112
 *            "name": "test",
113
 *            "impl": "{context.TestSupportLeaflet}"
114
 *          }
115
 *          }],
116
 *        "toolsOptions": {
117
 *          "test": {
118
 *            "label": "Hello"
119
 *          }
120
 *          ...
121
 *        }
122
 *      }
123
 *    }
124
 * ```
125
 *  - name is a unique name for the tool
126
 *  - impl is a placeholder (“{context.ToolName}”) where ToolName is the name you gave the tool in plugins.js (TestSupportLeaflet in our example)
127
 *
128
 * You can no longer specify a list of fonts that have to be loaded before map rendering, we are now only loading FontAwesome for the icons
129
 * We will pre-load FontAwesome only if needed, i.e you need to show markers with symbols (e.g. Annotations).
130
 *
131
 * An additional feature to is limit the area and/or the minimum level of zoom in the localConfig.json file using "mapConstraints" property
132
 *
133
 *  e.g
134
 * ```json
135
 * "mapConstraints": {
136
 *  "minZoom": 12, // minimal allowed zoom used by default
137
 *  "crs":"EPSG:3857", // crs of the restrictedExtent
138
 *  "restrictedExtent":[ // limits the area accessible to the user to this bounding box
139
 *    1060334.456371965,5228292.734706056,
140
 *    1392988.403469052,5503466.036532691
141
 *   ],
142
 *   "projectionsConstraints": {
143
 *       "EPSG:1234": { "minZoom": 5 } // customization of minZoom for different projections
144
 *   }
145
 *  }
146
 * ```
147
 *
148
 * With this setup you can configure a restricted area and/or a minimum zoom level for the whole application.
149
 * If you have different reference systems for your maps, for each of them you can even set a minimum zoom
150
 * using the entry `projectionsConstraints` as written in the example.
151
 *
152
 * ```
153
 *
154
 * @memberof plugins
155
 * @class Map
156
 * @prop {array} additionalLayers static layers available in addition to those loaded from the configuration
157
 * @prop {object} mapOptions map options grouped by map type
158
 * @prop {boolean} mapOptions.cesium.navigationTools enable cesium navigation tool (default false)
159
 * @prop {boolean} mapOptions.cesium.showSkyAtmosphere enable sky atmosphere of the globe (default true)
160
 * @prop {boolean} mapOptions.cesium.showGroundAtmosphere enable ground atmosphere of the globe (default false)
161
 * @prop {boolean} mapOptions.cesium.enableFog enable fog in the view (default false)
162
 * @prop {boolean} mapOptions.cesium.depthTestAgainstTerrain if true all primitive 3d features will be tested against the terrain while if false they will be drawn on top of the terrain even if hidden by it (default true)
163
 * @prop {number} mapOptions.cesium.maximumZoomDistance max zoom limit (in meter unit) to restrict the zoom out operation based on it
164
 * @prop {number} mapOptions.cesium.minimumZoomDistance  min zoom limit (in meter unit) to restrict the zoom in operation based on it
165
 * @static
166
 * @example
167
 * // Adding a layer to be used as a source for the elevation (shown in the MousePosition plugin configured with showElevation = true)
168
 * {
169
 *   "cfg": {
170
 *     "additionalLayers": [{
171
 *         "type": "wms",
172
 *         "url": "http://localhost:8090/geoserver/wms",
173
 *         "visibility": true,
174
 *         "title": "Elevation",
175
 *         "name": "topp:elevation",
176
 *         "format": "application/bil16",
177
 *         "useForElevation": true,
178
 *         "nodata": -9999,
179
 *         "littleendian": false,
180
 *         "hidden": true
181
 *      }]
182
 *   }
183
 * }
184
 *
185
 */
186

187

188
class MapPlugin extends React.Component {
189
    static propTypes = {
1✔
190
        mapType: PropTypes.string,
191
        map: PropTypes.object,
192
        mapId: PropTypes.number,
193
        layers: PropTypes.array,
194
        additionalLayers: PropTypes.array,
195
        zoomControl: PropTypes.bool,
196
        mapLoadingMessage: PropTypes.string,
197
        loadingSpinner: PropTypes.bool,
198
        loadingError: PropTypes.string,
199
        tools: PropTypes.array,
200
        options: PropTypes.object,
201
        mapOptions: PropTypes.object,
202
        projectionDefs: PropTypes.array,
203
        toolsOptions: PropTypes.object,
204
        onResolutionsChange: PropTypes.func,
205
        actions: PropTypes.object,
206
        features: PropTypes.array,
207
        securityToken: PropTypes.string,
208
        elevationEnabled: PropTypes.bool,
209
        isLocalizedLayerStylesEnabled: PropTypes.bool,
210
        localizedLayerStylesName: PropTypes.string,
211
        currentLocaleLanguage: PropTypes.string,
212
        items: PropTypes.array,
213
        onLoadingMapPlugins: PropTypes.func,
214
        onMapTypeLoaded: PropTypes.func,
215
        pluginsCreator: PropTypes.func,
216
        mapTitle: PropTypes.string
217
    };
218

219
    static defaultProps = {
1✔
220
        mapType: MapLibraries.OPENLAYERS,
221
        actions: {},
222
        zoomControl: false,
223
        mapLoadingMessage: "map.loading",
224
        loadingSpinner: true,
225
        tools: ["scalebar", "draw", "highlight", "popup", "box"],
226
        options: {},
227
        mapOptions: {},
228
        toolsOptions: {
229
            measurement: {},
230
            locate: {},
231
            scalebar: {
232
                [MapLibraries.LEAFLET]: {
233
                    position: "bottomright"
234
                }
235
            },
236
            overview: {
237
                overviewOpt: {
238
                    position: 'bottomright',
239
                    collapsedWidth: 25,
240
                    collapsedHeight: 25,
241
                    zoomLevelOffset: -5,
242
                    toggleDisplay: true
243
                },
244
                layers: [{type: "osm"}]
245
            }
246
        },
247
        securityToken: '',
248
        additionalLayers: [],
249
        elevationEnabled: false,
250
        onResolutionsChange: () => {},
251
        items: [],
252
        onLoadingMapPlugins: () => {},
253
        onMapTypeLoaded: () => {},
254
        pluginsCreator
255
    };
256

257
    state = {};
4✔
258
    UNSAFE_componentWillMount() {
259
        // moved the font load of FontAwesome only to styleParseUtils (#9653)
260
        this.updatePlugins(this.props);
4✔
261
        this._isMounted = true;
4✔
262
    }
263
    componentDidMount() {
264
        let isMapResource = this.props?.mapId;
4✔
265
        if (isMapResource) {
4!
266
            this.oldDocumentTitle = document.title;
×
267
        }
268
    }
269

270
    UNSAFE_componentWillReceiveProps(newProps) {
271
        if (newProps.mapType !== this.props.mapType || newProps.actions !== this.props.actions) {
3!
UNCOV
272
            this.updatePlugins(newProps);
×
273
        }
274
    }
275
    componentDidUpdate() {
276
        let isMapResource = this.props?.mapId;
7✔
277
        if (this.props.mapTitle && isMapResource) {
7!
UNCOV
278
            document.title = this.props.mapTitle;
×
279
        }
280
    }
281

282
    componentWillUnmount() {
283
        this._isMounted = false;
4✔
284
        let isMapResource = this.props?.mapId;
4✔
285
        if (isMapResource) {
4!
UNCOV
286
            document.title = this.oldDocumentTitle;
×
287
        }
288
    }
289

290
    getHighlightLayer = (projection, index, env) => {
4✔
UNCOV
291
        const plugins = this.state.plugins;
×
UNCOV
292
        const {features, ...options} = getHighlightLayerOptions({features: this.props.features});
×
UNCOV
293
        return (<plugins.Layer type="vector"
×
294
            srs={projection}
295
            position={index}
296
            key="highlight"
297
            env={env}
298
            options={{
299
                name: "highlight",
300
                ...options,
301
                features
302
            }} >
303
            {features.map( (feature) => {
UNCOV
304
                return (<plugins.Feature
×
305
                    msId={feature.id}
306
                    properties={feature.properties}
307
                    key={feature.id}
308
                    crs={projection}
309
                    type={feature.type}
310
                    style={feature.style || null }
×
311
                    geometry={feature.geometry}/>);
312
            })}
313
        </plugins.Layer>);
314
    };
315

316
    getTool = (tool) => {
4✔
317
        if (isString(tool)) {
35!
318
            return {
35✔
319
                name: tool,
320
                impl: this.state.plugins.tools[tool]
321
            };
322
        }
UNCOV
323
        return tool[this.props.mapType] || tool;
×
324
    };
325

326
    getConfigMapOptions = () => {
4✔
327
        return this.props.mapOptions && this.props.mapOptions[this.props.mapType] ||
7!
328
            ConfigUtils.getConfigProp("defaultMapOptions") && ConfigUtils.getConfigProp("defaultMapOptions")[this.props.mapType] || {};
329
    };
330

331
    renderLayers = () => {
4✔
332
        const projection = this.props.map.projection || 'EPSG:3857';
7✔
333
        const env = [];
7✔
334

335
        if (this.props.isLocalizedLayerStylesEnabled) {
7!
UNCOV
336
            env.push({
×
337
                name: this.props.localizedLayerStylesName,
338
                value: this.props.currentLocaleLanguage
339
            });
340
        }
341
        const plugins = this.state.plugins;
7✔
342
        // all layers must have a valid id to avoid useless re-render
343
        return [...this.props.layers, ...this.props.additionalLayers.map(({ id, ...layer }, idx) => ({ ...layer, id: id ? id : `additional-layers-${idx}` }))].filter(this.filterLayer).map((layer, index) => {
7!
344
            return (
1✔
345
                <plugins.Layer
346
                    type={layer.type}
347
                    srs={projection}
348
                    position={index}
349
                    key={layer.id || layer.name}
1!
350
                    options={layer}
351
                    securityToken={this.props.securityToken}
352
                    env={env}
353
                >
354
                    {this.renderLayerContent(layer, projection)}
355
                </plugins.Layer>
356
            );
357
        }).concat(this.props.features && this.props.features.length && this.getHighlightLayer(projection, this.props.layers.length, env) || []);
21!
358
    };
359

360
    renderLayerContent = (layer, projection) => {
4✔
361
        const plugins = this.state.plugins;
1✔
362
        if (layer.features) {
1!
UNCOV
363
            return layer.features.filter(createFeatureFilter(layer.filterObj)).map( (feature) => {
×
UNCOV
364
                return (
×
365
                    <plugins.Feature
366
                        key={feature.id}
367
                        msId={feature.id}
368
                        type={feature.type}
369
                        crs={projection}
370
                        geometry={feature.geometry}
371
                        features={feature.features}
372
                        featuresCrs={ layer.featuresCrs || 'EPSG:4326' }
×
373
                        // FEATURE STYLE OVERWRITE LAYER STYLE
374
                        layerStyle={layer.style}
375
                        style={ feature.style || layer.style || null }
×
376
                        properties={feature.properties}/>
377
                );
378
            });
379
        }
380
        return null;
1✔
381
    };
382

383
    renderSupportTools = () => {
4✔
384
        // Tools passed by other plugins
385
        const toolsFromItems = this.props.items
7✔
UNCOV
386
            .filter(({Tool}) => !!Tool)
×
UNCOV
387
            .map(({Tool, name, cfg}) => <Tool {...cfg} key={name} mapType={this.props.mapType} />);
×
388

389
        return this.props.tools.map((tool) => {
7✔
390
            const Tool = this.getTool(tool);
35✔
391
            const options = this.props.toolsOptions[Tool.name] && this.props.toolsOptions[Tool.name][this.props.mapType] || this.props.toolsOptions[Tool.name] || {};
35✔
392
            return <Tool.impl key={Tool.name} {...options}/>;
35✔
393
        }).concat(toolsFromItems);
394
    };
395

396
    render() {
397
        if (this.isValidMapConfiguration(this.props.map) && this.state.plugins) {
11✔
398
            const {mapOptions = {}} = this.props.map;
7✔
399

400
            return (
7✔
401
                <this.state.plugins.Map id="map"
402
                    {...this.props.options}
403
                    projectionDefs={this.props.projectionDefs}
404
                    {...this.props.map}
405
                    mapOptions={{...this.getConfigMapOptions(), ...mapOptions}}
406
                    zoomControl={this.props.zoomControl}
407
                    onResolutionsChange={this.props.onResolutionsChange}
408
                    errorPanel={ErrorPanel}
409
                >
410
                    {this.renderLayers()}
411
                    {this.renderSupportTools()}
412
                </this.state.plugins.Map>
413
            );
414
        }
415
        if (this.props.loadingError) {
4!
UNCOV
416
            return (<div style={{
×
417
                width: "100%",
418
                height: "100%",
419
                display: "flex",
420
                justifyContent: "center",
421
                alignItems: "center"
422
            }} className="mapErrorMessage">
423
                <Message msgId="map.loadingerror"/>:
424
                {this.props.loadingError}
425
            </div>);
426
        }
427
        return (<div style={{
4✔
428
            width: "100%",
429
            height: "100%",
430
            display: "flex",
431
            justifyContent: "center",
432
            alignItems: "center"
433
        }} className="mapLoadingMessage">
434
            {this.props.loadingSpinner ? <Spinner spinnerName="circle" overrideSpinnerClassName="spinner"/> : null}
4!
435
            <Message msgId={this.props.mapLoadingMessage}/>
436
        </div>);
437
    }
438
    filterLayer = (layer) => {
4✔
439
        if (layer.useForElevation) {
1!
UNCOV
440
            return this.props.mapType === 'cesium' || this.props.elevationEnabled;
×
441
        }
442
        return layer.type !== 'elevation' || this.props.elevationEnabled;
1!
443
    };
444
    updatePlugins = (props) => {
4✔
445
        this.currentMapType = props.mapType;
4✔
446
        props.onLoadingMapPlugins(true);
4✔
447
        // reset the map plugins to avoid previous map library in children
448
        this.setState({plugins: undefined });
4✔
449
        this.props.pluginsCreator(props.mapType, props.actions).then((plugins) => {
4✔
450
            // #6652 fix mismatch on multiple concurrent plugins loading
451
            // to make the last mapType match the list of plugins
452
            if (this._isMounted && plugins.mapType === this.currentMapType) {
4!
453
                this.setState({plugins});
4✔
454
                props.onLoadingMapPlugins(false, props.mapType);
4✔
455
                props.onMapTypeLoaded(true, props.mapType);
4✔
456
            }
457
        });
458
    };
459
    isValidMapConfiguration = (map) => {
4✔
460
        // when the center is included inside the map config
461
        // we know that the configuration has been loaded
462
        // we should prevent to mount the map component
463
        // in case we have a configuration like this one: { eventListeners: {}, mousePointer: '' }
464
        // if we allow invalid configuration default props will be used instead
465
        // initializing the map in the wrong position
466
        return !!map?.center;
11✔
467
    }
468
}
469

470
export default createPlugin('Map', {
471
    component: connect(selector, {
472
        onResolutionsChange: setMapResolutions,
473
        onMapTypeLoaded: mapPluginLoad
474
    })(withScalesDenominators(MapPlugin)),
475
    reducers: {
476
        map: mapReducer,
477
        layers: layersReducer,
478
        draw: drawReducer,
479
        box: boxReducer,
480
        highlight: highlightReducer,
481
        maptype: mapTypeReducer,
482
        additionallayers: additionalLayersReducer
483
    },
484
    epics: {
485
        ...mapEpics,
486
        ...backgroundSelector,
487
        ...catalog(API)
488
    },
489
    containers: {
UNCOV
490
        Settings: () => ({
×
491
            tool: <MapSettings />,
492
            position: 2
493
        })
494
    }
495
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc