@ndwnu/map 0.0.1-beta.5 → 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,230 +1,7 @@
1
- import { BehaviorSubject, map, Subject, takeUntil, filter } 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';
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
- ];
14
-
15
- const DEFAULT_MAP_CONFIG = {
16
- center: [5.387827, 52.155172],
17
- zoom: 8,
18
- maxZoom: 18,
19
- minZoom: 6,
20
- interactive: true,
21
- dragRotate: false,
22
- doubleClickZoom: true,
23
- scrollZoom: true,
24
- boxZoom: true,
25
- dragPan: true,
26
- keyboard: true,
27
- touchZoomRotate: true,
28
- };
29
- // Common bounds that users can optionally use
30
- const COMMON_BOUNDS = {
31
- NETHERLANDS: [
32
- [3.079667, 50.587611],
33
- [7.572028, 53.636667],
34
- ],
35
- AMERSFOORT: [
36
- [5.34458238242172, 52.11623605695118],
37
- [5.446205917577942, 52.21132028216525],
38
- ],
39
- };
40
-
41
- /**
42
- * Repository for managing map elements.
43
- * Provides methods to add, remove, show, hide, and toggle map elements.
44
- * Also tracks element visibility state and provides observable streams for elements.
45
- *
46
- * @typeparam TElementType - The type of ID used for the map elements
47
- */
48
- class MapElementRepository {
49
- /** Subject that holds the current array of map elements */
50
- mapElementsSubject = new BehaviorSubject([]);
51
- // mapElements are not directly exposed to prevent circular reference errors when
52
- // they are used in a template. Because the mapElements$ observable contains MapLibre
53
- // GL JS map objects, which have circular references.
54
- /** Observable stream of all map elements */
55
- mapElements$ = this.mapElementsSubject.asObservable();
56
- /** Observable stream of only visible map elements */
57
- visibleMapElements$ = this.mapElements$.pipe(map((elements) => elements.filter((element) => element.isVisible)));
58
- /**
59
- * Gets the ids (TElementType) as an array of all map element
60
- * @returns Array of TElementType
61
- */
62
- mapElementIds$ = this.mapElements$.pipe(map((elements) => elements.map((element) => element.id)));
63
- /**
64
- * Gets the ids (TElementType) as an array of all visible map element
65
- * @returns Array of TElementType
66
- */
67
- visibleMapElementIds$ = this.visibleMapElements$.pipe(map((elements) => elements.map((element) => element.id)));
68
- /**
69
- * Gets the current array of all map elements
70
- * @returns Array of map elements
71
- */
72
- get mapElements() {
73
- return this.mapElementsSubject.getValue();
74
- }
75
- /**
76
- * Gets only the currently visible map elements
77
- * @returns Array of visible map elements
78
- */
79
- get visibleMapElements() {
80
- return this.mapElements.filter((element) => element.isVisible);
81
- }
82
- /**
83
- * Finds a map element by its ID
84
- *
85
- * @param elementId - The ID of the element to find
86
- * @returns The map element if found, undefined otherwise
87
- */
88
- getMapElementById(elementId) {
89
- return this.mapElements.find((element) => element.id === elementId);
90
- }
91
- /**
92
- * Checks if a map element is currently visible
93
- *
94
- * @param elementId - The ID of the element to check
95
- * @returns True if the element is visible, false otherwise
96
- */
97
- isMapElementVisible(elementId) {
98
- return this.visibleMapElements.some((element) => element.id === elementId);
99
- }
100
- /**
101
- * Adds a new map element to the repository and initializes it.
102
- * Sets the element's visibility based on its configuration.
103
- *
104
- * @param mapElement - The map element to add
105
- */
106
- addMapElement(mapElement) {
107
- const currentElements = this.mapElements;
108
- this.mapElementsSubject.next([...currentElements, mapElement]);
109
- mapElement.onInit();
110
- if (mapElement.alwaysVisible || this.isMapElementVisible(mapElement.id)) {
111
- this.showMapElement(mapElement.id);
112
- }
113
- else {
114
- this.hideMapElement(mapElement.id);
115
- }
116
- }
117
- /**
118
- * Removes a map element from the repository and destroys it.
119
- *
120
- * @param mapElement - The map element to remove
121
- */
122
- removeMapElement(mapElement) {
123
- const filteredElements = this.mapElements.filter((element) => element.id !== mapElement.id);
124
- this.mapElementsSubject.next(filteredElements);
125
- mapElement.onDestroy();
126
- }
127
- /**
128
- * Removes all map elements from the repository and destroys them.
129
- */
130
- removeAllMapElements() {
131
- this.mapElements.forEach((element) => {
132
- this.removeMapElement(element);
133
- });
134
- }
135
- /**
136
- * Sets the visibility of a map element by its ID and updates the subject with the new state.
137
- *
138
- * @param elementId - The ID of the element to update
139
- * @param visible - Whether the element should be visible or hidden
140
- */
141
- setMapElementVisibility(elementId, visible) {
142
- const element = this.getMapElementById(elementId);
143
- if (element) {
144
- element.setVisible(visible);
145
- const elements = this.mapElements.filter((el) => el.id !== elementId);
146
- this.mapElementsSubject.next([...elements, element]);
147
- }
148
- }
149
- /**
150
- * Shows a map element by its ID and updates the subject with the new state.
151
- *
152
- * @param elementId - The ID of the element to show
153
- */
154
- showMapElement(elementId) {
155
- this.setMapElementVisibility(elementId, true);
156
- }
157
- /**
158
- * Hides a map element by its ID and updates the subject with the new state.
159
- *
160
- * @param elementId - The ID of the element to hide
161
- */
162
- hideMapElement(elementId) {
163
- this.setMapElementVisibility(elementId, false);
164
- }
165
- /**
166
- * Toggles the visibility of a map element by its ID.
167
- * If the element is currently visible, it will be hidden, and vice versa.
168
- *
169
- * @param elementId - The ID of the element to toggle visibility
170
- */
171
- toggleMapElement(elementId) {
172
- if (this.visibleMapElements.some((element) => element.id === elementId)) {
173
- this.hideMapElement(elementId);
174
- }
175
- else {
176
- this.showMapElement(elementId);
177
- }
178
- }
179
- /**
180
- * Gets the ID of the layer that should come before the specified element's layers
181
- * in the map's layer stack. This is used for maintaining proper layer ordering.
182
- *
183
- * @param elementId - The ID of the element to find a "before" reference for
184
- * @returns The ID of the first layer of the next element in order, or undefined if none exists
185
- */
186
- getBeforeId(elementId) {
187
- const currentElement = this.getMapElementById(elementId);
188
- if (!currentElement) {
189
- return undefined;
190
- }
191
- const nextElement = this.mapElements
192
- .filter((item) => item.elementOrder > currentElement.elementOrder)
193
- .sort((a, b) => a.elementOrder - b.elementOrder)[0];
194
- const nextElementLayerIds = nextElement?.sources
195
- .flatMap((source) => source.layers)
196
- .filter((layer) => layer.initialized)
197
- .map((layer) => layer.id);
198
- return nextElement && nextElementLayerIds.length > 0 ? nextElementLayerIds[0] : undefined;
199
- }
200
- }
201
-
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
- }
4
+ import { Map as Map$1 } from 'maplibre-gl';
228
5
 
