• 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

77.99
/web/client/components/map/openlayers/DrawSupport.jsx
1
/*
2
 * Copyright 2018, 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 React from 'react';
10
import concat from 'lodash/concat';
11
import head from 'lodash/head';
12
import find from 'lodash/find';
13
import slice from 'lodash/slice';
14
import omit from 'lodash/omit';
15
import isArray from 'lodash/isArray';
16
import last from 'lodash/last';
17
import filter from 'lodash/filter';
18
import isNil from 'lodash/isNil';
19
import castArray from 'lodash/castArray';
20

21
import PropTypes from 'prop-types';
22
import uuid from 'uuid';
23
import axios from '../../../libs/ajax';
24
import {isSimpleGeomType, getSimpleGeomType} from '../../../utils/MapUtils';
25
import {reprojectGeoJson, calculateDistance, reproject} from '../../../utils/CoordinatesUtils';
26
import {createStylesAsync} from '../../../utils/VectorStyleUtils';
27
import {transformPolygonToCircle} from '../../../utils/openlayers/DrawSupportUtils';
28
import {isCompletePolygon} from '../../../plugins/Annotations/utils/AnnotationsUtils';
29
import { parseStyles, getStyle, defaultStyles, getMarkerStyle, getMarkerStyleLegacy } from './VectorStyle';
30

31
import {GeoJSON} from 'ol/format';
32
import Feature from 'ol/Feature';
33
import VectorSource from 'ol/source/Vector';
34
import VectorLayer from 'ol/layer/Vector';
35
import ImageLayer from 'ol/layer/Image';
36
import TileLayer from 'ol/layer/Tile';
37
import Draw from 'ol/interaction/Draw';
38
import DrawHole from '../../../utils/openlayers/hole/DrawHole';
39
import { Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, Circle} from 'ol/geom';
40
import GeometryCollection from 'ol/geom/GeometryCollection';
41
import {Style, Stroke, Fill, Text} from 'ol/style';
42
import CircleStyle from 'ol/style/Circle';
43
import Collection from 'ol/Collection';
44
import {always, primaryAction, altKeyOnly} from 'ol/events/condition';
45
import DoubleClickZoom from 'ol/interaction/DoubleClickZoom';
46
import Translate from 'ol/interaction/Translate';
47
import Modify from 'ol/interaction/Modify';
48
import Select from 'ol/interaction/Select';
49
import {unByKey} from 'ol/Observable';
50
import {getCenter} from 'ol/extent';
51
import {fromCircle, circular} from 'ol/geom/Polygon';
52
import {Snap} from "ol/interaction";
53
import {bbox, all} from "ol/loadingstrategy";
54
import {getFeatureURL} from "../../../api/WFS";
55

56
const geojsonFormat = new GeoJSON();
1✔
57

58
/**
59
 * Component that allows to draw and edit geometries as (Point, LineString, Polygon, Rectangle, Circle, MultiGeometries)
60
 Feature* @class DrawSupport
61
 * @memberof components
62
 * @prop {object} map the map usedto drawing on
63
 * @prop {string} drawOwner the owner of the drawn features
64
 * @prop {string} drawStatus the status that allows to do different things. see UNSAFE_componentWillReceiveProps method
65
 * @prop {string} drawMethod the method used to draw different geometries. can be Circle,BBOX, or a geomType from Point to MultiPolygons
66
 * @prop {object} options it contains the params used to enable the interactions or simply stop the DrawSupport after a ft is drawn
67
 * @prop {boolean} options.geodesic enable to draw a geodesic geometry (supported only for Circle)
68
 * @prop {object[]} features an array of geojson features used as a starting point for drawing new shapes or edit them
69
 * @prop {function} onChangeDrawingStatus method use to change the status of the DrawSupport
70
 * @prop {function} onGeometryChanged when a features is edited or drawn this methos is fired
71
 * @prop {function} onDrawStopped action fired if the DrawSupport stops
72
 * @prop {function} onDrawingFeatures triggered when user clicks on a map in order to draw something
73
 * @prop {function} onSelectFeatures triggered when select interaction is enabled and user click on map in order to draw something, without using drawinteraction
74
 * @prop {function} onEndDrawing action fired when a shape is drawn
75
 * @prop {object} style
76
*/
77

