xv-webcomponents 1.22.2 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cjs/loader.cjs.js +1 -1
  2. package/dist/cjs/maplibre-gl-CCZS3Rg5.js +900 -0
  3. package/dist/cjs/{xv-accordion-v2_48.cjs.entry.js → xv-accordion-v2_49.cjs.entry.js} +655 -66
  4. package/dist/cjs/xv-header-next.cjs.entry.js +1 -1
  5. package/dist/cjs/xv-mega-menu-next.cjs.entry.js +2 -2
  6. package/dist/cjs/xv-webcomponents.cjs.js +1 -1
  7. package/dist/collection/collection-manifest.json +1 -0
  8. package/dist/collection/components/xv-accordion/xv-accordion.js +24 -4
  9. package/dist/collection/components/xv-accordion-item/xv-accordion-item.js +11 -5
  10. package/dist/collection/components/xv-breadcrumbs/xv-breadcrumbs.js +1 -1
  11. package/dist/collection/components/xv-button/xv-button.js +2 -2
  12. package/dist/collection/components/xv-button-group/xv-button-group.js +1 -1
  13. package/dist/collection/components/xv-card/xv-card.js +1 -1
  14. package/dist/collection/components/xv-checkbox/xv-checkbox.js +2 -2
  15. package/dist/collection/components/xv-content-switcher/xv-content-switcher-item/xv-content-switcher-item.js +1 -1
  16. package/dist/collection/components/xv-content-switcher/xv-content-switcher.js +2 -2
  17. package/dist/collection/components/xv-data-table/xv-data-table-cell/xv-data-table-cell.js +1 -1
  18. package/dist/collection/components/xv-data-table/xv-data-table-collapse/xv-data-table-collapse.js +1 -1
  19. package/dist/collection/components/xv-data-table/xv-data-table-row/xv-data-table-row.js +2 -2
  20. package/dist/collection/components/xv-data-table/xv-data-table.js +1 -1
  21. package/dist/collection/components/xv-datepicker/xv-datepicker.js +63 -5
  22. package/dist/collection/components/xv-dropdown/xv-dropdown-item/xv-dropdown-item.js +1 -1
  23. package/dist/collection/components/xv-dropdown/xv-dropdown.js +1 -1
  24. package/dist/collection/components/xv-file-uploader/xv-file-uploader.js +1 -1
  25. package/dist/collection/components/xv-footer/xv-footer.js +1 -1
  26. package/dist/collection/components/xv-geo-map/xv-geo-map.css +907 -0
  27. package/dist/collection/components/xv-geo-map/xv-geo-map.js +984 -0
  28. package/dist/collection/components/xv-header/xv-header.js +1 -1
  29. package/dist/collection/components/xv-header-next/xv-header-next.js +1 -1
  30. package/dist/collection/components/xv-image-viewer/xv-image-viewer.js +1 -1
  31. package/dist/collection/components/xv-link/xv-link.js +1 -1
  32. package/dist/collection/components/xv-mega-menu/xv-mega-menu.js +2 -2
  33. package/dist/collection/components/xv-mega-menu-next/xv-mega-menu-next.js +2 -2
  34. package/dist/collection/components/xv-modal/xv-modal.js +1 -1
  35. package/dist/collection/components/xv-notification/xv-notification.js +1 -1
  36. package/dist/collection/components/xv-number-input/xv-number-input.js +2 -2
  37. package/dist/collection/components/xv-pagination/xv-pagination.js +1 -1
  38. package/dist/collection/components/xv-progress-indicator/xv-progress-indicator-item/xv-progress-indicator-item.js +1 -1
  39. package/dist/collection/components/xv-progress-indicator/xv-progress-indicator.js +1 -1
  40. package/dist/collection/components/xv-radio/xv-radio.js +2 -2
  41. package/dist/collection/components/xv-slider/xv-slider.js +1 -1
  42. package/dist/collection/components/xv-table/xv-table-cell/xv-table-cell.js +1 -1
  43. package/dist/collection/components/xv-table/xv-table-expand/xv-table-expand.js +1 -1
  44. package/dist/collection/components/xv-table/xv-table-row/xv-table-row.js +1 -1
  45. package/dist/collection/components/xv-table/xv-table.js +1 -1
  46. package/dist/collection/components/xv-tabs/xv-tab/xv-tab.js +1 -1
  47. package/dist/collection/components/xv-tag/xv-tag.js +1 -1
  48. package/dist/collection/components/xv-tag-combobox/xv-tag-combobox.js +2 -2
  49. package/dist/collection/components/xv-text-input/xv-text-input.js +2 -2
  50. package/dist/collection/components/xv-textarea/xv-textarea.js +2 -2
  51. package/dist/collection/components/xv-tile/xv-tile.js +2 -2
  52. package/dist/collection/components/xv-toggle/xv-toggle.js +1 -1
  53. package/dist/collection/components/xv-toggle-tip/xv-toggle-tip.js +1 -1
  54. package/dist/collection/components/xv-tooltip/xv-tooltip.js +2 -2
  55. package/dist/esm/loader.js +1 -1
  56. package/dist/esm/maplibre-gl-CS4Eagm9.js +813 -0
  57. package/dist/esm/maplibre-gl-shared.mjs +6 -0
  58. package/dist/esm/maplibre-gl-worker.mjs +6 -0
  59. package/dist/esm/{xv-accordion-v2_48.entry.js → xv-accordion-v2_49.entry.js} +655 -67
  60. package/dist/esm/xv-header-next.entry.js +1 -1
  61. package/dist/esm/xv-mega-menu-next.entry.js +2 -2
  62. package/dist/esm/xv-webcomponents.js +1 -1
  63. package/dist/types/components/xv-accordion/xv-accordion.d.ts +5 -4
  64. package/dist/types/components/xv-accordion-item/xv-accordion-item.d.ts +3 -0
  65. package/dist/types/components/xv-datepicker/xv-datepicker.d.ts +12 -0
  66. package/dist/types/components/xv-geo-map/xv-geo-map.d.ts +115 -0
  67. package/dist/types/components.d.ts +217 -0
  68. package/dist/xv-webcomponents/{p-511519a7.entry.js → p-77bd6e1f.entry.js} +2 -2
  69. package/dist/xv-webcomponents/{p-38e5f1e1.entry.js → p-93bd2dca.entry.js} +1 -1
  70. package/dist/xv-webcomponents/{p-7e05cb50.entry.js → p-9e3130ff.entry.js} +1 -1
  71. package/dist/xv-webcomponents/p-CS4Eagm9.js +10 -0
  72. package/dist/xv-webcomponents/xv-webcomponents.esm.js +1 -1
  73. package/package.json +2 -1