229
6
  class MapLayer {
230
7
  config;
@@ -265,8 +42,12 @@ class MapLayer {
265
42
  #setupClickHandlers() {
266
43
  if (!this.onClick)
267
44
  return;
45
+ const mapElement = this.config.mapElementRepository.getMapElementById(this.config.elementId);
46
+ if (!mapElement?.isInteractive) {
47
+ return;
48
+ }
268
49
  this.config.map.on('click', this.id, (event) => {
269
- this.onClick?.(this.#getFeatures(event));
50
+ this.onClick?.(event.point, this.#getFeatures(event));
270
51
  });
271
52
  this.config.maplibreCursorService.setMouseCursor(this.config.map, this.id);
272
53
  }
@@ -274,7 +55,7 @@ class MapLayer {
274
55
  if (!this.onClick)
275
56
  return;
276
57
  this.config.map.off('click', this.id, (event) => {
277
- this.onClick?.(this.#getFeatures(event));
58
+ this.onClick?.(event.point, this.#getFeatures(event));
278
59
  });
279
60
  }
280
61
  #getFeatures(event) {
@@ -307,27 +88,95 @@ class MapLayer {
307
88
  }
308
89
  }
309
90
 
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());
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' };
324
103
  }
325
- this.layers.forEach((layer) => layer.onInit());
326
- this.isInitialized = true;
327
104
  }
328
- onDestroy() {
329
- this.isInitialized = false;
330
- this.layers.forEach((layer) => layer.onDestroy());
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());
331
180
  this.unsubscribe.next();
332
181
  this.unsubscribe.complete();
333
182
  }
