@geosdi/ngx-leaflet-layers-plugin
NgxLeafletLayers
General information
The base layers of a Leaflet map, already wired: a store of ready-made layers, a fluent builder to pick the ones an application offers, and the switch between them.
The service also carries the two things a map UI needs whenever the base layer changes and that no Leaflet control gives you: which logo to show over the map, and which contrast colour the controls on top of it should use — a satellite layer needs white, OpenStreetMap needs black.
It attaches to an L.Map already created by the application; it never
creates one.
Installation
npm i @geosdi/ngx-leaflet-layers-plugin --save
It depends on :
"@angular/common": ">=20.0.0",
"@angular/core": ">=20.0.0",
"typescript": ">=5.0.0",
"leaflet": ">=1.9.3",
"leaflet-bing-layer": ">=3.3.1",
"rxjs": ">=7.8.2"
leaflet-bing-layer is imported by the service itself, so it must be installed
even by an application that offers none of the Bing layers.
Usage
Everything goes through NgxLeafletLayersPluginService, which is
providedIn: "root" and stateful: it holds one set of base layers and one
current layer, so an application has one map's worth of state in it.
import { Injectable, inject } from "@angular/core";
import {
BaseLayerEnum,
NgxLeafletLayersPluginService,
} from "@geosdi/ngx-leaflet-layers-plugin";
@Injectable({ providedIn: "root" })
export class BootstrapService {
private layersService: NgxLeafletLayersPluginService = inject(
NgxLeafletLayersPluginService,
);
initBaseMaps(map: L.Map): void {
this.layersService
.withBaseLayers([
BaseLayerEnum.OPEN_STREET_MAP,
BaseLayerEnum.DARK_BASE_MAP,
BaseLayerEnum.ORTHO_MAP,
])
.withDefaultBaseLayer(BaseLayerEnum.OPEN_STREET_MAP)
.withMap(map)
.build();
}
}
build() adds the default layer to the map at zIndex 0 and notifies the logo
and the contrast. The builder methods can be called in any order, but build()
needs both withMap and withBaseLayers: it alerts if either is
missing — see the note on alert() under Limitations.
withBaseLayers creates every layer it is given straight away, so the list is
the place where the cost is paid, not the switch.
To feed a Leaflet control, or a control of your own, getAllBaseLayers()
returns the created BaseLayer objects.
BaseLayer
interface BaseLayer {
key: string;
label: string;
layer: any; // the Leaflet layer, already created
logo_type: LogoType; // "logoBlack" | "logoWhite"
contrast: string; // "#000" | "#fff"
}
The base layers
Thirteen keys, in BaseLayerEnum. Status measured on 10 September 2026 by
requesting one tile from each — it is the one column here that ages by itself.
| key | label | what it serves | status |
|---|---|---|---|
OPEN_STREET_MAP |
OpenStreetMap | OSM Standard, tile.openstreetmap.org |
works, no key |
DARK_BASE_MAP |
DarkBaseMap | the same OSM tiles, darkened in CSS | works, no key — see below |
ORTHO_MAP |
OrthoMap | Esri World Imagery | works, no key |
GOOGLE_NORMAL |
Google Normal | mt{0-3}.google.com/vt?lyrs=m |
answers, but see the note below |
GOOGLE_SATELLITE |
Google Satellite | the same endpoint, lyrs=s |
answers, but see the note below |
GOOGLE_HYBRID |
Google Hybrid | the same endpoint, lyrs=y,h |
answers, but see the note below |
BING_AERIAL |
Bing Aerial Layer | Bing Maps, via leaflet-bing-layer |
dead: the bundled key is refused |
BING_ROAD_LAYER |
Bing Road Layer | same | dead: the bundled key is refused |
BING_HYBRID |
Bing Hybrid | same | dead: the bundled key is refused |
METACARTA |
Metacarta | WMS vmap0.tiles.osgeo.org |
dead: the host answers 404 |
GEOSDI_BASE |
geoSdi | WMS dpc.geosdi.org, Mappa_di_Base |
dead: LayerNotDefined |
GEOSDI_NULL_BASE |
geoSdi No Map | the same WMS, StratiDiBase:nullMap |
dead: LayerNotDefined |
EMPTY |
EmptyLayer (c) geoSDI | nothing — a WMS layer with no url | by design: an empty background |
Three things worth knowing before picking from that list.
The Bing layers cannot work as published. The Bing Maps key is a constant
inside this library, and Bing answers it with
403 DeniedCredentials — your credentials may be denied or suspended. There
is no option to pass your own: offering these three needs a change here, not
configuration.
The two geoSDI WMS layers and Metacarta are gone. dpc.geosdi.org is
reachable but publishes no layers at all, so both GEOSDI_* layers answer with
a LayerNotDefined service exception, and vmap0.tiles.osgeo.org is a 404.
All three are also plain http://, which a page served over https blocks as
mixed content before the request even leaves the browser.
The Google layers use an undocumented endpoint. mt{0-3}.google.com/vt is
not the Maps API: it needs no key, it answers, and it is outside Google's terms
of service. It is in this list for historical reasons — an application with a
Maps licence should use the official SDK instead.
DARK_BASE_MAP
DARK_BASE_MAP serves the plain OpenStreetMap tiles, darkened by a CSS
filter. There is no dark raster style at tile.openstreetmap.org — the dark
map on openstreetmap.org is its vector layer, styled client-side by MapLibre —
and the filter is what the site itself applies to its own raster layers.
Nothing to install and no API key: the layer injects this rule into <head> the
first time it is created, once per page.
.ngx-dark-base-map .leaflet-tile {
filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(1.5);
}
To darken it differently, override the class with a more specific rule, or
replace the injected <style id="ngx-dark-base-map-style">.
The class is set through the Leaflet className option, so it lands on this
layer's tile container only. Writing the same filter on .leaflet-tile, as
most examples do, would invert every tile layer on the map — the application's
WMS overlays included.
Before 1.2.3 this layer was CARTO dark_all. CARTO now stamps
API KEY REQUIRED across every tile served without a key, on all of its basemap
styles, and that key belongs to whoever deploys the application rather than to
this library.
The logo and the contrast
Both come from the layer definition and change with it: logo_type says which
of two images to show, contrast which colour to paint the controls sitting on
the map.
Two ways to read them — pull, or push:
// pull, whenever needed
const logo: LogoType = this.layersService.getLogo();
const layer: BaseLayer | undefined = this.layersService.currentBaseLayer;
// push, on every change
this.layersService.subjectNotifyLogo$.subscribe(
(logo: LogoType) => (this.logo = logo),
);
this.layersService.subjectNotifyContrast$.subscribe(
(contrast: string) => (this.contrast = contrast),
);
- the two subjects are
BehaviorSubjects that start with"", so a subscriber gets one empty value before the firstbuild(); getLogo()reads the current layer and therefore throws beforebuild();logo_typeis"logoBlack"or"logoWhite": the file names of your images. This library ships no assets — putlogoBlack.pngandlogoWhite.png, or whatever those two values name for you, in the application'sassetsfolder;- the
LogoTypeenum is not exported from the package, althoughBaseLayer.logo_typeis typed with it. Until it is, compare against the two string values rather than importing the enum.
Methods
| Method | action |
|---|---|
withBaseLayers(layers: BaseLayerEnum[]) |
creates the layers an application offers; returns the service, for chaining |
withDefaultBaseLayer(type: string | BaseLayerEnum) |
the one shown first |
withMap(map: L.Map) |
the map to attach to, already created |
build() |
adds the default layer at zIndex 0 and notifies logo and contrast |
changeBaseLayer(type: string | BaseLayerEnum, force: boolean) |
switches: force: true puts the new layer on the map, force: false only notifies and leaves the swap to a control |
currentBaseLayer (getter) |
the BaseLayer in use, or undefined before build() |
getAllBaseLayers() |
every created BaseLayer, to feed a control |
createLayer(type: BaseLayerEnum) |
one layer, created and not registered — for a map that manages its own |
getLogo() |
the current LogoType |
applyCurrentLayer() |
adds the current layer to the map |
removeCurrentBaseLayer() |
removes it |
Events
| Event | action |
|---|---|
subjectNotifyLogo$ |
the LogoType of the new base layer, on every change |
subjectNotifyContrast$ |
the contrast colour of the new base layer, on every change |
Both fire on build() and on every changeBaseLayer(), force or not.
Limitations
Three behaviours that are easier to read here than to discover:
changeBaseLayer(type, true)does not remove the previous layer. It adds the new one over it, and the old one keeps requesting tiles. Until it is fixed, either switch withforce: falseand let a Leaflet layers control own the exclusivity, or callremoveCurrentBaseLayer()before changing;- errors are reported with
alert(), not by throwing: a missing map, an empty layer list, an unknown key. A modal dialog blocks the page, and in an automated browser it blocks the session; - the service is a singleton with the state of one map. Two maps in the same
application share the current layer, the logo and the contrast; the second one
to
build()wins. For a second map, provide the service again in that component'sproviders.