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

geosolutions-it / MapStore2 / 14534587011

18 Apr 2025 11:41AM UTC coverage: 76.977% (-0.02%) from 76.993%
14534587011

Pull #11037

github

web-flow
Merge f22d700f6 into 48d6a1a15
Pull Request #11037: Remove object assign pollyfills

30792 of 47937 branches covered (64.23%)

446 of 556 new or added lines in 94 files covered. (80.22%)

8 existing lines in 4 files now uncovered.

38277 of 49725 relevant lines covered (76.98%)

36.07 hits per line

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

65.28
/web/client/components/map/leaflet/MeasurementSupport.jsx
1
import PropTypes from 'prop-types';
2
import React from 'react';
3
import L from 'leaflet';
4
import {
5
    isNil,
6
    slice
7
} from 'lodash';
8
import {
9
    reproject,
10
    calculateAzimuth,
11
    calculateDistance,
12
    transformLineToArcs
13
} from '../../../utils/CoordinatesUtils';
14
import {
15
    convertUom,
16
    getFormattedBearingValue
17
} from '../../../utils/MeasureUtils';
18
import 'leaflet-draw';
19
import 'leaflet-draw/dist/leaflet.draw.css';
20

21
let defaultPrecision = {
1✔
22
    km: 2,
23
    ha: 2,
24
    m: 2,
25
    mi: 2,
26
    ac: 2,
27
    yd: 0,
28
    ft: 0,
29
    nm: 2,
30
    sqkm: 2,
31
    sqha: 2,
32
    sqm: 2,
33
    sqmi: 2,
34
    sqac: 2,
35
    sqyd: 2,
36
    sqft: 2,
37
    sqnm: 2
38
};
39

40

41
L.getMeasureWithTreshold = (value, threshold, source, dest, precision, sourceLabel, destLabel) => {
1✔
42
    if (value > threshold) {
12✔
43
        return L.GeometryUtil.formattedNumber(convertUom(value, source, dest), precision[dest]) + ' ' + destLabel;
6✔
44
    }
45
    return L.GeometryUtil.formattedNumber(value, precision[source]) + ' ' + sourceLabel;
6✔
46
};
47

48
/** @method readableDistance(distance, units): string
49
 * Converts a metric distance to one of [ feet, nauticalMile, metric or yards ] string
50
 *
51
 * @alternative
52
 * @method readableDistance(distance, isMetric, useFeet, isNauticalMile, precision, options): string
53
 * Converts metric distance to distance string.
54
 * The value will be rounded as defined by the precision option object.
55
 * this override is necesary due to the customization on how the measure label is presented and for adding bearing support
56
*/
57
const originalReadableDistance = L.GeometryUtil.readableDistance;
1✔
58

59

60
function readableDistance(distance, isMetric, isFeet, isNauticalMile, precision, options) {
61
    if (!options) {
9!
62
        return originalReadableDistance.apply(null, arguments);
×
63
    }
64
    if (options.geomType === 'Bearing') {
9✔
65
        return options.bearing;
3✔
66
    }
67
    const p = L.Util.extend({}, defaultPrecision, precision);
6✔
68
    const {unit, label} = options.uom.length;
6✔
69

70
    let distanceStr = L.GeometryUtil.formattedNumber(convertUom(distance, "m", unit), p[unit]) + ' ' + label;
6✔
71
    if (options.useTreshold) {
6✔
72
        if (isMetric) {
5✔
73
            distanceStr = L.getMeasureWithTreshold(distance, 1000, "m", "km", p, "m", "km");
3✔
74
        }
75
        if (unit === "mi") {
5✔
76
            distanceStr = L.getMeasureWithTreshold(convertUom(distance, "m", "yd"), 1760, "yd", "mi", p, "yd", "mi");
2✔
77
        }
78
    }
79
    return distanceStr;
6✔
80
}
81
L.GeometryUtil.readableDistance = readableDistance;
1✔
82
/** @method readableArea(area, isMetric, precision): string
83
 * @return a readable area string in yards or metric.
84
 * The value will be rounded as defined by the precision option object.
85
 * this override is necesary due to the customization on how the area measure label is presented
86
 supporting also the square nautical miles and square feets
87
 */
