Tile Sources
Raster tiles, vector tiles (including PMTiles) and georeferenced images as declarative sources.
Raster tiles
#ng-map-raster-source adds a raster tile source and the layer that draws it (XYZ, TMS or WMS templates). opacity and visible are reconciled on the existing layer.
import { Component, signal } from '@angular/core';
import { MapComponent, MapRasterSourceComponent } from 'ng-mapcn';
@Component({
selector: 'app-raster-map',
standalone: true,
imports: [MapComponent, MapRasterSourceComponent],
template: `
<input type="range" min="0" max="1" step="0.05" [value]="opacity()"
(input)="opacity.set(+$any($event.target).value)" />
<button (click)="visible.set(!visible())">Toggle</button>
<ng-map [center]="[10, 50]" [zoom]="3.5">
<ng-map-raster-source
id="tiles"
[tiles]="['https://tile.openstreetmap.org/{z}/{x}/{y}.png']"
[tileSize]="256"
[maxzoom]="19"
attribution="© OpenStreetMap contributors"
[opacity]="opacity()"
[visible]="visible()"
/>
</ng-map>
`,
})
export class RasterMapComponent {
readonly opacity = signal(0.7);
readonly visible = signal(true);
}Image overlay
#ng-map-image-overlay pins an image to four corner coordinates. Changing coordinates moves the live image source (setCoordinates) without re-creating it.
import { Component, signal } from '@angular/core';
import {
type ImageOverlayCoordinates,
MapComponent,
MapImageOverlayComponent,
} from 'ng-mapcn';
@Component({
selector: 'app-image-overlay-map',
standalone: true,
imports: [MapComponent, MapImageOverlayComponent],
template: `
<button (click)="shift()">Move east</button>
<ng-map [center]="[-74, 24]" [zoom]="4.5">
<!-- Corners are [lng, lat], clockwise from the top-left. -->
<ng-map-image-overlay
id="chart"
url="/assets/sea-chart.png"
[coordinates]="corners()"
[opacity]="0.8"
/>
</ng-map>
`,
})
export class ImageOverlayMapComponent {
readonly corners = signal<ImageOverlayCoordinates>([
[-80, 28],
[-68, 28],
[-68, 20],
[-80, 20],
]);
shift() {
// A new array: only the changed coordinates are pushed to the live image source.
this.corners.update((c) => c.map(([lng, lat]) => [lng + 2, lat]) as unknown as ImageOverlayCoordinates);
}
}Vector tiles from PMTiles
#ng-map-vector-source adds the source only. This example reads a single static .pmtiles archive (no tile server) and draws it with a small layer component that waits for the source by id. Hover a parcel to highlight it with feature-state. The same code is the pmtiles block; copying it requires npm i pmtiles.
import { Component, afterNextRender } from '@angular/core';
import { addProtocol } from 'maplibre-gl';
import { MapComponent, MapVectorSourceComponent } from 'ng-mapcn';
import { Protocol } from 'pmtiles'; // npm i pmtiles
// Once per page, before a source with a pmtiles:// url is added.
let registered = false;
function registerPmtilesProtocol() {
if (registered) return;
registered = true;
addProtocol('pmtiles', new Protocol().tile);
}
@Component({
selector: 'app-parcels-map',
standalone: true,
// ParcelsLayerComponent: see the "Draw the layers" section below.
imports: [MapComponent, MapVectorSourceComponent, ParcelsLayerComponent],
template: `
<ng-map [center]="[-74.0046, 40.5811]" [zoom]="11.6">
<ng-map-vector-source
id="parcels"
url="pmtiles://https://example.com/parcels.pmtiles"
promoteId="parcel"
/>
<app-parcels-layer source="parcels" />
</ng-map>
`,
})
export class ParcelsMapComponent {
constructor() {
afterNextRender(() => registerPmtilesProtocol());
}
}Registering the PMTiles protocol
# ng-mapcn does not depend on pmtiles. Run npm i pmtiles in your app (also required to copy the pmtiles block) and register the protocol once, before a pmtiles:// source is added. The URL is pmtiles:// followed by the archive URL; the server must support HTTP range requests.
// main.ts or an app initializer: the library does not depend on pmtiles.
import { provideAppInitializer } from '@angular/core';
import { addProtocol } from 'maplibre-gl';
import { Protocol } from 'pmtiles';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
// Browser only: skip it during server rendering.
if (typeof window !== 'undefined') addProtocol('pmtiles', new Protocol().tile);
}),
],
};Draw the layers
# A vector source has no layers of its own. Add them with map.addLayer from a component that reads MapContextService and waits for the source with hasSource(id). When a source component is torn down, or a theme switch reloads the style, it removes every layer still using its source (yours included) and then the source. Gate on hasSource(id) so your layers are re-added when the source comes back.
import { Component, effect, inject, input } from '@angular/core';
import { MapContextService } from 'ng-mapcn';
@Component({ selector: 'app-parcels-layer', standalone: true, template: '' })
export class ParcelsLayerComponent {
readonly source = input.required<string>();
private readonly ctx = inject(MapContextService);
constructor() {
effect((onCleanup) => {
const map = this.ctx.map();
const source = this.source();
// isLoaded() closes during a theme switch; hasSource() closes until the source
// component has re-added the source, so addLayer never runs before it.
if (!map || !this.ctx.isLoaded() || !this.ctx.hasSource(source)) return;
map.addLayer({
id: `${source}-fill`,
type: 'fill',
source,
'source-layer': 'parcels',
paint: { 'fill-color': '#3b82f6', 'fill-opacity': 0.5 },
});
onCleanup(() => {
try {
if (map.getLayer(`${source}-fill`)) map.removeLayer(`${source}-fill`);
} catch {
// style may be mid-reload
}
});
});
}
}Source ids and the registry
# The MapLibre source id is exactly the id input (auto-generated when omitted), so layers can reference it as source: 'parcels'. Raster layers are named raster-layer-<id> and image layers image-layer-<id>. Each source component announces its source in MapContextService while it exists on the map: it disappears when a theme switch removes the style and comes back once the source is re-added, so a consumer re-runs by itself.
import { effect, inject } from '@angular/core';
import { MapContextService } from 'ng-mapcn';
const ctx = inject(MapContextService);
effect(() => {
// Reactive: true only while a source component has the source on the map.
if (ctx.hasSource('parcels')) {
// safe to add a layer with source: 'parcels'
}
// ctx.sourceIds() is the whole set (a ReadonlySet<string> signal).
});URLs, TileJSON and WMS
# Give tiles (URL templates) or url (a TileJSON document). Both are updated in place with setTiles / setUrl. tileSize, minzoom, maxzoom, bounds, scheme, attribution and promoteId are read when the source is created. The mode (url or tiles) is fixed at creation too: switching between them later is ignored. Change the id to rebuild it with new ones.
<!-- URL templates -->
<ng-map-vector-source id="roads" [tiles]="['https://tiles.example.com/roads/{z}/{x}/{y}.mvt']" [maxzoom]="14" />
<!-- A TileJSON document: zoom range, bounds and attribution come from it -->
<ng-map-vector-source id="roads" url="https://tiles.example.com/roads.json" />
<!-- WMS served as raster tiles -->
<ng-map-raster-source
id="wms"
[tiles]="['https://geo.example.com/wms?service=WMS&request=GetMap&layers=land&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&format=image/png&transparent=true']"
[tileSize]="256"
/>