Skip to main content

Indoor & Levels

WemapMap renders multi-level (indoor) livemaps and, when indoor is enabled in your livemap, drives floor selection automatically.

Automatic building & level selection

After core.init(), WemapMap reads indoor.enable and indoor.minZoom from the livemap snippet. When indoor is enabled it fetches buildings for the viewport on movement (debounced), selects the building nearest the viewport centre, and switches to that building's default level. Below indoor.minZoom there is no current building.

There is nothing to wire up for a standard indoor livemap:

import { core, type Building } from '@wemap/core';
import { WemapMap } from '@wemap/map';

await core.init({ emmid, token });
const map = new WemapMap({ container: 'map' });

map.onBuildingChange((building: Building | null) => {
// e.g. rebuild a floor switcher from building?.levels
});

Override snippet indoor settings when needed:

const map = new WemapMap({
container: 'map',
indoor: { enable: true, minZoom: 16 },
});

Display a level

setLevel(level) rewrites the filter of every level-aware layer so only features on that level (plus level-agnostic features) stay visible. A feature is kept when:

  • its level property equals the requested level, or
  • it declares a min_level / max_level range containing the level, or
  • it has no level property at all.
map.setLevel(1);
const unsubscribe = map.onLevelChange((level) => console.log('now on', level));

setLevel is idempotent (it never stacks filters), and the active level is re-applied automatically when the style is swapped.

Built-in level switcher

For an easy way to display a floor switcher, drop in the opt-in LevelControl. It renders one button per level of the current building (top floor first), reflects the active level, and shows nothing when no indoor building is in view:

import { WemapMap, LevelControl } from '@wemap/map';

const map = new WemapMap({ container: 'map' });
await map.whenReady();
map.addControl(new LevelControl(map)); // defaults to 'bottom-right'

It reuses maplibre's own control styling (from maplibre-gl.css), so no extra stylesheet is required.

Make your own layers level-aware

Give a runtime layer a level property on its features and wire it into setLevel, either when adding it:

map.addSource('my-src', { type: 'geojson', data });
map.addLayer({ id: 'my-rooms', type: 'fill', source: 'my-src' }, { indoor: true });

…or by registering a layer you added through the raw instance (unregisterIndoorLayer restores the original filter):

map.maplibre.addLayer(myLevelAwareLayer);
map.registerIndoorLayer(myLevelAwareLayer.id);

Use getLevelProperty(level) to build the GeoJSON properties for a feature at a given floor.

Automatic pose-driven level sync

When indoor is enabled, two rules run automatically (no configuration):

  1. Building selection — when the active building changes (pan), the map switches to that building's default_level.
  2. Pose level — when a user pose reports a new floor, the pose is inside the active building's footprint, and the level exists on the building, the map follows.

There is no auto level change when no building is selected, and a manual setLevel() (e.g. a floor picker) sticks until one of these triggers fires. See the User location guide for the pose stream.

For the full API, see the Map API reference.