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

compassinformatics / cpsi-mapview / 22096723327

17 Feb 2026 11:29AM UTC coverage: 26.228% (+0.03%) from 26.203%
22096723327

Pull #719

github

web-flow
Merge eae267f8c into 5e19e710a
Pull Request #719: Change pointer when hovering over clickable features

499 of 2409 branches covered (20.71%)

Branch coverage included in aggregate %.

7 of 15 new or added lines in 2 files covered. (46.67%)

1 existing line in 1 file now uncovered.

1476 of 5121 relevant lines covered (28.82%)

2.38 hits per line

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

8.4
/app/controller/MapController.js
1
/**
2
 * This class is the controller for the map view of cpsi mapview
3
 *
4
 */
5
Ext.define('CpsiMapview.controller.MapController', {
2✔
6
    extend: 'Ext.app.ViewController',
7

8
    alias: 'controller.cmv_map',
9

10
    requires: ['BasiGX.util.Map'],
11

12
    mixins: {
13
        editWindowOpenerMixin: 'CpsiMapview.util.EditWindowOpenerMixin'
14
    },
15

16
    /**
17
     * A configuration object to store settings relating to opening forms when clicking on a feature.
18
     * This should be overridden in inherited projects and should be in the following form:
19
     *
20
     * {
21
     *   layerKeyName: {
22
     *       keyField: 'ObjectId', // the field containing the unique feature Id to open in a form
23
     *       modelClass: 'CpsiMapview.model.ModelName', // the name of the class used to load a model
24
     *       editWindowClass: 'CpsiMapview.view.EditWindow' // the name of the form view class to open when clicking on a feature
25
     *   }
26
     * }
27
     *
28
     * You also use a function to return a configuration object which returns an Id instead of a
29
     * keyField which allows custom logic to be applied. The function receives a single argument which
30
     * is the clicked ol.Feature
31
     * {
32
     *   layerKeyName: function (feat) {
33
     *       return {
34
     *           id: Math.abs(feat.get('ObjectId')), // the unique feature Id to open in a form
35
     *           modelClass: 'CpsiMapview.model.ModelName', // the name of the class used to load a model
36
     *           editWindowClass: 'CpsiMapview.view.EditWindow' // the name of the form view class to open when clicking on a feature
37
     *       }
38
     *   }
39
     * }
40
     *
41
     * Multiple configurations can be provided to support clicks on multiple layers
42
     */
43
    clickableLayerConfigs: {},
44

45
    /**
46
     * Change the mouse cursor to a pointer if hovering over a feature that can be clicked to open a form
47
     * @param {any} evt
48
     * @returns
49
     */
50
    onPointerMove: function (evt) {
51
        const me = this;
4✔
52
        const map = me.getView().map;
4✔
53
        // the hover hitTolerance should match the click hitTolerance
54
        const hitTolerance = me.getView().mapViewConfig.hitTolerance;
4✔
55

56
        if (evt.dragging) return;
4!
57
        const hit = map.hasFeatureAtPixel(evt.pixel, {
4✔
58
            hitTolerance: hitTolerance, // add a pixel-buffer
59
            layerFilter: function (layer) {
NEW
60
                const layerKey = layer.get('layerKey');
×
NEW
61
                return layerKey && me.clickableLayerConfigs[layerKey];
×
62
            }
63
        });
64

65
        map.getTargetElement().style.cursor = hit ? 'pointer' : '';
4✔
66
    },
67

68
    /**
69
     * Handle map clicks so data entry forms can be opened by clicking directly
70
     * on vector features
71
     * @param {any} clickedFeatures
72
     */
73
    onMapClick: function (clickedFeatures) {
74
        let feat, recId;
75
        const me = this;
×
76

77
        // Prevent triggering other map events, if we click on the coordinate marker
78
        const coordinateMousePanel = Ext.ComponentQuery.query(
×
79
            'basigx-panel-coordinatemouseposition'
80
        )[0];
81
        if (coordinateMousePanel) {
×
82
            const coordinateMarker = Ext.Array.findBy(
×
83
                clickedFeatures,
84
                function (clickedFeature) {
85
                    return coordinateMousePanel.fireEvent(
×
86
                        'isMapMarker',
87
                        clickedFeature.feature
88
                    );
89
                }
90
            );
91
            if (coordinateMarker) {
×
92
                coordinateMousePanel.fireEvent('removeMarker');
×
93
                return false;
×
94
            }
95
        }
96
        // avoid opening the form if another tool is active (see CpsiMapview.util.ApplicationMixin)
97
        const map = me.getView().map;
×
98
        if (map.get('defaultClickEnabled') === false) {
×
99
            return;
×
100
        }
101

102
        // filter out any features without the layer we want
103
        const editableFeatures = [];
×
104

105
        Ext.each(clickedFeatures, function (f) {
×
106
            if (f.layer) {
×
107
                const layerKey = f.layer.get('layerKey');
×
108
                if (
×
109
                    Object.prototype.hasOwnProperty.call(
110
                        me.clickableLayerConfigs,
111
                        layerKey
112
                    )
113
                ) {
114
                    editableFeatures.push(f);
×
115
                }
116
            }
117
        });
118

119
        if (editableFeatures.length > 0) {
×
120
            const editableFeature = editableFeatures[0].feature; // get the first feature
×
121
            const selectedLayerKey = editableFeatures[0].layer.get('layerKey'); // get layer for feature
×
122

123
            const featureCluster = editableFeature.getProperties().features;
×
124

125
            if (featureCluster) {
×
126
                feat = featureCluster[0];
×
127
            } else {
128
                feat = editableFeature;
×
129
            }
130

131
            // get the window and model types
132
            const selectedLayerConfig =
133
                me.clickableLayerConfigs[selectedLayerKey];
×
134
            let configObject;
135

136
            if (typeof selectedLayerConfig === 'function') {
×
137
                configObject = selectedLayerConfig(feat) || {};
×
138
                recId = configObject.id;
×
139
            } else {
140
                configObject = selectedLayerConfig;
×
141
                recId = feat.get(configObject.keyField);
×
142
            }
143

144
            if (!recId) {
×
145
                // return early and stop other map handlers
146
                return false;
×
147
            }
148

149
            const modelClass = configObject.modelClass;
×
150
            const editWindowClass = configObject.editWindowClass;
×
151

152
            const modelPrototype = Ext.ClassManager.get(modelClass);
×
153

154
            // check if the window is already open
155
            let win = me.getExistingEditingFormWindow(recId, editWindowClass);
×
156

157
            // if the record is not already opened, create a new window and load the record
158
            if (win === null) {
×
NEW
159
                const mainPanel = me.getView().up();
×
NEW
160
                if (mainPanel && mainPanel.mask) {
×
NEW
161
                    mainPanel.mask('Loading...');
×
162
                }
UNCOV
163
                modelPrototype.load(recId, {
×
164
                    success: function (rec) {
165
                        win = Ext.create(editWindowClass);
×
166
                        const vm = win.getViewModel();
×
167
                        vm.set('currentRecord', rec);
×
168
                        win.show();
×
169
                    },
170
                    failure: function () {
171
                        Ext.toast({
×
172
                            html: 'Cannot load the record with id ' + recId,
173
                            title: 'Record Loading Failed',
174
                            width: 200,
175
                            align: 'br'
176
                        });
177
                    },
178
                    callback: function () {
179
                        // remove the mask once loaded
NEW
180
                        if (mainPanel && mainPanel.unmask) {
×
NEW
181
                            mainPanel.unmask();
×
182
                        }
183
                    }
184
                });
185
            } else {
186
                // if the window is minimised make sure it is restored
187
                if (win.isMinimized) {
×
188
                    win.show();
×
189
                }
190
                Ext.WindowManager.bringToFront(win);
×
191
            }
192

193
            return false; // stop other map handlers
×
194
        }
195
    },
196

197
    /**
198
     * Function called after render of map component
199
     */
200
    afterMapRender: function () {
201
        const me = this;
×
202
        const mapPanel = me.getView();
×
203

204
        if (mapPanel.addScaleBarToMap) {
×
205
            const removeCtrls = [];
×
206
            // Cleanup existing controls first
207
            mapPanel.olMap.getControls().forEach(function (ctrl) {
×
208
                if (ctrl instanceof ol.control.ScaleLine) {
×
209
                    removeCtrls.push(ctrl);
×
210
                }
211
            });
212
            Ext.each(removeCtrls, function (removeCtrl) {
×
213
                mapPanel.olMap.removeControl(removeCtrl);
×
214
            });
215

216
            const sbTarget =
217
                mapPanel.body && mapPanel.body.dom
×
218
                    ? mapPanel.body.dom
219
                    : mapPanel.getEl().dom;
220
            const scaleLineCtrl = new ol.control.ScaleLine({
×
221
                target: sbTarget
222
            });
223
            mapPanel.olMap.addControl(scaleLineCtrl);
×
224
        }
225
    }
226
});
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