78
// TODO FIX doc
79
export default class DrawSupport extends React.Component {
80
    static propTypes = {
1✔
81
        map: PropTypes.object,
82
        drawOwner: PropTypes.string,
83
        drawStatus: PropTypes.string,
84
        drawMethod: PropTypes.string,
85
        options: PropTypes.object,
86
        features: PropTypes.array,
87
        onChangeDrawingStatus: PropTypes.func,
88
        onGeometryChanged: PropTypes.func,
89
        onDrawStopped: PropTypes.func,
90
        onDrawingFeatures: PropTypes.func,
91
        onSelectFeatures: PropTypes.func,
92
        onEndDrawing: PropTypes.func,
93
        style: PropTypes.object,
94

95
        snapping: PropTypes.bool,
96
        snappingLayer: PropTypes.object,
97
        snappingLayerInstance: PropTypes.object,
98
        isSnappingLoading: PropTypes.bool,
99
        snapConfig: PropTypes.object,
100
        onRefreshSnappingLayer: PropTypes.func,
101
        toggleSnappingIsLoading: PropTypes.func
102
    };
103

104
    static defaultProps = {
1✔
105
        map: null,
106
        drawOwner: null,
107
        drawStatus: null,
108
        drawMethod: null,
109
        features: null,
110
        options: {
111
            stopAfterDrawing: true
112
        },
113
        onChangeDrawingStatus: () => {},
114
        onGeometryChanged: () => {},
115
        onDrawStopped: () => {},
116
        onDrawingFeatures: () => {},
117
        onSelectFeatures: () => {},
118
        onEndDrawing: () => {}
119
    };
120

121
    /**
122
     * Inside this lifecycle method the `drawStatus` is checked to manipulate the behavior of the DrawSupport
123
     * Here is the list of all status:
124
     * - `create` allows to create features
125
     * - `start` allows to start drawing features
126
     * - `drawOrEdit` allows to start drawing or editing the passed features or both
127
     * - `stop` allows to stop drawing features
128
     * - `replace` allows to replace all the features drawn by DrawSupport with new ones
129
     * - `clean` it cleans the drawn features and stop the DrawSupport
130
     * - `endDrawing` as for 'replace' action allows to replace all the features in addition triggers end drawing action to store data in state
131
     *
132
     * Moreover `options` define the behavior of the DrawSupport, expecially in `drawOrEdit` status.
133
     * - `drawEnabled` in `drawOrEdit` status allows to enable the draw interaction
134
     * - `editEnabled` in `drawOrEdit` status allows to enable the modify interaction
135
     * - `stopAfterDrawing` trigger a change `stop` status after a feature is drawn
136
     * - `hole`: if the geometry is a `Polygon` or a `MultiPolygon`, this option allows to draw holes in them, instead of creating new polygons
137
     * - `featureProjection`: define the projection of the feature passed. It is used also to convert the coordinates of the drawn features.
138
     * - `style`: define the style of the drawn features
139
     *
140
     * @memberof components.map.DrawSupport
141
     * @function UNSAFE_componentWillReceiveProps
142
    */
143
    UNSAFE_componentWillReceiveProps(newProps) {
144
        if (this.drawLayer) {
93✔
145
            this.updateFeatureStyles(newProps.features);
14✔
146
        }
147
        if (!newProps.drawStatus && this.selectInteraction) {
93!
148
            this.selectInteraction.getFeatures().clear();
×
149
        }
150

151
        if (
93✔
152
            this.props.drawStatus !== newProps.drawStatus ||
114✔
153
            this.props.drawMethod !== newProps.drawMethod ||
154
            this.props.features !== newProps.features
155
        ) {
156
            switch (newProps.drawStatus) {
88✔
157
            case "create": this.addLayer(newProps); break; // deprecated, not used (addLayer is automatically called by other commands when needed)
8✔
158
            case "start":/* only starts draw*/ this.addInteractions(newProps); break;
15✔
159
            case "drawOrEdit": this.addDrawOrEditInteractions(newProps); break;
34✔
160
            case "stop": /* only stops draw*/ this.removeDrawInteraction(); break;
1✔
161
            case "replace": this.replaceFeatures(newProps); break;
10✔
162
            case "updateStyle": this.updateOnlyFeatureStyles(newProps); break;
1✔
163
            case "clean": this.clean(); break;
13✔
164
            case "cleanAndContinueDrawing": this.clean(true); break;
1✔
165
            case "endDrawing": this.endDrawing(newProps); break;
3✔
166
            default : break;
2✔
167
            }
168
        }
169
        this.updateSnapInteraction(newProps);
93✔
170
    }
171
    getNewFeature = (newDrawMethod, coordinates, radius, center) => {
89✔
172
        return new Feature({
10✔
173
            geometry: this.createOLGeometry({type: newDrawMethod, coordinates, radius, center})
174
        });
175
    }
176
    getMapCrs = () => {
89✔
177
        return this.props.map.getView().getProjection().getCode();
191✔
178
    }
179

180
    getWMSSnapSource = (snappingLayerInstance, snapConfig) => {
89✔
181
        const isLoading = this.props.toggleSnappingIsLoading;
1✔
182
        if (this?.snapMetadata?.id !== snappingLayerInstance.id) {
1!
183
            const source = new VectorSource({
1✔
184
                format: new GeoJSON(),
185
                loader: function(extent, resolution, projection) {
186
                    const proj = projection.getCode();
×
187
                    const url = getFeatureURL(snappingLayerInstance.search.url, snappingLayerInstance.name, {
×
188
                        version: '1.1.0',
189
                        outputFormat: 'application/json',
190
                        srsname: proj,
191
                        bbox: extent.join(',') + ',' + proj,
192
                        maxFeatures: snapConfig?.maxFeatures ?? 500000
×
193
                    });
194
                    isLoading();
×
195
                    const onError = (err) => {
×
196
                        source.removeLoadedExtent(extent);
×
197
                        err && console.warn(err);
×
198
                    };
199
                    axios.get(url)
×
200
                        .then(res => {
201
                            isLoading();
×
202
                            if (res.status === 200) {
×
203
                                source.addFeatures(
×
204
                                    source.getFormat().readFeatures(res.data)
205
                                );
206
                            } else {
207
                                onError();
×
208
                            }
209

210
                        })
211
                        .catch(onError);
212
                },
213
                strategy: this.selectLoadingStrategy(snapConfig)
214
            });
215
            this.snapMetadata = {
1✔
216
                id: snappingLayerInstance.id,
217
                source
218
            };
219
        }
220
        return this.snapMetadata.source;
1✔
221
    }
222

223
    /**
224
     * Callback to get layer instance from the map using layerId
225
     * @param {string} layerId
226
     * @returns {object|boolean}
227
     */
228
    getLayerInstance = (layerId) => {
89✔
229
        return this.props.map.getLayers().getArray().find(l => l.get('msId') === layerId);
2✔
230
    }
231

232
    render() {
233
        return null;
182✔
234
    }
235

236
    updateFeatureStyles = (features) => {
89✔
237
        if (features && features.length > 0) {
84✔
238
            features.forEach(f => {
46✔
239
                if (f.style) {
46✔
240
                    let olFeature = this.toOlFeature(f);
3✔
241
                    if (olFeature) {
3!
242
                        olFeature.setStyle(f.style ? getStyle(f) : this.toOlStyle(f.style, f.selected));
×
243
                    }
244
                }
245
            });
246
        }
247
    };
248

249
    updateOnlyFeatureStyles = (newProps) => {
89✔
250
        if (this.drawLayer) {
1!
251
            this.drawLayer.getSource().getFeatures().forEach(ftOl => {
1✔
252

253
                let features = head(newProps.features).features || newProps.features; // checking FeatureCollection or an array of simple features
1✔
254

255
                let originalGeoJsonFeature = find(features, ftTemp => ftTemp.properties.id === ftOl.getProperties().id);
1✔
256
                if (originalGeoJsonFeature) {
1!
257
                    // only if it finds a feature drawn then update its style
258
                    let promises = createStylesAsync(castArray(originalGeoJsonFeature.style));
1✔
259
                    axios.all(promises).then((styles) => {
1✔
260
                        ftOl.setStyle(() => parseStyles({...originalGeoJsonFeature, style: styles}));
3✔
261
                    });
262
                }
263
            });
264
        }
265
    }
266

267
    addLayer = (newProps, addInteraction) => {
89✔
268
        let layerStyle = null;
62✔
269
        const styleType = this.convertGeometryTypeToStyleType(newProps.drawMethod);
62✔
270
        /**
271
            This is a style function that applies array of styles to the features.
272
            It takes the style from the features in the props being drawn because
273
            the style array from the geojson feature model is not passed to Feature
274
            @param {object} ftOl it is an Feature object
275
        */
276
        layerStyle = (ftOl) => {
62✔
277
            let originalFeature = head(newProps.features) && find(head(newProps.features).features, ftTemp => ftTemp.properties.id === ftOl.getProperties().id) || null;
×
278
            if (originalFeature) {
×
279
                let promises = createStylesAsync(castArray(originalFeature.style));
×
280
                axios.all(promises).then((styles) => {
×
281
                    ftOl.setStyle(() => parseStyles({...originalFeature, style: styles}));
×
282
                });
283
                return null;
×
284
            }
285
            // if the styles is not present in the feature it uses a default one based on the drawMethod basically
286
            return parseStyles({style: defaultStyles[styleType]});
×
287
        };
288
        this.geojson = new GeoJSON();
62✔
289
        this.drawSource = new VectorSource();
62✔
290
        this.drawLayer = new VectorLayer({
62✔
291
            source: this.drawSource,
292
            zIndex: 100000000,
293
            style: layerStyle
294
        });
295

296
        this.props.map.addLayer(this.drawLayer);
62✔
297

298
        if (addInteraction) {
62✔
299
            this.addInteractions(newProps);
1✔
300
        }
301
        let newFeature = head(newProps.features);
62✔
302
        if (newFeature && newFeature.features && newFeature.features.length) {
62✔
303
            // filtering invalid circles features or keep all when drawing is disabled
304
            const featuresFiltered = newFeature.features.filter(f => !f.properties.isCircle || f.properties.isCircle && !f.properties.canEdit || !newProps.options.drawEnabled);
1✔
305
            return this.addFeatures(Object.assign({}, newProps, {features: [{...newFeature, features: featuresFiltered }]}));
1✔
306
        }
307
        return this.addFeatures(newProps);
61✔
308

309
    };
310

311
    addFeatures = ({features, drawMethod, options}) => {
89✔
312
        const mapCrs = this.getMapCrs();
70✔
313
        let feature;
314
        features.forEach((f) => {
70✔
315
            if (f.type === "FeatureCollection") {
36✔
316
                let featuresOL = (new GeoJSON()).readFeatures(f);
1✔
317
                if (!options.geodesic) featuresOL = featuresOL.map(ft => transformPolygonToCircle(ft, mapCrs));
1!
318
                this.drawSource = new VectorSource({
1✔
319
                    features: featuresOL
320
                });
321
                this.drawLayer.setSource(this.drawSource);
1✔
322
            } else {
323
                let center = null;
35✔
324
                let geometry = f;
35✔
325
                if (geometry.geometry && geometry.geometry.type !== "GeometryCollection") {
35✔
326
                    geometry = reprojectGeoJson(geometry, geometry.featureProjection, mapCrs).geometry;
9✔
327
                }
328
                if (geometry.type !== "GeometryCollection") {
35✔
329
                    if (drawMethod === "Circle" && geometry && (geometry.properties && geometry.properties.center || geometry.center)) {
27!
330
                        center = geometry.properties && geometry.properties.center ? reproject(geometry.properties.center, "EPSG:4326", mapCrs) : geometry.center;
3!
331
                        center = [center.x, center.y];
3✔
332
                        feature = new Feature({
3✔
333
                            geometry: this.createOLGeometry({type: "Circle", center, projection: "EPSG:3857", radius: geometry.properties && geometry.properties.radius || geometry.radius, options})
6!
334
                        });
335
                    } else {
336
                        feature = new Feature({
24✔
337
                            geometry: this.createOLGeometry(geometry.geometry ? geometry.geometry : {...geometry, radius: geometry.properties?.radius, center })
24!
338
                        });
339
                    }
340
                    feature.setProperties(f.properties);
27✔
341
                    this.drawSource.addFeature(feature);
27✔
342
                }
343
            }
344
        });
345

346
        // TODO CHECK THIS WITH FeatureCollection
347
        if (features.length === 0 && (options.editEnabled || options.drawEnabled)) {
70✔
348
            if (options.transformToFeatureCollection) {
16✔
349
                this.drawSource = new VectorSource({
5✔
350
                    features: (new GeoJSON()).readFeatures(
351
                        {
352
                            type: "FeatureCollection", features: []
353
                        })
354
                });
355
                this.drawLayer.setSource(this.drawSource);
5✔
356
            } else {
357
                feature = new Feature({
11✔
358
                    geometry: this.createOLGeometry({type: drawMethod, coordinates: null})
359
                });
360
                this.drawSource.addFeature(feature);
11✔
361
            }
362
        } else {
363
            if (features[0] && features[0].type === "GeometryCollection" ) {
54✔
364
                // HERE IT ENTERS WITH EDIT
365
                this.drawSource = new VectorSource({
8✔
366
                    features: (new GeoJSON()).readFeatures(features[0])
367
                });
368

369
                let geoms = this.replacePolygonsWithCircles(this.drawSource.getFeatures()[0]);
8✔
370
                this.drawSource.getFeatures()[0].getGeometry().setGeometries(geoms);
8✔
371
                this.drawLayer.setSource(this.drawSource);
8✔
372
            }
373
            if (features[0] && features[0].geometry && features[0].geometry.type === "GeometryCollection" ) {
54!
374
                // HERE IT ENTERS WITH REPLACE
375
                feature = reprojectGeoJson(features[0], options.featureProjection, mapCrs).geometry;
×
376
                this.drawSource = new VectorSource({
×
377
                    features: (new GeoJSON()).readFeatures(feature)
378
                });
379
                // TODO remove this props
380
                this.drawSource.getFeatures()[0].set("textGeometriesIndexes", features[0].properties && features[0].properties.textGeometriesIndexes);
×
381
                this.drawSource.getFeatures()[0].set("textValues", features[0].properties && features[0].properties.textValues);
×
382
                this.drawSource.getFeatures()[0].set("circles", features[0].properties && features[0].properties.circles);
×
383
                this.drawLayer.setSource(this.drawSource);
×
384
            }
385
        }
386
        this.updateFeatureStyles(features);
70✔
387
        return feature;
70✔
388
    };
389

390
    replaceFeatures = (newProps) => {
89✔
391
        let feature;
392
        if (!this.drawLayer) {
13✔
393
            feature = this.addLayer(newProps, newProps.options && newProps.options.drawEnabled || false);
5✔
394
        } else {
395
            this.drawSource.clear();
8✔
396
            feature = this.addFeatures(newProps);
8✔
397
            if (newProps.style) {
8✔
398
                this.drawLayer.setStyle((ftOl) => {
1✔
399
                    let originalFeature = find(head(newProps.features).features, ftTemp => ftTemp.properties.id === ftOl.getProperties().id);
×
400
                    if (originalFeature) {
×
401
                        let promises = createStylesAsync(castArray(originalFeature.style));
×
402
                        axios.all(promises).then((styles) => {
×
403
                            ftOl.setStyle(() => parseStyles({...originalFeature, style: styles}));
×
404
                        });
405
                        return null;
×
406
                    }
407
                    const styleType = this.convertGeometryTypeToStyleType(newProps.drawMethod);
×
408
                    return parseStyles({style: defaultStyles[styleType]});
×
409
                });
410
            }
411
        }
412
        return feature;
13✔
413
    };
414

415
    endDrawing = (newProps) => {
89✔
416
        const olFeature = this.replaceFeatures(newProps);
3✔
417
        if (olFeature) {
3✔
418
            const feature = this.fromOLFeature(olFeature);
2✔
419
            if (newProps.drawMethod === "Circle" && newProps && newProps.features && newProps.features.length && newProps.features[0] && newProps.features[0].radius >= 0) {
2!
420
                // this prevents the radius coming from `fromOLFeature` to override the radius set from an external tool
421
                // this is because `endDrawing` need to impose the radius value, without any re-calculation or approximation
422
                feature.radius = newProps.features[0].radius;
2✔
423
            }
424
            this.props.onEndDrawing(feature, newProps.drawOwner);
2✔
425
        }
426
    }
427

428
    addDrawInteraction = (drawMethod, startingPoint, maxPoints, newProps) => {
89✔
429
        if (this.drawInteraction) {
15!
430
            this.removeDrawInteraction();
×
431
        }
432
        this.drawInteraction = new Draw(this.drawPropertiesForGeometryType(drawMethod, maxPoints, this.drawSource, newProps));
15✔
433
        this.props.map.disableEventListener('singleclick');
15✔
434
        this.drawInteraction.on('drawstart', () => {
15✔
435
            if (this.selectInteraction) {
2!
436
                this.selectInteraction.getFeatures().clear();
×
437
                this.selectInteraction.setActive(false);
×
438
            }
439
        });
440
        this.drawInteraction.on('drawend', (evt) => {
15✔
441
            const sketchFeature = evt.feature.clone();
3✔
442
            sketchFeature.set('id', uuid.v1());
3✔
443
            if (this.props.drawMethod === "Circle" && sketchFeature.getGeometry().getType() === "Circle") {
3✔
444
                const radius = sketchFeature.getGeometry().getRadius();
1✔
445
                const center = sketchFeature.getGeometry().getCenter();
1✔
446
                sketchFeature.setGeometry(this.polygonFromCircle(center, radius));
1✔
447
            }
448
            const feature = this.fromOLFeature(sketchFeature, startingPoint);
3✔
449

450
            this.props.onEndDrawing(feature, this.props.drawOwner);
3✔
451
            if (this.props.options.stopAfterDrawing) {
3✔
452
                this.props.onChangeDrawingStatus('stop', this.props.drawMethod, this.props.drawOwner, this.props.features.concat([feature]));
2✔
453
            }
454
            if (this.selectInteraction) {
3!
455
                // TODO update also the selected features
456
                this.addSelectInteraction();
×
457
                this.selectInteraction.setActive(true);
×
458
            }
459
        });
460

461
        this.props.map.addInteraction(this.drawInteraction);
15✔
462
        this.setDoubleClickZoomEnabled(false);
15✔
463
    };
464

465
    selectLoadingStrategy = (config) => {
89✔
466
        switch (config?.strategy) {
1!
467
        case 'all':
468
            return all;
×
469
        case 'bbox':
470
        default:
471
            return bbox;
1✔
472
        }
473
    }
474

475
    createSnapInteraction = ({
89✔
476
        snapConfig,
477
        snappingLayerInstance,
478
        mapLayerInstance,
479
        layerType
480
    }) => {
481
        // type is not exposed anymore
482
        // we need to compare the layer instances
483
        // we cannot read the constructor name because it changes while minified
484
        if (mapLayerInstance instanceof VectorLayer) {
2✔
485
            return new Snap({...snapConfig, source: mapLayerInstance.getSource()});
1✔
486
        }
487
        if ((mapLayerInstance instanceof TileLayer || mapLayerInstance instanceof ImageLayer)
1!
488
            && layerType === 'wms') {
489
            const source = this.getWMSSnapSource(snappingLayerInstance, snapConfig);
1✔
490
            this.snapLayer = new VectorLayer({
1✔
491
                source,
492
                style: new Style({
493
                    stroke: new Stroke({
494
                        color: 'rgba(255,255,0,0)'
495
                    })
496
                })
497
            });
498
            this.props.map.addLayer(this.snapLayer);
1✔
499
            return new Snap({...snapConfig, source});
1✔
500
        }
501
        return null;
×
502
    }
503

504
    /**
505
     * Handler that activates snap interaction for snapping layer and draw data.
506
     * Vector layer data is taken directly from the map while tile layers data will be loaded using WFS query (if supported)
507
     * @param {boolean} snapping - state of snapping tool
508
     * @param {object} snappingLayerInstance - snapping layer definition from the store
509
     * @param {object} snapConfig - snapping tool configuration, merged from values set by localconfig.json and overwritten by user via snapping tool dropdown
510
     */
511
    addSnapInteraction = ({ snapping, snappingLayerInstance, snapConfig }) => {
89✔
512
        if (!snapping) return;
2!
513
        const mapLayerInstance = this.getLayerInstance(snappingLayerInstance.id);
2✔
514
        const layerType = snappingLayerInstance.type;
2✔
515
        this.removeSnapInteraction();
2✔
516
        const snapInteraction = this.createSnapInteraction({
2✔
517
            snapConfig,
518
            snappingLayerInstance,
519
            mapLayerInstance,
520
            layerType
521
        });
522
        if (snapInteraction) {
2!
523
            this.snapInteraction = snapInteraction;
2✔
524
            this.props.map.addInteraction(this.snapInteraction);
2✔
525
        }
526
    };
527
    toMulti = (geometry) => {
89✔
528
        if (geometry.getType() === 'Point') {
8!
529
            return new MultiPoint([geometry.getCoordinates()]);
×
530
        }
531
        return geometry;
8✔
532
    };
533

534
    handleDrawAndEdit = (drawMethod, startingPoint, maxPoints, newProps) => {
89✔
535
        if (this.drawInteraction) {
19!
536
            this.removeDrawInteraction();
×
537
        }
538
        this.drawInteraction = new Draw(this.drawPropertiesForGeometryType(getSimpleGeomType(drawMethod), maxPoints, isSimpleGeomType(drawMethod) ? this.drawSource : null, newProps ));
19✔
539
        this.props.map.disableEventListener('singleclick');
19✔
540
        this.drawInteraction.on('drawstart', () => {
19✔
541
            if (this.selectInteraction) {
1!
542
                this.selectInteraction.getFeatures().clear();
×
543
                this.selectInteraction.setActive(false);
×
544
            }
545
        });
546

547
        this.drawInteraction.on('drawend', (evt) => {
19✔
548
            const sketchFeature = evt.feature.clone();
14✔
549
            const id = uuid.v1();
14✔
550
            sketchFeature.set('id', id);
14✔
551
            let drawnGeom = sketchFeature.getGeometry();
14✔
552
            let drawnFeatures = this.drawLayer.getSource().getFeatures();
14✔
553
            let previousGeometries;
554
            let features = this.props.features;
14✔
555
            let geomCollection;
556
            let newDrawMethod;
557
            if (this.props.options.transformToFeatureCollection) {
14✔
558
                let newFeature;
559
                if (drawMethod === "Circle") {
5✔
560
                    newDrawMethod = "Polygon";
2✔
561
                    let radius;
562
                    let center;
563
                    let coordinates;
564
                    if (this.props.options.geodesic) {
2✔
565
                        center = evt.feature.getGeometry().geodesicCenter || getCenter(drawnGeom.getExtent());
1✔
566
                        const projection = this.props.map.getView().getProjection().getCode();
1✔
567
                        const wgs84Coordinates = [[...center],
1✔
568
                            [...drawnGeom.getCoordinates()[0][0]]].map((coordinate) => {
569
                            return this.reprojectCoordinatesToWGS84(coordinate, projection);
2✔
570
                        });
571
                        radius = calculateDistance(wgs84Coordinates, 'haversine');
1✔
572
                        coordinates = circular(wgs84Coordinates[0], radius).clone().transform('EPSG:4326', projection).getCoordinates();
1✔
573
                    } else {
574
                        radius = drawnGeom.getRadius();
1✔
575
                        center = drawnGeom.getCenter();
1✔
576
                        coordinates = this.polygonCoordsFromCircle(center, radius);
1✔
577
                    }
578
                    newFeature = this.getNewFeature(newDrawMethod, coordinates);
2✔
579
                    // TODO verify center is projected in 4326 and is an array
580
                    center = reproject(center, this.getMapCrs(), "EPSG:4326", false);
2✔
581
                    const originalId = newProps && newProps.features && newProps.features.length && newProps.features[0] && newProps.features[0].features && newProps.features[0].features.length && newProps.features[0].features.filter(f => f.properties.isDrawing)[0].properties.id || id;
2✔
582
                    newFeature.setProperties({isCircle: true, radius, center: [center.x, center.y], id: originalId, crs: this.getMapCrs(), isGeodesic: this.props.options.geodesic});
2✔
583
                } else if (drawMethod === "Polygon") {
3✔
584
                    newDrawMethod = this.props.drawMethod;
1✔
585
                    let coordinates = drawnGeom.getCoordinates();
1✔
586
                    coordinates[0].push(coordinates[0][0]);
1✔
587
                    newFeature = this.getNewFeature(newDrawMethod, coordinates);
1✔
588
                } else {
589
                    newDrawMethod = (drawMethod === "Text") ? "Point" : this.props.drawMethod;
2✔
590
                    let coordinates = drawnGeom.getCoordinates();
2✔
591
                    newFeature = this.getNewFeature(newDrawMethod, coordinates);
2✔
592
                    if (drawMethod === "Text") {
2✔
593
                        newFeature.setProperties({isText: true, valueText: "."});
1✔
594
                    }
595
                }
596
                // drawnFeatures is array of Feature
597
                const previousFeatures = drawnFeatures.length >= 1 ? [...this.replaceCirclesWithPolygonsInFeatureColl(drawnFeatures)] : [];
5!
598
                if (!newFeature.getProperties().id) {
5✔
599
                    newFeature.setProperties({id: uuid.v1()});
3✔
600
                }
601
                const newFeatures = [...previousFeatures, newFeature];
5✔
602
                // create FeatureCollection externalize as function
603
                let newFeatureColl = geojsonFormat.writeFeaturesObject(newFeatures);
5✔
604
                const vectorSource = new VectorSource({
5✔
605
                    features: (new GeoJSON()).readFeatures(newFeatureColl)
606
                });
607
                this.drawLayer.setSource(vectorSource);
5✔
608
                let feature = reprojectGeoJson(newFeatureColl, this.getMapCrs(), "EPSG:4326");
5✔
609
                this.props.onGeometryChanged([feature], this.props.drawOwner, this.props.options && this.props.options.stopAfterDrawing ? "enterEditMode" : "", drawMethod === "Text", drawMethod === "Circle");
5✔
610
                this.props.onEndDrawing(feature, this.props.drawOwner);
5✔
611
                this.props.onDrawingFeatures([last(feature.features)]);
5✔
612

613
            } else {
614
                if (drawMethod === "Circle") {
9✔
615
                    newDrawMethod = "Polygon";
1✔
616
                    const radius = drawnGeom.getRadius();
1✔
617
                    const center = drawnGeom.getCenter();
1✔
618
                    const coordinates = this.polygonCoordsFromCircle(center, radius);
1✔
619
                    const newMultiGeom = this.toMulti(this.createOLGeometry({type: newDrawMethod, coordinates}));
1✔
620
                    if (features.length === 1 && features[0] && !features[0].geometry) {
1!
621
                        previousGeometries = [];
×
622
                        geomCollection = new GeometryCollection([newMultiGeom]);
×
623
                    } else {
624
                        previousGeometries = this.toMulti(head(drawnFeatures).getGeometry());
1✔
625
                        if (previousGeometries.getGeometries) {
1!
626
                            // transform also previous circles into polygon
627
                            const geoms = this.replaceCirclesWithPolygons(head(drawnFeatures));
1✔
628
                            geomCollection = new GeometryCollection([...geoms, newMultiGeom]);
1✔
629
                        } else {
630
                            geomCollection = new GeometryCollection([previousGeometries, newMultiGeom]);
×
631
                        }
632
                    }
633
                    sketchFeature.setGeometry(geomCollection);
1✔
634

635
                } else if (drawMethod === "Text" || drawMethod === "MultiPoint") {
8✔
636
                    let coordinates = drawnGeom.getCoordinates();
1✔
637
                    newDrawMethod = "MultiPoint";
1✔
638
                    let newMultiGeom = this.toMulti(this.createOLGeometry({type: newDrawMethod, coordinates: [coordinates]}));
1✔
639
                    if (features.length === 1 && !features[0].geometry) {
1!
640
                        previousGeometries = [];
×
641
                        geomCollection = newMultiGeom.clone();
×
642
                    } else {
643
                        previousGeometries = this.toMulti(head(drawnFeatures).getGeometry());
1✔
644
                        if (previousGeometries.getGeometries) {
1!
645
                            let geoms = this.replaceCirclesWithPolygons(head(drawnFeatures));
1✔
646
                            geomCollection = new GeometryCollection([...geoms, newMultiGeom]);
1✔
647
                        } else {
648
                            geomCollection = previousGeometries.clone();
×
649
                            geomCollection.appendPoint(newMultiGeom.getPoint(0));
×
650
                        }
651
                    }
652
                    sketchFeature.setGeometry(geomCollection);
1✔
653
                } else if (!isSimpleGeomType(drawMethod)) {
7✔
654
                    let newMultiGeom;
655
                    geomCollection = null;
2✔
656
                    if (features.length === 1 && !features[0].geometry) {
2!
657
                        previousGeometries = this.toMulti(this.createOLGeometry({type: drawMethod, coordinates: null}));
×
658
                    } else {
659
                        previousGeometries = this.toMulti(head(drawnFeatures).getGeometry());
2✔
660
                    }
661

662
                    // find geometry of same type
663
                    let geometries = drawnFeatures.map(f => {
2✔
664
                        if (f.getGeometry().getType() === "GeometryCollection") {
2!
665
                            return f.getGeometry().getGeometries();
2✔
666
                        }
667
                        return f.getGeometry();
×
668
                    });
669
                    if (drawnFeatures[0].getGeometry().getType() === "GeometryCollection") {
2!
670
                        geometries = geometries[0];
2✔
671
                    }
672
                    let geomAlreadyPresent = find(geometries, (olGeom) => olGeom.getType() === drawMethod);
2✔
673
                    if (geomAlreadyPresent) {
2!
674
                        // append
675
                        this.appendToMultiGeometry(drawMethod, geomAlreadyPresent, drawnGeom);
×
676
                    } else {
677
                        // create new multi geom
678
                        newMultiGeom = this.toMulti(this.createOLGeometry({type: drawMethod, coordinates: drawnGeom.getCoordinates()}));
2✔
679
                    }
680

681
                    if (drawnGeom.getType() !== getSimpleGeomType(previousGeometries.getType())) {
2!
682
                        let geoms = head(drawnFeatures).getGeometry().getGeometries ? this.replaceCirclesWithPolygons(head(drawnFeatures)) : [];
2!
683
                        if (geomAlreadyPresent) {
2!
684
                            let newGeoms = geoms.map(gg => {
×
685
                                return gg.getType() === geomAlreadyPresent.getType() ? geomAlreadyPresent : gg;
×
686
                            });
687
                            geomCollection = new GeometryCollection(newGeoms);
×
688
                        } else {
689
                            if (previousGeometries.getType() === "GeometryCollection") {
2!
690
                                geomCollection = new GeometryCollection([...geoms, newMultiGeom]);
2✔
691
                            } else {
692
                                if (drawMethod === "Text") {
×
693
                                    geomCollection = new GeometryCollection([newMultiGeom]);
×
694
                                } else {
695
                                    geomCollection = new GeometryCollection([previousGeometries, newMultiGeom]);
×
696
                                }
697
                            }
698
                        }
699
                        sketchFeature.setGeometry(geomCollection);
2✔
700
                    } else {
701
                        sketchFeature.setGeometry(geomAlreadyPresent);
×
702
                    }
703
                }
704
                let properties = this.props.features[0].properties;
9✔
705
                if (drawMethod === "Text") {
9!
NEW
706
                    properties = Object.assign({}, this.props.features[0].properties, {
×
707
                        textValues: (this.props.features[0].properties.textValues || []).concat(["."]),
×
708
                        textGeometriesIndexes: (this.props.features[0].properties.textGeometriesIndexes || []).concat([sketchFeature.getGeometry().getGeometries().length - 1])
×
709
                    });
710
                }
711
                if (drawMethod === "Circle") {
9✔
712
                    properties = Object.assign({}, properties, {
1✔
713
                        circles: (this.props.features[0].properties.circles || []).concat([sketchFeature.getGeometry().getGeometries().length - 1])
2✔
714
                    });
715
                }
716
                let feature = this.fromOLFeature(sketchFeature, startingPoint, properties);
9✔
717
                const vectorSource = new VectorSource({
9✔
718
                    features: (new GeoJSON()).readFeatures(feature)
719
                });
720
                this.drawLayer.setSource(vectorSource);
9✔
721

722
                let newFeature = reprojectGeoJson(geojsonFormat.writeFeatureObject(sketchFeature.clone()), this.getMapCrs(), "EPSG:4326");
9✔
723
                if (newFeature.geometry.type === "Polygon") {
9✔
724
                    newFeature.geometry.coordinates[0].push(newFeature.geometry.coordinates[0][0]);
1✔
725
                }
726

727
                this.props.onGeometryChanged([newFeature], this.props.drawOwner, this.props.options && this.props.options.stopAfterDrawing ? "enterEditMode" : "", drawMethod === "Text", drawMethod === "Circle");
9✔
728
                this.props.onEndDrawing(feature, this.props.drawOwner);
9✔
729
                feature = reprojectGeoJson(feature, this.getMapCrs(), "EPSG:4326");
9✔
730

731
                const newFeatures = isSimpleGeomType(this.props.drawMethod) && this.props.features[0].geometry?.type !== "GeometryCollection" ?
9✔
732
                    this.props.features
733
                        .filter(feat => feat.geometry !== null)
5✔
734
                        .map(feat => ({
4✔
735
                            ...feat,
736
                            featureProjection: this.getMapCrs() // useful for reprojecting it after in replace method flow
737
                        })).concat([{
738
                            ...feature,
739
                            type: "Feature",
740
                            geometry: {
741
                                type: feature.type,
742
                                coordinates: feature.coordinates
743
                            },
744
                            featureProjection: this.getMapCrs(), // useful for reprojecting it after in replace method flow
745
                            properties}]) :
746
                    [{...feature, properties}];
747
                if (this.props.options.stopAfterDrawing) {
9✔
748
                    this.props.onChangeDrawingStatus('stop', this.props.drawMethod, this.props.drawOwner, newFeatures);
2✔
749
                } else {
750
                    this.props.onChangeDrawingStatus('replace', this.props.drawMethod, this.props.drawOwner,
7✔
751
                        newFeatures.map((f) => reprojectGeoJson(f, "EPSG:4326", this.getMapCrs())),
9✔
752
                        Object.assign({}, this.props.options, { featureProjection: this.getMapCrs()}));
753
                }
754
                if (this.selectInteraction) {
9!
755
                    // TODO update also the selected features
756
                    this.addSelectInteraction();
×
757
                    this.selectInteraction.setActive(true);
×
758
                }
759
            }
760

761
        });
762

763
        this.props.map.addInteraction(this.drawInteraction);
19✔
764
        this.addDrawHoleInteraction(drawMethod, maxPoints, newProps);
19✔
765
        this.setDoubleClickZoomEnabled(false);
19✔
766
    };
767
    addDrawHoleInteraction = (drawMethod, maxPoints, newProps) => {
89✔
768
        this.drawHoleInteraction = new DrawHole(this.drawPropertiesForGeometryType(drawMethod, maxPoints, this.drawSource, newProps));
19✔
769
        if (newProps?.options?.hole) {
19✔
770
            this.drawInteraction.setActive(false);
2✔
771
            this.drawHoleInteraction.setActive(true);
2✔
772
        } else {
773
            this.drawInteraction.setActive(true);
17✔
774
            this.drawHoleInteraction.setActive(false);
17✔
775
        }
776

777
        this.drawHoleInteraction.on('modifyend', event => {
19✔
778
            if (this.drawInteraction) {
2!
779
                this.drawInteraction.setActive(true);
2✔
780
            }
781
            this.drawHoleInteraction.setActive(false); // disable the drawHoleInteraction end
2✔
782
            const vectorSource = new VectorSource({
2✔
783
                features: event.features
784
            });
785
            this.drawLayer.setSource(vectorSource);
2✔
786
            let features = [];
2✔
787
            vectorSource.forEachFeature(feature => { features.push((new GeoJSON()).writeFeatureObject(feature)); });
2✔
788

789
            this.props.onGeometryChanged(features, this.props.drawOwner, this.props.options && this.props.options.stopAfterDrawing ? "enterEditMode" : "", drawMethod === "Text", drawMethod === "Circle");
2!
790
            this.props.onEndDrawing(features, this.props.drawOwner);
2✔
791
            features = features.map(feature => reprojectGeoJson(feature, this.getMapCrs(), "EPSG:4326"));
2✔
792
            if (this.props.options.stopAfterDrawing) {
2!
793
                this.props.onChangeDrawingStatus('stop', this.props.drawMethod, this.props.drawOwner, features);
2✔
794
            } else {
795
                this.props.onChangeDrawingStatus('replace', this.props.drawMethod, this.props.drawOwner,
×
796
                    features.map((f) => reprojectGeoJson(f, "EPSG:4326", this.getMapCrs())),
×
797
                    Object.assign({}, this.props.options, { featureProjection: this.getMapCrs()}));
798
            }
799
            // restore select interaction if it was disabled
800
            if (this.selectInteraction) {
2!
801
                // TODO update also the selected features
802
                this.addSelectInteraction();
×
803
                this.selectInteraction.setActive(true);
×
804
            }
805
        });
806
        this.props.map.addInteraction(this.drawHoleInteraction);
19✔
807
    };
808
    drawPropertiesForGeometryType = (geometryType, maxPoints, source, newProps = {}) => {
89!
809
        let drawBaseProps = {
55✔
810
            source: this.drawSource || source,
55!
811
            // type is mandatory in new version
812
            // if it's not provided we get an error
813
            type: /** @type {ol.geom.GeometryType} */ geometryType ?? 'Point',
55!
814
            style: geometryType === "Marker" ? getMarkerStyle(newProps.style) : new Style({
55!
815
                fill: new Fill({
816
                    color: 'rgba(255, 255, 255, 0.2)'
817
                }),
818
                stroke: new Stroke({
819
                    color: 'rgba(0, 0, 0, 0.5)',
820
                    lineDash: [10, 10],
821
                    width: 2
822
                }),
823
                image: new CircleStyle({
824
                    radius: 5,
825
                    stroke: new Stroke({
826
                        color: 'rgba(0, 0, 0, 0.7)'
827
                    }),
828
                    fill: new Fill({
829
                        color: 'rgba(255, 255, 255, 0.2)'
830
                    })
831
                })
832
            }),
833
            features: new Collection(),
834
            condition: always
835
        };
836
        let roiProps = {};
55✔
837
        switch (geometryType) {
55!
838
        case "BBOX": {
839
            roiProps.type = "LineString";
3✔
840
            roiProps.maxPoints = 2;
3✔
841
            roiProps.geometryFunction = function(coordinates, geometry) {
3✔
842
                let geom = geometry;
1✔
843
                if (!geom) {
1!
844
                    geom = new Polygon([]);
1✔
845
                }
846
                let start = coordinates[0];
1✔
847
                let end = coordinates[1];
1✔
848
                geom.setCoordinates(
1✔
849
                    [
850
                        [
851
                            start,
852
                            [start[0], end[1]],
853
                            end,
854
                            [end[0],
855
                                start[1]], start
856
                        ]
857
                    ]);
858
                return geom;
1✔
859
            };
860
            break;
3✔
861
        }
862
        case "Circle": {
863
            roiProps.maxPoints = 100;
11✔
864
            if (newProps.options && newProps.options.geodesic) {
11✔
865
                roiProps.geometryFunction = (coordinates, geometry) => {
6✔
866
                    let geom = geometry;
1✔
867
                    if (!geom) {
1!
868
                        geom = new Polygon([]);
1✔
869
                        geom.setProperties({ geodesicCenter: [...coordinates[0]] }, true);
1✔
870
                    }
871
                    let projection = this.props.map.getView().getProjection().getCode();
1✔
872
                    let wgs84Coordinates = [...coordinates].map((coordinate) => {
1✔
873
                        return this.reprojectCoordinatesToWGS84(coordinate, projection);
1✔
874
                    });
875
                    let radius = calculateDistance(wgs84Coordinates, 'haversine');
1✔
876
                    let coords = circular(wgs84Coordinates[0], radius).clone().transform('EPSG:4326', projection).getCoordinates();
1✔
877
                    geom.setCoordinates(coords);
1✔
878
                    return geom;
1✔
879
                };
880
            } else {
881
                roiProps.type = geometryType;
5✔
882
            }
883
            break;
11✔
884
        }
885
        case "Marker": case "Point": case "Text": case "LineString": case "Polygon": case "MultiPoint": case "MultiLineString": case "MultiPolygon": case "GeometryCollection": {
886
            if (geometryType === "LineString") {
41✔
887
                roiProps.maxPoints = maxPoints;
4✔
888
            }
889
            let geomType = geometryType === "Text" || geometryType === "Marker" ? "Point" : geometryType;
41✔
890
            roiProps.type = geomType;
41✔
891
            roiProps.geometryFunction = (coordinates, geometry) => {
41✔
892
                let geom = geometry;
×
893
                if (!geom) {
×
894
                    geom = this.createOLGeometry({ type: geomType, coordinates: null, options: newProps.options });
×
895
                }
896
                geom.setCoordinates(coordinates);
×
897
                return geom;
×
898
            };
899
            break;
41✔
900
        }
901
        default: return {};
×
902
        }
903
        return Object.assign({}, drawBaseProps, roiProps);
55✔
904
    };
905

906
    setDoubleClickZoomEnabled = (enabled) => {
89✔
907
        let interactions = this.props.map.getInteractions();
38✔
908
        for (let i = 0; i < interactions.getLength(); i++) {
38✔
909
            let interaction = interactions.item(i);
28✔
910
            if (interaction instanceof DoubleClickZoom) {
28✔
911
                interaction.setActive(enabled);
14✔
912
                break;
14✔
913
            }
914
        }
915
    };
916

917
    updateFeatureExtent = (event) => {
89✔
918
        const movedFeatures = event.features.getArray();
1✔
919
        const updatedFeatures = this.props.features.map((f) => {
1✔
920
            const moved = head(movedFeatures.filter((mf) => this.fromOLFeature(mf).id === f.id));
×
NEW
921
            return moved ? Object.assign({}, f, {
×
922
                geometry: moved.geometry,
923
                center: moved.center,
924
                extent: moved.extent,
925
                coordinate: moved.coordinates,
926
                radius: moved.radius
927
            }) : f;
928
        });
929

930
        this.props.onChangeDrawingStatus('replace', this.props.drawMethod, this.props.drawOwner, updatedFeatures);
1✔
931
    };
932
    addInteractions = (newProps) => {
89✔
933
        this.clean();
15✔
934
        if (!this.drawLayer) {
15!
935
            this.addLayer(newProps);
15✔
936
        }
937
        this.addDrawInteraction(newProps.drawMethod, newProps.options.startingPoint, newProps.options.maxPoints, newProps);
15✔
938
        if (newProps.options && newProps.options.editEnabled) {
15✔
939
            this.addSelectInteraction();
6✔
940
            if (this.translateInteraction) {
6!
941
                this.props.map.removeInteraction(this.translateInteraction);
×
942
            }
943

944
            this.translateInteraction = new Translate({
6✔
945
                features: this.selectInteraction.getFeatures()
946
            });
947
            this.translateInteraction.setActive(false);
6✔
948

949
            this.translateInteraction.on('translateend', this.updateFeatureExtent);
6✔
950
            this.props.map.addInteraction(this.translateInteraction);
6✔
951

952
            this.addTranslateListener();
6✔
953
            if (this.modifyInteraction) {
6!
954
                this.props.map.removeInteraction(this.modifyInteraction);
×
955
            }
956

957
            this.modifyInteraction = new Modify({
6✔
958
                features: this.selectInteraction.getFeatures(),
959
                condition: (e) => {
960
                    return primaryAction(e) && !altKeyOnly(e);
×
961
                }
962
            });
963

964
            this.props.map.addInteraction(this.modifyInteraction);
6✔
965
        }
966
        this.drawSource.clear();
15✔
967
        if (newProps.features.length > 0 ) {
15!
968
            this.addFeatures(newProps);
×
969
        }
970
    };
971

972

973
    addSingleClickListener = (singleclickCallback, props) => {
89✔
974
        let evtKey = props.map.on('singleclick', singleclickCallback);
6✔
975
        return evtKey;
6✔
976
    };
977

978
    unSingleClickCallback() {
979
        if (this.state && this.state.keySingleClickCallback) {
35!
980
            unByKey(this.state.keySingleClickCallback);
×
981
        }
982
    }
983

984
    addDrawOrEditInteractions = (newProps) => {
89✔
985
        this.unSingleClickCallback();
34✔
986
        const singleClickCallback = (event) => {
34✔
987
            if (this.drawSource && newProps.options) {
5!
988
                let previousFeatures = this.drawSource.getFeatures();
5✔
989
                let previousFtIndex = 0;
5✔
990

991
                const previousFt = previousFeatures && previousFeatures.length && previousFeatures.filter((f, i) => {
5✔
992
                    if (f.getProperties().canEdit) {
5✔
993
                        previousFtIndex = i;
3✔
994
                    }
995
                    return f.getProperties().canEdit;
5✔
996
                })[0] || null;
997
                const previousCoords = previousFt && previousFt.getGeometry() && previousFt.getGeometry().getCoordinates && previousFt.getGeometry().getCoordinates() || [];
5✔
998
                let actualCoords = [];
5✔
999
                let olFt;
1000
                let newDrawMethod = newProps.drawMethod;
5✔
1001
                switch (newDrawMethod) {
5!
1002
                case "Polygon": {
1003
                    if (previousCoords.length) {
1!
1004
                        if (isCompletePolygon(previousCoords)) {
1!
1005
                            // insert at penultimate position
1006
                            actualCoords = slice(previousCoords[0], 0, previousCoords[0].length - 1);
1✔
1007
                            actualCoords = actualCoords.concat([event.coordinate]);
1✔
1008
                            actualCoords = [actualCoords.concat([previousCoords[0][0]])];
1✔
1009
                        } else {
1010
                            // insert at ultimate position if more than 2 point
1011
                            actualCoords = previousCoords[0].length > 1 ? [[...previousCoords[0], event.coordinate, previousCoords[0][0] ]] : [[...previousCoords[0], event.coordinate ]];
×
1012
                        }
1013
                    } else {
1014
                        // insert at first position
1015
                        actualCoords = [[event.coordinate]];
×
1016
                    }
1017
                    olFt = this.getNewFeature(newDrawMethod, actualCoords);
1✔
1018
                    olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry"));
1!
1019
                    break;
1✔
1020
                }
1021
                case "LineString": case "MultiPoint": {
1022
                    actualCoords = previousCoords.length ? [...previousCoords, event.coordinate] : [event.coordinate];
1!
1023
                    olFt = this.getNewFeature(newDrawMethod, actualCoords);
1✔
1024
                    olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry"));
1!
1025
                    break;
1✔
1026
                }
1027
                case "Circle": {
1028
                    newDrawMethod = "Polygon";
2✔
1029
                    const radius = previousFt && previousFt.getProperties() && previousFt.getProperties().radius || 10000;
2!
1030
                    let center = event.coordinate;
2✔
1031
                    let coords = this.polygonCoordsFromCircle(center, radius);
2✔
1032
                    if (newProps.options.geodesic) {
2✔
1033
                        const projection = this.props.map.getView().getProjection().getCode();
1✔
1034
                        const wgs84Coordinates = [[...center]].map((coordinate) => {
1✔
1035
                            return this.reprojectCoordinatesToWGS84(coordinate, projection);
1✔
1036
                        });
1037
                        coords = circular(wgs84Coordinates[0], radius).clone().transform('EPSG:4326', projection).getCoordinates();
1✔
1038
                    }
1039
                    olFt = this.getNewFeature(newDrawMethod, coords);
2✔
1040
                    // TODO verify center is projected in 4326 and is an array
1041
                    center = reproject(center, this.getMapCrs(), "EPSG:4326", false);
2✔
1042
                    olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry"));
2!
1043
                    olFt.setProperties({isCircle: true, radius, center: [center.x, center.y], isGeodesic: this.props.options.geodesic});
2✔
1044
                    break;
2✔
1045
                }
1046
                case "Text": {
1047
                    newDrawMethod = "Point";
1✔
1048
                    olFt = this.getNewFeature(newDrawMethod, event.coordinate);
1✔
1049
                    olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry"));
1!
1050
                    olFt.setProperties({isText: true, valueText: previousFt && previousFt.getProperties() && previousFt.getProperties().valueText || newProps.options.defaultTextAnnotation || "New" });
1!
1051
                    break;
1✔
1052
                }
1053
                // point
1054
                default: {
1055
                    actualCoords = event.coordinate;
×
1056
                    olFt = this.getNewFeature(newDrawMethod, actualCoords);
×
1057
                    olFt.setProperties(omit(previousFt && previousFt.getProperties() || {}, "geometry"));
×
1058
                }
1059
                }
1060

1061
                let drawnFtWGS84 = reprojectGeoJson(geojsonFormat.writeFeaturesObject([olFt.clone()]), this.getMapCrs(), "EPSG:4326");
5✔
1062
                const coordinates = [...drawnFtWGS84.features[0].geometry.coordinates];
5✔
1063

1064
                let ft = {
5✔
1065
                    type: "Feature",
1066
                    geometry: {
1067
                        coordinates,
1068
                        type: newDrawMethod
1069
                    },
1070
                    properties: {
1071
                        ...omit(olFt.getProperties(), "geometry")
1072
                    }
1073
                };
1074

1075
                this.props.onDrawingFeatures([ft]);
5✔
1076

1077
                if (!newProps.options.geodesic) olFt = transformPolygonToCircle(olFt, this.getMapCrs());
5✔
1078
                previousFeatures[previousFtIndex] = olFt;
5✔
1079
                this.drawSource = new VectorSource({
5✔
1080
                    features: previousFeatures
1081
                });
1082
                this.drawLayer.setSource(this.drawSource);
5✔
1083
                this.addModifyInteraction(newProps);
5✔
1084
            }
1085
        };
1086
        this.clean();
34✔
1087

1088
        let newFeatures = newProps.features.map(f => {
34✔
1089
            return reprojectGeoJson(f, newProps.options.featureProjection, this.getMapCrs()) || {};
33✔
1090
        });
1091

1092
        let props;
1093
        const allHaveFeatures = newFeatures.every(ft => {
34✔
1094
            if (ft && ft.features && ft.features.length) {
33✔
1095
                return true;
1✔
1096
            }
1097
            return false;
32✔
1098
        });
1099

1100
        const allHaveCircleProperty = newFeatures.every(ft => {
34✔
1101
            if (ft && ft.properties && ft.properties.isCircle) {
33✔
1102
                return true;
4✔
1103
            }
1104
            return false;
29✔
1105
        });
1106

1107
        if (allHaveFeatures) {
34✔
1108
            props = Object.assign({}, newProps, {features: newFeatures});
2✔
1109
        } else {
1110
            if (allHaveCircleProperty) {
32✔
1111
                props = Object.assign({}, newProps, {features: []});
4✔
1112
            } else {
1113
                const fts = newFeatures.reduce((pre, curr) => {
28✔
1114
                    if (curr.geometry) {
28✔
1115
                        return [...pre, {...curr.geometry, properties: curr.properties}];
23✔
1116
                    }
1117
                    return pre;
5✔
1118
                }, []);
1119
                props = Object.assign({}, newProps, {features: fts});
28✔
1120
            }
1121
        }
1122

1123
        // TODO investigate if this newFeature.geometry is needed instead of only newFeature
1124
        if (!this.drawLayer) {
34!
1125
            this.addLayer(props);
34✔
1126
        } else {
1127
            this.drawSource.clear();
×
1128

1129
            this.addFeatures(props);
×
1130
        }
1131
        if (newProps.options.editEnabled) {
34✔
1132

1133
            if (!newProps.options.geodesic) {
14✔
1134
                this.addModifyInteraction(newProps);
12✔
1135
            }
1136
            // removed for polygon because of the issue https://github.com/geosolutions-it/MapStore2/issues/2378
1137
            if (newProps.options.translateEnabled !== false) {
14✔
1138
                this.addTranslateInteraction();
13✔
1139
            }
1140
            if (newProps.options.addClickCallback) {
14✔
1141
                this.setState({keySingleClickCallback: this.addSingleClickListener(singleClickCallback, newProps)});
6✔
1142
            }
1143
        }
1144
        if (newProps.options && newProps.options.selectEnabled) {
34✔
1145
            this.addSelectInteraction(newProps.options && newProps.options.selected, newProps);
2✔
1146

1147
        }
1148

1149
        if (newProps.options.drawEnabled) {
34✔
1150
            this.handleDrawAndEdit(newProps.drawMethod, newProps.options.startingPoint, newProps.options.maxPoints, newProps);
19✔
1151
        }
1152
    };
1153

1154
    addSelectInteraction = (selectedFeature, props) => {
89✔
1155
        if (this.selectInteraction) {
8!
1156
            this.props.map.removeInteraction(this.selectInteraction);
×
1157
        }
1158
        let olFt;
1159
        if (selectedFeature) {
8✔
1160
            olFt = find(this.drawSource.getFeatures(), f => f.getProperties().id === selectedFeature.properties.id );
1✔
1161
            if (olFt) {
1!
1162
                this.selectFeature(olFt);
1✔
1163
            }
1164
        }
1165
        this.selectInteraction = new Select({
8✔
1166
            layers: [this.drawLayer],
1167
            features: new Collection(selectedFeature && olFt ? [olFt] : null)
17✔
1168
        });
1169
        if (olFt) {
8✔
1170
            const styleType = this.convertGeometryTypeToStyleType(props.drawMethod);
1✔
1171
            olFt.setStyle(getStyle({ ...props, style: {...props.style, type: styleType, highlight: true, useSelectedStyle: props.options.useSelectedStyle }}, false, props.features[0] && props.features[0].properties && props.features[0].properties.valueText && [props.features[0].properties.valueText] || [] ));
1!
1172
        }
1173
        this.selectInteraction.on('select', (evt) => {
8✔
1174

1175
            let selectedFeatures = this.selectInteraction.getFeatures().getArray();
2✔
1176
            let featuresSelected = [];
2✔
1177
            if (selectedFeatures.length) {
2✔
1178
                featuresSelected = this.props.features.map(f => {
1✔
1179
                    let selected = false;
1✔
1180
                    if (f.type === "FeatureCollection" && selectedFeatures.length > 0) {
1!
1181
                        let ftSelected = head(selectedFeatures);
×
1182
                        this.selectFeature(ftSelected);
×
1183
                        // TODO SELECT SMALLEST ONE IF THERE ARE >= 2 features selected
1184

1185
                        if (ftSelected.getGeometry && ftSelected.getGeometry().getType() === "Circle") {
×
1186
                            let radius = ftSelected.getGeometry().getRadius();
×
1187
                            let center = reproject(ftSelected.getGeometry().getCenter(), this.getMapCrs(), "EPSG:4326");
×
1188
                            ftSelected.setProperties({center: [center.x, center.y], radius});
×
1189
                            ftSelected = this.replaceCircleWithPolygon(ftSelected.clone());
×
1190
                        }
1191
                        this.drawSource.getFeatures().forEach(feat => {
×
1192
                            if (feat.getProperties().id === ftSelected.getProperties().id) {
×
1193
                                this.selectFeature(ftSelected);
×
1194
                            } else {
1195
                                this.deselectFeature(feat);
×
1196
                            }
1197
                        });
1198
                        return reprojectGeoJson(geojsonFormat.writeFeatureObject(ftSelected.clone()), this.getMapCrs(), "EPSG:4326");
×
1199
                    }
1200
                    selected = selectedFeatures.reduce((previous, current) => {
1✔
1201
                        return current.get('id') === f.id ? true : previous;
1!
1202
                    }, false);
1203
                    return Object.assign({}, f, { selected: selected, selectedFeature: evt.selected });
1✔
1204
                });
1205
                this.props.onSelectFeatures(featuresSelected);
1✔
1206
            }
1207
            if (selectedFeatures.length === 0) {
2✔
1208
                this.props.onSelectFeatures([]);
1✔
1209
                this.drawSource.getFeatures().map( ft => this.deselectFeature(ft));
1✔
1210
                return null;
1✔
1211
            }
1212
            return null;
1✔
1213
        });
1214

1215
        this.props.map.addInteraction(this.selectInteraction);
8✔
1216
    };
1217
    selectFeature = (f) => {
89✔
1218
        f.setProperties({selected: true});
1✔
1219
    }
1220
    deselectFeature = (f) => {
89✔
1221
        f.setProperties({selected: false});
1✔
1222
    }
1223

1224
    removeDrawInteraction = () => {
89✔
1225
        if (this.drawInteraction) {
63✔
1226
            this.props.map.removeInteraction(this.drawInteraction);
4✔
1227
            this.drawInteraction = null;
4✔
1228
            /** Map Singleclick event is dealyed by 250 ms see here
1229
              * https://openlayers.org/en/latest/apidoc/ol.MapBrowserEvent.html#event:singleclick
1230
              * This timeout prevents ol map to throw mapClick event that has alredy been managed
1231
              * by the draw interaction.
1232
             */
1233
            setTimeout(() => this.props.map.enableEventListener('singleclick'), 500);
4✔
1234
            setTimeout(() => this.setDoubleClickZoomEnabled(true), 250);
4✔
1235
        }
1236
    };
1237

1238
    updateSnapInteraction = (newProps) => {
89✔
1239
        const snappingLayerExists = !!newProps.snappingLayerInstance?.id;
93✔
1240
        !snappingLayerExists && !!this.snapInteraction && this.removeSnapInteraction();
93!
1241
        if (!!this.snapInteraction) {
93!
1242
            const snappingConfigChanged = this.props.snapConfig !== newProps.snapConfig;
×
1243
            const snappingLayerChanged = this.props.snappingLayerInstance?.id !== newProps.snappingLayerInstance?.id;
×
1244
            const snappingToggledOff = !newProps.snapping && this.props.snapping;
×
1245
            const snappingToggledOn = newProps.snapping && !this.props.snapping;
×
1246
            if (snappingToggledOn) {
×
1247
                this.activateSnapInteraction();
×
1248
            }
1249
            if (snappingToggledOff) {
×
1250
                this.deactivateSnapInteraction();
×
1251
            }
1252
            if (snappingToggledOn || snappingLayerChanged || snappingConfigChanged) {
×
1253
                this.addSnapInteraction(newProps);
×
1254
            }
1255
        } else if (newProps.snapping && snappingLayerExists) {
93✔
1256
            this.addSnapInteraction(newProps);
2✔
1257
        }
1258
    }
1259
    activateSnapInteraction = () => {
89✔
1260
        this.snapInteraction?.setActive(true);
×
1261
    };
1262

1263
    deactivateSnapInteraction = () => {
89✔
1264
        this.snapInteraction?.setActive(false);
×
1265
    }
1266

1267
    removeSnapInteraction = () => {
89✔
1268
        if (this.snapInteraction) {
64!
1269
            this.props.map.removeInteraction(this.snapInteraction);
×
1270
            this.snapInteraction = null;
×
1271
        }
1272
        if (this.snapLayer) {
64!
1273
            this.props.map.removeLayer(this.snapLayer);
×
1274
        }
1275
    };
1276

1277
    removeInteractions = () => {
89✔
1278
        this.removeDrawInteraction();
62✔
1279
        this.removeSnapInteraction();
62✔
1280
        if (this.selectInteraction) {
62✔
1281
            this.props.map.enableEventListener('singleclick');
1✔
1282
            this.props.map.removeInteraction(this.selectInteraction);
1✔
1283
        }
1284

1285
        if (this.modifyInteraction) {
62✔
1286
            this.props.map.removeInteraction(this.modifyInteraction);
1✔
1287
            this.unSingleClickCallback();
1✔
1288
        }
1289

1290
        if (this.translateInteraction) {
62✔
1291
            this.props.map.removeInteraction(this.translateInteraction);
1✔
1292
        }
1293
    };
1294

1295
    clean = (continueDrawing) => {
89✔
1296
        if (!continueDrawing) {
63✔
1297
            this.removeInteractions();
62✔
1298
        }
1299
        if (this.drawLayer) {
63✔
1300
            this.props.map.removeLayer(this.drawLayer);
4✔
1301
            this.geojson = null;
4✔
1302
            this.drawLayer = null;
4✔
1303
            this.drawSource = null;
4✔
1304
        }
1305
    };
1306

1307
    fromOLFeature = (feature, startingPoint, properties) => {
89✔
1308
        const geometry = feature.getGeometry();
15✔
1309
        // retrieve geodesic center from properties
1310
        // it's different from extent center
1311
        const projection = this.props.map.getView().getProjection().getCode();
15✔
1312
        const type = geometry.getType();
15✔
1313
        // LineString, Polygon, MultiLineString, MultiPolygon
1314
        if (geometry.getCoordinates) {
15✔
1315
            const extent = geometry.getExtent();
11✔
1316
            const geometryProperties = geometry.getProperties();
11✔
1317
            const center = geometryProperties && geometryProperties.geodesicCenter || getCenter(extent);
11✔
1318
            let coordinates = geometry.getCoordinates();
11✔
1319
            if (startingPoint) {
11!
1320
                coordinates = concat(startingPoint, coordinates);
×
1321
                geometry.setCoordinates(coordinates);
×
1322
            }
1323
            let radius;
1324
            if (this.props.drawMethod === "Circle") {
11✔
1325
                if (this.props.options.geodesic) {
4!
1326
                    const wgs84Coordinates = [[...center], [...coordinates[0][0]]].map((coordinate) => {
4✔
1327
                        return this.reprojectCoordinatesToWGS84(coordinate, projection);
8✔
1328
                    });
1329
                    radius = calculateDistance(wgs84Coordinates, 'haversine');
4✔
1330
                } else {
1331
                    radius = this.calculateRadius(center, coordinates);
×
1332
                }
1333
            }
1334
            return Object.assign({}, {
11✔
1335
                id: feature.get('id'),
1336
                type,
1337
                extent,
1338
                center,
1339
                coordinates,
1340
                radius,
1341
                style: this.fromOlStyle(feature.getStyle()),
1342
                projection: this.getMapCrs()
1343
            });
1344

1345
        }
1346
        let geometries = geometry.getGeometries().map((g, i) => {
4✔
1347
            const extent = g.getExtent();
8✔
1348
            const center = getCenter(extent);
8✔
1349
            let coordinates = g.getCoordinates();
8✔
1350
            if (startingPoint) {
8!
1351
                coordinates = concat(startingPoint, coordinates);
×
1352
                g.setCoordinates(coordinates);
×
1353
            }
1354
            let radius;
1355
            if (properties.circles && properties.circles.indexOf(i) !== -1) {
8✔
1356
                if (this.props.options.geodesic) {
1!
1357
                    const wgs84Coordinates = [[...center], [...coordinates[0][0]]].map((coordinate) => {
×
1358
                        return this.reprojectCoordinatesToWGS84(coordinate, projection);
×
1359
                    });
1360
                    radius = calculateDistance(wgs84Coordinates, 'haversine');
×
1361
                } else {
1362
                    radius = this.calculateRadius(center, coordinates);
1✔
1363
                }
1364
            } else {
1365
                radius = 0;
7✔
1366
            }
1367
            return Object.assign({}, {
8✔
1368
                id: feature.get('id'),
1369
                type: g.getType(),
1370
                extent,
1371
                center,
1372
                coordinates,
1373
                radius,
1374
                style: this.fromOlStyle(feature.getStyle()),
1375
                projection: this.getMapCrs()
1376
            });
1377
        });
1378
        return Object.assign({}, {
4✔
1379
            type: "Feature",
1380
            id: feature.get('id'),
1381
            style: this.fromOlStyle(feature.getStyle()),
1382
            geometry: {
1383
                type: "GeometryCollection",
1384
                geometries
1385
            },
1386
            projection
1387
        });
1388
    };
1389

1390
    reprojectCoordinatesToWGS84 = (coordinate, projection) => {
89✔
1391
        let reprojectedCoordinate = reproject(coordinate, projection, 'EPSG:4326');
16✔
1392
        return [reprojectedCoordinate.x, reprojectedCoordinate.y];
16✔
1393
    };
1394

1395
    toOlFeature = (feature) => {
89✔
1396
        return head(this.drawSource.getFeatures().filter((f) => f.get('id') === feature.id));
3✔
1397
    };
1398

1399
    fromOlStyle = (olStyle) => {
89✔
1400
        if (!olStyle) {
23!
1401
            return {};
23✔
1402
        }
1403

1404
        return {
×
1405
            fillColor: this.rgbToHex(olStyle.getFill().getColor()),
1406
            fillTransparency: olStyle.getFill().getColor()[3],
1407
            strokeColor: olStyle.getStroke().getColor(),
1408
            strokeWidth: olStyle.getStroke().getWidth(),
1409
            text: olStyle.getText().getText()
1410
        };
1411
    };
1412

1413
    toOlStyle = (style, selected, type) => {
89✔
1414
        let fillColor = style && style.fillColor ? style.fillColor : [255, 255, 255, 0.2];
6✔
1415
        if (typeof fillColor === 'string') {
6✔
1416
            fillColor = this.hexToRgb(fillColor).concat([style.fillOpacity >= 0 && style.fillOpacity <= 1 ? style.fillOpacity : 1]);
2!
1417
        }
1418

1419
        if (style && style.fillTransparency) {
6✔
1420
            fillColor[3] = style.fillTransparency;
1✔
1421
        }
1422

1423
        let strokeColor = style && (style.strokeColor || style.color) ? style.strokeColor || style.color : '#ffcc33';
6!
1424
        if (selected) {
6!
1425
            strokeColor = '#4a90e2';
×
1426
        }
1427
        strokeColor = this.hexToRgb(strokeColor).concat([style && style.opacity || 1]);
6✔
1428
        let newStyle = new Style({
6✔
1429
            fill: new Fill({
1430
                color: fillColor
1431
            }),
1432
            stroke: new Stroke({
1433
                color: strokeColor,
1434
                width: style && (style.strokeWidth || style.weight) ? style.strokeWidth || style.weight : 2
22!
1435
            }),
1436
            text: new Text({
1437
                text: style && style.text ? style.text : '',
17!
1438
                fill: new Fill({ color: style && (style.strokeColor || style.color) ? style.strokeColor || style.color : '#000' }),
22!
1439
                stroke: new Stroke({ color: '#fff', width: 2 }),
1440
                font: style && style.fontSize ? style.fontSize + 'px helvetica' : ''
17!
1441
            })
1442
        });
1443

1444

1445
        if (type === "GeometryCollection") {
6!
1446
            return [...getMarkerStyleLegacy({
×
1447
                style: { iconGlyph: 'comment',
1448
                    iconShape: 'square',
1449
                    iconColor: 'blue' }
1450
            }), newStyle];
1451
        }
1452
        if (style && (style.iconUrl || style.iconGlyph)) {
6✔
1453
            return getMarkerStyleLegacy({
2✔
1454
                style
1455
            });
1456
        }
1457

1458

1459
        return newStyle;
4✔
1460
    };
1461

1462
    hexToRgb = (hex) => {
89✔
1463
        // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
1464
        var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
8✔
1465

1466
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.replace(shorthandRegex, function(m, r, g, b) {
8✔
1467
            return r + r + g + g + b + b;
3✔
1468
        }));
1469
        return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null;
8!
1470
    };
