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

geosolutions-it / MapStore2 / 16496890050

24 Jul 2025 12:29PM UTC coverage: 76.791% (+0.006%) from 76.785%
16496890050

Pull #11356

github

web-flow
Merge 04e04cf09 into 7544f6593
Pull Request #11356: [Backport 2024.02.xx] Fix #11175 parsing WMS capabilities when no global SRS is present (#11177)

31240 of 48784 branches covered (64.04%)

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

15 existing lines in 3 files now uncovered.

38934 of 50701 relevant lines covered (76.79%)

33.95 hits per line

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

91.71
/web/client/components/map/openlayers/Map.jsx
1
/**
2
 * Copyright 2015-2016, 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 { defaults, DragPan, MouseWheelZoom } from 'ol/interaction';
10
import { defaults as defaultControls } from 'ol/control';
11
import Map from 'ol/Map';
12
import View from 'ol/View';
13
import { get as getProjection, toLonLat } from 'ol/proj';
14
import Zoom from 'ol/control/Zoom';
15
import GeoJSON from 'ol/format/GeoJSON';
16

17
import proj4 from 'proj4';
18
import { register } from 'ol/proj/proj4.js';
19
import PropTypes from 'prop-types';
20
import React from 'react';
21
import assign from 'object-assign';
22

23
import {reproject, reprojectBbox, normalizeLng, normalizeSRS } from '../../../utils/CoordinatesUtils';
24
import { getProjection as msGetProjection }  from '../../../utils/ProjectionUtils';
25
import ConfigUtils from '../../../utils/ConfigUtils';
26
import mapUtils, { getResolutionsForProjection } from '../../../utils/MapUtils';
27
import projUtils from '../../../utils/openlayers/projUtils';
28
import { DEFAULT_INTERACTION_OPTIONS } from '../../../utils/openlayers/DrawUtils';
29

30
import {isEqual, find, throttle, isArray, isNil} from 'lodash';
31

32
import 'ol/ol.css';
33

34
// add overrides for css
35
import './mapstore-ol-overrides.css';
36

37
const geoJSONFormat = new GeoJSON();
1✔
38

39
class OpenlayersMap extends React.Component {
40
    static propTypes = {
1✔
41
        id: PropTypes.string,
42
        document: PropTypes.object,
43
        style: PropTypes.object,
44
        center: ConfigUtils.PropTypes.center,
45
        zoom: PropTypes.number.isRequired,
46
        mapStateSource: ConfigUtils.PropTypes.mapStateSource,
47
        projection: PropTypes.string,
48
        projectionDefs: PropTypes.array,
49
        onMapViewChanges: PropTypes.func,
50
        onResolutionsChange: PropTypes.func,
51
        onClick: PropTypes.func,
52
        mapOptions: PropTypes.object,
53
        zoomControl: PropTypes.bool,
54
        mousePointer: PropTypes.string,
55
        onMouseMove: PropTypes.func,
56
        onLayerLoading: PropTypes.func,
57
        onLayerLoad: PropTypes.func,
58
        onLayerError: PropTypes.func,
59
        resize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
60
        measurement: PropTypes.object,
61
        changeMeasurementState: PropTypes.func,
62
        registerHooks: PropTypes.bool,
63
        hookRegister: PropTypes.object,
64
        interactive: PropTypes.bool,
65
        onCreationError: PropTypes.func,
66
        bbox: PropTypes.object,
67
        wpsBounds: PropTypes.object,
68
        onWarning: PropTypes.func,
69
        maxExtent: PropTypes.array,
70
        limits: PropTypes.object,
71
        onMouseOut: PropTypes.func
72
    };
73

74
    static defaultProps = {
1✔
75
        id: 'map',
76
        onMapViewChanges: () => { },
77
        onResolutionsChange: () => { },
78
        onCreationError: () => { },
79
        onClick: null,
80
        onMouseMove: () => { },
81
        mapOptions: {},
82
        projection: 'EPSG:3857',
83
        projectionDefs: [],
84
        onLayerLoading: () => { },
85
        onLayerLoad: () => { },
86
        onLayerError: () => { },
87
        resize: 0,
88
        registerHooks: true,
89
        hookRegister: mapUtils,
90
        interactive: true,
91
        onMouseOut: () => {},
92
        center: { x: 13, y: 45, crs: 'EPSG:4326' },
93
        zoom: 5
94
    };
95

96
    componentDidMount() {
97
        // adding EPSG:4269, by default included in proj4 definitions,
98
        // so that we have extents needed by ol
99
        const defs = [{
132✔
100
            "code": "EPSG:4269",
101
            "def": "+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs",
102
            "axisOrientation": "neu",
103
            "extent": [-172.54, 23.81, -47.74, 86.46],
104
            "worldExtent": [-172.54, 23.81, -47.74, 86.46]
105
        }, ...this.props.projectionDefs];
106
        defs.forEach(p => {
132✔
107
            const projDef = proj4.defs(p.code);
137✔
108
            projUtils.addProjections(p.code, p.extent, p.worldExtent, p.axisOrientation || projDef.axis || 'enu', projDef.units || 'm');
137!
109
        });
110
        // It may be a good idea to check if CoordinateUtils also registered the projectionDefs
111
        // normally it happens ad application level.
112
        let center = reproject([this.props.center.x, this.props.center.y], 'EPSG:4326', this.props.projection);
132✔
113
        register(proj4);
132✔
114
        // interactive flag is used only for initializations,
115
        // TODO manage it also when it changes status (ComponentWillReceiveProps)
116
        let interactionsOptions = assign(
132✔
117
            this.props.interactive ?
132✔
118
                {} :
119
                {
120
                    doubleClickZoom: false,
121
                    dragPan: false,
122
                    altShiftDragRotate: false,
123
                    keyboard: false,
124
                    mouseWheelZoom: false,
125
                    shiftDragZoom: false,
126
                    pinchRotate: false,
127
                    pinchZoom: false
128
                },
129
            this.props.mapOptions.interactions);
130

131
        let interactions = defaults(assign({
132✔
132
            dragPan: false,
133
            mouseWheelZoom: false
134
        }, interactionsOptions, {}));
135
        if (interactionsOptions === undefined || interactionsOptions.dragPan === undefined) {
132✔
136
            this.dragPanInteraction = new DragPan({ kinetic: false });
85✔
137
            interactions.extend([
85✔
138
                this.dragPanInteraction
139
            ]);
140
        }
141
        if (interactionsOptions === undefined || interactionsOptions.mouseWheelZoom === undefined) {
132✔
142
            this.mouseWheelInteraction = new MouseWheelZoom({ duration: 0 });
84✔
143
            interactions.extend([
84✔
144
                this.mouseWheelInteraction
145
            ]);
146
        }
147
        let controls = defaultControls(assign({
132✔
148
            zoom: this.props.zoomControl,
149
            attributionOptions: assign({
150
                collapsible: false
151
            }, this.props.mapOptions.attribution && this.props.mapOptions.attribution.container ? {
273✔
152
                target: this.getDocument().querySelector(this.props.mapOptions.attribution.container)
153
            } : {})
154
        }, this.props.mapOptions.controls));
155

156
        let map = new Map({
132✔
157
            layers: [],
158
            controls: controls,
159
            interactions: interactions,
160
            maxTilesLoading: Infinity,
161
            target: this.getDocument().getElementById(this.props.id) || `${this.props.id}`,
132!
162
            view: this.createView(center, Math.round(this.props.zoom), this.props.projection, this.props.mapOptions && this.props.mapOptions.view, this.props.limits)
264✔
163
        });
164

165
        this.map = map;
132✔
166
        if (this.props.registerHooks) {
132✔
167
            this.registerHooks();
114✔
168
        }
169
        this.map.disabledListeners = {};
132✔
170
        this.map.disableEventListener = (event) => {
132✔
171
            this.map.disabledListeners[event] = true;
2✔
172
        };
173
        this.map.enableEventListener = (event) => {
132✔
174
            delete this.map.disabledListeners[event];
×
175
        };
176
        // The timeout is needed to cover the delay we have for the throttled mouseMove event.
177
        this.map.getViewport().addEventListener('mouseout', () => {
132✔
178
            setTimeout(() => this.props.onMouseOut(), 150);
×
179
        });
180
        // TODO support disableEventListener
181
        map.on('moveend', this.updateMapInfoState);
132✔
182
        map.on('singleclick', (event) => {
132✔
183
            if (this.props.onClick && !this.map.disabledListeners.singleclick) {
8✔
184
                let pos = event.coordinate.slice();
6✔
185
                let projectionExtent = this.getExtent(this.map, this.props);
6✔
186
                if (this.props.projection === 'EPSG:4326') {
6✔
187
                    pos[0] = normalizeLng(pos[0]);
5✔
188
                }
189
                if (this.props.projection === 'EPSG:900913' || this.props.projection === 'EPSG:3857') {
6✔
190
                    pos = toLonLat(pos, this.props.projection);
1✔
191
                    projectionExtent = reprojectBbox(projectionExtent, this.props.projection, "EPSG:4326");
1✔
192
                }
193
                // prevent user from clicking outside the projection extent
194
                if (pos[0] >= projectionExtent[0] && pos[0] <= projectionExtent[2] &&
6!
195
                    pos[1] >= projectionExtent[1] && pos[1] <= projectionExtent[3]) {
196
                    let coords;
197
                    if (this.props.projection !== 'EPSG:900913' && this.props.projection !== 'EPSG:3857') {
6✔
198
                        coords = reproject(pos, this.props.projection, "EPSG:4326");
5✔
199
                    } else {
200
                        coords = { x: pos[0], y: pos[1] };
1✔
201
                    }
202

203
                    let layerInfo;
204
                    this.markerPresent = false;
6✔
205
                    /*
206
                     * Handle special case for vector features with handleClickOnLayer=true
207
                     * Modifies the clicked point coordinates to center the marker and sets the layerInfo for
208
                     * the clickPoint event (used as flag to show or hide marker)
209
                     */
