Skip to main content

@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 ResizeObserver handles 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: false disables 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), or viewer.navigateTo(nodeId) (fire and forget), and viewer.nearestNode(coordinates, level?) to find the node closest to a place (after viewer.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();
EventPayloadWhen
ready—The first image is on screen (once)
navigateViewerNodeA move to a node starts (click or code)
headingnumber (degrees)The look heading changes
locateLocatedPointA click in locate mode was located
measureMeasurementTwo clicks in measure mode were measured
clickViewerClickA click in navigate mode walked nowhere (above the horizon, or no node there): its heading, pitch and node, to pick your own geometry

Options​

OptionTypeDefaultDescription
zoombooleantrueMouse-wheel zoom
maxFovnumber80Widest vertical field of view, in degrees
navigationbooleantrueClick the floor to walk to the nearest node
onNavigate(nodeId: number) => voidA move to a node starts
onHeadingChange(heading: number) => voidThe look heading changes
onReady() => voidThe first image is on screen (once)
onLocate(point: LocatedPoint) => voidA click in locate mode was located
onMeasure(measurement: Measurement) => voidTwo clicks in measure mode were measured
onError(error: Error) => voidGraph, image, located point or measurement failed

API​

MemberDescription
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 | nullThe 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(): numberCurrent look heading, degrees (0 = north, clockwise)
lookAt(heading, pitch?)Turn to a heading/pitch, animated
setClickMode(mode) / clickModeWhat 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.


wemap-sdk-js


viewer

Classes​

Interfaces​

Type Aliases​

Functions​