• 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

0.44
/app/controller/button/TracingMixin.js
1
/**
2
 * A mixin to add tracing functionality to a drawing tool.
3
 *
4
 * The basic functionality is taken from the official OpenLayers example:
5
 * https://openlayers.org/en/latest/examples/tracing.html
6
 *
7
 * @class CpsiMapview.controller.button.TracingMixin
8
 */
9
Ext.define('CpsiMapview.controller.button.TracingMixin', {
1✔
10
    extend: 'Ext.Mixin',
11

12
    requires: ['BasiGX.util.Layer', 'CpsiMapview.util.Tracing'],
13

14
    /**
15
     * The style of the preview line during tracing.
16
     */
17
    previewStyle: new ol.style.Style({
18
        stroke: new ol.style.Stroke({
19
            color: 'rgba(255, 0, 0, 1)',
20
            width: 10
21
        })
22
    }),
23

24
    /**
25
     * Used to store the position of the last mouse coord, taking into account snapping
26
     */
27
    lastSnappedCoord: null,
28

29
    /**
30
     * Interaction to track and populate lastSnappedCoord
31
     */
32
    getSnapCoordinateInteraction: null,
33

34
    /**
35
     * Enhances drawing functionality by adding tracing to it.
36
     *
37
     * @param {String[]} tracingLayerKeys The keys of the layers to trace
38
     * @param {ol.interaction.Draw} drawInteraction draw interaction to attach the tracing on
39
     * @param {Boolean} [showTraceableEdges=false] If the traceable edges shall be shown (useful for debugging)
40
     */
41
    initTracing: function (
42
        tracingLayerKeys,
43
        drawInteraction,
44
        showTraceableEdges
45
    ) {
46
        const me = this;
×
47

48
        if (me.getView()) {
×
49
            me.getView().fireEvent('tracingstart');
×
50
        }
51

52
        if (!drawInteraction) {
×
53
            return;
×
54
        }
55

56
        me.tracingDrawInteraction = drawInteraction;
×
57

58
        me.onTracingDrawStart = me.onTracingDrawStart.bind(this);
×
59
        me.onTracingDrawEnd = me.onTracingDrawEnd.bind(this);
×
60

61
        me.tracingActive = false;
×
62
        me.tracingDrawInteraction.on('drawstart', me.onTracingDrawStart);
×
63
        me.tracingDrawInteraction.on('drawend', me.onTracingDrawEnd);
×
64
        me.trackSnappedCoords();
×
65

66
        if (!tracingLayerKeys) {
×
67
            return;
×
68
        }
69
        // get tracing layers
70
        me.tracingLayers = [];
×
71
        Ext.each(tracingLayerKeys, function (key) {
×
72
            const foundLayers = BasiGX.util.Layer.getLayersBy('layerKey', key);
×
73
            if (foundLayers.length > 0) {
×
74
                me.tracingLayers.push(foundLayers[0]);
×
75
            }
76
        });
77

78
        // the options for the eachFeature functions
79
        me.forEachFeatureOptions = {
×
80
            hitTolerance: 10,
81
            layerFilter: function (layer) {
82
                return Ext.Array.contains(me.tracingLayers, layer);
×
83
            }
84
        };
85

86
        me.tracingFeature = new ol.Feature({
×
87
            geometry: new ol.geom.LineString([])
88
        });
89
        me.tracingFeatureArray = [];
×
90
        me.tracingStartPoint = null;
×
91
        me.tracingEndPoint = null;
×
92

93
        // For debugging the traceable edges can be displayed
94
        me.tracingVector = new ol.layer.Vector({
×
95
            source: new ol.source.Vector({
96
                features: [me.tracingFeature]
97
            }),
98
            style: new ol.style.Style({
99
                stroke: new ol.style.Stroke({
100
                    color: [255, 255, 0, 0.5],
101
                    width: 25
102
                })
103
            })
104
        });
105
        if (showTraceableEdges) {
×
106
            me.map.addLayer(me.tracingVector);
×
107
        }
108

109
        // the tracing util
110
        me.tracingUtil = CpsiMapview.util.Tracing;
×
111

112
        // the visible tracing line while editing
113
        me.previewLine = new ol.Feature({
×
114
            geometry: new ol.geom.LineString([])
115
        });
116

117
        me.previewVector = new ol.layer.Vector({
×
118
            source: new ol.source.Vector({
119
                features: [me.previewLine]
120
            }),
121
            style: me.previewStyle
122
        });
123
        me.map.addLayer(me.previewVector);
×
124

125
        // bind scope to listener functions
126
        me.onTracingPointerMove = me.onTracingPointerMove.bind(me);
×
127
        me.onTracingMapClick = me.onTracingMapClick.bind(me);
×
128

129
        me.map.on('click', me.onTracingMapClick);
×
130
        me.map.on('pointermove', me.onTracingPointerMove);
×
131
    },
132

133
    /**
134
     * Sets the variable 'me.tracingActive' to true.
135
     */
136
    onTracingDrawStart: function () {
137
        const me = this;
×
138
        me.tracingActive = true;
×
139
    },
140

141
    /**
142
     * Sets the variable 'me.tracingActive' to false.
143
     */
144
    onTracingDrawEnd: function () {
145
        const me = this;
×
146
        me.tracingActive = false;
×
147
        // Clear previous tracingFeature data
148
        me.tracingFeature.getGeometry().setCoordinates([]);
×
149
        me.tracingFeatureArray = [];
×
150
        me.previewLine.getGeometry().setCoordinates([]);
×
151
    },
152

153
    /**
154
     * Remove listeners and layers
155
     */
156
    cleanupTracing: function () {
157
        const me = this;
×
158

159
        // nothing to cleanup if tracing has not been initialised
160
        if (!me.tracingDrawInteraction) {
×
161
            return;
×
162
        }
163

164
        me.map.removeLayer(me.previewVector);
×
165
        me.map.removeLayer(me.tracingVector);
×
166
        me.map.un('click', me.onTracingMapClick);
×
167
        me.map.un('pointermove', me.onTracingPointerMove);
×
168
        me.tracingDrawInteraction.un('drawstart', me.onTracingDrawStart);
×
169
        me.tracingDrawInteraction.un('drawend', me.onTracingDrawEnd);
×
170
        me.map.removeInteraction(me.getSnapCoordinateInteraction);
×
171
    },
172

173
    /**
174
     * Add a generic Interaction to the map before the last Snap interaction
175
     * So that we can collected the coordinates of the latest snapped edge/vertex/node
176
     * The new Interaction needs to be before the last Snap interaction so that the
177
     * Snap interaction modifies the coordinates to the snapped edge/vertex/node
178
     * And passes them down to the next interaction
179
     */
180
    trackSnappedCoords: function () {
181
        const me = this;
×
182
        const interactions = me.map.getInteractions();
×
183
        let lastSnapInteractionIndex;
184

185
        me.getSnapCoordinateInteraction = new ol.interaction.Interaction({
×
186
            handleEvent: function (e) {
187
                me.lastSnappedCoord = e.coordinate;
×
188
                return true;
×
189
            }
190
        });
191

192
        interactions.forEach(function (interaction, i) {
×
193
            if (interaction instanceof ol.interaction.Snap) {
×
194
                lastSnapInteractionIndex = i;
×
195
            }
196
        });
197

198
        interactions.insertAt(
×
199
            lastSnapInteractionIndex,
200
            me.getSnapCoordinateInteraction
201
        );
202
    },
203

204
    /**
205
     * Listen to click to start and end the tracing.
206
     *
207
     * @param {Event} event The OpenLayers click event.
208
     */
209
    onTracingMapClick: function (event) {
210
        const me = this;
×
211

212
        // Ignore the event if drawing is finished
213
        if (!me.tracingActive) {
×
214
            return;
×
215
        }
216

217
        let hit = false;
×
218
        me.map.forEachFeatureAtPixel(
×
219
            event.pixel,
220
            function (feature) {
221
                if (
×
222
                    me.tracingUtil.lineStringPopulated(me.tracingFeature) &&
×
223
                    !me.tracingFeatureArray.includes(feature)
224
                ) {
225
                    return;
×
226
                }
227

228
                hit = true;
×
229

230
                // second click on the tracing feature: append the ring coordinates
231
                if (
×
232
                    me.tracingUtil.lineStringPopulated(me.tracingFeature) &&
×
233
                    me.tracingFeatureArray.includes(feature)
234
                ) {
235
                    me.tracingEndPoint = me.lastSnappedCoord;
×
236
                    const appendCoords = me.tracingUtil.getPartialSegmentCoords(
×
237
                        me.tracingFeature,
238
                        me.tracingStartPoint,
239
                        me.tracingEndPoint
240
                    );
241

242
                    // send coordinates to parent component
243
                    if (me.getView()) {
×
244
                        // NOTE: we transfer the coordinates via an event,
245
                        //       but this mixin has access to the drawInteraction as well,
246
                        //       hence we could directly apply the coordinates to the drawInteraction
247
                        //       without an event
248
                        me.getView().fireEvent('tracingend', appendCoords);
×
249
                    }
250

251
                    me.tracingFeature.getGeometry().setCoordinates([]);
×
252
                    me.tracingFeatureArray = [];
×
253
                    me.previewLine.getGeometry().setCoordinates([]);
×
254
                }
255

256
                const geom = feature.clone().getGeometry();
×
257
                // start tracing on the feature ring
258
                const coords = geom.getCoordinates();
×
259

260
                me.tracingFeature.getGeometry().setCoordinates(coords);
×
261
                me.tracingFeatureArray.push(feature);
×
262
                me.tracingStartPoint = me.lastSnappedCoord;
×
263
            },
264
            me.forEachFeatureOptions
265
        );
266

267
        if (!hit) {
×
268
            // clear current tracing feature & preview
269
            me.previewLine.getGeometry().setCoordinates([]);
×
270
            me.tracingFeature.getGeometry().setCoordinates([]);
×
271
            me.tracingFeatureArray = [];
×
272
        }
273
    },
274

275
    /**
276
     * Create the tracing geometry when pointer is moved.
277
     *
278
     * @param {Event} event The OpenLayers move event
279
     */
280
    onTracingPointerMove: function (event) {
281
        const me = this;
×
282
        const pixel = event.pixel;
×
283

284
        if (
×
285
            me.tracingUtil.lineStringPopulated(me.tracingFeature) &&
×
286
            me.tracingActive
287
        ) {
288
            let coordOnFoundFeature = null;
×
289
            me.map.forEachFeatureAtPixel(
×
290
                pixel,
291
                function (foundFeature) {
292
                    // find coordinate on found feature
293
                    if (me.tracingFeatureArray.includes(foundFeature)) {
×
294
                        coordOnFoundFeature =
×
295
                            me.map.getCoordinateFromPixel(pixel);
296
                    }
297

298
                    if (me.tracingFeatureArray.includes(foundFeature)) {
×
299
                        me.updateTraceableFeature(foundFeature);
×
300
                    } else {
301
                        // new feature found that needs to be added to tracingfeature
302

303
                        const foundGeom = foundFeature.getGeometry();
×
304
                        const tracingGeom = me.tracingFeature.getGeometry();
×
305

306
                        const touchingStartEnd =
307
                            me.tracingUtil.linesTouchAtStartEndPoint(
×
308
                                foundGeom,
309
                                tracingGeom
310
                            );
311

312
                        // TODO: the cases where lines touch at interior points only work in some cases
313
                        //       it might fail in some edge cases, also tracing consecutively on many
314
                        //       features does not work
315
                        const tracingInteriorTouchesFoundFeatureStartEnd =
316
                            me.tracingUtil.lineInteriorTouchesLineStartEnd(
×
317
                                tracingGeom,
318
                                foundGeom
319
                            );
320
                        const tracingStartEndTouchesFoundInterior =
321
                            me.tracingUtil.lineStartEndTouchesLineInterior(
×
322
                                tracingGeom,
323
                                foundGeom
324
                            );
325

326
                        if (touchingStartEnd) {
×
327
                            me.setNewTracingOnStartEndTouch(foundFeature);
×
328
                        } else if (tracingInteriorTouchesFoundFeatureStartEnd) {
×
329
                            me.setNewTracingOnInteriorStartEndTouch(
×
330
                                foundFeature,
331
                                tracingInteriorTouchesFoundFeatureStartEnd
332
                            );
333
                        } else if (tracingStartEndTouchesFoundInterior) {
×
334
                            me.setNewTracingOnStartEndInteriorTouch(
×
335
                                foundFeature,
336
                                tracingStartEndTouchesFoundInterior,
337
                                pixel
338
                            );
339
                        }
340
                    }
341
                },
342
                me.forEachFeatureOptions
343
            );
344

345
            let previewCoords = [];
×
346
            if (coordOnFoundFeature) {
×
347
                me.tracingEndPoint = me.tracingFeature
×
348
                    .getGeometry()
349
                    .getClosestPoint(coordOnFoundFeature);
350
                previewCoords = me.tracingUtil.getPartialSegmentCoords(
×
351
                    me.tracingFeature,
352
                    me.tracingStartPoint,
353
                    me.tracingEndPoint
354
                );
355
            }
356
            me.previewLine.getGeometry().setCoordinates(previewCoords);
×
357
        }
358
    },
359

360
    /**
361
     * Updates the currently active traceable feature.
362
     *
363
     * @param {ol.Feature} foundFeature The hovered feature found in the tracing feature array
364
     */
365
    updateTraceableFeature: function (foundFeature) {
366
        const me = this;
×
367

368
        // check if found feature is last of array
369
        const index = me.tracingFeatureArray.indexOf(foundFeature);
×
370
        if (index !== 0 && (!index || index === -1)) {
×
371
            Ext.Logger.error(
×
372
                'The found feature must be in the found features array.'
373
            );
374
            return;
×
375
        }
376
        const notLastFeature = index !== me.tracingFeatureArray.length - 1;
×
377

378
        if (notLastFeature) {
×
379
            // remove all features after hovered feature
380
            me.tracingFeatureArray = me.tracingFeatureArray.slice(0, index + 1);
×
381
            // update coordinates of tracingFeature
382
            let updatedCoords = [];
×
383
            Ext.each(me.tracingFeatureArray, function (f) {
×
384
                const geom = f.getGeometry();
×
385
                const coords = geom.getCoordinates();
×
386

387
                if (updatedCoords && updatedCoords.length === 0) {
×
388
                    updatedCoords = coords;
×
389
                } else {
390
                    updatedCoords = me.tracingUtil.concatLineCoords(
×
391
                        updatedCoords,
392
                        coords
393
                    );
394
                }
395
            });
396

397
            // concatLineCoords can return undefined if lines do not touch
398
            if (updatedCoords) {
×
399
                me.tracingFeature.getGeometry().setCoordinates(updatedCoords);
×
400
            }
401
        }
402
    },
403

404
    /**
405
     * Sets the new tracing feature if it is touching via startpoint or endpoint.
406
     *
407
     * @param {ol.Feature} foundFeature The hovered feature to set as new tracing feature
408
     */
409
    setNewTracingOnStartEndTouch: function (foundFeature) {
410
        const me = this;
×
411
        const foundGeom = foundFeature.getGeometry();
×
412
        const tracingGeom = me.tracingFeature.getGeometry();
×
413
        const tracingCoords = tracingGeom.getCoordinates();
×
414

415
        const coords = foundGeom.getCoordinates();
×
416

417
        const resultCoords = me.tracingUtil.concatLineCoords(
×
418
            tracingCoords,
419
            coords
420
        );
421

422
        if (resultCoords) {
×
423
            me.tracingFeature.getGeometry().setCoordinates(resultCoords);
×
424
            me.tracingFeatureArray.push(foundFeature);
×
425
        }
426
    },
427

428
    /**
429
     * Sets the new tracing feature if interior of line touches the startpoint or the endpoint of another line.
430
     *
431
     * @param {ol.Feature} foundFeature The hovered feature to set as new tracing feature
432
     * @param {ol.coordinate.Coordinate} touchCoordinate The coordinate of the touching point
433
     */
434
    setNewTracingOnInteriorStartEndTouch: function (
435
        foundFeature,
436
        touchCoordinate
437
    ) {
438
        const me = this;
×
439
        const tracingGeom = me.tracingFeature.getGeometry();
×
440
        const tracingCoords = tracingGeom.getCoordinates();
×
441

442
        const touchingIndex = me.tracingUtil.getCoordIndex(
×
443
            tracingCoords,
444
            touchCoordinate
445
        );
446

447
        const foundSplitPoint = me.tracingUtil.getClosestCoordinateToPoint(
×
448
            tracingCoords,
449
            me.tracingStartPoint
450
        );
451
        const startingPointIndex = me.tracingUtil.getCoordIndex(
×
452
            tracingCoords,
453
            foundSplitPoint
454
        );
455

456
        // we cut the tracing feature by the split point
457
        // we have to take the order into account
458
        let partUntilIntersection;
459
        if (touchingIndex < startingPointIndex) {
×
460
            partUntilIntersection = tracingCoords.slice(
×
461
                touchingIndex,
462
                tracingCoords.length
463
            );
464
        } else {
465
            partUntilIntersection = tracingCoords.slice(0, touchingIndex + 1);
×
466
        }
467

468
        const newTracingCoords = me.tracingUtil.concatLineCoords(
×
469
            partUntilIntersection,
470
            foundFeature.getGeometry().getCoordinates()
471
        );
472

473
        me.tracingFeature.getGeometry().setCoordinates(newTracingCoords);
×
474
        me.tracingFeatureArray.push(foundFeature);
×
475
    },
476

477
    /**
478
     * Sets the new tracing feature if startpoint or endpoint touches the interior of another line.
479
     *
480
     * @param {ol.Feature} foundFeature The hovered feature to set as new tracing feature
481
     * @param {ol.coordinate.Coordinate} touchCoordinate The coordinate of the touching point
482
     * @param {ol.pixel} pixel The pixel the user hovered on
483
     */
484
    setNewTracingOnStartEndInteriorTouch: function (
485
        foundFeature,
486
        touchCoordinate,
487
        pixel
488
    ) {
489
        const me = this;
×
490
        const foundGeom = foundFeature.getGeometry();
×
491
        const tracingGeom = me.tracingFeature.getGeometry();
×
492
        const tracingCoords = tracingGeom.getCoordinates();
×
493

494
        const touchingIndex = me.tracingUtil.getCoordIndex(
×
495
            foundGeom.getCoordinates(),
496
            touchCoordinate
497
        );
498

499
        const hoverCoord = me.map.getCoordinateFromPixel(pixel);
×
500
        const foundSplitPoint = me.tracingUtil.getClosestCoordinateToPoint(
×
501
            foundGeom.getCoordinates(),
502
            hoverCoord
503
        );
504
        const hoverIndex = me.tracingUtil.getCoordIndex(
×
505
            foundGeom.getCoordinates(),
506
            foundSplitPoint
507
        );
508

509
        // we cut the tracing feature by the split point
510
        // we have to take the order into account
511
        const foundCoords = foundGeom.getCoordinates();
×
512
        let partUntilIntersection;
513
        if (hoverIndex >= touchingIndex) {
×
514
            partUntilIntersection = foundCoords.slice(
×
515
                touchingIndex,
516
                foundCoords.length
517
            );
518
        } else {
519
            partUntilIntersection = foundCoords.slice(0, touchingIndex + 1);
×
520
        }
521

522
        const newTracingCoords = me.tracingUtil.concatLineCoords(
×
523
            partUntilIntersection,
524
            tracingCoords
525
        );
526

527
        me.tracingFeature.getGeometry().setCoordinates(newTracingCoords);
×
528
        me.tracingFeatureArray.push(foundFeature);
×
529
    }
530
});
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