1471

1472
    componentToHex = (c) => {
89✔
1473
        var hex = c.toString(16);
×
1474
        return hex.length === 1 ? "0" + hex : hex;
×
1475
    };
1476

1477
    rgbToHex = (rgb) => {
89✔
1478
        return "#" + this.componentToHex(rgb[0]) + this.componentToHex(rgb[1]) + this.componentToHex(rgb[2]);
×
1479
    };
1480

1481
    addModifyInteraction = (props) => {
89✔
1482
        if (this.modifyInteraction) {
17✔
1483
            this.props.map.removeInteraction(this.modifyInteraction);
4✔
1484
        }
1485
        /*
1486
            filter features to be edited
1487
        */
1488
        const editFilter = props && props.options && props.options.editFilter;
17✔
1489
        this.modifyFeatureColl = new Collection(filter(this.drawLayer.getSource().getFeatures(), editFilter));
17✔
1490

1491

1492
        this.modifyInteraction = new Modify({
17✔
1493
            features: this.modifyFeatureColl,
1494
            condition: (e) => {
1495
                return primaryAction(e) && !altKeyOnly(e);
×
1496
            }
1497
        });
1498

1499
        this.modifyInteraction.on('modifyend', (e) => {
17✔
1500

1501
            let features = e.features.getArray().map((f) => {
2✔
1502
                // transform back circles in polygons
1503
                let newFt = f.clone();
2✔
1504

1505
                if (newFt.getGeometry && newFt.getGeometry().getType() === "GeometryCollection") {
2!
1506
                    newFt.getGeometry().setGeometries(this.replaceCirclesWithPolygons(newFt));
×
1507
                }
1508
                if (newFt.getGeometry && newFt.getGeometry() && newFt.getGeometry().getType() === "Circle") {
2!
1509
                    let center = reproject(newFt.getGeometry().getCenter(), this.getMapCrs(), "EPSG:4326");
2✔
1510
                    let radius = newFt.getGeometry().getRadius();
2✔
1511
                    newFt.setProperties({center: [center.x, center.y], radius});
2✔
1512
                    f.setProperties({center: [center.x, center.y], radius});
2✔
1513
                    newFt = this.replaceCircleWithPolygon(newFt.clone());
2✔
1514
                }
1515
                return reprojectGeoJson(geojsonFormat.writeFeatureObject(newFt), this.getMapCrs(), "EPSG:4326");
2✔
1516
            });
1517
            if (this.props.options.transformToFeatureCollection) {
2✔
1518
                this.props.onDrawingFeatures(features);
1✔
1519
            } else {
1520
                this.props.onGeometryChanged(features, this.props.drawOwner, false, "editing", "editing"); // TODO FIX THIS
1✔
1521
            }
1522
        });
1523
        this.props.map.addInteraction(this.modifyInteraction);
17✔
1524
    }
