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

geosolutions-it / MapStore2 / 15422327504

03 Jun 2025 04:08PM UTC coverage: 76.952% (-0.04%) from 76.993%
15422327504

Pull #11024

github

web-flow
Merge 2ddc9a6d7 into 2dbe8dab2
Pull Request #11024: Update User Guide - Upload image on Text Widget

31021 of 48282 branches covered (64.25%)

38629 of 50199 relevant lines covered (76.95%)

36.23 hits per line

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

93.17
/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 = [{
134✔
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 => {
134✔
107
            const projDef = proj4.defs(p.code);
139✔
108
            projUtils.addProjections(p.code, p.extent, p.worldExtent, p.axisOrientation || projDef.axis || 'enu', projDef.units || 'm');
139!
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);
134✔
113
        register(proj4);
134✔
114
        // interactive flag is used only for initializations,
115
        // TODO manage it also when it changes status (ComponentWillReceiveProps)
116
        let interactionsOptions = assign(
134✔
117
            this.props.interactive ?
134✔
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({
134✔
132
            dragPan: false,
133
            mouseWheelZoom: false
134
        }, interactionsOptions, {}));
135
        if (interactionsOptions === undefined || interactionsOptions.dragPan === undefined) {
134✔
136
            this.dragPanInteraction = new DragPan({ kinetic: false });
105✔
137
            interactions.extend([
105✔
138
                this.dragPanInteraction
139
            ]);
140
        }
141
        if (interactionsOptions === undefined || interactionsOptions.mouseWheelZoom === undefined) {
134✔
142
            this.mouseWheelInteraction = new MouseWheelZoom({ duration: 0 });
104✔
143
            interactions.extend([
104✔
144
                this.mouseWheelInteraction
145
            ]);
146
        }
147
        let controls = defaultControls(assign({
134✔
148
            zoom: this.props.zoomControl,
149
            attributionOptions: assign({
150
                collapsible: false
151
            }, this.props.mapOptions.attribution && this.props.mapOptions.attribution.container ? {
277✔
152
                target: this.getDocument().querySelector(this.props.mapOptions.attribution.container)
153
            } : {})
154
        }, this.props.mapOptions.controls));
155

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

165
        this.map = map;
134✔
166
        if (this.props.registerHooks) {
134✔
167
            this.registerHooks();
114✔
168
        }
169
        this.map.disabledListeners = {};
134✔
170
        this.map.disableEventListener = (event) => {
134✔
171
            this.map.disabledListeners[event] = true;
2✔
172
        };
173
        this.map.enableEventListener = (event) => {
134✔
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', () => {
134✔
178
            setTimeout(() => this.props.onMouseOut(), 150);
×
179
        });
180
        // TODO support disableEventListener
181
        map.on('moveend', this.updateMapInfoState);
134✔
182
        map.on('singleclick', (event) => {
134✔
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);
134✔
247
        // TODO support disableEventListener
248
        map.on('pointermove', mouseMove);
134✔
249
        this.updateMapInfoState();
134✔
250
        this.setMousePointer(this.props.mousePointer);
134✔
251
        // NOTE: this re-call render function after div creation to have the map initialized.
252
        this.forceUpdate();
134✔
253

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

257
    UNSAFE_componentWillReceiveProps(newProps) {
258
        if (newProps.mousePointer !== this.props.mousePointer) {
43!
259
            this.setMousePointer(newProps.mousePointer);
×
260
        }
261
        if (newProps.zoomControl !== this.props.zoomControl) {
43!
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)) {
43✔
277
            const newInteractions = newProps.mapOptions.interactions || {};
8!
278
            const mapInteractions = this.map.getInteractions().getArray();
8✔
279
            Object.keys(newInteractions).forEach(newInteraction => {
8✔
280
                const {Instance, options} = DEFAULT_INTERACTION_OPTIONS[newInteraction] || {};
15!
281
                let interaction = find(mapInteractions, inter => DEFAULT_INTERACTION_OPTIONS[newInteraction] && inter instanceof Instance);
41✔
282
                if (!interaction) {
15✔
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));
11✔
289
                } else {
290
                    // otherwise use existing interaction and enable or disable it based on newProps values
291
                    interaction.setActive(newInteractions[newInteraction]);
4✔
292
                }
293
            });
294
        }
295

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

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

