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