1525

1526
    addTranslateInteraction = () => {
89✔
1527
        if (this.translateInteraction) {
13!
1528
            this.props.map.removeInteraction(this.translateInteraction);
×
1529
        }
1530
        this.translateInteraction = new Translate({
13✔
1531
            features: new Collection(this.drawLayer.getSource().getFeatures())
1532
        });
1533
        this.translateInteraction.setActive(false);
13✔
1534
        this.translateInteraction.on('translateend', (e) => {
13✔
1535
            let features = e.features.getArray().map(f => {
×
1536
                // transform back circles in polygons
1537
                let newFt = f.clone();
×
1538
                if (newFt.getGeometry && newFt.getGeometry().getType() === "GeometryCollection") {
×
1539
                    newFt.getGeometry().setGeometries(this.replaceCirclesWithPolygons(newFt));
×
1540
                }
1541
                if (newFt.getGeometry && newFt.getGeometry() && newFt.getGeometry().getType() === "Circle") {
×
1542
                    let center = reproject(newFt.getGeometry().getCenter(), this.getMapCrs(), "EPSG:4326");
×
1543
                    let radius = newFt.getGeometry().getRadius();
×
1544
                    newFt.setProperties({center: [center.x, center.y], radius});
×
1545
                    newFt = this.replaceCircleWithPolygon(newFt);
×
1546
                }
1547
                if (f.getProperties() && f.getProperties().selected) {
×
1548
                    this.props.onSelectFeatures([reprojectGeoJson(geojsonFormat.writeFeatureObject(newFt), this.getMapCrs(), "EPSG:4326")]);
×
1549
                }
1550
                return reprojectGeoJson(geojsonFormat.writeFeatureObject(newFt), this.getMapCrs(), "EPSG:4326");
×
1551
            });
1552
            if (this.props.options.transformToFeatureCollection) {
×
1553
                this.props.onDrawingFeatures(features);
×
1554
            } else {
1555
                this.props.onGeometryChanged(features, this.props.drawOwner, this.props.drawOwner, false, this.props.drawMethod === "Text", this.props.drawMethod === "Circle");
×
1556
            }
1557
        });
1558
        this.addTranslateListener();
13✔
1559
        this.props.map.addInteraction(this.translateInteraction);
13✔
1560
    }