210
                    map.forEachFeatureAtPixel(event.pixel, (feature, layer) => {
6✔
211
                        if (layer && layer.get('handleClickOnLayer')) {
5✔
212
                            const geom = feature.getGeometry();
2✔
213
                            // TODO: We should find out a better way to identify it then checking geometry type
214
                            if (!this.markerPresent && geom.getType() === "Point") {
2✔
215
                                this.markerPresent = true;
1✔
216
                                layerInfo = layer.get('msId');
1✔
217
                                const arr = toLonLat(geom.getFirstCoordinate(), this.props.projection);
1✔
218
                                coords = { x: arr[0], y: arr[1] };
1✔
219
                            }
220
                        }
221
                    });
222
                    const intersectedFeatures = this.getIntersectedFeatures(map, event?.pixel);
6✔
223
                    const tLng = normalizeLng(coords.x);
6✔
224
                    this.props.onClick({
6✔
225
                        pixel: {
226
                            x: event.pixel[0],
227
                            y: event.pixel[1]
228
                        },
229
                        latlng: {
230
                            lat: coords.y,
231
                            lng: tLng,
232
                            z: this.getElevation(pos, event.pixel)
233
                        },
234
                        rawPos: event.coordinate.slice(),
235
                        modifiers: {
236
                            alt: event.originalEvent.altKey,
237
                            ctrl: event.originalEvent.ctrlKey,
238
                            metaKey: event.originalEvent.metaKey, // MAC OS
239
                            shift: event.originalEvent.shiftKey
240
                        },
241
                        intersectedFeatures
242
                    }, layerInfo);
