3D Buildings & Terrain
Extrude buildings and drape the map over real elevation, declaratively.
3D buildings
#ng-map-fill-extrusion extrudes polygons by a height property. With no inputs it extrudes the building layer of the Carto basemap. Tilt the map with the pitch option (or right-drag) and zoom past 14. Opacity and color are applied with diffed setters.
import { Component, signal } from '@angular/core';
import { MapComponent, MapFillExtrusionComponent } from 'ng-mapcn';
@Component({
selector: 'app-buildings-map',
standalone: true,
imports: [MapComponent, MapFillExtrusionComponent],
template: `
<input type="range" min="0.2" max="1" step="0.05" [value]="opacity()"
(input)="opacity.set(+$any($event.target).value)" />
<button (click)="byHeight.set(!byHeight())">Color by height</button>
<ng-map
[center]="[-74.0134, 40.7061]"
[zoom]="15.5"
[options]="{ pitch: 60, bearing: -20 }"
>
<!-- Defaults target the Carto basemap: source "carto", layer "building". -->
<ng-map-fill-extrusion
id="buildings"
[opacity]="opacity()"
[color]="byHeight() ? heightColor : undefined"
/>
</ng-map>
`,
})
export class BuildingsMapComponent {
readonly opacity = signal(0.8);
readonly byHeight = signal(false);
readonly heightColor = [
'interpolate', ['linear'], ['coalesce', ['to-number', ['get', 'height']], 0],
0, '#93c5fd', 50, '#818cf8', 150, '#c084fc', 300, '#f472b6',
];
}Terrain and hillshade
#ng-map-terrain turns a raster elevation (DEM) source into real 3D relief with map.setTerrain. Without tiles it uses public Terrarium tiles. Raise the pitch (up to maxPitch) to see Mount Fuji rise.
import { Component, signal } from '@angular/core';
import { MapComponent, MapTerrainComponent } from 'ng-mapcn';
@Component({
selector: 'app-terrain-map',
standalone: true,
imports: [MapComponent, MapTerrainComponent],
template: `
<input type="range" min="0.5" max="3" step="0.1" [value]="exaggeration()"
(input)="exaggeration.set(+$any($event.target).value)" />
<label><input type="checkbox" [checked]="hillshade()"
(change)="hillshade.set($any($event.target).checked)" /> Hillshade</label>
<ng-map
[center]="[138.7274, 35.3606]"
[zoom]="10.5"
[options]="{ pitch: 70, bearing: 30, maxPitch: 85 }"
>
<!-- No tiles given: public Terrarium elevation tiles are used. -->
<ng-map-terrain id="relief" [exaggeration]="exaggeration()" [hillshade]="hillshade()" />
</ng-map>
`,
})
export class TerrainMapComponent {
readonly exaggeration = signal(1.5);
readonly hillshade = signal(true);
}Basemap buildings
# The Carto styles (Positron, Dark Matter, Voyager) have one vector source named protomaps with a buildings layer carrying height and min_height. Those are the defaults, with buildings drawn from zoom 14, where the basemap fades in their flat footprints. Earlier Carto styles named the source carto, with the OpenMapTiles building layer (render_height, render_min_height): with no source, the component uses whichever of the two the style has. The default filter skips OpenMapTiles outlines flagged hide_3d (their parts carry the real heights); pass [filter]="null" for none. Another basemap with a vector source works the same way, naming its source and sourceLayer. The blank style and raster basemaps have no vector source, so use your own (below).
<!-- Defaults: source="protomaps" sourceLayer="buildings"
heightProperty="height" baseProperty="min_height"
[minZoom]="14" [opacity]="0.8" and a theme-aware color. -->
<ng-map-fill-extrusion />
<!-- Another basemap with an OpenMapTiles schema (OpenFreeMap, MapTiler, Stadia) -->
<ng-map-fill-extrusion
source="openmaptiles"
sourceLayer="building"
heightProperty="render_height"
baseProperty="render_min_height"
[opacity]="0.8"
beforeId="waterway_label"
/>Your own source
#ng-map-fill-extrusion never creates a source. It waits until map.getSource(source) exists: a basemap source, one of the tile source components (it reacts to hasSource) or one your application adds later. After a theme switch the layer comes back as soon as its source does. A source component removes the extrusion layer with it when it is torn down. Use the paint input for any other fill-extrusion property.
<ng-map [center]="[-74.0, 40.7]" [zoom]="15" [options]="{ pitch: 60 }">
<!-- Your own vector tiles (here a PMTiles archive) ... -->
<ng-map-vector-source id="footprints" url="pmtiles://https://example.com/footprints.pmtiles" />
<!-- ... extruded by a property: the layer waits for the source and survives theme switches. -->
<ng-map-fill-extrusion
source="footprints"
sourceLayer="footprints"
heightProperty="floors"
[paint]="{ 'fill-extrusion-height': ['*', ['get', 'floors'], 3.2] }"
/>
</ng-map>
<!-- A GeoJSON source needs no sourceLayer -->
<ng-map-fill-extrusion source="my-geojson-source" heightProperty="height" />Elevation tiles
#tiles / url, encoding (terrarium or mapbox), tileSize and the zoom limits are read when the source is created. The terrain is style-level state that a theme switch discards: the component enables it again with its sources once the new style is loaded. Changing exaggeration re-applies the terrain (MapLibre rebuilds its mesh), so a slider is fine but not free. A map has one terrain at a time. The default tiles come from a public open-data bucket with no SLA: in production pass your own provider with tiles or url, and allow the host (s3.amazonaws.com) if your CSP is strict. The hillshade has its own DEM source because MapLibre warns when it shares one with the terrain; it is added and removed with hillshade and styled with hillshadeExaggeration, the three color inputs and hillshadePaint.
<!-- Mapbox Terrain-RGB / MapTiler tiles instead of the default Terrarium ones -->
<ng-map-terrain
[tiles]="['https://api.maptiler.com/tiles/terrain-rgb-v2/{z}/{x}/{y}.webp?key=KEY']"
encoding="mapbox"
[tileSize]="512"
[maxzoom]="12"
attribution="© MapTiler"
[exaggeration]="1.5"
[hillshade]="true"
[hillshadeShadowColor]="'#1e293b'"
beforeId="waterway_label"
/>Ids and z-order
#beforeId inserts the extrusion or the hillshade layer below an existing layer (for example the labels). It is applied at creation only, and changing id re-creates the layers.
<ng-map-terrain id="relief" [hillshade]="true" />
// Ids derived from id="relief":
// DEM source: terrain-source-relief (announced via MapContextService.hasSource)
// hillshade source: terrain-hillshade-source-relief (only while hillshade is true)
// hillshade layer: terrain-hillshade-relief
//
// <ng-map-fill-extrusion id="buildings" /> creates the layer
// fill-extrusion-layer-buildings (and no source of its own)