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

geosolutions-it / MapStore2 / 14062056961

25 Mar 2025 02:22PM UTC coverage: 77.763% (+0.8%) from 76.983%
14062056961

Pull #10955

github

web-flow
Merge c2b1dbfa2 into 2cefcabae
Pull Request #10955: #10936: Add resource name in the window title of the browser

7973 of 12204 branches covered (65.33%)

17 of 22 new or added lines in 5 files covered. (77.27%)

23 existing lines in 4 files now uncovered.

10764 of 13842 relevant lines covered (77.76%)

129.75 hits per line

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

75.34
/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 { createVectorFeatureFilter } 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
        layers: PropTypes.array,
193
        additionalLayers: PropTypes.array,
194
        zoomControl: PropTypes.bool,
195
        mapLoadingMessage: PropTypes.string,
196
        loadingSpinner: PropTypes.bool,
197
        loadingError: PropTypes.string,
198
        tools: PropTypes.array,
199
        options: PropTypes.object,
200
        mapOptions: PropTypes.object,
201
        projectionDefs: PropTypes.array,
202
        toolsOptions: PropTypes.object,
203
        onResolutionsChange: PropTypes.func,
204
        actions: PropTypes.object,
205
        features: PropTypes.array,
206
        securityToken: PropTypes.string,
207
        elevationEnabled: PropTypes.bool,
208
        isLocalizedLayerStylesEnabled: PropTypes.bool,
209
        localizedLayerStylesName: PropTypes.string,
210
        currentLocaleLanguage: PropTypes.string,
211
        items: PropTypes.array,
212
        onLoadingMapPlugins: PropTypes.func,
213
        onMapTypeLoaded: PropTypes.func,
214
        pluginsCreator: PropTypes.func,
215
        mapTitle: PropTypes.string
216
    };
217

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

256
    state = {};
5✔
257
    componentDidMount() {
258
        let isMapResource = this.props?.mapId;
5✔
259
        if (isMapResource) {
5!
NEW
260
            this.oldDocumentTitle = document.title;
×
261
        }
262
    }
263
    componentDidUpdate() {
264
        let isMapResource = this.props?.mapId;
8✔
265
        if (this.props.mapTitle && isMapResource) {
8!
NEW
266
            document.title = this.props.mapTitle;
×
267
        }
268
    }
269
    UNSAFE_componentWillMount() {
270
        // moved the font load of FontAwesome only to styleParseUtils (#9653)
271
        this.updatePlugins(this.props);
5✔
272
        this._isMounted = true;
5✔
273
    }
274

275
    UNSAFE_componentWillReceiveProps(newProps) {
276
        if (newProps.mapType !== this.props.mapType || newProps.actions !== this.props.actions) {
3!
277
            this.updatePlugins(newProps);
×
278
        }
279
    }
280

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

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

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

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

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

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

359
    renderLayerContent = (layer, projection) => {
5✔
360
        const plugins = this.state.plugins;
1✔
361
        if (layer.features) {
1!
362
            const vectorFeatureFilter = createVectorFeatureFilter(layer);
×
363
            return layer.features.filter(vectorFeatureFilter).map((feature) => {
×
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 = () => {
5✔
384
        // Tools passed by other plugins
385
        const toolsFromItems = this.props.items
7✔
386
            .filter(({Tool}) => !!Tool)
×
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) {
13✔
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) {
6!
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={{
6✔
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}
6!
435
            <Message msgId={this.props.mapLoadingMessage}/>
436
        </div>);
437
    }
438
    filterLayer = (layer) => {
5✔
439
        if (layer.useForElevation) {
1!
440
            return this.props.mapType === 'cesium' || this.props.elevationEnabled;
×
441
        }
442
        return layer.type !== 'elevation' || this.props.elevationEnabled;
1!
443
    };
444
    updatePlugins = (props) => {
5✔
445
        this.currentMapType = props.mapType;
5✔
446
        props.onLoadingMapPlugins(true);
5✔
447
        // reset the map plugins to avoid previous map library in children
448
        this.setState({plugins: undefined });
5✔
449
        this.props.pluginsCreator(props.mapType, props.actions).then((plugins) => {
5✔
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) {
5!
453
                this.setState({plugins});
5✔
454
                props.onLoadingMapPlugins(false, props.mapType);
5✔
455
                props.onMapTypeLoaded(true, props.mapType);
5✔
456
            }
457
        });
458
    };
459
    isValidMapConfiguration = (map) => {
5✔
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;
13✔
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: {
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