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

geographika / cpsi-mapview / 12356785512

16 Dec 2024 04:17PM UTC coverage: 26.211% (+2.1%) from 24.092%
12356785512

push

github

geographika
Disable SSL

487 of 2328 branches covered (20.92%)

Branch coverage included in aggregate %.

1445 of 5043 relevant lines covered (28.65%)

1.16 hits per line

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

10.14
/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
        var 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
        var 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
        var 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

152
        var me = this;
×
153
        var view = me.getView();
×
154
        var type = view.getType();
×
155

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

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

165
        var drawInteractionConfig = {
×
166
            type: view.getMulti() ? 'Multi' + type : type,
×
167
            source: layer.getSource(),
168
            condition: drawCondition
169
        };
170

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

176
        me.drawInteraction = new ol.interaction.Draw(drawInteractionConfig);
×
177

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

185
        me.map.addInteraction(me.drawInteraction);
×
186
    },
187

188
    /**
189
     * Set the modify interaction, used to modify
190
     * existing features created in the drawLayer
191
     * @param {any} layer
192
     */
193
    setModifyInteraction: function (layer) {
194

195
        var me = this;
×
196
        var view = me.getView();
×
197
        var type = view.getType();
×
198

199
        if (me.modifyInteraction) {
×
200
            me.map.removeInteraction(me.modifyInteraction);
×
201
        }
202

203
        var drawCondition = function (evt) {
×
204
            // the modify interaction does not work with the singleClick condition.
205
            return ol.events.condition.primaryAction(evt) && ol.events.condition.noModifierKeys(evt) && !me.blockedEventHandling;
×
206
        };
207

208
        var deleteCondition = function (evt) {
×
209
            return ol.events.condition.primaryAction(evt) && ol.events.condition.platformModifierKeyOnly(evt) && !me.blockedEventHandling;
×
210
        };
211

212
        // create the modify interaction
213
        if (type !== 'Circle') {
×
214
            var modifyInteractionConfig = {
×
215
                features: layer.getSource().getFeaturesCollection(),
216
                condition: drawCondition,
217
                deleteCondition: deleteCondition
218
            };
219
            me.modifyInteraction = new ol.interaction.Modify(modifyInteractionConfig);
×
220
            // add listeners when connected to a backend service
221
            if (view.getApiUrl()) {
×
222
                me.modifyInteraction.on('modifyend', me.handleModifyEnd);
×
223
            }
224
            me.map.addInteraction(me.modifyInteraction);
×
225
        }
226
    },
227

228
    /**
229
     * Create the pointer interaction used to delete
230
     * and add solver points
231
     * */
232
    setPointerInteraction: function () {
233

234
        var me = this;
×
235
        var view = me.getView();
×
236
        var type = view.getType();
×
237

238
        if (me.pointerInteraction) {
×
239
            me.map.removeInteraction(me.pointerInteraction);
×
240
        }
241

242
        if (type === 'Point') {
×
243
            var clickCondition = function (evt) {
×
244
                return ol.events.condition.primaryAction(evt) && ol.events.condition.noModifierKeys(evt) && !me.blockedEventHandling;
×
245
            };
246

247
            var deleteCondition = function (evt) {
×
248
                return ol.events.condition.primaryAction(evt) && ol.events.condition.platformModifierKeyOnly(evt) && !me.blockedEventHandling;
×
249
            };
250

251
            me.pointerInteraction = new ol.interaction.Pointer({
×
252
                handleEvent: function (evt) {
253

254
                    // fire an event to handle drag event ends for local drawing
255
                    if (evt.type === 'pointerup') {
×
256
                        if (!view.getApiUrl()) {
×
257
                            var drawSource = me.drawLayer.getSource();
×
258
                            drawSource.dispatchEvent({ type: 'localdrawend' });
×
259
                        }
260
                    }
261

262
                    if (deleteCondition(evt)) {
×
263
                        return me.handlePointDelete(evt);
×
264
                    }
265
                    if (clickCondition(evt)) {
×
266
                        // allow local drawing of features with no API calls
267
                        if (!view.getApiUrl()) {
×
268
                            return true;
×
269
                        }
270
                        return me.handlePointClick(evt);
×
271
                    }
272
                    return true;
×
273
                }
274
            });
275
            me.map.addInteraction(me.pointerInteraction);
×
276
        }
277
    },
278

279
    /**
280
     * Set the snap interaction used to snap to features
281
     * already in the drawLayer
282
     * @param {any} layer
283
     */
284
    setSnapInteraction: function (layer) {
285

286
        var me = this;
×
287
        var view = me.getView();
×
288
        var type = view.getType();
×
289

290
        if (me.snapVertexInteraction) {
×
291
            me.map.removeInteraction(me.snapVertexInteraction);
×
292
        }
293

294
        if (me.snapEdgeInteraction) {
×
295
            me.map.removeInteraction(me.snapEdgeInteraction);
×
296
        }
297

298
        if (type !== 'Circle') {
×
299
            me.snapVertexInteraction = new ol.interaction.Snap({
×
300
                source: layer.getSource()
301
            });
302
            me.map.addInteraction(me.snapVertexInteraction);
×
303
            me.snapEdgeInteraction = new ol.interaction.Snap({
×
304
                features: me.resultLayer.getSource().getFeaturesCollection(),
305
                vertex: false
306
            });
307
            me.map.addInteraction(me.snapEdgeInteraction);
×
308
        }
309
    },
