@ndwnu/map 0.0.1-beta.1 → 0.0.1-beta.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 CHANGED
@@ -1,7 +1,300 @@
1
- # map
1
+ # @ndwnu/map
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ A facade pattern library for MapLibre GL that simplifies the management of complex map sources and layers. Built by NDW (Nationaal Dataportaan Wegverkeer) for easier development with MapLibre.
4
4
 
5
- ## Running unit tests
5
+ ## Overview
6
6
 
7
- Run `nx test map` to execute the unit tests.
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
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @ndwnu/map maplibre-gl
19
+ ```
20
+
21
+ ### CSS Setup
22
+
23
+ Add MapLibre GL CSS to your `angular.json` styles array:
24
+
25
+ ```json
26
+ {
27
+ "styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css"]
28
+ }
29
+ ```
30
+
31
+ ## Development Process
32
+
33
+ ### 1. Create Map Elements
34
+
35
+ Create a folder structure for your map elements and define an enum for element identification:
36
+
37
+ ```typescript
38
+ // map-element.enum.ts
39
+ export enum MapElementEnum {
40
+ MyElement = 'my-element',
41
+ AnotherElement = 'another-element',
42
+ }
43
+ ```
44
+
45
+ ```
46
+ src/
47
+ map-elements/
48
+ map-element.enum.ts
49
+ my-element/
50
+ my-element.element.ts
51
+ my-element.source.ts
52
+ my-element.layer.ts
53
+ ```
54
+
55
+ Example MapElement:
56
+
57
+ ```typescript
58
+ import { MapElement, MapElementConfig, MapSource } from '@ndwnu/map';
59
+ import { MapElementEnum } from '../map-element.enum';
60
+
61
+ export class MyElement extends MapElement<MapElementEnum> {
62
+ constructor(config: MapElementConfig<MapElementEnum>) {
63
+ super(config);
64
+ this.sources = [new MyElementSource(config)];
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### 2. Create Element Repository
70
+
71
+ Extend the MapElementRepository to manage your elements:
72
+
73
+ ```typescript
74
+ import { Injectable } from '@angular/core';
75
+ import { MapElementRepository } from '@ndwnu/map';
76
+ import { Map } from 'maplibre-gl';
77
+ import { MapElementEnum } from './map-element.enum';
78
+
79
+ @Injectable({ providedIn: 'root' })
80
+ export class MyMapElementRepository extends MapElementRepository<MapElementEnum> {
81
+ registerMapElements(map: Map) {
82
+ const config = {
83
+ map,
84
+ mapElementRepository: this,
85
+ maplibreCursorService: this.maplibreCursorService,
86
+ };
87
+
88
+ [
89
+ new MyElement({ ...config, elementId: MapElementEnum.MyElement, elementOrder: 0 }),
90
+ // Add more elements...
91
+ ].forEach((element) => this.addMapElement(element));
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### 3. Create Map Component
97
+
98
+ Extend the base MapComponent and optionally provide configuration:
99
+
100
+ ```typescript
101
+ import { Component, inject } from '@angular/core';
102
+ import { MapComponent, MapConfig } from '@ndwnu/map';
103
+ import { MyMapElementRepository } from './map-elements/my-map-element.repository';
104
+
105
+ @Component({
106
+ selector: 'app-map',
107
+ template: `<div class="map-container"><!-- Map renders here --></div>`,
108
+ styleUrls: ['./map.component.scss'],
109
+ })
110
+ export class MyMapComponent extends MapComponent {
111
+ readonly #repository = inject(MyMapElementRepository);
112
+
113
+ protected onLoadMap() {
114
+ this.#repository.registerMapElements(this.map);
115
+ // Show initial elements
116
+ this.#repository.showMapElement(MapElementEnum.MyElement);
117
+ }
118
+
119
+ protected onRemoveMap() {
120
+ this.#repository.removeAllMapElements();
121
+ }
122
+
123
+ protected onIdle() {
124
+ // Handle map idle events
125
+ }
126
+
127
+ toggleElement(elementId: MapElementEnum) {
128
+ this.#repository.toggleMapElement(elementId);
129
+ }
130
+ }
131
+ ```
132
+
133
+ ### 4. Configure Map (Optional)
134
+
135
+ You can customize the map behavior by passing a configuration object:
136
+
137
+ ```typescript
138
+ // In your parent component template
139
+ @Component({
140
+ template: ` <app-map [config]="mapConfig"></app-map> `,
141
+ })
142
+ export class ParentComponent {
143
+ mapConfig: Partial<MapConfig> = {
144
+ maxZoom: 20,
145
+ minZoom: 8,
146
+ dragRotate: true,
147
+ center: [5.387827, 52.155172],
148
+ zoom: 12,
149
+ scrollZoom: false, // Disable scroll zoom
150
+ };
151
+ }
152
+ ```
153
+
154
+ Available configuration options:
155
+
156
+ - `center`: Initial map center position
157
+ - `zoom`: Initial zoom level
158
+ - `maxZoom`/`minZoom`: Zoom level constraints
159
+ - `bounds`: Initial bounds to fit (overrides center/zoom if provided)
160
+ - `interactive`: Enable/disable all interactions
161
+ - `dragRotate`: Enable/disable rotation via drag
162
+ - `doubleClickZoom`: Enable/disable double-click zoom
163
+ - `scrollZoom`: Enable/disable scroll wheel zoom
164
+ - `boxZoom`: Enable/disable shift+drag box zoom
165
+ - `dragPan`: Enable/disable drag to pan
166
+ - `keyboard`: Enable/disable keyboard navigation
167
+ - `touchZoomRotate`: Enable/disable touch gestures
168
+
169
+ **Note**: If `bounds` is provided, it will override `center` and `zoom` settings.
170
+
171
+ You can use predefined bounds:
172
+
173
+ ```typescript
174
+ import { COMMON_BOUNDS } from '@ndwnu/map';
175
+
176
+ mapConfig: Partial<MapConfig> = {
177
+ bounds: COMMON_BOUNDS.NETHERLANDS, // or COMMON_BOUNDS.AMERSFOORT
178
+ maxZoom: 20,
179
+ dragRotate: true,
180
+ };
181
+ ```
182
+
183
+ ### 5. Component Styling
184
+
185
+ **Important**: Set a height for your map component:
186
+
187
+ ```css
188
+ .map-container {
189
+ height: 500px; /* or 100vh for full viewport */
190
+ width: 100%;
191
+ }
192
+ ```
193
+
194
+ ### 6. Register Map Elements
195
+
196
+ In your `onLoadMap()` method, call `registerMapElements()` to initialize all map elements:
197
+
198
+ ```typescript
199
+ protected onLoadMap() {
200
+ this.#repository.registerMapElements(this.map);
201
+ // Set initial visibility
202
+ this.#repository.showMapElement(MapElementEnum.MyElement);
203
+ }
204
+ ```
205
+
206
+ ## Filtering in MapLibre
207
+
208
+ 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:
209
+
210
+ 1. **Provide filter observables** to your MapElement
211
+ 2. **Pass filters to sources** during initialization
212
+ 3. **Apply filters to layers** within each source's layer definitions
213
+
214
+ Example implementation:
215
+
216
+ ```typescript
217
+ // In your MapElement
218
+ export class MyElement extends MapElement<MapElementEnum> {
219
+ constructor(
220
+ config: MapElementConfig<MapElementEnum>,
221
+ private filters$: Observable<FilterObject>,
222
+ ) {
223
+ super(config);
224
+ this.sources = [new MyElementSource(config, filters$)];
225
+ }
226
+ }
227
+
228
+ // In your MapSource
229
+ export class MyElementSource extends MapSource<MapElementEnum> {
230
+ constructor(
231
+ config: MapElementConfig<MapElementEnum>,
232
+ private filters$: Observable<FilterObject>,
233
+ ) {
234
+ super(config);
235
+ // Subscribe to filter changes and update layers
236
+ this.filters$.subscribe((filters) => this.updateLayerFilters(filters));
237
+ }
238
+
239
+ private updateLayerFilters(filters: FilterObject) {
240
+ // Apply filters to each layer in this source
241
+ this.layers.forEach((layer) => {
242
+ layer.applyFilter(filters);
243
+ });
244
+ }
245
+ }
246
+ ```
247
+
248
+ **Note**: Filter management may be included in future versions of this library to provide a more streamlined filtering experience.
249
+
250
+ ## Example Usage
251
+
252
+ See the [playground application](../../apps/playground/src/app/pages/map) for a complete implementation example.
253
+
254
+ ## API
255
+
256
+ ### MapComponent (Abstract)
257
+
258
+ Base component that provides MapLibre integration.
259
+
260
+ **Methods:**
261
+
262
+ - `resizeMap()`: Resize the map to fit container
263
+ - `zoomToLevel(level: number, options?)`: Zoom to specific level
264
+
265
+ **Abstract Methods:**
266
+
267
+ - `onLoadMap()`: Called when map is loaded
268
+ - `onRemoveMap()`: Called before map destruction
269
+ - `onIdle()`: Called when map becomes idle
270
+
271
+ ### MapElementRepository<T> (Abstract)
272
+
273
+ Manages collection of map elements.
274
+
275
+ **Methods:**
276
+
277
+ - `addMapElement(element)`: Add element to repository
278
+ - `removeMapElement(element)`: Remove and destroy element
279
+ - `showMapElement(id)`: Make element visible
280
+ - `hideMapElement(id)`: Hide element
281
+ - `toggleMapElement(id)`: Toggle element visibility
282
+
283
+ ### MapElement<T> (Abstract)
284
+
285
+ Container for related sources and layers.
286
+
287
+ **Properties:**
288
+
289
+ - `id`: Unique identifier
290
+ - `elementOrder`: Display order
291
+ - `sources`: Array of MapSource instances
292
+ - `isVisible`: Current visibility state
293
+
294
+ ## License
295
+
296
+ MIT
297
+
298
+ ## About NDW
299
+
300
+ 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,6 +1,6 @@
1
1
  import { BehaviorSubject, map, Subject, takeUntil } from 'rxjs';
2
2
  import * as i0 from '@angular/core';
3
- import { viewChild, Component, ChangeDetectionStrategy, Injectable } from '@angular/core';
3
+ import { viewChild, input, ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
4
4
  import { Map } from 'maplibre-gl';
5
5
 
6
6
  const BOUNDS_NL = [
@@ -12,6 +12,32 @@ const BOUNDS_AMERSFOORT = [
12
12
  [5.446205917577942, 52.21132028216525],
13
13
  ];
14
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
+
15
41
  /**
16
42
  * Repository for managing map elements.
17
43
  * Provides methods to add, remove, show, hide, and toggle map elements.
@@ -348,6 +374,7 @@ class MapSource {
348
374
 
349
375
  class MapComponent {
350
376
  mapContainer = viewChild.required('mapContainer');
377
+ config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
351
378
  map;
352
379
  ngAfterViewInit() {
353
380
  this.map = this.#createMap(this.mapContainer().nativeElement);
@@ -368,33 +395,35 @@ class MapComponent {
368
395
  this.map?.zoomTo(zoomLevel, options);
369
396
  }
370
397
  #createMap(container) {
371
- const map = new Map({
398
+ const config = { ...DEFAULT_MAP_CONFIG, ...this.config() };
399
+ const options = {
372
400
  container,
373
401
  style: {
374
402
  version: 8,
375
403
  sources: {},
376
404
  layers: [],
377
405
  },
378
- interactive: true,
379
- maxZoom: 18,
380
- minZoom: 6,
381
- bounds: [
382
- [5.34458238242172, 52.11623605695118],
383
- [5.446205917577942, 52.21132028216525],
384
- ],
385
- });
386
- // Prevent map rotation
387
- map.dragRotate.disable();
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);
388
417
  return map;
389
418
  }
390
419
  #initiateMapLoading() {
391
420
  this.onLoadMap();
392
421
  this.resizeMap();
393
422
  }
394
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: MapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
395
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.0.7", type: MapComponent, isStandalone: true, selector: "ng-component", 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 });
423
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: MapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
424
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.2.4", 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 });
396
425
  }
397
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: MapComponent, decorators: [{
426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: MapComponent, decorators: [{
398
427
  type: Component,
399
428
  args: [{
400
429
  standalone: true,
@@ -485,10 +514,10 @@ class MaplibreCursorService {
485
514
  console.warn(`Cursor type '${cursor}' is not supported.`);
486
515
  }
487
516
  }
488
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: MaplibreCursorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
489
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: MaplibreCursorService, providedIn: 'root' });
517
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: MaplibreCursorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
518
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: MaplibreCursorService, providedIn: 'root' });
490
519
  }
491
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: MaplibreCursorService, decorators: [{
520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: MaplibreCursorService, decorators: [{
492
521
  type: Injectable,
493
522
  args: [{
494
523
  providedIn: 'root',
@@ -499,5 +528,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImpor
499
528
  * Generated bundle index. Do not edit.
500
529
  */
501
530
 
502
- export { BOUNDS_AMERSFOORT, BOUNDS_NL, MapComponent, MapElement, MapElementRepository, MapLayer, MapSource, MaplibreCursorService };
531
+ export { BOUNDS_AMERSFOORT, BOUNDS_NL, COMMON_BOUNDS, DEFAULT_MAP_CONFIG, MapComponent, MapElement, MapElementRepository, MapLayer, MapSource, MaplibreCursorService };
503
532
  //# sourceMappingURL=ndwnu-map.mjs.map