xv-webcomponents 1.17.19-dev.0 → 1.17.21-qa.0
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.
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/maplibre-gl-MCSOKSuo.js +870 -0
- package/dist/cjs/{xv-accordion-v2_48.cjs.entry.js → xv-accordion-v2_49.cjs.entry.js} +457 -56
- package/dist/cjs/xv-header-next.cjs.entry.js +1 -1
- package/dist/cjs/xv-mega-menu-next.cjs.entry.js +2 -2
- package/dist/cjs/xv-webcomponents.cjs.js +1 -1
- package/dist/collection/collection-manifest.json +1 -0
- package/dist/collection/components/xv-accordion/xv-accordion.js +1 -1
- package/dist/collection/components/xv-accordion-item/xv-accordion-item.js +1 -1
- package/dist/collection/components/xv-breadcrumbs/xv-breadcrumbs.js +1 -1
- package/dist/collection/components/xv-button/xv-button.js +2 -2
- package/dist/collection/components/xv-button-group/xv-button-group.js +1 -1
- package/dist/collection/components/xv-card/xv-card.js +1 -1
- package/dist/collection/components/xv-checkbox/xv-checkbox.js +2 -2
- package/dist/collection/components/xv-content-switcher/xv-content-switcher-item/xv-content-switcher-item.js +1 -1
- package/dist/collection/components/xv-content-switcher/xv-content-switcher.js +2 -2
- package/dist/collection/components/xv-data-table/xv-data-table-cell/xv-data-table-cell.js +1 -1
- package/dist/collection/components/xv-data-table/xv-data-table-collapse/xv-data-table-collapse.js +1 -1
- package/dist/collection/components/xv-data-table/xv-data-table-row/xv-data-table-row.js +2 -2
- package/dist/collection/components/xv-data-table/xv-data-table.js +1 -1
- package/dist/collection/components/xv-datepicker/xv-datepicker.js +2 -2
- package/dist/collection/components/xv-dropdown/xv-dropdown-item/xv-dropdown-item.js +1 -1
- package/dist/collection/components/xv-dropdown/xv-dropdown.js +1 -1
- package/dist/collection/components/xv-file-uploader/xv-file-uploader.js +1 -1
- package/dist/collection/components/xv-footer/xv-footer.js +1 -1
- package/dist/collection/components/xv-geo-map/xv-geo-map.css +866 -0
- package/dist/collection/components/xv-geo-map/xv-geo-map.js +790 -0
- package/dist/collection/components/xv-header/xv-header.js +1 -1
- package/dist/collection/components/xv-header-next/xv-header-next.js +1 -1
- package/dist/collection/components/xv-image-viewer/xv-image-viewer.js +1 -1
- package/dist/collection/components/xv-link/xv-link.js +1 -1
- package/dist/collection/components/xv-mega-menu/xv-mega-menu.js +2 -2
- package/dist/collection/components/xv-mega-menu-next/xv-mega-menu-next.js +2 -2
- package/dist/collection/components/xv-modal/xv-modal.js +1 -1
- package/dist/collection/components/xv-notification/xv-notification.js +1 -1
- package/dist/collection/components/xv-number-input/xv-number-input.js +2 -2
- package/dist/collection/components/xv-pagination/xv-pagination.js +1 -1
- package/dist/collection/components/xv-progress-indicator/xv-progress-indicator-item/xv-progress-indicator-item.js +1 -1
- package/dist/collection/components/xv-progress-indicator/xv-progress-indicator.js +1 -1
- package/dist/collection/components/xv-radio/xv-radio.js +2 -2
- package/dist/collection/components/xv-slider/xv-slider.js +1 -1
- package/dist/collection/components/xv-table/xv-table-cell/xv-table-cell.js +1 -1
- package/dist/collection/components/xv-table/xv-table-expand/xv-table-expand.js +1 -1
- package/dist/collection/components/xv-table/xv-table-row/xv-table-row.js +1 -1
- package/dist/collection/components/xv-table/xv-table.js +1 -1
- package/dist/collection/components/xv-tabs/xv-tab/xv-tab.js +1 -1
- package/dist/collection/components/xv-tag/xv-tag.js +1 -1
- package/dist/collection/components/xv-tag-combobox/xv-tag-combobox.js +2 -2
- package/dist/collection/components/xv-text-input/xv-text-input.js +2 -2
- package/dist/collection/components/xv-textarea/xv-textarea.js +2 -2
- package/dist/collection/components/xv-tile/xv-tile.js +2 -2
- package/dist/collection/components/xv-toggle/xv-toggle.js +1 -1
- package/dist/collection/components/xv-toggle-tip/xv-toggle-tip.js +1 -1
- package/dist/collection/components/xv-tooltip/xv-tooltip.js +2 -2
- package/dist/esm/loader.js +1 -1
- package/dist/esm/maplibre-gl-CT7CGYe6.js +813 -0
- package/dist/esm/maplibre-gl-shared.mjs +6 -0
- package/dist/esm/maplibre-gl-worker.mjs +6 -0
- package/dist/esm/{xv-accordion-v2_48.entry.js → xv-accordion-v2_49.entry.js} +457 -57
- package/dist/esm/xv-header-next.entry.js +1 -1
- package/dist/esm/xv-mega-menu-next.entry.js +2 -2
- package/dist/esm/xv-webcomponents.js +1 -1
- package/dist/types/components/xv-geo-map/xv-geo-map.d.ts +90 -0
- package/dist/types/components.d.ts +193 -0
- package/dist/xv-webcomponents/{p-6d4d397f.entry.js → p-3c3179fb.entry.js} +2 -2
- package/dist/xv-webcomponents/{p-38e5f1e1.entry.js → p-597b353d.entry.js} +1 -1
- package/dist/xv-webcomponents/p-CT7CGYe6.js +9 -0
- package/dist/xv-webcomponents/{p-7e05cb50.entry.js → p-ef3c818d.entry.js} +1 -1
- package/dist/xv-webcomponents/xv-webcomponents.esm.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,790 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Minimal ATV planner map: MapLibre + PLZ boundaries from the XvGeoTool API.
|
|
8
|
+
* Click a PLZ to (de)select it. No further UI — selection state and reach
|
|
9
|
+
* data are emitted via `changeSelection` for the host page to display.
|
|
10
|
+
*
|
|
11
|
+
* MapLibre (~250 KB gzip) is loaded lazily when the component scrolls near
|
|
12
|
+
* the viewport, so the host page's initial load stays untouched.
|
|
13
|
+
*/
|
|
14
|
+
export class XvGeoMap {
|
|
15
|
+
constructor() {
|
|
16
|
+
/**
|
|
17
|
+
* Base URL of the geo API (Xv.Api v1/geo — contract-compatible with the XvGeoTool API).
|
|
18
|
+
* Default is the same-origin Front Door route, so no CORS is involved on crossvertise pages.
|
|
19
|
+
*/
|
|
20
|
+
this.apiBase = '/api-fd/v1/geo';
|
|
21
|
+
/** Indicator whose per-unit values feed the tooltips and the emitted population sum (e.g. ATV_DEVICES) */
|
|
22
|
+
this.indicator = 'MBI_P_T_2025';
|
|
23
|
+
/** Unit label shown behind the indicator value in the tooltip */
|
|
24
|
+
this.indicatorLabel = 'Einwohner';
|
|
25
|
+
/** Geo level to display and select: plz5, gemeinde, kreis, regbez, bundesland */
|
|
26
|
+
this.level = 'plz5';
|
|
27
|
+
/** Shape resolution: medium = simplified geometry (snappy), high = originals */
|
|
28
|
+
this.resolution = 'medium';
|
|
29
|
+
/**
|
|
30
|
+
* Raster tile URL template. Default: CARTO Positron — light, minimal, made for
|
|
31
|
+
* data overlays; fits the crossvertise design. For production traffic, license
|
|
32
|
+
* a tile plan (CARTO/MapTiler/Geofabrik) and set the keyed URL here.
|
|
33
|
+
*/
|
|
34
|
+
this.tiles = 'https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}@2x.png';
|
|
35
|
+
this.attribution = '© CARTO, © OpenStreetMap contributors';
|
|
36
|
+
/** Initial map center */
|
|
37
|
+
this.centerLng = 10.45;
|
|
38
|
+
this.centerLat = 51.16;
|
|
39
|
+
this.zoom = 5.5;
|
|
40
|
+
/** Minimum zoom before boundaries are loaded and selectable (use ~5 for bundesland, 9 for plz5) */
|
|
41
|
+
this.selectZoom = 9;
|
|
42
|
+
/** Load the map bundle only when scrolled near the viewport (default: true) */
|
|
43
|
+
this.lazy = true;
|
|
44
|
+
this.ready = false;
|
|
45
|
+
this.loadError = false;
|
|
46
|
+
this.zoomGated = true;
|
|
47
|
+
this.initStarted = false;
|
|
48
|
+
this.readyPromise = new Promise(res => (this.resolveReady = res));
|
|
49
|
+
this.units = new Map(); // code -> unit, accumulated across viewports
|
|
50
|
+
this.selected = new Map(); // code -> unit
|
|
51
|
+
this.populations = new Map(); // code -> indicator value (for tooltips + local sums)
|
|
52
|
+
this.fetchedBoxes = []; // areas already loaded
|
|
53
|
+
this.fetchSeq = 0;
|
|
54
|
+
this.popSeq = 0;
|
|
55
|
+
}
|
|
56
|
+
/** switching the level (e.g. PLZ mode -> Bundesländer mode) resets all loaded data and the selection */
|
|
57
|
+
levelChanged() {
|
|
58
|
+
if (!this.map)
|
|
59
|
+
return;
|
|
60
|
+
this.units.clear();
|
|
61
|
+
this.selected.clear();
|
|
62
|
+
this.populations.clear();
|
|
63
|
+
this.fetchedBoxes = [];
|
|
64
|
+
this.fetchSeq++;
|
|
65
|
+
this.renderUnits();
|
|
66
|
+
this.updateSelectedFilter();
|
|
67
|
+
this.emitSelection();
|
|
68
|
+
this.refreshUnits();
|
|
69
|
+
}
|
|
70
|
+
selectZoomChanged() {
|
|
71
|
+
if (!this.map)
|
|
72
|
+
return;
|
|
73
|
+
['units-fill', 'units-hover', 'units-line'].forEach(l => this.map.setLayerZoomRange(l, this.selectZoom, 24));
|
|
74
|
+
this.refreshUnits();
|
|
75
|
+
}
|
|
76
|
+
componentDidLoad() {
|
|
77
|
+
if (!this.lazy || !('IntersectionObserver' in window)) {
|
|
78
|
+
this.initMap();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.observer = new IntersectionObserver(entries => {
|
|
82
|
+
var _a;
|
|
83
|
+
if (entries.some(e => e.isIntersecting)) {
|
|
84
|
+
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
85
|
+
this.initMap();
|
|
86
|
+
}
|
|
87
|
+
}, { rootMargin: '300px' });
|
|
88
|
+
this.observer.observe(this.el);
|
|
89
|
+
}
|
|
90
|
+
disconnectedCallback() {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
93
|
+
(_b = this.map) === null || _b === void 0 ? void 0 : _b.remove();
|
|
94
|
+
}
|
|
95
|
+
async initMap() {
|
|
96
|
+
if (this.initStarted)
|
|
97
|
+
return;
|
|
98
|
+
this.initStarted = true;
|
|
99
|
+
try {
|
|
100
|
+
const maplibregl = await import('maplibre-gl');
|
|
101
|
+
const map = new maplibregl.Map({
|
|
102
|
+
container: this.mapContainer,
|
|
103
|
+
style: {
|
|
104
|
+
version: 8,
|
|
105
|
+
sources: {
|
|
106
|
+
basemap: {
|
|
107
|
+
type: 'raster',
|
|
108
|
+
tiles: [this.tiles],
|
|
109
|
+
tileSize: 256,
|
|
110
|
+
attribution: this.attribution,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
layers: [{ id: 'basemap', type: 'raster', source: 'basemap' }],
|
|
114
|
+
},
|
|
115
|
+
center: [this.centerLng, this.centerLat],
|
|
116
|
+
zoom: this.zoom,
|
|
117
|
+
minZoom: 5,
|
|
118
|
+
maxBounds: GERMANY_BOUNDS,
|
|
119
|
+
attributionControl: { compact: true },
|
|
120
|
+
});
|
|
121
|
+
this.map = map;
|
|
122
|
+
this.el._map = map; // e2e/test handle
|
|
123
|
+
map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
|
|
124
|
+
map.on('load', () => {
|
|
125
|
+
// PLZ boundaries in the XV accent color + dashed, so they are never mistaken
|
|
126
|
+
// for toner's black road lines on the black/white basemap
|
|
127
|
+
const accent = getComputedStyle(this.el).getPropertyValue('--link-primary').trim() || '#76b82a';
|
|
128
|
+
map.addSource('units', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
|
129
|
+
// boundary mesh only appears at selectable zoom (cleaner overview);
|
|
130
|
+
// the selected areas stay visible at every zoom level
|
|
131
|
+
map.addLayer({
|
|
132
|
+
id: 'units-fill',
|
|
133
|
+
type: 'fill',
|
|
134
|
+
source: 'units',
|
|
135
|
+
minzoom: this.selectZoom,
|
|
136
|
+
paint: { 'fill-color': accent, 'fill-opacity': 0.04 },
|
|
137
|
+
});
|
|
138
|
+
map.addLayer({
|
|
139
|
+
id: 'units-selected',
|
|
140
|
+
type: 'fill',
|
|
141
|
+
source: 'units',
|
|
142
|
+
filter: ['in', ['get', 'code'], ['literal', []]],
|
|
143
|
+
paint: { 'fill-color': accent, 'fill-opacity': 0.35 },
|
|
144
|
+
});
|
|
145
|
+
map.addLayer({
|
|
146
|
+
id: 'units-hover',
|
|
147
|
+
type: 'fill',
|
|
148
|
+
source: 'units',
|
|
149
|
+
minzoom: this.selectZoom,
|
|
150
|
+
filter: ['==', ['get', 'code'], ''],
|
|
151
|
+
paint: { 'fill-color': accent, 'fill-opacity': 0.15 },
|
|
152
|
+
});
|
|
153
|
+
map.addLayer({
|
|
154
|
+
id: 'units-line',
|
|
155
|
+
type: 'line',
|
|
156
|
+
source: 'units',
|
|
157
|
+
minzoom: this.selectZoom,
|
|
158
|
+
paint: { 'line-color': accent, 'line-width': 2, 'line-dasharray': [2, 1.5] },
|
|
159
|
+
});
|
|
160
|
+
this.popup = new maplibregl.Popup({
|
|
161
|
+
closeButton: false,
|
|
162
|
+
closeOnClick: false,
|
|
163
|
+
offset: 12,
|
|
164
|
+
maxWidth: 'none',
|
|
165
|
+
className: 'xv-geo-map-tooltip',
|
|
166
|
+
});
|
|
167
|
+
// instant toggle; a double click fires click twice (select + deselect = net zero)
|
|
168
|
+
// and then zooms, so no debounce delay is needed
|
|
169
|
+
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); });
|
|
170
|
+
map.on('mousemove', 'units-fill', e => {
|
|
171
|
+
var _a, _b;
|
|
172
|
+
const props = (_b = (_a = e.features) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.properties;
|
|
173
|
+
if (!props)
|
|
174
|
+
return;
|
|
175
|
+
map.getCanvas().style.cursor = 'pointer';
|
|
176
|
+
map.setFilter('units-hover', ['==', ['get', 'code'], props.code]);
|
|
177
|
+
const pop = this.populations.get(props.code);
|
|
178
|
+
const selected = this.selected.has(props.code);
|
|
179
|
+
this.popup
|
|
180
|
+
.setLngLat(e.lngLat)
|
|
181
|
+
.setHTML(`<strong>${props.code}</strong> ${props.name}` +
|
|
182
|
+
(pop != null ? `<br>${pop.toLocaleString('de-DE')} ${this.indicatorLabel}` : '') +
|
|
183
|
+
(selected ? '<br><em>Klicken zum Abwählen</em>' : '<br><em>Klicken zum Auswählen</em>'))
|
|
184
|
+
.addTo(map);
|
|
185
|
+
});
|
|
186
|
+
map.on('mouseleave', 'units-fill', () => {
|
|
187
|
+
map.getCanvas().style.cursor = '';
|
|
188
|
+
map.setFilter('units-hover', ['==', ['get', 'code'], '']);
|
|
189
|
+
this.popup.remove();
|
|
190
|
+
});
|
|
191
|
+
map.on('moveend', () => this.refreshUnits());
|
|
192
|
+
this.ready = true;
|
|
193
|
+
this.mapReady.emit();
|
|
194
|
+
this.resolveReady();
|
|
195
|
+
this.refreshUnits();
|
|
196
|
+
});
|
|
197
|
+
map.on('error', () => {
|
|
198
|
+
if (!this.ready)
|
|
199
|
+
this.loadError = true;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (_a) {
|
|
203
|
+
this.loadError = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async refreshUnits() {
|
|
207
|
+
if (!this.map)
|
|
208
|
+
return;
|
|
209
|
+
this.zoomGated = this.map.getZoom() < this.selectZoom;
|
|
210
|
+
if (this.zoomGated)
|
|
211
|
+
return;
|
|
212
|
+
const b = this.map.getBounds();
|
|
213
|
+
// skip the fetch entirely when the viewport is inside an already-loaded area
|
|
214
|
+
if (this.fetchedBoxes.some(f => b.getNorth() <= f.n && b.getSouth() >= f.s && b.getEast() <= f.e && b.getWest() >= f.w)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// fetch with a half-viewport margin so small pans stay within the loaded area
|
|
218
|
+
const mLat = (b.getNorth() - b.getSouth()) / 2;
|
|
219
|
+
const mLng = (b.getEast() - b.getWest()) / 2;
|
|
220
|
+
const box = { n: b.getNorth() + mLat, s: b.getSouth() - mLat, e: b.getEast() + mLng, w: b.getWest() - mLng };
|
|
221
|
+
const seq = ++this.fetchSeq;
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetch(`${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/by-bounds?resolution=${this.resolution}`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ north: box.n, south: box.s, east: box.e, west: box.w }),
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok || seq !== this.fetchSeq)
|
|
229
|
+
return;
|
|
230
|
+
const list = unwrap(await res.json());
|
|
231
|
+
this.fetchedBoxes.push(box);
|
|
232
|
+
list.forEach(u => this.units.set(u.code, u));
|
|
233
|
+
this.renderUnits();
|
|
234
|
+
this.loadPopulations(list.filter(u => !this.populations.has(u.code)));
|
|
235
|
+
}
|
|
236
|
+
catch (_a) {
|
|
237
|
+
/* viewport fetch failed (offline/CORS) — map stays usable, next moveend retries */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** fetch the configured indicator per unit for hover tooltips (fire-and-forget) */
|
|
241
|
+
async loadPopulations(units) {
|
|
242
|
+
if (!units.length)
|
|
243
|
+
return;
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${this.apiBase}/indicators/values`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ geoUnitIds: units.map(u => u.id), indicatorCodes: [this.indicator] }),
|
|
249
|
+
});
|
|
250
|
+
if (!res.ok)
|
|
251
|
+
return;
|
|
252
|
+
const idToCode = new Map(units.map(u => [u.id, u.code]));
|
|
253
|
+
for (const v of unwrap(await res.json())) {
|
|
254
|
+
const code = idToCode.get(v.geoUnitId);
|
|
255
|
+
if (code)
|
|
256
|
+
this.populations.set(code, v.value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (_a) {
|
|
260
|
+
/* tooltips just omit population */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
renderUnits() {
|
|
264
|
+
// render everything loaded so far — panning back never flickers or re-parses
|
|
265
|
+
const source = this.map.getSource('units');
|
|
266
|
+
source === null || source === void 0 ? void 0 : source.setData({
|
|
267
|
+
type: 'FeatureCollection',
|
|
268
|
+
features: [...this.units.values()].map(u => ({
|
|
269
|
+
type: 'Feature',
|
|
270
|
+
properties: { code: u.code, name: u.name },
|
|
271
|
+
geometry: u.geography,
|
|
272
|
+
})),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
toggleUnit(code) {
|
|
276
|
+
if (!code)
|
|
277
|
+
return;
|
|
278
|
+
if (this.selected.has(code))
|
|
279
|
+
this.selected.delete(code);
|
|
280
|
+
else
|
|
281
|
+
this.selected.set(code, this.units.get(code));
|
|
282
|
+
this.updateSelectedFilter();
|
|
283
|
+
this.emitSelection();
|
|
284
|
+
}
|
|
285
|
+
updateSelectedFilter() {
|
|
286
|
+
this.map.setFilter('units-selected', ['in', ['get', 'code'], ['literal', [...this.selected.keys()]]]);
|
|
287
|
+
}
|
|
288
|
+
async emitSelection() {
|
|
289
|
+
var _a, _b, _c;
|
|
290
|
+
const postalCodes = [...this.selected.keys()];
|
|
291
|
+
const unitIds = [...this.selected.values()].map(u => u.id);
|
|
292
|
+
const seq = ++this.popSeq;
|
|
293
|
+
// populations are already cached per PLZ (tooltips) — sum locally, emit instantly
|
|
294
|
+
if (postalCodes.every(c => this.populations.has(c))) {
|
|
295
|
+
const population = postalCodes.reduce((sum, c) => sum + this.populations.get(c), 0);
|
|
296
|
+
const selection = { postalCodes, population: postalCodes.length ? population : null, unitIds };
|
|
297
|
+
this.changeSelection.emit(selection);
|
|
298
|
+
return selection;
|
|
299
|
+
}
|
|
300
|
+
// fallback: some populations not cached yet — aggregate server-side
|
|
301
|
+
let population = null;
|
|
302
|
+
try {
|
|
303
|
+
const res = await fetch(`${this.apiBase}/indicators/aggregate`, {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify({ geoUnitIds: unitIds, indicatorCodes: [this.indicator] }),
|
|
307
|
+
});
|
|
308
|
+
if (res.ok)
|
|
309
|
+
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;
|
|
310
|
+
}
|
|
311
|
+
catch (_d) {
|
|
312
|
+
/* population stays null — host can fall back */
|
|
313
|
+
}
|
|
314
|
+
if (seq !== this.popSeq)
|
|
315
|
+
return null; // a newer selection superseded this one
|
|
316
|
+
const selection = { postalCodes, population, unitIds };
|
|
317
|
+
this.changeSelection.emit(selection);
|
|
318
|
+
return selection;
|
|
319
|
+
}
|
|
320
|
+
// ---- public API for host applications (Angular planner) ----
|
|
321
|
+
/** Select all PLZs within `km` around a start PLZ ("Umkreis"). Replaces the selection unless `add` is true. */
|
|
322
|
+
async selectByRadius(startPlz, km, add = false) {
|
|
323
|
+
await this.ensureReady();
|
|
324
|
+
const url = `${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/by-radius` +
|
|
325
|
+
`?address=${encodeURIComponent(startPlz)}&km=${km}&country=DE&resolution=${this.resolution}`;
|
|
326
|
+
const res = await fetch(url);
|
|
327
|
+
if (!res.ok)
|
|
328
|
+
throw new Error(`by-radius failed: HTTP ${res.status}`);
|
|
329
|
+
return this.applySelection(unwrap(await res.json()), add);
|
|
330
|
+
}
|
|
331
|
+
/** Select an explicit list of PLZs ("PLZ-Liste" paste, query-param restore). Replaces the selection unless `add` is true. */
|
|
332
|
+
async selectPostalCodes(codes, add = false) {
|
|
333
|
+
await this.ensureReady();
|
|
334
|
+
const clean = [...new Set(codes.map(c => String(c).trim()).filter(c => /^\d{5}$/.test(c)))];
|
|
335
|
+
if (!clean.length)
|
|
336
|
+
return this.applySelection([], add);
|
|
337
|
+
const res = await fetch(`${this.apiBase}/geo-units/${encodeURIComponent(this.level)}/lookup?resolution=${this.resolution}`, {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
340
|
+
body: JSON.stringify({ codes: clean }),
|
|
341
|
+
});
|
|
342
|
+
if (!res.ok)
|
|
343
|
+
throw new Error(`lookup failed: HTTP ${res.status}`);
|
|
344
|
+
return this.applySelection(unwrap(await res.json()), add);
|
|
345
|
+
}
|
|
346
|
+
/** Clear the whole selection. */
|
|
347
|
+
async clearSelection() {
|
|
348
|
+
await this.ensureReady();
|
|
349
|
+
this.selected.clear();
|
|
350
|
+
this.updateSelectedFilter();
|
|
351
|
+
this.emitSelection();
|
|
352
|
+
}
|
|
353
|
+
/** A method call means the map is needed now — start loading even if it hasn't scrolled into view. */
|
|
354
|
+
ensureReady() {
|
|
355
|
+
var _a;
|
|
356
|
+
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
357
|
+
this.initMap();
|
|
358
|
+
return this.readyPromise;
|
|
359
|
+
}
|
|
360
|
+
async applySelection(units, add) {
|
|
361
|
+
if (!add)
|
|
362
|
+
this.selected.clear();
|
|
363
|
+
units.forEach(u => {
|
|
364
|
+
this.units.set(u.code, u);
|
|
365
|
+
this.selected.set(u.code, u);
|
|
366
|
+
});
|
|
367
|
+
this.renderUnits();
|
|
368
|
+
this.updateSelectedFilter();
|
|
369
|
+
this.fitToSelection();
|
|
370
|
+
await this.loadPopulations(units.filter(u => !this.populations.has(u.code)));
|
|
371
|
+
return this.emitSelection();
|
|
372
|
+
}
|
|
373
|
+
fitToSelection() {
|
|
374
|
+
if (!this.selected.size)
|
|
375
|
+
return;
|
|
376
|
+
let minX = 180, minY = 90, maxX = -180, maxY = -90;
|
|
377
|
+
const scan = (c) => {
|
|
378
|
+
if (typeof c[0] === 'number') {
|
|
379
|
+
minX = Math.min(minX, c[0]);
|
|
380
|
+
maxX = Math.max(maxX, c[0]);
|
|
381
|
+
minY = Math.min(minY, c[1]);
|
|
382
|
+
maxY = Math.max(maxY, c[1]);
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
c.forEach(scan);
|
|
386
|
+
};
|
|
387
|
+
this.selected.forEach(u => scan(u.geography.coordinates));
|
|
388
|
+
this.map.fitBounds([[minX, minY], [maxX, maxY]], { padding: 48, maxZoom: 12, duration: 800 });
|
|
389
|
+
}
|
|
390
|
+
render() {
|
|
391
|
+
return (h(Host, { key: '82731429f864561b048ba0a21ca719a76286f884' }, h("div", { key: '7e7c074f44ec08595ea2cc5f0142ba52ba84867d', class: "container" }, h("div", { key: '5506193db332fffff278e454bf172897370ecc3a', class: "map", ref: el => (this.mapContainer = el) }), this.ready && this.zoomGated && (h("div", { key: '3db0dbe194852676b055b2a38295f6c47f031bb9', class: "hint" }, "Reinzoomen, um PLZ-Gebiete zu laden")), !this.ready && !this.loadError && (h("div", { key: 'bb26b9d1ca563095d27b0628758fabe7f1b76ec6', class: "skeleton", "aria-busy": "true" }, h("xv-loader-v2", { key: '8f1ec44838087a24467fda2b7e7f32148c42e4ff', variant: "vertical" }, "Karte wird geladen \u2026"))), this.loadError && h("div", { key: 'b352ea83fb5a2be51754aefe4443dca665ae2499', class: "skeleton error" }, "Karte konnte nicht geladen werden."))));
|
|
392
|
+
}
|
|
393
|
+
static get is() { return "xv-geo-map-v2"; }
|
|
394
|
+
static get encapsulation() { return "shadow"; }
|
|
395
|
+
static get originalStyleUrls() {
|
|
396
|
+
return {
|
|
397
|
+
"$": ["xv-geo-map.scss"]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
static get styleUrls() {
|
|
401
|
+
return {
|
|
402
|
+
"$": ["xv-geo-map.css"]
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
static get properties() {
|
|
406
|
+
return {
|
|
407
|
+
"apiBase": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"mutable": false,
|
|
410
|
+
"complexType": {
|
|
411
|
+
"original": "string",
|
|
412
|
+
"resolved": "string",
|
|
413
|
+
"references": {}
|
|
414
|
+
},
|
|
415
|
+
"required": false,
|
|
416
|
+
"optional": false,
|
|
417
|
+
"docs": {
|
|
418
|
+
"tags": [],
|
|
419
|
+
"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."
|
|
420
|
+
},
|
|
421
|
+
"getter": false,
|
|
422
|
+
"setter": false,
|
|
423
|
+
"reflect": false,
|
|
424
|
+
"attribute": "api-base",
|
|
425
|
+
"defaultValue": "'/api-fd/v1/geo'"
|
|
426
|
+
},
|
|
427
|
+
"indicator": {
|
|
428
|
+
"type": "string",
|
|
429
|
+
"mutable": false,
|
|
430
|
+
"complexType": {
|
|
431
|
+
"original": "string",
|
|
432
|
+
"resolved": "string",
|
|
433
|
+
"references": {}
|
|
434
|
+
},
|
|
435
|
+
"required": false,
|
|
436
|
+
"optional": false,
|
|
437
|
+
"docs": {
|
|
438
|
+
"tags": [],
|
|
439
|
+
"text": "Indicator whose per-unit values feed the tooltips and the emitted population sum (e.g. ATV_DEVICES)"
|
|
440
|
+
},
|
|
441
|
+
"getter": false,
|
|
442
|
+
"setter": false,
|
|
443
|
+
"reflect": false,
|
|
444
|
+
"attribute": "indicator",
|
|
445
|
+
"defaultValue": "'MBI_P_T_2025'"
|
|
446
|
+
},
|
|
447
|
+
"indicatorLabel": {
|
|
448
|
+
"type": "string",
|
|
449
|
+
"mutable": false,
|
|
450
|
+
"complexType": {
|
|
451
|
+
"original": "string",
|
|
452
|
+
"resolved": "string",
|
|
453
|
+
"references": {}
|
|
454
|
+
},
|
|
455
|
+
"required": false,
|
|
456
|
+
"optional": false,
|
|
457
|
+
"docs": {
|
|
458
|
+
"tags": [],
|
|
459
|
+
"text": "Unit label shown behind the indicator value in the tooltip"
|
|
460
|
+
},
|
|
461
|
+
"getter": false,
|
|
462
|
+
"setter": false,
|
|
463
|
+
"reflect": false,
|
|
464
|
+
"attribute": "indicator-label",
|
|
465
|
+
"defaultValue": "'Einwohner'"
|
|
466
|
+
},
|
|
467
|
+
"level": {
|
|
468
|
+
"type": "string",
|
|
469
|
+
"mutable": false,
|
|
470
|
+
"complexType": {
|
|
471
|
+
"original": "string",
|
|
472
|
+
"resolved": "string",
|
|
473
|
+
"references": {}
|
|
474
|
+
},
|
|
475
|
+
"required": false,
|
|
476
|
+
"optional": false,
|
|
477
|
+
"docs": {
|
|
478
|
+
"tags": [],
|
|
479
|
+
"text": "Geo level to display and select: plz5, gemeinde, kreis, regbez, bundesland"
|
|
480
|
+
},
|
|
481
|
+
"getter": false,
|
|
482
|
+
"setter": false,
|
|
483
|
+
"reflect": false,
|
|
484
|
+
"attribute": "level",
|
|
485
|
+
"defaultValue": "'plz5'"
|
|
486
|
+
},
|
|
487
|
+
"resolution": {
|
|
488
|
+
"type": "string",
|
|
489
|
+
"mutable": false,
|
|
490
|
+
"complexType": {
|
|
491
|
+
"original": "'medium' | 'high'",
|
|
492
|
+
"resolved": "\"high\" | \"medium\"",
|
|
493
|
+
"references": {}
|
|
494
|
+
},
|
|
495
|
+
"required": false,
|
|
496
|
+
"optional": false,
|
|
497
|
+
"docs": {
|
|
498
|
+
"tags": [],
|
|
499
|
+
"text": "Shape resolution: medium = simplified geometry (snappy), high = originals"
|
|
500
|
+
},
|
|
501
|
+
"getter": false,
|
|
502
|
+
"setter": false,
|
|
503
|
+
"reflect": false,
|
|
504
|
+
"attribute": "resolution",
|
|
505
|
+
"defaultValue": "'medium'"
|
|
506
|
+
},
|
|
507
|
+
"tiles": {
|
|
508
|
+
"type": "string",
|
|
509
|
+
"mutable": false,
|
|
510
|
+
"complexType": {
|
|
511
|
+
"original": "string",
|
|
512
|
+
"resolved": "string",
|
|
513
|
+
"references": {}
|
|
514
|
+
},
|
|
515
|
+
"required": false,
|
|
516
|
+
"optional": false,
|
|
517
|
+
"docs": {
|
|
518
|
+
"tags": [],
|
|
519
|
+
"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."
|
|
520
|
+
},
|
|
521
|
+
"getter": false,
|
|
522
|
+
"setter": false,
|
|
523
|
+
"reflect": false,
|
|
524
|
+
"attribute": "tiles",
|
|
525
|
+
"defaultValue": "'https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}@2x.png'"
|
|
526
|
+
},
|
|
527
|
+
"attribution": {
|
|
528
|
+
"type": "string",
|
|
529
|
+
"mutable": false,
|
|
530
|
+
"complexType": {
|
|
531
|
+
"original": "string",
|
|
532
|
+
"resolved": "string",
|
|
533
|
+
"references": {}
|
|
534
|
+
},
|
|
535
|
+
"required": false,
|
|
536
|
+
"optional": false,
|
|
537
|
+
"docs": {
|
|
538
|
+
"tags": [],
|
|
539
|
+
"text": ""
|
|
540
|
+
},
|
|
541
|
+
"getter": false,
|
|
542
|
+
"setter": false,
|
|
543
|
+
"reflect": false,
|
|
544
|
+
"attribute": "attribution",
|
|
545
|
+
"defaultValue": "'\u00A9 CARTO, \u00A9 OpenStreetMap contributors'"
|
|
546
|
+
},
|
|
547
|
+
"centerLng": {
|
|
548
|
+
"type": "number",
|
|
549
|
+
"mutable": false,
|
|
550
|
+
"complexType": {
|
|
551
|
+
"original": "number",
|
|
552
|
+
"resolved": "number",
|
|
553
|
+
"references": {}
|
|
554
|
+
},
|
|
555
|
+
"required": false,
|
|
556
|
+
"optional": false,
|
|
557
|
+
"docs": {
|
|
558
|
+
"tags": [],
|
|
559
|
+
"text": "Initial map center"
|
|
560
|
+
},
|
|
561
|
+
"getter": false,
|
|
562
|
+
"setter": false,
|
|
563
|
+
"reflect": false,
|
|
564
|
+
"attribute": "center-lng",
|
|
565
|
+
"defaultValue": "10.45"
|
|
566
|
+
},
|
|
567
|
+
"centerLat": {
|
|
568
|
+
"type": "number",
|
|
569
|
+
"mutable": false,
|
|
570
|
+
"complexType": {
|
|
571
|
+
"original": "number",
|
|
572
|
+
"resolved": "number",
|
|
573
|
+
"references": {}
|
|
574
|
+
},
|
|
575
|
+
"required": false,
|
|
576
|
+
"optional": false,
|
|
577
|
+
"docs": {
|
|
578
|
+
"tags": [],
|
|
579
|
+
"text": ""
|
|
580
|
+
},
|
|
581
|
+
"getter": false,
|
|
582
|
+
"setter": false,
|
|
583
|
+
"reflect": false,
|
|
584
|
+
"attribute": "center-lat",
|
|
585
|
+
"defaultValue": "51.16"
|
|
586
|
+
},
|
|
587
|
+
"zoom": {
|
|
588
|
+
"type": "number",
|
|
589
|
+
"mutable": false,
|
|
590
|
+
"complexType": {
|
|
591
|
+
"original": "number",
|
|
592
|
+
"resolved": "number",
|
|
593
|
+
"references": {}
|
|
594
|
+
},
|
|
595
|
+
"required": false,
|
|
596
|
+
"optional": false,
|
|
597
|
+
"docs": {
|
|
598
|
+
"tags": [],
|
|
599
|
+
"text": ""
|
|
600
|
+
},
|
|
601
|
+
"getter": false,
|
|
602
|
+
"setter": false,
|
|
603
|
+
"reflect": false,
|
|
604
|
+
"attribute": "zoom",
|
|
605
|
+
"defaultValue": "5.5"
|
|
606
|
+
},
|
|
607
|
+
"selectZoom": {
|
|
608
|
+
"type": "number",
|
|
609
|
+
"mutable": false,
|
|
610
|
+
"complexType": {
|
|
611
|
+
"original": "number",
|
|
612
|
+
"resolved": "number",
|
|
613
|
+
"references": {}
|
|
614
|
+
},
|
|
615
|
+
"required": false,
|
|
616
|
+
"optional": false,
|
|
617
|
+
"docs": {
|
|
618
|
+
"tags": [],
|
|
619
|
+
"text": "Minimum zoom before boundaries are loaded and selectable (use ~5 for bundesland, 9 for plz5)"
|
|
620
|
+
},
|
|
621
|
+
"getter": false,
|
|
622
|
+
"setter": false,
|
|
623
|
+
"reflect": false,
|
|
624
|
+
"attribute": "select-zoom",
|
|
625
|
+
"defaultValue": "9"
|
|
626
|
+
},
|
|
627
|
+
"lazy": {
|
|
628
|
+
"type": "boolean",
|
|
629
|
+
"mutable": false,
|
|
630
|
+
"complexType": {
|
|
631
|
+
"original": "boolean",
|
|
632
|
+
"resolved": "boolean",
|
|
633
|
+
"references": {}
|
|
634
|
+
},
|
|
635
|
+
"required": false,
|
|
636
|
+
"optional": false,
|
|
637
|
+
"docs": {
|
|
638
|
+
"tags": [],
|
|
639
|
+
"text": "Load the map bundle only when scrolled near the viewport (default: true)"
|
|
640
|
+
},
|
|
641
|
+
"getter": false,
|
|
642
|
+
"setter": false,
|
|
643
|
+
"reflect": false,
|
|
644
|
+
"attribute": "lazy",
|
|
645
|
+
"defaultValue": "true"
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
static get states() {
|
|
650
|
+
return {
|
|
651
|
+
"ready": {},
|
|
652
|
+
"loadError": {},
|
|
653
|
+
"zoomGated": {}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
static get events() {
|
|
657
|
+
return [{
|
|
658
|
+
"method": "changeSelection",
|
|
659
|
+
"name": "changeSelection",
|
|
660
|
+
"bubbles": true,
|
|
661
|
+
"cancelable": true,
|
|
662
|
+
"composed": true,
|
|
663
|
+
"docs": {
|
|
664
|
+
"tags": [],
|
|
665
|
+
"text": "Selection changed: selected PLZs + population sum of the selection"
|
|
666
|
+
},
|
|
667
|
+
"complexType": {
|
|
668
|
+
"original": "GeoMapSelection",
|
|
669
|
+
"resolved": "GeoMapSelection",
|
|
670
|
+
"references": {
|
|
671
|
+
"GeoMapSelection": {
|
|
672
|
+
"location": "local",
|
|
673
|
+
"path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
|
|
674
|
+
"id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}, {
|
|
679
|
+
"method": "mapReady",
|
|
680
|
+
"name": "mapReady",
|
|
681
|
+
"bubbles": true,
|
|
682
|
+
"cancelable": true,
|
|
683
|
+
"composed": true,
|
|
684
|
+
"docs": {
|
|
685
|
+
"tags": [],
|
|
686
|
+
"text": "Map finished initializing"
|
|
687
|
+
},
|
|
688
|
+
"complexType": {
|
|
689
|
+
"original": "void",
|
|
690
|
+
"resolved": "void",
|
|
691
|
+
"references": {}
|
|
692
|
+
}
|
|
693
|
+
}];
|
|
694
|
+
}
|
|
695
|
+
static get methods() {
|
|
696
|
+
return {
|
|
697
|
+
"selectByRadius": {
|
|
698
|
+
"complexType": {
|
|
699
|
+
"signature": "(startPlz: string, km: number, add?: boolean) => Promise<GeoMapSelection | null>",
|
|
700
|
+
"parameters": [{
|
|
701
|
+
"name": "startPlz",
|
|
702
|
+
"type": "string",
|
|
703
|
+
"docs": ""
|
|
704
|
+
}, {
|
|
705
|
+
"name": "km",
|
|
706
|
+
"type": "number",
|
|
707
|
+
"docs": ""
|
|
708
|
+
}, {
|
|
709
|
+
"name": "add",
|
|
710
|
+
"type": "boolean",
|
|
711
|
+
"docs": ""
|
|
712
|
+
}],
|
|
713
|
+
"references": {
|
|
714
|
+
"Promise": {
|
|
715
|
+
"location": "global",
|
|
716
|
+
"id": "global::Promise"
|
|
717
|
+
},
|
|
718
|
+
"GeoMapSelection": {
|
|
719
|
+
"location": "local",
|
|
720
|
+
"path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
|
|
721
|
+
"id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
"return": "Promise<GeoMapSelection>"
|
|
725
|
+
},
|
|
726
|
+
"docs": {
|
|
727
|
+
"text": "Select all PLZs within `km` around a start PLZ (\"Umkreis\"). Replaces the selection unless `add` is true.",
|
|
728
|
+
"tags": []
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
"selectPostalCodes": {
|
|
732
|
+
"complexType": {
|
|
733
|
+
"signature": "(codes: string[], add?: boolean) => Promise<GeoMapSelection | null>",
|
|
734
|
+
"parameters": [{
|
|
735
|
+
"name": "codes",
|
|
736
|
+
"type": "string[]",
|
|
737
|
+
"docs": ""
|
|
738
|
+
}, {
|
|
739
|
+
"name": "add",
|
|
740
|
+
"type": "boolean",
|
|
741
|
+
"docs": ""
|
|
742
|
+
}],
|
|
743
|
+
"references": {
|
|
744
|
+
"Promise": {
|
|
745
|
+
"location": "global",
|
|
746
|
+
"id": "global::Promise"
|
|
747
|
+
},
|
|
748
|
+
"GeoMapSelection": {
|
|
749
|
+
"location": "local",
|
|
750
|
+
"path": "/home/vsts/work/1/s/src/components/xv-geo-map/xv-geo-map.tsx",
|
|
751
|
+
"id": "src/components/xv-geo-map/xv-geo-map.tsx::GeoMapSelection"
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
"return": "Promise<GeoMapSelection>"
|
|
755
|
+
},
|
|
756
|
+
"docs": {
|
|
757
|
+
"text": "Select an explicit list of PLZs (\"PLZ-Liste\" paste, query-param restore). Replaces the selection unless `add` is true.",
|
|
758
|
+
"tags": []
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
"clearSelection": {
|
|
762
|
+
"complexType": {
|
|
763
|
+
"signature": "() => Promise<void>",
|
|
764
|
+
"parameters": [],
|
|
765
|
+
"references": {
|
|
766
|
+
"Promise": {
|
|
767
|
+
"location": "global",
|
|
768
|
+
"id": "global::Promise"
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
"return": "Promise<void>"
|
|
772
|
+
},
|
|
773
|
+
"docs": {
|
|
774
|
+
"text": "Clear the whole selection.",
|
|
775
|
+
"tags": []
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
static get elementRef() { return "el"; }
|
|
781
|
+
static get watchers() {
|
|
782
|
+
return [{
|
|
783
|
+
"propName": "level",
|
|
784
|
+
"methodName": "levelChanged"
|
|
785
|
+
}, {
|
|
786
|
+
"propName": "selectZoom",
|
|
787
|
+
"methodName": "selectZoomChanged"
|
|
788
|
+
}];
|
|
789
|
+
}
|
|
790
|
+
}
|