310

311
    /**
312
     * Main handler which activates or deactivates the interactions and listeners
313
     * @param {Ext.button.Button} btn The button that has been pressed
314
     * @param {boolean} pressed The toggle state of the button
315
     */
316
    onToggle: function (btn, pressed) {
317
        var me = this;
×
318
        var view = me.getView();
×
319
        var type = view.getType();
×
320

321
        // guess the map if not given
322
        if (!me.map) {
×
323
            me.map = BasiGX.util.Map.getMapComponent().map;
×
324
        }
325

326
        // use default cmv_map Ext.panel.Panel for circle toolbar if not defined
327
        if (!me.circleToolbarParent) {
×
328
            me.circleToolbarParent = Ext.ComponentQuery.query('cmv_map')[0];
×
329
        }
330

331
        // create a temporary draw layer unless one has already been set
332

333
        if (!me.drawLayer) {
×
334
            if (view.drawLayer) {
×
335
                me.drawLayer = view.drawLayer;
×
336
            } else {
337
                me.drawLayer = new ol.layer.Vector({
×
338
                    source: new ol.source.Vector({
339
                        features: new ol.Collection()
340
                    })
341
                });
342

343
                // apply any draw style set from the view
344
                var drawStyle = view.getDrawLayerStyle();
×
345
                if (drawStyle) {
×
346
                    me.drawLayer.setStyle(drawStyle);
×
347
                }
348
                me.map.addLayer(me.drawLayer);
×
349
            }
350
        }
351

352
        // create a result layer unless one has already been set
353
        if (!me.resultLayer) {
×
354
            if (view.resultLayer) {
×
355
                me.resultLayer = view.resultLayer;
×
356
            } else {
357
                me.resultLayer = new ol.layer.Vector({
×
358
                    name: 'resultLayer',
359
                    source: new ol.source.Vector({
360
                        features: new ol.Collection()
361
                    }),
362
                    style: view.getResultLayerStyle()
363
                });
364
                me.map.addLayer(me.resultLayer);
×
365
            }
366
        }
367

368
        me.setDrawInteraction(me.drawLayer);
×
369
        me.setModifyInteraction(me.drawLayer);
×
370
        me.setPointerInteraction();
×
371
        me.setSnapInteraction(me.drawLayer);
×
372

373
        if (pressed) {
×
374
            me.drawInteraction.setActive(true);
×
375
            if (type !== 'Circle') {
×
376
                me.modifyInteraction.setActive(true);
×
377
                me.snapVertexInteraction.setActive(true);
×
378
                me.snapEdgeInteraction.setActive(true);
×
379
            }
380
            if (type === 'Point') {
×
381
                me.pointerInteraction.setActive(true);
×
382
                me.drawLayer.setVisible(true);
×
383
            }
384
            me.map.getViewport().addEventListener('contextmenu', me.contextHandler);
×
385
        } else {
386
            me.drawInteraction.setActive(false);
×
387
            if (type !== 'Circle') {
×
388
                me.modifyInteraction.setActive(false);
×
389
                me.snapVertexInteraction.setActive(false);
×
390
                me.snapEdgeInteraction.setActive(false);
×
391
            }
392
            if (type === 'Point') {
×
393
                me.pointerInteraction.setActive(false);
×
394
                // hide/show the draw layer based on if the tool is active
395
                // but leave circle/polygon features visible
396
                me.drawLayer.setVisible(false);
×
397
            }
398
            if (type === 'Circle' && me.circleToolbar != null) {
×
399
                me.removeCircleSelectToolbar();
×
400
            }
401
            me.map.getViewport().removeEventListener('contextmenu', me.contextHandler);
×
402

403
            if (me.getView().getResetOnToggle()) {
×
404
                me.drawLayer.getSource().clear();
×
405
                me.clearActiveGroup();
×
406
                // reset context menu entries
407
                me.activeGroupIdx = 0;
×
408
                me.contextMenuGroupsCounter = 0;
×
409
            }
410
        }
411
    },
412

413
    blockEventHandling: function () {
414
        var me = this;
×
415

416
        me.blockedEventHandling = true;
×
417

418
        setTimeout(function () {
×
419
            me.blockedEventHandling = false;
×
420
        }, 300);
421
    },
422

423
    /**
424
     * Returns an Ext.form.field.Radio for the context menu
425
     * @param {number} idx The index that is used as value and label of the radio
426
     * @param {boolean} checked Boolean indicating if the radio shall be checked
427
     * @returns {object} An config object to create an Ext.form.field.Radio
428
     */
