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

jumpinjackie / mapguide-react-layout / 15160437878

21 May 2025 11:00AM UTC coverage: 21.631% (-42.6%) from 64.24%
15160437878

Pull #1552

github

web-flow
Merge 8b7153d9e into 236e2ea07
Pull Request #1552: Feature/package updates 2505

839 of 1165 branches covered (72.02%)

11 of 151 new or added lines in 25 files covered. (7.28%)

1332 existing lines in 50 files now uncovered.

4794 of 22163 relevant lines covered (21.63%)

6.89 hits per line

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

0.0
/src/components/tooltips/selected-features.ts
1
import olMap from "ol/Map";
1✔
2
import olOverlay from "ol/Overlay";
×
3
import olImageLayer from "ol/layer/Image";
×
4
import olTileLayer from "ol/layer/Tile";
×
5
import olSourceWMS from "ol/source/ImageWMS";
×
6
import olSourceTileWMS from "ol/source/TileWMS";
×
7
import GeoJSON from "ol/format/GeoJSON";
×
8
import Collection from 'ol/Collection';
9
import { tr } from '../../api/i18n';
×
10
import { ILayerManager, Coordinate2D, LayerProperty, Dictionary } from '../../api/common';
×
11
import { Client } from '../../api/client';
×
12
import { parseEpsgCodeFromCRS } from '../layer-manager/wfs-capabilities-panel';
×
13
import { ProjectionLike } from 'ol/proj';
14
import { LayerSetGroupBase } from '../../api/layer-set-group-base';
15
import { ISelectedFeaturePopupTemplateConfiguration } from '../../actions/defs';
16
import { strIsNullOrEmpty, extractPlaceholderTokens, strReplaceAll } from '../../utils/string';
×
17
import LayerBase from "ol/layer/Base";
18
import { WmsQueryAugmentation } from '../map-providers/base';
19
import { isClusteredFeature, getClusterSubFeatures } from '../../api/ol-style-helpers';
×
20
import stickybits from 'stickybits';
×
21
import type { OLFeature, OLLayer } from "../../api/ol-types";
NEW
22
import DOMPurify from "dompurify";
×
23

24
export interface IQueryWmsFeaturesCallback {
25
    getLocale(): string | undefined;
26
    /**
27
     * 
28
     * @param feat 
29
     * @param l 
30
     * @since 0.14
31
     */
32
    addClientSelectedFeature(feat: OLFeature, l: LayerBase): void;
33
    addFeatureToHighlight(feat: OLFeature | undefined, bAppend: boolean): void;
34
    getWmsRequestAugmentations(): Dictionary<WmsQueryAugmentation>;
35
}
36

37
/**
38
 * @since 0.14
39
 */
40
export type SelectionPopupContentRenderer = (feat: OLFeature, locale?: string, popupConfig?: ISelectedFeaturePopupTemplateConfiguration) => string;
41

42
/**
43
 * @since 0.14
44
 */
45
export interface ISelectionPopupContentOverrideProvider {
46
    getSelectionPopupRenderer(layerName: string): SelectionPopupContentRenderer | undefined;
47
}
48