88

89
const originalReadableArea = L.GeometryUtil.readableArea;
1✔
90

91
function readableArea(area, isMetric, precision, options) {
92
    if (!options) {
7!
93
        return originalReadableArea.apply(null, arguments);
×
94
    }
95
    const {unit, label} = options.uom.area;
7✔
96
    const p = L.Util.extend({}, defaultPrecision, precision);
7✔
97
    let areaStr = L.GeometryUtil.formattedNumber(convertUom(area, "sqm", unit), p[unit]) + ' ' + label;
7✔
98
    if (options.useTreshold) {
7✔
99
        if (isMetric) {
5✔
100
            areaStr = L.getMeasureWithTreshold(area, 1000000, "sqm", "sqkm", p, "m²", "km²");
3✔
101
        }
102
        if (unit === "sqmi") {
5✔
103
            areaStr = L.getMeasureWithTreshold(convertUom(area, "sqm", "sqyd"), 3097600, "sqyd", "sqmi", p, "yd²", "mi²");
2✔
104
        }
105
    }
106
    return areaStr;
7✔
107
}
108
L.GeometryUtil.readableArea = readableArea;
1✔
109
/**
110
 * this is need to pass custom options as uom, useTreshold to the readableArea function
111
*/
112
const originalGetMeasurementStringPolygon = L.Draw.Polygon.prototype._getMeasurementString;
1✔
113

114
L.Draw.Polygon.prototype._getMeasurementString = function() {
1✔
115
    if (!this.options.uom) {
1!
116
        return originalGetMeasurementStringPolygon.apply(this, arguments);
×
117
    }
118
    let area = this._area;
1✔
119
    let measurementString = '';
1✔
120
    if (!area && !this.options.showLength) {
1!
121
        return null;
×
122
    }
123
    if (this.options.showLength) {
1!
124
        measurementString = L.Draw.Polyline.prototype._getMeasurementString.call(this);
×
125
    }
126

127
    if (area) {
1!
128
        // here is the custom option passed to geom util func
129
        const opt = {
1✔
130
            uom: this.options.uom,
131
            useTreshold: this.options.useTreshold
132
        };
133
        measurementString += this.options.showLength ? '<br>' : '' + L.GeometryUtil.readableArea(area, this.options.metric, this.options.precision, opt);
1!
134
    }
135
    return measurementString;
1✔
136
};
137
/**
138
 * this is need to pass custom options as uom, useTreshold, bearing to the readableDistance function
139
*/
140
const originalGetMeasurementStringPolyline = L.Draw.Polyline.prototype._getMeasurementString;
1✔
141

142
L.Draw.Polyline.prototype._getMeasurementString = function() {
1✔
143
    if (!this.options.uom) {
2!
144
        return originalGetMeasurementStringPolyline.apply(this, arguments);
×
145
    }
146
    let currentLatLng = this._currentLatLng;
2✔
147
    let previousLatLng = this._markers[this._markers.length - 1].getLatLng();
2✔
148
    let distance;
149

150
    // Calculate the distance from the last fixed point to the mouse position based on the version
151
    if (L.GeometryUtil.isVersion07x()) {
2!
152
        distance = previousLatLng && currentLatLng && currentLatLng.distanceTo ? this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0;
×
153
    } else {
154
        distance = previousLatLng && currentLatLng ? this._measurementRunningTotal + this._map.distance(currentLatLng, previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0;
2!
155
    }
156
    // here is the custom option passed to geom util func
157
    const opt = {
2✔
158
        uom: this.options.uom,
159
        useTreshold: this.options.useTreshold,
160
        geomType: this.options.geomType,
161
        bearing: this.options.bearing ? getFormattedBearingValue(this.options.bearing, this.options.trueBearing) : 0
2✔
162
    };
163
    return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic, this.options.precision, opt);
2✔
164
};
165

