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="&copy; 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"
/>