@@ -344,93 +193,537 @@ class MapSource {
344
193
  set isInitialized(value) {
345
194
  this.#isInitialized = value;
346
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
+ }
362
+
363
+ const DEFAULT_MAP_CONFIG = {
364
+ center: [5.387827, 52.155172],
365
+ zoom: 8,
366
+ maxZoom: 18,
367
+ minZoom: 6,
368
+ interactive: true,
369
+ dragRotate: false,
370
+ doubleClickZoom: true,
371
+ scrollZoom: true,
372
+ boxZoom: true,
373
+ dragPan: true,
374
+ keyboard: true,
375
+ touchZoomRotate: true,
376
+ };
377
+ // Common bounds that users can optionally use
378
+ const COMMON_BOUNDS = {
379
+ NETHERLANDS: [
380
+ [3.079667, 50.587611],
381
+ [7.572028, 53.636667],
382
+ ],
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,
498
+ ],
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
+ ];
544
+
545
+ /**
546
+ * Repository for managing map elements.
547
+ * Provides methods to add, remove, show, hide, and toggle map elements.
548
+ * Also tracks element visibility state and provides observable streams for elements.
549
+ *
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
553
+ */
554
+ class MapElementRepository {
555
+ /** Subject that holds the current array of map elements */
556
+ mapElementsSubject = new BehaviorSubject([]);
557
+ // mapElements are not directly exposed to prevent circular reference errors when
558
+ // they are used in a template. Because the mapElements$ observable contains MapLibre
559
+ // GL JS map objects, which have circular references.
560
+ /** Observable stream of all map elements */
561
+ mapElements$ = this.mapElementsSubject.asObservable();
562
+ /** Observable stream of only visible map elements */
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)));
566
+ /**
567
+ * Gets the ids (TElementType) as an array of all map element
568
+ * @returns Array of TElementType
569
+ */
570
+ mapElementIds$ = this.mapElements$.pipe(map((elements) => elements.map((element) => element.id)));
571
+ /**
572
+ * Gets the ids (TElementType) as an array of all visible map element
573
+ * @returns Array of TElementType
574
+ */
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
+ }
583
+ /**
584
+ * Gets the current array of all map elements
585
+ * @returns Array of map elements
586
+ */
587
+ get mapElements() {
588
+ return this.mapElementsSubject.getValue();
589
+ }
590
+ /**
591
+ * Gets only the currently visible map elements
592
+ * @returns Array of visible map elements
593
+ */
594
+ get visibleMapElements() {
595
+ return this.mapElements.filter((element) => element.isVisible);
596
+ }
597
+ /**
598
+ * Finds a map element by its ID
599
+ *
600
+ * @param elementId - The ID of the element to find
601
+ * @returns The map element if found, undefined otherwise
602
+ */
603
+ getMapElementById(elementId) {
604
+ return this.mapElements.find((element) => element.id === elementId);
605
+ }
606
+ /**
607
+ * Checks if a map element is currently visible
608
+ *
609
+ * @param elementId - The ID of the element to check
610
+ * @returns True if the element is visible, false otherwise
611
+ */
612
+ isMapElementVisible(elementId) {
613
+ return this.visibleMapElements.some((element) => element.id === elementId);
614
+ }
615
+ /**
616
+ * Adds a new map element to the repository and initializes it.
617
+ * Sets the element's visibility based on its configuration.
618
+ *
619
+ * @param mapElement - The map element to add
620
+ */
621
+ addMapElement(mapElement) {
622
+ const currentElements = this.mapElements;
623
+ this.mapElementsSubject.next([...currentElements, mapElement]);
624
+ mapElement.onInit();
625
+ if (mapElement.alwaysVisible || this.isMapElementVisible(mapElement.id)) {
626
+ this.showMapElement(mapElement.id);
627
+ }
628
+ else {
629
+ this.hideMapElement(mapElement.id);
630
+ }
631
+ }
632
+ /**
633
+ * Removes a map element from the repository and destroys it.
634
+ *
635
+ * @param mapElement - The map element to remove
636
+ */
637
+ removeMapElement(mapElement) {
638
+ const filteredElements = this.mapElements.filter((element) => element.id !== mapElement.id);
639
+ this.mapElementsSubject.next(filteredElements);
640
+ mapElement.onDestroy();
347
641
  }
