Skip to main content

Positioning Guide

The positioning package provides location tracking capabilities using various location sources. This section covers everything you need to know to get started with positioning.

How to Initialize Core SDK with Positioning​

The Core SDK must be initialized before creating location sources, as location sources may depend on configuration data from the Core SDK (especially for VPS).

import { core } from '@wemap/core';
import { GnssWifiLocationSource } from '@wemap/positioning';

// Step 1: Initialize Core SDK first
await core.init({
emmid: 'your-map-id',
token: 'your-token',
});

// Step 2: Now you can create location sources
const locationSource = new GnssWifiLocationSource({
// Configuration options
});

Location Sources​

A Location Source is a system used to track user position. Wemap provides two main location source types:

GnssWifiLocationSource​

Geolocation based on GPS and WiFi positioning. This is the standard location source that works in most environments.

import { GnssWifiLocationSource } from '@wemap/positioning';

const gnssLocationSource = new GnssWifiLocationSource({
// Configuration options
});

// Start the location source
await gnssLocationSource.start();

// Listen for position updates
gnssLocationSource.onUpdate((pose) => {
// Both fields are optional — guard before reading them.
if (pose.position) {
console.log('Position:', pose.position.lat, pose.position.lng);
}
if (pose.attitude) {
console.log('Orientation:', pose.attitude.headingDegrees);
}
});

Check out the API reference for GnssWifiLocationSource →

The Pose object​

Every onUpdate callback receives a Pose:

FieldTypeNotes
positionUserPosition | undefinedA Coordinates with time/accuracy/bearing. Exposes lat/lng (and latitude/longitude), alt, level. Absent before the first fix.
attitudeAttitude | undefinedDevice orientation; attitude.headingDegrees is the heading in degrees. Absent until orientation data is available.
inclinationnumber | undefinedInclination in radians.
timenumber | undefinedTimestamp (ms since epoch).
accuracynumber | undefinedAccuracy in meters.

position and attitude are optional — always check they exist before reading them.

VPSLocationSource (Visual Positioning System)​

Geolocation based on visual positioning system. VPS uses camera input to provide highly accurate indoor positioning, especially useful in environments where GPS signals are weak or unavailable.

import { VPSLocationSource } from '@wemap/positioning';

const vpsLocationSource = new VPSLocationSource({
// Configuration options
});
info

Wemap VPS must be configured by the Wemap team. Please contact us if you are interested in using it.

For detailed information about VPS, including setup, camera configuration, and scan procedures, see the VPS Location Source guide.

How to Handle Authorization​

Location sources require various browser permissions. The positioning package provides helper functions to request these permissions:

Camera Permissions (for VPS)​

import { requestCameraPermissions } from '@wemap/camera';

const hasPermission = await requestCameraPermissions();
if (!hasPermission) {
throw new Error('Camera permission denied');
}

Location Permissions (for GNSS/WiFi)​

import { requestLocationPermissions } from '@wemap/positioning';

const hasPermission = await requestLocationPermissions();
if (!hasPermission) {
throw new Error('Location permission denied');
}

Device Orientation Permissions (iOS)​

On iOS, you need to request device orientation permission. This request needs to be triggered after a user interaction (click, tap, etc.).

import { requestSensorPermissions } from '@wemap/positioning';

const hasPermission = await requestSensorPermissions();
if (!hasPermission) {
throw new Error('Sensor permission denied');
}

Keeping the motion sensors alive​

Between two VPS scans, the position moves with the phone's motion sensors. Browsers stop delivering them in a few situations without raising any error: the blue dot then stands still while the user walks.

Embedding in an iframe​

Delegate the sensors and the wake lock to the frame:

<iframe src="..." allow="camera; geolocation; accelerometer; gyroscope; magnetometer; screen-wake-lock"></iframe>
  • Without accelerometer; gyroscope; magnetometer, Chrome on Android delivers no motion event and reports nothing.
  • Without screen-wake-lock, the screen goes to sleep during the session and the sensors stop with it.
  • In a cross-origin frame, Chrome on Android delivers no motion event until the visitor taps a button inside the frame. Start positioning from such a tap, not on page load.

Asking for permissions​

On Chrome for Android, every permission prompt (camera, location, notifications…) pauses the motion sensors until the page gets the focus back, often several seconds after the prompt closes. Request every permission before navigation starts, never in the middle of it. Safari is not affected.

Scrolling on iOS​

On Safari, a page that scrolls pauses the motion sensors for as long as the scroll lasts. A map that consumes its own gestures is not affected; keep the rest of the page from scrolling during navigation.

Brave​

Brave denies motion sensors by default, without showing a prompt. The visitor has to allow them in Settings → Site settings → Motion sensors.

Holding the phone​

The position moves in the direction the phone points. Ask users to hold it in front of them, pointed where they walk.

Location source lifecycle​

Location sources follow a start → stop → dispose lifecycle, and let you subscribe / unsubscribe to updates and errors:

const source = new GnssWifiLocationSource();

const onPose = (pose) => console.log(pose.position);
source.onUpdate(onPose); // subscribe
await source.start(); // begin emitting updates