@@ -0,0 +1,984 @@
1
+ import { Host, h } from "@stencil/core";
2
+ // Xv.Api wraps responses in { data: ... } (ResponseEnvelopeFilter); the XvGeoTool API returns raw payloads.
3
+ const unwrap = (json) => (json != null && !Array.isArray(json) && 'data' in json ? json.data : json);
4
+ // Germany bounding box (with a small margin) — the map cannot be panned outside
5
+ const GERMANY_BOUNDS = [[4.5, 46.4], [16.5, 55.6]];
6
+ // amber: distinct from the green selection AND from CARTO's road/water tones
7
+ const PREVIEW_COLOR = '#f59e0b';
8
+ /**
9
+ * Minimal ATV planner map: MapLibre + PLZ boundaries from the XvGeoTool API.
10
+ * Click a PLZ to (de)select it. No further UI — selection state and reach
11
+ * data are emitted via `changeSelection` for the host page to display.
12
+ *
13
+ * MapLibre (~250 KB gzip) is loaded lazily when the component scrolls near
14
+ * the viewport, so the host page's initial load stays untouched.
15
+ */
16
+ export class XvGeoMap {
17
+ constructor() {
18
+ /**
19
+ * Base URL of the geo API (Xv.Api v1/geo — contract-compatible with the XvGeoTool API).
20
+ * Default is the same-origin Front Door route, so no CORS is involved on crossvertise pages.
21
+ */
22
+ this.apiBase = '/api-fd/v1/geo';
23
+ /** Indicator whose per-unit values feed the tooltips and the emitted population sum (e.g. ATV_DEVICES) */
24
+ this.indicator = 'MBI_P_T_2025';
25
+ /** Unit label shown behind the indicator value in the tooltip */
26
+ this.indicatorLabel = 'Einwohner';
27
+ /** Geo level to display and select: plz5, gemeinde, kreis, regbez, bundesland */
28
+ this.level = 'plz5';
29
+ /** Shape resolution: medium = simplified geometry (snappy), high = originals */
30
+ this.resolution = 'medium';
31
+ /**
32
+ * Raster tile URL template. Default: CARTO Positron — light, minimal, made for
33
+ * data overlays; fits the crossvertise design. For production traffic, license
34
+ * a tile plan (CARTO/MapTiler/Geofabrik) and set the keyed URL here.
35
+ */
36
+ this.tiles = 'https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}@2x.png';
37
+ this.attribution = '© CARTO, © OpenStreetMap contributors';
38
+ /** Initial map center */
39
+ this.centerLng = 10.45;
40
+ this.centerLat = 51.16;
41
+ this.zoom = 5.5;
42
+ /** Minimum zoom before boundaries are loaded and selectable (use ~5 for bundesland, 9 for plz5) */
43
+ this.selectZoom = 9;
44
+ /** Load the map bundle only when scrolled near the viewport (default: true) */
45
+ this.lazy = true;
46
+ this.ready = false;
47
+ this.loadError = false;
48
+ this.zoomGated = true;
49
+ this.searchResults = [];
50
+ this.searchQuery = '';
51
+ this.searchSeq = 0;
52
+ this.initStarted = false;
53
+ this.readyPromise = new Promise(res => (this.resolveReady = res));
54
+ this.units = new Map(); // code -> unit, accumulated across viewports
55
+ this.selected = new Map(); // code -> unit
56
+ this.preview = new Map(); // optimizer proposal (visual only)
57
+ this.populations = new Map(); // code -> indicator value (for tooltips + local sums)
58
+ this.fetchedBoxes = []; // areas already loaded
59
+ this.fetchSeq = 0;
60
+ this.popSeq = 0;
61
+ }
62
+ /** switching the level (e.g. PLZ mode -> Bundesländer mode) resets all loaded data and the selection */
63
+ levelChanged() {
64
+ if (!this.map)
65
+ return;
66
+ this.units.clear();
67
+ this.selected.clear();
68
+ this.populations.clear();
69
+ this.fetchedBoxes = [];
70
+ this.fetchSeq++;
71
+ this.renderUnits();
72
+ this.updateSelectedFilter();
73
+ this.emitSelection();
74
+ // follow the mode with the camera: out to the overview for coarse levels,
75
+ // in to selectable zoom for fine ones. Deferred so a select-zoom attribute
76
+ // changing in the same render is already applied.
77
+ setTimeout(() => {
78
+ var _a;
79
+ (_a = this.map) === null || _a === void 0 ? void 0 : _a.easeTo({ zoom: this.selectZoom + 0.5, duration: 800 });
80
+ this.refreshUnits();
81
+ }, 0);
82
+ }
83
+ selectZoomChanged() {
84
+ if (!this.map)
85
+ return;
86
+ ['units-fill', 'units-hover', 'units-line'].forEach(l => this.map.setLayerZoomRange(l, this.selectZoom, 24));
87
+ this.refreshUnits();
88
+ }
89
+ componentDidLoad() {
90
+ if (!this.lazy || !('IntersectionObserver' in window)) {
91
+ this.initMap();
92
+ return;
93
+ }
94
+ this.observer = new IntersectionObserver(entries => {
95
+ var _a;
96
+ if (entries.some(e => e.isIntersecting)) {
97
+ (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
98
+ this.initMap();
99
+ }
100
+ }, { rootMargin: '300px' });
101
+ this.observer.observe(this.el);
102
+ }
103
+ disconnectedCallback() {
104
+ var _a, _b;
105
+ (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
106
+ (_b = this.map) === null || _b === void 0 ? void 0 : _b.remove();
107
+ }
108
+ async initMap() {
109
+ if (this.initStarted)
110
+ return;
111
+ this.initStarted = true;
112
+ try {
113
+ const maplibregl = await import('maplibre-gl');
114
+ if (this.workerUrl) {
115
+ maplibregl.setWorkerUrl(new URL(this.workerUrl, document.baseURI).href);
116
+ }
117
+ const map = new maplibregl.Map({
118
+ container: this.mapContainer,
119
+ style: {
120
+ version: 8,
121
+ sources: {
122
+ basemap: {
123
+ type: 'raster',
124
+ tiles: [this.tiles],
125
+ tileSize: 256,
126
+ attribution: this.attribution,
127
+ },
128
+ },
129
+ layers: [{ id: 'basemap', type: 'raster', source: 'basemap' }],
130
+ },
131
+ center: [this.centerLng, this.centerLat],
132
+ zoom: this.zoom,
133
+ minZoom: 5,
134
+ maxBounds: GERMANY_BOUNDS,
135
+ attributionControl: { compact: true },
136
+ });
137
+ this.map = map;
138
+ this.el._map = map; // e2e/test handle
139
+ map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
140
+ map.on('load', () => {
141
+ // Design tokens (Masha, MAR-15783): selection = highlight-01 fill / interactive-02
142
+ // outline; optimizer preview = highlight-02 fill / interactive-01 outline. Fallbacks
143
+ // keep the previous XV green/amber where the host page lacks the tokens.
144
+ const style = getComputedStyle(this.el);
145
+ const token = (name, fallback) => style.getPropertyValue(name).trim() || fallback;
146
+ const accent = token('--link-primary', '#76b82a');
147
+ const selectionFill = token('--highlight-01', accent);
148
+ const selectionLine = token('--interactive-02', accent);
149
+ const previewFill = token('--highlight-02', PREVIEW_COLOR);
150
+ const previewLine = token('--interactive-01', PREVIEW_COLOR);
151
+ // the highlight tokens are pale full-strength tints — at the legacy 0.3 opacity they
152
+ // vanish over the basemap; the saturated fallbacks still need the low opacity
153
+ const hasTokens = !!style.getPropertyValue('--highlight-01').trim();
154
+ const selectedOpacity = hasTokens ? 0.85 : 0.35;
155
+ const previewOpacity = hasTokens ? 0.85 : 0.3;
156
+ map.addSource('units', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
157
+ // boundary mesh only appears at selectable zoom (cleaner overview);
158
+ // the selected areas stay visible at every zoom level
159
+ map.addLayer({
160
+ id: 'units-fill',
161
+ type: 'fill',
162
+ source: 'units',
163
+ minzoom: this.selectZoom,
164
+ paint: { 'fill-color': selectionFill, 'fill-opacity': 0.04 },
165
+ });
166
+ map.addLayer({
167
+ id: 'units-selected',
168
+ type: 'fill',
169
+ source: 'units',
170
+ filter: ['in', ['get', 'code'], ['literal', []]],
171
+ paint: { 'fill-color': selectionFill, 'fill-opacity': selectedOpacity },
172
+ });
173
+ // optimizer preview: units a proposal would ADD, in their own tokens so they read
174
+ // as "suggested" next to the actual selection; visual only, never selected
175
+ map.addLayer({
176
+ id: 'units-preview',
177
+ type: 'fill',
178
+ source: 'units',
179
+ filter: ['in', ['get', 'code'], ['literal', []]],
180
+ paint: { 'fill-color': previewFill, 'fill-opacity': previewOpacity },
181
+ });
182
+ map.addLayer({
183
+ id: 'units-preview-line',
184
+ type: 'line',
185
+ source: 'units',
186
+ filter: ['in', ['get', 'code'], ['literal', []]],
187
+ paint: { 'line-color': previewLine, 'line-width': 2 },
188
+ });
189
+ map.addLayer({
190
+ id: 'units-hover',
191
+ type: 'fill',
192
+ source: 'units',
193
+ minzoom: this.selectZoom,
194
+ filter: ['==', ['get', 'code'], ''],
195
+ paint: { 'fill-color': selectionFill, 'fill-opacity': 0.15 },
196
+ });
197
+ map.addLayer({
198
+ id: 'units-line',
199
+ type: 'line',
200
+ source: 'units',
201
+ minzoom: this.selectZoom,
202
+ paint: { 'line-color': selectionLine, 'line-width': 2, 'line-dasharray': [2, 1.5] },
203
+ });
204
+ this.popup = new maplibregl.Popup({
205
+ closeButton: false,
206
+ closeOnClick: false,
207
+ offset: 12,
208
+ maxWidth: 'none',
209
+ className: 'xv-geo-map-tooltip',
210
+ });
211
+ // instant toggle; a double click fires click twice (select + deselect = net zero)
212
+ // and then zooms, so no debounce delay is needed
213
+ map.on('click', 'units-fill', e => { var _a, _b, _c; return this.toggleUnit((_c = (_b = (_a = e.features) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.code); });
214
+ // below the selectable zoom nothing is clickable — make a click dive to the
215
+ // clicked area at select level instead of doing nothing
216
+ map.on('click', e => {
217
+ if (map.getZoom() < this.selectZoom) {
218
+ map.easeTo({ center: e.lngLat, zoom: this.selectZoom + 0.5, duration: 700 });
219
+ }
220
+ });
221
+ map.on('mousemove', 'units-fill', e => {
222
+ var _a, _b;
223
+ const props = (_b = (_a = e.features) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.properties;
224
+ if (!props)
225
+ return;
226
+ map.getCanvas().style.cursor = 'pointer';
227
+ map.setFilter('units-hover', ['==', ['get', 'code'], props.code]);
228
+ const pop = this.populations.get(props.code);
229
+ const selected = this.selected.has(props.code);
230
+ this.popup
231
+ .setLngLat(e.lngLat)
232
+ .setHTML(`<strong>${props.code}</strong> ${props.name}` +
233
+ (pop != null ? `<br>${pop.toLocaleString('de-DE')} ${this.indicatorLabel}` : '') +
234
+ (selected ? '<br><em>Klicken zum Abwählen</em>' : '<br><em>Klicken zum Auswählen</em>'))
235
+ .addTo(map);
236
+ });
237
+ map.on('mouseleave', 'units-fill', () => {
238
+ map.getCanvas().style.cursor = '';
239
+ map.setFilter('units-hover', ['==', ['get', 'code'], '']);
240
+ this.popup.remove();
241
+ });
242
+ map.on('moveend', () => this.refreshUnits());
243
+ this.ready = true;
244
+ this.mapReady.emit();
245
+ this.resolveReady();
246
+ this.refreshUnits();
247
+ });
248
+ map.on('error', () => {
249
+ if (!this.ready)
250
+ this.loadError = true;
251
+ });
252
+ }
253
+ catch (_a) {
254
+ this.loadError = true;
255
+ }
256
+ }
257
+ async refreshUnits() {
258
+ if (!this.map)
259
+ return;
260
+ this.zoomGated = this.map.getZoom() < this.selectZoom;
261
+ if (this.zoomGated)
262
+ return;
263
+ const b = this.map.getBounds();
264
+ // skip the fetch entirely when the viewport is inside an already-loaded area
265
+ if (this.fetchedBoxes.some(f => b.getNorth() <= f.n && b.getSouth() >= f.s && b.getEast() <= f.e && b.getWest() >= f.w)) {
266
+ return;
267
+ }
268
+ // fetch with a half-viewport margin so small pans stay within the loaded area
269
+ const mLat = (b.getNorth() - b.getSouth()) / 2;
270
+ const mLng = (b.getEast() - b.getWest()) / 2;
271
+ const box = { n: b.getNorth() + mLat, s: b.getSouth() - mLat, e: b.getEast() + mLng, w: b.getWest() - mLng };
272
+ const seq = ++this.fetchSeq;
273
+ try {
274
+ const res = await fetch(`${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/by-bounds?resolution=${this.resolution}`, {
275
+ method: 'POST',
276
+ headers: { 'Content-Type': 'application/json' },
277
+ body: JSON.stringify({ north: box.n, south: box.s, east: box.e, west: box.w }),
278
+ });
279
+ if (!res.ok || seq !== this.fetchSeq)
280
+ return;
281
+ const list = unwrap(await res.json());
282
+ this.fetchedBoxes.push(box);
283
+ list.forEach(u => this.units.set(u.code, u));
284
+ this.renderUnits();
285
+ this.loadPopulations(list.filter(u => !this.populations.has(u.code)));
286
+ }
287
+ catch (_a) {
288
+ /* viewport fetch failed (offline/CORS) — map stays usable, next moveend retries */
289
+ }
290
+ }
291
+ /** fetch the configured indicator per unit for hover tooltips (fire-and-forget) */
292
+ async loadPopulations(units) {
293
+ if (!units.length)
294
+ return;
295
+ try {
296
+ const res = await fetch(`${this.apiBase}/indicators/values`, {
297
+ method: 'POST',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify({ geoUnitIds: units.map(u => u.id), indicatorCodes: [this.indicator] }),
300
+ });
301
+ if (!res.ok)
302
+ return;
303
+ const idToCode = new Map(units.map(u => [u.id, u.code]));
304
+ for (const v of unwrap(await res.json())) {
305
+ const code = idToCode.get(v.geoUnitId);
306
+ if (code)
307
+ this.populations.set(code, v.value);
308
+ }
309
+ }
310
+ catch (_a) {
311
+ /* tooltips just omit population */
312
+ }
313
+ }
314
+ renderUnits() {
315
+ // render everything loaded so far — panning back never flickers or re-parses
316
+ const source = this.map.getSource('units');
317
+ source === null || source === void 0 ? void 0 : source.setData({
318
+ type: 'FeatureCollection',
319
+ features: [...this.units.values()].map(u => ({
320
+ type: 'Feature',
321
+ properties: { code: u.code, name: u.name },
322
+ geometry: u.geography,
323
+ })),
324
+ });
325
+ }
326
+ toggleUnit(code) {
327
+ if (!code)
328
+ return;
329
+ if (this.selected.has(code))
330
+ this.selected.delete(code);
331
+ else
332
+ this.selected.set(code, this.units.get(code));
333
+ this.resetPreview(); // user interaction makes a shown proposal stale
334
+ this.updateSelectedFilter();
335
+ this.emitSelection();
336
+ }
337
+ updateSelectedFilter() {
338
+ this.map.setFilter('units-selected', ['in', ['get', 'code'], ['literal', [...this.selected.keys()]]]);
339
+ }
340
+ resetPreview() {
341
+ if (!this.preview.size)
342
+ return;
343
+ this.preview.clear();
344
+ this.updatePreviewFilter();
345
+ }
346
+ updatePreviewFilter() {
347
+ const codes = ['in', ['get', 'code'], ['literal', [...this.preview.keys()]]];
348
+ this.map.setFilter('units-preview', codes);
349
+ this.map.setFilter('units-preview-line', codes);
350
+ }
351
+ async emitSelection() {
352
+ var _a, _b, _c;
353
+ const postalCodes = [...this.selected.keys()];
354
+ const names = [...this.selected.values()].map(u => { var _a; return (_a = u === null || u === void 0 ? void 0 : u.name) !== null && _a !== void 0 ? _a : ''; });
355
+ const unitIds = [...this.selected.values()].map(u => u.id);
356
+ const seq = ++this.popSeq;
357
+ // populations are already cached per PLZ (tooltips) — sum locally, emit instantly
358
+ if (postalCodes.every(c => this.populations.has(c))) {
359
+ const population = postalCodes.reduce((sum, c) => sum + this.populations.get(c), 0);
360
+ const selection = { postalCodes, names, population: postalCodes.length ? population : null, unitIds };
361
+ this.changeSelection.emit(selection);
362
+ return selection;
363
+ }
364
+ // fallback: some populations not cached yet — aggregate server-side
365
+ let population = null;
366
+ try {
367
+ const res = await fetch(`${this.apiBase}/indicators/aggregate`, {
368
+ method: 'POST',
369
+ headers: { 'Content-Type': 'application/json' },
370
+ body: JSON.stringify({ geoUnitIds: unitIds, indicatorCodes: [this.indicator] }),
371
+ });
372
+ if (res.ok)
373
+ population = (_c = (_b = (_a = unwrap(await res.json())) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.sum) !== null && _c !== void 0 ? _c : null;
374
+ }
375
+ catch (_d) {
376
+ /* population stays null — host can fall back */
377
+ }
378
+ if (seq !== this.popSeq)
379
+ return null; // a newer selection superseded this one
380
+ const selection = { postalCodes, names, population, unitIds };
381
+ this.changeSelection.emit(selection);
382
+ return selection;
383
+ }
384
+ // ---- public API for host applications (Angular planner) ----
385
+ /** Select all PLZs within `km` around a start PLZ ("Umkreis"). Replaces the selection unless `add` is true. */
386
+ async selectByRadius(startPlz, km, add = false) {
387
+ await this.ensureReady();
388
+ const url = `${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/by-radius` +
389
+ `?address=${encodeURIComponent(startPlz)}&km=${km}&country=DE&resolution=${this.resolution}`;
390
+ const res = await fetch(url);
391
+ if (!res.ok)
392
+ throw new Error(`by-radius failed: HTTP ${res.status}`);
393
+ return this.applySelection(unwrap(await res.json()), add);
394
+ }
395
+ /** Select an explicit list of PLZs ("PLZ-Liste" paste, query-param restore). Replaces the selection unless `add` is true. */
396
+ async selectPostalCodes(codes, add = false) {
397
+ await this.ensureReady();
398
+ // 1–5 digits: PLZ5 and Kreis codes are 5, Bundesland codes are 1–2 (a strict \d{5}
399
+ // silently dropped state selections restored via this method)
400
+ const clean = [...new Set(codes.map(c => String(c).trim()).filter(c => /^\d{1,5}$/.test(c)))];
401
+ if (!clean.length)
402
+ return this.applySelection([], add);
403
+ const res = await fetch(`${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/lookup?resolution=${this.resolution}`, {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify({ codes: clean }),
407
+ });
408
+ if (!res.ok)
409
+ throw new Error(`lookup failed: HTTP ${res.status}`);
410
+ return this.applySelection(unwrap(await res.json()), add);
411
+ }
412
+ /** Clear the whole selection. */
413
+ async clearSelection() {
414
+ await this.ensureReady();
415
+ this.selected.clear();
416
+ this.resetPreview();
417
+ this.updateSelectedFilter();
418
+ this.emitSelection();
419
+ }
420
+ /**
421
+ * Highlight what a radius extension would ADD (optimizer proposal): the units within
422
+ * `km` that are not part of the selection render in the preview color. Visual only —
423
+ * the selection and its change events stay untouched. Returns the number of added units.
424
+ */
425
+ async previewByRadius(startPlz, km) {
426
+ await this.ensureReady();
427
+ const url = `${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/by-radius` +
428
+ `?address=${encodeURIComponent(startPlz)}&km=${km}&country=DE&resolution=${this.resolution}`;
429
+ const res = await fetch(url);
430
+ if (!res.ok)
431
+ throw new Error(`by-radius failed: HTTP ${res.status}`);
432
+ const units = unwrap(await res.json());
433
+ this.preview.clear();
434
+ units.forEach(u => {
435
+ this.units.set(u.code, u);
436
+ if (!this.selected.has(u.code))
437
+ this.preview.set(u.code, u);
438
+ });
439
+ this.renderUnits();
440
+ this.updatePreviewFilter();
441
+ this.fitTo([...this.selected.values(), ...this.preview.values()]);
442
+ return this.preview.size;
443
+ }
444
+ /** Remove the optimizer proposal highlight. */
445
+ async clearPreview() {
446
+ await this.ensureReady();
447
+ this.resetPreview();
448
+ this.fitTo([...this.selected.values()]);
449
+ }
450
+ // ---- search box (MAR-15783): find areas by name or code, click to select ----
451
+ onSearchInput(value) {
452
+ this.searchQuery = value;
453
+ clearTimeout(this.searchDebounce);
454
+ const query = value.trim();
455
+ if (query.length < 2) {
456
+ this.searchResults = [];
457
+ return;
458
+ }
459
+ this.searchDebounce = setTimeout(async () => {
460
+ var _a;
461
+ const seq = ++this.searchSeq;
462
+ try {
463
+ const res = await fetch(`${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/search?q=${encodeURIComponent(query)}`);
464
+ if (!res.ok || seq !== this.searchSeq)
465
+ return;
466
+ this.searchResults = (_a = unwrap(await res.json())) !== null && _a !== void 0 ? _a : [];
467
+ }
468
+ catch (_b) {
469
+ this.searchResults = []; // search silently unavailable — the map stays usable
470
+ }
471
+ }, 300);
472
+ }
473
+ pickSearchResult(result) {
474
+ this.searchQuery = '';
475
+ this.searchResults = [];
476
+ // adds to the selection and flies there — exactly what "find this area" means here
477
+ void this.selectPostalCodes([result.code], true).catch(() => undefined);
478
+ }
479
+ /** A method call means the map is needed now — start loading even if it hasn't scrolled into view. */
480
+ ensureReady() {
481
+ var _a;
482
+ (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
483
+ this.initMap();
484
+ return this.readyPromise;
485
+ }
486
+ async applySelection(units, add) {
487
+ if (!add)
488
+ this.selected.clear();
489
+ units.forEach(u => {
490
+ this.units.set(u.code, u);
491
+ this.selected.set(u.code, u);
492
+ });
493
+ this.resetPreview();
494
+ this.renderUnits();
495
+ this.updateSelectedFilter();
496
+ this.fitTo([...this.selected.values()]);
497
+ await this.loadPopulations(units.filter(u => !this.populations.has(u.code)));
498
+ return this.emitSelection();
499
+ }
500
+ fitTo(units) {
501
+ if (!units.length)
502
+ return;
503
+ let minX = 180, minY = 90, maxX = -180, maxY = -90;
504
+ const scan = (c) => {
505
+ if (typeof c[0] === 'number') {
506
+ minX = Math.min(minX, c[0]);
507
+ maxX = Math.max(maxX, c[0]);
508
+ minY = Math.min(minY, c[1]);
509
+ maxY = Math.max(maxY, c[1]);
510
+ }
511
+ else
512
+ c.forEach(scan);
513
+ };
514
+ units.forEach(u => scan(u.geography.coordinates));
515
+ this.map.fitBounds([[minX, minY], [maxX, maxY]], { padding: 48, maxZoom: 12, duration: 800 });
516
+ }
517
+ render() {
518
+ return (h(Host, { key: 'b8f13f50cdb82e11502053eb7f14c31069580d5b' }, h("div", { key: '71e33adcb25ea4e35279ba8e060b9f9429126b6d', class: "container" }, h("div", { key: '14f9ad939968590fdde28d0be00d3fdf275234b5', class: "map", ref: el => (this.mapContainer = el) }), this.ready && (h("div", { key: '6711f30c53ac1c58090618af44b5dc5fde989223', class: "search" }, h("input", { key: '6e37e84e155721edd451c3465dc5ea426961b3dc', type: "search", placeholder: "Ort oder PLZ suchen", value: this.searchQuery, onInput: e => this.onSearchInput(e.target.value) }), this.searchResults.length > 0 && (h("ul", { key: 'a1df4aceef222e8d64f5d877b3ed73a821fe1f4b', class: "search-results" }, this.searchResults.map(r => (h("li", { key: r.code, onClick: () => this.pickSearchResult(r) }, h("strong", null, r.code), " ", r.name))))))), this.ready && this.zoomGated && (h("div", { key: '2079f7b7223fca39bd6d427b904c02b2a8643a9e', class: "hint" }, "Reinzoomen, um PLZ-Gebiete zu laden")), !this.ready && !this.loadError && (h("div", { key: 'd6ca85b7dd7407d6ed4af23abe74fe9b875f9cf6', class: "skeleton", "aria-busy": "true" }, h("xv-loader-v2", { key: 'b6548dd92dc724cf05c54274aa5d49a09a1f5362', variant: "vertical" }, "Karte wird geladen \u2026"))), this.loadError && h("div", { key: '7df300e7a5172e813d36638153285de64f17e4b9', class: "skeleton error" }, "Karte konnte nicht geladen werden."))));
519
+ }
520
+ static get is() { return "xv-geo-map-v2"; }
521
+ static get encapsulation() { return "shadow"; }
522
+ static get originalStyleUrls() {
523
+ return {
524
+ "$": ["xv-geo-map.scss"]
525
+ };
526
+ }
527
+ static get styleUrls() {
528
+ return {
529
+ "$": ["xv-geo-map.css"]
530
+ };
531
+ }
532
+ static get properties() {
533
+ return {
534
+ "apiBase": {
535
+ "type": "string",
536
+ "mutable": false,
537
+ "complexType": {
538
+ "original": "string",
539
+ "resolved": "string",
540
+ "references": {}
541
+ },
542
+ "required": false,
543
+ "optional": false,
544
+ "docs": {
545
+ "tags": [],
546
+ "text": "Base URL of the geo API (Xv.Api v1/geo \u2014 contract-compatible with the XvGeoTool API).\nDefault is the same-origin Front Door route, so no CORS is involved on crossvertise pages."
547
+ },
548
+ "getter": false,
549
+ "setter": false,
550
+ "reflect": false,
551
+ "attribute": "api-base",
552
+ "defaultValue": "'/api-fd/v1/geo'"
553
+ },
554
+ "indicator": {
555
+ "type": "string",
556
+ "mutable": false,
557
+ "complexType": {
558
+ "original": "string",
559
+ "resolved": "string",
560
+ "references": {}
561
+ },
562
+ "required": false,
563
+ "optional": false,
564
+ "docs": {
565
+ "tags": [],
566
+ "text": "Indicator whose per-unit values feed the tooltips and the emitted population sum (e.g. ATV_DEVICES)"
567
+ },
568
+ "getter": false,
569
+ "setter": false,
570
+ "reflect": false,
571
+ "attribute": "indicator",
572
+ "defaultValue": "'MBI_P_T_2025'"
573
+ },
574
+ "indicatorLabel": {
575
+ "type": "string",
576
+ "mutable": false,
577
+ "complexType": {
578
+ "original": "string",
579
+ "resolved": "string",
580
+ "references": {}
581
+ },
582
+ "required": false,
583
+ "optional": false,
584
+ "docs": {
585
+ "tags": [],
586
+ "text": "Unit label shown behind the indicator value in the tooltip"
587
+ },
588
+ "getter": false,
589
+ "setter": false,
590
+ "reflect": false,
591
+ "attribute": "indicator-label",
592
+ "defaultValue": "'Einwohner'"
593
+ },
594
+ "level": {
595
+ "type": "string",
596
+ "mutable": false,
597
+ "complexType": {
598
+ "original": "string",
599
+ "resolved": "string",
600
+ "references": {}
601
+ },
602
+ "required": false,
603
+ "optional": false,
604
+ "docs": {
605
+ "tags": [],
606
+ "text": "Geo level to display and select: plz5, gemeinde, kreis, regbez, bundesland"
607
+ },
608
+ "getter": false,
609
+ "setter": false,
610
+ "reflect": false,
611
+ "attribute": "level",
612
+ "defaultValue": "'plz5'"
613
+ },
614
+ "resolution": {
615
+ "type": "string",
616
+ "mutable": false,
617
+ "complexType": {
618
+ "original": "'medium' | 'high'",
619
+ "resolved": "\"high\" | \"medium\"",
620
+ "references": {}
621
+ },
622
+ "required": false,
623
+ "optional": false,
624
+ "docs": {
625
+ "tags": [],
626
+ "text": "Shape resolution: medium = simplified geometry (snappy), high = originals"
627
+ },
628
+ "getter": false,
629
+ "setter": false,
630
+ "reflect": false,
631
+ "attribute": "resolution",
632
+ "defaultValue": "'medium'"
633
+ },
634
+ "tiles": {
635
+ "type": "string",
636
+ "mutable": false,
637
+ "complexType": {
638
+ "original": "string",
639
+ "resolved": "string",
640
+ "references": {}
641
+ },
642
+ "required": false,
643
+ "optional": false,
644
+ "docs": {
645
+ "tags": [],
646
+ "text": "Raster tile URL template. Default: CARTO Positron \u2014 light, minimal, made for\ndata overlays; fits the crossvertise design. For production traffic, license\na tile plan (CARTO/MapTiler/Geofabrik) and set the keyed URL here."
647
+ },
648
+ "getter": false,
649
+ "setter": false,
650
+ "reflect": false,
651
+ "attribute": "tiles",
652
+ "defaultValue": "'https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}@2x.png'"
653
+ },
654
+ "attribution": {
655
+ "type": "string",
656
+ "mutable": false,
657
+ "complexType": {
658
+ "original": "string",
659
+ "resolved": "string",
660
+ "references": {}
661
+ },
662
+ "required": false,
663
+ "optional": false,
664
+ "docs": {
665
+ "tags": [],
666
+ "text": ""
667
+ },
668
+ "getter": false,
669
+ "setter": false,
670
+ "reflect": false,
671
+ "attribute": "attribution",
672
+ "defaultValue": "'\u00A9 CARTO, \u00A9 OpenStreetMap contributors'"
673
+ },
674
+ "centerLng": {
675
+ "type": "number",
676
+ "mutable": false,
677
+ "complexType": {
678
+ "original": "number",
679
+ "resolved": "number",
680
+ "references": {}
681
+ },
682
+ "required": false,
683
+ "optional": false,
684
+ "docs": {
685
+ "tags": [],
686
+ "text": "Initial map center"
687
+ },
688
+ "getter": false,
689
+ "setter": false,
690
+ "reflect": false,
691
+ "attribute": "center-lng",
692
+ "defaultValue": "10.45"
693
+ },
694
+ "centerLat": {
695
+ "type": "number",
696
+ "mutable": false,
697
+ "complexType": {
698
+ "original": "number",
699
+ "resolved": "number",
700
+ "references": {}
701
+ },
702
+ "required": false,
703
+ "optional": false,
704
+ "docs": {
705
+ "tags": [],
706
+ "text": ""
707
+ },
708
+ "getter": false,
709
+ "setter": false,
710
+ "reflect": false,
711
+ "attribute": "center-lat",
712
+ "defaultValue": "51.16"
713
+ },
714
+ "zoom": {
715
+ "type": "number",
716
+ "mutable": false,
717
+ "complexType": {
718
+ "original": "number",
719
+ "resolved": "number",
720
+ "references": {}
721
+ },
722
+ "required": false,
723
+ "optional": false,
724
+ "docs": {
725
+ "tags": [],
726
+ "text": ""
727
+ },
728
+ "getter": false,
729
+ "setter": false,
730
+ "reflect": false,
731
+ "attribute": "zoom",
732
+ "defaultValue": "5.5"
733
+ },
734
+ "selectZoom": {
735
+ "type": "number",
736
+ "mutable": false,
737
+ "complexType": {
738
+ "original": "number",
739
+ "resolved": "number",
740
+ "references": {}
741
+ },
742
+ "required": false,
743
+ "optional": false,
744
+ "docs": {
745
+ "tags": [],
746
+ "text": "Minimum zoom before boundaries are loaded and selectable (use ~5 for bundesland, 9 for plz5)"
747
+ },
748
+ "getter": false,
749
+ "setter": false,
750
+ "reflect": false,
751
+ "attribute": "select-zoom",
752
+ "defaultValue": "9"
753
+ },
754
+ "workerUrl": {
755
+ "type": "string",
756
+ "mutable": false,
757
+ "complexType": {
758
+ "original": "string",
759
+ "resolved": "string",
760
+ "references": {}
761
+ },
762
+ "required": false,
763
+ "optional": true,
764
+ "docs": {
765
+ "tags": [],
766
+ "text": "Explicit URL of maplibre-gl-worker.mjs (maplibre-gl-shared.mjs must sit next to it).\nNeeded when the host bundler relocates the maplibre chunk so import.meta.url-relative\nworker resolution breaks (e.g. Angular/Vite dev server)."
767
+ },
768
+ "getter": false,
769
+ "setter": false,
770
+ "reflect": false,
771
+ "attribute": "worker-url"
772
+ },
773
+ "lazy": {
774
+ "type": "boolean",
775
+ "mutable": false,
776
+ "complexType": {
777
+ "original": "boolean",
778
+ "resolved": "boolean",
779
+ "references": {}
780
+ },
781
+ "required": false,
782
+ "optional": false,
783
+ "docs": {
784
+ "tags": [],
785
+ "text": "Load the map bundle only when scrolled near the viewport (default: true)"
786
+ },
787
+ "getter": false,
788
+ "setter": false,
789
+ "reflect": false,
790
+ "attribute": "lazy",
791
+ "defaultValue": "true"
792
+ }
793
+ };
794
+ }
795
+ static get states() {
796
+ return {
797
+ "ready": {},
798
+ "loadError": {},
799
+ "zoomGated": {},
800
+ "searchResults": {},
801
+ "searchQuery": {}
802
+ };
803
+ }
804
+ static get events() {
805
+ return [{
806
+ "method": "changeSelection",
807
+ "name": "changeSelection",
808
+ "bubbles": true,
809
+ "cancelable": true,
810
+ "composed": true,
811
+ "docs": {
812
+ "tags": [],
813
+ "text": "Selection changed: selected PLZs + population sum of the selection"
814
+ },
815
+ "complexType": {
816
+ "original": "GeoMapSelection",
817
+ "resolved": "GeoMapSelection",
818
+ "references": {
819
+ "GeoMapSelection": {
820
+ "location": "local",
821
+ "path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
822
+ "id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
823
+ }
824
+ }
825
+ }
826
+ }, {
827
+ "method": "mapReady",
828
+ "name": "mapReady",
829
+ "bubbles": true,
830
+ "cancelable": true,
831
+ "composed": true,
832
+ "docs": {
833
+ "tags": [],
834
+ "text": "Map finished initializing"
835
+ },
836
+ "complexType": {
837
+ "original": "void",
838
+ "resolved": "void",
839
+ "references": {}
840
+ }
841
+ }];
842
+ }
843
+ static get methods() {
844
+ return {
845
+ "selectByRadius": {
846
+ "complexType": {
847
+ "signature": "(startPlz: string, km: number, add?: boolean) => Promise<GeoMapSelection | null>",
848
+ "parameters": [{
849
+ "name": "startPlz",
850
+ "type": "string",
851
+ "docs": ""
852
+ }, {
853
+ "name": "km",
854
+ "type": "number",
855
+ "docs": ""
856
+ }, {
857
+ "name": "add",
858
+ "type": "boolean",
859
+ "docs": ""
860
+ }],
861
+ "references": {
862
+ "Promise": {
863
+ "location": "global",
864
+ "id": "global::Promise"
865
+ },
866
+ "GeoMapSelection": {
867
+ "location": "local",
868
+ "path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
869
+ "id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
870
+ }
871
+ },
872
+ "return": "Promise<GeoMapSelection>"
873
+ },
874
+ "docs": {
875
+ "text": "Select all PLZs within `km` around a start PLZ (\"Umkreis\"). Replaces the selection unless `add` is true.",
876
+ "tags": []
877
+ }
878
+ },
879
+ "selectPostalCodes": {
880
+ "complexType": {
881
+ "signature": "(codes: string[], add?: boolean) => Promise<GeoMapSelection | null>",
882
+ "parameters": [{
883
+ "name": "codes",
884
+ "type": "string[]",
885
+ "docs": ""
886
+ }, {
887
+ "name": "add",
888
+ "type": "boolean",
889
+ "docs": ""
890
+ }],
891
+ "references": {
892
+ "Promise": {
893
+ "location": "global",
894
+ "id": "global::Promise"
895
+ },
896
+ "GeoMapSelection": {
897
+ "location": "local",
898
+ "path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
899
+ "id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
900
+ }
901
+ },
902
+ "return": "Promise<GeoMapSelection>"
903
+ },
904
+ "docs": {
905
+ "text": "Select an explicit list of PLZs (\"PLZ-Liste\" paste, query-param restore). Replaces the selection unless `add` is true.",
906
+ "tags": []
907
+ }
908
+ },
909
+ "clearSelection": {
910
+ "complexType": {
911
+ "signature": "() => Promise<void>",
912
+ "parameters": [],
913
+ "references": {
914
+ "Promise": {
915
+ "location": "global",
916
+ "id": "global::Promise"
917
+ }
918
+ },
919
+ "return": "Promise<void>"
920
+ },
921
+ "docs": {
922
+ "text": "Clear the whole selection.",
923
+ "tags": []
924
+ }
925
+ },
926
+ "previewByRadius": {
927
+ "complexType": {
928
+ "signature": "(startPlz: string, km: number) => Promise<number>",
929
+ "parameters": [{
930
+ "name": "startPlz",
931
+ "type": "string",
932
+ "docs": ""
933
+ }, {
934
+ "name": "km",
935
+ "type": "number",
936
+ "docs": ""
937
+ }],
938
+ "references": {
939
+ "Promise": {
940
+ "location": "global",
941
+ "id": "global::Promise"
942
+ },
943
+ "GeoUnit": {
944
+ "location": "global",
945
+ "id": "global::GeoUnit"
946
+ }
947
+ },
948
+ "return": "Promise<number>"
949
+ },
950
+ "docs": {
951
+ "text": "Highlight what a radius extension would ADD (optimizer proposal): the units within\n`km` that are not part of the selection render in the preview color. Visual only \u2014\nthe selection and its change events stay untouched. Returns the number of added units.",
952
+ "tags": []
953
+ }
954
+ },
955
+ "clearPreview": {
956
+ "complexType": {
957
+ "signature": "() => Promise<void>",
958
+ "parameters": [],
959
+ "references": {
960
+ "Promise": {
961
+ "location": "global",
962
+ "id": "global::Promise"
963
+ }
964
+ },
965
+ "return": "Promise<void>"
966
+ },
967
+ "docs": {
968
+ "text": "Remove the optimizer proposal highlight.",
969
+ "tags": []
970
+ }
971
+ }
972
+ };
973
+ }
974
+ static get elementRef() { return "el"; }
975
+ static get watchers() {
976
+ return [{
977
+ "propName": "level",
978
+ "methodName": "levelChanged"
979
+ }, {
980
+ "propName": "selectZoom",
981
+ "methodName": "selectZoomChanged"
982
+ }];
983
+ }
984
+ }