@ndwnu/map
v0.0.1-beta.3
Published
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.
Keywords
Readme
@ndwnu/map
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.
Overview
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.
Key Features
- MapElement Pattern: Groups multiple sources and layers into logical units
- Visibility Management: Easy show/hide functionality for complex map elements
- Repository Pattern: Centralized management of all map elements
Installation
npm install @ndwnu/map maplibre-glCSS Setup
Add MapLibre GL CSS to your angular.json styles array:
{
"styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css"]
}Development Process
1. Create Map Elements
Create a folder structure for your map elements and define an enum for element identification:
// map-element.enum.ts
export enum MapElementEnum {
MyElement = 'my-element',
AnotherElement = 'another-element',
}src/
map-elements/
map-element.enum.ts
my-element/
my-element.element.ts
my-element.source.ts
my-element.layer.tsExample MapElement:
import { MapElement, MapElementConfig, MapSource } from '@ndwnu/map';
import { MapElementEnum } from '../map-element.enum';
export class MyElement extends MapElement<MapElementEnum> {
constructor(config: MapElementConfig<MapElementEnum>) {
super(config);
this.sources = [new MyElementSource(config)];
}
}2. Create Element Repository
Extend the MapElementRepository to manage your elements:
import { Injectable } from '@angular/core';
import { MapElementRepository } from '@ndwnu/map';
import { Map } from 'maplibre-gl';
import { MapElementEnum } from './map-element.enum';
@Injectable({ providedIn: 'root' })
export class MyMapElementRepository extends MapElementRepository<MapElementEnum> {
registerMapElements(map: Map) {
const config = {
map,
mapElementRepository: this,
maplibreCursorService: this.maplibreCursorService,
};
[
new MyElement({ ...config, elementId: MapElementEnum.MyElement, elementOrder: 0 }),
// Add more elements...
].forEach((element) => this.addMapElement(element));
}
}3. Create Map Component
Extend the base MapComponent and optionally provide configuration:
import { Component, inject } from '@angular/core';
import { MapComponent, MapConfig } from '@ndwnu/map';
import { MyMapElementRepository } from './map-elements/my-map-element.repository';
@Component({
selector: 'app-map',
template: `<div class="map-container"><!-- Map renders here --></div>`,
styleUrls: ['./map.component.scss'],
})
export class MyMapComponent extends MapComponent {
readonly #repository = inject(MyMapElementRepository);
protected onLoadMap() {
this.#repository.registerMapElements(this.map);
// Show initial elements
this.#repository.showMapElement(MapElementEnum.MyElement);
}
protected onRemoveMap() {
this.#repository.removeAllMapElements();
}
protected onIdle() {
// Handle map idle events
}
toggleElement(elementId: MapElementEnum) {
this.#repository.toggleMapElement(elementId);
}
}4. Configure Map (Optional)
You can customize the map behavior by passing a configuration object:
// In your parent component template
@Component({
template: ` <app-map [config]="mapConfig"></app-map> `,
})
export class ParentComponent {
mapConfig: Partial<MapConfig> = {
maxZoom: 20,
minZoom: 8,
dragRotate: true,
center: [5.387827, 52.155172],
zoom: 12,
scrollZoom: false, // Disable scroll zoom
};
}Available configuration options:
center: Initial map center positionzoom: Initial zoom levelmaxZoom/minZoom: Zoom level constraintsbounds: Initial bounds to fit (overrides center/zoom if provided)interactive: Enable/disable all interactionsdragRotate: Enable/disable rotation via dragdoubleClickZoom: Enable/disable double-click zoomscrollZoom: Enable/disable scroll wheel zoomboxZoom: Enable/disable shift+drag box zoomdragPan: Enable/disable drag to pankeyboard: Enable/disable keyboard navigationtouchZoomRotate: Enable/disable touch gestures
Note: If bounds is provided, it will override center and zoom settings.
You can use predefined bounds:
import { COMMON_BOUNDS } from '@ndwnu/map';
mapConfig: Partial<MapConfig> = {
bounds: COMMON_BOUNDS.NETHERLANDS, // or COMMON_BOUNDS.AMERSFOORT
maxZoom: 20,
dragRotate: true,
};5. Component Styling
Important: Set a height for your map component:
.map-container {
height: 500px; /* or 100vh for full viewport */
width: 100%;
}6. Register Map Elements
In your onLoadMap() method, call registerMapElements() to initialize all map elements:
protected onLoadMap() {
this.#repository.registerMapElements(this.map);
// Set initial visibility
this.#repository.showMapElement(MapElementEnum.MyElement);
}Filtering in MapLibre
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:
- Provide filter observables to your MapElement
- Pass filters to sources during initialization
- Apply filters to layers within each source's layer definitions
Example implementation:
// In your MapElement
export class MyElement extends MapElement<MapElementEnum> {
constructor(
config: MapElementConfig<MapElementEnum>,
private filters$: Observable<FilterObject>,
) {
super(config);
this.sources = [new MyElementSource(config, filters$)];
}
}
// In your MapSource
export class MyElementSource extends MapSource<MapElementEnum> {
constructor(
config: MapElementConfig<MapElementEnum>,
private filters$: Observable<FilterObject>,
) {
super(config);
// Subscribe to filter changes and update layers
this.filters$.subscribe((filters) => this.updateLayerFilters(filters));
}
private updateLayerFilters(filters: FilterObject) {
// Apply filters to each layer in this source
this.layers.forEach((layer) => {
layer.applyFilter(filters);
});
}
}Note: Filter management may be included in future versions of this library to provide a more streamlined filtering experience.
Example Usage
See the playground application for a complete implementation example.
API
MapComponent (Abstract)
Base component that provides MapLibre integration.
Methods:
resizeMap(): Resize the map to fit containerzoomToLevel(level: number, options?): Zoom to specific level
Abstract Methods:
onLoadMap(): Called when map is loadedonRemoveMap(): Called before map destructiononIdle(): Called when map becomes idle
MapElementRepository (Abstract)
Manages collection of map elements.
Methods:
addMapElement(element): Add element to repositoryremoveMapElement(element): Remove and destroy elementshowMapElement(id): Make element visiblehideMapElement(id): Hide elementtoggleMapElement(id): Toggle element visibility
MapElement (Abstract)
Container for related sources and layers.
Properties:
id: Unique identifierelementOrder: Display ordersources: Array of MapSource instancesisVisible: Current visibility state
License
MIT
About NDW
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.