// ... later ...
source.offUpdate(onPose); // unsubscribe a specific callback
await source.stop(); // stop emitting; the source can be started again

// When you are done with the source for good, release its resources:
await source.dispose(); // stops it and clears every registered callback
  • start() / stop() — start and stop emitting. A stopped source can be restarted with start().
  • dispose() — permanent teardown: stops the source and removes all update/error/state listeners. Call it when the source is no longer needed (e.g. on view unmount); the instance should be discarded afterwards.
  • onUpdate(cb) / offUpdate(cb) and onError(cb) / offError(cb) — add and remove callbacks. Keep a reference to the callback to remove it later.

Error handling​

import { core } from '@wemap/core';
import { GnssWifiLocationSource } from '@wemap/positioning';

try {
// Rejects with an Error if emmid/token are missing, or an HttpError
// (carrying .status) if the livemap can't be fetched (bad token → 401/403,
// unknown emmid → 404).
await core.init({ emmid: 'your-map-id', token: 'your-token' });
} catch (error) {
console.error('SDK init failed:', error);
}

const source = new GnssWifiLocationSource();

// Runtime errors (permission revoked, sensor/provider failures) are delivered
// to the onError callback rather than thrown from start().
source.onError((error) => {
console.error('Location error:', error.message);
});

await source.start();

For VPS, per-scan failures are reported through onError as well — startScan() resolves to false on a scan error rather than rejecting (it only rejects when VPS is unavailable, e.g. no camera). Route calculation (router.directions()) rejects with an HttpError if the request fails.

How to handle Itinerary + Map Matching​

Map matching projects your position onto a predefined route, providing more accurate navigation.

Step 1: Calculate a Route​

import { Router } from '@wemap/routing';

const router = new Router();

// Calculate route from current position to destination
const itineraries = await router.directions(
{ lat: 48.8566, lng: 2.3522 }, // Origin
{ lat: 48.8606, lng: 2.3376 }, // Destination
'WALK', // Travel mode
);

// Use the first (usually best) itinerary
const itinerary = itineraries[0];

Step 2: Set Itinerary for Map Matching​

import { MapMatching } from '@wemap/positioning';

// Set the itinerary for map matching
MapMatching.setItinerary(itinerary);

// Now all location sources will project positions onto this route

Step 3: Receive Map-Matched Positions​

locationSource.onUpdate((pose) => {
// Position is automatically projected onto the route
if (pose.position) {
console.log('Map-matched position:', pose.position);
// Use this position for navigation display
}
});

Step 4: Clear Itinerary​

MapMatching.clearItinerary();

Complete Map Matching Example​

import { core } from '@wemap/core';
import { GnssWifiLocationSource, MapMatching } from '@wemap/positioning';
import { Router } from '@wemap/routing';

async function setupNavigationWithMapMatching() {
// 1. Initialize Core SDK
await core.init({ emmid: '...', token: '...' });

// 2. Create location source
const locationSource = new GnssWifiLocationSource();

// 3. Set up position updates
locationSource.onUpdate((pose) => {
console.log('Position update:', pose.position);
// Update your UI/map with the position
});

// 4. Start location source
await locationSource.start();

// 5. Calculate route
const router = new Router();
const itineraries = await router.directions(
origin,
destination,
'WALK'
);

// 6. Set itinerary for map matching
MapMatching.setItinerary(itineraries[0]);

// Now positions will be automatically projected onto the route
}

// To clear map matching later:
MapMatching.clearItinerary();
info

By default, map matching does not strictly project the position onto the route: a position is snapped to the route only when it lies within maxDistance meters of it (default 30) and within the bearing tolerance. Positions further away are left unprojected. Set useStrict: true to project every position onto the route regardless of distance.

These map-matching parameters are passed in the location source configuration:

const locationSource = new GnssWifiLocationSource({
maxDistance: 20, // snap only within 20m of the route (default 30)
useStrict: false, // set true to always project onto the route
});

Integrate Map Matching with your own itinerary routing system​

Our map matching system is not stricly tied to the routing package. You can use it with your own itinerary routing system.

import { Itinerary, ItineraryInfoManager } from '@wemap/routing';
import { GnssWifiLocationSource, MapMatching, type Pose, Coordinates, UserPosition } from '@wemap/positioning';

const locationSource = new GnssWifiLocationSource();
await locationSource.start();

let userPosition: UserPosition | null = null;

locationSource.onUpdate((pose: Pose) => {
// pose.position is a UserPosition (a Coordinates) or undefined before the first fix
userPosition = pose.position ?? null;
});

/*
* Use your own itinerary routing system to get the itinerary
*/
const itinerary = await yourItineraryRoutingSystem.getItineraries(userPosition, destination);
const orderedCoordinates = itinerary.coords.map((point) => new Coordinates(point.lat, point.lng));

/**
* Transform your itinerary shape to the one expected by the map matching system
* You can also use the Itinerary.fromOrderedPointsArray method
*/
const transformedItinerary = Itinerary.fromOrderedCoordinates(orderedCoordinates, itinerary.origin, itinerary.destination);

MapMatching.setItinerary(transformedItinerary);