1561

1562
    createOLGeometry = ({type, coordinates, radius, center, geometries, projection, options = {}}) => {
89✔
1563
        if (type === "GeometryCollection") {
57!
1564
            return geometries && geometries.length ? new GeometryCollection(geometries.map(g => this.olGeomFromType({type: g.type}))) : new GeometryCollection([]);
×
1565
        }
1566
        return this.olGeomFromType({type, coordinates, radius, center, projection, options});
57✔
1567
    };
1568
    olGeomFromType = ({type, coordinates, radius, center, projection, options}) => {
89✔
1569
        // TODO check correct number of nesting arrays of coordinates for each case
1570
        let geometry;
1571
        switch (type) {
57✔
1572
        case "Point": case "Marker": case "Text": { geometry = new Point(coordinates ? coordinates : []); break; }
11✔
1573
        case "LineString": { geometry = new LineString(coordinates ? coordinates : []); break; }
7!
1574
        case "MultiPoint": { geometry = new MultiPoint(coordinates ? coordinates : []); break; }
1!
1575
        case "MultiLineString": { geometry = new MultiLineString(coordinates ? coordinates : []); break; }
1!
1576
        case "MultiPolygon": { geometry = new MultiPolygon(coordinates ? coordinates : []); break; }
2!
1577
        // default is Polygon
1578
        default: {
1579
            let correctCenter = isArray(center) ? {x: center[0], y: center[1]} : center;
35✔
1580
            const isCircle = projection
35✔
1581
                    && !isNaN(parseFloat(radius))
1582
                    && correctCenter
1583
                    && !isNil(correctCenter.x)
1584
                    && !isNil(correctCenter.y)
1585
                    && !isNaN(parseFloat(correctCenter.x))
1586
                    && !isNaN(parseFloat(correctCenter.y));
1587

1588
            // TODO simplify, too much use of elvis operator
1589
            geometry = isCircle ?
35✔
1590
                options.geodesic ?
4!
1591
                    circular(this.reprojectCoordinatesToWGS84([correctCenter.x, correctCenter.y], projection), radius, 100).clone().transform('EPSG:4326', projection)
1592
                    : fromCircle(new Circle([correctCenter.x, correctCenter.y], radius), 100)
1593
                : new Polygon(coordinates && isArray(coordinates[0]) ? coordinates : []);
81✔
1594

1595
            // store geodesic center
1596
            if (geometry && isCircle && options.geodesic) {
35✔
1597
                geometry.setProperties({geodesicCenter: [correctCenter.x, correctCenter.y]}, true);
4✔
1598
            }
1599
        }
1600
        }
1601
        return geometry;
57✔
1602
    }