166
class MeasurementSupport extends React.Component {
167
    static displayName = 'MeasurementSupport';
1✔
168

169
    static propTypes = {
1✔
170
        map: PropTypes.object,
171
        metric: PropTypes.bool,
172
        feet: PropTypes.bool,
173
        nautic: PropTypes.bool,
174
        enabled: PropTypes.bool,
175
        useTreshold: PropTypes.bool,
176
        projection: PropTypes.string,
177
        measurement: PropTypes.object,
178
        changeMeasurementState: PropTypes.func,
179
        messages: PropTypes.object,
180
        uom: PropTypes.object,
181
        updateOnMouseMove: PropTypes.bool
182
    };
183

184
    static contextTypes = {
1✔
185
        messages: PropTypes.object
186
    };
187

188
    static defaultProps = {
1✔
189
        uom: {
190
            length: {unit: 'm', label: 'm'},
191
            area: {unit: 'sqm', label: 'm²'}
192
        },
193
        updateOnMouseMove: false,
194
        metric: true,
195
        nautic: false,
196
        useTreshold: false,
197
        feet: false
198
    };
199

200
    UNSAFE_componentWillMount() {
201
        if (this.props.measurement?.geomType && (this.props.measurement?.lineMeasureEnabled || this.props.measurement?.areaMeasureEnabled || this.props.measurement?.bearingMeasureEnabled) && isNil(this.drawControl) && this.props.enabled) {
8!
202
            this.addDrawInteraction(this.props);
×
203
        }
204
    }
205

206
    UNSAFE_componentWillReceiveProps(newProps) {
207
        if ((newProps && newProps.uom && newProps.uom.length && newProps.uom.length.unit) !== (this.props && this.props.uom && this.props.uom.length && this.props.uom.length.unit) && this.drawControl) {
7!
208
            const uomOptions = this.uomLengthOptions(newProps);
×
209
            this.drawControl.setOptions({...uomOptions, uom: newProps.uom});
×
210
        }
211
        if ((newProps && newProps.uom && newProps.uom.area && newProps.uom.area.unit) !== (this.props && this.props.uom && this.props.uom.area && this.props.uom.area.unit) && this.drawControl) {
7!
212
            const uomOptions = this.uomAreaOptions(newProps);
×
213
            this.drawControl.setOptions({...uomOptions, uom: newProps.uom});
×
214
        }
215
        if (newProps.measurement.geomType && newProps.measurement.geomType !== this.props.measurement.geomType ||
7!
216
            /* check also when a default is set
217
             * if so the first condition does not match
218
             * because the old geomType is not changed (it was already defined as default)
219
             * and the measure tool is getting enabled
220
            */
221
            (newProps.measurement.geomType && this.props.measurement.geomType && (newProps.measurement.lineMeasureEnabled || newProps.measurement.areaMeasureEnabled || newProps.measurement.bearingMeasureEnabled) && !this.props.enabled && newProps.enabled) ) {
222
            this.addDrawInteraction(newProps);
6✔
223
        }
224
        if (!newProps.measurement.geomType) {
7✔
225
            this.removeDrawInteraction();
1✔
226
        }
227
    }
228

229
    componentWillUnmount() {
230
        this.removeDrawInteraction();
8✔
231
    }
232

233
    onDrawStart = () => {
8✔
234
        this.props.map.off('click', this.restartDrawing, this);
6✔
235
        this.removeArcLayer();
6✔
236
        if (this.props.map.doubleClickZoom) {
6!
237
            this.props.map.doubleClickZoom.disable();
6✔
238
        }
239
        this.drawing = true;
6✔
240
    };
241

242
    onDrawCreated = (evt) => {
8✔
243
        this.drawing = false;
1✔
244
        // let drawn geom stay on the map
245
        this.props.map.addLayer(evt.layer);
1✔
246
        // preserve the currently created layer to remove it later on
247
        this.lastLayer = evt.layer;
1✔
248

249
        let feature = this.lastLayer && this.lastLayer.toGeoJSON() || {};
1!
250
        if (this.props.measurement.geomType === 'LineString') {
1!
NEW
251
            feature = Object.assign({}, feature, {
×
252
                geometry: Object.assign({}, feature.geometry, {
253
                    coordinates: transformLineToArcs(feature.geometry.coordinates)
254
                })
255
            });
256
        }
257
        if (this.props.measurement.geomType === 'Point') {
1!
258
            let pos = this.drawControl._marker.getLatLng();
1✔
259
            let point = {
1✔
260
                x: pos.lng,
261
                y: pos.lat,
262
                srs: 'EPSG:4326'
263
            };
264
            let newMeasureState = Object.assign({}, this.props.measurement, {
1✔
265
                point: point,
266
                feature
267
            });
268
            this.props.changeMeasurementState(newMeasureState);
1✔
269
        } else {
NEW
270
            let newMeasureState = Object.assign({}, this.props.measurement, {
×
271
                feature
272
            });
273
            this.props.changeMeasurementState(newMeasureState);
×
274
        }
275
        if (this.props.measurement.lineMeasureEnabled && this.lastLayer) {
1!
276
            this.addArcsToMap([feature]);
×
277
        }
278
        setTimeout(() => {
1✔
279
            this.props.map.off('click', this.restartDrawing, this);
1✔
280
            this.props.map.on('click', this.restartDrawing, this);
1✔
281
        }, 100);
282
    };
283

284
    onDrawVertex = () => {
8✔
285
        let bearingMarkers = this.drawControl._markers || [];
×
286

287
        if (this.props.measurement.geomType === 'Bearing' && bearingMarkers.length >= 2) {
×
288
            setTimeout(() => {
×
289
                this.drawControl._markers = slice(this.drawControl._markers, 0, 2);
×
290
                this.drawControl._poly._latlngs = slice(this.drawControl._poly._latlngs, 0, 2);
×
291
                this.drawControl._poly._originalPoints = slice(this.drawControl._poly._originalPoints, 0, 2);
×
292
                this.updateMeasurementResults();
×
293
                this.drawControl._finishShape();
×
294
                this.drawControl.disable();
×
295
            }, 100);
296
        } else {
297
            this.updateMeasurementResults();
×
298
        }
299
    };
300

301
    render() {
302
        // moved here the translations because when language changes it is forced a render of this component. (see connect of measure plugin)
303
        var drawingStrings = this.props.messages || (this.context.messages ? this.context.messages.drawLocal : false);
15!
304
        if (drawingStrings) {
15✔
305
            L.drawLocal = drawingStrings;
1✔
306
        }
307
        return null;
15✔
308
    }
309

310
    /**
311
     * This method adds arcs converting from a LineString features
312
     */
313
    addArcsToMap = (features) => {
8✔
314
        this.removeLastLayer();
×
315
        let newFeatures = features.map(f => {
×
NEW
316
            return Object.assign({}, f, {
×
317
                geometry: Object.assign({}, f.geometry, {
318
                    coordinates: transformLineToArcs(f.geometry.coordinates)
319
                })
320
            });
321
        });
322
        this.arcLayer = L.geoJson(newFeatures, {
×
323
            style: {
324
                color: '#ffcc33',
325
                opacity: 1,
326
                weight: 1,
327
                fillColor: '#ffffff',
328
                fillOpacity: 0.2,
329
                clickable: false
330
            }
331
        });
332
        this.props.map.addLayer(this.arcLayer);
×
333
        if (newFeatures && newFeatures.length > 0) {
×
334
            this.arcLayer.addData(newFeatures);
×
335
        }
336
    }
337
    updateMeasurementResults = () => {
8✔
338
        if (!this.drawing || !this.drawControl) {
×
339
            return;
×
340
        }
341
        let distance = 0;
×
342
        let area = 0;
×
343
        let bearing = 0;
×
344

345
        let currentLatLng = this.drawControl._currentLatLng;
×
346
        if (this.props.measurement.geomType === 'LineString' && this.drawControl._markers && this.drawControl._markers.length > 1) {
×
347
            // calculate length
348
            const reprojectedCoords = this.drawControl._markers.reduce((p, c) => {
×
349
                const { lng, lat } = c.getLatLng();
×
350
                return [...p, [lng, lat]];
×
351
            }, []);
352

353
            distance = calculateDistance(reprojectedCoords, this.props.measurement.lengthFormula);
×
354

355
        } else if (this.props.measurement.geomType === 'Polygon' && this.drawControl._poly) {
×
356
            // calculate area
357
            let latLngs = [...this.drawControl._poly.getLatLngs(), currentLatLng];
×
358
            area = L.GeometryUtil.geodesicArea(latLngs);
×
359
        } else if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) {
×
360
            // calculate bearing
361
            bearing = this.calculateBearing();
×
362
        }
363
        // let drawn geom stay on the map
NEW
364
        let newMeasureState = Object.assign({}, this.props.measurement, {
×
365
            point: null, // Point is set in onDraw.created
366
            len: distance,
367
            area: area,
368
            bearing: bearing
369
        });
370
        this.props.changeMeasurementState(newMeasureState);
×
371
    };