429
    getRadioGroupItem: function (idx, checked) {
430
        return {
×
431
            boxLabel: 'Group ' + (idx + 1).toString(),
432
            name: 'radiobutton',
433
            inputValue: idx,
434
            checked: checked
435
        };
436
    },
437

438
    /**
439
     * Method shows the context menu on mouse right click
440
     * @param {Event} evt The browser event
441
     */
442
    showContextMenu: function (evt) {
443
        // suppress default browser behaviour
444
        evt.preventDefault();
×
445

446
        var me = this.scope;
×
447
        var view = me.getView();
×
448

449
        var radioGroupItems = [];
×
450
        if (me.contextMenuGroupsCounter === 0) {
×
451
            radioGroupItems.push(me.getRadioGroupItem(0, true));
×
452
        } else {
453
            for (var i = 0; i <= me.contextMenuGroupsCounter; i++) {
×
454
                radioGroupItems.push(me.getRadioGroupItem(i, me.activeGroupIdx === i));
×
455
            }
456
        }
457

458
        var menuItems;
459
        if (view.getGroups()) {
×
460
            menuItems = [
×
461
                {
462
                    text: 'Start new Group',
463
                    handler: function () {
464
                        me.contextMenuGroupsCounter++;
×
465
                        me.activeGroupIdx = me.contextMenuGroupsCounter;
×
466
                    }
467
                }, {
468
                    text: 'Active Group',
469
                    menu: {
470
                        name: 'active-group-submenu',
471
                        items: [{
472
                            xtype: 'radiogroup',
473
                            columns: 1,
474
                            vertical: true,
475
                            items: radioGroupItems,
476
                            listeners: {
477
                                'change': function (radioGroup, newVal) {
478
                                    me.activeGroupIdx = newVal.radiobutton;
×
479
                                    me.updateDrawSource();
×
480
                                }
481
                            }
482
                        }]
483
                    }
484
                }, {
485
                    text: 'Clear Active Group',
486
                    handler: function () {
487
                        me.clearActiveGroup(me.activeGroupIdx);
×
488
                    }
489
                }
490
            ];
491
        } else {
492
            menuItems = [{
×
493
                text: 'Clear All',
494
                handler: function () {
495
                    me.drawLayer.getSource().clear();
×
496
                    me.clearActiveGroup(me.activeGroupIdx);
×
497
                }
498
            }];
499
        }
500

501
        var menu = Ext.create('Ext.menu.Menu', {
×
502
            width: 100,
503
            plain: true,
504
            renderTo: Ext.getBody(),
505
            items: menuItems
506
        });
507
        menu.showAt(evt.pageX, evt.pageY);
×
508
    },
509

510
    /**
511
     * Returns all features in the active group from the result layer
512
     * @returns {ol.Feature[]}
513
     */
514
    getActiveGroupFeatures: function () {
515
        var me = this;
2✔
516
        return this.resultLayer.getSource().getFeatures()
2✔
517
            .filter(function (feature) {
518
                return feature.get('group') === me.activeGroupIdx;
2✔
519
            });
520
    },
521

522
    /**
523
     * Returns only the solver points from the result layer in correct order
524
     * @returns {ol.Feature[]}
525
     */
526
    getSolverPoints: function () {
527
        return this.getActiveGroupFeatures()
2✔
528
            .filter(function (feature) {
529
                return feature.getGeometry() instanceof ol.geom.Point;
1✔
530
            })
531
            .sort(function (a, b) {
532
                return a.get('index') - b.get('index');
×
533
            });
534
    },
535

536
    /**
537
    * Handles the drawend event when using the feature to draw features that don't require
538
    * sending or returning any data from a back-end service
539
    * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
540
    */
541
    handleLocalDrawEnd: function () {
542
        var me = this;
×
543

544
        var drawSource = me.drawLayer.getSource();
×
545
        // clear all previous features so only the last drawn feature remains
546
        drawSource.clear();
×
547
        // fire an event when the drawing is complete
548
        drawSource.dispatchEvent({ type: 'localdrawend' });
×
549
    },
550

551
    /**
552
     * Handles the drawend event and gets the netsolver result which is passed to `handleFinalResult`
553
     * @param {ol.interaction.Draw.Event} evt The OpenLayers draw event containing the features
554
     */
555
    handleDrawEnd: function (evt) {
556
        var me = this;
×
557
        var view = me.getView();
×
558
        var resultPromise;
559

560
        me.blockEventHandling();
×
561

562
        switch (view.getType()) {
×
563
            case 'Point':
564
                var points = me.getSolverPoints();
×
565
                resultPromise = me.getNetByPoints(points.concat([evt.feature]));
×
566
                break;
×
567
            case 'Polygon':
568
                resultPromise = me.getNetByPolygon(evt.feature);
×
569
                break;
×
570
            case 'Circle':
571
                resultPromise = me.getNetByCircle(evt.feature);
×
572
                break;
×
573
            default:
574
                BasiGX.warn('Please implement your custom handler here for ' + view.getType());
×
575
                return;
×
576
        }
577

578
        resultPromise
×
579
            .then(me.handleFinalResult.bind(me))
580
            .then(me.updateDrawSource.bind(me));
581
    },