348
- setVisible(visible) {
349
- this.layers.forEach((layer) => layer.setVisible(visible));
642
+ /**
643
+ * Removes all map elements from the repository and destroys them.
644
+ */
645
+ removeAllMapElements() {
646
+ this.mapElements.forEach((element) => {
647
+ this.removeMapElement(element);
648
+ });
350
649
  }
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
- });
650
+ /**
651
+ * Sets the visibility of a map element by its ID and updates the subject with the new state.
652
+ *
653
+ * @param elementId - The ID of the element to update
654
+ * @param visible - Whether the element should be visible or hidden
655
+ */
656
+ setMapElementVisibility(elementId, visible) {
657
+ const element = this.getMapElementById(elementId);
658
+ if (element) {
659
+ element.setVisible(visible);
660
+ const elements = this.mapElements.filter((el) => el.id !== elementId);
661
+ this.mapElementsSubject.next([...elements, element]);
371
662
  }
372
663
  }
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();
664
+ /**
665
+ * Shows a map element by its ID and updates the subject with the new state.
666
+ *
667
+ * @param elementId - The ID of the element to show
668
+ */
669
+ showMapElement(elementId) {
670
+ this.setMapElementVisibility(elementId, true);
390
671
  }
391
- resizeMap() {
392
- this.map?.resize();
672
+ /**
673
+ * Hides a map element by its ID and updates the subject with the new state.
674
+ *
675
+ * @param elementId - The ID of the element to hide
676
+ */
677
+ hideMapElement(elementId) {
678
+ this.setMapElementVisibility(elementId, false);
393
679
  }
394
- zoomToLevel(zoomLevel, options) {
395
- this.map?.zoomTo(zoomLevel, options);
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
+ }
396
690
  }
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;
691
+ /**
692
+ * Toggles the visibility of a map element by its ID.
693
+ * If the element is currently visible, it will be hidden, and vice versa.
694
+ *
695
+ * @param elementId - The ID of the element to toggle visibility
696
+ */
697
+ toggleMapElement(elementId) {
698
+ if (this.visibleMapElements.some((element) => element.id === elementId)) {
699
+ this.hideMapElement(elementId);
700
+ }
701
+ else {
702
+ this.showMapElement(elementId);
703
+ }
418
704
  }
419
- #initiateMapLoading() {
420
- this.onLoadMap();
421
- this.resizeMap();
705
+ /**
706
+ * Gets the ID of the layer that should come before the specified element's layers
707
+ * in the map's layer stack. This is used for maintaining proper layer ordering.
708
+ *
709
+ * @param elementId - The ID of the element to find a "before" reference for
710
+ * @returns The ID of the first layer of the next element in order, or undefined if none exists
711
+ */
712
+ getBeforeId(elementId) {
713
+ const currentElement = this.getMapElementById(elementId);
714
+ if (!currentElement) {
715
+ return undefined;
716
+ }
717
+ const nextElement = this.mapElements
718
+ .filter((item) => item.elementOrder > currentElement.elementOrder)
719
+ .sort((a, b) => a.elementOrder - b.elementOrder)[0];
720
+ const nextElementLayerIds = nextElement?.sources
721
+ .flatMap((source) => source.layers)
722
+ .filter((layer) => layer.initialized)
723
+ .map((layer) => layer.id);
724
+ return nextElement && nextElementLayerIds.length > 0 ? nextElementLayerIds[0] : undefined;
422
725
  }
423
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: MapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
424
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.9", 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
726
  }
426
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", 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
727
 
