@ndwnu/map 3.0.1 → 3.0.3
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 +372 -372
- package/fesm2022/ndwnu-map.mjs +64 -22
- package/fesm2022/ndwnu-map.mjs.map +1 -1
- package/package.json +1 -1
- package/src/assets/sprites/custom_ndw.json +3628 -3628
- package/src/assets/sprites/custom_ndw@2x.json +3628 -3628
- package/src/assets/sprites/sdf.json +834 -834
- package/src/assets/sprites/sdf@2x.json +834 -834
- package/src/lib/style/style.json +3971 -3969
- package/types/ndwnu-map.d.ts +59 -0
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.
|