582

583

584
    /**
585
     * Handles the modifyend event and gets the netsolver result which is passed to `handleFinalResult`
586
     * @param {ol.interaction.Modify.Event} evt The OpenLayers modify event containing the features
587
     */
588
    handleModifyEnd: function (evt) {
589
        var me = this;
×
590
        var view = me.getView();
×
591
        var resultPromise;
592

593
        me.blockEventHandling();
×
594

595
        switch (view.getType()) {
×
596
            case 'Point':
597
                // find modified feature
598
                var drawFeature = me.map.getFeaturesAtPixel(evt.mapBrowserEvent.pixel, {
×
599
                    layerFilter: function (layer) {
600
                        return layer === me.drawLayer;
×
601
                    }
602
                })[0];
603

604
                var index = drawFeature.get('index');
×
605
                var points = me.getSolverPoints();
×
606

607
                if (index === points.length - 1) {
×
608
                    points.splice(index, 1, drawFeature);
×
609
                    resultPromise = me.getNetByPoints(points);
×
610
                } else {
611
                    // we first get the corrected point from the netsolver and then recalculate the whole path
612
                    resultPromise = me.getNetByPoints([drawFeature])
×
613
                        .then(function (features) {
614
                            if (features) {
×
615
                                var newFeature = features[0];
×
616
                                newFeature.set('index', index);
×
617
                                points.splice(index, 1, newFeature);
×
618
                                return me.getNetByPoints(points);
×
619
                            }
620
                        });
621
                }
622

623
                break;
×
624
            case 'Polygon':
625
                resultPromise = me.getNetByPolygon(evt.features.getArray()[0]);
×
626
                break;
×
627
        }
628

629
        resultPromise
×
630
            .then(me.handleFinalResult.bind(me))
631
            .then(me.updateDrawSource.bind(me));
632
    },
633

634
    /**
635
     * Handles a click registered by the pointer interaction if the deleteCondition is met.
636
     * If it returns false all other interaction at this point are ignored
637
     * @param {ol.MapBrowserEvent} evt
638
     */
639
    handlePointDelete: function (evt) {
640
        var me = this;
×
641

642
        var features = me.map.getFeaturesAtPixel(evt.pixel, {
×
643
            layerFilter: function (layer) {
644
                return layer === me.drawLayer;
×
645
            }
646
        });
647
        if (features.length > 0) {
×
648
            me.blockEventHandling();
×
649

650
            var drawFeature = features[0];
×
651

652
            var points = me.getSolverPoints();
×
653
            points.splice(drawFeature.get('index'), 1);
×
654

655
            if (Ext.isEmpty(points)) {
×
656
                me.handleFinalResult([]);
×
657
                me.updateDrawSource();
×
658
            } else {
659
                me.getNetByPoints(points)
×
660
                    .then(me.handleFinalResult.bind(me))
661
                    .then(me.updateDrawSource.bind(me));
662
            }
663

664
            return false;
×
665
        } else {
666
            return true;
×
667
        }
668
    },
669

670
    /**
671
     * Handles the click registered by the pointer interaction.
672
     * If it returns false all other interaction at this point are ignored
673
     * @param {ol.MapBrowserEvent} evt
674
     */
675
    handlePointClick: function (evt) {
676
        var me = this;
×
677

678
        var features = me.map.getFeaturesAtPixel(evt.pixel, {
×
679
            layerFilter: function (layer) {
680
                return layer === me.drawLayer;
×
681
            }
682
        });
683

684
        var points = me.getSolverPoints();
×
685

686
        if (features.length > 0 && features[0] !== points[points.length - 1]) {
×
687

688
            // for pointerdown and pointerup events we simply want to return
689
            // we only want to create a new point on a user click
690
            if (evt.type !== 'singleclick') {
×
691
                return true;
×
692
            }
693

694
            me.blockEventHandling();
×
695

696
            // to allow loops to be created we need to allow users to click on the
697
            // start point to add a duplicate end point
698

699
            me.getNetByPoints(points.concat([features[0]]))
×
700
                .then(me.handleFinalResult.bind(me))
701
                .then(me.updateDrawSource.bind(me));
702

703
            return false;
×
704
        } else {
705
            // allow edges to be selected by clicking on them
706
            // by temporarily setting a blockedEventHandling flag
707
            var hasEdge = me.map.hasFeatureAtPixel(evt.pixel, {
×
708
                layerFilter: function (layer) {
709
                    return layer === me.resultLayer;
×
710
                }
711
            });
712
            if (hasEdge) {
×
713
                me.blockEventHandling();
×
714
            }
715
            return !hasEdge;
×
716
        }
717
    },
718

