@ndwnu/map 0.0.1-beta.6 → 0.0.1-beta.7
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/fesm2022/ndwnu-map.mjs +541 -248
- package/fesm2022/ndwnu-map.mjs.map +1 -1
- package/package.json +3 -2
- package/types/ndwnu-map.d.ts +167 -105
package/fesm2022/ndwnu-map.mjs
CHANGED
|
@@ -1,16 +1,364 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Subject, takeUntil, filter, BehaviorSubject, map } from 'rxjs';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { viewChild, input, ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
|
|
4
|
-
import { Map } from 'maplibre-gl';
|
|
4
|
+
import { Map as Map$1 } from 'maplibre-gl';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
class MapLayer {
|
|
7
|
+
config;
|
|
8
|
+
sourceId;
|
|
9
|
+
layerIdSuffix;
|
|
10
|
+
initialized = false;
|
|
11
|
+
unsubscribe = new Subject();
|
|
12
|
+
constructor(config, sourceId, layerIdSuffix) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.sourceId = sourceId;
|
|
15
|
+
this.layerIdSuffix = layerIdSuffix;
|
|
16
|
+
}
|
|
17
|
+
get id() {
|
|
18
|
+
const suffix = this.layerIdSuffix ? `-${this.layerIdSuffix}` : '';
|
|
19
|
+
return `${this.sourceId}${suffix}`;
|
|
20
|
+
}
|
|
21
|
+
get styleLayer() {
|
|
22
|
+
return this.config.map.getLayer(this.id);
|
|
23
|
+
}
|
|
24
|
+
onInit() {
|
|
25
|
+
// Add the layer to the map, with the correct ordering (beforeId)
|
|
26
|
+
const beforeId = this.config.mapElementRepository.getBeforeId(this.config.elementId);
|
|
27
|
+
this.config.map.addLayer(this.getSpecification(), beforeId);
|
|
28
|
+
// Keeping track of which layers have been added to the map to allow for beforeId determination
|
|
29
|
+
this.#setupClickHandlers();
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
}
|
|
32
|
+
onDestroy() {
|
|
33
|
+
this.initialized = false;
|
|
34
|
+
this.#removeClickHandlers();
|
|
35
|
+
this.config.map.removeLayer(this.id);
|
|
36
|
+
this.unsubscribe.next();
|
|
37
|
+
this.unsubscribe.complete();
|
|
38
|
+
}
|
|
39
|
+
setVisible(visible) {
|
|
40
|
+
this.config.map.setLayoutProperty(this.id, 'visibility', visible ? 'visible' : 'none');
|
|
41
|
+
}
|
|
42
|
+
#setupClickHandlers() {
|
|
43
|
+
if (!this.onClick)
|
|
44
|
+
return;
|
|
45
|
+
const mapElement = this.config.mapElementRepository.getMapElementById(this.config.elementId);
|
|
46
|
+
if (!mapElement?.isInteractive) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.config.map.on('click', this.id, (event) => {
|
|
50
|
+
this.onClick?.(event.point, this.#getFeatures(event));
|
|
51
|
+
});
|
|
52
|
+
this.config.maplibreCursorService.setMouseCursor(this.config.map, this.id);
|
|
53
|
+
}
|
|
54
|
+
#removeClickHandlers() {
|
|
55
|
+
if (!this.onClick)
|
|
56
|
+
return;
|
|
57
|
+
this.config.map.off('click', this.id, (event) => {
|
|
58
|
+
this.onClick?.(event.point, this.#getFeatures(event));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
#getFeatures(event) {
|
|
62
|
+
if (event.features && event.features.length > 0) {
|
|
63
|
+
return this.#distinctFeatures(event.features);
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Filters an array of features to remove duplicates based on their properties.
|
|
69
|
+
* Two features are considered duplicates if they have identical properties.
|
|
70
|
+
* Particularly vector sources can yield duplicate features due to the way they
|
|
71
|
+
* are rendered in tiles.
|
|
72
|
+
*
|
|
73
|
+
* @param features - Array of GeoJSON features to filter
|
|
74
|
+
* @returns Array of unique features with no property duplicates
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
#distinctFeatures(features) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const uniqueFeatures = [];
|
|
80
|
+
features.forEach((feature) => {
|
|
81
|
+
const propertiesString = JSON.stringify(feature.properties);
|
|
82
|
+
if (!seen.has(propertiesString)) {
|
|
83
|
+
seen.add(propertiesString);
|
|
84
|
+
uniqueFeatures.push(feature);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return uniqueFeatures;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class ApiLayer extends MapLayer {
|
|
92
|
+
_layerSpecification;
|
|
93
|
+
constructor(config, _layerSpecification) {
|
|
94
|
+
const sourceId = 'source' in _layerSpecification ? _layerSpecification.source : '';
|
|
95
|
+
super(config, sourceId);
|
|
96
|
+
this._layerSpecification = _layerSpecification;
|
|
97
|
+
// Ensure that layout visibility is set after the base constructor has been called
|
|
98
|
+
if (this._layerSpecification.layout) {
|
|
99
|
+
this._layerSpecification.layout.visibility = 'none';
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this._layerSpecification.layout = { visibility: 'none' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
get id() {
|
|
106
|
+
return this._layerSpecification.id;
|
|
107
|
+
}
|
|
108
|
+
getSpecification() {
|
|
109
|
+
return this._layerSpecification;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class ApiBackgroundLayer extends ApiLayer {
|
|
114
|
+
constructor(config, layerSpecification) {
|
|
115
|
+
super(config, layerSpecification);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class MapElement {
|
|
120
|
+
config;
|
|
121
|
+
id;
|
|
122
|
+
elementOrder;
|
|
123
|
+
sources = [];
|
|
124
|
+
alwaysVisible = false;
|
|
125
|
+
isVisible = false;
|
|
126
|
+
isInteractive = true;
|
|
127
|
+
unsubscribe = new Subject();
|
|
128
|
+
constructor(config) {
|
|
129
|
+
this.config = config;
|
|
130
|
+
this.id = config.elementId;
|
|
131
|
+
this.elementOrder = config.elementOrder;
|
|
132
|
+
if (config.isInteractive !== undefined) {
|
|
133
|
+
this.isInteractive = config.isInteractive;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
get legendItems() {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
onInit() {
|
|
140
|
+
this.sources.forEach((source) => source.onInit());
|
|
141
|
+
}
|
|
142
|
+
onDestroy() {
|
|
143
|
+
this.sources.forEach((source) => source.onDestroy());
|
|
144
|
+
this.unsubscribe.next();
|
|
145
|
+
this.unsubscribe.complete();
|
|
146
|
+
}
|
|
147
|
+
setVisible(visible) {
|
|
148
|
+
this.isVisible = visible;
|
|
149
|
+
this.sources.forEach((source) => source.setVisible(visible));
|
|
150
|
+
}
|
|
151
|
+
reload() {
|
|
152
|
+
this.sources.forEach((source) => source.reload());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class MapSource {
|
|
157
|
+
id;
|
|
158
|
+
config;
|
|
159
|
+
layers = [];
|
|
160
|
+
unsubscribe = new Subject();
|
|
161
|
+
activeFilter;
|
|
162
|
+
#isInitialized = false;
|
|
163
|
+
#filter$;
|
|
164
|
+
#filterSubscription;
|
|
165
|
+
#featureCollection$;
|
|
166
|
+
constructor(id, config) {
|
|
167
|
+
this.id = id;
|
|
168
|
+
this.config = config;
|
|
169
|
+
}
|
|
170
|
+
onInit() {
|
|
171
|
+
if (!this.config.map.getSource(this.id)) {
|
|
172
|
+
this.config.map.addSource(this.id, this.getSpecification());
|
|
173
|
+
}
|
|
174
|
+
this.layers.forEach((layer) => layer.onInit());
|
|
175
|
+
this.isInitialized = true;
|
|
176
|
+
}
|
|
177
|
+
onDestroy() {
|
|
178
|
+
this.isInitialized = false;
|
|
179
|
+
this.layers.forEach((layer) => layer.onDestroy());
|
|
180
|
+
this.unsubscribe.next();
|
|
181
|
+
this.unsubscribe.complete();
|
|
182
|
+
}
|
|
183
|
+
get featureCollection$() {
|
|
184
|
+
return this.#featureCollection$;
|
|
185
|
+
}
|
|
186
|
+
set featureCollection$(value) {
|
|
187
|
+
this.#featureCollection$ = value;
|
|
188
|
+
this.#subscribeToFeatureCollection();
|
|
189
|
+
}
|
|
190
|
+
get isInitialized() {
|
|
191
|
+
return this.#isInitialized;
|
|
192
|
+
}
|
|
193
|
+
set isInitialized(value) {
|
|
194
|
+
this.#isInitialized = value;
|
|
195
|
+
this.#subscribeToFeatureCollection();
|
|
196
|
+
this.#subscribeToFilters();
|
|
197
|
+
}
|
|
198
|
+
get filter$() {
|
|
199
|
+
return this.#filter$;
|
|
200
|
+
}
|
|
201
|
+
set filter$(value) {
|
|
202
|
+
this.#filter$ = value;
|
|
203
|
+
this.#subscribeToFilters();
|
|
204
|
+
}
|
|
205
|
+
setVisible(visible) {
|
|
206
|
+
this.layers.forEach((layer) => layer.setVisible(visible));
|
|
207
|
+
}
|
|
208
|
+
reload() {
|
|
209
|
+
this.config.map.refreshTiles(this.id);
|
|
210
|
+
}
|
|
211
|
+
applyFilterToLayers(filterValue) {
|
|
212
|
+
const hasSourceFilter = !!this.getFilterSpecification;
|
|
213
|
+
const hasLayerFilter = this.layers.some((layer) => !!layer.getFilterSpecification);
|
|
214
|
+
if (!hasSourceFilter && !hasLayerFilter) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.activeFilter = filterValue;
|
|
218
|
+
const sourceFilter = this.getFilterSpecification?.(filterValue);
|
|
219
|
+
this.layers.forEach((layer) => {
|
|
220
|
+
const layerFilter = layer.getFilterSpecification?.(filterValue);
|
|
221
|
+
const combinedFilter = (sourceFilter && layerFilter
|
|
222
|
+
? ['all', sourceFilter, layerFilter]
|
|
223
|
+
: sourceFilter || layerFilter);
|
|
224
|
+
this.config.map.setFilter(layer.id, combinedFilter);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
#subscribeToFilters() {
|
|
228
|
+
if (!this.isInitialized)
|
|
229
|
+
return;
|
|
230
|
+
if (this.#filterSubscription) {
|
|
231
|
+
this.#filterSubscription.unsubscribe();
|
|
232
|
+
}
|
|
233
|
+
if (this.#filter$) {
|
|
234
|
+
this.#filterSubscription = this.#filter$.pipe(takeUntil(this.unsubscribe)).subscribe({
|
|
235
|
+
next: (filter) => {
|
|
236
|
+
this.applyFilterToLayers(filter);
|
|
237
|
+
},
|
|
238
|
+
error: (error) => {
|
|
239
|
+
console.error('Error updating filter', error);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.applyFilterToLayers(undefined);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
#subscribeToFeatureCollection() {
|
|
248
|
+
if (this.isInitialized && this.featureCollection$) {
|
|
249
|
+
this.featureCollection$.pipe(takeUntil(this.unsubscribe)).subscribe({
|
|
250
|
+
next: (featureCollection) => {
|
|
251
|
+
const source = this.config.map.getSource(this.id);
|
|
252
|
+
if (source && source.type === 'geojson') {
|
|
253
|
+
source.setData(featureCollection);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// not needed because source is always specified as geojson with empty collection
|
|
257
|
+
this.config.map.addSource(this.id, {
|
|
258
|
+
type: 'geojson',
|
|
259
|
+
data: featureCollection,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
error: (error) => {
|
|
264
|
+
console.error('Error updating feature collection', error);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class ApiSource extends MapSource {
|
|
272
|
+
_styleSpecification;
|
|
273
|
+
_layerFilter;
|
|
274
|
+
constructor(id, config, _styleSpecification, _layerFilter) {
|
|
275
|
+
super(id, config);
|
|
276
|
+
this._styleSpecification = _styleSpecification;
|
|
277
|
+
this._layerFilter = _layerFilter;
|
|
278
|
+
}
|
|
279
|
+
getSpecification() {
|
|
280
|
+
return this._styleSpecification.sources[this.id];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Creates ApiLayer instances for this source without initializing them.
|
|
284
|
+
*/
|
|
285
|
+
createLayers(layers) {
|
|
286
|
+
this.layers = layers
|
|
287
|
+
.filter((layer) => layer.type !== 'background')
|
|
288
|
+
.filter((layer) => layer.source === this.id)
|
|
289
|
+
.filter((layer) => !this._layerFilter || this._layerFilter(layer))
|
|
290
|
+
.map((layer) => new ApiLayer(this.config, layer));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class ApiElement extends MapElement {
|
|
295
|
+
_http;
|
|
296
|
+
_mapStyleUrl;
|
|
297
|
+
_layerFilter;
|
|
298
|
+
backgroundLayers = [];
|
|
299
|
+
#visible = false;
|
|
300
|
+
constructor(config, _http, _mapStyleUrl, _layerFilter) {
|
|
301
|
+
super(config);
|
|
302
|
+
this._http = _http;
|
|
303
|
+
this._mapStyleUrl = _mapStyleUrl;
|
|
304
|
+
this._layerFilter = _layerFilter;
|
|
305
|
+
}
|
|
306
|
+
onInit() {
|
|
307
|
+
super.onInit();
|
|
308
|
+
this._http
|
|
309
|
+
.get(this._mapStyleUrl)
|
|
310
|
+
.pipe(filter((styleSpecification) => !!styleSpecification), takeUntil(this.unsubscribe))
|
|
311
|
+
.subscribe((styleSpecification) => {
|
|
312
|
+
this.#createBackgroundLayers(styleSpecification.layers);
|
|
313
|
+
this.#createSources(styleSpecification);
|
|
314
|
+
// If setVisible() was called before these layers were created,
|
|
315
|
+
// re-apply the current visibility state so the new layers respect it.
|
|
316
|
+
this.setVisible(this.#visible);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
setVisible(visible) {
|
|
320
|
+
this.#visible = visible;
|
|
321
|
+
super.setVisible(visible);
|
|
322
|
+
this.backgroundLayers.forEach((layer) => layer.setVisible(visible));
|
|
323
|
+
}
|
|
324
|
+
#createSources(styleSpecification) {
|
|
325
|
+
const layers = styleSpecification.layers;
|
|
326
|
+
// First, create all sources and their layer instances (without initializing them)
|
|
327
|
+
this.sources = Object.entries(styleSpecification.sources).map(([id]) => {
|
|
328
|
+
const source = new ApiSource(id, this.config, styleSpecification, this._layerFilter);
|
|
329
|
+
source.onInit();
|
|
330
|
+
source.createLayers(layers);
|
|
331
|
+
return source;
|
|
332
|
+
});
|
|
333
|
+
// Then, initialize all layers according to the style.json order
|
|
334
|
+
// This ensures correct ordering regardless of which source they belong to
|
|
335
|
+
this.#initializeLayersInOrder(layers);
|
|
336
|
+
}
|
|
337
|
+
#initializeLayersInOrder(layers) {
|
|
338
|
+
// Create a map of layerId -> ApiLayer for quick lookup
|
|
339
|
+
const layerMap = new Map(this.sources.flatMap((source) => source.layers.map((layer) => [layer.id, layer])));
|
|
340
|
+
// Initialize layers in the order they appear in style.json
|
|
341
|
+
for (const layerSpec of layers) {
|
|
342
|
+
const layer = layerMap.get(layerSpec.id);
|
|
343
|
+
if (layer) {
|
|
344
|
+
layer.onInit();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// API elements are created from the style specification and this can contain layers of type 'background'
|
|
349
|
+
// These layers do not have a source and therefore don't fit in the element > source > layer hierarchy
|
|
350
|
+
// That is why we create these layers from the element itself
|
|
351
|
+
#createBackgroundLayers(layerSpecifications) {
|
|
352
|
+
this.backgroundLayers = layerSpecifications
|
|
353
|
+
.filter((layer) => layer.type === 'background')
|
|
354
|
+
.filter((layer) => !this._layerFilter || this._layerFilter(layer))
|
|
355
|
+
.map((layerSpecification) => {
|
|
356
|
+
const apiLayer = new ApiBackgroundLayer(this.config, layerSpecification);
|
|
357
|
+
apiLayer.onInit();
|
|
358
|
+
return apiLayer;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
14
362
|
|
|
15
363
|
const DEFAULT_MAP_CONFIG = {
|
|
16
364
|
center: [5.387827, 52.155172],
|
|
@@ -32,11 +380,167 @@ const COMMON_BOUNDS = {
|
|
|
32
380
|
[3.079667, 50.587611],
|
|
33
381
|
[7.572028, 53.636667],
|
|
34
382
|
],
|
|
35
|
-
AMERSFOORT: [
|
|
36
|
-
[5.34458238242172, 52.11623605695118],
|
|
37
|
-
[5.446205917577942, 52.21132028216525],
|
|
383
|
+
AMERSFOORT: [
|
|
384
|
+
[5.34458238242172, 52.11623605695118],
|
|
385
|
+
[5.446205917577942, 52.21132028216525],
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
class MapComponent {
|
|
390
|
+
mapContainer = viewChild.required('mapContainer');
|
|
391
|
+
config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
392
|
+
map;
|
|
393
|
+
ngAfterViewInit() {
|
|
394
|
+
this.map = this.#createMap(this.mapContainer().nativeElement);
|
|
395
|
+
this.map.once('load', () => this.#initiateMapLoading());
|
|
396
|
+
}
|
|
397
|
+
ngOnDestroy() {
|
|
398
|
+
// Note: Using `this.map.once('remove', this.onRemoveMap())` does not work correctly because,
|
|
399
|
+
// by the time the `onRemoveMap` event is triggered, it is no longer possible to interact with the map for cleanup.
|
|
400
|
+
// To address this we call `onRemoveMap` explicitly before destroying the MapLibre instance. This
|
|
401
|
+
// ensures that our map elements are cleaned up properly before the map is removed.
|
|
402
|
+
this.onRemoveMap();
|
|
403
|
+
this.map?.remove();
|
|
404
|
+
}
|
|
405
|
+
resizeMap() {
|
|
406
|
+
this.map?.resize();
|
|
407
|
+
}
|
|
408
|
+
zoomToLevel(zoomLevel, options) {
|
|
409
|
+
this.map?.zoomTo(zoomLevel, options);
|
|
410
|
+
}
|
|
411
|
+
#createMap(container) {
|
|
412
|
+
const config = { ...DEFAULT_MAP_CONFIG, ...this.config() };
|
|
413
|
+
const options = {
|
|
414
|
+
container,
|
|
415
|
+
style: {
|
|
416
|
+
version: 8,
|
|
417
|
+
sources: {},
|
|
418
|
+
layers: [],
|
|
419
|
+
},
|
|
420
|
+
maxZoom: config.maxZoom,
|
|
421
|
+
minZoom: config.minZoom,
|
|
422
|
+
interactive: config.interactive,
|
|
423
|
+
doubleClickZoom: config.doubleClickZoom,
|
|
424
|
+
scrollZoom: config.scrollZoom,
|
|
425
|
+
boxZoom: config.boxZoom,
|
|
426
|
+
dragPan: config.dragPan,
|
|
427
|
+
keyboard: config.keyboard,
|
|
428
|
+
touchZoomRotate: config.touchZoomRotate,
|
|
429
|
+
};
|
|
430
|
+
const map = new Map$1(options);
|
|
431
|
+
return map;
|
|
432
|
+
}
|
|
433
|
+
#initiateMapLoading() {
|
|
434
|
+
this.onLoadMap();
|
|
435
|
+
this.resizeMap();
|
|
436
|
+
}
|
|
437
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
438
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.8", type: MapComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "mapContainer", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: '<div #mapContainer class="map"></div>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
439
|
+
}
|
|
440
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MapComponent, decorators: [{
|
|
441
|
+
type: Component,
|
|
442
|
+
args: [{
|
|
443
|
+
standalone: true,
|
|
444
|
+
template: '<div #mapContainer class="map"></div>',
|
|
445
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
446
|
+
}]
|
|
447
|
+
}], propDecorators: { mapContainer: [{ type: i0.ViewChild, args: ['mapContainer', { isSignal: true }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }] } });
|
|
448
|
+
|
|
449
|
+
const BOUNDS_NL = [
|
|
450
|
+
[3.079667, 50.587611],
|
|
451
|
+
[7.572028, 53.636667],
|
|
452
|
+
];
|
|
453
|
+
const BOUNDS_AMERSFOORT = [
|
|
454
|
+
[5.34458238242172, 52.11623605695118],
|
|
455
|
+
[5.446205917577942, 52.21132028216525],
|
|
456
|
+
];
|
|
457
|
+
const lineWidthFrcSpecification = [
|
|
458
|
+
'interpolate',
|
|
459
|
+
['exponential', 1.1],
|
|
460
|
+
['zoom'],
|
|
461
|
+
10,
|
|
462
|
+
[
|
|
463
|
+
'match',
|
|
464
|
+
['get', 'functionalRoadClass'],
|
|
465
|
+
'0',
|
|
466
|
+
2.5,
|
|
467
|
+
'1',
|
|
468
|
+
2.5,
|
|
469
|
+
'2',
|
|
470
|
+
2.5,
|
|
471
|
+
'3',
|
|
472
|
+
1.5,
|
|
473
|
+
'4',
|
|
474
|
+
1.5,
|
|
475
|
+
'5',
|
|
476
|
+
1,
|
|
477
|
+
0,
|
|
478
|
+
],
|
|
479
|
+
13,
|
|
480
|
+
[
|
|
481
|
+
'match',
|
|
482
|
+
['get', 'functionalRoadClass'],
|
|
483
|
+
'0',
|
|
484
|
+
7,
|
|
485
|
+
'1',
|
|
486
|
+
7,
|
|
487
|
+
'2',
|
|
488
|
+
5.7,
|
|
489
|
+
'3',
|
|
490
|
+
4.1,
|
|
491
|
+
'4',
|
|
492
|
+
4.1,
|
|
493
|
+
'5',
|
|
494
|
+
2.7,
|
|
495
|
+
'6',
|
|
496
|
+
1.5,
|
|
497
|
+
0,
|
|
38
498
|
],
|
|
39
|
-
|
|
499
|
+
15,
|
|
500
|
+
[
|
|
501
|
+
'match',
|
|
502
|
+
['get', 'functionalRoadClass'],
|
|
503
|
+
'0',
|
|
504
|
+
10.4,
|
|
505
|
+
'1',
|
|
506
|
+
10.4,
|
|
507
|
+
'2',
|
|
508
|
+
8.4,
|
|
509
|
+
'3',
|
|
510
|
+
6.4,
|
|
511
|
+
'4',
|
|
512
|
+
6.4,
|
|
513
|
+
'5',
|
|
514
|
+
5.4,
|
|
515
|
+
'6',
|
|
516
|
+
3.4,
|
|
517
|
+
'7',
|
|
518
|
+
3.4,
|
|
519
|
+
3,
|
|
520
|
+
],
|
|
521
|
+
20,
|
|
522
|
+
[
|
|
523
|
+
'match',
|
|
524
|
+
['get', 'functionalRoadClass'],
|
|
525
|
+
'0',
|
|
526
|
+
24,
|
|
527
|
+
'1',
|
|
528
|
+
24,
|
|
529
|
+
'2',
|
|
530
|
+
22,
|
|
531
|
+
'3',
|
|
532
|
+
20,
|
|
533
|
+
'4',
|
|
534
|
+
18,
|
|
535
|
+
'5',
|
|
536
|
+
16,
|
|
537
|
+
'6',
|
|
538
|
+
14,
|
|
539
|
+
'7',
|
|
540
|
+
10,
|
|
541
|
+
8,
|
|
542
|
+
],
|
|
543
|
+
];
|
|
40
544
|
|
|
41
545
|
/**
|
|
42
546
|
* Repository for managing map elements.
|
|
@@ -44,6 +548,8 @@ const COMMON_BOUNDS = {
|
|
|
44
548
|
* Also tracks element visibility state and provides observable streams for elements.
|
|
45
549
|
*
|
|
46
550
|
* @typeparam TElementType - The type of ID used for the map elements
|
|
551
|
+
* @typeparam TFilter - The type of filter used for the map elements
|
|
552
|
+
* @typeparam TLegendItem - The type of legend item used for the map elements
|
|
47
553
|
*/
|
|
48
554
|
class MapElementRepository {
|
|
49
555
|
/** Subject that holds the current array of map elements */
|
|
@@ -55,6 +561,8 @@ class MapElementRepository {
|
|
|
55
561
|
mapElements$ = this.mapElementsSubject.asObservable();
|
|
56
562
|
/** Observable stream of only visible map elements */
|
|
57
563
|
visibleMapElements$ = this.mapElements$.pipe(map((elements) => elements.filter((element) => element.isVisible)));
|
|
564
|
+
/** Observable stream of legend items from all visible map elements */
|
|
565
|
+
legendItems$ = this.visibleMapElements$.pipe(map((elements) => elements.flatMap((element) => element.legendItems)));
|
|
58
566
|
/**
|
|
59
567
|
* Gets the ids (TElementType) as an array of all map element
|
|
60
568
|
* @returns Array of TElementType
|
|
@@ -65,6 +573,13 @@ class MapElementRepository {
|
|
|
65
573
|
* @returns Array of TElementType
|
|
66
574
|
*/
|
|
67
575
|
visibleMapElementIds$ = this.visibleMapElements$.pipe(map((elements) => elements.map((element) => element.id)));
|
|
576
|
+
/**
|
|
577
|
+
* Gets the ids (TElementType) as an array of all visible map element
|
|
578
|
+
* @returns Array of TElementType
|
|
579
|
+
*/
|
|
580
|
+
get visibleMapElementIds() {
|
|
581
|
+
return this.visibleMapElements.map((element) => element.id);
|
|
582
|
+
}
|
|
68
583
|
/**
|
|
69
584
|
* Gets the current array of all map elements
|
|
70
585
|
* @returns Array of map elements
|
|
@@ -162,6 +677,17 @@ class MapElementRepository {
|
|
|
162
677
|
hideMapElement(elementId) {
|
|
163
678
|
this.setMapElementVisibility(elementId, false);
|
|
164
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Triggers a reload of the tiles in the sources of the map element with the given ID
|
|
682
|
+
*
|
|
683
|
+
* @param elementId - The ID of the element to reload
|
|
684
|
+
*/
|
|
685
|
+
reloadMapElement(elementId) {
|
|
686
|
+
const element = this.getMapElementById(elementId);
|
|
687
|
+
if (element) {
|
|
688
|
+
element.reload();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
165
691
|
/**
|
|
166
692
|
* Toggles the visibility of a map element by its ID.
|
|
167
693
|
* If the element is currently visible, it will be hidden, and vice versa.
|
|
@@ -199,239 +725,6 @@ class MapElementRepository {
|
|
|
199
725
|
}
|
|
200
726
|
}
|
|
201
727
|
|
|
202
|
-
class MapElement {
|
|
203
|
-
config;
|
|
204
|
-
id;
|
|
205
|
-
elementOrder;
|
|
206
|
-
sources = [];
|
|
207
|
-
alwaysVisible = false;
|
|
208
|
-
isVisible = false;
|
|
209
|
-
unsubscribe = new Subject();
|
|
210
|
-
constructor(config) {
|
|
211
|
-
this.config = config;
|
|
212
|
-
this.id = config.elementId;
|
|
213
|
-
this.elementOrder = config.elementOrder;
|
|
214
|
-
}
|
|
215
|
-
onInit() {
|
|
216
|
-
this.sources.forEach((source) => source.onInit());
|
|
217
|
-
}
|
|
218
|
-
onDestroy() {
|
|
219
|
-
this.sources.forEach((source) => source.onDestroy());
|
|
220
|
-
this.unsubscribe.next();
|
|
221
|
-
this.unsubscribe.complete();
|
|
222
|
-
}
|
|
223
|
-
setVisible(visible) {
|
|
224
|
-
this.isVisible = visible;
|
|
225
|
-
this.sources.forEach((source) => source.setVisible(visible));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
class MapLayer {
|
|
230
|
-
config;
|
|
231
|
-
sourceId;
|
|
232
|
-
layerIdSuffix;
|
|
233
|
-
initialized = false;
|
|
234
|
-
unsubscribe = new Subject();
|
|
235
|
-
constructor(config, sourceId, layerIdSuffix) {
|
|
236
|
-
this.config = config;
|
|
237
|
-
this.sourceId = sourceId;
|
|
238
|
-
this.layerIdSuffix = layerIdSuffix;
|
|
239
|
-
}
|
|
240
|
-
get id() {
|
|
241
|
-
const suffix = this.layerIdSuffix ? `-${this.layerIdSuffix}` : '';
|
|
242
|
-
return `${this.sourceId}${suffix}`;
|
|
243
|
-
}
|
|
244
|
-
get styleLayer() {
|
|
245
|
-
return this.config.map.getLayer(this.id);
|
|
246
|
-
}
|
|
247
|
-
onInit() {
|
|
248
|
-
// Add the layer to the map, with the correct ordering (beforeId)
|
|
249
|
-
const beforeId = this.config.mapElementRepository.getBeforeId(this.config.elementId);
|
|
250
|
-
this.config.map.addLayer(this.getSpecification(), beforeId);
|
|
251
|
-
// Keeping track of which layers have been added to the map to allow for beforeId determination
|
|
252
|
-
this.#setupClickHandlers();
|
|
253
|
-
this.initialized = true;
|
|
254
|
-
}
|
|
255
|
-
onDestroy() {
|
|
256
|
-
this.initialized = false;
|
|
257
|
-
this.#removeClickHandlers();
|
|
258
|
-
this.config.map.removeLayer(this.id);
|
|
259
|
-
this.unsubscribe.next();
|
|
260
|
-
this.unsubscribe.complete();
|
|
261
|
-
}
|
|
262
|
-
setVisible(visible) {
|
|
263
|
-
this.config.map.setLayoutProperty(this.id, 'visibility', visible ? 'visible' : 'none');
|
|
264
|
-
}
|
|
265
|
-
#setupClickHandlers() {
|
|
266
|
-
if (!this.onClick)
|
|
267
|
-
return;
|
|
268
|
-
this.config.map.on('click', this.id, (event) => {
|
|
269
|
-
this.onClick?.(this.#getFeatures(event));
|
|
270
|
-
});
|
|
271
|
-
this.config.maplibreCursorService.setMouseCursor(this.config.map, this.id);
|
|
272
|
-
}
|
|
273
|
-
#removeClickHandlers() {
|
|
274
|
-
if (!this.onClick)
|
|
275
|
-
return;
|
|
276
|
-
this.config.map.off('click', this.id, (event) => {
|
|
277
|
-
this.onClick?.(this.#getFeatures(event));
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
#getFeatures(event) {
|
|
281
|
-
if (event.features && event.features.length > 0) {
|
|
282
|
-
return this.#distinctFeatures(event.features);
|
|
283
|
-
}
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Filters an array of features to remove duplicates based on their properties.
|
|
288
|
-
* Two features are considered duplicates if they have identical properties.
|
|
289
|
-
* Particularly vector sources can yield duplicate features due to the way they
|
|
290
|
-
* are rendered in tiles.
|
|
291
|
-
*
|
|
292
|
-
* @param features - Array of GeoJSON features to filter
|
|
293
|
-
* @returns Array of unique features with no property duplicates
|
|
294
|
-
* @private
|
|
295
|
-
*/
|
|
296
|
-
#distinctFeatures(features) {
|
|
297
|
-
const seen = new Set();
|
|
298
|
-
const uniqueFeatures = [];
|
|
299
|
-
features.forEach((feature) => {
|
|
300
|
-
const propertiesString = JSON.stringify(feature.properties);
|
|
301
|
-
if (!seen.has(propertiesString)) {
|
|
302
|
-
seen.add(propertiesString);
|
|
303
|
-
uniqueFeatures.push(feature);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
return uniqueFeatures;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
class MapSource {
|
|
311
|
-
id;
|
|
312
|
-
config;
|
|
313
|
-
layers = [];
|
|
314
|
-
unsubscribe = new Subject();
|
|
315
|
-
#isInitialized = false;
|
|
316
|
-
#featureCollection$;
|
|
317
|
-
constructor(id, config) {
|
|
318
|
-
this.id = id;
|
|
319
|
-
this.config = config;
|
|
320
|
-
}
|
|
321
|
-
onInit() {
|
|
322
|
-
if (!this.config.map.getSource(this.id)) {
|
|
323
|
-
this.config.map.addSource(this.id, this.getSpecification());
|
|
324
|
-
}
|
|
325
|
-
this.layers.forEach((layer) => layer.onInit());
|
|
326
|
-
this.isInitialized = true;
|
|
327
|
-
}
|
|
328
|
-
onDestroy() {
|
|
329
|
-
this.isInitialized = false;
|
|
330
|
-
this.layers.forEach((layer) => layer.onDestroy());
|
|
331
|
-
this.unsubscribe.next();
|
|
332
|
-
this.unsubscribe.complete();
|
|
333
|
-
}
|
|
334
|
-
get featureCollection$() {
|
|
335
|
-
return this.#featureCollection$;
|
|
336
|
-
}
|
|
337
|
-
set featureCollection$(value) {
|
|
338
|
-
this.#featureCollection$ = value;
|
|
339
|
-
this.#subscribeToFeatureCollection();
|
|
340
|
-
}
|
|
341
|
-
get isInitialized() {
|
|
342
|
-
return this.#isInitialized;
|
|
343
|
-
}
|
|
344
|
-
set isInitialized(value) {
|
|
345
|
-
this.#isInitialized = value;
|
|
346
|
-
this.#subscribeToFeatureCollection();
|
|
347
|
-
}
|
|
348
|
-
setVisible(visible) {
|
|
349
|
-
this.layers.forEach((layer) => layer.setVisible(visible));
|
|
350
|
-
}
|
|
351
|
-
#subscribeToFeatureCollection() {
|
|
352
|
-
if (this.isInitialized && this.featureCollection$) {
|
|
353
|
-
this.featureCollection$.pipe(takeUntil(this.unsubscribe)).subscribe({
|
|
354
|
-
next: (featureCollection) => {
|
|
355
|
-
const source = this.config.map.getSource(this.id);
|
|
356
|
-
if (source && source.type === 'geojson') {
|
|
357
|
-
source.setData(featureCollection);
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
// not needed because source is always specified as geojson with empty collection
|
|
361
|
-
this.config.map.addSource(this.id, {
|
|
362
|
-
type: 'geojson',
|
|
363
|
-
data: featureCollection,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
error: (error) => {
|
|
368
|
-
console.error('Error updating feature collection', error);
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
class MapComponent {
|
|
376
|
-
mapContainer = viewChild.required('mapContainer');
|
|
377
|
-
config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
378
|
-
map;
|
|
379
|
-
ngAfterViewInit() {
|
|
380
|
-
this.map = this.#createMap(this.mapContainer().nativeElement);
|
|
381
|
-
this.map.once('load', () => this.#initiateMapLoading());
|
|
382
|
-
}
|
|
383
|
-
ngOnDestroy() {
|
|
384
|
-
// Note: Using `this.map.once('remove', this.onRemoveMap())` does not work correctly because,
|
|
385
|
-
// by the time the `onRemoveMap` event is triggered, it is no longer possible to interact with the map for cleanup.
|
|
386
|
-
// To address this we call `onRemoveMap` explicitly before destroying the MapLibre instance. This
|
|
387
|
-
// ensures that our map elements are cleaned up properly before the map is removed.
|
|
388
|
-
this.onRemoveMap();
|
|
389
|
-
this.map?.remove();
|
|
390
|
-
}
|
|
391
|
-
resizeMap() {
|
|
392
|
-
this.map?.resize();
|
|
393
|
-
}
|
|
394
|
-
zoomToLevel(zoomLevel, options) {
|
|
395
|
-
this.map?.zoomTo(zoomLevel, options);
|
|
396
|
-
}
|
|
397
|
-
#createMap(container) {
|
|
398
|
-
const config = { ...DEFAULT_MAP_CONFIG, ...this.config() };
|
|
399
|
-
const options = {
|
|
400
|
-
container,
|
|
401
|
-
style: {
|
|
402
|
-
version: 8,
|
|
403
|
-
sources: {},
|
|
404
|
-
layers: [],
|
|
405
|
-
},
|
|
406
|
-
maxZoom: config.maxZoom,
|
|
407
|
-
minZoom: config.minZoom,
|
|
408
|
-
interactive: config.interactive,
|
|
409
|
-
doubleClickZoom: config.doubleClickZoom,
|
|
410
|
-
scrollZoom: config.scrollZoom,
|
|
411
|
-
boxZoom: config.boxZoom,
|
|
412
|
-
dragPan: config.dragPan,
|
|
413
|
-
keyboard: config.keyboard,
|
|
414
|
-
touchZoomRotate: config.touchZoomRotate,
|
|
415
|
-
};
|
|
416
|
-
const map = new Map(options);
|
|
417
|
-
return map;
|
|
418
|
-
}
|
|
419
|
-
#initiateMapLoading() {
|
|
420
|
-
this.onLoadMap();
|
|
421
|
-
this.resizeMap();
|
|
422
|
-
}
|
|
423
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
424
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.8", type: MapComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "mapContainer", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: '<div #mapContainer class="map"></div>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
425
|
-
}
|
|
426
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MapComponent, decorators: [{
|
|
427
|
-
type: Component,
|
|
428
|
-
args: [{
|
|
429
|
-
standalone: true,
|
|
430
|
-
template: '<div #mapContainer class="map"></div>',
|
|
431
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
432
|
-
}]
|
|
433
|
-
}], propDecorators: { mapContainer: [{ type: i0.ViewChild, args: ['mapContainer', { isSignal: true }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }] } });
|
|
434
|
-
|
|
435
728
|
/**
|
|
436
729
|
* Service for managing MapLibre map cursor interactions.
|
|
437
730
|
*
|
|
@@ -528,5 +821,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
528
821
|
* Generated bundle index. Do not edit.
|
|
529
822
|
*/
|
|
530
823
|
|
|
531
|
-
export { BOUNDS_AMERSFOORT, BOUNDS_NL, COMMON_BOUNDS, DEFAULT_MAP_CONFIG, MapComponent, MapElement, MapElementRepository, MapLayer, MapSource, MaplibreCursorService };
|
|
824
|
+
export { ApiBackgroundLayer, ApiElement, ApiLayer, ApiSource, BOUNDS_AMERSFOORT, BOUNDS_NL, COMMON_BOUNDS, DEFAULT_MAP_CONFIG, MapComponent, MapElement, MapElementRepository, MapLayer, MapSource, MaplibreCursorService, lineWidthFrcSpecification };
|
|
532
825
|
//# sourceMappingURL=ndwnu-map.mjs.map
|