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

geosolutions-it / MapStore2 / 13125376331

04 Feb 2025 12:06AM UTC coverage: 76.858% (+0.008%) from 76.85%
13125376331

Pull #10798

github

web-flow
Merge edbbfc354 into 490d96d88
Pull Request #10798: #10737: Interactive legend for TOC layers [Vector Layer part]

31107 of 48619 branches covered (63.98%)

18 of 30 new or added lines in 4 files covered. (60.0%)

6 existing lines in 3 files now uncovered.

38654 of 50293 relevant lines covered (76.86%)

34.4 hits per line

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

73.13
/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, filterVectorLayerFeatures } 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
            let renderedFeatures = layer.features;
×
347
            // For openlayers, rendering features of vector layer will be handled by 'plugins.Feature' here
348
            // so filter the vector featuers should be handled here as well
349
            // for leaflet/cesium --> rendering the vector layer is implemented within the VectorLayer not Feature
NEW
350
            const isOLVectorLayer = layer.type === 'vector' && plugins.mapType === MapLibraries.OPENLAYERS;
×
NEW
351
            if (isOLVectorLayer) {
×
NEW
352
                renderedFeatures = renderedFeatures.filter(filterVectorLayerFeatures(layer));
×
353
            }
NEW
354
            return renderedFeatures.filter(createFeatureFilter(layer.filterObj)).map( (feature) => {
×
UNCOV
355
                return (
×
356
                    <plugins.Feature
357
                        key={feature.id}
358
                        msId={feature.id}
359
                        type={feature.type}
360
                        crs={projection}
361
                        geometry={feature.geometry}
362
                        features={feature.features}
363
                        featuresCrs={ layer.featuresCrs || 'EPSG:4326' }
×
364
                        // FEATURE STYLE OVERWRITE LAYER STYLE
365
                        layerStyle={layer.style}
366
                        style={ feature.style || layer.style || null }
×
367
                        properties={feature.properties}/>
368
                );
369
            });
370
        }
371
        return null;
1✔
372
    };
373

374
    renderSupportTools = () => {
5✔
375
        // Tools passed by other plugins
376
        const toolsFromItems = this.props.items
7✔
377
            .filter(({Tool}) => !!Tool)
×
378
            .map(({Tool, name, cfg}) => <Tool {...cfg} key={name} mapType={this.props.mapType} />);
×
379

380
        return this.props.tools.map((tool) => {
7✔
381
            const Tool = this.getTool(tool);
35✔
382
            const options = this.props.toolsOptions[Tool.name] && this.props.toolsOptions[Tool.name][this.props.mapType] || this.props.toolsOptions[Tool.name] || {};
35✔
383
            return <Tool.impl key={Tool.name} {...options}/>;
35✔
384
        }).concat(toolsFromItems);
385
    };
386

387
    render() {
388
        if (this.isValidMapConfiguration(this.props.map) && this.state.plugins) {
13✔
389
            const {mapOptions = {}} = this.props.map;
7✔
390

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

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