719
    /**
720
     * Handles the draw end event of the circle type by getting the feature and passing it
721
     * to the CircleSelection component
722
     * @param {DrawEvent} evt The OpenLayers draw event containing the features
723
     */
724
    handleCircleDrawEnd: function (evt) {
725
        var me = this;
×
726
        me.blockEventHandling();
×
727
        // deactivate the creation of another circle
728
        me.drawInteraction.setActive(false);
×
729
        me.circleToolbar = Ext.create('CpsiMapview.view.toolbar.CircleSelectionToolbar', {
×
730
            feature: evt.feature,
731
        });
732
        me.circleToolbar.getController().on({
×
733
            circleSelectApply: me.onCircleSelectApply,
734
            circleSelectCancel: me.onCircleSelectCancel,
735
            scope: me
736
        });
737
        me.circleToolbarParent.addDocked(me.circleToolbar);
×
738
    },
739

740
    /**
741
     * Handles the `apply` event of the CircleSelection by passing the created circle
742
     * to the `handleDrawEnd` function. Also handles the cleanup of the CircleSelection toolbar
743
     * and enables the drawing interaction
744
     * @param {ol.Feature} feat
745
     */
746
    onCircleSelectApply: function (feat) {
747
        var me = this;
×
748
        var evt = { feature: feat };
×
749
        me.handleDrawEnd(evt);
×
750
        me.removeCircleSelectToolbar();
×
751
        me.drawInteraction.setActive(true);
×
752
    },
753

754
    /**
755
     * Handles the `cancel` event of the CircleSelection by cleaning up the CircleSelection toolbar
756
     * and enabling the drawing interaction
757
     */
758
    onCircleSelectCancel: function () {
759
        var me = this;
×
760
        me.drawLayer.getSource().clear();
×
761
        me.removeCircleSelectToolbar();
×
762
        me.drawInteraction.setActive(true);
×
763
    },
764

765
    /**
766
     * Handles the removal of the CircleSelect toolbar
767
     */
768
    removeCircleSelectToolbar: function () {
769
        var me = this;
×
770
        me.circleToolbarParent.removeDocked(me.circleToolbar);
×
771
        me.circleToolbar = null;
×
772
    },
773

774
    /**
775
     * Asynchronously gets a path between the given points from the netsolver.
776
     * @param {ol.Feature[]} features Expects the features in the correct order for solving. The coordinates of the last
777
     *      feature will get corrected by the netsolver. The other coordinates need to be valid coordinates for the
778
     *      netsolver (i.e. already corrected points)
779
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
780
     */
781
    getNetByPoints: function (features) {
782
        var me = this;
×
783
        var view = me.getView();
×
784
        var format = new ol.format.GeoJSON({
×
785
            dataProjection: me.map.getView().getProjection().getCode()
786
        });
787
        var jsonParams, searchParams;
788

789
        features.forEach(function (feature, index) {
×
790
            feature.set('index', index);
×
791
        });
792

793
        // The Netsolver endpoint expects bbox to be sent within a request.
794
        // The lower left and upper right coordinates cannot be the same so
795
        // we have to apply a small buffer on the point geometry to get a
796
        // small bbox around the clicked point.
797
        if (view.getPointExtentBuffer()) {
×
798
            jsonParams = format.writeFeatures(features.slice(0, -1));
×
799
            var extent = features[features.length - 1].getGeometry().getExtent();
×
800
            var buffered = ol.extent.buffer(extent, view.getPointExtentBuffer());
×
801
            searchParams = 'bbox=' + encodeURIComponent(buffered.join(','));
×
802
        } else {
803
            jsonParams = format.writeFeatures(features);
×
804
        }
805

806
        return me.doAjaxRequest(jsonParams, searchParams)
×
807
            .then(me.parseNetsolverResponse.bind(me));
808
    },
809

810
    /**
811
     * Asynchronously gets all lines inside the given polygon from the netsolver
812
     * @param {ol.Feature} feat
813
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
814
     */
815
    getNetByPolygon: function (feat) {
816
        var me = this;
×
817
        var srs = me.map.getView().getProjection().getCode();
×
818
        var format = new ol.format.GeoJSON({
×
819
            dataProjection: srs
820
        });
821
        var geoJson = format.writeFeature(feat);
×
822
        var jsonParams = {};
×
823
        var geometryParamName = 'geometry' + srs.replace('EPSG:', '');
×
824
        jsonParams[geometryParamName] = Ext.JSON.decode(geoJson).geometry;
×
825
        return me.doAjaxRequest(jsonParams)
×
826
            .then(me.parseNetsolverResponse.bind(me));
827
    },
828

829
    /**
830
     * Asynchronously gets all lines inside the given circle from the netsolver
831
     * @param {ol.Feature} feat
832
     * @returns {Ext.Promise<ol.Feature[]|undefined>}
833
     */
