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

geosolutions-it / MapStore2 / 13454072692

21 Feb 2025 09:38AM UTC coverage: 76.727% (+0.01%) from 76.717%
13454072692

push

github

web-flow
#10737: Interactive legend for TOC layers [Vector Layer part] (#10798)



---------

Co-authored-by: stefano bovio <stefano.bovio@geosolutionsgroup.com>

31262 of 48901 branches covered (63.93%)

25 of 40 new or added lines in 6 files covered. (62.5%)

2 existing lines in 2 files now uncovered.

38804 of 50574 relevant lines covered (76.73%)

34.45 hits per line

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

76.56
/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
    };
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!
265
            this.updatePlugins(newProps);
×
266
        }
267
    }
268

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

273
    getHighlightLayer = (projection, index, env) => {
5✔
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) => {
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
        }
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!
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!
NEW
346
            const vectorFeatureFilter = createVectorFeatureFilter(layer);
×
NEW
347
            return layer.features.filter(vectorFeatureFilter).map((feature) => {
×
UNCOV
348
                return (
×
349
                    <plugins.Feature
350
                        key={feature.id}
351
                        msId={feature.id}
352
                        type={feature.type}
353
                        crs={projection}
354
                        geometry={feature.geometry}
355
                        features={feature.features}
356
                        featuresCrs={ layer.featuresCrs || 'EPSG:4326' }
×
357
                        // FEATURE STYLE OVERWRITE LAYER STYLE
358
                        layerStyle={layer.style}
359
                        style={ feature.style || layer.style || null }
×
360
                        properties={feature.properties}/>
361
                );
362
            });
363
        }
364
        return null;
1✔
365
    };
366

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

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

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

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

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