243
                }
244
            }
245
        });
246
        const mouseMove = throttle(this.mouseMoveEvent, 100);
132✔
247
        // TODO support disableEventListener
248
        map.on('pointermove', mouseMove);
132✔
249
        this.updateMapInfoState();
132✔
250
        this.setMousePointer(this.props.mousePointer);
132✔
251
        // NOTE: this re-call render function after div creation to have the map initialized.
252
        this.forceUpdate();
132✔
253

254
        this.props.onResolutionsChange(this.getResolutions());
132✔
255
    }
256

257
    UNSAFE_componentWillReceiveProps(newProps) {
258
        if (newProps.mousePointer !== this.props.mousePointer) {
39!
259
            this.setMousePointer(newProps.mousePointer);
×
260
        }
261
        if (newProps.zoomControl !== this.props.zoomControl) {
39!
262
            if (newProps.zoomControl) {
×
263
                this.map.addControl(new Zoom());
×
264
            } else {
265
                this.map.removeControl(this.map.getControls().getArray().filter((ctl) => ctl instanceof Zoom)[0]);
×
266
            }
267
        }
268

269
        /*
270
         * Manage interactions programmatically.
271
         * map interactions may change, i.e. becoming enabled or disabled
272
         * TODO: with re-generation of mapOptions the application could do this operation
273
         * on every render. We should prevent it with something like isEqual if this becomes
274
         * a performance problem
275
         */
276
        if (this.map && (this.props.mapOptions && this.props.mapOptions.interactions) !== (newProps.mapOptions && newProps.mapOptions.interactions)) {
39✔
277
            const newInteractions = newProps.mapOptions.interactions || {};
6!
278
            const mapInteractions = this.map.getInteractions().getArray();
6✔
279
            Object.keys(newInteractions).forEach(newInteraction => {
6✔
280
                const {Instance, options} = DEFAULT_INTERACTION_OPTIONS[newInteraction] || {};
11!
281
                let interaction = find(mapInteractions, inter => DEFAULT_INTERACTION_OPTIONS[newInteraction] && inter instanceof Instance);
30✔
282
                if (!interaction) {
11✔
283
                    /* if the interaction
284
                     *   does not exist in the map && now is enabled
285
                     * then
286
                     *   add it
287
                    */
288
                    newInteractions[newInteraction] && Instance && this.map.addInteraction(new Instance(options));
8✔
289
                } else {
290
                    // otherwise use existing interaction and enable or disable it based on newProps values
291
                    interaction.setActive(newInteractions[newInteraction]);
3✔
292
                }
293
            });
294
        }
295

296
        if (this.map && this.props.id !== newProps.mapStateSource) {
39✔
297
            this._updateMapPositionFromNewProps(newProps);
36✔
298
        }
299

300
        if (this.map && newProps.resize !== this.props.resize) {
39!
UNCOV
301
            setTimeout(() => {
×
UNCOV
302
                this.map.updateSize();
×
303
            }, 0);
304
        }
305

306
        if (this.map && ((this.props.projection !== newProps.projection) || this.haveResolutionsChanged(newProps)) || this.haveRotationChanged(newProps) || this.props.limits !== newProps.limits) {
39✔
307
            if (this.props.projection !== newProps.projection || this.props.limits !== newProps.limits || this.haveRotationChanged(newProps)) {
3✔
308
                let mapProjection = newProps.projection;
2✔
309
                const center = reproject([
2✔
310
                    newProps.center.x,
311
                    newProps.center.y
312
                ], 'EPSG:4326', mapProjection);
313
                this.map.setView(this.createView(center, newProps.zoom, newProps.projection, newProps.mapOptions && newProps.mapOptions.view, newProps.limits));
2✔
314
                this.props.onResolutionsChange(this.getResolutions());
2✔
315
            }
316
            // We have to force ol to drop tile and reload
317
            this.map.getLayers().forEach((l) => {
3✔
318
                let source = l.getSource();
×
319
                if (source.getTileLoadFunction) {
×
320
                    source.setTileLoadFunction(source.getTileLoadFunction());
×
321
                }
322

323
            });
324

325
            this.map.render();
3✔
326
        }
327
    }