834
    getNetByCircle: function (feat) {
835
        // ol circle objects consist of a center coordinate and a radius in the
836
        // unit of the projection. In order to convert it into a geoJSON, we have
837
        // to convert the circle to a polygon first.
838
        var circleAsPolygon = new ol.geom.Polygon.fromCircle(feat.getGeometry());
×
839
        var polygonAsFeature = new ol.Feature({ geometry: circleAsPolygon });
×
840

841
        return this.getNetByPolygon(polygonAsFeature);
×
842
    },
843

844
    /**
845
     * Parses the netsolver result to openlayers features
846
     * @param {XMLHttpRequest} response
847
     * @returns {ol.Feature[]}
848
     */
849
    parseNetsolverResponse: function (response) {
850
        if (response) {
2!
851
            var me = this;
2✔
852
            var format = new ol.format.GeoJSON();
2✔
853
            var json;
854

855
            if (!Ext.isEmpty(response.responseText)) {
2!
856
                try {
2✔
857
                    json = Ext.decode(response.responseText);
2✔
858
                } catch (e) {
859
                    BasiGX.error('Could not parse the response: ' +
×
860
                        response.responseText);
861
                    return;
×
862
                }
863
                if (json.success && json.data && json.data.features) {
2✔
864
                    var features = json.data.features;
1✔
865

866
                    return features.map(function (feat) {
1✔
867
                        // api will respond with non unique ids, which
868
                        // will collide with OpenLayers feature ids not
869
                        // being unique. That's why we delete it here.
870
                        delete feat.id;
1✔
871
                        // set the current active group as property
872
                        feat.properties.group = me.activeGroupIdx;
1✔
873
                        return format.readFeature(feat);
1✔
874
                    });
875
                } else {
876
                    Ext.toast({
1✔
877
                        html: 'No features found at this location' +
878
                            (json.message ? ' (' + json.message + ')' : ''),
1!
879
                        title: 'No Features',
880
                        width: 200,
881
                        align: 'br'
882
                    });
883
                }
884
            } else {
885
                BasiGX.error('Response was empty');
×
886
            }
887
        }
888
    },
889

890
    /**
891
     * Issues an Ext.Ajax.request against the configured endpoint with
892
     * the given params.
893
     * @param {object} jsonParams Object containing the params to send
894
     * @param {string} searchParams The serarchParams which will be
895
     *   appended to the request url
896
     * @returns {Ext.request.Base}
897
     */
898
    doAjaxRequest: function (jsonParams, searchParams) {
899
        var me = this;
×
900
        var mapComponent = me.mapComponent || BasiGX.util.Map.getMapComponent();
×
901
        var view = me.getView();
×
902
        var url = view.getApiUrl();
×
903

904
        if (!url) {
×
905
            return;
×
906
        }
907

908
        if (searchParams) {
×
909
            url = Ext.urlAppend(url, searchParams);
×
910
        }
911

912
        mapComponent.setLoading(true);
×
913

914
        return new Ext.Promise(function (resolve) {
×
915
            Ext.Ajax.request({
×
916
                url: url,
917
                method: 'POST',
918
                jsonData: jsonParams,
919
                success: function (response) {
920
                    mapComponent.setLoading(false);
×
921
                    resolve(response);
×
922
                },
923
                failure: function (response) {
924
                    mapComponent.setLoading(false);
×
925

926
                    if (response.aborted !== true) {
×
927
                        var errorMessage = 'Error while requesting the API endpoint';
×
928

929
                        if (response.responseText && response.responseText.message) {
×
930
                            errorMessage += ': ' + response.responseText.message;
×
931
                        }
932

933
                        BasiGX.error(errorMessage);
×
934
                    }
935
                }
936
            });
937
        });
938
    },
939

940
    updateDrawSource: function () {
941
        var me = this;
×
942
        var view = me.getView();
×
943

944
        var drawSource = me.drawLayer.getSource();
×
945
        var type = view.getType();
×
946

947
        if (type === 'Point') {
×
948
            drawSource.clear();
×
949

950
            var drawFeatures = this.getSolverPoints()
×
951
                .map(function (feature) {
952
                    return feature.clone();
×
953
                });
954
            drawSource.addFeatures(drawFeatures);
×
955
        } else if (type === 'Polygon' || type === 'Circle') {
×
956
            if (drawSource.getFeatures().length > 1) {
×
957
                // keep the last drawn feature and remove the oldest one
958
                // it seems that the a half-completed draw polygon can consist of multiple features
959
                drawSource.removeFeature(drawSource.getFeatures()[0]);
×
960
            }
961
        }
962
    },
963

964
    /***
965
     * Get the total length of all features in the results layer
966
     * If a feature does not have a length property it will be assumed to
967
     * have a length of 0 (for example points)
968
     * */
969
    getResultGeometryLength: function () {
970

971
        var me = this;
1✔
972
        var allFeatures = me.resultLayer.getSource().getFeatures();
1✔
973
        var resultLength = 0;
1✔
974

975
        Ext.each(allFeatures, function (f) {
1✔
976
            if (f.get('group') === me.activeGroupIdx) {
×
977
                resultLength += f.get('length') ? f.get('length') : 0;
×
978
            }
979
        });
980

981
        return resultLength;
1✔
982
    },