372

373
    restartDrawing = () => {
8✔
374
        this.props.map.off('click', this.restartDrawing, this);
×
375
        if (this.props.map.doubleClickZoom) {
×
376
            this.props.map.doubleClickZoom.enable();
×
377
        }
378
        // re-enable draw control, since it is stopped after
379
        // every finished sketch
380
        this.props.map.removeLayer(this.lastLayer);
×
381
        this.drawControl.enable();
×
382
        this.drawing = true;
×
383
    };
384

385
    addDrawInteraction = (newProps) => {
8✔
386

387
        this.removeDrawInteraction();
6✔
388

389
        this.props.map.on('draw:created', this.onDrawCreated, this);
6✔
390
        this.props.map.on('draw:drawstart', this.onDrawStart, this);
6✔
391
        this.props.map.on('draw:drawvertex', this.onDrawVertex, this);
6✔
392
        // this.props.map.on('click', this.mapClickHandler, this);
393
        this.props.map.on('mousemove', this.updateBearing, this);
6✔
394

395
        if (this.props.updateOnMouseMove) {
6!
396
            this.props.map.on('mousemove', this.updateMeasurementResults, this);
×
397
        }
398
        if (newProps.measurement.geomType === 'Point') {
6✔
399
            this.drawControl = new L.Draw.Marker(this.props.map, {
1✔
400
                repeatMode: false
401
            });
402
        } else if (newProps.measurement.geomType === 'LineString' ||
5✔
403
            newProps.measurement.geomType === 'Bearing') {
404
            const uomOptions = this.uomLengthOptions(newProps);
3✔
405
            this.drawControl = new L.Draw.Polyline(this.props.map, {
3✔
406
                shapeOptions: {
407
                    color: '#ffcc33',
408
                    weight: 2
409
                },
410
                showLength: true,
411
                useTreshold: newProps.useTreshold,
412
                uom: newProps.uom,
413
                geomType: newProps.measurement.geomType,
414
                ...uomOptions,
415
                repeatMode: false,
416
                icon: new L.DivIcon({
417
                    iconSize: new L.Point(8, 8),
418
                    className: 'leaflet-div-icon leaflet-editing-icon'
419
                }),
420
                touchIcon: new L.DivIcon({
421
                    iconSize: new L.Point(8, 8),
422
                    className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon'
423
                }),
424
                trueBearing: newProps.measurement.trueBearing
425
            });
426
        } else if (newProps.measurement.geomType === 'Polygon') {
2!
427
            const uomOptions = this.uomAreaOptions(newProps);
2✔
428
            this.drawControl = new L.Draw.Polygon(this.props.map, {
2✔
429
                shapeOptions: {
430
                    color: '#ffcc33',
431
                    weight: 2,
432
                    fill: 'rgba(255, 255, 255, 0.2)'
433
                },
434
                showArea: true,
435
                allowIntersection: false,
436
                showLength: false,
437
                repeatMode: false,
438
                useTreshold: newProps.useTreshold,
439
                uom: newProps.uom,
440
                geomType: newProps.measurement.geomType,
441
                ...uomOptions,
442
                icon: new L.DivIcon({
443
                    iconSize: new L.Point(8, 8),
444
                    className: 'leaflet-div-icon leaflet-editing-icon'
445
                }),
446
                touchIcon: new L.DivIcon({
447
                    iconSize: new L.Point(8, 8),
448
                    className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon'
449
                })
450
            });
451
        }
452

453
        // start the draw control
454
        this.drawControl.enable();
6✔
455
    };