328

329
    componentWillUnmount() {
330
        const attributionContainer = this.props.mapOptions.attribution && this.props.mapOptions.attribution.container
130✔
331
            && this.getDocument().querySelector(this.props.mapOptions.attribution.container);
332
        if (attributionContainer && attributionContainer.querySelector('.ol-attribution')) {
130✔
333
            try {
8✔
334
                attributionContainer.removeChild(attributionContainer.querySelector('.ol-attribution'));
8✔
335
            } catch (e) {
336
                // do nothing... probably an old configuration
337
            }
338

339
        }
340
        if (this.map) {
130!
341
            this.map.setTarget(null);
130✔
342
        }
343
    }
344
    getDocument = () => {
132✔
345
        return this.props.document || document;
150✔
346
    };
347
    /**
348
     * Calculates resolutions accordingly with default algorithm in GeoWebCache.
349
     * See this: https://github.com/GeoWebCache/geowebcache/blob/5e913193ff50a61ef9dd63a87887189352fa6b21/geowebcache/core/src/main/java/org/geowebcache/grid/GridSetFactory.java#L196
350
     * It allows to have the resolutions aligned to the default generated grid sets on server side.
351
     * **NOTES**: this solution doesn't support:
352
     * - custom grid sets with `alignTopLeft=true` (e.g. GlobalCRS84Pixel). Custom resolutions will need to be configured as `mapOptions.view.resolutions`
353
     * - custom grid set with custom extent. You need to customize the projection definition extent to make it work.
354
     * - custom grid set is partially supported by mapOptions.view.resolutions but this is not managed by projection change yet
355
     * - custom tile sizes
356
     *
357
     */