983

984
    /**
985
     * Handles the final result from netsolver.
986
     * Features will get set a new property `group` in order
987
     * to maintain their membership to the current selected group.
988
     * A responseFeatures event is fired.
989
     * @param {undefined|ol.Feature[]} features The features returned from the API.
990
     */
991
    handleFinalResult: function (features) {
992
        if (features) {
1!
993
            var me = this;
1✔
994

995
            var originalSolverPoints = me.getSolverPoints();
1✔
996
            var originalLength = me.getResultGeometryLength();
1✔
997

998
            // get the original solver points before they are removed
999
            var resultSource = me.resultLayer.getSource();
1✔
1000
            // remove all features from the current active group
1001
            var allFeatures = me.resultLayer.getSource().getFeatures().slice(0);
1✔
1002
            Ext.each(allFeatures, function (f) {
1✔
1003
                if (f.get('group') === me.activeGroupIdx || !f.get('group')) {
×
1004
                    if (!f.get('group')) {
×
1005
                        // the property must be updated before removing the feature, or it is readded to the store
1006
                        f.set('group', me.activeGroupIdx);
×
1007
                    }
1008
                    resultSource.removeFeature(f);
×
1009
                }
1010
            });
1011

1012
            // add the new features for the current active group
1013
            resultSource.addFeatures(features);
1✔
1014

1015
            // now get the new solver points once they have been added
1016
            var newSolverPoints = me.getSolverPoints();
1✔
1017
            var newLength = 0;
1✔
1018

1019
            Ext.each(features, function (f) {
1✔
1020
                newLength += f.get('length') ? f.get('length') : 0;
2✔
1021
            });
1022

1023
            var newEdgeCount = features.filter(function (feature) {
1✔
1024
                return feature.getGeometry() instanceof ol.geom.LineString;
2✔
1025
            }).length;
1026

1027
            var modifications = {
1✔
1028
                originalLength: originalLength,
1029
                newLength: newLength,
1030
                newEdgeCount: newEdgeCount,
1031
                originalSolverPoints: originalSolverPoints,
1032
                newSolverPoints: newSolverPoints,
1033
                toolType: me.getView().type
1034
            };
1035

1036
            // fire a custom event from the source so a listener can be added once
1037
            // all features have been added/removed
1038
            // the event object includes a custom modifications object containing
1039
            // details of before and after the solve
1040
            resultSource.dispatchEvent({ type: 'featuresupdated', modifications: modifications });
1✔
1041

1042
            // The response from the API, parsed as OpenLayers features, will be
1043
            // fired here and the event can be used application-wide to access
1044
            // and handle the feature response.
1045
            me.getView().fireEvent('responseFeatures', features);
1✔
1046
        }
1047
    },
1048

1049
    /**
1050
     * Remove the interaction when this component gets destroyed
1051
     */
1052
    onBeforeDestroy: function () {
1053

1054
        var me = this;
×
1055
        var btn = me.getView();
×
1056

1057
        // detoggle button
1058
        me.onToggle(btn, false);
×
1059

1060
        // fire the button's toggle event so that the defaultClickEnabled property
1061
        // is updated in CpsiMapview.util.ApplicationMixin to re-enable clicks
1062
        btn.pressed = false;
×
1063
        btn.fireEvent('toggle');
×
1064

1065
        if (me.drawInteraction) {
×
1066
            me.map.removeInteraction(me.drawInteraction);
×
1067
        }
1068

1069
        if (me.modifyInteraction) {
×
1070
            me.map.removeInteraction(me.modifyInteraction);
×
1071
        }
1072

1073
        if (me.pointerInteraction) {
×
1074
            me.map.removeInteraction(me.pointerInteraction);
×
1075
        }
1076

1077
        if (me.snapVertexInteraction) {
×
1078
            me.map.removeInteraction(me.snapVertexInteraction);
×
1079
        }
1080

1081
        if (me.snapEdgeInteraction) {
×
1082
            me.map.removeInteraction(me.snapEdgeInteraction);
×
1083
        }
1084

1085
        if (me.drawLayer) {
×
1086
            me.map.removeLayer(me.drawLayer);
×
1087
        }
1088

1089
        if (me.resultLayer) {
×
1090
            me.map.removeLayer(me.resultLayer);
×
1091
        }
1092

1093
        if (me.circleToolbar) {
×
1094
            me.circleToolbar.destroy();
×
1095
        }
1096

1097
    },
1098

1099
    /**
1100
     * Zooms the map to the extent of the clicked feature
1101
     * Method may be removed as its actually a showcase, like `onResponseFeatures`
1102
     */
1103
    zoomToFeatures: function (grid, td, index, rec) {
1104
        var me = this;
×
1105
        var extent = rec.olObject.getGeometry().getExtent();
×
1106
        me.map.getView().fit(extent, {
×
1107
            size: me.map.getSize(),
1108
            padding: [5, 5, 5, 5]
1109
        });
1110
    },
