• 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

10.12
/app/controller/button/DigitizeButtonController.js
1
/**
2
 * This class is the controller for the DigitizeButton.
3
 */
4
Ext.define('CpsiMapview.controller.button.DigitizeButtonController', {
1✔
5
    extend: 'Ext.app.ViewController',
6
    requires: [
7
        'BasiGX.util.Map',
8
        'BasiGX.util.MsgBox',
9
        'Ext.menu.Menu',
10
        'CpsiMapview.view.window.MinimizableWindow',
11
        'GeoExt.component.FeatureRenderer',
12
        'GeoExt.data.store.Features',
13
        'CpsiMapview.view.toolbar.CircleSelectionToolbar'
14
    ],
15

16
    alias: 'controller.cmv_digitize_button',
17

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

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

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

33
    /**
34
     * Temporary vector layer used to display the response features
35
     */
36
    resultLayer: null,
37

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

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

49
    /**
50
     * OpenLayers pointer interaction for deleting points
51
     */
52
    pointerInteraction: null,
53

54
    /**
55
     * OpenLayers snap interaction for better vertex selection
56
     */
57
    snapVertexInteraction: null,
58

59
    /**
60
     * OpenLayers snap interaction for better edge selection
61
     */
62
    snapEdgeInteraction: null,
63

64
    /**
65
     * CircleToolbar that will be set
66
     * when pressing a button of type `Circle`
67
     */
68
    circleToolbar: null,
69

70
    /**
71
     * Parent to add the circleToolbar to. MUST
72
     * implement the method `addDocked()`.
73
     */
74
    circleToolbarParent: null,
75

76
    /**
77
     * The index of the currently active group
78
     * Only used when `useContextMenu` is true
79
     */
80
    activeGroupIdx: 0,
81

82
    /**
83
     * The counter reflecting the number of groups
84
     * the user has created through the context menu
85
     */
86
    contextMenuGroupsCounter: 0,
87

88
    /**
89
     * Determines if event handling is blocked.
90
     */
91
    blockedEventHandling: false,
92

93
    constructor: function () {
94
        const me = this;
27✔
95

96
        me.handleDrawEnd = me.handleDrawEnd.bind(me);
27✔
97
        me.handleLocalDrawEnd = me.handleLocalDrawEnd.bind(me);
27✔
98
        me.handleCircleDrawEnd = me.handleCircleDrawEnd.bind(me);
27✔
99
        me.handleModifyEnd = me.handleModifyEnd.bind(me);
27✔
100

101
        me.callParent(arguments);
27✔
102
    },
103

104
    /**
105
     * Set the layer to store features drawn by the editing
106
     * tools
107
     * @param {any} layer
108
     */
109
    setDrawLayer: function (layer) {
110
        const me = this;
×
111

112
        if (!me.map) {
×
113
            return;
×
114
        }
115

116
        if (me.drawLayer) {
×
117
            me.map.removeLayer(me.drawLayer);
×
118
        }
119

120
        me.drawLayer = layer;
×
121
        me.setDrawInteraction(layer);
×
122
        me.setModifyInteraction(layer);
×
123
        me.setSnapInteraction(layer);
×
124
    },
125

126
    /**
127
     * Set the layer used to store features returned
128
     * by the digitising services
129
     * @param {any} layer
130
     */
131
    setResultLayer: function (layer) {
132
        const me = this;
×
133

134
        if (!me.map) {
×
135
            return;
×
136
        }
137

138
        if (me.resultLayer) {
×
139
            me.map.removeLayer(me.resultLayer);
×
140
        }
141

142
        me.resultLayer = layer;
×
143
    },
144

145
    /**
146
     * Set the map drawing interaction
147
     * which will allow features to be added to the drawLayer
148
     * @param {any} layer
149
     */
150
    setDrawInteraction: function (layer) {
151
        const me = this;
×
152
        const view = me.getView();
×
153
        const type = view.getType();
×
154

155
        if (me.drawInteraction) {
×
156
            me.map.removeInteraction(me.drawInteraction);
×
157
        }
158

159
        const drawCondition = function (evt) {
×
160
            // the draw interaction does not work with the singleClick condition.
161
            return (
×
162
                ol.events.condition.primaryAction(evt) &&
×
163
                ol.events.condition.noModifierKeys(evt) &&
164
                !me.blockedEventHandling
165
            );
166
        };
167

168
        const drawInteractionConfig = {
×
169
            type: view.getMulti() ? 'Multi' + type : type,
×
170
            source: layer.getSource(),
171
            condition: drawCondition
172
        };
173

174
        if (type === 'Circle') {
×
175
            // Circle type does not support "multi", so we make sure that it is set appropriately
176
            drawInteractionConfig.type = type;
×
177
        }
178

179
        me.drawInteraction = new ol.interaction.Draw(drawInteractionConfig);
×
180

181
        // register listeners when connected to a backend service
182
        if (view.getApiUrl()) {
×
183
            me.drawInteraction.on(
×
184
                'drawend',
185
                type === 'Circle' ? me.handleCircleDrawEnd : me.handleDrawEnd
×
186
            );
187
        } else {
188
            me.drawInteraction.on('drawend', me.handleLocalDrawEnd);
×
189
        }
190

191
        me.map.addInteraction(me.drawInteraction);
×
192
    },
193

194
    /**
195
     * Set the modify interaction, used to modify
196
     * existing features created in the drawLayer
197
     * @param {any} layer
198
     */
199
    setModifyInteraction: function (layer) {
200
        const me = this;
×
201
        const view = me.getView();
×
202
        const type = view.getType();
×
203

204
        if (me.modifyInteraction) {
×
205
            me.map.removeInteraction(me.modifyInteraction);
×
206
        }
207

208
        const drawCondition = function (evt) {
×
209
            // the modify interaction does not work with the singleClick condition.
210
            return (
×
211
                ol.events.condition.primaryAction(evt) &&
×
212
                ol.events.condition.noModifierKeys(evt) &&
213
                !me.blockedEventHandling
214
            );
215
        };
216

217
        const deleteCondition = function (evt) {
×
218
            return (
×
219
                ol.events.condition.primaryAction(evt) &&
×
220
                ol.events.condition.platformModifierKeyOnly(evt) &&
221
                !me.blockedEventHandling
222
            );
223
        };
224

225
        // create the modify interaction
226
        if (type !== 'Circle') {
×
227
            const modifyInteractionConfig = {
×
228
                features: layer.getSource().getFeaturesCollection(),
229
                condition: drawCondition,
230
                deleteCondition: deleteCondition
231
            };
232
            me.modifyInteraction = new ol.interaction.Modify(
×
233
                modifyInteractionConfig
234
            );
235
            // add listeners when connected to a backend service
236
            if (view.getApiUrl()) {
×
237
                me.modifyInteraction.on('modifyend', me.handleModifyEnd);
×
238
            }
239
            me.map.addInteraction(me.modifyInteraction);
×
240
        }
241
    },
242

243
    /**
244
     * Create the pointer interaction used to delete
245
     * and add solver points
246
     * */
247
    setPointerInteraction: function () {
248
        const me = this;
×
249
        const view = me.getView();
×
250
        const type = view.getType();
×
251

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

256
        if (type === 'Point') {
×
257
            const clickCondition = function (evt) {
×
258
                return (
×
259
                    ol.events.condition.primaryAction(evt) &&
×
260
                    ol.events.condition.noModifierKeys(evt) &&
261
                    !me.blockedEventHandling
262
                );
263
            };
264

265
            const deleteCondition = function (evt) {
×
266
                return (
×
267
                    ol.events.condition.primaryAction(evt) &&
×
268
                    ol.events.condition.platformModifierKeyOnly(evt) &&
269
                    !me.blockedEventHandling
270
                );
271
            };
272

273
            me.pointerInteraction = new ol.interaction.Pointer({
×
274
                handleEvent: function (evt) {
275
                    // fire an event to handle drag event ends for local drawing
276
                    if (evt.type === 'pointerup') {
×
277
                        if (!view.getApiUrl()) {
×
278
                            const drawSource = me.drawLayer.getSource();
×
279
                            drawSource.dispatchEvent({ type: 'localdrawend' });
×
280
                        }
281
                    }
282

283
                    if (deleteCondition(evt)) {
×
284
                        return me.handlePointDelete(evt);
×
285
                    }
286
                    if (clickCondition(evt)) {
×
287
                        // allow local drawing of features with no API calls
288
                        if (!view.getApiUrl()) {
×
289
                            return true;
×
290
                        }
291
                        return me.handlePointClick(evt);
×
292
                    }
293
                    return true;
×
294
                }
295
            });
296
            me.map.addInteraction(me.pointerInteraction);
×
297
        }
298
    },
299

300
    /**
301
     * Set the snap interaction used to snap to features
302
     * already in the drawLayer
303
     * @param {any} layer
304
     */
305
    setSnapInteraction: function (layer) {
306
        const me = this;
×
307
        const view = me.getView();
×
308
        const type = view.getType();
×
309

310
        if (me.snapVertexInteraction) {
×
311
            me.map.removeInteraction(me.snapVertexInteraction);
×
312
        }
313

314
        if (me.snapEdgeInteraction) {
×
315
            me.map.removeInteraction(me.snapEdgeInteraction);
×
316
        }
317

318
        if (type !== 'Circle') {
×
319
            me.snapVertexInteraction = new ol.interaction.Snap({
×
320
                source: layer.getSource()
321
            });
322
            me.map.addInteraction(me.snapVertexInteraction);
×
323
            me.snapEdgeInteraction = new ol.interaction.Snap({
×
324
                features: me.resultLayer.getSource().getFeaturesCollection(),
325
                vertex: false
326
            });
327
            me.map.addInteraction(me.snapEdgeInteraction);
×
328
        }
329
    },
330

331
    /**
332
     * Main handler which activates or deactivates the interactions and listeners
333
     * @param {Ext.button.Button} btn The button that has been pressed
334
     * @param {boolean} pressed The toggle state of the button
335
     */
336
    onToggle: function (btn, pressed) {
337
        const me = this;
×
338
        const view = me.getView();
×
339
        const type = view.getType();
×
340

341
        // guess the map if not given
342
        if (!me.map) {
×
343
            me.map = BasiGX.util.Map.getMapComponent().map;
×
344
        }
345

346
        // use default cmv_map Ext.panel.Panel for circle toolbar if not defined
347
        if (!me.circleToolbarParent) {
×
348
            me.circleToolbarParent = Ext.ComponentQuery.query('cmv_map')[0];
×
349
        }
350

351
        // create a temporary draw layer unless one has already been set
352

353
        if (!me.drawLayer) {
×
354
            if (view.drawLayer) {
×
355
                me.drawLayer = view.drawLayer;
×
356
            } else {
357
                me.drawLayer = new ol.layer.Vector({
×
358
                    source: new ol.source.Vector({
359
                        features: new ol.Collection()
360
                    })
361
                });
362

363
                // apply any draw style set from the view
364
                const drawStyle = view.getDrawLayerStyle();
×
365
                if (drawStyle) {
×
366
                    me.drawLayer.setStyle(drawStyle);
×
367
                }
368
                me.map.addLayer(me.drawLayer);
×
369
            }
370
        }
371

372
        // create a result layer unless one has already been set
373
        if (!me.resultLayer) {
×
374
            if (view.resultLayer) {
×
375
                me.resultLayer = view.resultLayer;
×
376
            } else {
377
                me.resultLayer = new ol.layer.Vector({
×
378
                    name: 'resultLayer',
379
                    source: new ol.source.Vector({
380
                        features: new ol.Collection()
381
                    }),
382
                    style: view.getResultLayerStyle()
383
                });
384
                me.map.addLayer(me.resultLayer);
×
385
            }
386
        }
387

388
        me.setDrawInteraction(me.drawLayer);
×
389
        me.setModifyInteraction(me.drawLayer);
×
390
        me.setPointerInteraction();
×
391
        me.setSnapInteraction(me.drawLayer);
×
392

393
        if (pressed) {
×
394
            me.drawInteraction.setActive(true);
×
395
            if (type !== 'Circle') {
×
396
                me.modifyInteraction.setActive(true);
×
397
                me.snapVertexInteraction.setActive(true);
×
398
                me.snapEdgeInteraction.setActive(true);
×
399
            }
400
            if (type === 'Point') {
×
401
                me.pointerInteraction.setActive(true);
×
402
                me.drawLayer.setVisible(true);
×
403
            }
404
            me.map
×
405
                .getViewport()
406
                .addEventListener('contextmenu', me.contextHandler);
407
        } else {
408
            me.drawInteraction.setActive(false);
×
409
            if (type !== 'Circle') {
×
410
                me.modifyInteraction.setActive(false);
×
411
                me.snapVertexInteraction.setActive(false);
×
412
                me.snapEdgeInteraction.setActive(false);
×
413
            }
414
            if (type === 'Point') {
×
415
                me.pointerInteraction.setActive(false);
×
416
                // hide/show the draw layer based on if the tool is active
417
                // but leave circle/polygon features visible
418
                me.drawLayer.setVisible(false);
×
419
            }
420
            if (type === 'Circle' && me.circleToolbar != null) {
×
421
                me.removeCircleSelectToolbar();
×
422
            }
423
            me.map
×
424
                .getViewport()
425
                .removeEventListener('contextmenu', me.contextHandler);
426

427
            if (me.getView().getResetOnToggle()) {
×
428
                me.drawLayer.getSource().clear();
×
429
                me.clearActiveGroup();
×
430
                // reset context menu entries
431
                me.activeGroupIdx = 0;
×
432
                me.contextMenuGroupsCounter = 0;
×
433
            }
434
        }
435
    },
436

437
    blockEventHandling: function () {
438
        const me = this;
×
439

440
        me.blockedEventHandling = true;
×
441

442
        setTimeout(function () {
×
443
            me.blockedEventHandling = false;
×
444
        }, 300);
445
    },
446

447
    /**
448
     * Returns an Ext.form.field.Radio for the context menu
449
     * @param {number} idx The index that is used as value and label of the radio
450
     * @param {boolean} checked Boolean indicating if the radio shall be checked
451
     * @returns {object} An config object to create an Ext.form.field.Radio
452
     */
453
    getRadioGroupItem: function (idx, checked) {
454
        return {
×
455
            boxLabel: 'Group ' + (idx + 1).toString(),
456
            name: 'radiobutton',
457
            inputValue: idx,
458
            checked: checked
459
        };
460
    },
461

462
    /**
463
     * Method shows the context menu on mouse right click
464
     * @param {Event} evt The browser event
465
     */
466
    showContextMenu: function (evt) {
467
        // suppress default browser behaviour
468
        evt.preventDefault();
×
469

470
        const me = this.scope;
×
471
        const view = me.getView();
×
472

473
        const radioGroupItems = [];
×
474
        if (me.contextMenuGroupsCounter === 0) {
×
475
            radioGroupItems.push(me.getRadioGroupItem(0, true));
×
476
        } else {
477
            for (let i = 0; i <= me.contextMenuGroupsCounter; i++) {
×
478
                radioGroupItems.push(
×
479
                    me.getRadioGroupItem(i, me.activeGroupIdx === i)
480
                );
481
            }
482
        }
483

484
        let menuItems;
485
        if (view.getGroups()) {
×
486
            menuItems = [
×
487
                {
488
                    text: 'Start new Group',
489
                    handler: function () {
490
                        me.contextMenuGroupsCounter++;
×
491
                        me.activeGroupIdx = me.contextMenuGroupsCounter;
×
492
                    }
493
                },
494
                {
495
                    text: 'Active Group',
496
                    menu: {
497
                        name: 'active-group-submenu',
498
                        items: [
499
                            {
500
                                xtype: 'radiogroup',
501
                                columns: 1,
502
                                vertical: true,
503
                                items: radioGroupItems,
504
                                listeners: {
505
                                    change: function (radioGroup, newVal) {
506
                                        me.activeGroupIdx = newVal.radiobutton;
×
507
                                        me.updateDrawSource();
×
508
                                    }
509
                                }
510
                            }
511
                        ]
512
                    }
513
                },
514
                {
515
                    text: 'Clear Active Group',
516
                    handler: function () {
517
                        me.clearActiveGroup(me.activeGroupIdx);
×
518
                    }
519
                }
520
            ];
521
        } else {
522
            menuItems = [
×
523
                {
524
                    text: 'Clear All',
525
                    handler: function () {
526
                        me.drawLayer.getSource().clear();
×
527
                        me.clearActiveGroup(me.activeGroupIdx);
×
528
                    }
529
                }
530
            ];
531
        }
532

533
        const menu = Ext.create('Ext.menu.Menu', {
×
534
            width: 100,
535
            plain: true,
536
            renderTo: Ext.getBody(),
537
            items: menuItems
538
        });
539
        menu.showAt(evt.pageX, evt.pageY);
×
540
    },
541

542
    /**
543
     * Returns all features in the active group from the result layer
544
     * @returns {ol.Feature[]}
545
     */
546
    getActiveGroupFeatures: function () {
547
        const me = this;
2✔
548
        return this.resultLayer
2✔
549
            .getSource()
550
            .getFeatures()
551
            .filter(function (feature) {
552
                return feature.get('group') === me.activeGroupIdx;
2✔
553
            });
554
    },
555

556
    /**
557
     * Returns only the solver points from the result layer in correct order
558
     * @returns {ol.Feature[]}
559
     */
560
    getSolverPoints: function () {
561
        return this.getActiveGroupFeatures()
2✔
562
            .filter(function (feature) {
563
                return feature.getGeometry() instanceof ol.geom.Point;
1✔
564
            })
565
            .sort(function (a, b) {
566
                return a.get('index') - b.get('index');
×
567
            });
568
    },
569

570
    /**
571
     * Handles the drawend event when using the feature to draw features that don't require
572
     * sending or returning any data from a back-end service
573
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
574
     */
575
    handleLocalDrawEnd: function () {
576
        const me = this;
×
577

578
        const drawSource = me.drawLayer.getSource();
×
579
        // clear all previous features so only the last drawn feature remains
580
        drawSource.clear();
×
581
        // fire an event when the drawing is complete
582
        drawSource.dispatchEvent({ type: 'localdrawend' });
×
583
    },
584

585
    /**
586
     * Handles the drawend event and gets the netsolver result which is passed to `handleFinalResult`
587
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
588
     */
589
    handleDrawEnd: function (evt) {
590
        const me = this;
×
591
        const view = me.getView();
×
592
        let resultPromise;
593

594
        me.blockEventHandling();
×
595

596
        switch (view.getType()) {
×
597
            case 'Point': {
598
                const points = me.getSolverPoints();
×
599
                resultPromise = me.getNetByPoints(points.concat([evt.feature]));
×
600
                break;
×
601
            }
602
            case 'Polygon':
603
                resultPromise = me.getNetByPolygon(evt.feature);
×
604
                break;
×
605
            case 'Circle':
606
                resultPromise = me.getNetByCircle(evt.feature);
×
607
                break;
×
608
            default:
609
                BasiGX.warn(
×
610
                    'Please implement your custom handler here for ' +
611
                        view.getType()
612
                );
613
                return;
×
614
        }
615

616
        resultPromise
×
617
            .then(me.handleFinalResult.bind(me))
618
            .then(me.updateDrawSource.bind(me));
619
    },
620

621
    /**
622
     * Handles the modifyend event and gets the netsolver result which is passed to `handleFinalResult`
623
     * @param {ol.interaction.Modify.Event} evt The OpenLayers modify event containing the features
624
     */
625
    handleModifyEnd: function (evt) {
626
        const me = this;
×
627
        const view = me.getView();
×
628
        let resultPromise;
629

630
        me.blockEventHandling();
×
631

632
        switch (view.getType()) {
×
633
            case 'Point': {
634
                // find modified feature
635
                const drawFeature = me.map.getFeaturesAtPixel(
×
636
                    evt.mapBrowserEvent.pixel,
637
                    {
638
                        layerFilter: function (layer) {
639
                            return layer === me.drawLayer;
×
640
                        }
641
                    }
642
                )[0];
643

644
                const index = drawFeature.get('index');
×
645
                const points = me.getSolverPoints();
×
646

647
                if (index === points.length - 1) {
×
648
                    points.splice(index, 1, drawFeature);
×
649
                    resultPromise = me.getNetByPoints(points);
×
650
                } else {
651
                    // we first get the corrected point from the netsolver and then recalculate the whole path
652
                    resultPromise = me
×
653
                        .getNetByPoints([drawFeature])
654
                        .then(function (features) {
655
                            if (features) {
×
656
                                const newFeature = features[0];
×
657
                                newFeature.set('index', index);
×
658
                                points.splice(index, 1, newFeature);
×
659
                                return me.getNetByPoints(points);
×
660
                            }
661
                        });
662
                }
663

664
                break;
×
665
            }
666
            case 'Polygon':
667
                resultPromise = me.getNetByPolygon(evt.features.getArray()[0]);
×
668
                break;
×
669
        }
670

671
        resultPromise
×
672
            .then(me.handleFinalResult.bind(me))
673
            .then(me.updateDrawSource.bind(me));
674
    },
675

676
    /**
677
     * Handles a click registered by the pointer interaction if the deleteCondition is met.
678
     * If it returns false all other interaction at this point are ignored
679
     * @param {ol.MapBrowserEvent} evt
680
     */
681
    handlePointDelete: function (evt) {
682
        const me = this;
×
683

684
        const features = me.map.getFeaturesAtPixel(evt.pixel, {
×
685
            layerFilter: function (layer) {
686
                return layer === me.drawLayer;
×
687
            }
688
        });
689
        if (features.length > 0) {
×
690
            me.blockEventHandling();
×
691

692
            const drawFeature = features[0];
×
693

694
            const points = me.getSolverPoints();
×
695
            points.splice(drawFeature.get('index'), 1);
×
696

697
            if (Ext.isEmpty(points)) {
×
698
                me.handleFinalResult([]);
×
699
                me.updateDrawSource();
×
700
            } else {
701
                me.getNetByPoints(points)
×
702
                    .then(me.handleFinalResult.bind(me))
703
                    .then(me.updateDrawSource.bind(me));
704
            }
705

706
            return false;
×
707
        } else {
708
            return true;
×
709
        }
710
    },
711

712
    /**
713
     * Handles the click registered by the pointer interaction.
714
     * If it returns false all other interaction at this point are ignored
715
     * @param {ol.MapBrowserEvent} evt
716
     */
717
    handlePointClick: function (evt) {
718
        const me = this;
×
719

720
        const features = me.map.getFeaturesAtPixel(evt.pixel, {
×
721
            layerFilter: function (layer) {
722
                return layer === me.drawLayer;
×
723
            }
724
        });
725

726
        const points = me.getSolverPoints();
×
727

728
        if (features.length > 0 && features[0] !== points[points.length - 1]) {
×
729
            // for pointerdown and pointerup events we simply want to return
730
            // we only want to create a new point on a user click
731
            if (evt.type !== 'singleclick') {
×
732
                return true;
×
733
            }
734

735
            me.blockEventHandling();
×
736

737
            // to allow loops to be created we need to allow users to click on the
738
            // start point to add a duplicate end point
739

740
            me.getNetByPoints(points.concat([features[0]]))
×
741
                .then(me.handleFinalResult.bind(me))
742
                .then(me.updateDrawSource.bind(me));
743

744
            return false;
×
745
        } else {
746
            // allow edges to be selected by clicking on them
747
            // by temporarily setting a blockedEventHandling flag
748
            const hasEdge = me.map.hasFeatureAtPixel(evt.pixel, {
×
749
                layerFilter: function (layer) {
750
                    return layer === me.resultLayer;
×
751
                }
752
            });
753
            if (hasEdge) {
×
754
                me.blockEventHandling();
×
755
            }
756
            return !hasEdge;
×
757
        }
758
    },
759

760
    /**
761
     * Handles the draw end event of the circle type by getting the feature and passing it
762
     * to the CircleSelection component
763
     * @param {DrawEvent} evt The OpenLayers draw event containing the features
764
     */
765
    handleCircleDrawEnd: function (evt) {
766
        const me = this;
×
767
        me.blockEventHandling();
×
768
        // deactivate the creation of another circle
769
        me.drawInteraction.setActive(false);
×
770
        me.circleToolbar = Ext.create(
×
771
            'CpsiMapview.view.toolbar.CircleSelectionToolbar',
772
            {
773
                feature: evt.feature
774
            }
775
        );
776
        me.circleToolbar.getController().on({
×
777
            circleSelectApply: me.onCircleSelectApply,
778
            circleSelectCancel: me.onCircleSelectCancel,
779
            scope: me
780
        });
781
        me.circleToolbarParent.addDocked(me.circleToolbar);
×
782
    },
783

784
    /**
785
     * Handles the `apply` event of the CircleSelection by passing the created circle
786
     * to the `handleDrawEnd` function. Also handles the cleanup of the CircleSelection toolbar
787
     * and enables the drawing interaction
788
     * @param {ol.Feature} feat
789
     */
790
    onCircleSelectApply: function (feat) {
791
        const me = this;
×
792
        const evt = { feature: feat };
×
793
        me.handleDrawEnd(evt);
×
794
        me.removeCircleSelectToolbar();
×
795
        me.drawInteraction.setActive(true);
×
796
    },
797

798
    /**
799
     * Handles the `cancel` event of the CircleSelection by cleaning up the CircleSelection toolbar
800
     * and enabling the drawing interaction
801
     */
802
    onCircleSelectCancel: function () {
803
        const me = this;
×
804
        me.drawLayer.getSource().clear();
×
805
        me.removeCircleSelectToolbar();
×
806
        me.drawInteraction.setActive(true);
×
807
    },
808

809
    /**
810
     * Handles the removal of the CircleSelect toolbar
811
     */
812
    removeCircleSelectToolbar: function () {
813
        const me = this;
×
814
        me.circleToolbarParent.removeDocked(me.circleToolbar);
×
815
        me.circleToolbar = null;
×
816
    },
817

818
    /**
819
     * Asynchronously gets a path between the given points from the netsolver.
820
     * @param {ol.Feature[]} features Expects the features in the correct order for solving. The coordinates of the last
821
     *      feature will get corrected by the netsolver. The other coordinates need to be valid coordinates for the
822
     *      netsolver (i.e. already corrected points)
823
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
824
     */
825
    getNetByPoints: function (features) {
826
        const me = this;
×
827
        const view = me.getView();
×
828
        const format = new ol.format.GeoJSON({
×
829
            dataProjection: me.map.getView().getProjection().getCode()
830
        });
831
        let jsonParams, searchParams;
832

833
        features.forEach(function (feature, index) {
×
834
            feature.set('index', index);
×
835
        });
836

837
        // The Netsolver endpoint expects bbox to be sent within a request.
838
        // The lower left and upper right coordinates cannot be the same so
839
        // we have to apply a small buffer on the point geometry to get a
840
        // small bbox around the clicked point.
841
        if (view.getPointExtentBuffer()) {
×
842
            jsonParams = format.writeFeatures(features.slice(0, -1));
×
843
            const extent = features[features.length - 1]
×
844
                .getGeometry()
845
                .getExtent();
846
            const buffered = ol.extent.buffer(
×
847
                extent,
848
                view.getPointExtentBuffer()
849
            );
850
            searchParams = 'bbox=' + encodeURIComponent(buffered.join(','));
×
851
        } else {
852
            jsonParams = format.writeFeatures(features);
×
853
        }
854

855
        return me
×
856
            .doAjaxRequest(jsonParams, searchParams)
857
            .then(me.parseNetsolverResponse.bind(me));
858
    },
859

860
    /**
861
     * Asynchronously gets all lines inside the given polygon from the netsolver
862
     * @param {ol.Feature} feat
863
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
864
     */
865
    getNetByPolygon: function (feat) {
866
        const me = this;
×
867
        const srs = me.map.getView().getProjection().getCode();
×
868
        const format = new ol.format.GeoJSON({
×
869
            dataProjection: srs
870
        });
871
        const geoJson = format.writeFeature(feat);
×
872
        const jsonParams = {};
×
873
        const geometryParamName = 'geometry' + srs.replace('EPSG:', '');
×
874
        jsonParams[geometryParamName] = Ext.JSON.decode(geoJson).geometry;
×
875
        return me
×
876
            .doAjaxRequest(jsonParams)
877
            .then(me.parseNetsolverResponse.bind(me));
878
    },
879

880
    /**
881
     * Asynchronously gets all lines inside the given circle from the netsolver
882
     * @param {ol.Feature} feat
883
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
884
     */
885
    getNetByCircle: function (feat) {
886
        // ol circle objects consist of a center coordinate and a radius in the
887
        // unit of the projection. In order to convert it into a geoJSON, we have
888
        // to convert the circle to a polygon first.
889
        const circleAsPolygon = new ol.geom.Polygon.fromCircle(
×
890
            feat.getGeometry()
891
        );
892
        const polygonAsFeature = new ol.Feature({ geometry: circleAsPolygon });
×
893

894
        return this.getNetByPolygon(polygonAsFeature);
×
895
    },
896

897
    /**
898
     * Parses the netsolver result to openlayers features
899
     * @param {XMLHttpRequest} response
900
     * @returns {ol.Feature[]}
901
     */
902
    parseNetsolverResponse: function (response) {
903
        if (response) {
2!
904
            const me = this;
2✔
905
            const format = new ol.format.GeoJSON();
2✔
906
            let json;
907

908
            if (!Ext.isEmpty(response.responseText)) {
2!
909
                try {
2✔
910
                    json = Ext.decode(response.responseText);
2✔
911
                } catch (e) {
912
                    BasiGX.error(
×
913
                        'Could not parse the response: ' + response.responseText
914
                    );
915
                    Ext.log.error(e);
×
916
                    return;
×
917
                }
918
                if (json.success && json.data && json.data.features) {
2✔
919
                    const features = json.data.features;
1✔
920

921
                    return features.map(function (feat) {
1✔
922
                        // api will respond with non unique ids, which
923
                        // will collide with OpenLayers feature ids not
924
                        // being unique. That's why we delete it here.
925
                        delete feat.id;
1✔
926
                        // set the current active group as property
927
                        feat.properties.group = me.activeGroupIdx;
1✔
928
                        return format.readFeature(feat);
1✔
929
                    });
930
                } else {
931
                    Ext.toast({
1✔
932
                        html:
933
                            'No features found at this location' +
934
                            (json.message ? ' (' + json.message + ')' : ''),
1!
935
                        title: 'No Features',
936
                        width: 200,
937
                        align: 'br'
938
                    });
939
                }
940
            } else {
941
                BasiGX.error('Response was empty');
×
942
            }
943
        }
944
    },
945

946
    /**
947
     * Issues an Ext.Ajax.request against the configured endpoint with
948
     * the given params.
949
     * @param {object} jsonParams Object containing the params to send
950
     * @param {string} searchParams The serarchParams which will be
951
     *   appended to the request url
952
     * @returns {Ext.request.Base}
953
     */
954
    doAjaxRequest: function (jsonParams, searchParams) {
955
        const me = this;
×
956
        const mapComponent =
957
            me.mapComponent || BasiGX.util.Map.getMapComponent();
×
958
        const view = me.getView();
×
959
        let url = view.getApiUrl();
×
960

961
        if (!url) {
×
962
            return;
×
963
        }
964

965
        if (searchParams) {
×
966
            url = Ext.urlAppend(url, searchParams);
×
967
        }
968

969
        mapComponent.setLoading(true);
×
970

971
        return new Ext.Promise(function (resolve) {
×
972
            Ext.Ajax.request({
×
973
                url: url,
974
                method: 'POST',
975
                jsonData: jsonParams,
976
                success: function (response) {
977
                    mapComponent.setLoading(false);
×
978
                    resolve(response);
×
979
                },
980
                failure: function (response) {
981
                    mapComponent.setLoading(false);
×
982

983
                    if (response.aborted !== true) {
×
984
                        let errorMessage =
985
                            'Error while requesting the API endpoint';
×
986

987
                        if (
×
988
                            response.responseText &&
×
989
                            response.responseText.message
990
                        ) {
991
                            errorMessage +=
×
992
                                ': ' + response.responseText.message;
993
                        }
994

995
                        BasiGX.error(errorMessage);
×
996
                    }
997
                }
998
            });
999
        });
1000
    },
1001

1002
    updateDrawSource: function () {
1003
        const me = this;
×
1004
        const view = me.getView();
×
1005

1006
        const drawSource = me.drawLayer.getSource();
×
1007
        const type = view.getType();
×
1008

1009
        if (type === 'Point') {
×
1010
            drawSource.clear();
×
1011

1012
            const drawFeatures = this.getSolverPoints().map(function (feature) {
×
1013
                return feature.clone();
×
1014
            });
1015
            drawSource.addFeatures(drawFeatures);
×
1016
        } else if (type === 'Polygon' || type === 'Circle') {
×
1017
            if (drawSource.getFeatures().length > 1) {
×
1018
                // keep the last drawn feature and remove the oldest one
1019
                // it seems that the a half-completed draw polygon can consist of multiple features
1020
                drawSource.removeFeature(drawSource.getFeatures()[0]);
×
1021
            }
1022
        }
1023
    },
1024

1025
    /***
1026
     * Get the total length of all features in the results layer
1027
     * If a feature does not have a length property it will be assumed to
1028
     * have a length of 0 (for example points)
1029
     * */
1030
    getResultGeometryLength: function () {
1031
        const me = this;
1✔
1032
        const allFeatures = me.resultLayer.getSource().getFeatures();
1✔
1033
        let resultLength = 0;
1✔
1034

1035
        Ext.each(allFeatures, function (f) {
1✔
1036
            if (f.get('group') === me.activeGroupIdx) {
×
1037
                resultLength += f.get('length') ? f.get('length') : 0;
×
1038
            }
1039
        });
1040

1041
        return resultLength;
1✔
1042
    },
1043

1044
    /**
1045
     * Handles the final result from netsolver.
1046
     * Features will get set a new property `group` in order
1047
     * to maintain their membership to the current selected group.
1048
     * A responseFeatures event is fired.
1049
     * @param {undefined|ol.Feature[]} features The features returned from the API.
1050
     */
1051
    handleFinalResult: function (features) {
1052
        if (features) {
1!
1053
            const me = this;
1✔
1054

1055
            const originalSolverPoints = me.getSolverPoints();
1✔
1056
            const originalLength = me.getResultGeometryLength();
1✔
1057

1058
            // get the original solver points before they are removed
1059
            const resultSource = me.resultLayer.getSource();
1✔
1060
            // remove all features from the current active group
1061
            const allFeatures = me.resultLayer
1✔
1062
                .getSource()
1063
                .getFeatures()
1064
                .slice(0);
1065
            Ext.each(allFeatures, function (f) {
1✔
1066
                if (f.get('group') === me.activeGroupIdx || !f.get('group')) {
×
1067
                    if (!f.get('group')) {
×
1068
                        // the property must be updated before removing the feature, or it is readded to the store
1069
                        f.set('group', me.activeGroupIdx);
×
1070
                    }
1071
                    resultSource.removeFeature(f);
×
1072
                }
1073
            });
1074

1075
            // add the new features for the current active group
1076
            resultSource.addFeatures(features);
1✔
1077

1078
            // now get the new solver points once they have been added
1079
            const newSolverPoints = me.getSolverPoints();
1✔
1080
            let newLength = 0;
1✔
1081

1082
            Ext.each(features, function (f) {
1✔
1083
                newLength += f.get('length') ? f.get('length') : 0;
2✔
1084
            });
1085

1086
            const newEdgeCount = features.filter(function (feature) {
1✔
1087
                return feature.getGeometry() instanceof ol.geom.LineString;
2✔
1088
            }).length;
1089

1090
            const modifications = {
1✔
1091
                originalLength: originalLength,
1092
                newLength: newLength,
1093
                newEdgeCount: newEdgeCount,
1094
                originalSolverPoints: originalSolverPoints,
1095
                newSolverPoints: newSolverPoints,
1096
                toolType: me.getView().type
1097
            };
1098

1099
            // fire a custom event from the source so a listener can be added once
1100
            // all features have been added/removed
1101
            // the event object includes a custom modifications object containing
1102
            // details of before and after the solve
1103
            resultSource.dispatchEvent({
1✔
1104
                type: 'featuresupdated',
1105
                modifications: modifications
1106
            });
1107

1108
            // The response from the API, parsed as OpenLayers features, will be
1109
            // fired here and the event can be used application-wide to access
1110
            // and handle the feature response.
1111
            me.getView().fireEvent('responseFeatures', features);
1✔
1112
        }
1113
    },
1114

1115
    /**
1116
     * Remove the interaction when this component gets destroyed
1117
     */
1118
    onBeforeDestroy: function () {
1119
        const me = this;
×
1120
        const btn = me.getView();
×
1121

1122
        // detoggle button
1123
        me.onToggle(btn, false);
×
1124

1125
        // fire the button's toggle event so that the defaultClickEnabled property
1126
        // is updated in CpsiMapview.util.ApplicationMixin to re-enable clicks
1127
        btn.pressed = false;
×
1128
        btn.fireEvent('toggle');
×
1129

1130
        if (me.drawInteraction) {
×
1131
            me.map.removeInteraction(me.drawInteraction);
×
1132
        }
1133

1134
        if (me.modifyInteraction) {
×
1135
            me.map.removeInteraction(me.modifyInteraction);
×
1136
        }
1137

1138
        if (me.pointerInteraction) {
×
1139
            me.map.removeInteraction(me.pointerInteraction);
×
1140
        }
1141

1142
        if (me.snapVertexInteraction) {
×
1143
            me.map.removeInteraction(me.snapVertexInteraction);
×
1144
        }
1145

1146
        if (me.snapEdgeInteraction) {
×
1147
            me.map.removeInteraction(me.snapEdgeInteraction);
×
1148
        }
1149

1150
        if (me.drawLayer) {
×
1151
            me.map.removeLayer(me.drawLayer);
×
1152
        }
1153

1154
        if (me.resultLayer) {
×
1155
            me.map.removeLayer(me.resultLayer);
×
1156
        }
1157

1158
        if (me.circleToolbar) {
×
1159
            me.circleToolbar.destroy();
×
1160
        }
1161
    },
1162

1163
    /**
1164
     * Zooms the map to the extent of the clicked feature
1165
     * Method may be removed as its actually a showcase, like `onResponseFeatures`
1166
     */
1167
    zoomToFeatures: function (grid, td, index, rec) {
1168
        const me = this;
×
1169
        const extent = rec.olObject.getGeometry().getExtent();
×
1170
        me.map.getView().fit(extent, {
×
1171
            size: me.map.getSize(),
1172
            padding: [5, 5, 5, 5]
1173
        });
1174
    },
1175

1176
    /**
1177
     * Showcasing the handling of the response features by adding them
1178
     * to an `GeoExt.data.store.Features` and showing them in a grid.
1179
     * Method may be removed as its actually a showcase, like `zoomToFeatures`
1180
     */
1181
    onResponseFeatures: function () {
1182
        // the code below is just a show case representing how the response
1183
        // features can be used within a feature grid.
1184
        const me = this;
×
1185

1186
        const featStore = Ext.create('GeoExt.data.store.Features', {
×
1187
            layer: this.resultLayer,
1188
            map: me.map
1189
        });
1190

1191
        featStore.filterBy(function (rec) {
×
1192
            return rec.get('geometry').getType() !== 'Point';
×
1193
        });
1194

1195
        const view = me.getView();
×
1196
        const selectStyle = view.getResultLayerSelectStyle();
×
1197

1198
        if (me.win) {
×
1199
            me.win.destroy();
×
1200
        }
1201
        me.win = Ext.create('CpsiMapview.view.window.MinimizableWindow', {
×
1202
            height: 500,
1203
            width: 300,
1204
            layout: 'fit',
1205
            title: 'Your data',
1206
            name: 'gridwin',
1207
            items: [
1208
                {
1209
                    xtype: 'grid',
1210
                    store: featStore,
1211
                    selModel: {
1212
                        type: 'featurerowmodel',
1213
                        mode: 'MULTI',
1214
                        allowDeselect: true,
1215
                        mapSelection: true,
1216
                        selectStyle: selectStyle,
1217
                        map: me.map
1218
                    },
1219
                    columns: [
1220
                        {
1221
                            xtype: 'widgetcolumn',
1222
                            width: 40,
1223
                            widget: {
1224
                                xtype: 'gx_renderer'
1225
                            },
1226
                            onWidgetAttach: function (
1227
                                column,
1228
                                gxRenderer,
1229
                                record
1230
                            ) {
1231
                                // update the symbolizer with the related feature
1232
                                const featureRenderer =
1233
                                    GeoExt.component.FeatureRenderer;
×
1234
                                const feature = record.getFeature();
×
1235
                                gxRenderer.update({
×
1236
                                    feature: feature,
1237
                                    symbolizers:
1238
                                        featureRenderer.determineStyle(record)
1239
                                });
1240
                            }
1241
                        },
1242
                        {
1243
                            text: 'ID',
1244
                            dataIndex: 'segmentId',
1245
                            flex: 1
1246
                        },
1247
                        {
1248
                            text: 'Code',
1249
                            dataIndex: 'segmentCode',
1250
                            flex: 1
1251
                        },
1252
                        {
1253
                            text: 'Length',
1254
                            dataIndex: 'segmentLength',
1255
                            flex: 1,
1256
                            renderer: function (val) {
1257
                                return Ext.String.format(
×
1258
                                    '{0} m',
1259
                                    val.toFixed(0).toString()
1260
                                );
1261
                            }
1262
                        }
1263
                    ]
1264
                }
1265
            ]
1266
        });
1267
        me.win.showAt(100, 100);
×
1268
    },
1269

1270
    /**
1271
     * Clears all features of the active group from the result source
1272
     * and fire a custom featuresupdated event
1273
     * If no activeGroupIdx is supplied then all features are removed from the
1274
     * resultLayer
1275
     */
1276
    clearActiveGroup: function (activeGroupIdx) {
1277
        const me = this;
×
1278
        const view = me.getView();
×
1279

1280
        if (!me.resultLayer) {
×
1281
            // no results have been returned so nothing to clear
1282
            return;
×
1283
        }
1284

1285
        const originalSolverPoints = me.getSolverPoints();
×
1286
        const originalLength = me.getResultGeometryLength();
×
1287

1288
        const resultSource = me.resultLayer.getSource();
×
1289

1290
        if (view.getGroups() === true) {
×
1291
            resultSource
×
1292
                .getFeatures()
1293
                .slice(0)
1294
                .filter(function (feature) {
1295
                    return feature.get('group') === activeGroupIdx;
×
1296
                })
1297
                .forEach(function (feature) {
1298
                    resultSource.removeFeature(feature);
×
1299
                });
1300
        } else {
1301
            // remove all features
1302
            resultSource.clear();
×
1303
        }
1304

1305
        const modifications = {
×
1306
            originalLength: originalLength,
1307
            newLength: 0,
1308
            originalSolverPoints: originalSolverPoints,
1309
            newSolverPoints: []
1310
        };
1311

1312
        resultSource.dispatchEvent({
×
1313
            type: 'featuresupdated',
1314
            modifications: modifications
1315
        });
1316
        me.updateDrawSource();
×
1317

1318
        // also fire a view event
1319
        me.getView().fireEvent('responseFeatures', []);
×
1320
    },
1321

1322
    init: function () {
1323
        const me = this;
26✔
1324

1325
        // create an object for the contextmenu eventhandler
1326
        // so it can be removed correctly
1327
        me.contextHandler = {
26✔
1328
            handleEvent: me.showContextMenu,
1329
            scope: me
1330
        };
1331
    }
1332
});
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