358
    getResolutions = (srs) => {
132✔
359
        if (this.props.mapOptions && this.props.mapOptions.view && this.props.mapOptions.view.resolutions) {
342✔
360
            return this.props.mapOptions.view.resolutions;
26✔
361
        }
362
        const projection = srs ? getProjection(srs) : this.map.getView().getProjection();
316✔
363
        const extent = projection.getExtent();
316✔
364
        return getResolutionsForProjection(
316✔
365
            srs ?? this.map.getView().getProjection().getCode(),
625✔
366
            {
367
                minResolution: this.props.mapOptions.minResolution,
368
                maxResolution: this.props.mapOptions.maxResolution,
369
                minZoom: this.props.mapOptions.minZoom,
370
                maxZoom: this.props.mapOptions.maxZoom,
371
                zoomFactor: this.props.mapOptions.zoomFactor,
372
                extent
373
            }
374
        );
375
    };
376

377
    getExtent = (map, props) => {
132✔
378
        const view = map.getView();
148✔
379
        return view.getProjection().getExtent() || msGetProjection(props.projection).extent;
148!
380
    };
381

382
    getIntersectedFeatures = (map, pixel) => {
132✔
383
        let groupIntersectedFeatures = {};
27✔
384
        map.forEachFeatureAtPixel(pixel, (feature, layer) => {
27✔
385
            if (layer?.get('msId')) {
11✔
386
                const geoJSONFeature = geoJSONFormat.writeFeatureObject(feature, {
4✔
387
                    featureProjection: this.props.projection,
388
                    dataProjection: 'EPSG:4326'
389
                });
390
                groupIntersectedFeatures[layer.get('msId')] = groupIntersectedFeatures[layer.get('msId')]
4!
391
                    ? [ ...groupIntersectedFeatures[layer.get('msId')], geoJSONFeature ]
392
                    : [ geoJSONFeature ];
393
            }
394
        });
395
        const intersectedFeatures = Object.keys(groupIntersectedFeatures).map(id => ({ id, features: groupIntersectedFeatures[id] }));
27✔
396
        return intersectedFeatures;
27✔
397
    };