1111

1112
    /**
1113
     * Showcasing the handling of the response features by adding them
1114
     * to an `GeoExt.data.store.Features` and showing them in a grid.
1115
     * Method may be removed as its actually a showcase, like `zoomToFeatures`
1116
     */
1117
    onResponseFeatures: function () {
1118
        // the code below is just a show case representing how the response
1119
        // features can be used within a feature grid.
1120
        var me = this;
×
1121

1122
        var featStore = Ext.create('GeoExt.data.store.Features', {
×
1123
            layer: this.resultLayer,
1124
            map: me.map
1125
        });
1126

1127
        featStore.filterBy(function (rec) {
×
1128
            return rec.get('geometry').getType() !== 'Point';
×
1129
        });
1130

1131
        var view = me.getView();
×
1132
        var selectStyle = view.getResultLayerSelectStyle();
×
1133

1134
        if (me.win) {
×
1135
            me.win.destroy();
×
1136
        }
1137
        me.win = Ext.create('CpsiMapview.view.window.MinimizableWindow', {
×
1138
            height: 500,
1139
            width: 300,
1140
            layout: 'fit',
1141
            title: 'Your data',
1142
            name: 'gridwin',
1143
            items: [{
1144
                xtype: 'grid',
1145
                store: featStore,
1146
                selModel: {
1147
                    type: 'featurerowmodel',
1148
                    mode: 'MULTI',
1149
                    allowDeselect: true,
1150
                    mapSelection: true,
1151
                    selectStyle: selectStyle,
1152
                    map: me.map
1153
                },
1154
                columns: [{
1155
                    xtype: 'widgetcolumn',
1156
                    width: 40,
1157
                    widget: {
1158
                        xtype: 'gx_renderer'
1159
                    },
1160
                    onWidgetAttach: function (column, gxRenderer, record) {
1161
                        // update the symbolizer with the related feature
1162
                        var featureRenderer = GeoExt.component.FeatureRenderer;
×
1163
                        var feature = record.getFeature();
×
1164
                        gxRenderer.update({
×
1165
                            feature: feature,
1166
                            symbolizers: featureRenderer.determineStyle(record)
1167
                        });
1168
                    }
1169
                }, {
1170
                    text: 'ID',
1171
                    dataIndex: 'segmentId',
1172
                    flex: 1
1173
                }, {
1174
                    text: 'Code',
1175
                    dataIndex: 'segmentCode',
1176
                    flex: 1
1177
                }, {
1178
                    text: 'Length',
1179
                    dataIndex: 'segmentLength',
1180
                    flex: 1,
1181
                    renderer: function (val) {
1182
                        return Ext.String.format(
×
1183
                            '{0} m',
1184
                            val.toFixed(0).toString()
1185
                        );
1186
                    }
1187
                }]
1188
            }]
1189
        });
1190
        me.win.showAt(100, 100);
×
1191
    },
1192

1193
    /**
1194
     * Clears all features of the active group from the result source
1195
     * and fire a custom featuresupdated event
1196
     * If no activeGroupIdx is supplied then all features are removed from the
1197
     * resultLayer
1198
     */
1199
    clearActiveGroup: function (activeGroupIdx) {
1200
        var me = this;
×
1201
        var view = me.getView();
×
1202

1203
        if (!me.resultLayer) {
×
1204
            // no results have been returned so nothing to clear
1205
            return;
×
1206
        }
1207

1208
        var originalSolverPoints = me.getSolverPoints();
×
1209
        var originalLength = me.getResultGeometryLength();
×
1210

1211
        var resultSource = me.resultLayer.getSource();
×
1212

1213
        if (view.getGroups() === true) {
×
1214
            resultSource.getFeatures()
×
1215
                .slice(0)
1216
                .filter(function (feature) {
1217
                    return feature.get('group') === activeGroupIdx;
×
1218
                })
1219
                .forEach(function (feature) {
1220
                    resultSource.removeFeature(feature);
×
1221
                });
1222
        } else {
1223
            // remove all features
1224
            resultSource.clear();
×
1225
        }
1226

1227
        var modifications = {
×
1228
            originalLength: originalLength,
1229
            newLength: 0,
1230
            originalSolverPoints: originalSolverPoints,
1231
            newSolverPoints: []
1232
        };
1233

1234
        resultSource.dispatchEvent({ type: 'featuresupdated', modifications: modifications });
×
1235
        me.updateDrawSource();
×
1236

1237
        // also fire a view event
1238
        me.getView().fireEvent('responseFeatures', []);
×
1239

1240
    },
1241

1242
    init: function () {
1243

1244
        var me = this;
26✔
1245

1246
        // create an object for the contextmenu eventhandler
1247
        // so it can be removed correctly
1248
        me.contextHandler = {
26✔
1249
            handleEvent: me.showContextMenu,
1250
            scope: me
1251
        };
1252
    }
1253
});
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