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 ARCore.

Google provides a precise list of supported devices, but you can also check at runtime if device is supported via:

fun checkAvailability() {
WemapVPSARCoreLocationSource.checkAvailabilityAsync(requireContext()) { availability ->
when (availability) {
SUPPORTED_INSTALLED -> // launch VPS Location Source
SUPPORTED_NOT_INSTALLED -> // install ARCore
else -> // explain to the user that his device is not supported
}
}
}

Or you can add a Manifest property to restrict installation on unsupported devices via:

<meta-data
android:name="com.google.ar.core"
android:value="required" />

Installation

Check common installation.

Permissions

VPS Location source requires camera permission. Declare android.permission.CAMERA in your app Manifest as follows:

<uses-permission android:name="android.permission.CAMERA" />

Please request camera permission before creating and starting WemapVPSARCoreLocationSource, because without access to the camera - system can't work. If you don't do it, system will crash on first access to camera.

private val cameraPermissionHandler =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (!granted) {
// User denied camera access. show a message explaining how to enable it
return
}
// User granted camera access. Continue with WemapVPSARCoreLocationSource
}

fun requestCameraPermission() {
// show a model explaining that camera access is crucial for indoor localization and navigation.
// then check if it's arealy granted and request if not yet
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
// User already granted camera access, Continue with WemapVPSARCoreLocationSource
} else {
requestCameraPermission.launch(Manifest.permission.CAMERA)
}
}

Known limitations

ARCore-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.

1. VPS Location Source in Wemap ecosystem

Setting Up the VPS ARCore Location Source

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

fun setupLocationSource(mapData: MapData) {
vpsLocationSource = WemapVPSARCoreLocationSource(requireContext(), mapData)
vpsLocationSource.bind(requireContext(), surfaceView)
vpsLocationSource.vpsListeners.add(vpsListener)

// assign it to the map if you are using WemapMapSDK
mapView.locationManager.apply {
locationSource = vpsLocationSource
isEnabled = true
}

// 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 WemapVPSARCoreLocationSourceListener, as users may need to scan their environment.

private val vpsListener by lazy {
object: WemapVPSARCoreLocationSourceListener {
override fun onStateChanged(state: State) {
// Handle state changes
}

override fun onScanStatusChanged(status: ScanStatus) {
// Handle scan status changes
}

override fun onBackgroundScanStatusChanged(status: ScanStatus) {
// Handle background scan status changes
}

override fun onLocalizedUser(coordinate: Coordinate, attitude: Attitude, backgroundScan: Boolean) {
// 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:

fun startScan() {
vpsLocationSource.startScan()
}

Once the system successfully recognizes the user’s location, it will report the ACCURATE_POSITIONING state to WemapVPSARCoreLocationSourceListener. 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 LocationSourceListener.

The VPS system will also report:

  • NOT_POSITIONING – 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().
  • DEGRADED_POSITIONING – indicates that user location tracking is limited. A scan is recommended, but not mandatory, to restore the ACCURATE_POSITIONING state. We suggest a subtle UI indication when this state occurs, such as a location icon warning or a small toast message prompting the user to 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 WemapVPSARCoreLocationSourceListener method onBackgroundScanStatusChanged. Where are few common scenarios:

  • User walked a long distance without a re-scan and VPS state is DEGRADED_POSITIONING. System starts a background scan. This means that the system tries to restore ACCURATE_POSITIONING, 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 ACCURATE_POSITIONING.

  • User was standing with his phone for a long time and system started background scan, but VPS state is still ACCURATE_POSITIONING. 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 ARCore Location Source

Once you have the MapData, create a WemapVPSARCoreLocationSource instance, set vpsListener as well as listener and start Location Source:

fun setupLocationSource(mapData: MapData) {
vpsLocationSource = WemapVPSARCoreLocationSource(requireContext(), mapData)
vpsLocationSource.bind(requireContext(), surfaceView)
vpsLocationSource.vpsListeners.add(vpsListener)

// assign delegate and start it if you are using WemapPositioningSDK/VPSARCore with third-party Map or AR
vpsLocationSource.listener = locationListener
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 LocationSourceListener and assign it to VPS Location Source.

private val locationListener by lazy {
object : LocationSourceListener {

override fun onCoordinateChanged(coordinate: Coordinate) {
// Pass the updated coordinate to your map or AR implementation
}

override fun onAttitudeChanged(attitude: Attitude) {
// Pass the updated attitude to your map or AR implementation
}

override fun onError(error: Throwable) {
// 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 WemapVPSARCoreLocationSource. Assigning an itinerary to WemapVPSARCoreLocationSource 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 WemapVPSARCoreLocationSource.

Itinerary from Wemap API

fun calculateItinerary() {

val origin = Coordinate(48.88007462, 2.35591097, 0f)
val destination = Coordinate(48.88141308, 2.35747255, -2f)

ServiceFactory
.getItineraryProvider()
.itineraries(origin, destination, mapId = mapData.id)
.subscribe({ itineraries ->
vpsLocationSource.itinerary = itineraries.first()
}, { error ->
println("Failed to calculate itineraries with error: $error")
})
.disposedBy(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.
fun hardcodedItinerary(): Itinerary {
val origin = Coordinate(48.88007462, 2.35591097, 0f)
val destination = Coordinate(48.88141308, 2.35747255, -2f)

val coordinatesLevel0 = listOf(
listOf(2.3559003, 48.88005135),
...
listOf(2.35657153, 48.88013655)
).map { Coordinate(it[1], it[0], 0f) }

val legSegmentsLevel0 = LegSegment.fromCoordinates(coordinatesLevel0)

val coordinatesFrom0ToMinus1 = listOf(
listOf(2.35657153, 48.88013655),
listOf(2.3567008, 48.8801748)
).map { Coordinate(it[1], it[0], listOf(-1f, 0f)) }

val legSegmentsFrom0ToMinus1 = LegSegment.fromCoordinates(
coordinatesFrom0ToMinus1, -1f, Kind.ESCALATOR, Direction.DOWN
)

val coordinatesLevelMinus1 = listOf(
listOf(2.3567008, 48.8801748),
...
listOf(2.357253, 48.88061996)
).map { Coordinate(it[1], it[0], -1f) }

val legSegmentsLevelMinus1 = LegSegment.fromCoordinates(coordinatesLevelMinus1)

val coordinatesFromMinus1ToMinus2 = listOf(
listOf(2.357253, 48.88061996),
listOf(2.35727559, 48.88066565)
).map { Coordinate(it[1], it[0], listOf(-2f, -1f)) }

val legSegmentsFromMinus1ToMinus2 = LegSegment.fromCoordinates(
coordinatesFromMinus1ToMinus2, -1f, Kind.ESCALATOR, Direction.DOWN
)

val coordinatesLevelMinus2 = listOf(
listOf(2.35727559, 48.88066565),
...
listOf(2.35748253, 48.88143515)
).map { Coordinate(it[1], it[0], -2f) }

val legSegmentsLevelMinus2 = LegSegment.fromCoordinates(coordinatesLevelMinus2)

val segments = legSegmentsLevel0 + legSegmentsFrom0ToMinus1 + legSegmentsLevelMinus1 +
legSegmentsFromMinus1ToMinus2 + legSegmentsLevelMinus2

return Itinerary.fromSegments(origin, destination, segments)
}

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

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.