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

geosolutions-it / MapStore2 / 5332643255

pending completion
5332643255

push

github

web-flow
updated CHANGELOG.md (#9243)

27535 of 41912 branches covered (65.7%)

34275 of 43648 relevant lines covered (78.53%)

43.1 hits per line

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

76.13
/web/client/components/map/leaflet/Map.jsx
1
/**
2
 * Copyright 2015, 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
import L from 'leaflet';
9
import PropTypes from 'prop-types';
10
import React from 'react';
11
import ConfigUtils from '../../../utils/ConfigUtils';
12
import {reprojectBbox, reproject} from '../../../utils/CoordinatesUtils';
13
import assign from 'object-assign';
14
import {
15
    getGoogleMercatorResolutions,
16
    EXTENT_TO_ZOOM_HOOK,
17
    RESOLUTIONS_HOOK,
18
    COMPUTE_BBOX_HOOK,
19
    GET_PIXEL_FROM_COORDINATES_HOOK,
20
    GET_COORDINATES_FROM_PIXEL_HOOK,
21
    ZOOM_TO_EXTENT_HOOK,
22
    registerHook
23
} from '../../../utils/MapUtils';
24
import Rx from 'rxjs';
25

26
import {throttle} from 'lodash';
27
import 'leaflet/dist/leaflet.css';
28

29
import './SingleClick';
30

31
class LeafletMap extends React.Component {
32
    static propTypes = {
1✔
33
        id: PropTypes.string,
34
        document: PropTypes.object,
35
        center: ConfigUtils.PropTypes.center,
36
        zoom: PropTypes.number.isRequired,
37
        viewerOptions: PropTypes.object,
38
        mapStateSource: ConfigUtils.PropTypes.mapStateSource,
39
        style: PropTypes.object,
40
        projection: PropTypes.string,
41
        onMapViewChanges: PropTypes.func,
42
        onClick: PropTypes.func,
43
        onRightClick: PropTypes.func,
44
        mapOptions: PropTypes.object,
45
        limits: PropTypes.object,
46
        zoomControl: PropTypes.bool,
47
        mousePointer: PropTypes.string,
48
        onMouseMove: PropTypes.func,
49
        onLayerLoading: PropTypes.func,
50
        onLayerLoad: PropTypes.func,
51
        onLayerError: PropTypes.func,
52
        resize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
53
        measurement: PropTypes.object,
54
        changeMeasurementState: PropTypes.func,
55
        registerHooks: PropTypes.bool,
56
        interactive: PropTypes.bool,
57
        resolutions: PropTypes.array,
58
        hookRegister: PropTypes.object,
59
        onCreationError: PropTypes.func,
60
        onMouseOut: PropTypes.func,
61
        onResolutionsChange: PropTypes.func
62
    };
63

64
    static defaultProps = {
1✔
65
        id: 'map',
66
        onMapViewChanges: () => {},
67
        onCreationError: () => {},
68
        onClick: null,
69
        onMouseMove: () => {},
70
        zoomControl: true,
71
        mapOptions: {
72
            zoomAnimation: true,
73
            attributionControl: false
74
        },
75
        projection: "EPSG:3857",
76
        center: {x: 13, y: 45, crs: "EPSG:4326"},
77
        zoom: 5,
78
        onLayerLoading: () => {},
79
        onLayerLoad: () => {},
80
        onLayerError: () => {},
81
        resize: 0,
82
        registerHooks: true,
83
        hookRegister: {
84
            registerHook
85
        },
86
        style: {},
87
        interactive: true,
88
        resolutions: getGoogleMercatorResolutions(0, 23),
89
        onMouseOut: () => {},
90
        onResolutionsChange: () => {}
91
    };
92

93
    state = { };
67✔
94

95
    UNSAFE_componentWillMount() {
96
        this.zoomOffset = 0;
67✔
97
        if (this.props.mapOptions && this.props.mapOptions.view && this.props.mapOptions.view.resolutions && this.props.mapOptions.view.resolutions.length > 0) {
67!
98
            const scaleFun = L.CRS.EPSG3857.scale;
×
99
            const ratio = this.props.mapOptions.view.resolutions[0] / getGoogleMercatorResolutions(0, 23)[0];
×
100
            this.crs = assign({}, L.CRS.EPSG3857, {
×
101
                scale: (zoom) => {
102
                    return scaleFun.call(L.CRS.EPSG3857, zoom) / Math.pow(2, Math.round(Math.log2(ratio)));
×
103
                }
104
            });
105
            this.zoomOffset = Math.round(Math.log2(ratio));
×
106
        }
107
    }
108
    componentDidMount() {
109
        const {limits = {}} = this.props;
67✔
110
        const maxBounds = limits.restrictedExtent && limits.crs && reprojectBbox(limits.restrictedExtent, limits.crs, "EPSG:4326");
67!
111
        let mapOptions = assign({}, this.props.interactive ? {} : {
67✔
112
            dragging: false,
113
            touchZoom: false,
114
            scrollWheelZoom: false,
115
            doubleClickZoom: false,
116
            boxZoom: false,
117
            tap: false,
118
            attributionControl: false,
119
            maxBounds: maxBounds && L.latLngBounds([
12!
120
                [maxBounds[1], maxBounds[0]],
121
                [maxBounds[3], maxBounds[2]]
122
            ]),
123
            maxBoundsViscosity: maxBounds && 1.0,
12!
124
            minZoom: limits && limits.minZoom,
24✔
125
            maxZoom: limits && limits.maxZoom || 23
36✔
126
        }, this.props.mapOptions, this.crs ? {crs: this.crs} : {});
67!
127
        // it is not possible to use #<id> in a query selector if the id starts with a number
128
        const map = L.map(this.getDocument().querySelector(`[id='${this.props.id}'] > .map-viewport`), assign({ zoomControl: false }, mapOptions) ).setView([this.props.center.y, this.props.center.x],
67✔
129
            Math.round(this.props.zoom));
130

131
        this.map = map;
67✔
132
        if (this.props.registerHooks) {
67✔
133
            this.registerHooks();
62✔
134
        }
135
        // store zoomControl in the class to target the right control while add/remove
136
        if (this.props.zoomControl) {
67✔
137
            this.mapZoomControl = L.control.zoom();
51✔
138
            this.map.addControl(this.mapZoomControl);
51✔
139
        }
140

141
        this.attribution = L.control.attribution();
67✔
142
        this.attribution.addTo(this.map);
67✔
143
        const mapDocument = this.getDocument();
67✔
144
        if (this.props.mapOptions.attribution && this.props.mapOptions.attribution.container) {
67✔
145
            mapDocument.querySelector(this.props.mapOptions.attribution.container).appendChild(this.attribution.getContainer());
2✔
146
            if (mapDocument.querySelector('.leaflet-control-container .leaflet-control-attribution')) {
2!
147
                mapDocument.querySelector('.leaflet-control-container .leaflet-control-attribution').parentNode.removeChild(mapDocument.querySelector('.leaflet-control-container .leaflet-control-attribution'));
2✔
148
            }
149
        }
150

151
        this.map.on('moveend', this.updateMapInfoState);
67✔
152
        // this uses the hook defined in ./SingleClick.js for leaflet 0.7.*
153
        this.map.on('singleclick', (event) => {
67✔
154
            if (this.props.onClick) {
3!
155
                const intersectedFeatures = this.getIntersectedFeatures(map, event.latlng);
3✔
156
                this.props.onClick({
3✔
157
                    pixel: {
158
                        x: event.containerPoint.x,
159
                        y: event.containerPoint.y
160
                    },
161
                    latlng: {
162
                        lat: event.latlng.lat,
163
                        lng: event.latlng.lng,
164
                        z: this.elevationLayer && this.elevationLayer.getElevation(event.latlng, event.containerPoint) || undefined
6✔
165
                    },
166
                    rawPos: [event.latlng.lat, event.latlng.lng],
167
                    modifiers: {
168
                        alt: event.originalEvent.altKey,
169
                        ctrl: event.originalEvent.ctrlKey,
170
                        metaKey: event.originalEvent.metaKey, // MAC OS
171
                        shift: event.originalEvent.shiftKey
172
                    },
173
                    intersectedFeatures
174
                });
175
            }
176
        });
177
        const mouseMove = throttle(this.mouseMoveEvent, 100);
67✔
178
        this.map.on('dragstart', () => { this.map.off('mousemove', mouseMove); });
67✔
179
        this.map.on('dragend', () => { this.map.on('mousemove', mouseMove); });
67✔
180
        this.map.on('mousemove', mouseMove);
67✔
181
        this.map.on('contextmenu', () => {
67✔
182
            if (this.props.onRightClick) {
×
183
                this.props.onRightClick(event.containerPoint);
×
184
            }
185
        });
186
        // The timeout is needed to cover the delay we have for the throttled mouseMove event.
187
        this.map.on('mouseout', () => {
67✔
188
            setTimeout(() => this.props.onMouseOut(), 150);
×
189
        });
190

191
        this.updateMapInfoState();
67✔
192
        this.setMousePointer(this.props.mousePointer);
67✔
193
        // NOTE: this re-call render function after div creation to have the map initialized.
194
        this.forceUpdate();
67✔
195
        this.props.onResolutionsChange(this.getResolutions());
67✔
196
        this.map.on('layeradd', (event) => {
67✔
197
            // we want to run init code only the first time a layer is added to the map
198
            if (event.layer._ms2Added) {
73!
199
                const isStopped = event.layer.layerLoadingStream$ && event.layer.layerLoadingStream$.isStopped;
×
200
                this.addLayerObservable(event, isStopped);
×
201
                return;
×
202
            }
203
            event.layer._ms2Added = true;
73✔
204
            if (event.layer.getElevation) {
73✔
205
                this.elevationLayer = event.layer;
3✔
206
            }
207

208
            // avoid binding if not possible, e.g. for measurement vector layers
209
            if (!event.layer.layerId) {
73✔
210
                return;
19✔
211
            }
212
            if (event.layer && event.layer.options && event.layer.options.msLayer === 'vector') {
54!
213
                return;
×
214
            }
215

216
            if (event && event.layer && event.layer.on ) {
54!
217
                // TODO check event.layer.on is a function
218
                // Needed to fix GeoJSON Layer neverending loading
219

220
                this.addLayerObservable(event, true);
54✔
221

222
                if (!(event.layer.options && event.layer.options.hideLoading)) {
54✔
223
                    this.props.onLayerLoading(event.layer.layerId);
52✔
224
                    event.layer.layerLoadingStream$.next();
52✔
225
                }
226

227
                event.layer.on('loading', (loadingEvent) => {
54✔
228
                    this.props.onLayerLoading(loadingEvent.target.layerId);
×
229
                    event.layer.layerLoadingStream$.next();
×
230
                });
231

232
                event.layer.on('load', (loadEvent) => {
54✔
233
                    this.props.onLayerLoad(loadEvent.target.layerId);
3✔
234
                    event.layer.layerLoadStream$.next();
3✔
235

236
                });
237

238
                event.layer.on('tileloadstart ', () => { event.layer._ms2LoadingTileCount++; });
54✔
239
                if (event.layer.options && !event.layer.options.hideErrors || !event.layer.options) {
54!
240
                    event.layer.on('tileerror', (errorEvent) => { event.layer.layerErrorStream$.next(errorEvent); });
54✔
241
                }
242
                // WFS data
243
                event.layer.on('loaderror', (error) => {
54✔
244
                    this.props.onLayerError(error.target.layerId);
×
245
                });
246
            }
247
        });
248

249
        this.map.on('layerremove', (event) => {
67✔
250
            if (event.layer.layerLoadingStream$) {
×
251
                event.layer.layerLoadingStream$.complete();
×
252
                event.layer.layerLoadStream$.complete();
×
253
                event.layer.layerErrorStream$.complete();
×
254
            }
255
        });
256

257
        this.drawControl = null;
67✔
258
    }
259

260
    UNSAFE_componentWillReceiveProps(newProps) {
261

262
        if (newProps.mousePointer !== this.props.mousePointer) {
23!
263
            this.setMousePointer(newProps.mousePointer);
×
264
        }
265
        // update the position if the map is not the source of the state change
266
        if (this.map && newProps.mapStateSource !== this.props.id) {
23✔
267
            this._updateMapPositionFromNewProps(newProps);
21✔
268
        }
269
        if (newProps.zoomControl !== this.props.zoomControl) {
23!
270
            if (newProps.zoomControl) {
×
271
                this.mapZoomControl = L.control.zoom();
×
272
                this.map.addControl(this.mapZoomControl);
×
273
            } else if (this.mapZoomControl && !newProps.zoomControl) {
×
274
                this.map.removeControl(this.mapZoomControl);
×
275
                this.mapZoomControl = undefined;
×
276
            }
277
        }
278
        if (newProps.resize !== this.props.resize) {
23✔
279
            setTimeout(() => {
2✔
280
                if (this.map) {
2!
281
                    this.map.invalidateSize(false);
×
282
                }
283
            }, 0);
284
        }
285
        // update map limits
286
        if (this.props.limits !== newProps.limits) {
23✔
287
            const {limits = {}} = newProps;
1!
288
            const {limits: oldLimits} = this.props;
1✔
289
            if (limits.restrictedExtent !== (oldLimits && oldLimits.restrictedExtent)) {
1!
290
                const maxBounds = limits.restrictedExtent && limits.crs && reprojectBbox(limits.restrictedExtent, limits.crs, "EPSG:4326");
×
291
                this.map.setMaxBounds(limits.restrictedExtent && L.latLngBounds([
×
292
                    [maxBounds[1], maxBounds[0]],
293
                    [maxBounds[3], maxBounds[2]]
294
                ]));
295
            }
296
            if (limits.minZoom !== (oldLimits && oldLimits.minZoom)) {
1!
297
                this.map.setMinZoom(limits.minZoom);
×
298
            }
299
            this.props.onResolutionsChange(this.getResolutions());
1✔
300
        }
301
        return false;
23✔
302
    }
303

304
    componentWillUnmount() {
305
        const mapDocument = this.getDocument();
67✔
306
        const attributionContainer = this.props.mapOptions.attribution && this.props.mapOptions.attribution.container && mapDocument.querySelector(this.props.mapOptions.attribution.container);
67✔
307
        if (attributionContainer && this.attribution.getContainer() && attributionContainer.querySelector('.leaflet-control-attribution')) {
67✔
308
            try {
1✔
309
                attributionContainer.removeChild(this.attribution.getContainer());
1✔
310
            } catch (e) {
311
                // do nothing... probably an old configuration
312
            }
313
        }
314

315
        if (this.mapZoomControl) {
67✔
316
            this.map.removeControl(this.mapZoomControl);
51✔
317
            this.mapZoomControl = undefined;
51✔
318
        }
319
        // remove all events
320
        this.map.off();
67✔
321
        // remove map and set to undefined for setTimeout for .invalidateSize action
322
        this.map.remove();
67✔
323
        this.map = undefined;
67✔
324
    }
325

326
    getDocument = () => {
67✔
327
        return this.props.document || document;
201✔
328
    };
329

330
    getResolutions = () => {
67✔
331
        return this.props.resolutions;
468✔
332
    };
333

334
    getIntersectedFeatures = (map, latlng) => {
67✔
335
        let groupIntersectedFeatures = {};
3✔
336
        const clickBounds = L.latLngBounds(latlng, latlng);
3✔
337
        map.eachLayer((layer) => {
3✔
338
            if (layer?.layerId && layer?.eachLayer) {
4✔
339
                layer.eachLayer(feature => {
1✔
340

341
                    const centerBounds = feature?.getLatLng
1!
342
                        ? L.latLngBounds(feature.getLatLng(), feature.getLatLng())
343
                        : null;
344
                    const bounds = feature?.getBounds
1!
345
                        ? feature.getBounds()
346
                        : centerBounds;
347

348
                    if (bounds && feature?.toGeoJSON) {
1!
349
                        if (bounds && clickBounds.intersects(bounds)) {
1!
350
                            const geoJSONFeature = feature.toGeoJSON();
1✔
351
                            groupIntersectedFeatures[layer.layerId] = groupIntersectedFeatures[layer.layerId]
1!
352
                                ? [ ...groupIntersectedFeatures[layer.layerId], geoJSONFeature ]
353
                                : [ geoJSONFeature ];
354
                        }
355
                    }
356
                });
357
            }
358
        });
359
        return Object.keys(groupIntersectedFeatures).map(id => ({ id, features: groupIntersectedFeatures[id] }));
3✔
360
    }
361

362
    render() {
363
        const map = this.map;
155✔
364
        const mapProj = this.props.projection;
155✔
365
        const children = map ? React.Children.map(this.props.children, child => {
155✔
366
            return child ? React.cloneElement(child, {
181✔
367
                map: map,
368
                projection: mapProj,
369
                zoomOffset: this.zoomOffset,
370
                onCreationError: this.props.onCreationError,
371
                onClick: this.props.onClick,
372
                resolutions: this.getResolutions(),
373
                zoom: this.props.zoom
374
            }) : null;
375
        }) : null;
376
        return (
155✔
377
            <div id={this.props.id} style={this.props.style}>
378
                <div
379
                    className="map-viewport"
380
                    style={{
381
                        position: 'relative',
382
                        overflow: 'hidden',
383
                        width: '100%',
384
                        height: '100%'
385
                    }}
386
                ></div>
387
                {children}
388
            </div>
389
        );
390
    }
391

392
    _updateMapPositionFromNewProps = (newProps) => {
67✔
393
        // current implementation will update the map only if the movement
394
        // between 12 decimals in the reference system to avoid rounded value
395
        // changes due to float mathematic operations.
396
        const isNearlyEqual = function(a, b) {
21✔
397
            if (a === undefined || b === undefined) {
43!
398
                return false;
×
399
            }
400
            return a.toFixed(12) - b.toFixed(12) === 0;
43✔
401
        };
402

403
        // getting all centers we need to check
404
        const newCenter = newProps.center;
21✔
405
        const currentCenter = this.props.center;
21✔
406
        const mapCenter = this.map.getCenter();
21✔
407
        // checking if the current props are the same
408
        const propsCentersEqual = isNearlyEqual(newCenter.x, currentCenter.x) &&
21✔
409
                                  isNearlyEqual(newCenter.y, currentCenter.y);
410
        // if props are the same nothing to do, otherwise
411
        // we need to check if the new center is equal to map center
412
        const centerIsNotUpdated = propsCentersEqual ||
21✔
413
                                   isNearlyEqual(newCenter.x, mapCenter.lng) &&
414
                                    isNearlyEqual(newCenter.y, mapCenter.lat);
415

416
        // getting all zoom values we need to check
417
        const newZoom = newProps.zoom;
21✔
418
        const currentZoom = this.props.zoom;
21✔
419
        const mapZoom = this.map.getZoom();
21✔
420
        // checking if the current props are the same
421
        const propsZoomEqual = newZoom === currentZoom;
21✔
422
        // if props are the same nothing to do, otherwise
423
        // we need to check if the new zoom is equal to map zoom
424
        const zoomIsNotUpdated = propsZoomEqual || newZoom === mapZoom;
21✔
425

426
        // do the change at the same time, to avoid glitches
427
        if (!centerIsNotUpdated && !zoomIsNotUpdated) {
21✔
428
            this.map.setView([newProps.center.y, newProps.center.x], Math.round(newProps.zoom));
4✔
429
        } else if (!zoomIsNotUpdated) {
17!
430
            this.map.setZoom(newProps.zoom);
×
431
        } else if (!centerIsNotUpdated) {
17!
432
            this.map.setView([newProps.center.y, newProps.center.x]);
×
433
        }
434
    };
435

436
    updateMapInfoState = () => {
67✔
437
        const bbox = this.map.getBounds().toBBoxString().split(',');
77✔
438
        const size = {
77✔
439
            height: this.map.getSize().y,
440
            width: this.map.getSize().x
441
        };
442
        const center = this.map.getCenter();
77✔
443
        const zoom = this.map.getZoom();
77✔
444
        const viewerOptions = this.props.viewerOptions;
77✔
445
        this.props.onMapViewChanges(
77✔
446
            {
447
                x: center.lng,
448
                y: center.lat,
449
                crs: "EPSG:4326"
450
            },
451
            zoom,
452
            {
453
                bounds: {
454
                    minx: parseFloat(bbox[0]),
455
                    miny: parseFloat(bbox[1]),
456
                    maxx: parseFloat(bbox[2]),
457
                    maxy: parseFloat(bbox[3])
458
                },
459
                crs: 'EPSG:4326',
460
                rotation: 0
461
            },
462
            size,
463
            this.props.id,
464
            this.props.projection,
465
            viewerOptions, // viewerOptions
466
            this.getResolutions()[zoom] // resolution
467
        );
468
    };
469

470
    setMousePointer = (pointer) => {
67✔
471
        if (this.map) {
67!
472
            const mapDiv = this.map.getContainer();
67✔
473
            mapDiv.style.cursor = pointer || 'auto';
67✔
474
        }
475
    };
476

477
    mouseMoveEvent = (event) => {
67✔
478
        let pos = event.latlng.wrap();
2✔
479
        this.props.onMouseMove({
2✔
480
            x: pos.lng || 0.0,
2!
481
            y: pos.lat || 0.0,
2!
482
            z: this.elevationLayer && this.elevationLayer.getElevation(pos, event.containerPoint) || undefined,
4✔
483
            crs: "EPSG:4326",
484
            pixel: {
485
                x: event.containerPoint.x,
486
                y: event.containerPoint.x
487
            },
488
            latlng: {
489
                lat: event.latlng.lat,
490
                lng: event.latlng.lng,
491
                z: this.elevationLayer && this.elevationLayer.getElevation(event.latlng, event.containerPoint) || undefined
4✔
492
            },
493
            rawPos: [event.latlng.lat, event.latlng.lng]
494
        });
495
    };
496

497
    registerHooks = () => {
67✔
498
        this.props.hookRegister.registerHook(EXTENT_TO_ZOOM_HOOK, (extent) => {
62✔
499
            var repojectedPointA = reproject([extent[0], extent[1]], this.props.projection, 'EPSG:4326');
×
500
            var repojectedPointB = reproject([extent[2], extent[3]], this.props.projection, 'EPSG:4326');
×
501
            return this.map.getBoundsZoom([[repojectedPointA.y, repojectedPointA.x], [repojectedPointB.y, repojectedPointB.x]]) - 1;
×
502
        });
503
        this.props.hookRegister.registerHook(RESOLUTIONS_HOOK, () => {
62✔
504
            return this.getResolutions();
157✔
505
        });
506
        this.props.hookRegister.registerHook(COMPUTE_BBOX_HOOK, (center, zoom) => {
62✔
507
            let latLngCenter = L.latLng([center.y, center.x]);
1✔
508
            // this call will use map internal size
509
            let topLeftPoint = this.map._getNewPixelOrigin(latLngCenter, zoom);
1✔
510
            let pixelBounds = new L.Bounds(topLeftPoint, topLeftPoint.add(this.map.getSize()));
1✔
511
            let southWest = this.map.unproject(pixelBounds.getBottomLeft(), zoom);
1✔
512
            let northEast = this.map.unproject(pixelBounds.getTopRight(), zoom);
1✔
513
            let bbox = new L.LatLngBounds(southWest, northEast).toBBoxString().split(',');
1✔
514
            return {
1✔
515
                bounds: {
516
                    minx: parseFloat(bbox[0]),
517
                    miny: parseFloat(bbox[1]),
518
                    maxx: parseFloat(bbox[2]),
519
                    maxy: parseFloat(bbox[3])
520
                },
521
                crs: 'EPSG:4326',
522
                rotation: 0
523
            };
524
        });
525
        this.props.hookRegister.registerHook(GET_PIXEL_FROM_COORDINATES_HOOK, (pos) => {
62✔
526
            let latLng = reproject(pos, this.props.projection, 'EPSG:4326');
×
527
            let pixel = this.map.latLngToContainerPoint([latLng.y, latLng.x]);
×
528
            return [pixel.x, pixel.y];
×
529
        });
530
        this.props.hookRegister.registerHook(GET_COORDINATES_FROM_PIXEL_HOOK, (pixel) => {
62✔
531
            const point = this.map.containerPointToLatLng(pixel);
×
532
            let pos = reproject([point.lng, point.lat], 'EPSG:4326', this.props.projection);
×
533
            return [pos.x, pos.y];
×
534
        });
535
        this.props.hookRegister.registerHook(ZOOM_TO_EXTENT_HOOK, (extent, { padding, crs, maxZoom, duration } = {}) => {
62!
536
            const paddingTopLeft = padding && L.point(padding.left || 0, padding.top || 0);
2!
537
            const paddingBottomRight = padding && L.point(padding.right || 0, padding.bottom || 0);
2!
538
            const bounds = reprojectBbox(extent, crs, 'EPSG:4326');
2✔
539
            // bbox is minx, miny, maxx, maxy'southwest_lng,southwest_lat,northeast_lng,northeast_lat'
540
            this.map.fitBounds([
2✔
541
                // sw
542
                [bounds[1], bounds[0]],
543
                // ne
544
                [bounds[3], bounds[2]]
545
            ],
546
            {
547
                paddingTopLeft,
548
                paddingBottomRight,
549
                maxZoom,
550
                duration,
551
                animate: duration === 0 ? false : undefined
2✔
552
            }
553
            );
554
        });
555
    };
556

557
    addLayerObservable = (event, stopped) => {
67✔
558

559
        if (!event.layer.layerId
58!
560
        || event.layer && event.layer.options && event.layer.options.msLayer === 'vector') {
561
            return;
×
562
        }
563

564
        if (event && event.layer && event.layer.on && stopped) {
58✔
565

566
            event.layer._ms2LoadingTileCount = 0;
57✔
567

568
            event.layer.layerLoadingStream$ = new Rx.Subject();
57✔
569
            event.layer.layerLoadStream$ = new Rx.Subject();
57✔
570
            event.layer.layerErrorStream$ = new Rx.Subject();
57✔
571
            event.layer.layerErrorStream$
57✔
572
                .bufferToggle(
573
                    event.layer.layerLoadingStream$,
574
                    () => event.layer.layerLoadStream$)
55✔
575
                .subscribe({
576
                    next: errorEvent => {
577
                        const loadingTileCount = event.layer._ms2LoadingTileCount || errorEvent && errorEvent.length || 0;
6✔
578
                        if (errorEvent && errorEvent.length > 0) {
6✔
579
                            this.props.onLayerError(errorEvent[0].target.layerId, loadingTileCount, errorEvent.length);
2✔
580
                        }
581
                        event.layer._ms2LoadingTileCount = 0;
6✔
582
                    }
583
                });
584
        }
585
    };
586
}
587

588
export default LeafletMap;
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