@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.
@@ -1,16 +1,364 @@
1
- import { BehaviorSubject, map, Subject, takeUntil } from 'rxjs';
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
- const BOUNDS_NL = [
7
- [3.079667, 50.587611],
8
- [7.572028, 53.636667],
9
- ];
10
- const BOUNDS_AMERSFOORT = [
11
- [5.34458238242172, 52.11623605695118],
12
- [5.446205917577942, 52.21132028216525],
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