Skip to main content

VPS Location Source

The VPS (Visual Positioning System) Location Source provides highly accurate indoor positioning using camera input. It's especially useful in environments where GPS signals are weak or unavailable.

info

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

You can find more information about VPS in the VPS Best Practices guide.

There are two slightly different ways to configure VPS Location Source:

  1. VPS Locaton Source in Wemap ecosystem (with WemapMapSDK or/and WemapGeoARSDK)
  2. VPS Locaton Source standalone. For example when you want to use it with third-party Map/AR.

Below you'll find guides for both cases.

Requirements

In addition to common requirement user device has to support ARKit.

Apple doesn't provide a precise list of supported devices, but you can check at runtime if device is supported via:

func checkAvailability() {
guard VPSARKitLocationSource.isAvailable else {
// explain to the user that his device is not supported
return
}
// launch VPS Location Source
}

Or you can add an Info.plist property to restrict installation on unsupported devices via:

<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arkit</string>
</array>

Installation

Check common installation.

Permissions

VPS Location Source requires camera permission. Declare NSCameraUsageDescription in your app Info.plist as follows:

<key>NSCameraUsageDescription</key>
<string>Camera is used to understand your surroundings and provide accurate indoor navigation</string>

Please request camera permission before creating and starting VPS Location Source, because without access to the camera - system can't work.

func requestCameraPermission() {
// show a model explaining that camera access is crucial for indoor localization and navigation.
// then request for camera permission
AVCaptureDevice.requestAccess(for: .video) { granted in
guard granted else {
// User denied camera access. show a message explaining how to enable it in settings
return
}
// User granted camera access. VPS Location Source can be created and started
}
}

Known limitations

ARKit-based relative positioning may occasionally show jumps or drift. Performance can also be affected by factors such as temperature, lighting, and environmental conditions. For example, relative positioning quality may decrease over time due to device temperature or in challenging environments (such as corridors with plain white walls). While we use overlays to reduce these effects, some issues may persist. We are aware of this and are working on improvements.

Device-specific issue: Relative positioning may be unreliable on iPhone SE (3rd generation). We are investigating this and working on a fix.

1. VPS Location Source in Wemap ecosystem

Setting Up the VPS ARKit Location Source

Once you have the MapData, create a VPSARKitLocationSource instance, set vpsDelegate, then assign it to Map or/and GeoAR as follows:

func setupLocationSource(mapData: MapData) {
vpsLocationSource = VPSARKitLocationSource(mapData: mapData)
vpsLocationSource.vpsDelegate = self

// assign it to the map if you are using WemapMapSDK
map.userLocationManager.locationSource = vpsLocationSource

// or/and assign it to GeoARView if you are using WemapGeoARSDK
geoARView.locationManager.locationSource = vpsLocationSource
}

Handling Location Source State Changes

You must handle state changes in VPSARKitLocationSourceDelegate, as users may need to scan their environment.

extension ViewController: VPSARKitLocationSourceDelegate {

func locationSource(_ locationSource: VPSARKitLocationSource, didChangeState state: VPSARKitLocationSource.State) {
// Handle state changes
}

func locationSource(_ locationSource: VPSARKitLocationSource, didChangeScanStatus status: VPSARKitLocationSource.ScanStatus) {
// Handle scan status changes
}

func locationSource(_ locationSource: VPSARKitLocationSource, didChangeBackgroundScanStatus status: VPSARKitLocationSource.ScanStatus) {
// Handle background scan status changes
}

func locationSource(_ locationSource: VPSARKitLocationSource, didLocalizeUserAtCoordinate coordinate: Coordinate, attitude: Attitude, backgroundScan: Bool) {
// here you could let user know that scan succeded or/and hide other hints
}
}

Scanning the Environment

To start tracking the user’s location, you need to allow the user to scan their environment:

func startScan() {
vpsLocationSource.startScan()
}

Once the system successfully recognizes the user’s location, it will report the accuratePositioning state to VPSARKitLocationSourceDelegate. Shortly afterward, the VPS system will start updating the user’s location indicator on the map or in GeoAR. If you are using it with third-party Map or AR you will start receiving updated Coordinate and Attitude values in LocationSourceDelegate.

The VPS system will also report:

  • notPositioning – indicates that the system hasn't yet recognized the environment and a VPS scan is required. At this point, you should present the camera view to the user to allow them to scan using vpsLocationSource.startScan().
  • degradedPositioning – indicates that user location tracking is limited for various reasons (see reasons in API reference documentation). A scan is recommended, but not mandatory, to restore the accuratePositioning state. We recommend a small impact in the UI when this state occurs (location icon warning or a small toast message to suggest a re-scan).

Background scan

In addition to the scan initiated by the user, system sometimes can self-initiate background scan in attempts to improve user tracking. It is based on a variety of different conditions, such as the distance traveled, the time elapsed since the last successful scan, the changed environmental conditions, etc.

When background scan status changes, you will receive events to VPSARKitLocationSourceDelegate method didChangeBackgroundScanStatus. Where are few common scenarios:

  • User walked a long distance without a re-scan and VPS state is degradedPositioning. System starts a background scan. This means that the system tries to restore accuratePositioning, but it can't (usually because the phone is targeting the floor). So we suggest you to show a small hint on the screen for the user saying - "Please hold your phone vertically in front of you to let system recognize your surroundings" until background scan will be stopped or/and VPS state will be changed to accuratePositioning.

  • User was standing with his phone for a long time and system started background scan, but VPS state is still accuratePositioning. This means that the system suspects upcoming degradation in tracking quality, and it tries to preserve it. No action is required on your side at this moment.

2. VPS Location Source standalone (outside Wemap ecosystem)

Setting Up the VPS ARKit Location Source