306
        if (this.map && ((this.props.projection !== newProps.projection) || this.haveResolutionsChanged(newProps)) || this.haveRotationChanged(newProps) || this.props.limits !== newProps.limits) {
43✔
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
132✔
331
            && this.getDocument().querySelector(this.props.mapOptions.attribution.container);
332
        if (attributionContainer && attributionContainer.querySelector('.ol-attribution')) {
132✔
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) {
132!
341
            this.map.setTarget(null);
132✔
342
        }
343
    }
344
    getDocument = () => {
134✔
345
        return this.props.document || document;
152✔
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) => {
134✔
359
        if (this.props.mapOptions && this.props.mapOptions.view && this.props.mapOptions.view.resolutions) {
499✔
360
            return this.props.mapOptions.view.resolutions;
43✔
361
        }
362
        const projection = srs ? getProjection(srs) : this.map.getView().getProjection();
456✔
363
        const extent = projection.getExtent();
456✔
364
        return getResolutionsForProjection(
456✔
365
            srs ?? this.map.getView().getProjection().getCode(),
778✔
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) => {
134✔
378
        const view = map.getView();
150✔
379
        return view.getProjection().getExtent() || msGetProjection(props.projection).extent;
150!
380
    };
381

382
    getIntersectedFeatures = (map, pixel) => {
134✔
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;
292✔
406
        const children = map ? React.Children.map(this.props.children, child => {
292✔
407
            return child ? React.cloneElement(child, {
178✔
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 (
292✔
420
            <div id={this.props.id} style={this.props.style}>
421
                {children}
422
            </div>
423
        );
424
    }
425

426
    mouseMoveEvent = (event) => {
134✔
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 = () => {
134✔
461
        let view = this.map.getView();
144✔
462
        let tempCenter = view.getCenter();
144✔
463
        let projectionExtent = this.getExtent(this.map, this.props);
144✔
464
        const crs = view.getProjection().getCode();
144✔
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'];
144✔
468
        // prevent user from dragging outside the projection extent
469
        if (wrappedProjections.indexOf(crs) !== -1
144✔
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());
142✔
473
            let bbox = view.calculateExtent(this.map.getSize());
142✔
474
            let size = {
142✔
475
                width: this.map.getSize()[0],
476
                height: this.map.getSize()[1]
477
            };
478
            this.props.onMapViewChanges(
142✔
479
                {
480
                    x: c[0] || 0.0, y: c[1] || 0.0,
392✔
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) => {
134✔
504
        const resolutions = this.props.mapOptions && this.props.mapOptions.view ? this.props.mapOptions.view.resolutions : undefined;
66✔
505
        const newResolutions = newProps.mapOptions && newProps.mapOptions.view ? newProps.mapOptions.view.resolutions : undefined;
66✔
506
        return !isEqual(resolutions, newResolutions);
66✔
507
    };
508

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

515
    createView = (center, zoom, projection, options, limits = {}) => {
134✔
516
        // limit has a crs defined
517
        const extent = limits.restrictedExtent && limits.crs && reprojectBbox(limits.restrictedExtent, limits.crs, normalizeSRS(projection));
144!
518
        const newOptions = !options || (options && !options.view) ? assign({}, options, { extent }) : assign({}, options);
144!
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({}, {
144✔
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
            resolutions: this.getResolutions(normalizeSRS(projection))
534
        }, newOptions || {});
144!
535
        return new View(viewOptions);
144✔
536
    };
537

538
    isNearlyEqual = (a, b) => {
134✔
539
        /**
540
         * this implementation will update the map only if the movement
541
         * between 8 decimals (coordinate precision in mm) in the reference system
542
         * to avoid rounded value changes due to float mathematic operations or transformed value
543
        */
544
        if (a === undefined || b === undefined) {
73!
545
            return false;
×
546
        }
547
        // using abs because the difference can be negative, creating a false positive
548
        return Math.abs(a.toFixed(8) - b.toFixed(8)) <= 0.00000001;
73✔
549
    };
550

551
    _updateMapPositionFromNewProps = (newProps) => {
134✔
552
        var view = this.map.getView();
38✔
553
        const currentCenter = this.props.center;
38✔
554
        const centerIsUpdated = this.isNearlyEqual(newProps.center.y, currentCenter.y) &&
38✔
555
            this.isNearlyEqual(newProps.center.x, currentCenter.x);
556

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

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

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

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

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

634
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