1603

1604
    convertGeometryTypeToStyleType = (drawMethod) => {
89✔
1605
        switch (drawMethod) {
63✔
1606
        case "BBOX": return "LineString";
2✔
1607
        default: return drawMethod;
61✔
1608
        }
1609
    }
1610
    appendToMultiGeometry = (drawMethod, geometry, drawnGeom) => {
89✔
1611
        switch (drawMethod) {
×
1612
        case "MultiPoint": geometry.appendPoint(drawnGeom); break;
×
1613
        case "MultiLineString": geometry.appendLineString(drawnGeom); break;
×
1614
        case "MultiPolygon": {
1615
            let coords = drawnGeom.getCoordinates();
×
1616
            coords[0].push(coords[0][0]);
×
1617
            drawnGeom.setCoordinates(coords);
×
1618
            geometry.appendPolygon(drawnGeom); break;
×
1619
        }
1620
        default: break;
×
1621
        }
1622
    }
1623
    calculateRadius = (center, coordinates) => {
89✔
1624
        return isArray(coordinates) && isArray(coordinates[0]) && isArray(coordinates[0][0]) ? Math.sqrt(Math.pow(center[0] - coordinates[0][0][0], 2) + Math.pow(center[1] - coordinates[0][0][1], 2)) : 100;
1!
1625
    }
