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

geographika / cpsi-mapview / 6431635665

06 Oct 2023 12:30PM UTC coverage: 24.101% (+3.7%) from 20.445%
6431635665

push

github

geographika
Snap to the ends of lines if nodeIds are properties of the line

446 of 2300 branches covered (0.0%)

Branch coverage included in aggregate %.

49 of 49 new or added lines in 1 file covered. (100.0%)

1309 of 4982 relevant lines covered (26.27%)

1.04 hits per line

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

37.12
/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: [
18
        'CpsiMapview.controller.button.TracingMixin'
19
    ],
20

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

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

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

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

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

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

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

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

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

67
    constructor: function () {
68
        var me = this;
16✔
69
        me.handleDrawStart = me.handleDrawStart.bind(me);
16✔
70
        me.handleDrawEnd = me.handleDrawEnd.bind(me);
16✔
71
        me.handleModifyEnd = me.handleModifyEnd.bind(me);
16✔
72
        me.handleKeyPress = me.handleKeyPress.bind(me);
16✔
73
        me.callParent(arguments);
16✔
74
    },
75

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

84
        if (!me.map) {
×
85
            return;
×
86
        }
87

88
        if (me.drawLayer) {
×
89
            me.map.removeLayer(me.drawLayer);
×
90
        }
91

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

98
    /**
99
     * Set the map drawing interaction
100
     * which will allow features to be added to the drawLayer
101
     * @param {any} layer
102
     */