Once you have the MapData, create a VPSARKitLocationSource instance, set vpsDelegate as well as delegate and start Location Source:

func setupLocationSource(mapData: MapData) {
vpsLocationSource = VPSARKitLocationSource(mapData: mapData)
vpsLocationSource.vpsDelegate = self
// assign delegate and start it if you are using WemapPositioningSDK/VPSARKit with third-party Map or AR
vpsLocationSource.delegate = self
vpsLocationSource.start()
}

Handling Location Source State Changes

This part is identical

Retrieving User's Coordinate and Attitude

When you are using VPS Location Source with WemapMapSDK or WemapGeoARSDK you don't need to handle user's coordinate and attitude yourself. But if you need to integrate it with third-party Map or AR you need to implement LocationSourceDelegate and assign it to VPS Location Source.

extension VPSViewController: LocationSourceDelegate {

func locationSource(_ locationSource: any LocationSource, didUpdateCoordinate coordinate: Coordinate) {
// Pass the updated coordinate to your map or AR implementation
}

func locationSource(_ locationSource: any LocationSource, didUpdateAttitude attitude: Attitude) {
// Pass the updated attitude to your map or AR implementation
}

func locationSource(_ locationSource: any LocationSource, didFailWithError error: any Error) {
// Handle errors
}
}

Scanning the Environment

This part is identical

MapMatching (Assigning an Itinerary)

When you are using VPS Location Source with WemapMapSDK or WemapGeoARSDK you don't need to handle mapmatching yourself because it's handled internally by the system.

Once you start receiving updated Coordinates, you can assign an itinerary to the VPSARKitLocationSource. Assigning an itinerary to VPSARKitLocationSource when a user is following an A→B itinerary enhances the overall navigation experience (e.g., itinerary projections, conveyor detection, etc.).

For example, when a conveyor is detected, the system will prompt you to re-scan the environment to restore tracking.

Below are two examples demonstrating how to obtain an itinerary and assign it to VPSARKitLocationSource.

Itinerary from Wemap API

func calculateItinerary() {
let origin = Coordinate(latitude: 48.88007462, longitude: 2.35591097, level: 0)
let destination = Coordinate(latitude: 48.88141308, longitude: 2.35747255, level: -2)

ServiceFactory
.getItineraryProvider()
.itineraries(origin: origin, destination: destination, mapId: mapData.id)
.subscribe(onSuccess: { itineraries in
self.vpsLocationSource.itinerary = itineraries.first
}, onFailure: { error in
debugPrint("Failed to calculate itineraries with error: \(error)")
})
.disposed(by: disposeBag)
}

Manually created Itinerary

You can create an itinerary by providing the minimum necessary information, as shown below.

An Itinerary consists of an origin, a destination, and a list of segments, where:

  • origin is the starting point of the itinerary.
  • destination is the ending point of the itinerary.
  • segments is a collection of segment objects, where each:
    • segment is a struct with two connected points (p1, p2) along the itinerary, and optionally levelDifference, stepKind, stepDirection that describe the transition between levels.
func hardcodedItinerary() -> Itinerary {

let origin = Coordinate(latitude: 48.88007462, longitude: 2.35591097, level: 0)
let destination = Coordinate(latitude: 48.88141308, longitude: 2.35747255, level: -2)

let coordinatesLevel0 = [
[2.3559003, 48.88005135],
...
[2.35657153, 48.88013655]
].map { Coordinate(latitude: $0[1], longitude: $0[0], level: 0) }

let legSegmentsLevel0 = LegSegment.fromCoordinates(coordinatesLevel0)

let coordinatesFrom0ToMinus1 = [
[2.35657153, 48.88013655],
[2.3567008, 48.8801748]
].map { Coordinate(latitude: $0[1], longitude: $0[0], levels: [-1, 0]) }

let legSegmentsFrom0ToMinus1 = LegSegment.fromCoordinates(
coordinatesFrom0ToMinus1, levelDifference: -1, stepKind: .escalator, stepDirection: .down
)

let coordinatesLevelMinus1 = [
[2.3567008, 48.8801748],
...
[2.357253, 48.88061996]
].map { Coordinate(latitude: $0[1], longitude: $0[0], level: -1) }

let legSegmentsLevelMinus1 = LegSegment.fromCoordinates(coordinatesLevelMinus1)

let coordinatesFromMinus1ToMinus2 = [
[2.357253, 48.88061996],
[2.35727559, 48.88066565]
].map { Coordinate(latitude: $0[1], longitude: $0[0], levels: [-2, -1]) }

let legSegmentsFromMinus1ToMinus2 = LegSegment.fromCoordinates(
coordinatesFromMinus1ToMinus2, levelDifference: -1, stepKind: .escalator, stepDirection: .down
)

let coordinatesLevelMinus2 = [
[2.35727559, 48.88066565],
...
[2.35748253, 48.88143515]
].map { Coordinate(latitude: $0[1], longitude: $0[0], level: -2) }

let legSegmentsLevelMinus2 = LegSegment.fromCoordinates(coordinatesLevelMinus2)

let segments = legSegmentsLevel0 + legSegmentsFrom0ToMinus1 + legSegmentsLevelMinus1
+ legSegmentsFromMinus1ToMinus2 + legSegmentsLevelMinus2
return .init(origin: origin, destination: destination, segments: segments)
}

You can also check out an example of how a sample GeoJSON is transformed into a Wemap Itinerary in our GitHub repository.

Important note

Starting from iOS 18, there is a bug where ARSession is automatically stopped by ARView when the view is dismissed or hidden. As a result, the positioning process stops after a few seconds. If you are using ARView, we recommend a workaround in our sample application

Examples

For additional examples and sample implementations of WemapSDKs, visit the official GitHub repository.

Clone the repository and follow the README instructions to run the sample application.