49
function defaultPopupContentRenderer(feat: OLFeature, locale?: string, popupConfig?: ISelectedFeaturePopupTemplateConfiguration) {
×
50
    let html = "";
×
51
    const bClustered = isClusteredFeature(feat);
×
52

53
    let title = popupConfig?.title ?? tr("SEL_FEATURE_PROPERTIES", locale);
×
54
    const size = getClusterSubFeatures(feat)?.length;;
×
55
    if (bClustered && size > 1) {
×
56
        title = popupConfig?.clusteredTitle?.(size) ?? tr("SEL_CLUSTER_PROPERTIES", locale, { size });
×
57
    }
×
NEW
58
    html += "<div class='selected-popup-header'><div>" + DOMPurify.sanitize(title) + "</div><a id='feat-popup-closer' class='closer' href='#'>[x]</a><div class='clearit'></div></div>";
×
59

60
    const renderForMultipleSanitized = (subFeatures: OLFeature[]) => {
×
61
        let table = "<table class='selected-popup-cluster-table'>";
×
62
        const fheadings = popupConfig?.propertyMappings
×
63
            ? popupConfig.propertyMappings.filter(pm => pm.name != subFeatures[0].getGeometryName()).map(pm => pm.value)
×
64
            : Object.keys(subFeatures[0].getProperties()).filter(pn => pn != subFeatures[0].getGeometryName());
×
65
        const fprops = popupConfig?.propertyMappings
×
66
            ? popupConfig.propertyMappings.map(pm => pm.value)
×
67
            : Object.keys(subFeatures[0].getProperties()).filter(pn => pn != subFeatures[0].getGeometryName());
×
68
        table += "<thead><tr>";
×
69
        for (const heading of fheadings) {
×
NEW
70
            table += `<th>${DOMPurify.sanitize(heading)}</th>`;
×
71
        }
×
72
        table += "</tr></thead>";
×
73
        table += "<tbody>"
×
74
        for (const f of subFeatures) {
×
75
            table += "<tr>";
×
76
            for (const property of fprops) {
×
77
                const val = f.get(property);
×
NEW
78
                table += `<td>${DOMPurify.sanitize(val)}</td>`;
×
79
            }
×
80
            table += "</tr>";
×
81
        }
×
82
        table += "</tbody>";
×
83
        table += "</table>";
×
84
        return table
×
85
    };
×
86
    const renderForSingleSanitized = (feature: OLFeature): [string, number, string | undefined] => {
×
87
        let linkFragment: string | undefined;
×
88
        let table = "<table class='selected-popup-single-properties-table'>";
×
89
        table += "<tbody>";
×
90
        const f = feature.getProperties();
×
91
        let pc = 0;
×
92
        if (popupConfig?.propertyMappings) {
×
93
            for (const pm of popupConfig.propertyMappings) {
×
94
                if (pm.name == feat.getGeometryName()) {
×
95
                    continue;
×
96
                }
×
97
                table += "<tr>";
×
NEW
98
                table += "<td class='property-name-cell'>" + DOMPurify.sanitize(pm.value) + "</td>";
×
NEW
99
                table += "<td class='property-value-cell'>" + DOMPurify.sanitize(f[pm.name]) + "</td>";
×
100
                table += "</tr>";
×
101
                pc++;
×
102
            }
×
103
        } else {
×
104
            for (const key in f) {
×
105
                if (key == feat.getGeometryName()) {
×
106
                    continue;
×
107
                }
×
108
                table += "<tr>";
×
NEW
109
                table += "<td class='property-name-cell'>" + DOMPurify.sanitize(key) + "</td>";
×
NEW
110
                table += "<td class='property-value-cell'>" + DOMPurify.sanitize(f[key]) + "</td>";
×
111
                table += "</tr>";
×
112
                pc++;
×
113
            }
×
114
        }
×
115
        table += "</tbody>";
×
116
        table += "</table>";
×
117

118
        if (popupConfig?.linkProperty) {
×
119
            const { name, label, linkTarget } = popupConfig.linkProperty;
×
120
            let linkHref: string | undefined;
×
121
            if (typeof (name) == 'string') {
×
122
                linkHref = encodeURI(f[name]);
×
123
            } else {
×
124
                const expr = name.expression;
×
125
                let url = expr;
×
126
                const pBegin = name.placeholderBegin ?? "{";
×
127
                const pEnd = name.placeholderEnd ?? "}";
×
128
                const tokens = extractPlaceholderTokens(expr, pBegin, pEnd);
×
129
                for (const t of tokens) {
×
130
                    const al = encodeURIComponent(f[t] ?? "");
×
131
                    url = strReplaceAll(url, `${pBegin}${t}${pEnd}`, al);
×
132
                }
×
133
                linkHref = url;
×
134
            }
×
135
            if (!strIsNullOrEmpty(linkHref)) {
×
NEW
136
                linkFragment = `<div class='select-popup-single-link-wrapper'><a href="${DOMPurify.sanitize(linkHref)}" target='${DOMPurify.sanitize(linkTarget)}'>${DOMPurify.sanitize(label)}</a></div>`;
×
137
            }
×
138
        }
×
139
        return [table, pc, linkFragment];
×
140
    };
×
141
    const singlePopupContentRender = (feature: OLFeature, appendHtml: (h: string) => void) => {
×
142
        const [table, pc, linkFragment] = renderForSingleSanitized(feature);
×
143
        if (pc > 0) {
×
144
            appendHtml(`<div class='selected-popup-content-wrapper'>${table}</div>`);
×
145
        } else {
×
NEW
146
            appendHtml("<div class='selected-popup-content-none'>" + DOMPurify.sanitize(tr("SEL_FEATURE_PROPERTIES_NONE", locale)) + "</div>");
×
147
        }
×
148
        if (!strIsNullOrEmpty(linkFragment)) {
×
149
            appendHtml(linkFragment);
×
150
        }
×
151
    }
×
152

153
    if (bClustered) {
×
154
        const subFeatures = getClusterSubFeatures(feat);
×
155
        if (subFeatures.length == 1) {
×
156
            singlePopupContentRender(subFeatures[0], h => html += h);
×
157
        } else {
×
158
            const table = renderForMultipleSanitized(subFeatures);
×
159
            html += `<div class='selected-popup-content-wrapper'>${table}</div>`;
×
160
        }
×
161
    } else {
×
162
        singlePopupContentRender(feat, h => html += h);
×
163
    }
×
164
    return html;
×
165
}
×
166

