start static method
Future<NavigationSession>
start({
- required GoogleMapController controller,
- required NavigationOptions options,
- String polylineId = 'gmns_nav_route',
- Color polylineColor = const Color(0xFF1976D2),
Implementation
static Future<NavigationSession> start({
required GoogleMapController controller,
required NavigationOptions options,
String polylineId = 'gmns_nav_route',
Color polylineColor = const Color(0xFF1976D2),
}) async {
// Fetch route (Directions classic ou Routes v2)
final route = await _fetchNavigationRoute(options);
// Draw polyline & fit bounds
final Color routeClr = options.routeColor ?? polylineColor;
await controller.addPolyline(PolylineOptions(id: polylineId, points: route.points, color: routeClr, width: options.routeWidth));
await controller.animateToBounds(route.northeast, route.southwest, padding: 60);
// Prepare TTS / Audio
FlutterTts? tts;
if (options.voiceGuidance) {
tts = FlutterTts();
try {
await tts.setLanguage(options.language);
await tts.setSpeechRate(options.ttsRate);
await tts.setPitch(options.ttsPitch);
if (options.ttsVoice != null) {
try {
await tts.setVoice({'name': options.ttsVoice!});
} catch (_) {}
}
await tts.awaitSpeakCompletion(true);
// iOS ducking
if (options.iosDuckOthers) {
try { await (tts as dynamic).setIosAudioCategory('playback', ['duckOthers']); } catch (_) {}
}
} catch (_) {}
// Android AudioFocus
if (defaultTargetPlatform == TargetPlatform.android && options.androidRequestAudioFocus) {
try { await AudioFocus.request(); } catch (_) {}
}
}
// Ensure permissions are handled by the host app
final locPerm = await Geolocator.checkPermission();
if (locPerm == LocationPermission.denied || locPerm == LocationPermission.deniedForever) {
// We don't request here to keep plugin passive; consumer app must request beforehand.
}
DateTime lastReroute = DateTime.fromMillisecondsSinceEpoch(0);
final stateCtl = StreamController<NavState>.broadcast();
final instCtl = StreamController<NavInstruction>.broadcast();
final progressCtl = StreamController<NavProgress>.broadcast();
final speedCtl = StreamController<SpeedAlert>.broadcast();
stateCtl.add(NavState.navigating);
// Precompute route length cumulatives for progress
final cumul = _cumulativeDistances(route.points);
final totalMeters = cumul.isNotEmpty ? cumul.last : 0.0;
// Estado compartilhado runtime
final shared = _NavShared();
// Device heading (compass/rotation)
double? latestHeading;
StreamSubscription<double>? headingSub;
_AngleSmoother? headingSmoother = options.headingFilterAlpha > 0 ? _AngleSmoother(options.headingFilterAlpha) : null;
if (options.followBearing && options.useDeviceHeading) {
try {
headingSub = DeviceHeading.stream.listen((h) { latestHeading = h; });
} catch (_) {}
}
// Fala por aproximação em múltiplos limiares
final Map<int, Set<double>> spokenByStep = {};
// Auxiliares de limite de velocidade dinâmico
double? dynSpeedLimit;
DateTime lastSpeedFetch = DateTime.fromMillisecondsSinceEpoch(0);
bool vehicleMarkerAdded = false;
// Função para processar uma atualização de posição (GPS ou simulação)
Future<void> onPosition(LatLng user, {double speedKmh = 0.0, double? course}) async {
if (shared.closed || shared.paused) return;
// Snapping e bearing
LatLng camTarget = user;
double? camBearing;
double? routeBearing;
if (options.snapToRoute && route.points.length >= 2) {
final np = _nearestPointOnPolyline(user, route.points);
camTarget = np.point;
final a = route.points[np.segIndex];
final b = route.points[np.segIndex + 1];
routeBearing = _normalizeBearing(_bearingDegrees(a, b));
}
// Determine camera bearing
if (options.followBearing) {
final speed = speedKmh.isFinite ? speedKmh : 0.0;
double? candidate;
if (speed >= options.lowSpeedAutoRotateKmh) {
// Moving: prefer course from location; fallback to route bearing
candidate = _normalizeBearing(course) ?? routeBearing;
} else {
// Low speed / stationary: prefer device heading; fallback to route bearing
candidate = _normalizeBearing(latestHeading) ?? routeBearing;
}
if (candidate != null) {
if (headingSmoother != null) {
camBearing = headingSmoother.update(candidate);
} else {
camBearing = candidate;
}
}
}
// Vehicle marker: add/update if enabled
if (options.showVehicleMarker) {
final markerPos = options.snapToRoute ? camTarget : user;
if (!vehicleMarkerAdded) {
try {
await controller.addMarker(MarkerOptions(
id: options.vehicleMarkerId,
position: markerPos,
iconUrl: options.vehicleIconUrl,
iconDp: options.vehicleIconDp,
anchorU: options.vehicleIconAnchorU,
anchorV: options.vehicleIconAnchorV,
rotation: (options.vehicleRotationSource == VehicleRotationSource.deviceHeading)
? (camBearing ?? 0)
: (options.vehicleRotationSource == VehicleRotationSource.route
? (routeBearing ?? camBearing ?? 0)
: (_normalizeBearing(course) ?? routeBearing ?? camBearing ?? 0)),
zIndex: 9999,
));
vehicleMarkerAdded = true;
} catch (_) {}
} else {
double? rot;
switch (options.vehicleRotationSource) {
case VehicleRotationSource.course:
rot = _normalizeBearing(course) ?? routeBearing ?? camBearing;
break;
case VehicleRotationSource.route:
rot = routeBearing ?? _normalizeBearing(course) ?? camBearing;
break;
case VehicleRotationSource.deviceHeading:
rot = camBearing ?? routeBearing ?? _normalizeBearing(course);
break;
}
try {
await controller.updateMarker(options.vehicleMarkerId, position: markerPos, rotation: rot);
} catch (_) {}
}
}
await controller.animateCamera(
camTarget,
zoom: options.cameraZoom,
tilt: options.cameraTilt,
bearing: options.followBearing ? camBearing : null,
);
// Velocidade/alertas (opcional provider dinâmico)
if (options.speedAlertsEnabled) {
final now = DateTime.now();
if (options.speedLimitProvider != null && now.difference(lastSpeedFetch) > const Duration(seconds: 20)) {
lastSpeedFetch = now;
try { dynSpeedLimit = await options.speedLimitProvider!(user); } catch (_) {}
}
final limit = dynSpeedLimit ?? options.speedLimitKmh;
if (limit != null && speedKmh > limit + 2.0) {
speedCtl.add(SpeedAlert(speedKmh: speedKmh, speedLimitKmh: limit, overLimit: true));
}
}
// Progresso/ETA
if (totalMeters > 0) {
final traveled = _traveledAlongRoute(camTarget, route.points, cumul);
final remaining = (totalMeters - traveled).clamp(0.0, totalMeters);
double? etaSeconds;
if (route.durationSeconds != null && totalMeters > 1) {
final frac = remaining / totalMeters;
etaSeconds = (route.durationSeconds! * frac).toDouble();
} else if (speedKmh > 1) {
etaSeconds = (remaining / (speedKmh / 3.6));
}
progressCtl.add(NavProgress(distanceRemainingMeters: remaining, etaSeconds: etaSeconds));
}
// Chegada
final dDest = _distanceMeters(camTarget, options.destination);
if (dDest <= options.arrivalThresholdMeters) {
stateCtl.add(NavState.arrived);
if (options.voiceGuidance && tts != null) {
final txt = options.language.startsWith('pt') ? 'Você chegou ao destino' : 'You have arrived at your destination';
try { await tts.speak(txt); } catch (_) {}
}
return;
}
// Instruções por passo (aproximação em múltiplos limiares)
if (options.voiceGuidance && route.steps.isNotEmpty && tts != null) {
// Passo mais próximo adiante
int idx = _closestUpcomingStepIndex(camTarget, route.steps) ?? 0;
final step = route.steps[idx];
final d = _distanceMeters(camTarget, step.startLocation);
final spokenSet = spokenByStep.putIfAbsent(idx, () => <double>{});
for (final th in options.approachSpeakMeters) {
if (!spokenSet.contains(th) && d <= th) {
spokenSet.add(th);
final text = _instructionVoiceText(step, options.language);
try {
if (options.interruptOnNewInstruction) { try { await tts.stop(); } catch (_) {} }
await tts.speak(text);
} catch (_) {}
instCtl.add(NavInstruction(stepIndex: idx, text: step.instructionText, maneuver: step.maneuver, distanceMeters: d.round()));
break;
}
}
}
// Off-route e Reroute
final off = _distanceToPolylineMeters(camTarget, route.points);
if (options.rerouteOnOffRoute && off > options.offRouteThresholdMeters) {
stateCtl.add(NavState.offRoute);
final now = DateTime.now();
if (now.difference(lastReroute) >= options.minTimeBetweenReroutes) {
lastReroute = now;
stateCtl.add(NavState.rerouting);
try {
if (options.useRoutesV2) {
final res = await RoutesApi.computeRoutes(
apiKey: options.apiKey,
origin: Waypoint(location: camTarget),
destination: Waypoint(location: options.destination),
intermediates: options.intermediates,
languageCode: options.language,
alternatives: false,
);
if (res.routes.isNotEmpty) {
final r = res.routes.first;
final newSteps = <DirectionStep>[];
for (final leg in r.legs) {
for (final st in leg.steps) {
LatLng? s;
LatLng? e;
if (st.points.isNotEmpty) { s = st.points.first; e = st.points.last; }
newSteps.add(DirectionStep(
startLocation: s ?? camTarget,
endLocation: e ?? options.destination,
distanceMeters: st.distanceMeters ?? 0,
instructionHtml: st.instruction ?? '',
maneuver: st.maneuver,
));
}
}
await controller.addPolyline(PolylineOptions(id: polylineId, points: r.points, color: routeClr, width: options.routeWidth));
route.points..clear()..addAll(r.points);
route.steps..clear()..addAll(newSteps);
cumul..clear()..addAll(_cumulativeDistances(route.points));
spokenByStep.clear();
}
} else {
final newRoute = await DirectionsService.fetchRoute(
apiKey: options.apiKey,
origin: camTarget,
destination: options.destination,
mode: options.mode,
language: options.language,
);
await controller.addPolyline(PolylineOptions(id: polylineId, points: newRoute.points, color: routeClr, width: options.routeWidth));
route.points..clear()..addAll(newRoute.points);
cumul..clear()..addAll(_cumulativeDistances(route.points));
spokenByStep.clear();
}
stateCtl.add(NavState.navigating);
} catch (_) {
stateCtl.add(NavState.navigating);
}
}
}
}
StreamSubscription<Position>? sub;
if (options.simulationSpeedKmh != null && totalMeters > 0) {
// Simulação baseada no polyline
double progress = 0.0; // metros ao longo da rota
shared.simSpeedKmh = options.simulationSpeedKmh!.clamp(1.0, 200.0);
shared.simTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
if (shared.paused || shared.closed) return;
final spd = shared.simSpeedKmh.clamp(1.0, 200.0);
progress = (progress + spd / 3.6).clamp(0.0, totalMeters);
final interp = _interpolateAlongRoute(route.points, cumul, progress);
await onPosition(interp.position, speedKmh: spd, course: interp.bearing);
});
} else {
sub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(accuracy: LocationAccuracy.best, distanceFilter: 2),
).listen((pos) async {
final user = LatLng(pos.latitude, pos.longitude);
final speedKmh = (pos.speed.isFinite ? pos.speed : 0.0) * 3.6;
final course = pos.heading.isFinite ? pos.heading : _bearingFromVelocity(pos);
await onPosition(user, speedKmh: speedKmh, course: course);
});
}
final sess = NavigationSession._(controller, options, route, sub, tts, polylineId, shared, stateCtl, instCtl, progressCtl, speedCtl);
sess._headingSub = headingSub;
return sess;
}