@ndwnu/map 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,372 +1,372 @@
1
- # @ndwnu/map
2
-
3
- A facade pattern library for MapLibre GL that simplifies the management of complex map sources and layers. Built by NDW (Nationaal Dataportaal Wegverkeer) for easier development with MapLibre. It also contains the style json file that contains the NDW Basemap styling.
4
-
5
- ## Overview
6
-
7
- This library provides a structured approach to managing MapLibre GL maps by introducing the **MapElement** pattern - a container that groups related sources and layers as a single logical unit. MapElements use a generic type (typically an enum) for identification. Instead of managing individual MapLibre sources and layers, you work with MapElements that can be toggled on/off from the UI perspective while handling multiple underlying sources and layers automatically.
8
-
9
- ## Key Features
10
-
11
- - **MapElement Pattern**: Groups multiple sources and layers into logical units
12
- - **Visibility Management**: Easy show/hide functionality for complex map elements
13
- - **Repository Pattern**: Centralized management of all map elements
14
- - **NDW Basemap style**: A json file with the NDW basemap
15
-
16
- ## Installation of @ndwnu/map
17
-
18
- ```bash
19
- npm install @ndwnu/map maplibre-gl
20
- ```
21
-
22
- ### CSS Setup
23
-
24
- Add MapLibre GL CSS to your `angular.json` styles array:
25
-
26
- ```json
27
- {
28
- "styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css"]
29
- }
30
- ```
31
-
32
- ## Development Process
33
-
34
- ### 1. Create Map Elements
35
-
36
- Create a folder structure for your map elements and define an enum for element identification:
37
-
38
- ```typescript
39
- // map-element.enum.ts
40
- export enum MapElementEnum {
41
- MyElement = 'my-element',
42
- AnotherElement = 'another-element',
43
- }
44
- ```
45
-
46
- ```
47
- src/
48
- map-elements/
49
- map-element.enum.ts
50
- my-element/
51
- my-element.element.ts
52
- my-element.source.ts
53
- my-element.layer.ts
54
- ```
55
-
56
- Example MapElement:
57
-
58
- ```typescript
59
- import { MapElement, MapElementConfig, MapSource } from '@ndwnu/map';
60
- import { MapElementEnum } from '../map-element.enum';
61
-
62
- export class MyElement extends MapElement<MapElementEnum> {
63
- constructor(config: MapElementConfig<MapElementEnum>) {
64
- super(config);
65
- this.sources = [new MyElementSource(config)];
66
- }
67
- }
68
- ```
69
-
70
- ### 2. Create Element Repository
71
-
72
- Extend the MapElementRepository to manage your elements:
73
-
74
- ```typescript
75
- import { Injectable } from '@angular/core';
76
- import { MapElementRepository } from '@ndwnu/map';
77
- import { Map } from 'maplibre-gl';
78
- import { MapElementEnum } from './map-element.enum';
79
-
80
- @Injectable({ providedIn: 'root' })
81
- export class MyMapElementRepository extends MapElementRepository<MapElementEnum> {
82
- registerMapElements(map: Map) {
83
- const config = {
84
- map,
85
- mapElementRepository: this,
86
- maplibreCursorService: this.maplibreCursorService,
87
- };
88
-
89
- [
90
- new MyElement({ ...config, elementId: MapElementEnum.MyElement, elementOrder: 0 }),
91
- // Add more elements...
92
- ].forEach((element) => this.addMapElement(element));
93
- }
94
- }
95
- ```
96
-
97
- ### 3. Create Map Component
98
-
99
- Extend the base MapComponent and optionally provide configuration:
100
-
101
- ```typescript
102
- import { Component, inject } from '@angular/core';
103
- import { MapComponent, MapConfig } from '@ndwnu/map';
104
- import { MyMapElementRepository } from './map-elements/my-map-element.repository';
105
-
106
- @Component({
107
- selector: 'app-map',
108
- template: `<div class="map-container"><!-- Map renders here --></div>`,
109
- styleUrls: ['./map.component.scss'],
110
- })
111
- export class MyMapComponent extends MapComponent {
112
- readonly #repository = inject(MyMapElementRepository);
113
-
114
- protected onLoadMap() {
115
- this.#repository.registerMapElements(this.map);
116
- // Show initial elements
117
- this.#repository.showMapElement(MapElementEnum.MyElement);
118
- }
119
-
120
- protected onRemoveMap() {
121
- this.#repository.removeAllMapElements();
122
- }
123
-
124
- protected onIdle() {
125
- // Handle map idle events
126
- }
127
-
128
- toggleElement(elementId: MapElementEnum) {
129
- this.#repository.toggleMapElement(elementId);
130
- }
131
- }
132
- ```
133
-
134
- ### 4. Configure Map (Optional)
135
-
136
- You can customize the map behavior by passing a configuration object:
137
-
138
- ```typescript
139
- // In your parent component template
140
- @Component({
141
- template: ` <app-map [config]="mapConfig"></app-map> `,
142
- })
143
- export class ParentComponent {
144
- mapConfig: Partial<MapConfig> = {
145
- maxZoom: 20,
146
- minZoom: 8,
147
- dragRotate: true,
148
- center: [5.387827, 52.155172],
149
- zoom: 12,
150
- scrollZoom: false, // Disable scroll zoom
151
- };
152
- }
153
- ```
154
-
155
- Available configuration options:
156
-
157
- - `center`: Initial map center position
158
- - `zoom`: Initial zoom level
159
- - `maxZoom`/`minZoom`: Zoom level constraints
160
- - `bounds`: Initial bounds to fit (overrides center/zoom if provided)
161
- - `interactive`: Enable/disable all interactions
162
- - `dragRotate`: Enable/disable rotation via drag
163
- - `doubleClickZoom`: Enable/disable double-click zoom
164
- - `scrollZoom`: Enable/disable scroll wheel zoom
165
- - `boxZoom`: Enable/disable shift+drag box zoom
166
- - `dragPan`: Enable/disable drag to pan
167
- - `keyboard`: Enable/disable keyboard navigation
168
- - `touchZoomRotate`: Enable/disable touch gestures
169
-
170
- **Note**: If `bounds` is provided, it will override `center` and `zoom` settings.
171
-
172
- You can use predefined bounds:
173
-
174
- ```typescript
175
- import { COMMON_BOUNDS } from '@ndwnu/map';
176
-
177
- mapConfig: Partial<MapConfig> = {
178
- bounds: COMMON_BOUNDS.NETHERLANDS, // or COMMON_BOUNDS.AMERSFOORT
179
- maxZoom: 20,
180
- dragRotate: true,
181
- };
182
- ```
183
-
184
- ### 5. Component Styling
185
-
186
- **Important**: Set a height for your map component:
187
-
188
- ```css
189
- .map-container {
190
- height: 500px; /* or 100vh for full viewport */
191
- width: 100%;
192
- }
193
- ```
194
-
195
- ### 6. Register Map Elements
196
-
197
- In your `onLoadMap()` method, call `registerMapElements()` to initialize all map elements:
198
-
199
- ```typescript
200
- protected onLoadMap() {
201
- this.#repository.registerMapElements(this.map);
202
- // Set initial visibility
203
- this.#repository.showMapElement(MapElementEnum.MyElement);
204
- }
205
- ```
206
-
207
- ## Filtering in MapLibre
208
-
209
- When working with MapLibre, filters need to be applied to each individual layer, while the filter 'shape' (structure) is tied to the source data. To implement filtering with the current architecture:
210
-
211
- 1. **Provide filter observables** to your MapElement
212
- 2. **Pass filters to sources** during initialization
213
- 3. **Apply filters to layers** within each source's layer definitions
214
-
215
- Example implementation:
216
-
217
- ```typescript
218
- // In your MapElement
219
- export class MyElement extends MapElement<MapElementEnum> {
220
- constructor(
221
- config: MapElementConfig<MapElementEnum>,
222
- private filters$: Observable<FilterObject>,
223
- ) {
224
- super(config);
225
- this.sources = [new MyElementSource(config, filters$)];
226
- }
227
- }
228
-
229
- // In your MapSource
230
- export class MyElementSource extends MapSource<MapElementEnum> {
231
- constructor(
232
- config: MapElementConfig<MapElementEnum>,
233
- private filters$: Observable<FilterObject>,
234
- ) {
235
- super(config);
236
- // Subscribe to filter changes and update layers
237
- this.filters$.subscribe((filters) => this.updateLayerFilters(filters));
238
- }
239
-
240
- private updateLayerFilters(filters: FilterObject) {
241
- // Apply filters to each layer in this source
242
- this.layers.forEach((layer) => {
243
- layer.applyFilter(filters);
244
- });
245
- }
246
- }
247
- ```
248
-
249
- **Note**: Filter management may be included in future versions of this library to provide a more streamlined filtering experience.
250
-
251
- ## Example Usage
252
-
253
- See the [playground application](../../apps/playground/src/app/pages/map) for a complete implementation example.
254
-
255
- ## API
256
-
257
- ### MapComponent (Abstract)
258
-
259
- Base component that provides MapLibre integration.
260
-
261
- **Methods:**
262
-
263
- - `resizeMap()`: Resize the map to fit container
264
- - `zoomToLevel(level: number, options?)`: Zoom to specific level
265
-
266
- **Abstract Methods:**
267
-
268
- - `onLoadMap()`: Called when map is loaded
269
- - `onRemoveMap()`: Called before map destruction
270
- - `onIdle()`: Called when map becomes idle
271
-
272
- ### MapElementRepository<T> (Abstract)
273
-
274
- Manages collection of map elements.
275
-
276
- **Methods:**
277
-
278
- - `addMapElement(element)`: Add element to repository
279
- - `removeMapElement(element)`: Remove and destroy element
280
- - `showMapElement(id)`: Make element visible
281
- - `hideMapElement(id)`: Hide element
282
- - `toggleMapElement(id)`: Toggle element visibility
283
-
284
- ### MapElement<T> (Abstract)
285
-
286
- Container for related sources and layers.
287
-
288
- **Properties:**
289
-
290
- - `id`: Unique identifier
291
- - `elementOrder`: Display order
292
- - `sources`: Array of MapSource instances
293
- - `isVisible`: Current visibility state
294
-
295
- ## NDW Basemap style
296
-
297
- The NDW modular basemap is a single MapLibre style file that lets developers compose the right map for their application by toggling grouped layers on and off. The core idea: separate **context** (the quiet reference map you pan and zoom across) from **relevant** (the bold, thematic overlay that tells the story).
298
-
299
- ### Context / relevant
300
-
301
- - **Context layers** are unobtrusive — light, desaturated colors meant to support, not distract. Basemap, roads, admin boundaries, labels.
302
- - **Relevant layers** are saturated and bold. They carry the data the user actually came for: accessibility, road safety, parking, your own datasets.
303
-
304
- ### Composition
305
-
306
- Each layer carries a `metadata` block — group, sub-group, type, legend name, description. Metadata doesn't affect rendering, but it lets the app reason about layers at runtime:
307
-
308
- ```json
309
- "metadata": {
310
- "group": "context-roads",
311
- "sub-group": "WKD-parking-areas",
312
- "type": "context",
313
- "legendName": "Parking areas",
314
- "desc": ""
315
- }
316
- ```
317
-
318
- Example of how to use groups / subgroups to display / hide a subset of layers with the @ndwnu/map MapElement structure:
319
-
320
- ```ts
321
- export class NdwElement extends ApiElement<MapElementEnum, MapFilter, GrgLegendItem> {
322
- constructor(config: GrgMapElementConfig, http: HttpClient) {
323
- const layerFilter: NdwLayerFilterFunction = (layer) => {
324
- return (
325
- layer.metadata.group === 'context-map' ||
326
- (layer.metadata.group === 'context-roads' &&
327
- layer.metadata['sub-group'] !== 'NWB-hectometersigns' &&
328
- layer.metadata.group === 'context-roads' &&
329
- layer.metadata['sub-group'] !== 'WKD-parking-areas' &&
330
- layer.metadata.group === 'context-roads' &&
331
- layer.metadata['sub-group'] !== 'FCD-segments')
332
- );
333
- };
334
- super(
335
- config,
336
- http,
337
- environment.mapStyles.ndw,
338
- layerFilter as (layer: LayerSpecification) => boolean,
339
- );
340
- }
341
- }
342
- ```
343
-
344
- When you don't use the MapElement structure, toggle a whole group with plain JavaScript:
345
-
346
- ```ts
347
- function toggleGroupLayers(group: string) {
348
- const style = map.getStyle();
349
- style.layers.forEach((layer) => {
350
- if (layer.metadata?.group === group) {
351
- const visibility = map.getLayoutProperty(layer.id, 'visibility') || 'visible';
352
- map.setLayoutProperty(layer.id, 'visibility', visibility === 'none' ? 'visible' : 'none');
353
- }
354
- });
355
- }
356
- ```
357
-
358
- ### Include in your project
359
-
360
- You can either import the style file via `import` and provide it to MapLibre, or you can use the url on maps.ndw.nu:
361
-
362
- https://maps.ndw.nu/styles/ndw-basemap/dev/style.json
363
-
364
- TODO when style.json hosting task is done by NLS update URL here. [NLS story](https://dev.azure.com/ndwnu/NLS/_workitems/edit/115149)
365
-
366
- ## License
367
-
368
- MIT
369
-
370
- ## About NDW
371
-
372
- NDW (Nationaal Dataportaan Wegverkeer) - Data from and about road traffic are our core business. We collect, monitor quality, enrich data, store it and make it available.
1
+ # @ndwnu/map
2
+
3
+ A facade pattern library for MapLibre GL that simplifies the management of complex map sources and layers. Built by NDW (Nationaal Dataportaal Wegverkeer) for easier development with MapLibre. It also contains the style json file that contains the NDW Basemap styling.
4
+
5
+ ## Overview
6
+
7
+ This library provides a structured approach to managing MapLibre GL maps by introducing the **MapElement** pattern - a container that groups related sources and layers as a single logical unit. MapElements use a generic type (typically an enum) for identification. Instead of managing individual MapLibre sources and layers, you work with MapElements that can be toggled on/off from the UI perspective while handling multiple underlying sources and layers automatically.
8
+
9
+ ## Key Features
10
+
11
+ - **MapElement Pattern**: Groups multiple sources and layers into logical units
12
+ - **Visibility Management**: Easy show/hide functionality for complex map elements
13
+ - **Repository Pattern**: Centralized management of all map elements
14
+ - **NDW Basemap style**: A json file with the NDW basemap
15
+
16
+ ## Installation of @ndwnu/map
17
+
18
+ ```bash
19
+ npm install @ndwnu/map maplibre-gl
20
+ ```
21
+
22
+ ### CSS Setup
23
+
24
+ Add MapLibre GL CSS to your `angular.json` styles array:
25
+
26
+ ```json
27
+ {
28
+ "styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css"]
29
+ }
30
+ ```
31
+
32
+ ## Development Process
33
+
34
+ ### 1. Create Map Elements
35
+
36
+ Create a folder structure for your map elements and define an enum for element identification:
37
+
38
+ ```typescript
39
+ // map-element.enum.ts
40
+ export enum MapElementEnum {
41
+ MyElement = 'my-element',
42
+ AnotherElement = 'another-element',
43
+ }
44
+ ```
45
+
46
+ ```
47
+ src/
48
+ map-elements/
49
+ map-element.enum.ts
50
+ my-element/
51
+ my-element.element.ts
52
+ my-element.source.ts
53
+ my-element.layer.ts
54
+ ```
55
+
56
+ Example MapElement:
57
+
58
+ ```typescript
59
+ import { MapElement, MapElementConfig, MapSource } from '@ndwnu/map';
60
+ import { MapElementEnum } from '../map-element.enum';
61
+
62
+ export class MyElement extends MapElement<MapElementEnum> {
63
+ constructor(config: MapElementConfig<MapElementEnum>) {
64
+ super(config);
65
+ this.sources = [new MyElementSource(config)];
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### 2. Create Element Repository
71
+
72
+ Extend the MapElementRepository to manage your elements:
73
+
74
+ ```typescript
75
+ import { Injectable } from '@angular/core';
76
+ import { MapElementRepository } from '@ndwnu/map';
77
+ import { Map } from 'maplibre-gl';
78
+ import { MapElementEnum } from './map-element.enum';
79
+
80
+ @Injectable({ providedIn: 'root' })
81
+ export class MyMapElementRepository extends MapElementRepository<MapElementEnum> {
82
+ registerMapElements(map: Map) {
83
+ const config = {
84
+ map,
85
+ mapElementRepository: this,
86
+ maplibreCursorService: this.maplibreCursorService,
87
+ };
88
+
89
+ [
90
+ new MyElement({ ...config, elementId: MapElementEnum.MyElement, elementOrder: 0 }),
91
+ // Add more elements...
92
+ ].forEach((element) => this.addMapElement(element));
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### 3. Create Map Component
98
+
99
+ Extend the base MapComponent and optionally provide configuration:
100
+
101
+ ```typescript
102
+ import { Component, inject } from '@angular/core';
103
+ import { MapComponent, MapConfig } from '@ndwnu/map';
104
+ import { MyMapElementRepository } from './map-elements/my-map-element.repository';
105
+
106
+ @Component({
107
+ selector: 'app-map',
108
+ template: `<div class="map-container"><!-- Map renders here --></div>`,
109
+ styleUrls: ['./map.component.scss'],
110
+ })
111
+ export class MyMapComponent extends MapComponent {
112
+ readonly #repository = inject(MyMapElementRepository);
113
+
114
+ protected onLoadMap() {
115
+ this.#repository.registerMapElements(this.map);
116
+ // Show initial elements
117
+ this.#repository.showMapElement(MapElementEnum.MyElement);
118
+ }
119
+
120
+ protected onRemoveMap() {
121
+ this.#repository.removeAllMapElements();
122
+ }
123
+
124
+ protected onIdle() {
125
+ // Handle map idle events
126
+ }
127
+
128
+ toggleElement(elementId: MapElementEnum) {
129
+ this.#repository.toggleMapElement(elementId);
130
+ }
131
+ }
132
+ ```
133
+
134
+ ### 4. Configure Map (Optional)
135
+
136
+ You can customize the map behavior by passing a configuration object:
137
+
138
+ ```typescript
139
+ // In your parent component template
140
+ @Component({
141
+ template: ` <app-map [config]="mapConfig"></app-map> `,
142
+ })
143
+ export class ParentComponent {
144
+ mapConfig: Partial<MapConfig> = {
145
+ maxZoom: 20,
146
+ minZoom: 8,
147
+ dragRotate: true,
148
+ center: [5.387827, 52.155172],
149
+ zoom: 12,
150
+ scrollZoom: false, // Disable scroll zoom
151
+ };
152
+ }
153
+ ```
154
+
155
+ Available configuration options:
156
+
157
+ - `center`: Initial map center position
158
+ - `zoom`: Initial zoom level
159
+ - `maxZoom`/`minZoom`: Zoom level constraints
160
+ - `bounds`: Initial bounds to fit (overrides center/zoom if provided)
161
+ - `interactive`: Enable/disable all interactions
162
+ - `dragRotate`: Enable/disable rotation via drag
163
+ - `doubleClickZoom`: Enable/disable double-click zoom
164
+ - `scrollZoom`: Enable/disable scroll wheel zoom
165
+ - `boxZoom`: Enable/disable shift+drag box zoom
166
+ - `dragPan`: Enable/disable drag to pan
167
+ - `keyboard`: Enable/disable keyboard navigation
168
+ - `touchZoomRotate`: Enable/disable touch gestures
169
+
170
+ **Note**: If `bounds` is provided, it will override `center` and `zoom` settings.
171
+
172
+ You can use predefined bounds:
173
+
174
+ ```typescript
175
+ import { COMMON_BOUNDS } from '@ndwnu/map';
176
+
177
+ mapConfig: Partial<MapConfig> = {
178
+ bounds: COMMON_BOUNDS.NETHERLANDS, // or COMMON_BOUNDS.AMERSFOORT
179
+ maxZoom: 20,
180
+ dragRotate: true,
181
+ };
182
+ ```
183
+
184
+ ### 5. Component Styling
185
+
186
+ **Important**: Set a height for your map component:
187
+
188
+ ```css
189
+ .map-container {
190
+ height: 500px; /* or 100vh for full viewport */
191
+ width: 100%;
192
+ }
193
+ ```
194
+
195
+ ### 6. Register Map Elements
196
+
197
+ In your `onLoadMap()` method, call `registerMapElements()` to initialize all map elements:
198
+
199
+ ```typescript
200
+ protected onLoadMap() {
201
+ this.#repository.registerMapElements(this.map);
202
+ // Set initial visibility
203
+ this.#repository.showMapElement(MapElementEnum.MyElement);
204
+ }
205
+ ```
206
+
207
+ ## Filtering in MapLibre
208
+
209
+ When working with MapLibre, filters need to be applied to each individual layer, while the filter 'shape' (structure) is tied to the source data. To implement filtering with the current architecture:
210
+
211
+ 1. **Provide filter observables** to your MapElement
212
+ 2. **Pass filters to sources** during initialization
213
+ 3. **Apply filters to layers** within each source's layer definitions
214
+
215
+ Example implementation:
216
+
217
+ ```typescript
218
+ // In your MapElement
219
+ export class MyElement extends MapElement<MapElementEnum> {
220
+ constructor(
221
+ config: MapElementConfig<MapElementEnum>,
222
+ private filters$: Observable<FilterObject>,
223
+ ) {
224
+ super(config);
225
+ this.sources = [new MyElementSource(config, filters$)];
226
+ }
227
+ }
228
+
229
+ // In your MapSource
230
+ export class MyElementSource extends MapSource<MapElementEnum> {
231
+ constructor(
232
+ config: MapElementConfig<MapElementEnum>,
233
+ private filters$: Observable<FilterObject>,
234
+ ) {
235
+ super(config);
236
+ // Subscribe to filter changes and update layers
237
+ this.filters$.subscribe((filters) => this.updateLayerFilters(filters));
238
+ }
239
+
240
+ private updateLayerFilters(filters: FilterObject) {
241
+ // Apply filters to each layer in this source
242
+ this.layers.forEach((layer) => {
243
+ layer.applyFilter(filters);
244
+ });
245
+ }
246
+ }
247
+ ```
248
+
249
+ **Note**: Filter management may be included in future versions of this library to provide a more streamlined filtering experience.
250
+
251
+ ## Example Usage
252
+
253
+ See the [playground application](../../apps/playground/src/app/pages/map) for a complete implementation example.
254
+
255
+ ## API
256
+
257
+ ### MapComponent (Abstract)
258
+
259
+ Base component that provides MapLibre integration.
260
+
261
+ **Methods:**
262
+
263
+ - `resizeMap()`: Resize the map to fit container
264
+ - `zoomToLevel(level: number, options?)`: Zoom to specific level
265
+
266
+ **Abstract Methods:**
267
+
268
+ - `onLoadMap()`: Called when map is loaded
269
+ - `onRemoveMap()`: Called before map destruction
270
+ - `onIdle()`: Called when map becomes idle
271
+
272
+ ### MapElementRepository<T> (Abstract)
273
+
274
+ Manages collection of map elements.
275
+
276
+ **Methods:**
277
+
278
+ - `addMapElement(element)`: Add element to repository
279
+ - `removeMapElement(element)`: Remove and destroy element
280
+ - `showMapElement(id)`: Make element visible
281
+ - `hideMapElement(id)`: Hide element
282
+ - `toggleMapElement(id)`: Toggle element visibility
283
+
284
+ ### MapElement<T> (Abstract)
285
+
286
+ Container for related sources and layers.
287
+
288
+ **Properties:**
289
+
290
+ - `id`: Unique identifier
291
+ - `elementOrder`: Display order
292
+ - `sources`: Array of MapSource instances
293
+ - `isVisible`: Current visibility state
294
+
295
+ ## NDW Basemap style
296
+
297
+ The NDW modular basemap is a single MapLibre style file that lets developers compose the right map for their application by toggling grouped layers on and off. The core idea: separate **context** (the quiet reference map you pan and zoom across) from **relevant** (the bold, thematic overlay that tells the story).
298
+
299
+ ### Context / relevant
300
+
301
+ - **Context layers** are unobtrusive — light, desaturated colors meant to support, not distract. Basemap, roads, admin boundaries, labels.
302
+ - **Relevant layers** are saturated and bold. They carry the data the user actually came for: accessibility, road safety, parking, your own datasets.
303
+
304
+ ### Composition
305
+
306
+ Each layer carries a `metadata` block — group, sub-group, type, legend name, description. Metadata doesn't affect rendering, but it lets the app reason about layers at runtime:
307
+
308
+ ```json
309
+ "metadata": {
310
+ "group": "context-roads",
311
+ "sub-group": "WKD-parking-areas",
312
+ "type": "context",
313
+ "legendName": "Parking areas",
314
+ "desc": ""
315
+ }
316
+ ```
317
+
318
+ Example of how to use groups / subgroups to display / hide a subset of layers with the @ndwnu/map MapElement structure:
319
+
320
+ ```ts
321
+ export class NdwElement extends ApiElement<MapElementEnum, MapFilter, GrgLegendItem> {
322
+ constructor(config: GrgMapElementConfig, http: HttpClient) {
323
+ const layerFilter: NdwLayerFilterFunction = (layer) => {
324
+ return (
325
+ layer.metadata.group === 'context-map' ||
326
+ (layer.metadata.group === 'context-roads' &&
327
+ layer.metadata['sub-group'] !== 'NWB-hectometersigns' &&
328
+ layer.metadata.group === 'context-roads' &&
329
+ layer.metadata['sub-group'] !== 'WKD-parking-areas' &&
330
+ layer.metadata.group === 'context-roads' &&
331
+ layer.metadata['sub-group'] !== 'FCD-segments')
332
+ );
333
+ };
334
+ super(
335
+ config,
336
+ http,
337
+ environment.mapStyles.ndw,
338
+ layerFilter as (layer: LayerSpecification) => boolean,
339
+ );
340
+ }
341
+ }
342
+ ```
343
+
344
+ When you don't use the MapElement structure, toggle a whole group with plain JavaScript:
345
+
346
+ ```ts
347
+ function toggleGroupLayers(group: string) {
348
+ const style = map.getStyle();
349
+ style.layers.forEach((layer) => {
350
+ if (layer.metadata?.group === group) {
351
+ const visibility = map.getLayoutProperty(layer.id, 'visibility') || 'visible';
352
+ map.setLayoutProperty(layer.id, 'visibility', visibility === 'none' ? 'visible' : 'none');
353
+ }
354
+ });
355
+ }
356
+ ```
357
+
358
+ ### Include in your project
359
+
360
+ You can either import the style file via `import` and provide it to MapLibre, or you can use the url on maps.ndw.nu:
361
+
362
+ https://maps.ndw.nu/styles/ndw-basemap/dev/style.json
363
+
364
+ TODO when style.json hosting task is done by NLS update URL here. [NLS story](https://dev.azure.com/ndwnu/NLS/_workitems/edit/115149)
365
+
366
+ ## License
367
+
368
+ MIT
369
+
370
+ ## About NDW
371
+
372
+ NDW (Nationaal Dataportaan Wegverkeer) - Data from and about road traffic are our core business. We collect, monitor quality, enrich data, store it and make it available.