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

compassinformatics / cpsi-mapview / 15022980938

14 May 2025 02:11PM UTC coverage: 26.333% (+0.04%) from 26.29%
15022980938

push

github

geographika
Move describe to test globals

492 of 2344 branches covered (20.99%)

Branch coverage included in aggregate %.

1464 of 5084 relevant lines covered (28.8%)

1.17 hits per line

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

39.43
/app/controller/button/DrawingButtonController.js
1
/**
2
 * This class is the controller for the DrawingButton.
3
 */
4
Ext.define('CpsiMapview.controller.button.DrawingButtonController', {
1✔
5
    extend: 'Ext.app.ViewController',
6
    requires: [
7
        'BasiGX.util.Map',
8
        'BasiGX.util.Layer',
9
        'Ext.menu.Menu',
10
        'GeoExt.component.FeatureRenderer',
11
        'GeoExt.data.store.Features',
12
        'CpsiMapview.controller.button.TracingMixin'
13
    ],
14

15
    alias: 'controller.cmv_drawing_button',
16

17
    mixins: ['CpsiMapview.controller.button.TracingMixin'],
18

19
    /**
20
     * The OpenLayers map. If not given, will be auto-detected
21
     */
22
    map: null,
23

24
    /**
25
     * The BasiGX mapComponent. If not given, will be auto-detected
26
     */
27
    mapComponent: null,
28

29
    /**
30
     * Temporary vector layer used while drawing points, lines or polygons
31
     */
32
    drawLayer: null,
33

34
    /**
35
     * OpenLayers draw interaction for drawing of lines and polygons
36
     */
37
    drawInteraction: null,
38

39
    /**
40
     * OpenLayers modify interaction
41
     * Used in polygon and point draw mode
42
     */
43
    modifyInteraction: null,
44

45
    /**
46
     * OpenLayers snap interaction for allowing easier tracing
47
     */
48
    snapInteraction: null,
49

50
    /**
51
     * If user has started to edit a line, this means the first point of a line is already set
52
     */
53
    currentlyDrawing: false,
54

55
    /**
56
     * Stores event listener keys to be un-listened to on destroy or button toggle
57
     */
58
    listenerKeys: [],
59

60
    /**
61
     * Determines if event handling is blocked.
62
     */
63
    //blockedEventHandling: false,
64

65
    constructor: function () {
66
        const me = this;
18✔
67
        me.handleDrawStart = me.handleDrawStart.bind(me);
18✔
68
        me.handleDrawEnd = me.handleDrawEnd.bind(me);
18✔
69
        me.handleModifyEnd = me.handleModifyEnd.bind(me);
18✔
70
        me.handleKeyPress = me.handleKeyPress.bind(me);
18✔
71
        me.callParent(arguments);
18✔
72
    },
73

74
    /**
75
     * Set the layer to store features drawn by the editing
76
     * tools
77
     * @param {any} layer
78
     */
79
    setDrawLayer: function (layer) {
80
        const me = this;
×
81

82
        if (!me.map) {
×
83
            return;
×
84
        }
85

86
        if (me.drawLayer) {
×
87
            me.map.removeLayer(me.drawLayer);
×
88
        }
89

90
        me.drawLayer = layer;
×
91
        me.setDrawInteraction(layer);
×
92
        me.setModifyInteraction(layer);
×
93
        me.setSnapInteraction(layer);
×
94
    },
95

96
    /**
97
     * Set the map drawing interaction
98
     * which will allow features to be added to the drawLayer
99
     * @param {any} layer
100
     */
101
    setDrawInteraction: function (layer) {
102
        const me = this;
×
103
        const view = me.getView();
×
104

105
        if (me.drawInteraction) {
×
106
            me.map.removeInteraction(me.drawInteraction);
×
107
        }
108

109
        const drawCondition = function (evt) {
×
110
            // the draw interaction does not work with the singleClick condition.
111
            return (
×
112
                ol.events.condition.primaryAction(evt) &&
×
113
                ol.events.condition.noModifierKeys(evt)
114
            );
115
        };
116

117
        const source = layer.getSource();
×
118
        const collection = source.getFeaturesCollection();
×
119
        const drawInteractionConfig = {
×
120
            type: 'LineString',
121
            features: collection,
122
            condition: drawCondition,
123
            style: me.getDrawStyleFunction(),
124
            snapTolerance: view.getDrawInteractionSnapTolerance()
125
        };
126

127
        me.drawInteraction = new ol.interaction.Draw(drawInteractionConfig);
×
128
        me.drawInteraction.on('drawstart', me.handleDrawStart);
×
129
        me.drawInteraction.on('drawend', me.handleDrawEnd);
×
130

131
        me.map.addInteraction(me.drawInteraction);
×
132
    },
133

134
    /**
135
     * Prepare the styles retrieved from config.
136
     */
137
    prepareDrawingStyles: function () {
138
        const me = this;
×
139
        const view = me.getView();
×
140

141
        // ensure styles are applied at right conditions
142
        view.getDrawBeforeEditingPoint().setGeometry(function (feature) {
×
143
            const geom = feature.getGeometry();
×
144
            if (!me.currentlyDrawing) {
×
145
                return geom;
×
146
            }
147
        });
148
        view.getDrawStyleStartPoint().setGeometry(function (feature) {
×
149
            const geom = feature.getGeometry();
×
150
            const coords = geom.getCoordinates();
×
151
            const firstCoord = coords[0];
×
152
            return new ol.geom.Point(firstCoord);
×
153
        });
154
        view.getDrawStyleEndPoint().setGeometry(function (feature) {
×
155
            const coords = feature.getGeometry().getCoordinates();
×
156
            if (coords.length > 1) {
×
157
                const lastCoord = coords[coords.length - 1];
×
158
                return new ol.geom.Point(lastCoord);
×
159
            }
160
        });
161

162
        // ensure snap styles are always on top
163
        view.getSnappedNodeStyle().setZIndex(Infinity);
×
164
        view.getSnappedEdgeStyle().setZIndex(Infinity);
×
165
    },
166

167
    /**
168
     * Creates the style function for the drawn feature.
169
     *
170
     * @returns {Function} The style function for the drawn feature.
171
     */
172
    getDrawStyleFunction: function () {
173
        const me = this;
×
174
        const view = me.getView();
×
175

176
        return function (feature) {
×
177
            const coordinate = feature.getGeometry().getCoordinates();
×
178
            const pixel = me.map.getPixelFromCoordinate(coordinate);
×
179

180
            // remember if we have hit a referenced layer
181

182
            let node, edge, polygon, self;
183

184
            me.map.forEachFeatureAtPixel(pixel, function (foundFeature, layer) {
×
185
                if (layer) {
×
186
                    const key = layer.get('layerKey');
×
187
                    if (key === view.getNodeLayerKey()) {
×
188
                        node = foundFeature;
×
189
                    } else if (key === view.getEdgeLayerKey()) {
×
190
                        edge = foundFeature;
×
191
                    } else if (key === view.getPolygonLayerKey()) {
×
192
                        polygon = foundFeature;
×
193
                    } else if (me.drawLayer === layer) {
×
194
                        // snapping to self drawn feature
195
                        self = foundFeature;
×
196
                    }
197
                }
198
            });
199

200
            if (node) {
×
201
                return view.getSnappedNodeStyle();
×
202
            } else if (edge) {
×
203
                if (view.getShowVerticesOfSnappedEdge()) {
×
204
                    // Prepare style for vertices of snapped edge
205
                    // we create a MultiPoint from the edge's vertices
206
                    // and set it as geometry in our style function
207
                    const geom = edge.getGeometry();
×
208
                    let coords = [];
×
209
                    if (geom.getType() === 'MultiLineString') {
×
210
                        // use all vertices of containing LineStrings
211
                        const lineStrings = geom.getLineStrings();
×
212
                        Ext.each(lineStrings, function (lineString) {
×
213
                            const lineStringCoords =
214
                                lineString.getCoordinates();
×
215
                            coords = coords.concat(lineStringCoords);
×
216
                        });
217
                    } else {
218
                        coords = geom.getCoordinates();
×
219
                    }
220
                    const verticesMultiPoint = new ol.geom.MultiPoint(coords);
×
221
                    const snappedEdgeVertexStyle = view
×
222
                        .getSnappedEdgeVertexStyle()
223
                        .clone();
224
                    snappedEdgeVertexStyle.setGeometry(verticesMultiPoint);
×
225

226
                    // combine style for snapped point and vertices of snapped edge
227
                    return [snappedEdgeVertexStyle, view.getSnappedEdgeStyle()];
×
228
                } else {
229
                    return view.getSnappedEdgeStyle();
×
230
                }
231
            } else if (polygon) {
×
232
                return view.getSnappedPolygonStyle();
×
233
            } else if (self) {
×
234
                return view.getModifySnapPointStyle();
×
235
            } else {
236
                return me.defaultDrawStyle;
×
237
            }
238
        };
239
    },
240

241
    /**
242
     * Set the modify interaction, used to modify
243
     * existing features created in the drawLayer
244
     * We cannot however simply stop and start redrawing the line, adding directly to the vertices
245
     * https://stackoverflow.com/questions/45836955/openlayers-3-continue-drawing-the-initial-line-after-drawend-triggered-with-do/45859390#45859390
246
     * @param {any} layer
247
     */
248
    setModifyInteraction: function (layer) {
249
        const me = this;
×
250

251
        if (me.modifyInteraction) {
×
252
            me.map.removeInteraction(me.modifyInteraction);
×
253
        }
254

255
        const condition = function (evt) {
×
256
            // only allow modifying when the CTRL key is pressed, otherwise we cannot add new line
257
            // segments once the first feature is drawn
258
            return (
×
259
                ol.events.condition.primaryAction(evt) &&
×
260
                ol.events.condition.platformModifierKeyOnly(evt)
261
            );
262
        };
263

264
        // create the modify interaction
265
        const modifyInteractionConfig = {
×
266
            features: layer.getSource().getFeaturesCollection(),
267
            condition: condition,
268
            // intentionally pass empty style, because modify style is
269
            // done in the draw interaction
270
            style: new ol.style.Style({})
271
        };
272
        me.modifyInteraction = new ol.interaction.Modify(
×
273
            modifyInteractionConfig
274
        );
275
        me.map.addInteraction(me.modifyInteraction);
×
276
        me.modifyInteraction.on('modifyend', me.handleModifyEnd);
×
277
    },
278

279
    /**
280
     * Set the snap interaction used to snap to features
281
     * @param {any} layer
282
     */
283
    setSnapInteraction: function (drawLayer) {
284
        const me = this;
11✔
285

286
        if (me.snapInteraction) {
11✔
287
            me.map.removeInteraction(me.snapInteraction);
1✔
288
        }
289

290
        // unbind any previous layer event listeners
291
        me.unBindLayerListeners();
11✔
292

293
        const snapCollection = new ol.Collection([], {
11✔
294
            unique: true
295
        });
296

297
        const fc = drawLayer.getSource().getFeaturesCollection();
11✔
298

299
        fc.on('add', function (evt) {
11✔
300
            snapCollection.push(evt.element);
×
301
        });
302

303
        fc.on('remove', function (evt) {
11✔
304
            snapCollection.remove(evt.element);
×
305
        });
306

307
        // Adds Features to a Collection, catches and ignores exceptions thrown
308
        // by the Collection if trying to add a duplicate feature, but still maintains
309
        // a unique collection of features. Used as an alternative to .extend but ensures
310
        // any potential errors related to unique features are handled / suppressed.
311
        const addUniqueFeaturesToCollection = function (collection, features) {
11✔
312
            Ext.Array.each(features, function (f) {
16✔
313
                // eslint-disable-next-line
314
                try {
31✔
315
                    collection.push(f);
31✔
316
                } catch (e) {}
317
            });
318
        };
319

320
        // Checks if a feature exists in layers other than the current layer
321
        const isFeatureInOtherLayers = function (
11✔
322
            allLayers,
323
            currentLayer,
324
            feature
325
        ) {
326
            let found = false;
4✔
327
            Ext.Array.each(allLayers, function (layer) {
4✔
328
                if (layer !== currentLayer) {
8✔
329
                    if (layer.getSource().hasFeature(feature)) {
4✔
330
                        found = true;
2✔
331
                    }
332
                }
333
            });
334
            return found;
4✔
335
        };
336

337
        // get the layers to snap to
338
        const view = me.getView();
11✔
339
        const layerKeys = view.getSnappingLayerKeys();
11✔
340
        const allowSnapToHiddenFeatures = view.getAllowSnapToHiddenFeatures();
11✔
341
        const layers = Ext.Array.map(layerKeys, function (key) {
11✔
342
            return BasiGX.util.Layer.getLayersBy('layerKey', key)[0];
16✔
343
        });
344

345
        Ext.Array.each(layers, function (layer) {
11✔
346
            const feats = layer.getSource().getFeatures(); // these are standard WFS layers so we use getSource without getFeaturesCollection here
16✔
347
            // add inital features to the snap collection, if the layer is visible
348
            // or if allowSnapToHiddenFeatures is enabled
349
            if (layer.getVisible() || allowSnapToHiddenFeatures) {
16✔
350
                addUniqueFeaturesToCollection(snapCollection, feats);
14✔
351
            }
352

353
            // Update the snapCollection on addfeature or removefeature
354
            const addFeatureKey = layer
16✔
355
                .getSource()
356
                .on('addfeature', function (evt) {
357
                    if (layer.getVisible() || allowSnapToHiddenFeatures) {
1!
358
                        addUniqueFeaturesToCollection(snapCollection, [
1✔
359
                            evt.feature
360
                        ]);
361
                    }
362
                });
363

364
            const removefeatureKey = layer
16✔
365
                .getSource()
366
                .on('removefeature', function (evt) {
367
                    if (!isFeatureInOtherLayers(layers, layer, evt.feature)) {
2✔
368
                        snapCollection.remove(evt.feature);
1✔
369
                    }
370
                });
371

372
            // Update the snapCollection on layer visibility change
373
            // only handle layer visible change event if snapping to hidden features is disabled
374

375
            let changeVisibleKey = null;
16✔
376

377
            if (!allowSnapToHiddenFeatures) {
16✔
378
                changeVisibleKey = layer.on('change:visible', function () {
14✔
379
                    const features = layer.getSource().getFeatures();
2✔
380
                    if (layer.getVisible()) {
2✔
381
                        addUniqueFeaturesToCollection(snapCollection, features);
1✔
382
                    } else {
383
                        Ext.Array.each(features, function (f) {
1✔
384
                            if (!isFeatureInOtherLayers(layers, layer, f)) {
2✔
385
                                snapCollection.remove(f);
1✔
386
                            }
387
                        });
388
                    }
389
                });
390
            }
391

392
            me.listenerKeys.push(addFeatureKey, removefeatureKey);
16✔
393

394
            if (changeVisibleKey) {
16✔
395
                me.listenerKeys.push(changeVisibleKey);
14✔
396
            }
397
        });
398

399
        // vector tile sources cannot be used for snapping as they
400
        // do not provide a getFeatures function
401
        // see https://openlayers.org/en/latest/apidoc/module-ol_source_VectorTile-VectorTile.html
402

403
        me.snapInteraction = new ol.interaction.Snap({
11✔
404
            features: snapCollection
405
        });
406
        me.map.addInteraction(me.snapInteraction);
11✔
407
    },
408

409
    getSnappedEdge: function (coord, searchLayer) {
410
        const me = this;
9✔
411
        const extent = me.getBufferedCoordExtent(coord);
9✔
412

413
        const features = [];
9✔
414
        // find all intersecting edges
415
        // https://openlayers.org/en/latest/apidoc/module-ol_source_Vector-VectorSource.html
416
        searchLayer
9✔
417
            .getSource()
418
            .forEachFeatureIntersectingExtent(extent, function (feat) {
419
                features.push(feat);
12✔
420
            });
421

422
        if (features.length > 0) {
9✔
423
            let selectedFeat = null;
6✔
424
            const parentRec = me.getView().getParentRecord();
6✔
425
            if (parentRec) {
6✔
426
                features.forEach(function (feat) {
1✔
427
                    if (feat.getId() !== parentRec.getId()) {
2✔
428
                        // the found feature does not share the same Id as the associated record
429
                        // this allows us to avoid snapping a feature to itself
430
                        selectedFeat = feat;
1✔
431
                        return false;
1✔
432
                    }
433
                });
434
            }
435

436
            if (!selectedFeat) {
6✔
437
                selectedFeat = features[0]; // by default return the first feature
5✔
438
            }
439

440
            return selectedFeat;
6✔
441
        } else {
442
            return null;
3✔
443
        }
444
    },
445

446
    getBufferedCoordExtent: function (coord) {
447
        const me = this;
16✔
448
        const extent = ol.extent.boundingExtent([coord]); // still a single point
16✔
449
        const buffer = me.map.getView().getResolution() * 3; // use a 3 pixel tolerance for snapping
16✔
450
        return ol.extent.buffer(extent, buffer); // buffer the point as it may have snapped to a different feature than the nodes/edges
16✔
451
    },
452

453
    getSnappedFeatureId: function (coord, searchLayer) {
454
        const me = this;
1✔
455
        const feat = me.getSnappedEdge(coord, searchLayer);
1✔
456

457
        if (feat) {
1!
458
            // this requires all GeoJSON features used for the layer to have an id property
459
            //<debug>
460
            Ext.Assert.truthy(feat.getId());
×
461
            //</debug>
462
            return feat.getId();
×
463
        }
464

465
        return null;
1✔
466
    },
467

468
    /**
469
     * Rather than simply creating a new feature each time, attempt to
470
     * merge contiguous linestrings together if the end of the old line
471
     * matches the start of the new line
472
     * @param {any} origGeom
473
     * @param {any} newGeom
474
     */
475
    mergeLineStrings: function (origGeom, newGeom) {
476
        const newGeomFirstCoord = newGeom.getFirstCoordinate();
×
477
        const matchesFirstCoord = Ext.Array.equals(
×
478
            origGeom.getFirstCoordinate(),
479
            newGeomFirstCoord
480
        );
481
        const matchesLastCoord = Ext.Array.equals(
×
482
            origGeom.getLastCoordinate(),
483
            newGeomFirstCoord
484
        );
485

486
        if (matchesFirstCoord || matchesLastCoord) {
×
487
            const origCoords = origGeom.getCoordinates();
×
488
            // if drawing in continued from the start point of the original,
489
            // the original needs to be reversed to we end up with correct
490
            // start and end points
491
            if (matchesFirstCoord) {
×
492
                origCoords.reverse();
×
493
            }
494
            const newCoords = newGeom.getCoordinates();
×
495
            newGeom.setCoordinates(origCoords.concat(newCoords));
×
496
        } else {
497
            Ext.log('Start / End coordinates differ');
×
498
            Ext.log(
×
499
                'origGeom start/end coords: ',
500
                origGeom.getFirstCoordinate(),
501
                origGeom.getLastCoordinate()
502
            );
503
            Ext.log('newGeom start coord: ', newGeom.getFirstCoordinate());
×
504
        }
505

506
        return newGeom;
×
507
    },
508

509
    /**
510
     * Handles the drawstart event
511
     */
512
    handleDrawStart: function () {
513
        const me = this;
×
514
        me.currentlyDrawing = true;
×
515
    },
516

517
    /**
518
     * Handles the drawend event
519
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
520
     */
521
    handleDrawEnd: function (evt) {
522
        const me = this;
×
523
        const feature = evt.feature;
×
524
        const newGeom = feature.getGeometry();
×
525

526
        const drawSource = me.drawLayer.getSource();
×
527
        const currentFeature = drawSource.getFeaturesCollection().item(0);
×
528

529
        if (currentFeature) {
×
530
            // merge all linestrings to a single linestring
531
            // this is done in place
532
            me.mergeLineStrings(currentFeature.getGeometry(), newGeom);
×
533
        }
534

535
        me.calculateLineIntersections(feature);
×
536

537
        // clear all previous features so only the last drawn feature remains
538
        drawSource.getFeaturesCollection().clear();
×
539

540
        me.currentlyDrawing = false;
×
541
    },
542

543
    /**
544
     * Handles the modifyend event
545
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
546
     */
547
    handleModifyEnd: function (evt) {
548
        const me = this;
×
549
        const feature = evt.features.item(0);
×
550
        me.calculateLineIntersections(feature);
×
551
    },
552

553
    /**
554
     * Get a nodeId from the closest edge to the input coord
555
     * If both ends of the edge are within the snap tolerance of the
556
     * input coord then the node closest to the input point is used
557
     * @param {any} edgesLayer
558
     * @param {any} edgeLayerConfig
559
     * @param {any} coord
560
     * @returns {Number}
561
     */
562
    getNodeIdFromSnappedEdge: function (edgesLayer, edgeLayerConfig, coord) {
563
        const me = this;
4✔
564
        let nodeId;
565

566
        // check to see if the coord snaps to the start of any edges
567
        const edge = me.getSnappedEdge(coord, edgesLayer);
4✔
568
        if (!edge) {
4✔
569
            return null;
1✔
570
        }
571

572
        const inputPoint = new ol.geom.Point(coord);
3✔
573

574
        const startCoord = edge.getGeometry().getFirstCoordinate();
3✔
575
        const startExtent = me.getBufferedCoordExtent(startCoord);
3✔
576
        let startDistance = null;
3✔
577

578
        if (inputPoint.intersectsExtent(startExtent)) {
3!
579
            nodeId = edge.get(edgeLayerConfig.startNodeProperty);
3✔
580
            startDistance = new ol.geom.LineString([
3✔
581
                coord,
582
                startCoord
583
            ]).getLength();
584
        }
585

586
        const endCoord = edge.getGeometry().getLastCoordinate();
3✔
587
        const endExtent = me.getBufferedCoordExtent(endCoord);
3✔
588

589
        if (inputPoint.intersectsExtent(endExtent)) {
3!
590
            const endNodeId = edge.get(edgeLayerConfig.endNodeProperty);
3✔
591
            if (startDistance !== null) {
3!
592
                // if an input coord snaps to both ends of the line, then take the closest end
593
                const endDistance = new ol.geom.LineString([
3✔
594
                    coord,
595
                    endCoord
596
                ]).getLength();
597
                if (endDistance < startDistance) {
3✔
598
                    nodeId = endNodeId;
2✔
599
                }
600
            } else {
601
                nodeId = endNodeId;
×
602
            }
603
        }
604

605
        // if an edge has been found then it should snap at the start or the end
606
        // and we should not end up here
607

608
        //<debug>
609
        if (!nodeId) {
3!
610
            Ext.log.warn(
×
611
                'A coordinate snapped to an edge, but no nodeId was found. Check the edgeLayerConfig'
612
            );
613
        }
614
        //</debug>
615

616
        return nodeId;
3✔
617
    },
618

619
    /**
620
     * Calculate where the geometry intersects other parts of the network
621
     * @param {any} newGeom
622
     */
623
    calculateLineIntersections: function (feature) {
624
        const me = this;
1✔
625
        const view = me.getView();
1✔
626

627
        const newGeom = feature.getGeometry();
1✔
628

629
        const startCoord = newGeom.getFirstCoordinate();
1✔
630
        const endCoord = newGeom.getLastCoordinate();
1✔
631

632
        let foundFeatAtStart = false;
1✔
633
        let foundFeatAtEnd = false;
1✔
634

635
        // get any nodes that the line snaps to
636
        const nodeLayerKey = view.getNodeLayerKey();
1✔
637
        let startNodeId = null;
1✔
638
        let endNodeId = null;
1✔
639

640
        if (nodeLayerKey) {
1!
641
            const nodeLayer = BasiGX.util.Layer.getLayersBy(
×
642
                'layerKey',
643
                nodeLayerKey
644
            )[0];
645
            startNodeId = me.getSnappedFeatureId(startCoord, nodeLayer);
×
646
            endNodeId = me.getSnappedFeatureId(endCoord, nodeLayer);
×
647

648
            foundFeatAtStart = startNodeId ? true : false;
×
649
            foundFeatAtEnd = endNodeId ? true : false;
×
650
        }
651

652
        const edgeLayerKey = view.getEdgeLayerKey();
1✔
653
        let edgesLayer;
654

655
        if (edgeLayerKey) {
1!
656
            edgesLayer = BasiGX.util.Layer.getLayersBy(
1✔
657
                'layerKey',
658
                edgeLayerKey
659
            )[0];
660
        }
661

662
        // if the edge layer has been configured with to and from node fields
663
        // we will check if the feature snaps at the start or end of edges
664

665
        const edgeLayerConfig = view.getEdgeLayerConfig();
1✔
666

667
        if (edgesLayer && edgeLayerConfig) {
1!
668
            let nodeId;
669
            nodeId = me.getNodeIdFromSnappedEdge(
1✔
670
                edgesLayer,
671
                edgeLayerConfig,
672
                startCoord
673
            );
674

675
            if (nodeId) {
1!
676
                startNodeId = nodeId;
1✔
677
                foundFeatAtStart = true;
1✔
678
            }
679

680
            nodeId = me.getNodeIdFromSnappedEdge(
1✔
681
                edgesLayer,
682
                edgeLayerConfig,
683
                endCoord
684
            );
685

686
            if (nodeId) {
1!
687
                endNodeId = nodeId;
×
688
                foundFeatAtEnd = true;
×
689
            }
690
        }
691

692
        // now check for any edges at both ends, but only in the case
693
        // where there are no start and end nodes
694

695
        let startEdgeId = null;
1✔
696
        let endEdgeId = null;
1✔
697

698
        if (edgesLayer) {
1!
699
            if (!foundFeatAtStart) {
1!
700
                startEdgeId = me.getSnappedFeatureId(startCoord, edgesLayer);
×
701
                foundFeatAtStart = startEdgeId ? true : false;
×
702
            }
703

704
            if (!foundFeatAtEnd) {
1!
705
                endEdgeId = me.getSnappedFeatureId(endCoord, edgesLayer);
1✔
706
                foundFeatAtEnd = endEdgeId ? true : false;
1!
707
            }
708
        }
709

710
        // finally we will check if we have snapped to a polygon edge
711
        // this will allow us to create continua based on points around the polygon edge
712

713
        const polygonLayerKey = view.getPolygonLayerKey();
1✔
714
        let startPolygonId = null;
1✔
715
        let endPolygonId = null;
1✔
716

717
        if (polygonLayerKey) {
1!
718
            const polygonsLayer = BasiGX.util.Layer.getLayersBy(
×
719
                'layerKey',
720
                polygonLayerKey
721
            )[0];
722

723
            if (!foundFeatAtStart) {
×
724
                startPolygonId = me.getSnappedFeatureId(
×
725
                    startCoord,
726
                    polygonsLayer
727
                );
728
            }
729

730
            if (!foundFeatAtEnd) {
×
731
                endPolygonId = me.getSnappedFeatureId(endCoord, polygonsLayer);
×
732
            }
733
        }
734

735
        const result = {
1✔
736
            startNodeId: startNodeId,
737
            endNodeId: endNodeId,
738
            startCoord: startCoord,
739
            endCoord: endCoord,
740
            startEdgeId: startEdgeId,
741
            endEdgeId: endEdgeId,
742
            startPolygonId: startPolygonId,
743
            endPolygonId: endPolygonId
744
        };
745

746
        // fire an event when the drawing is complete
747
        const drawSource = me.drawLayer.getSource();
1✔
748
        drawSource.dispatchEvent({ type: 'localdrawend', result: result });
1✔
749
    },
750

751
    handleKeyPress: function (evt) {
752
        const me = this; // bound to the controller in the constructor
×
753

754
        // use DEL to remove last point
755
        if (evt.keyCode == 46) {
×
756
            if (evt.shiftKey === true) {
×
757
                // or set focus just on the map as per https://stackoverflow.com/questions/59453895/add-keyboard-event-to-openlayers-map
758
                // or if the delete key is used in a form it will also remove a point
759
                me.drawInteraction.removeLastPoint();
×
760
            }
761
        }
762

763
        // use ESC to cancel drawing mode
764
        if (evt.keyCode == 27) {
×
765
            me.drawInteraction.finishDrawing();
×
766
        }
767
    },
768

769
    /**
770
     * Main handler which activates or deactivates the interactions and listeners
771
     * @param {Ext.button.Button} btn The button that has been pressed
772
     * @param {boolean} pressed The toggle state of the button
773
     */
774
    onToggle: function (btn, pressed) {
775
        const me = this;
×
776
        const view = me.getView();
×
777

778
        // guess the map if not given
779
        if (!me.map) {
×
780
            me.map = BasiGX.util.Map.getMapComponent().map;
×
781
        }
782

783
        // use draw layer set in the view
784
        //<debug>
785
        Ext.Assert.truthy(view.drawLayer);
×
786
        //</debug>
787
        if (!me.drawLayer) {
×
788
            if (view.drawLayer) {
×
789
                me.drawLayer = view.drawLayer;
×
790
            }
791
        }
792

793
        if (!me.defaultDrawStyle) {
×
794
            me.prepareDrawingStyles();
×
795
            // set initial style for drawing features
796
            me.defaultDrawStyle = [
×
797
                view.getDrawBeforeEditingPoint(),
798
                view.getDrawStyleStartPoint(),
799
                view.getDrawStyleLine(),
800
                view.getDrawStyleEndPoint()
801
            ];
802
            me.drawLayer.setStyle(me.defaultDrawStyle);
×
803

804
            me.setDrawInteraction(me.drawLayer);
×
805
            me.setModifyInteraction(me.drawLayer);
×
806
            me.setSnapInteraction(me.drawLayer);
×
807
        }
808

809
        const viewPort = me.map.getViewport();
×
810

811
        if (pressed) {
×
812
            const tracingLayerKeys = view.getTracingLayerKeys();
×
813

814
            me.initTracing(tracingLayerKeys, me.drawInteraction);
×
815
            me.drawInteraction.setActive(true);
×
816
            me.modifyInteraction.setActive(true);
×
817
            me.snapInteraction.setActive(true);
×
818
            viewPort.addEventListener('contextmenu', me.contextHandler);
×
819
            document.addEventListener('keydown', me.handleKeyPress);
×
820
        } else {
821
            me.cleanupTracing();
×
822
            me.drawInteraction.setActive(false);
×
823
            me.modifyInteraction.setActive(false);
×
824
            me.snapInteraction.setActive(false);
×
825
            viewPort.removeEventListener('contextmenu', me.contextHandler);
×
826
            document.removeEventListener('keydown', me.handleKeyPress);
×
827
        }
828
    },
829

830
    /**
831
     * Called when new tracing coordinates are available.
832
     *
833
     * @param {ol.coordinate.Coordinate[]} appendCoords The new coordinates
834
     */
835
    handleTracingResult: function (appendCoords) {
836
        const me = this;
×
837
        me.drawInteraction.removeLastPoint();
×
838
        me.drawInteraction.appendCoordinates(appendCoords);
×
839
    },
840

841
    /**
842
     * Method shows the context menu on mouse right click
843
     * @param {Event} evt The browser event
844
     */
845
    showContextMenu: function (evt) {
846
        // suppress default browser behaviour
847
        evt.preventDefault();
×
848

849
        const me = this.scope;
×
850

851
        const menuItems = [
×
852
            {
853
                text: 'Clear All',
854
                handler: function () {
855
                    try {
×
856
                        me.drawLayer
×
857
                            .getSource()
858
                            .getFeaturesCollection()
859
                            .clear();
860
                    } catch (error) {
861
                        // sometimes get an error here when trying to clear the features collection
862
                        // Cannot read properties of null (reading 'findIndexBy')
863
                        // TODO debug - seems to occur after the layer is reloaded, so we may need to
864
                        // update the collection on reload? the source still has the same ol_uid
865
                        Ext.log.error(error);
×
866
                    }
867
                }
868
            }
869
        ];
870

871
        const menu = Ext.create('Ext.menu.Menu', {
×
872
            width: 100,
873
            plain: true,
874
            renderTo: Ext.getBody(),
875
            items: menuItems
876
        });
877
        menu.showAt(evt.pageX, evt.pageY);
×
878
    },
879

880
    /**
881
     * Remove the interaction when this component gets destroyed
882
     */
883
    onBeforeDestroy: function () {
884
        const me = this;
×
885
        const btn = me.getView();
×
886

887
        // detoggle button
888
        me.onToggle(btn, false);
×
889

890
        // fire the button's toggle event so that the defaultClickEnabled property
891
        // is updated in CpsiMapview.util.ApplicationMixin to re-enable clicks
892
        btn.pressed = false;
×
893
        btn.fireEvent('toggle');
×
894

895
        if (me.drawInteraction) {
×
896
            me.map.removeInteraction(me.drawInteraction);
×
897
        }
898

899
        if (me.modifyInteraction) {
×
900
            me.map.removeInteraction(me.modifyInteraction);
×
901
        }
902

903
        if (me.snapInteraction) {
×
904
            me.map.removeInteraction(me.snapInteraction);
×
905
        }
906

907
        if (me.drawLayer) {
×
908
            me.map.removeLayer(me.drawLayer);
×
909
        }
910

911
        me.unBindLayerListeners();
×
912
        me.cleanupTracing();
×
913
    },
914

915
    /**
916
     * Remove event listeners by key, for each key in the listenerKeys array
917
     *
918
     */
919
    unBindLayerListeners: function () {
920
        Ext.Array.each(this.listenerKeys, function (key) {
11✔
921
            ol.Observable.unByKey(key);
×
922
        });
923
        this.listenerKeys = [];
11✔
924
    },
925

926
    init: function () {
927
        const me = this;
17✔
928

929
        // create an object for the contextmenu eventhandler
930
        // so it can be removed correctly
931
        me.contextHandler = {
17✔
932
            handleEvent: me.showContextMenu,
933
            scope: me
934
        };
935
    }
936
});
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