456

457
    removeDrawInteraction = () => {
8✔
458
        if (this.drawControl !== null && this.drawControl !== undefined) {
15✔
459
            this.drawControl.disable();
6✔
460
            this.drawControl = null;
6✔
461
            this.removeLastLayer();
6✔
462
            this.removeArcLayer();
6✔
463
            this.props.map.off('draw:created', this.onDrawCreated, this);
6✔
464
            this.props.map.off('draw:drawstart', this.onDrawStart, this);
6✔
465
            this.props.map.off('draw:drawvertex', this.onDrawVertex, this);
6✔
466
            this.props.map.off('mousemove', this.updateBearing, this);
6✔
467
            this.props.map.off('click', this.restartDrawing, this);
6✔
468
            // this.props.map.off('click', this.mapClickHandler, this);
469
            if (this.props.updateOnMouseMove) {
6!
470
                this.props.map.off('mousemove', this.updateMeasurementResults, this);
×
471
            }
472
            if (this.props.map.doubleClickZoom) {
6!
473
                this.props.map.doubleClickZoom.enable();
6✔
474
            }
475
        }
476
    };
477
    removeLastLayer = () => {
8✔
478
        if (this.lastLayer) {
6✔
479
            this.props.map.removeLayer(this.lastLayer);
1✔
480
        }
481
    }