435
728
  /**
436
729
  * Service for managing MapLibre map cursor interactions.
@@ -514,125 +807,19 @@ class MaplibreCursorService {
514
807
  console.warn(`Cursor type '${cursor}' is not supported.`);
515
808
  }
516
809
  }
517
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: MaplibreCursorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
518
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: MaplibreCursorService, providedIn: 'root' });
810
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MaplibreCursorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
811
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MaplibreCursorService, providedIn: 'root' });
519
812
  }
520
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: MaplibreCursorService, decorators: [{
813
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MaplibreCursorService, decorators: [{
521
814
  type: Injectable,
522
815
  args: [{
523
816
  providedIn: 'root',
524
817
  }]
525
818
  }] });
526
819
 
527
- class ApiLayer extends MapLayer {
528
- _layerSpecification;
529
- constructor(config, _layerSpecification) {
530
- const sourceId = 'source' in _layerSpecification ? _layerSpecification.source : '';
531
- super(config, sourceId);
532
- this._layerSpecification = _layerSpecification;
533
- // Ensure that layout visibility is set after the base constructor has been called
534
- if (this._layerSpecification.layout) {
535
- this._layerSpecification.layout.visibility = 'none';
536
- }
537
- else {
538
- this._layerSpecification.layout = { visibility: 'none' };
539
- }
540
- }
541
- get id() {
542
- return this._layerSpecification.id;
543
- }
544
- getSpecification() {
545
- return this._layerSpecification;
546
- }
547
- }
548
-
549
- class ApiBackgroundLayer extends ApiLayer {
550
- constructor(config, layerSpecification) {
551
- super(config, layerSpecification);
552
- }
553
- }
554
-
555
- class ApiSource extends MapSource {
556
- _styleSpecification;
557
- _layerFilter;
558
- constructor(id, config, _styleSpecification, _layerFilter) {
559
- super(id, config);
560
- this._styleSpecification = _styleSpecification;
561
- this._layerFilter = _layerFilter;
562
- }
563
- overrideonInit() {
564
- super.onInit();
565
- this.createLayers(this._styleSpecification.layers);
566
- }
567
- getSpecification() {
568
- return this._styleSpecification.sources[this.id];
569
- }
570
- createLayers(layers) {
571
- this.layers = layers
572
- .filter((layer) => layer.source === this.id)
573
- .filter((layer) => !this._layerFilter || this._layerFilter(layer))
574
- .map((layer) => {
575
- const apiLayer = new ApiLayer(this.config, layer);
576
- apiLayer.onInit();
577
- return apiLayer;
578
- });
579
- }
580
- }
581
-
582
- class ApiElement extends MapElement {
583
- _http;
584
- _mapStyleUrl;
585
- _layerFilter;
586
- backgroundLayers = [];
587
- #visible = false;
588
- constructor(config, _http, _mapStyleUrl, _layerFilter) {
589
- super(config);
590
- this._http = _http;
591
- this._mapStyleUrl = _mapStyleUrl;
592
- this._layerFilter = _layerFilter;
593
- }
594
- onInit() {
595
- super.onInit();
596
- this._http
597
- .get(this._mapStyleUrl)
598
- .pipe(filter((styleSpecification) => !!styleSpecification), takeUntil(this.unsubscribe))
599
- .subscribe((styleSpecification) => {
600
- this.#createBackgroundLayers(styleSpecification.layers);
601
- this.#createSources(styleSpecification);
602
- // If setVisible() was called before these layers were created,
603
- // re-apply the current visibility state so the new layers respect it.
604
- this.setVisible(this.#visible);
605
- });
606
- }
607
- setVisible(visible) {
608
- this.#visible = visible;
609
- super.setVisible(visible);
610
- this.backgroundLayers.forEach((layer) => layer.setVisible(visible));
611
- }
612
- #createSources(styleSpecification) {
613
- this.sources = Object.entries(styleSpecification.sources).map(([id]) => {
614
- const source = new ApiSource(id, this.config, styleSpecification, this._layerFilter);
615
- source.onInit();
616
- return source;
617
- });
618
- }
619
- // API elements are created from the style specification and this can contain layers of type 'background'
620
- // These layers do not have a source and therefore don't fit in the element > source > layer hierarchy
621
- // That is why we create these layers from the element itself
622
- #createBackgroundLayers(layerSpecifications) {
623
- this.backgroundLayers = layerSpecifications
624
- .filter((layer) => layer.type === 'background')
625
- .map((layerSpecification) => {
626
- const apiLayer = new ApiBackgroundLayer(this.config, layerSpecification);
627
- apiLayer.onInit();
628
- return apiLayer;
629
- });
630
- }
631
- }
632
-
633
820
  /**
634
821
  * Generated bundle index. Do not edit.
635
822
  */
636
823
 
637
- export { ApiBackgroundLayer, ApiElement, ApiLayer, ApiSource, 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 };
638
825
  //# sourceMappingURL=ndwnu-map.mjs.map