Flutter Native - VPS (Visual Positioning System)
This guide covers how to use VPS (Visual Positioning System) with the Wemap SDK for precise indoor positioning. VPS uses the device camera to recognize surroundings — it does not require location permissions.
Overview
VPS (Visual Positioning System) uses the device camera to capture images of the environment, which are matched against mapped indoor spaces. It provides meter-level accuracy indoors (shopping malls, airports, hospitals, etc.).
VPS does not use GPS or location services. It requires:
- Camera permission
- Internet connection
- ARCore (Android) — Google Play Services for AR — see supported devices
- Mapped indoor environments
Permissions
VPS requires camera permission only. Location permission is not needed for VPS.
Android
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
iOS
Add to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for visual positioning (VPS) in indoor spaces.</string>
If you use the permission_handler package, add to your Podfile's post_install block:
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
]
Enabling VPS
Set locationSource in MapOptions:
MapOptions(
mapID: yourMapId,
token: yourToken,
environment: Environment.PROD,
locationSource: LocationSource.VPS,
)
Starting and stopping VPS
After permissions are granted, you can start and stop VPS programmatically using MapManager, which is provided in the onMapReady callback:
| Method | Description |
|---|---|
mapManager.startVPS() | Start VPS scanning to retrieve user position. Ensure camera permission is granted before calling. |
mapManager.stopVPS() | Stop the ongoing VPS scan. |
late MapManager _mapManager;
void _onMapReady(MapData mapData, MapManager mapManager) {
_mapManager = mapManager;
}
// Start VPS scan (after camera permission is granted)
void _startVpsScan() async {
await _mapManager.startVPS();
}
// Stop VPS scan
void _stopVpsScan() async {
await _mapManager.stopVPS();
}
VPS state and callbacks
VPSState
The positioning state is reported via onStateChanged:
| State | Meaning |
|---|---|
VPSState.NOT_POSITIONING | User is not localized |
VPSState.DEGRADED_POSITIONING | Localized with reduced accuracy |
VPSState.ACCURATE_POSITIONING | Localized with good accuracy |
NotPositioningReason
When not localized, onScanReasonChanged provides a reason:
| Reason | Meaning |
|---|---|
tooFarFromNavigation | Too far from the navigation route |
conveyingDetected | On elevator/escalator/moving walkway |
noRelocalization | Unable to relocalize |
none | No specific reason |
ScanStatus
onBackgroundScanStatusChanged reports background scan activity:
| Status | Meaning |
|---|---|
ScanStatus.started | Background scan has started |
ScanStatus.stopped | Background scan has stopped |
Callbacks
| Callback | Signature | When fired |
|---|---|---|
onStateChanged | void Function(VPSState state) | Positioning state changes |
onUserLocalized | void Function(Coordinate, Attitude, bool backgroundScan) | User is localized (backgroundScan bool indicates weither or not it's from background scan) |
onStartedVpsProcess | void Function() | VPS scan starts |
onStoppingVpsProcess | void Function() | VPS scan is stopping |
onScanReasonChanged | void Function(NotPositioningReason reason) | Reason for not being positioned changes |
onBackgroundScanStatusChanged | void Function(ScanStatus scanStatus) | Background scan starts or stops |
onVPSBadConnection | void Function() | VPS connection is poor |
onVPSNoConnection | void Function() | No internet during VPS |
Background scan
In addition to the scan initiated by the user, the system can sometimes self-initiate a background scan to improve user tracking. This is based on various conditions, such as distance traveled, time elapsed since the last successful scan, or changed environmental conditions.
When the background scan status changes, you receive events via onBackgroundScanStatusChanged. Here are common scenarios:
- User walked a long distance without a re-scan and VPS state is
DEGRADED_POSITIONING. The system starts a background scan to try to restoreACCURATE_POSITIONING, but it cannot (usually because the phone is targeting the floor). Consider showing a hint such as: "Please hold your phone vertically in front of you so the system can recognize your surroundings" until the background scan stops and/or the VPS state changes toACCURATE_POSITIONING. - User has been standing with the phone for a long time and the system started a background scan, but VPS state is still
ACCURATE_POSITIONING. The system suspects upcoming degradation in tracking quality and tries to preserve it. No action is required on your side.
Retrieving user location
Once the user is localized (VPS state is ACCURATE_POSITIONING or DEGRADED_POSITIONING), you can access position and attitude via MapManager:
| Method | Description |
|---|---|
mapManager.getUserLocation() | Get current user position once. Returns Coordinate? with latitude, longitude, level. Returns null if not localized. |
mapManager.getUserLocationStream() | Stream<Coordinate> — continuous position updates |
mapManager.getUserAttitudeStream() | Stream<Attitude> — device heading (headingDegrees 0–360) for the blue dot arrow |
void _onMapReady(MapData mapData, MapManager mapManager) {
mapManager.getUserLocation().then((coordinate) {
if (coordinate != null) {
print('User at: ${coordinate.latitude}, ${coordinate.longitude}, level: ${coordinate.level}');
}
});
}
// Or listen to continuous updates
mapManager.getUserLocationStream().listen((coordinate) {
// Handle each position update
});
VPS flow
- User taps the user location button (with
userLocationButtonEnabled: true) - Camera permission is requested if not yet granted
- ScanDialogWidget appears — user starts the scan
- Camera turns on; VPSInProcessScreen shows (if
vpsInProcessScreen: true) — dotted border, status text, cancel button - User moves the phone to capture surroundings
- On success:
onUserLocalizedfires,onStateChangedreportsVPSState.ACCURATE_POSITIONINGorDEGRADED_POSITIONING - On failure: dialogs such as
VPSBadConnectionDialogorVPSNoConnectionDialogmay appear (if enabled)
VPS dialogs and screens
Enable these WemapMap options to improve the VPS experience:
| Option | When shown |
|---|---|
vpsNoConnectionDialog | No internet when starting a VPS scan |
vpsBadConnectionDialog | Scan takes longer than expected (poor connection) |
vpsTimedOutDialog | Scan times out |
holdUpPhoneDialog | Prompt to hold phone up during background scan |
vpsInProcessScreen | Overlay during scan (dotted border, status, cancel button) |
scanSuggestionDialog | Suggests scanning when user is not localized |
WemapMap(
options: mapOptions,
userLocationButtonEnabled: true,
vpsNoConnectionDialog: true,
vpsBadConnectionDialog: true,
vpsTimedOutDialog: true,
holdUpPhoneDialog: true,
vpsInProcessScreen: true,
scanSuggestionDialog: true,
)
Complete VPS example
WemapMap(
options: MapOptions(
mapID: yourMapId,
token: yourToken,
environment: Environment.PROD,
locationSource: LocationSource.VPS,
),
userLocationButtonEnabled: true,
vpsNoConnectionDialog: true,
vpsBadConnectionDialog: true,
vpsTimedOutDialog: true,
holdUpPhoneDialog: true,
vpsInProcessScreen: true,
scanSuggestionDialog: true,
onStateChanged: (state) {
// Handle positioning state
},
onUserLocalized: (coordinate, attitude, backgroundScan) {
// User localized — e.g. haptic feedback
},
)
Troubleshooting
| Issue | Solution |
|---|---|
| Camera not opening | Check CAMERA permission and usage description (iOS) |
| "ARCore required" on Android | Ensure ARCore is installed (Google Play Services for AR) |
| Scan never completes | Ensure good lighting, mapped environment, and stable internet |
| VPS not available | Verify locationSource: LocationSource.VPS and camera permission |