<SpatialMap> Container Shell
The <SpatialMap> component is the central container for the DigiNest indoor mapping suite. It instantiates the underlying MapLibre GL JS map instance, registers the PMTiles protocol exactly once, manages the controlled or uncontrolled viewport camera state, and exposes context to hooks and child overlay components.
⚙️ Component Props API
import maplibregl from 'maplibre-gl';
export interface SpatialMapProps {
/** Array of building data structures serving this map view. */
buildings: Building[];
/** Controlled active building ID. Exploded floors and details are visible for this building. */
activeBuildingId?: string;
/** Callback fired when the active building switches (e.g. via zoom DynamicFocus threshold). */
onActiveBuildingChange?: (id: string) => void;
/** Uncontrolled initial viewport camera coordinates. */
initialViewState?: ViewState;
/** Controlled viewport camera coordinates. Pair with onViewStateChange. */
viewState?: ViewState;
/** Fired when coordinates or zoom values update during camera motions. */
onViewStateChange?: (vs: ViewState) => void;
/** Style mode selection. Extrudes 3D structures or locks to pitch-0 topdown plan. */
mode?: '2d' | '3d';
/** Fired when mode switches between 2D and 3D. */
onModeChange?: (m: '2d' | '3d') => void;
/** MapLibre style JSON specification or direct CDN string. Defaults to a bundled Protomaps light spec. */
mapStyle?: maplibregl.StyleSpecification | string;
/** Set to true to require a two-finger swipe to pan on mobile web views. */
cooperativeGestures?: boolean;
/** Enable or disable standard vector attribution credits overlay. Default is true. */
attributionControl?: boolean;
/** Maximum camera pitch allowed in degrees. Default is 70. */
maxPitch?: number;
/** Custom HTML className applied to the root wrapper. */
className?: string;
/** Inline React CSS variables applied to the root wrapper. */
style?: React.CSSProperties;
/** Constrain the camera to a lat/lon bounding box. Wires to map.setMaxBounds(); cleared on unmount. Added in v2.16 (#372). */
venueBounds?: VenueBounds;
/** Fired when the camera crosses the indoor/outdoor zoom threshold (zoom ≥ 18 → 'indoor'). Added in v2.16 (#373). */
onZoomModeChange?: (mode: ZoomMode) => void;
/** Child overlays and panels. */
children?: React.ReactNode;
}
/** Two-corner bounding box: [[minLng, minLat], [maxLng, maxLat]]. */
export type VenueBounds = [[number, number], [number, number]];
/** Camera context mode. 'overview' when zoom < 18; 'indoor' when zoom ≥ 18. */
export type ZoomMode = 'overview' | 'indoor';
export interface ViewState {
longitude: number;
latitude: number;
zoom: number;
pitch: number;
bearing: number;
}
export interface Building {
id: string;
name: Record<string, string>;
levels: Level[];
homography?: Homography;
}
export interface Level {
id: string;
ordinal: number;
name: Record<string, string>;
short_name: Record<string, string>;
}
export type Homography = readonly [
number, number, number,
number, number, number,
number, number, number
];🗺️ Venue Bounds & Zoom Mode (v2.16)
venueBounds
Prevents the user from panning outside a venue's geographic boundary:
const bounds: VenueBounds = [
[-79.3960, 43.6420], // [minLng, minLat]
[-79.3880, 43.6480], // [maxLng, maxLat]
];
<SpatialMap buildings={buildings} venueBounds={bounds}>
...
</SpatialMap>Internally calls map.setMaxBounds(venueBounds) after the map loads and map.setMaxBounds(null) on unmount.
onZoomModeChange
Subscribe to the camera crossing the indoor/outdoor threshold (zoom level 18):
<SpatialMap
buildings={buildings}
onZoomModeChange={(mode) => {
// mode: 'overview' | 'indoor'
console.log('zoom mode:', mode);
}}
>
...
</SpatialMap>The ZoomMode union type is 'overview' | 'indoor'. The transition fires at zoom ≥ 18 and is debounced by 100 ms to avoid rapid toggling.
🏢 Multi-Building Campus Architecture
The SDK supports single-building environments as well as massive campus footprints containing dozens of structures:
- Vector Footprints: Footprints of all loaded buildings are rendered on the basemap at low-level zoom scales.
- Active Focusing: Exploiting the
activeBuildingIdcontrols which building is selected and expanded. - Automatic Transition:
<DynamicFocus />handles automatic viewport-to-floor selection transitions when zoom ranges pass defined thresholds (zoom ≥18 locks on the building interior; zoom <16 fades back to outdoor basemaps).
🎨 MapLibre Layer Z-Ordering
Below is the authoritative layout hierarchy inside <SpatialMap> and <FloorLayers>. deck.gl layers share MapLibre's WebGL context via MapboxOverlay, preventing dual rendering delays:
| Layer Z-Index | Layer Identifier | Source Type | Description |
|---|---|---|---|
| 0 | basemap-* | PMTiles Vector | Static external Protomaps vectors |
| 100 | building-extrusion-3d | GeoJSON Footprint | 3D oblique structures |
| 200 | floorplan-image | WebGL homography raster | Scanned blueprint canvas overlays |
| 300 | floor-fill | GeoJSON Polygon | IMDF Unit fills |
| 310 | floor-outline | GeoJSON LineString | Unit boundaries |
| 320 | wall-3d | fill-extrusion | Extruded building partitions |
| 400 | door-*, opening-* | GeoJSON LineString | Exit paths / architectural openings |
| 500 | fixture-* | GeoJSON Polygon | Inside furniture / custom assets |
| 600 | amenity-icons | Symbol/Circle | Anchor point representations |
| 700 | data-layer-* | GeoJSON or deck.gl | Customer point/icon telemetry layers |
| 800 | heatmap-* | deck.gl Interleaved | Historical activity aggregation |
| 900 | wayfinding-route | GeoJSON LineString | Directed A* path paths |
| 950 | rtls-trail | deck.gl TripsLayer | Real-time moving particle vectors |
| 980 | bluedot | Symbol/Circle | Focused personal locator beacon |
| 990 | selection-highlight | GeoJSON LineString | Active click/select bounds |
🧼 Cleanup Protocols
<SpatialMap> handles system bindings transparently during unmounting:
- Removes
pmtiles://custom transport handlers usingmaplibregl.removeProtocol('pmtiles'). - Disconnects active Web Worker transport channels safely.
- Destroys local IndexedDB caching allocations for temporary viewState variables.