1626

1627
    /**
1628
     * @param {number[]} center in 3857 [lon, lat]
1629
     * @param {number} radius in meters
1630
     * @param {number} npoints number of sides
1631
     * @return {Polygon} the polygon which approximate the circle
1632
    */
1633
    polygonFromCircle = (center, radius, npoints = 100) => {
89✔
1634
        return fromCircle(new Circle(center, radius), npoints);
7✔
1635
    }
1636

1637
    polygonCoordsFromCircle = (center, radius, npoints = 100) => {
89✔
1638
        return this.polygonFromCircle(center, radius, npoints).getCoordinates();
6✔
1639
    }
1640
    /**
1641
     * replace circles with polygons in feature collection
1642
     * @param {Feature[]} features to transform
1643
     * @return {Feature[]} features transformed
1644
    */
1645
    replaceCirclesWithPolygonsInFeatureColl = (features) => {
89✔
1646
        return features.map(f => {
×
1647
            if (f.getGeometry().getType() !== "Circle") {
×
1648
                return f;
×
1649
            }
1650
            return this.replaceCircleWithPolygon(f);
×
1651
        });
1652
    }
1653
    /**
1654
     * tranform circle to polygon
1655
     * @param {Feature} feature to check if needs to be transformed
1656
     * @return {Feature} feature transformed in polygon
1657
    */