482
    removeArcLayer = () => {
8✔
483
        if (this.arcLayer) {
12!
484
            this.props.map.removeLayer(this.arcLayer);
×
485
        }
486
    }
487

488
    uomLengthOptions = (props) => {
8✔
489
        let {
490
            unit
491
        } = props.uom.length;
3✔
492
        const metric = unit === "m" || unit === "km"; // false = miles&yards
3!
493
        const nautic = unit === "nm";
3✔
494
        const feet = unit === "ft";
3✔
495
        return {
3✔
496
            metric,
497
            nautic,
498
            feet
499
        };
500
    }
501
    uomAreaOptions = (props) => {
8✔
502
        let {
503
            unit
504
        } = props.uom.area;
2✔
505
        const metric = unit === "sqm" || unit === "sqkm"; // false = miles
2!
506
        const nautic = unit === "sqnm";
2✔
507
        const feet = unit === "sqft";
2✔
508
        return {
2✔
509
            metric,
510
            nautic,
511
            feet
512
        };
513
    }
514

515
    calculateBearing = () => {
8✔
516
        let currentLatLng = this.drawControl._currentLatLng;
×
517
        let bearing = 0;
×
518
        let bearingMarkers = this.drawControl._markers;
×
519
        let coords1 = [bearingMarkers[0].getLatLng().lng, bearingMarkers[0].getLatLng().lat];
×
520
        let coords2;
521
        if (bearingMarkers.length === 1) {
×
522
            coords2 = [currentLatLng.lng, currentLatLng.lat];
×
523
        } else if (bearingMarkers.length === 2) {
×
524
            coords2 = [bearingMarkers[1].getLatLng().lng, bearingMarkers[1].getLatLng().lat];
×
525
        }
526
        // in order to align the results between leaflet and openlayers the coords are repojected only for leaflet
527
        coords1 = reproject(coords1, 'EPSG:4326', this.props.projection);
×
528
        coords2 = reproject(coords2, 'EPSG:4326', this.props.projection);
×
529
        // calculate the azimuth as base for bearing information
530
        bearing = calculateAzimuth(coords1, coords2, this.props.projection);
×
531
        return bearing;
×
532
    }
533
    updateBearing = () => {
8✔
534
        if (this.props.measurement.geomType === 'Bearing' && this.drawControl._markers && this.drawControl._markers.length > 0) {
×
535
            const trueBearing = this.props.measurement && this.props.measurement.trueBearing;
×
536
            this.drawControl.setOptions({ bearing: this.calculateBearing(), trueBearing });
×
537
        }
538
    }
539
}
540

541
export default MeasurementSupport;
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