167
export class SelectedFeaturesTooltip {
×
168
    private map: olMap;
169
    private featureTooltipElement: HTMLElement;
170
    private featureTooltip: olOverlay;
171
    private enabled: boolean;
172
    private isMouseOverTooltip: boolean;
173
    private closerEl: HTMLElement | null;
174
    constructor(map: olMap, private parent: ISelectionPopupContentOverrideProvider) {
×
175
        this.featureTooltipElement = document.createElement("div");
×
176
        this.featureTooltipElement.addEventListener("mouseover", () => this.isMouseOverTooltip = true);
×
177
        this.featureTooltipElement.addEventListener("mouseout", () => this.isMouseOverTooltip = false);
×
178
        this.featureTooltipElement.className = 'selected-tooltip';
×
179
        this.featureTooltip = new olOverlay({
×
180
            element: this.featureTooltipElement,
×
181
            offset: [15, 0],
×
182
            positioning: "center-left"
×
183
        })
×
184
        this.map = map;
×
185
        this.map.addOverlay(this.featureTooltip);
×
186
        this.enabled = true;
×
187
        this.isMouseOverTooltip = false;
×
188
    }
×
189
    public dispose() {
×
190
        this.featureTooltip.dispose();
×
191
    }
×
192
    public get isMouseOver() { return this.isMouseOverTooltip; }
×
193
    public isEnabled(): boolean {
×
194
        return this.enabled;
×
195
    }
×
196
    public setEnabled(enabled: boolean): void {
×
197
        this.enabled = enabled;
×
198
        if (!this.enabled) {
×
199
            this.featureTooltipElement.innerHTML = "";
×
200
            this.featureTooltipElement.classList.add("tooltip-hidden");
×
201
        }
×
202
    }
×
203
    public hide() {
×
204
        this.featureTooltipElement.innerHTML = "";
×
205
        this.featureTooltipElement.classList.add("tooltip-hidden");
×
206
    }
×
207
    public async queryWmsFeatures(currentLayerSet: LayerSetGroupBase | undefined, layerMgr: ILayerManager, coord: Coordinate2D, resolution: number, bAppendMode: boolean, callback: IQueryWmsFeaturesCallback) {
×
208
        let selected = 0;
×
209
        //See what WMS layers we have
210
        const client = new Client("", "mapagent");
×
211
        const format = new GeoJSON();
×
212
        const wmsSources: [LayerBase, (olSourceWMS | olSourceTileWMS)][] = [];
×
213
        //The active layer set may have a WMS layer
214
        const currentWmsSource = currentLayerSet?.tryGetWmsSource();
×
215
        if (currentWmsSource) {
×
216
            wmsSources.push(currentWmsSource);
×
217
        }
×
218
        const layers = layerMgr.getLayers().filter(l => l.visible && l.selectable && l.type == "WMS");
×
219
        for (const layer of layers) {
×
220
            const wmsLayer = layerMgr.getLayer(layer.name);
×
221
            if (wmsLayer instanceof olImageLayer || wmsLayer instanceof olTileLayer) {
×
222
                const source = wmsLayer.getSource();
×
223
                if (source instanceof olSourceWMS || source instanceof olSourceTileWMS) {
×
224
                    wmsSources.push([wmsLayer, source]);
×
225
                }
×
226
            }
×
227
        }
×
228
        for (const pair of wmsSources) {
×
229
            const [layer, source] = pair;
×
230
            let url = source.getFeatureInfoUrl(coord, resolution, this.map.getView().getProjection(), {
×
231
                'INFO_FORMAT': "application/json"
×
232
            });
×
233
            if (url) {
×
234
                const layerName = layer.get(LayerProperty.LAYER_NAME);
×
235
                //Check if we have an augmentation for this
236
                const augs = callback.getWmsRequestAugmentations();
×
237
                if (augs[layerName]) {
×
238
                    url = augs[layerName](url);
×
239
                }
×
240
                const resp = await client.getText(url);
×
241
                const json = JSON.parse(resp);
×
242
                if (json.features?.length > 0) {
×
243
                    let srcProj: ProjectionLike | null = source.getProjection();
×
244
                    if (!srcProj) {
×
245
                        const epsg = parseEpsgCodeFromCRS(json.crs?.properties?.name);
×
246
                        if (epsg) {
×
247
                            srcProj =  `EPSG:${epsg}`;
×
248
                        } else { //Type narrowing hack
×
249
                            srcProj = undefined;
×
250
                        }
×
251
                    }
×
252
                    const features = format.readFeatures(resp, {
×
253
                        dataProjection: srcProj,
×
254
                        featureProjection: this.map.getView().getProjection()
×
255
                    });
×
256
                    this.featureTooltip.setPosition(coord);
×
257
                    const popupConf: ISelectedFeaturePopupTemplateConfiguration | undefined = layer.get(LayerProperty.SELECTED_POPUP_CONFIGURATION);
×
258
                    const html = this.generateFeatureHtml(layerName, features[0], callback.getLocale(), popupConf);
×
259
                    callback.addClientSelectedFeature(features[0], layer);
×
260
                    currentLayerSet?.clearWmsSelectionOverlay();
×
261
                    currentLayerSet?.addWmsSelectionOverlay(features[0]);
×
262
                    callback.addFeatureToHighlight(features[0], bAppendMode);
×
263
                    selected++;
×
264
                    this.featureTooltipElement.innerHTML = html;
×
265
                    stickybits(".selected-popup-cluster-table th");
×
266
                    this.closerEl = document.getElementById("feat-popup-closer");
×
267
                    this.setPopupCloseHandler();
×
268
                    if (html == "") {
×
269
                        this.featureTooltipElement.classList.add("tooltip-hidden");
×
270
                    } else {
×
271
                        this.featureTooltipElement.classList.remove("tooltip-hidden");
×
272
                    }
×
273
                    return true;
×
274
                }
×
275
            }
×
276
        }
×
277
        // Clear if there was no selection made
278
        if (selected == 0) {
×
279
            callback.addFeatureToHighlight(undefined, false);
×
280
            this.hide();
×
281
        }
×
282
        return false;
×
283
    }
×
284
    private generateFeatureHtml(layerName: string | undefined, feat: OLFeature, locale?: string, popupConfig?: ISelectedFeaturePopupTemplateConfiguration) {
×
285
        if (layerName) {
×
286
            const customRenderer = this.parent.getSelectionPopupRenderer(layerName);
×
287
            if (customRenderer) {
×
288
                return customRenderer(feat, locale, popupConfig);
×
289
            } else {
×
290
                return defaultPopupContentRenderer(feat, locale, popupConfig);
×
291
            }
×
292
        }
×
293
        return defaultPopupContentRenderer(feat, locale, popupConfig);
×
294
    }
×
295
    private setPopupCloseHandler = () => {
×
296
        if (this.closerEl) {
×
297
            this.closerEl.onclick = this.closePopup;
×
298
        }
×
299
    }
×
300
    private closePopup = (e: any) => {
×
301
        e.preventDefault();
×
302
        this.hide();
×
303
        if (this.closerEl) {
×
304
            this.closerEl.onclick = null;
×
305
        }
×
306
        return false;
×
307
    };
×
308
    public showSelectedVectorFeatures(features: Collection<OLFeature>, pixel: [number, number], featureToLayerMap: [OLFeature, OLLayer][], locale?: string) {
×
309
        const coords = this.map.getCoordinateFromPixel(pixel);
×
310
        if (features.getLength() > 0) {
×
311
            this.featureTooltip.setPosition(coords);
×
312
            const f = features.item(0);
×
313
            let popupConf: ISelectedFeaturePopupTemplateConfiguration | undefined;
×
314
            const pair = featureToLayerMap.find(([feat, _]) => feat == f);
×
315
            let layerName;
×
316
            if (pair) {
×
317
                const layer = pair[1];
×
318
                popupConf = layer.get(LayerProperty.SELECTED_POPUP_CONFIGURATION);
×
319
                layerName = layer.get(LayerProperty.LAYER_NAME);
×
320
            }
×
321
            const html = this.generateFeatureHtml(layerName, f, locale, popupConf);
×
322
            this.featureTooltipElement.innerHTML = html;
×
323
            stickybits(".selected-popup-cluster-table th");
×
324
            this.closerEl = document.getElementById("feat-popup-closer");
×
325
            this.setPopupCloseHandler();
×
326
            if (html == "") {
×
327
                this.featureTooltipElement.classList.add("tooltip-hidden");
×
328
            } else {
×
329
                this.featureTooltipElement.classList.remove("tooltip-hidden");
×
330
            }
×
331
        } else {
×
332
            this.featureTooltipElement.innerHTML = "";
×
333
            this.featureTooltipElement.classList.add("tooltip-hidden");
×
334
        }
×
335
    }
×
336
}
×
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