1658
    replaceCircleWithPolygon = (feature) => {
89✔
1659
        if (feature.getProperties().isCircle && feature.getGeometry().getType() === "Circle") {
2!
1660
            const center = feature.getGeometry().getCenter();
×
1661
            const radius = feature.getGeometry().getRadius();
×
1662
            feature.setGeometry(this.polygonFromCircle(center, radius));
×
1663
            return feature;
×
1664
        }
1665
        return feature;
2✔
1666
    }
1667
    /**
1668
     * replace circles with polygons
1669
     * @param {Feature} feature must contain a geometry collection
1670
     * @return {ol.geom.SimpleGeometry[]} geometries
1671
    */
1672
    replaceCirclesWithPolygons = (feature) => {
89✔
1673
        if (feature.getGeometry && !feature.getGeometry().getGeometries) {
4!
1674
            return feature;
×
1675
        }
1676
        let geoms = feature.getGeometry().getGeometries();
4✔
1677
        return geoms.map((g, i) => {
4✔
1678
            if (g.getType() !== "Circle") {
4!
1679
                return g;
4✔
1680
            }
1681
            if (feature.getProperties() && feature.getProperties().circles && feature.getProperties().circles.indexOf(i) !== -1) {
×
1682
                const center = g.getCenter();
×
1683
                const radius = g.getRadius();
×
1684
                return this.polygonFromCircle(center, radius);
×
1685
            }
1686
            return g;
×
1687
        });
1688
    }
1689

1690

1691
    /**
1692
     * replace polygons with circles
1693
     * @param {Feature} feature must contain a geometry collection and property "circles"
1694
     * @return {ol.geom.SimpleGeometry[]} geometries
1695
     */
1696
    replacePolygonsWithCircles = (feature) => {
89✔
1697
        let geoms = feature.getGeometry().getGeometries();
8✔
1698
        return geoms.map((g, i) => {
8✔
1699
            if (g.getType() !== "Polygon") {
8!
1700
                return g;
8✔
1701
            }
1702
            if (feature.getProperties() && feature.getProperties().circles && feature.getProperties().circles.indexOf(i) !== -1) {
×
1703
                const extent = g.getExtent();
×
1704
                const center = getCenter(extent);
×
1705
                const radius = this.calculateRadius(center, g.getCoordinates());
×
1706
                return new Circle(center, radius);
×
1707
            }
1708
            return g;
×
1709
        });
1710
    }
1711

1712
    addTranslateListener = () => {
89✔
1713
        document.addEventListener("keydown", (event) => {
19✔
1714
            if (event.altKey && event.code === "AltLeft") {
×
1715
                this.translateInteraction.setActive(true);
×
1716
            }
1717
        });
1718
        document.addEventListener("keyup", (event) => {
19✔
1719
            if (event.code === "AltLeft") {
×
1720
                this.translateInteraction.setActive(false);
×
1721
            }
1722
        });
1723
    }
1724
}
1725

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