@ndwnu/map
v3.1.0
Published
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 Bas
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 Dataportaal Wegverkeer) for easier development with MapLibre. It also contains the style json file that contains the NDW Basemap styling.
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
- NDW Basemap style: A json file with the NDW basemap
Installation of @ndwnu/map
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
NDW Basemap style
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).
Context / relevant
- Context layers are unobtrusive — light, desaturated colors meant to support, not distract. Basemap, roads, admin boundaries, labels.
- Relevant layers are saturated and bold. They carry the data the user actually came for: accessibility, road safety, parking, your own datasets.
Composition
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:
"metadata": {
"group": "context-roads",
"sub-group": "WKD-parking-areas",
"type": "context",
"legendName": "Parking areas",
"desc": ""
}Example of how to use groups / subgroups to display / hide a subset of layers with the @ndwnu/map MapElement structure:
export class NdwElement extends ApiElement<MapElementEnum, MapFilter, GrgLegendItem> {
constructor(config: GrgMapElementConfig, http: HttpClient) {
const layerFilter: NdwLayerFilterFunction = (layer) => {
return (
layer.metadata.group === 'context-map' ||
(layer.metadata.group === 'context-roads' &&
layer.metadata['sub-group'] !== 'NWB-hectometersigns' &&
layer.metadata.group === 'context-roads' &&
layer.metadata['sub-group'] !== 'WKD-parking-areas' &&
layer.metadata.group === 'context-roads' &&
layer.metadata['sub-group'] !== 'FCD-segments')
);
};
super(
config,
http,
environment.mapStyles.ndw,
layerFilter as (layer: LayerSpecification) => boolean,
);
}
}When you don't use the MapElement structure, toggle a whole group with plain JavaScript:
function toggleGroupLayers(group: string) {
const style = map.getStyle();
style.layers.forEach((layer) => {
if (layer.metadata?.group === group) {
const visibility = map.getLayoutProperty(layer.id, 'visibility') || 'visible';
map.setLayoutProperty(layer.id, 'visibility', visibility === 'none' ? 'visible' : 'none');
}
});
}Include in your project
You can either import the style file via import and provide it to MapLibre, or you can use the url on maps.ndw.nu:
https://maps.ndw.nu/styles/ndw-basemap/dev/style.json
TODO when style.json hosting task is done by NLS update URL here. NLS story
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.