103
    setDrawInteraction: function (layer) {
104

105
        var me = this;
×
106
        var view = me.getView();
×
107

108
        if (me.drawInteraction) {
×
109
            me.map.removeInteraction(me.drawInteraction);
×
110
        }
111

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

117
        var source = layer.getSource();
×
118
        var collection = source.getFeaturesCollection();
×
119
        var 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
        var me = this;
×
139
        var view = me.getView();
×
140

141
        // ensure styles are applied at right conditions
142
        view.getDrawBeforeEditingPoint().setGeometry(function (feature) {
×
143
            var geom = feature.getGeometry();
×
144
            if (!me.currentlyDrawing) {
×
145
                return geom;
×
146
            }
147
        });
148
        view.getDrawStyleStartPoint().setGeometry(function (feature) {
×
149
            var geom = feature.getGeometry();
×
150
            var coords = geom.getCoordinates();
×
151
            var firstCoord = coords[0];
×
152
            return new ol.geom.Point(firstCoord);
×
153
        });
154
        view.getDrawStyleEndPoint().setGeometry(function (feature) {
×
155
            var coords = feature.getGeometry().getCoordinates();
×
156
            if (coords.length > 1) {
×
157
                var 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
        var me = this;
×
174
        var view = me.getView();
×
175

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

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

182
            var node, edge, polygon, self;
183

184
            me.map.forEachFeatureAtPixel(pixel, function (foundFeature, layer) {
×
185
                if (layer) {
×
186
                    var 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
                    var geom = edge.getGeometry();
×
208
                    var coords = [];
×
209
                    if (geom.getType() === 'MultiLineString') {
×
210
                        // use all vertices of containing LineStrings
211
                        var lineStrings = geom.getLineStrings();
×
212
                        Ext.each(lineStrings, function (lineString) {
×
213
                            var lineStringCoords = lineString.getCoordinates();
×
214
                            coords = coords.concat(lineStringCoords);
×
215
                        });
216
                    } else {
217
                        coords = geom.getCoordinates();
×
218
                    }
219
                    var verticesMultiPoint = new ol.geom.MultiPoint(coords);
×
220
                    var snappedEdgeVertexStyle = view.getSnappedEdgeVertexStyle().clone();
×
221
                    snappedEdgeVertexStyle.setGeometry(verticesMultiPoint);
×
222

223
                    // combine style for snapped point and vertices of snapped edge
224
                    return [
×
225
                        snappedEdgeVertexStyle,
226
                        view.getSnappedEdgeStyle()
227
                    ];
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

250
        var me = this;
×
251

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

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

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

274
    },
275

276
    /**
277
     * Set the snap interaction used to snap to features
278
     * @param {any} layer
279
     */
280
    setSnapInteraction: function (drawLayer) {
281

282
        var me = this;
11✔
283

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

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

291
        var snapCollection = new ol.Collection([], {
11✔
292
            unique: true
293
        });
294

295
        var fc = drawLayer.getSource().getFeaturesCollection();
11✔
296

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

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

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

316
        // Checks if a feature exists in layers other than the current layer
317
        var isFeatureInOtherLayers = function (allLayers, currentLayer, feature) {
11✔
318
            var found = false;
4✔
319
            Ext.Array.each(allLayers, function(layer) {
4✔
320
                if(layer !== currentLayer) {
8✔
321
                    if(layer.getSource().hasFeature(feature)) {
4✔
322
                        found = true;
2✔
323
                    }
324
                }
325
            });
326
            return found;
4✔
327
        };
328

329
        // get the layers to snap to
330
        var view = me.getView();
11✔
331
        var layerKeys = view.getSnappingLayerKeys();
11✔
332
        var allowSnapToHiddenFeatures = view.getAllowSnapToHiddenFeatures();
11✔
333
        var layers = Ext.Array.map(layerKeys, function (key) {
11✔
334
            return BasiGX.util.Layer.getLayersBy('layerKey', key)[0];
16✔
335
        });
336

337
        Ext.Array.each(layers, function (layer) {
11✔
338
            var feats = layer.getSource().getFeatures(); // these are standard WFS layers so we use getSource without getFeaturesCollection here
16✔
339
            // add inital features to the snap collection, if the layer is visible
340
            // or if allowSnapToHiddenFeatures is enabled
341
            if (layer.getVisible() || allowSnapToHiddenFeatures) {
16✔
342
                addUniqueFeaturesToCollection(snapCollection, feats);
14✔
343
            }
344

345
            // Update the snapCollection on addfeature or removefeature
346
            var addFeatureKey = layer.getSource().on('addfeature', function (evt) {
16✔
347
                if (layer.getVisible() || allowSnapToHiddenFeatures) {
1!
348
                    addUniqueFeaturesToCollection(snapCollection, [evt.feature]);
1✔
349
                }
350
            });
351

352
            var removefeatureKey = layer.getSource().on('removefeature', function (evt) {
16✔
353
                if (!isFeatureInOtherLayers(layers, layer, evt.feature)) {
2✔
354
                    snapCollection.remove(evt.feature);
1✔
355
                }
356
            });
357

358
            // Update the snapCollection on layer visibility change
359
            // only handle layer visible change event if snapping to hidden features is disabled
360
            if (!allowSnapToHiddenFeatures) {
16✔
361
                var changeVisibleKey = layer.on('change:visible', function () {
14✔
362
                    var features = layer.getSource().getFeatures();
2✔
363
                    if (layer.getVisible()) {
2✔
364
                        addUniqueFeaturesToCollection(snapCollection, features);
1✔
365
                    } else {
366
                        Ext.Array.each(features, function (f) {
1✔
367
                            if (!isFeatureInOtherLayers(layers, layer, f)) {
2✔
368
                                snapCollection.remove(f);
1✔
369
                            }
370
                        });
371
                    }
372
                });
373
            }
374

375
            me.listenerKeys.push(addFeatureKey, removefeatureKey, changeVisibleKey);
16✔
376
        });
377

378
        // vector tile sources cannot be used for snapping as they
379
        // do not provide a getFeatures function
380
        // see https://openlayers.org/en/latest/apidoc/module-ol_source_VectorTile-VectorTile.html
381

382
        me.snapInteraction = new ol.interaction.Snap({
11✔
383
            features: snapCollection
384
        });
385
        me.map.addInteraction(me.snapInteraction);
11✔
386

387
    },
388

389
    getSnappedEdge: function (coord, searchLayer) {
390

391
        var me = this;
7✔
392
        var extent = me.getBufferedCoordExtent(coord);
7✔
393

394
        var features = [];
7✔
395
        // find all intersecting edges
396
        // https://openlayers.org/en/latest/apidoc/module-ol_source_Vector-VectorSource.html
397
        searchLayer.getSource().forEachFeatureIntersectingExtent(extent, function (feat) {
7✔
398
            features.push(feat);
8✔
399
        });
400

401
        if (features.length > 0) {
7✔
402
            return features[0];
4✔
403
        } else {
404
            return null;
3✔
405
        }
406
    },
407

408
    getBufferedCoordExtent: function (coord) {
409

410
        var me = this;
14✔
411
        var extent = ol.extent.boundingExtent([coord]); // still a single point
14✔
412
        var buffer = me.map.getView().getResolution() * 3; // use a 3 pixel tolerance for snapping
14✔
413
        return ol.extent.buffer(extent, buffer); // buffer the point as it may have snapped to a different feature than the nodes/edges
14✔
414

415
    },
416

417
    getSnappedFeatureId: function (coord, searchLayer) {
418

419
        var me = this;
1✔
420
        var feat = me.getSnappedEdge(coord, searchLayer);
1✔
421

422
        if (feat) {
1!
423
            // this requires all GeoJSON features used for the layer to have an id property
424
            //<debug>
425
            Ext.Assert.truthy(feat.getId());
×
426
            //</debug>
427
            return feat.getId();
×
428
        }
429

430
        return null;
1✔
431
    },
432

433
    /**
434
     * Rather than simply creating a new feature each time, attempt to
435
     * merge contiguous linestrings together if the end of the old line
436
     * matches the start of the new line
437
     * @param {any} origGeom
438
     * @param {any} newGeom
439
     */
440
    mergeLineStrings: function (origGeom, newGeom) {
441
        var newGeomFirstCoord = newGeom.getFirstCoordinate();
×
442
        var matchesFirstCoord = Ext.Array.equals(origGeom.getFirstCoordinate(), newGeomFirstCoord);
×
443
        var matchesLastCoord = Ext.Array.equals(origGeom.getLastCoordinate(), newGeomFirstCoord);
×
444

445
        if (matchesFirstCoord || matchesLastCoord) {
×
446
            var origCoords = origGeom.getCoordinates();
×
447
            // if drawing in continued from the start point of the original,
448
            // the original needs to be reversed to we end up with correct
449
            // start and end points
450
            if (matchesFirstCoord) {
×
451
                origCoords.reverse();
×
452
            }
453
            var newCoords = newGeom.getCoordinates();
×
454
            newGeom.setCoordinates(origCoords.concat(newCoords));
×
455
        } else {
456
            Ext.log('Start / End coordinates differ');
×
457
            Ext.log('origGeom start/end coords: ', origGeom.getFirstCoordinate(), origGeom.getLastCoordinate());
×
458
            Ext.log('newGeom start coord: ', newGeom.getFirstCoordinate());
×
459
        }
460

461
        return newGeom;
×
462
    },
463

464
    /**
465
     * Handles the drawstart event
466
     */
467
    handleDrawStart: function () {
468
        var me = this;
×
469
        me.currentlyDrawing = true;
×
470
    },
471

472
    /**
473
     * Handles the drawend event
474
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
475
     */
476
    handleDrawEnd: function (evt) {
477
        var me = this;
×
478
        var feature = evt.feature;
×
479
        var newGeom = feature.getGeometry();
×
480

481
        var drawSource = me.drawLayer.getSource();
×
482
        var currentFeature = drawSource.getFeaturesCollection().item(0);
×
483

484
        if (currentFeature) {
×
485
            // merge all linestrings to a single linestring
486
            // this is done in place
487
            me.mergeLineStrings(currentFeature.getGeometry(), newGeom);
×
488
        }
489

490
        me.calculateLineIntersections(feature);
×
491

492
        // clear all previous features so only the last drawn feature remains
493
        drawSource.getFeaturesCollection().clear();
×
494

495
        me.currentlyDrawing = false;
×
496
    },
497

498
    /**
499
    * Handles the modifyend event
500
    * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
501
    */
502
    handleModifyEnd: function (evt) {
503
        var me = this;
×
504
        var feature = evt.features.item(0);
×
505
        me.calculateLineIntersections(feature);
×
506
    },
507

508
    /**
509
     * Get a nodeId from the closest edge to the input coord
510
     * If both ends of the edge are within the snap tolerance of the
511
     * input coord then the node closest to the input point is used
512
     * @param {any} edgesLayer
513
     * @param {any} edgeLayerConfig
514
     * @param {any} coord
515
     * @returns
516
     */
517
    getNodeIdFromSnappedEdge: function (edgesLayer, edgeLayerConfig, coord) {
518

519
        var me = this;
4✔
520
        var nodeId;
521

522
        // check to see if the coord snaps to the start of any edges
523
        var edge = me.getSnappedEdge(coord, edgesLayer);
4✔
524
        if (!edge) {
4✔
525
            return null;
1✔
526
        }
527

528
        var inputPoint = new ol.geom.Point(coord);
3✔
529

530
        var startCoord = edge.getGeometry().getFirstCoordinate();
3✔
531
        var startExtent = me.getBufferedCoordExtent(startCoord);
3✔
532
        var startDistance;
533

534
        if (inputPoint.intersectsExtent(startExtent)) {
3!
535
            nodeId = edge.get(edgeLayerConfig.startNodeProperty);
3✔
536
            startDistance = new ol.geom.LineString([coord, startCoord]).getLength();
3✔
537
        }
538

539
        var endCoord = edge.getGeometry().getLastCoordinate();
3✔
540
        var endExtent = me.getBufferedCoordExtent(endCoord);
3✔
541

542
        if (inputPoint.intersectsExtent(endExtent)) {
3!
543

544
            if (startDistance) {
3✔
545
                var endDistance = new ol.geom.LineString([coord, endCoord]).getLength();
2✔
546
                if (endDistance < startDistance) {
2!
547
                    nodeId = edge.get(edgeLayerConfig.endNodeProperty);
2✔
548
                }
549
            }
550
        }
551

552
        // if an edge has been found then it should snap at the start or the end
553
        // and we should not end up here
554

555
        //<debug>
556
        if (!nodeId) {
3!
557
            Ext.log.warn('A coordinate snapped to an edge, but no nodeId was found. Check the edgeLayerConfig');
×
558
        }
559
        //</debug>
560

561
        return nodeId;
3✔
562

563
    },
564

565
    /**
566
     * Calculate where the geometry intersects other parts of the network
567
     * @param {any} newGeom
568
     */
569
    calculateLineIntersections: function (feature) {
570

571
        var me = this;
1✔
572
        var view = me.getView();
1✔
573

574
        var newGeom = feature.getGeometry();
1✔
575

576
        var startCoord = newGeom.getFirstCoordinate();
1✔
577
        var endCoord = newGeom.getLastCoordinate();
1✔
578

579
        var foundFeatAtStart = false;
1✔
580
        var foundFeatAtEnd = false;
1✔
581

582
        // get any nodes that the line snaps to
583
        var nodeLayerKey = view.getNodeLayerKey();
1✔
584
        var startNodeId = null;
1✔
585
        var endNodeId = null;
1✔
586

587
        if (nodeLayerKey) {
1!
588
            var nodeLayer = BasiGX.util.Layer.getLayersBy('layerKey', nodeLayerKey)[0];
×
589
            startNodeId = me.getSnappedFeatureId(startCoord, nodeLayer);
×
590
            endNodeId = me.getSnappedFeatureId(endCoord, nodeLayer);
×
591

592
            foundFeatAtStart = startNodeId ? true : false;
×
593
            foundFeatAtEnd = endNodeId ? true : false;
×
594
        }
595

596
        var edgeLayerKey = view.getEdgeLayerKey();
1✔
597
        var edgesLayer;
598

599
        if (edgeLayerKey) {
1!
600
            edgesLayer = BasiGX.util.Layer.getLayersBy('layerKey', edgeLayerKey)[0];
1✔
601
        }
602

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

606
        var edgeLayerConfig = view.getEdgeLayerConfig();
1✔
607

608
        if (edgesLayer && edgeLayerConfig) {
1!
609
            var nodeId;
610
            nodeId = me.getNodeIdFromSnappedEdge(edgesLayer, edgeLayerConfig, startCoord);
1✔
611

612
            if (nodeId) {
1!
613
                startNodeId = nodeId;
1✔
614
                foundFeatAtStart = true;
1✔
615
            }
616

617
            nodeId = me.getNodeIdFromSnappedEdge(edgesLayer, edgeLayerConfig, endCoord);
1✔
618

619
            if (nodeId) {
1!
620
                endNodeId = nodeId;
×
621
                foundFeatAtEnd = true;
×
622
            }
623
        }
624

625
        // now check for any edges at both ends, but only in the case
626
        // where there are no start and end nodes
627

628

629
        var startEdgeId = null;
1✔
630
        var endEdgeId = null;
1✔
631

632
        if (edgesLayer) {
1!
633

634
            if (!foundFeatAtStart) {
1!
635
                startEdgeId = me.getSnappedFeatureId(startCoord, edgesLayer);
×
636
                foundFeatAtStart = startEdgeId ? true : false;
×
637
            }
638

639
            if (!foundFeatAtEnd) {
1!
640
                endEdgeId = me.getSnappedFeatureId(endCoord, edgesLayer);
1✔
641
                foundFeatAtEnd = endEdgeId ? true : false;
1!
642
            }
643
        }
644

645
        // finally we will check if we have snapped to a polygon edge
646
        // this will allow us to create continua based on points around the polygon edge
647

648
        var polygonLayerKey = view.getPolygonLayerKey();
1✔
649
        var startPolygonId = null;
1✔
650
        var endPolygonId = null;
1✔
651

652
        if (polygonLayerKey) {
1!
653
            var polygonsLayer = BasiGX.util.Layer.getLayersBy('layerKey', polygonLayerKey)[0];
×
654

655
            if (!foundFeatAtStart) {
×
656
                startPolygonId = me.getSnappedFeatureId(startCoord, polygonsLayer);
×
657
            }
658

659
            if (!foundFeatAtEnd) {
×
660
                endPolygonId = me.getSnappedFeatureId(endCoord, polygonsLayer);
×
661
            }
662
        }
663

664
        var result = {
1✔
665
            startNodeId: startNodeId,
666
            endNodeId: endNodeId,
667
            startCoord: startCoord,
668
            endCoord: endCoord,
669
            startEdgeId: startEdgeId,
670
            endEdgeId: endEdgeId,
671
            startPolygonId: startPolygonId,
672
            endPolygonId: endPolygonId
673
        };
674

675
        // set the node ids on the edge feature itself
676
        // as these can be used by a polygon tool / grid
677
        // the "magic" number -2 indicates a new node should be created
678
        // for the line, rather than snapping to an existing node
679
        feature.set('startNodeId', startNodeId ? startNodeId : -2);
1!
680
        feature.set('endNodeId', endNodeId ? endNodeId : -2);
1!
681

682
        // fire an event when the drawing is complete
683
        var drawSource = me.drawLayer.getSource();
1✔
684
        drawSource.dispatchEvent({ type: 'localdrawend', result: result });
1✔
685
    },
686

687
    handleKeyPress: function (evt) {
688

689
        var me = this; // bound to the controller in the constructor
×
690

691
        // use DEL to remove last point
692
        if (evt.keyCode == 46) {
×
693
            if (evt.shiftKey === true) {
×
694
                // or set focus just on the map as per https://stackoverflow.com/questions/59453895/add-keyboard-event-to-openlayers-map
695
                // or if the delete key is used in a form it will also remove a point
696
                me.drawInteraction.removeLastPoint();
×
697
            }
698
        }
699

700
        // use ESC to cancel drawing mode
701
        if (evt.keyCode == 27) {
×
702
            me.drawInteraction.finishDrawing();
×
703
        }
704
    },
705

706
    /**
707
     * Main handler which activates or deactivates the interactions and listeners
708
     * @param {Ext.button.Button} btn The button that has been pressed
709
     * @param {boolean} pressed The toggle state of the button
710
     */
711
    onToggle: function (btn, pressed) {
712
        var me = this;
×
713
        var view = me.getView();
×
714

715
        // guess the map if not given
716
        if (!me.map) {
×
717
            me.map = BasiGX.util.Map.getMapComponent().map;
×
718
        }
719

720
        // use draw layer set in the view
721
        //<debug>
722
        Ext.Assert.truthy(view.drawLayer);
×
723
        //</debug>
724
        if (!me.drawLayer) {
×
725
            if (view.drawLayer) {
×
726
                me.drawLayer = view.drawLayer;
×
727
            }
728
        }
729

730
        me.prepareDrawingStyles();
×
731

732
        // set initial style for drawing features
733
        me.defaultDrawStyle = [
×
734
            view.getDrawBeforeEditingPoint(),
735
            view.getDrawStyleStartPoint(),
736
            view.getDrawStyleLine(),
737
            view.getDrawStyleEndPoint(),
738
        ];
739
        me.drawLayer.setStyle(me.defaultDrawStyle);
×
740

741
        me.setDrawInteraction(me.drawLayer);
×
742
        me.setModifyInteraction(me.drawLayer);
×
743
        me.setSnapInteraction(me.drawLayer);
×
744

745
        var viewPort = me.map.getViewport();
×
746

747
        var tracingLayerKeys = view.getTracingLayerKeys();
×
748

749
        if (pressed) {
×
750

751
            me.initTracing(
×
752
                tracingLayerKeys,
753
                me.drawInteraction
754
            );
755
            me.drawInteraction.setActive(true);
×
756
            me.modifyInteraction.setActive(true);
×
757
            me.snapInteraction.setActive(true);
×
758
            viewPort.addEventListener('contextmenu', me.contextHandler);
×
759
            document.addEventListener('keydown', me.handleKeyPress);
×
760
        } else {
761
            me.cleanupTracing();
×
762
            me.drawInteraction.setActive(false);
×
763
            me.modifyInteraction.setActive(false);
×
764
            me.snapInteraction.setActive(false);
×
765
            viewPort.removeEventListener('contextmenu', me.contextHandler);
×
766
            document.removeEventListener('keydown', me.handleKeyPress);
×
767
        }
768
    },
769

770
    /**
771
     * Called when new tracing coordinates are available.
772
     *
773
     * @param {ol.coordinate.Coordinate[]} appendCoords The new coordinates
774
     */
775
    handleTracingResult: function (appendCoords) {
776
        var me = this;
×
777
        me.drawInteraction.removeLastPoint();
×
778
        me.drawInteraction.appendCoordinates(appendCoords);
×
779
    },
780

781
    /**
782
     * Method shows the context menu on mouse right click
783
     * @param {Event} evt The browser event
784
     */
785
    showContextMenu: function (evt) {
786
        // suppress default browser behaviour
787
        evt.preventDefault();
×
788

789
        var me = this.scope;
×
790

791
        var menuItems = [{
×
792
            text: 'Clear All',
793
            handler: function () {
794
                try {
×
795
                    me.drawLayer.getSource().getFeaturesCollection().clear();
×
796
                } catch (error) {
797
                    // sometimes get an error here when trying to clear the features collection
798
                    // Cannot read properties of null (reading 'findIndexBy')
799
                    // TODO debug - seems to occur after the layer is reloaded, so we may need to
800
                    // update the collection on reload? the source still has the same ol_uid
801
                    Ext.log.error(error);
×
802
                }
803
            }
804
        }];
805

806
        var menu = Ext.create('Ext.menu.Menu', {
×
807
            width: 100,
808
            plain: true,
809
            renderTo: Ext.getBody(),
810
            items: menuItems
811
        });
812
        menu.showAt(evt.pageX, evt.pageY);
×
813
    },
814

815
    /**
816
     * Remove the interaction when this component gets destroyed
817
     */
818
    onBeforeDestroy: function () {
819

820
        var me = this;
×
821
        var btn = me.getView();
×
822

823
        // detoggle button
824
        me.onToggle(btn, false);
×
825

826
        // fire the button's toggle event so that the defaultClickEnabled property
827
        // is updated in CpsiMapview.util.ApplicationMixin to re-enable clicks
828
        btn.pressed = false;
×
829
        btn.fireEvent('toggle');
×
830

831

832
        if (me.drawInteraction) {
×
833
            me.map.removeInteraction(me.drawInteraction);
×
834
        }
835

836
        if (me.modifyInteraction) {
×
837
            me.map.removeInteraction(me.modifyInteraction);
×
838
        }
839

840
        if (me.snapInteraction) {
×
841
            me.map.removeInteraction(me.snapInteraction);
×
842
        }
843

844
        if (me.drawLayer) {
×
845
            me.map.removeLayer(me.drawLayer);
×
846
        }
847

848
        me.unBindLayerListeners();
×
849
        me.cleanupTracing();
×
850
    },
851

852
    /**
853
     * Remove event listeners by key, for each key in the listenerKeys array
854
     *
855
     */
856
    unBindLayerListeners: function () {
857
        Ext.Array.each(this.listenerKeys, function (key) {
11✔
858
            ol.Observable.unByKey(key);
×
859
        });
860
        this.listenerKeys = [];
11✔
861
    },
862

863
    init: function () {
864

865
        var me = this;
15✔
866

867
        // create an object for the contextmenu eventhandler
868
        // so it can be removed correctly
869
        me.contextHandler = {
15✔
870
            handleEvent: me.showContextMenu,
871
            scope: me
872
        };
873
    }
874
});
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