398
    getElevation(pos, pixel) {
399
        const elevationLayers = this.map.get('msElevationLayers') || [];
27✔
400
        return elevationLayers?.[0]?.get('getElevation')
27✔
401
            ? elevationLayers[0].get('getElevation')(pos, pixel)
402
            : undefined;
403
    }
404
    render() {
405
        const map = this.map;
286✔
406
        const children = map ? React.Children.map(this.props.children, child => {
286✔
407
            return child ? React.cloneElement(child, {
165✔
408
                map: map,
409
                mapId: this.props.id,
410
                onLayerLoading: this.props.onLayerLoading,
411
                onLayerError: this.props.onLayerError,
412
                onLayerLoad: this.props.onLayerLoad,
413
                projection: this.props.projection,
414
                onCreationError: this.props.onCreationError,
415
                resolutions: this.getResolutions()
416
            }) : null;
417
        }) : null;
418

419
        return (
286✔
420
            <div id={this.props.id} style={this.props.style}>
421
                {children}
422
            </div>
423
        );
424
    }
425

426
    mouseMoveEvent = (event) => {
132✔
427
        if (!event.dragging && event.coordinate) {
38!
428
            let pos = event.coordinate.slice();
21✔
429
            let coords = toLonLat(pos, this.props.projection);
21✔
430
            let tLng = coords[0] / 360 % 1 * 360;
21✔
431
            if (tLng < -180) {
21!
432
                tLng = tLng + 360;
×
433
            } else if (tLng > 180) {
21!
434
                tLng = tLng - 360;
×
435
            }
436
            const intersectedFeatures = this.getIntersectedFeatures(this.map, event?.pixel);
21✔
437
            const elevation = this.getElevation(pos, event.pixel);
21✔
438
            this.props.onMouseMove({
21✔
439
                y: coords[1] || 0.0,
21!
440
                x: tLng || 0.0,
21!
441
                z: elevation,
442
                crs: "EPSG:4326",
443
                pixel: {
444
                    x: event.pixel[0],
445
                    y: event.pixel[1]
446
                },
447
                latlng: {
448
                    lat: coords[1],
449
                    lng: tLng,
450
                    z: elevation
451
                },
452
                lat: coords[1],
453
                lng: tLng,
454
                rawPos: event.coordinate.slice(),
455
                intersectedFeatures
456
            });
457
        }
458
    };
459

460
    updateMapInfoState = () => {
132✔
461
        let view = this.map.getView();
142✔
462
        let tempCenter = view.getCenter();
142✔
463
        let projectionExtent = this.getExtent(this.map, this.props);
142✔
464
        const crs = view.getProjection().getCode();
142✔
465
        // some projections are repeated on the x axis
466
        // and they need to be updated also if the center is outside of the projection extent
467
        const wrappedProjections = ['EPSG:3857', 'EPSG:900913', 'EPSG:4326'];
142✔
468
        // prevent user from dragging outside the projection extent
469
        if (wrappedProjections.indexOf(crs) !== -1
142✔
470
            || (tempCenter && tempCenter[0] >= projectionExtent[0] && tempCenter[0] <= projectionExtent[2] &&
471
                tempCenter[1] >= projectionExtent[1] && tempCenter[1] <= projectionExtent[3])) {
472
            let c = this.normalizeCenter(view.getCenter());
140✔
473
            let bbox = view.calculateExtent(this.map.getSize());
140✔
474
            let size = {
140✔
475
                width: this.map.getSize()[0],
476
                height: this.map.getSize()[1]
477
            };
478
            this.props.onMapViewChanges(
140✔
479
                {
480
                    x: c[0] || 0.0, y: c[1] || 0.0,
384✔
481
                    crs: 'EPSG:4326'
482
                },
483
                view.getZoom(),
484
                {
485
                    bounds: {
486
                        minx: bbox[0],
487
                        miny: bbox[1],
488
                        maxx: bbox[2],
489
                        maxy: bbox[3]
490
                    },
491
                    crs,
492
                    rotation: view.getRotation()
493
                },
494
                size,
495
                this.props.id,
496
                this.props.projection,
497
                undefined, // viewerOptions,
498
                view.getResolution() // resolution
499
            );
500
        }
501
    };
