@wemap/viewer
Framework-agnostic 360° panorama viewer for Wemap maps. It loads the map's image graph (the 360° photos captured on site) and lets users look around and walk from photo to photo. Built on three.js, with no UI framework.
Pair it with @wemap/viewer-map to show the graph and the look
direction on a @wemap/map.
Installation
npm install @wemap/core @wemap/geo @wemap/viewer
three and @tweenjs/tween.js are regular dependencies — nothing else to install.
No build step (<script> tag)
Load the UMD bundle dist/cdn/wemap-viewer.umd.js after the @wemap/core and
@wemap/geo bundles. It inlines three.js and exposes
window.Wemap.Viewer.
Requirements
core.init()first. Identity (emmid), auth and the VPS endpoint all come from@wemap/core; the viewer takes none of them.- A map with a VPS provider. Without it,
open()rejects. - A sized container. The canvas fills its container and follows its size
(a
ResizeObserverhandles resizes), so give the container a height.
Quick start
import { core } from '@wemap/core';
import { WemapViewer } from '@wemap/viewer';
import '@wemap/viewer/style.css';
await core.init({ emmid: '31668', token: '<token>' });
const viewer = new WemapViewer(document.getElementById('viewer')!, {
onNavigate: (nodeId) => console.log('now at', nodeId),
});
// Open on a node, looking east. Resolves once its image is on screen.
await viewer.open(1059484, { heading: 90 });
open() takes a node id (a deep link, a click on the map graph) or a position,
in which case it opens on the nearest node, on the position's level when it has
one:
import { Coordinates } from '@wemap/geo';
// Latitude, longitude, altitude (none), level 1.
await viewer.open(new Coordinates(43.60907, 3.91719, null, 1));
The first call fetches the image graph. Later calls move there like
navigateTo, and heading turns the view with an animation. Without heading
the look direction is kept. Awaiting is optional: ready and onError report
the same outcome.
Moving around
- Look around by dragging (with inertia), or with the arrow keys once the
viewer has focus (press on it, or Tab to it). Zoom with the mouse wheel
(
zoom: falsedisables it). - Walk by clicking the floor: hovering shows a disc under the pointer and a
cross on the nearest node; clicking walks there. Disable with
navigation: false. Paused while the view moves to the next node. - Programmatically:
viewer.open(target, { heading })(awaitable), orviewer.navigateTo(nodeId)(fire and forget), andviewer.nearestNode(coordinates, level?)to find the node closest to a place (afterviewer.loadGraph(), which fetches the graph without opening a photo).viewer.lookAt(heading, pitch = 0)turns the camera the short way round; dragging the panorama cancels it.
The look direction is a real compass heading, so it is kept when you change node: you keep facing the same way.
navigateTo updates currentNode and fires navigate right away, while the
new image loads and fades in. If that image fails to load, the viewer goes back
to the node still on screen (navigate fires again with it) and calls
onError.
Locating a point
In locate click mode, a click on the panorama gives the geo position of what
is under it (from the VPS depth estimate) instead of walking: to place a POI
where the user points, for example.
viewer.setClickMode('locate');
viewer.on('locate', ({ coordinates, node }) => {
console.log(coordinates.lat, coordinates.lng, coordinates.alt, coordinates.level);
});
viewer.setClickMode('navigate'); // back to walking
The cursor becomes a crosshair, and clicks during a transition are ignored. The
level is the clicked node's when the VPS gives none. Failures go to onError.
Measuring
In measure click mode, two clicks on the panorama give the distance between
the two points (from the VPS depth estimate). The viewer draws a dot on each
point and a line between them; a third click clears them, as do changing mode
and moving to another node.
viewer.setClickMode('measure');
viewer.on('measure', ({ distance }) => showLabel(`${distance.toFixed(2)} m`));
Drawing over the panorama
setPolygons draws polygons anchored in the real world: a shelf front, an area
on the floor, a door. Each vertex needs an altitude, in the same reference as
the graph nodes'. The polygons follow the user from node to node; a new call
replaces them, an empty list clears them. The panorama has no depth: a polygon
behind a wall still shows.
viewer.setPolygons([{
coordinates: [
new Coordinates(48.99068, 2.20736, -1.41),
new Coordinates(48.99067, 2.20742, -1.41),
new Coordinates(48.99067, 2.20742, 1.24),
new Coordinates(48.99068, 2.20736, 1.24),
],
color: '#3860BE',
}]);
Events
Each constructor callback (onNavigate, onHeadingChange, onReady,
onError) takes one listener. For more, subscribe with viewer.on(), which
returns an unsubscribe function:
const off = viewer.on('heading', (heading) => compass.rotate(heading));
off();
| Event | Payload | When |
|---|---|---|
ready | — | The first image is on screen (once) |
navigate | ViewerNode | A move to a node starts (click or code) |
heading | number (degrees) | The look heading changes |
locate | LocatedPoint | A click in locate mode was located |
measure | Measurement | Two clicks in measure mode were measured |
click | ViewerClick | A click in navigate mode walked nowhere (above the horizon, or no node there): its heading, pitch and node, to pick your own geometry |
Options
| Option | Type | Default | Description |
|---|---|---|---|
zoom | boolean | true | Mouse-wheel zoom |
maxFov | number | 80 | Widest vertical field of view, in degrees |
navigation | boolean | true | Click the floor to walk to the nearest node |
onNavigate | (nodeId: number) => void | A move to a node starts | |
onHeadingChange | (heading: number) => void | The look heading changes | |
onReady | () => void | The first image is on screen (once) | |
onLocate | (point: LocatedPoint) => void | A click in locate mode was located | |
onMeasure | (measurement: Measurement) => void | Two clicks in measure mode were measured | |
onError | (error: Error) => void | Graph, image, located point or measurement failed |
API
| Member | Description |
|---|---|
open(target, { heading?, pitch? }) | Open on a node id or the nearest node to a position; resolves once its image is on screen, rejects if the node is unknown or the graph or image fails |
loadGraph(): Promise<ViewerNode[]> | Fetch the image graph without opening a photo (once; shared with open()) |
getNodes(): ViewerNode[] | All nodes (id, coordinates, level, image, capture heading); empty until the graph is loaded |
currentNode: ViewerNode | null | The node the viewer is at (set as soon as a move starts) |
navigateTo(nodeId) | Move to a node; no-op if unknown or already there |
nearestNode(coordinates, level?) | Closest node to a place, on level when given; null before the graph is loaded |
getHeading(): number | Current look heading, degrees (0 = north, clockwise) |
lookAt(heading, pitch?) | Turn to a heading/pitch, animated |
setClickMode(mode) / clickMode | What a click does: 'navigate' (walk, default), 'locate' or 'measure' |
getGraphGeojson() | The graph as GeoJSON (used by @wemap/viewer-map) |
on(event, callback) | Subscribe to an event; returns an unsubscribe function |
resize() | Re-measure the container (automatic on resize) |
destroy() | Release the renderer, images, listeners and canvas |
ViewerDataClient (graph and image URLs) is exported for standalone use too.
viewer
Classes
Interfaces
- GraphLine
- GraphPoint
- LocatedPoint
- Measurement
- OpenOptions
- SphereControlsConfig
- ViewerClick
- ViewerEvents
- ViewerLabel
- ViewerNode
- ViewerOptions
- ViewerPolygon
- VisionGeojsonGraph
- VisionGraph