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

geosolutions-it / MapStore2 / 12767533386

14 Jan 2025 12:22PM UTC coverage: 77.12% (+0.4%) from 76.716%
12767533386

Pull #10609

github

web-flow
Merge 372554eeb into 779416da2
Pull Request #10609: Normalize layers/groups management in TOC #10247

30300 of 47064 branches covered (64.38%)

6 of 6 new or added lines in 1 file covered. (100.0%)

522 existing lines in 35 files now uncovered.

37639 of 48806 relevant lines covered (77.12%)

34.91 hits per line

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

77.78
/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
        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
    };
216

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

255
    state = {};
5✔
256

257
    UNSAFE_componentWillMount() {
258
        // moved the font load of FontAwesome only to styleParseUtils (#9653)
259
        this.updatePlugins(this.props);
5✔
260
        this._isMounted = true;
5✔
261
    }
262

263
    UNSAFE_componentWillReceiveProps(newProps) {
264
        if (newProps.mapType !== this.props.mapType || newProps.actions !== this.props.actions) {
3!
UNCOV
265
            this.updatePlugins(newProps);
×
266
        }
267
    }
268

269
    componentWillUnmount() {
270
        this._isMounted = false;
5✔
271
    }
272

273
    getHighlightLayer = (projection, index, env) => {
5✔
UNCOV
274
        const plugins = this.state.plugins;
×
275
        const {features, ...options} = getHighlightLayerOptions({features: this.props.features});
×
276
        return (<plugins.Layer type="vector"
×
277
            srs={projection}
278
            position={index}
279
            key="highlight"
280
            env={env}
281
            options={{
282
                name: "highlight",
283
                ...options,
284
                features
285
            }} >
286
            {features.map( (feature) => {
UNCOV
287
                return (<plugins.Feature
×
288
                    msId={feature.id}
289
                    properties={feature.properties}
290
                    key={feature.id}
291
                    crs={projection}
292
                    type={feature.type}
293
                    style={feature.style || null }
×
294
                    geometry={feature.geometry}/>);
295
            })}
296
        </plugins.Layer>);
297
    };
298

299
    getTool = (tool) => {
5✔
300
        if (isString(tool)) {
35!
301
            return {
35✔
302
                name: tool,
303
                impl: this.state.plugins.tools[tool]
304
            };
305
        }
UNCOV
306
        return tool[this.props.mapType] || tool;
×
307
    };
308

309
    getConfigMapOptions = () => {
5✔
310
        return this.props.mapOptions && this.props.mapOptions[this.props.mapType] ||
7!
311
            ConfigUtils.getConfigProp("defaultMapOptions") && ConfigUtils.getConfigProp("defaultMapOptions")[this.props.mapType] || {};
312
    };
313

314
    renderLayers = () => {
5✔
315
        const projection = this.props.map.projection || 'EPSG:3857';
7✔
316
        const env = [];
7✔
317

318
        if (this.props.isLocalizedLayerStylesEnabled) {
7!
UNCOV
319
            env.push({
×
320
                name: this.props.localizedLayerStylesName,
321
                value: this.props.currentLocaleLanguage
322
            });
323
        }
324
        const plugins = this.state.plugins;
7✔
325
        // all layers must have a valid id to avoid useless re-render
326
        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!
327
            return (
1✔
328
                <plugins.Layer
329
                    type={layer.type}
330
                    srs={projection}
331
                    position={index}
332
                    key={layer.id || layer.name}
1!
333
                    options={layer}
334
                    securityToken={this.props.securityToken}
335
                    env={env}
336
                >
337
                    {this.renderLayerContent(layer, projection)}
338
                </plugins.Layer>
339
            );
340
        }).concat(this.props.features && this.props.features.length && this.getHighlightLayer(projection, this.props.layers.length, env) || []);
21!
341
    };
342

343
    renderLayerContent = (layer, projection) => {
5✔
344
        const plugins = this.state.plugins;
1✔
345
        if (layer.features) {
1!
UNCOV
346
            return layer.features.filter(createFeatureFilter(layer.filterObj)).map( (feature) => {
×
347
                return (
×
348
                    <plugins.Feature
349
                        key={feature.id}
350
                        msId={feature.id}
351
                        type={feature.type}
352
                        crs={projection}
353
                        geometry={feature.geometry}
354
                        features={feature.features}
355
                        featuresCrs={ layer.featuresCrs || 'EPSG:4326' }
×
356
                        // FEATURE STYLE OVERWRITE LAYER STYLE
357
                        layerStyle={layer.style}
358
                        style={ feature.style || layer.style || null }
×
359
                        properties={feature.properties}/>
360
                );
361
            });
362
        }
363
        return null;
1✔
364
    };
365

366
    renderSupportTools = () => {
5✔
367
        // Tools passed by other plugins
368
        const toolsFromItems = this.props.items
7✔
UNCOV
369
            .filter(({Tool}) => !!Tool)
×
370
            .map(({Tool, name, cfg}) => <Tool {...cfg} key={name} mapType={this.props.mapType} />);
×
371

372
        return this.props.tools.map((tool) => {
7✔
373
            const Tool = this.getTool(tool);
35✔
374
            const options = this.props.toolsOptions[Tool.name] && this.props.toolsOptions[Tool.name][this.props.mapType] || this.props.toolsOptions[Tool.name] || {};
35✔
375
            return <Tool.impl key={Tool.name} {...options}/>;
35✔
376
        }).concat(toolsFromItems);
377
    };
378

379
    render() {
380
        if (this.isValidMapConfiguration(this.props.map) && this.state.plugins) {
13✔
381
            const {mapOptions = {}} = this.props.map;
7✔
382

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

453
export default createPlugin('Map', {
454
    component: connect(selector, {
455
        onResolutionsChange: setMapResolutions,
456
        onMapTypeLoaded: mapPluginLoad
457
    })(withScalesDenominators(MapPlugin)),
458
    reducers: {
459
        map: mapReducer,
460
        layers: layersReducer,
461
        draw: drawReducer,
462
        box: boxReducer,
463
        highlight: highlightReducer,
464
        maptype: mapTypeReducer,
465
        additionallayers: additionalLayersReducer
466
    },
467
    epics: {
468
        ...mapEpics,
469
        ...backgroundSelector,
470
        ...catalog(API)
471
    },
472
    containers: {
UNCOV
473
        Settings: () => ({
×
474
            tool: <MapSettings />,
475
            position: 2
476
        })
477
    }
478
});
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