502

503
    haveResolutionsChanged = (newProps) => {
132✔
504
        const resolutions = this.props.mapOptions && this.props.mapOptions.view ? this.props.mapOptions.view.resolutions : undefined;
62✔
505
        const newResolutions = newProps.mapOptions && newProps.mapOptions.view ? newProps.mapOptions.view.resolutions : undefined;
62✔
506
        return !isEqual(resolutions, newResolutions);
62✔
507
    };
508

509
    haveRotationChanged = (newProps) => {
132✔
510
        const rotation = this.props.mapOptions && this.props.mapOptions.view ? this.props.mapOptions.view.rotation : undefined;
63✔
511
        const newRotation = newProps.mapOptions && newProps.mapOptions.view ? newProps.mapOptions.view.rotation : undefined;
63✔
512
        return !isEqual(rotation, newRotation);
63✔
513
    };
514

515
    createView = (center, zoom, projection, options, limits = {}) => {
132✔
516
        // limit has a crs defined
517
        const extent = limits.restrictedExtent && limits.crs && reprojectBbox(limits.restrictedExtent, limits.crs, normalizeSRS(projection));
142!
518
        const newOptions = !options || (options && !options.view) ? assign({}, options, { extent }) : assign({}, options);
142!
519
        /*
520
        * setting the zoom level in the localConfig file is co-related to the projection extent(size)
521
        * it is recommended to use projections with the same coverage area (extent). If you want to have the same restricted zoom level (minZoom)
522
        */
523
        const viewOptions = assign({}, {
142✔
524
            projection: normalizeSRS(projection),
525
            center: [center.x, center.y],
526
            zoom: zoom,
527
            minZoom: limits.minZoom,
528
            // allow to zoom to level 0 and see world wrapping
529
            multiWorld: true,
530
            // does not allow intermediary zoom levels
531
            // we need this at true to set correctly the scale box
532
            constrainResolution: true
533
        }, newOptions || {});
142!
534
        return new View(viewOptions);
142✔
535
    };
536

537
    isNearlyEqual = (a, b) => {
132✔
538
        /**
539
         * this implementation will update the map only if the movement
540
         * between 8 decimals (coordinate precision in mm) in the reference system
541
         * to avoid rounded value changes due to float mathematic operations or transformed value
542
        */
543
        if (a === undefined || b === undefined) {
69!
544
            return false;
×
545
        }
546
        return a.toFixed(8) - b.toFixed(8) <= 0.00000001;
69✔
547
    };
548

549
    _updateMapPositionFromNewProps = (newProps) => {
132✔
550
        var view = this.map.getView();
36✔
551
        const currentCenter = this.props.center;
36✔
552
        const centerIsUpdated = this.isNearlyEqual(newProps.center.y, currentCenter.y) &&
36✔
553
            this.isNearlyEqual(newProps.center.x, currentCenter.x);
554

555
        if (!centerIsUpdated) {
36✔
556
            // let center = ol.proj.transform([newProps.center.x, newProps.center.y], 'EPSG:4326', newProps.projection);
557
            let center = reproject({ x: newProps.center.x, y: newProps.center.y }, 'EPSG:4326', newProps.projection, true);
3✔
558
            view.setCenter([center.x, center.y]);
3✔
559
        }
560
        if (Math.round(newProps.zoom) !== this.props.zoom) {
36✔
561
            view.setZoom(Math.round(newProps.zoom));
10✔
562
        }
563
        if (newProps.bbox && newProps.bbox.rotation !== undefined || this.bbox && this.bbox.rotation !== undefined && newProps.bbox.rotation !== this.props.bbox.rotation) {
36!
564
            view.setRotation(newProps.bbox.rotation);
×
565
        }
566
    };
