gmaps_vehicle_tracker
Smooth, road-aware live vehicle tracking (Uber / Rapido style) on Google Maps for Flutter.
You feed it timestamped location samples and, optionally, a route. It turns them into smooth motion that follows the road, with a stable heading, bounded prediction, freshness reporting, optional camera follow, built-in vehicle markers and route styling. Everything is drawn on google_maps_flutter, which this package re-exports.
- Copy-paste examples for every feature: EXAMPLE.md
- Runnable demo app:
example/
Screenshots
| Ride selector + live tracking | Fleet on real roads | Close-up (debug: raw GPS dots) | Motion and feed settings | Built-in vehicle gallery |
|---|---|---|---|---|
| Platforms | Android (verified on device, with a native no-flicker marker path) · iOS (configured, not yet device-verified; uses the plugin marker path) |
| Requires | Flutter ≥ 3.32, Dart ≥ 3.12 (sdk: ^3.12.0), google_maps_flutter ^2.18 |
| Network | None. The library makes no HTTP calls and stores nothing. |
Contents
- Setup
- Quick start
- Concepts
- Built-in vehicles
- API reference
- VehicleTracker
- LocationSample
- TrackingRoute / RouteStop
- VehicleIcon
- VehicleType / VehicleArt
- VehicleSelector (Uber-style picker)
- TrackingMap
- TrackingMap.builder / TrackingMapHooks
- TrackingLayer
- CameraDirector / CameraFollowConfig
- TrackingStyle and friends
- TrackingMapStyles
- TrackerConfig
- TrackingStatus, phases, diagnostics
- Events
- RenderedVehicleState
- PlaybackController (history)
- Simulation
- Utilities and pure-Dart core
- How the motion works
- Rendering and performance
- Limitations
- Costs, terms, privacy
- Adding or replacing built-in art
- Troubleshooting
- Development
1. Setup
dependencies:
gmaps_vehicle_tracker:
path: ../gmaps_vehicle_tracker # or your git/pub source
One import gives you this package and all of google_maps_flutter:
import 'package:gmaps_vehicle_tracker/gmaps_vehicle_tracker.dart';
Google Maps keys. Enable Maps SDK for Android and Maps SDK for iOS in Google Cloud and restrict the key: Android package name + SHA-1, iOS bundle ID.
- Android: add the key to
AndroidManifest.xml:
The example injects<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}"/>MAPS_API_KEYfromandroid/local.properties, which is not committed. - iOS: call
GMSServices.provideAPIKey(key)inAppDelegate. The example readsGMSApiKeyfromInfo.plist, which is filled fromios/Flutter/Secrets.xcconfig. The defaultgoogle_maps_flutter_iosimplementation is legacy; considergoogle_maps_flutter_ios_sdk10(iOS 16+) or_sdk9(iOS 15+). - Android warm-up (optional): call
GoogleMapsFlutterAndroid.warmup()early to avoid first-map jank.
2. Quick start
final route = TrackingRoute.fromEncodedPolyline(encodedPolylineFromBackend);
final tracker = VehicleTracker(route: route, icon: const VehicleIcon.builtIn(VehicleType.car));
tracker.bindStream(myLocationStream); // Stream<LocationSample>
TrackingMap(
trackers: [tracker],
camera: CameraDirector(), // follows north-up by default
initialCameraPosition: CameraPosition(target: route.start.toLatLng(), zoom: 16),
mapStyle: TrackingMapStyles.clean, // less clutter
padding: const EdgeInsets.only(bottom: 200),
);
// later: tracker.dispose();
3. Concepts
| Concept | What it is |
|---|---|
| Sample | A real observation from your backend or GPS (LocationSample). Authoritative. |
| Rendered state | The visual position drawn each frame (RenderedVehicleState). An estimate: never store it as the real location. |
| Render delay | The vehicle is drawn slightly in the past (adaptive, about 0.25–4 s depending on preset), so most frames interpolate between two real samples. |
| Prediction | When samples stop, motion continues briefly with decaying speed, then holds. Bounded by time and distance. |
| Route matching | With a route, samples are projected to a distance along the route. The vehicle moves along the road, never cuts corners and never reverses because of a late sample. |
| Primary / secondary | The first primary tracker gets full frame rate, the route line, stops and camera follow. Secondary trackers (nearby cars, fleets) update at a lower rate. |
| Phase | Freshness of data: tracking, predicting, stale, lost, and so on. Drive your "location delayed" UI from it. |
4. Built-in vehicles
12 top-down images ship with the package. Each is nose-up, transparent, has a soft rotation-invariant shadow, and comes in 1x–4x densities. Sizes follow real vehicle length, compressed so small vehicles stay visible. Size = whole image including shadow padding, in logical pixels (dp).
| Image | VehicleArt |
VehicleType |
Type default | Marker size (dp) | displayName |
|---|---|---|---|---|---|
carWhite |
car |
yes | 34 × 64 | Cab | |
carBlue |
car |
35 × 64 | Cab Premium | ||
carTaxi |
car |
35 × 64 | Taxi | ||
carHatchback |
car |
34 × 64 | Mini | ||
motorcycleBlack |
motorcycle |
yes (also used for bicycle at 0.85×) | 29 × 52 | Bike | |
motorcycleBlue |
motorcycle |
28 × 52 | Bike Plus | ||
motorcycleTaxi |
motorcycle |
28 × 52 | Bike Taxi | ||
busWhite |
bus |
yes | 32 × 92 | City Bus | |
busBlue |
bus |
32 × 92 | Coach | ||
busYellow |
bus |
32 × 92 | School Bus | ||
autoRickshaw |
autoRickshaw |
yes | 33 × 54 | Auto | |
eRickshaw |
eRickshaw (toto) |
yes | 34 × 56 | Toto |
Body length (nose to tail): bike 40 dp, auto 42 dp, e-rickshaw 44 dp, car 52 dp, bus 80 dp. VehicleType.bicycle has no art yet and uses motorcycleBlack at 0.85× (≈ 24 × 44 dp).
const VehicleIcon.builtIn(VehicleType.bus); // default bus art, bus size
VehicleIcon.art(VehicleArt.carTaxi); // specific image
VehicleIcon.builtIn(VehicleType.car, art: VehicleArt.carBlue, scale: 1.2);
VehicleArt.setDefault(VehicleType.car, VehicleArt.carTaxi); // app-wide default
The example app's Built-in vehicle gallery screen shows every image at its real marker size.
5. API reference
5.1 VehicleTracker
One tracked vehicle. You own it: call dispose().
Constructors
| Constructor | Purpose |
|---|---|
VehicleTracker({...}) |
Live mode; you push samples. |
VehicleTracker.history(List<LocationSample> samples, {..., double speed = 1, bool autoPlay = true}) |
Replay a recorded trip. Samples without timestamps are ignored. Controlled with playback. |
VehicleTracker.simulated(TrackingRoute route, {..., SimulationProfile profile = const SimulationProfile(), bool loop = true}) |
A simulated vehicle with a realistic noisy GPS feed, for demos and tests. |
Common constructor parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id |
String? |
auto vehicle-N |
Stable id (used in map object ids). |
config |
TrackerConfig? |
TrackerConfig(profile: <icon type>.profile) |
Motion, prediction, matching, validation. See 5.12. |
icon |
VehicleIcon |
VehicleIcon.builtIn(VehicleType.car) |
Marker image. |
route |
TrackingRoute? |
null |
Route to follow (not on simulated, which takes it positionally). |
priority |
VehiclePriority |
primary |
primary gets full rate, route line, stops and camera; secondary gets a reduced rate. |
clock |
TrackingClock |
SystemTrackingClock() |
Time source (inject FakeTrackingClock in tests). |
Members
| Member | Description |
|---|---|
addSample(LocationSample) |
Add one live sample. |
addSamples(Iterable<LocationSample>) |
Add a batch, e.g. a reconnect burst; any order. |
bindStream(Stream<LocationSample>, {bool cancelOnError = false}) |
Subscribe to a stream; replaces any previous binding. Returns the subscription. |
Future<void> setRoute(TrackingRoute) |
Set or replace the route. Routes with more than 2,000 points are indexed off the UI isolate. Throws ArgumentError for fewer than 2 distinct points. |
clearRoute() |
Continue without a route. |
setIcon(VehicleIcon) |
Swap the marker image; the old one stays until the new one is ready. |
pause() / resume() |
Freeze or resume rendering (and history playback). |
reset() |
Clear samples and visual state; keep route and icon (e.g. a new trip). |
dispose() |
Release everything. |
rendered |
ValueListenable<RenderedVehicleState?>, updated per frame while drawn. |
status |
ValueListenable<TrackingStatus>, updated on change. |
events |
Stream<TrackingEvent> (broadcast). |
icon |
ValueListenable<VehicleIcon>. |
route |
Current TrackingRoute?. |
routeChanges |
ValueListenable<int>, bumps on route set / replace / clear. |
playback |
PlaybackController? (history mode). |
simulator, simulationTime |
Simulated mode: the RouteSimulator (ground truth) and elapsed seconds. |
mode, priority, config, id, clock |
As constructed. |
5.2 LocationSample
| Field | Type | Required | Unit / notes |
|---|---|---|---|
position |
GeoPoint |
yes | WGS-84 degrees. LocationSample.latLng(lat, lng, ...) is a shortcut. |
timestamp |
DateTime? |
strongly recommended | Event time at the device (UTC). Without it the receipt time is used and confidence drops. Device clock skew is estimated and corrected automatically. |
heading |
double? |
no | Degrees clockwise from north. Ignored below minSpeedForHeading. Never required: heading is derived from the route or from movement. |
speed |
double? |
no | m/s. Improves stop detection and filtering. |
accuracy |
double? |
no | Metres (68 % radius). Default ValidationConfig.defaultAccuracy (15 m). |
altitude |
double? |
no | Carried through, unused. |
sequence |
int? |
no | Monotonic per source; used for de-duplication. |
id |
String? |
no | Opaque, for your diagnostics. |
Validation.
- Rejected: invalid or NaN coordinates, (0, 0) unless allowed, and accuracy worse than
maxAccuracy. - Dropped: duplicates (same
sequence, or same time and position) and samples older than what is already shown (late). - Physically impossible jumps are rejected, unless 3 consecutive samples agree; then the vehicle relocates (snaps).
- Out-of-order samples newer than the shown time are re-ordered.
5.3 TrackingRoute / RouteStop
| API | Description |
|---|---|
TrackingRoute(List<GeoPoint> points, {String? id, List<RouteStop> stops = const []}) |
At least 2 distinct points. A different id means a different route. |
TrackingRoute.fromEncodedPolyline(String encoded, {int precision = 5, String? id, List<RouteStop> stops}) |
Google encoded polyline (Routes API encodedPolyline; use polylineQuality: HIGH_QUALITY). |
points, stops, id, start, end |
Accessors. |
RouteStop(GeoPoint position, {RouteStopKind kind = waypoint, String? label}) |
RouteStopKind.pickup, .destination, .waypoint. Drawn as markers for the primary tracker. |
Use real road geometry. The vehicle follows the route exactly. A hand-made polyline won't line up with the streets Google draws.
5.4 VehicleIcon
All variants are rasterized once per (icon, size, pixel ratio) and cached. Rotation is a marker property, so images are never rebuilt per frame.
| Constructor | Parameters |
|---|---|
VehicleIcon.builtIn(VehicleType type, {VehicleArt? art, double scale = 1}) |
Bundled image, sized for the type. |
VehicleIcon.art(VehicleArt art, {double scale = 1}) |
A specific bundled image. |
VehicleIcon.asset(String assetName, {String? package, double? width, Offset anchor, double noseHeadingOffset = 0, bool rotates = true}) |
Your PNG asset. width in dp; null means intrinsic size. |
VehicleIcon.bytes(Uint8List bytes, {required double width, Offset anchor, double noseHeadingOffset = 0, bool rotates = true, String? key}) |
Encoded image bytes. Give a stable key when you recreate the bytes. |
VehicleIcon.imageProvider(ImageProvider provider, {required double width, ..., String? key}) |
Any provider, e.g. NetworkImage. Your app controls the network policy. |
VehicleIcon.picture(PictureIconBuilder builder, {required double width, required double height, required String key, ...}) |
A vector ui.Picture (render SVGs with your SVG package). |
| Common parameter | Default | Meaning |
|---|---|---|
anchor |
Offset(0.5, 0.5) |
Rotation pivot / anchor as a fraction of the image. |
noseHeadingOffset |
0 |
Direction the art faces, in degrees clockwise from up (e.g. 90 for east-facing art). |
rotates |
true |
false for pin-style icons (not rotated, not flat). |
Getters: resolvedArt, builtInSize, cacheKey, isBuiltIn, isAsset, isBytes, isProvider, isPicture.
Custom art should be top-down with the nose pointing up (or set noseHeadingOffset), have a transparent background, and have any shadow baked in. Live widgets, animated images and shaders are not possible with map markers.
5.5 VehicleType / VehicleArt
VehicleType member |
Description |
|---|---|
profile |
Default VehicleProfile (kinematic limits). |
arts |
Bundled images of this type. |
defaultArt |
Image used by VehicleIcon.builtIn(type). |
fallbackScale |
Scale used when the type borrows another type's art (bicycle: 0.85). |
VehicleArt member |
Description |
|---|---|
values |
All 12 images (see section 4). |
type, fileName, assetName, logicalSize, displayName |
Metadata. |
VehicleArt.package |
'gmaps_vehicle_tracker', for Image.asset(art.assetName, package: VehicleArt.package). |
VehicleArt.setDefault(VehicleType, VehicleArt?) |
Change the default image of a type app-wide; null restores it. |
5.5b VehicleSelector / VehicleOption / VehicleIconImage
An Uber-style picker that shows each vehicle's image and name. It's purely presentational: keep the selection in your state.
VehicleSelector parameter |
Default | Meaning |
|---|---|---|
options |
required | List<VehicleOption>. |
selectedId |
required | Id of the highlighted option (null = none). |
onSelected |
required | ValueChanged<VehicleOption>, e.g. tracker.setIcon(o.icon). |
layout |
VehicleSelectorLayout.list |
list (image · name/subtitle · trailing) or cards (horizontal). |
imageWidth / imageHeight |
72 / 44 | Image box. |
imageRotation |
90 | Clockwise rotation of the nose-up art (90 = facing right). |
selectedColor |
onSurface |
Border of the selected option. |
padding |
h12 v4 | |
shrinkWrap |
true |
false when placed in an Expanded / sheet. |
physics |
Scroll physics. | |
cardWidth |
108 | Width of each card (cards layout). |
VehicleOption |
Meaning |
|---|---|
VehicleOption({required id, required icon, required title, subtitle, trailing, badge, enabled = true}) |
Any VehicleIcon (built-in or custom). |
VehicleOption.art(VehicleArt art, {id, title, subtitle, trailing, badge, enabled}) |
id defaults to art.name; title defaults to art.displayName (Cab, Taxi, Bike Taxi, Auto, Toto, City Bus, …). |
VehicleIconImage(VehicleIcon icon, {width, height, rotationDegrees = 0, fit = BoxFit.contain}) renders any vehicle icon as a normal Flutter widget.
5.6 TrackingMap
A GoogleMap that draws and animates the trackers. It passes through every GoogleMap option. It does not dispose trackers.
| Parameter | Type | Default | Notes |
|---|---|---|---|
trackers |
List<VehicleTracker> |
required | Rebuild with a new list to add or remove vehicles. |
initialCameraPosition |
CameraPosition |
required | |
camera |
CameraDirector? |
internal, north-up follow | Pass your own to control follow / recenter. |
style |
TrackingStyle |
TrackingStyle() |
Route, stop and vehicle style. |
markers, polylines, circles, polygons, heatmaps, tileOverlays, groundOverlays, clusterManagers |
sets | empty | Your own map objects, merged with the library's. |
padding |
EdgeInsets |
zero | Keep UI clear of the Google logo; camera framing uses the padded area. |
mapId |
String? |
Cloud map styling / advanced markers. | |
mapStyle |
String? |
JSON style, e.g. TrackingMapStyles.clean. |
|
mapType |
MapType |
normal |
|
markerType |
GoogleMapMarkerType |
marker |
For your markers. |
cameraTargetBounds, minMaxZoomPreference |
unbounded | ||
myLocationEnabled |
bool |
false |
|
myLocationButtonEnabled |
bool |
false |
|
trafficEnabled |
bool |
false |
|
buildingsEnabled |
bool |
true |
Set false for a cleaner look. |
indoorViewEnabled, liteModeEnabled |
bool |
false |
|
compassEnabled |
bool |
true |
|
zoomControlsEnabled, mapToolbarEnabled |
bool |
false |
|
rotateGesturesEnabled, scrollGesturesEnabled, zoomGesturesEnabled, tiltGesturesEnabled |
bool |
true |
|
layoutDirection |
TextDirection? |
||
gestureRecognizers |
set | empty | For maps inside scrollables. |
onMapCreated |
MapCreatedCallback? |
Gives you the GoogleMapController. |
|
onTap, onLongPress |
ArgumentCallback<LatLng>? |
||
onCameraMoveStarted, onCameraMove, onCameraIdle |
Also fire for library-driven camera moves. | ||
onLayerCreated |
ValueChanged<TrackingLayer>? |
Advanced. |
5.7 TrackingMap.builder / TrackingMapHooks
Build the GoogleMap yourself for complete control.
TrackingMap.builder(
trackers: [...], camera: camera, style: style, padding: padding,
builder: (context, hooks) => GoogleMap(
markers: hooks.mergeMarkers(myMarkers), // required
onMapCreated: hooks.onMapCreated, // required
onCameraMove: hooks.onCameraMove, // required
...
),
)
TrackingMapHooks |
Description |
|---|---|
mergeMarkers([Set<Marker> own]) |
Your markers plus the library's: stop markers, and vehicles on the plugin path. |
trackerMarkers |
The library markers alone. |
onMapCreated(GoogleMapController) |
Must be wired. |
onCameraMove(CameraPosition) |
Must be wired. |
layer |
The underlying TrackingLayer. |
5.8 TrackingLayer
The engine behind TrackingMap. Use it directly only with a completely custom map widget.
| Member | Description |
|---|---|
TrackingLayer({required TickerProvider vsync, trackers, style, camera, maxPrimaryHz = 60, secondaryHz = 15, preferNativeMarkers = true, clock}) |
|
addTracker / removeTracker / setTrackers |
Manage vehicles. |
attach(GoogleMapController) / detach() |
Bind to the map. |
markers |
ValueListenable<Set<Marker>> to merge into GoogleMap.markers. |
handleCameraMove(CameraPosition) |
Forward from GoogleMap.onCameraMove. |
wrap(Widget map) |
Adds the touch listener that interrupts camera follow. |
viewportSize |
Set from layout (used for camera framing). |
style, camera, primary, trackers |
|
renderPath |
MarkerRenderPath.native (Android fast path), .plugin or .pending. |
primaryHz |
Current adaptive update rate. |
dispose() |
Does not dispose trackers. |
5.9 CameraDirector / CameraFollowConfig
CameraMode |
Behaviour |
|---|---|
free |
The library never moves the camera. |
followNorthUp |
Follows the vehicle, north up. |
followHeadingUp |
Follows the vehicle, map rotated to its heading, tilted. |
overview |
Keeps the vehicle, remaining route and stops in view. Re-fits when needed. |
CameraDirector member |
Description |
|---|---|
CameraDirector({CameraFollowConfig config}) |
|
mode, lastFollowMode, isFollowing, cameraPosition |
State. It's a ChangeNotifier, so you can rebuild on mode changes. |
setMode(CameraMode) |
Switch, with an animated transition. |
recenter() |
Back to the last follow mode. |
showOverview() |
Same as setMode(CameraMode.overview). |
changes |
Stream<CameraModeChange> (from, to, reason: api / userGesture / autoRecenter). |
dispose() |
Touching the map switches to free (if interruptOnGesture).
CameraFollowConfig field |
Default | Meaning |
|---|---|---|
initialMode |
followNorthUp |
Mode when the map appears. |
zoom |
17 |
Follow zoom. |
northUpTilt |
0 |
Degrees. |
headingUpTilt |
45 |
Degrees. |
northUpVehicleY |
0.5 |
Vehicle's vertical screen position (0 = top, 1 = bottom). |
headingUpVehicleY |
0.68 |
Same, in heading-up mode. |
maxUpdateHz |
30 |
Follow camera updates per second. |
headingUpSmoothing |
600 ms |
Map rotation smoothing. |
headingUpMaxTurnRate |
60 |
Degrees per second. |
transitionDuration |
700 ms |
Recenter / mode-switch animation. |
overviewPadding |
72 |
Logical px. |
overviewRefitInterval |
8 s |
Minimum time between overview re-fits. |
interruptOnGesture |
true |
Touch switches to free. |
autoRecenterAfter |
null |
Return to follow after this idle time. |
5.10 TrackingStyle and friends
TrackingStyle({RouteStyle route, StopMarkerStyle stops, VehicleMarkerStyle vehicle}). Presets: TrackingStyle.standard() and TrackingStyle.minimal() (thin line, no casing or completed part).
RouteStyle field |
Default | Meaning |
|---|---|---|
visible |
true |
Draw the route. |
remainingColor |
#1A73E8 |
Colour of the part still to travel. |
remainingWidth |
6 |
Width in screen px. |
casingColor |
#0D47A1 |
Outline colour; null disables it. |
casingWidth |
9 |
|
showCompleted |
true |
Grey out the travelled part. |
completedColor |
#9AA0A6 |
|
completedWidth |
6 |
|
dashed |
false |
Dashed remaining route (no casing). |
dashLength |
18 |
|
gapLength |
12 |
StopMarkerStyle field |
Default | Meaning |
|---|---|---|
visible |
true |
StopMarkerStyle.hidden() hides all stops. |
pickupIcon, destinationIcon, waypointIcon |
null |
BitmapDescriptor overrides. |
pickupHue, destinationHue, waypointHue |
green / red / orange | Default pin hues. |
builder |
null |
Marker? Function(RouteStop stop, Marker defaultMarker). Return a replacement (e.g. defaultMarker.copyWith(...)) or null to hide. |
VehicleMarkerStyle field |
Default | Meaning |
|---|---|---|
zIndex |
10 |
Primary vehicles. |
secondaryZIndex |
9 |
|
secondaryAlpha |
0.85 |
|
consumeTapEvents |
true |
Plugin path only. |
Polyline styling is limited to what google_maps_flutter exposes: solid colour, width, dash pattern, joints, caps. Gradients and per-segment colours are not available.
5.11 TrackingMapStyles
| Constant | Effect |
|---|---|
TrackingMapStyles.clean |
Hides POIs (shops, attractions, schools…), transit, road-shield icons, parcels and neighbourhood labels. Keeps street names and parks. |
TrackingMapStyles.muted |
clean plus desaturated base colours. |
Use these with mapStyle: (or GoogleMap.style:). Combine with buildingsEnabled: false.
5.12 TrackerConfig
Presets:
TrackerConfig.balanced()(default): visual delay about 0.6–3 s..smooth(): about 1–4 s, softest motion..responsive(): about 0.25–1.5 s, closest to real time, relies more on prediction.
Each takes {VehicleProfile profile}. Change fields with copyWith(...).
| Field | Default | |
|---|---|---|
motion |
MotionConfig() |
|
prediction |
PredictionConfig() |
|
matching |
MatchingConfig() |
|
validation |
ValidationConfig() |
|
buffer |
BufferConfig() |
|
profile |
VehicleProfile.car |
Use VehicleType.x.profile. |
processNoiseAcceleration |
1.5 m/s² |
Kalman process noise. |
MotionConfig
| Field | Default |
|---|---|
minRenderDelay / maxRenderDelay / initialRenderDelay |
600 ms / 3 s / 1.2 s |
renderDelayMargin |
250 ms (added to the p90 sample interval) |
correctionDuration / maxCorrectionDuration |
800 ms / 5 s |
maxCatchUpSpeed |
8 m/s (extra speed allowed while blending out an error) |
teleportThreshold |
250 m (larger errors snap) |
fadeOnTeleport |
true |
backwardCreepSpeed |
0.6 m/s (absorbs overshoot without visible reversing) |
headingSmoothing |
250 ms |
maxTurnRate |
180 °/s |
headingLookaheadTime / minHeadingLookahead / maxHeadingLookahead |
800 ms / 5 m / 25 m |
PredictionConfig
| Field | Default |
|---|---|
maxDuration / maxDistance / speedHalfLife (off route) |
3 s / 60 m / 1.5 s |
onRouteMaxDuration / onRouteMaxDistance / onRouteSpeedHalfLife |
8 s / 150 m / 5 s |
predictingAfter |
1 s (shorter extrapolation still counts as tracking) |
staleAfter / lostAfter |
10 s / 60 s |
MatchingConfig
| Field | Default |
|---|---|
minSearchRadius / maxSearchRadius / searchRadiusAccuracyFactor |
25 m / 150 m / 3 |
offRouteMinDistance / offRouteAccuracyFactor |
40 m / 2.5 (off-route distance = max(min, factor × accuracy)) |
offRouteConfirmSamples / offRouteConfirmDuration |
3 / 5 s (both required) |
rejoinConfirmSamples |
2 |
minBacktrack |
20 m |
headingWeight / progressWeight / backwardWeight |
4 / 1 / 2 |
ValidationConfig
| Field | Default |
|---|---|
defaultAccuracy / maxAccuracy |
15 m / 100 m |
allowNullIsland |
false |
futureTolerance |
5 s |
outlierSpeedFactor / relocationConfirmSamples |
1.5 / 3 |
minSpeedForHeading |
2 m/s |
stationarySpeed / stationaryReleaseSpeed / stationaryDwell |
0.8 m/s / 1.5 m/s / 2 s |
clockOffsetWindow |
2 min |
BufferConfig: maxSamples 512, maxAge 10 min (live mode; history keeps all).
VehicleProfile (maxSpeed m/s, maxAcceleration m/s²):
| Profile | maxSpeed | maxAcceleration |
|---|---|---|
bicycle |
12 | 1.5 |
motorcycle |
40 | 4 |
autoRickshaw |
20 | 2 |
eRickshaw |
12 | 1.5 |
car |
55 | 4 |
bus |
30 | 1.5 |
Custom profiles: VehicleProfile(maxSpeed: .., maxAcceleration: ..).
5.13 TrackingStatus, phases, diagnostics
TrackingStatus field |
Description |
|---|---|
phase |
TrackingPhase (below). |
routeStatus |
RouteStatus: noRoute, onRoute, deviating, offRoute, rejoining. |
freshness |
Age of the newest real sample (clock-offset corrected). |
confidence |
0..1, indicative (freshness × accuracy × match). |
lastAuthoritativeSample |
The newest real sample (never a predicted one). |
renderDelay |
Current visual latency. |
diagnostics |
TrackingDiagnostics: accepted, rejected (per RejectionReason), rejectedTotal, relocations, reordered. |
isStale |
phase is stale or lost. Show "location delayed". |
TrackingPhase |
Meaning |
|---|---|
idle |
Not started. |
awaitingFirstFix |
No valid sample yet. |
tracking |
Fresh data. |
predicting |
Briefly extrapolating past the newest sample. |
stale |
Overdue; prediction exhausted; vehicle held. |
lost |
No data for lostAfter. |
paused |
Paused by the app. |
ended |
History playback finished. |
RejectionReason: invalidCoordinate, nullIsland, lowAccuracy, duplicate, late, implausibleJump.
5.14 Events
tracker.events emits a sealed TrackingEvent:
| Event | Fields | When |
|---|---|---|
PhaseChanged |
previous, current |
Phase changes. |
RouteStatusChanged |
previous, current, isOffRoute |
Route relation changes. Request a new route when isOffRoute. |
SampleRejected |
sample, reason |
A sample was rejected. |
VehicleTeleported |
— | The vehicle snapped (relocation, huge correction, seek). |
PlaybackEnded |
— | History reached its end. |
SourceError |
error, stackTrace |
Your bound stream errored. |
VehicleTapped |
— | Marker tapped (plugin path only; not on the Android native path). |
5.15 RenderedVehicleState
position, heading, speed, mode (interpolating / extrapolating / holding), renderTime, progress (m along route), routeGeneration, onRoute, alpha, correcting, snapped, predictionExhausted.
It's a visual estimate only. Don't store it or upload it as the real location.
5.16 PlaybackController (history)
play(), pause(), seek(Duration), setSpeed(double) (0.25–64×), isPlaying, speed, position, duration, positionSeconds. It's a ChangeNotifier, so you can drive UI with ListenableBuilder.
5.17 Simulation
SimulationProfile (all optional):
| Field | Default |
|---|---|
cruiseSpeed |
11 m/s |
acceleration |
1.5 m/s² |
cornerSlowdown |
true |
stops |
[SimulatedStop(distanceMeters, dwell)] |
startDwell |
— |
sampleInterval |
2 s |
intervalJitter |
0 (0..1) |
gpsNoise |
4 m |
accuracy |
8 m |
dropoutProbability |
0 |
duplicateProbability |
0 |
latency / latencyJitter |
250 ms / 150 ms |
includeHeading / includeSpeed |
true / true |
clockSkew |
0 |
seed |
42 |
RouteSimulator(RouteIndex, SimulationProfile, {DateTime? startTime}) exposes duration, truthAt(t) (ground truth) and deliveries() (samples with delivery times), for your own tests.
5.18 Utilities and pure-Dart core
- Geo:
GeoPoint,distanceMeters,initialBearing,destinationPoint,shortestAngleDelta,normalizeDegrees,LocalFrame. - Polyline:
PolylineCodec.decode/encode(precision 1–7). - Route:
RouteIndex.build(points)giveslength,pointAt(s),smoothedBearing(s),candidates(...),subPath(s0, s1). - Conversions:
GeoPoint.toLatLng()andLatLng.toGeoPoint(). - Icons:
MarkerIconFactory.instance(resolve,resolvePng,clear). - Engine:
import 'package:gmaps_vehicle_tracker/core.dart'gives the engine (TrackingEngine) without Flutter or Google Maps, for tests or server-side replays.
6. How the motion works
- Validate and align time. Samples are checked, de-duplicated and re-ordered. Device clock skew and network delay are estimated (minimum-delay window) so data age is accurate.
- Match to route. Candidates near the sample are scored by distance, heading agreement, progress continuity and backward motion, within a window around the previous progress. This handles loops, parallel roads and hairpins. Leaving the route needs confirmation (3 samples and 5 s), after which the vehicle is shown at its true position.
- Filter. A constant-velocity Kalman filter runs along the route, or in 2-D off route. Stop detection uses reported speed or a regression over recent fixes, so GPS jitter at pickup doesn't move the marker.
- Render. At
now − renderDelay, progress is interpolated with monotone cubic Hermite, so it never reverses or overshoots. Heading is the route tangent with a speed-scaled look-ahead, smoothed with a turn-rate cap. - Predict. Past the newest sample, speed decays: up to 8 s / 150 m on route, 3 s / 60 m off route. The vehicle then holds and the phase becomes
stale. - Correct. New data that disagrees with what is shown is blended out with a speed cap. Very large errors snap with a short fade.
7. Rendering and performance
- Vehicle motion never rebuilds the
GoogleMapwidget. - Android: a small native component creates vehicle markers directly on the plugin's native map and only moves or rotates them. That's one platform message per frame for all vehicles, with no icon re-upload, which avoids the plugin's per-update icon re-decode that causes flicker.
- Other platforms: plugin markers are updated directly through the platform interface.
- Updates are skipped when the movement is under a third of a pixel. Primary vehicles update at up to 60 Hz, adapting down to 30 / 20 Hz if frames get slow; secondary vehicles at 15 Hz.
- Route polylines are sent only when the route changes, plus the completed part at ≤ 2 Hz.
- The ticker sleeps when nothing moves and stops in the background.
- Measured on an Android 16 phone with 8 moving vehicles and camera follow: p50 5 ms, p90 9 ms frame time, 2 % janky frames.
8. Limitations
- Road shape: the route must come from a router. Without a route, motion between samples is unconstrained.
- Polylines: no gradients or per-segment colours (plugin limitation).
- Markers: no live widgets or animated images; SVG must be rasterized.
- Taps: vehicle tap events aren't available on the Android native path.
- Bicycle art: not bundled yet; the motorcycle image is used at 0.85×.
- iOS: not yet device-verified; it uses the plugin marker path, which may flicker under frequent updates.
9. Costs, terms, privacy
- No billable calls from the library. Mobile map loads (Maps SDK) are free and unlimited. Routes API calls made by your app are billed: Essentials 10k/month free; traffic-aware routing is Pro; two-wheeler routing is Enterprise.
- Attribution: don't cover the Google logo; use
padding. - Caching: Google's terms limit how long route coordinates may be cached. Fetch routes when needed rather than shipping them.
- Privacy: all data stays in memory in bounded buffers. Nothing is persisted or transmitted.
reset()anddispose()clear everything.
10. Adding or replacing built-in art
Art lives in assets/raw_images/ (sources) and assets/vehicles/ (generated 1x–4x PNGs).
- Put a top-down source image in
assets/raw_images/. Any orientation works. It can have real transparency, a baked-in checkerboard, or a solid black background. - Add a line to the table in
tool/export_icons/export_all.dart:_Art('bicycle_red', '$_raw/My Bicycle.png', 34, 'checkerboard'), // body length dp, background mode - Run
dart run tool/export_icons/export_all.dart(or pass names to export only those). This writes the PNGs and regenerateslib/src/flutter/icons/vehicle_art_sizes.g.dart. - Add the enum value in
VehicleArt(lib/src/flutter/icons/vehicle_icon.dart), e.g.bicycleRed('bicycle_red', VehicleType.bicycle), and update the type's default if wanted. - Run
flutter test(test/flutter/vehicle_art_test.dartchecks files, sizes and proportions).tool/export_icons/contact_sheet.dartrenders a QA sheet.
11. Troubleshooting
| Symptom | Fix |
|---|---|
| Car drives over buildings | Your route geometry isn't road geometry. Use a router's high-quality polyline. |
| Vehicle lags behind | Use TrackerConfig.responsive(), or send samples more often. |
| Vehicle stops and jumps | Samples are too sparse for the prediction limits. Raise PredictionConfig.onRouteMaxDuration, or send samples more often. |
| "Location delayed" too early or late | Tune PredictionConfig.staleAfter / lostAfter. |
| Marker jitters at stops | Send speed and accuracy with samples. |
| Camera fights the user | It shouldn't; touch switches to free. Make sure the map is wrapped (TrackingMap does it; with TrackingLayer use layer.wrap). |
| Marker blinking on iOS | Lower TrackingLayer.maxPrimaryHz (native iOS path pending). |
12. Development
flutter test: unit tests plus scenario replays (noisy, irregular, delayed, reordered, duplicated and dropped GPS against ground truth, with road-adherence, smoothness, reversing and stop-jitter metrics).example/: Live tracking, History replay, Fleet & full map control, Built-in vehicle gallery. Run it with:flutter run --dart-define-from-file=secrets.jsonsecrets.jsonis{"MAPS_API_KEY": "..."}and is not committed. It enables real road routes from the Routes API; without it the demo uses an approximate offline route.tool/: icon exporter, contact sheet, image inspection (dev only; excluded by.pubignore).
Libraries
- core
- Provider-neutral tracking core: models, engine and simulation.
- gmaps_vehicle_tracker
- Road-aware, smooth vehicle tracking animation on Google Maps.