567

568
    normalizeCenter = (center) => {
132✔
569
        let c = reproject({ x: center[0], y: center[1] }, this.props.projection, 'EPSG:4326', true);
141✔
570
        return [c.x, c.y];
141✔
571
    };
572

573
    setMousePointer = (pointer) => {
132✔
574
        if (this.map) {
132!
575
            const mapDiv = this.map.getViewport();
132✔
576
            mapDiv.style.cursor = pointer || 'auto';
132✔
577
        }
578
    };
579

580
    zoomToExtentHandler = (extent, { padding, crs, maxZoom: zoomLevel, duration, nearest} = {})=> {
132!
581
        let bounds = reprojectBbox(extent, crs, this.props.projection);
4✔
582
        // TODO: improve this to manage all degenerated bounding boxes.
583
        if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] &&
4!
584
        crs === "EPSG:4326" && isArray(extent) && extent[0] === -180 && extent[1] === -90) {
585
            bounds = this.map.getView().getProjection().getExtent();
×
586
        }
587
        let maxZoom = zoomLevel;
4✔
588
        if (bounds && bounds[0] === bounds[2] && bounds[1] === bounds[3] && isNil(maxZoom)) {
4✔
589
            maxZoom = 21; // TODO: allow to this maxZoom to be customizable
1✔
590
        }
591
        this.map.getView().fit(bounds, {
4✔
592
            size: this.map.getSize(),
593
            padding: padding && [padding.top || 0, padding.right || 0, padding.bottom || 0, padding.left || 0],
9!
594
            maxZoom,
595
            duration,
596
            nearest
597
        });
598
    }
599

600
    registerHooks = () => {
132✔
601
        this.props.hookRegister.registerHook(mapUtils.RESOLUTIONS_HOOK, (srs) => {
114✔
602
            return this.getResolutions(srs);
60✔
603
        });
604
        this.props.hookRegister.registerHook(mapUtils.RESOLUTION_HOOK, () => {
114✔
605
            return this.map.getView().getResolution();
1✔
606
        });
607
        this.props.hookRegister.registerHook(mapUtils.COMPUTE_BBOX_HOOK, (center, zoom) => {
114✔
608
            var olCenter = reproject([center.x, center.y], 'EPSG:4326', this.props.projection);
8✔
609
            let view = this.createView(olCenter, zoom, this.props.projection, this.props.mapOptions && this.props.mapOptions.view, this.props.limits);
8✔
610
            let size = this.map.getSize();
8✔
611
            let bbox = view.calculateExtent(size);
8✔
612
            return {
8✔
613
                bounds: {
614
                    minx: bbox[0],
615
                    miny: bbox[1],
616
                    maxx: bbox[2],
617
                    maxy: bbox[3]
618
                },
619
                crs: this.props.projection,
620
                rotation: this.map.getView().getRotation()
621
            };
622
        });
623
        this.props.hookRegister.registerHook(mapUtils.GET_PIXEL_FROM_COORDINATES_HOOK, (pos) => {
114✔
624
            return this.map.getPixelFromCoordinate(pos);
×
625
        });
626
        this.props.hookRegister.registerHook(mapUtils.GET_COORDINATES_FROM_PIXEL_HOOK, (pixel) => {
114✔
627
            return this.map.getCoordinateFromPixel(pixel);
1✔
628
        });
629
        this.props.hookRegister.registerHook(mapUtils.ZOOM_TO_EXTENT_HOOK, this.zoomToExtentHandler);
114✔
630
    };
631
}
632

633
export default OpenlayersMap;
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