import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:intl/intl.dart'; import '../location_service.dart'; import '../map/map_provider_interface.dart'; import '../routes/route_models.dart'; import '../routes/route_provider.dart'; /// Navigation session status lifecycle. enum NavigationStatus { idle, navigating, paused, rerouting, arrived, } /// State of an active turn-by-turn navigation session. @immutable class NavigationSessionState { final NavigationStatus status; final MotorSignalRoute? route; final ResolvedPlace? destination; final RouteCalculationResult? calculationResult; final List maneuvers; final int currentManeuverIndex; final double distanceToNextManeuverMeters; final double remainingDistanceMeters; final int remainingDurationSeconds; final bool ttsEnabled; final String? lastAnnouncedInstruction; final RoutingOptions routingOptions; final DateTime? startedAt; const NavigationSessionState({ this.status = NavigationStatus.idle, this.route, this.destination, this.calculationResult, this.maneuvers = const [], this.currentManeuverIndex = 0, this.distanceToNextManeuverMeters = 0.0, this.remainingDistanceMeters = 0.0, this.remainingDurationSeconds = 0, this.ttsEnabled = true, this.lastAnnouncedInstruction, this.routingOptions = const RoutingOptions(), this.startedAt, }); bool get isNavigating => status == NavigationStatus.navigating; bool get hasActiveRoute => maneuvers.isNotEmpty; RouteManeuver? get currentManeuver => currentManeuverIndex < maneuvers.length ? maneuvers[currentManeuverIndex] : null; RouteManeuver? get nextManeuver => (currentManeuverIndex + 1) < maneuvers.length ? maneuvers[currentManeuverIndex + 1] : null; double get distanceToNextMiles => distanceToNextManeuverMeters * 0.000621371; double get remainingDistanceMiles => remainingDistanceMeters * 0.000621371; String get formattedDistanceToNext { if (distanceToNextMiles < 0.1) { final feet = (distanceToNextManeuverMeters * 3.28084).round(); return '$feet ft'; } return '${distanceToNextMiles.toStringAsFixed(1)} mi'; } String get formattedRemainingDistance { return '${remainingDistanceMiles.toStringAsFixed(1)} mi'; } String get formattedEta { if (remainingDurationSeconds <= 0) return '--:--'; final arrival = DateTime.now().add(Duration(seconds: remainingDurationSeconds)); return DateFormat('h:mm a').format(arrival); } String get formattedDurationRemaining { if (remainingDurationSeconds <= 0) return '0 min'; final totalSec = remainingDurationSeconds; final hours = totalSec ~/ 3600; final minutes = (totalSec % 3600) ~/ 60; if (hours > 0) { return '${hours}h ${minutes}m'; } return '${minutes.clamp(1, 60)} min'; } NavigationSessionState copyWith({ NavigationStatus? status, MotorSignalRoute? route, ResolvedPlace? destination, RouteCalculationResult? calculationResult, List? maneuvers, int? currentManeuverIndex, double? distanceToNextManeuverMeters, double? remainingDistanceMeters, int? remainingDurationSeconds, bool? ttsEnabled, String? lastAnnouncedInstruction, RoutingOptions? routingOptions, DateTime? startedAt, }) { return NavigationSessionState( status: status ?? this.status, route: route ?? this.route, destination: destination ?? this.destination, calculationResult: calculationResult ?? this.calculationResult, maneuvers: maneuvers ?? this.maneuvers, currentManeuverIndex: currentManeuverIndex ?? this.currentManeuverIndex, distanceToNextManeuverMeters: distanceToNextManeuverMeters ?? this.distanceToNextManeuverMeters, remainingDistanceMeters: remainingDistanceMeters ?? this.remainingDistanceMeters, remainingDurationSeconds: remainingDurationSeconds ?? this.remainingDurationSeconds, ttsEnabled: ttsEnabled ?? this.ttsEnabled, lastAnnouncedInstruction: lastAnnouncedInstruction ?? this.lastAnnouncedInstruction, routingOptions: routingOptions ?? this.routingOptions, startedAt: startedAt ?? this.startedAt, ); } } /// Controller managing live motorcycle turn-by-turn navigation session. class NavigationSessionNotifier extends StateNotifier { final FlutterTts? _tts; bool _ttsInitialized = false; NavigationSessionNotifier({FlutterTts? tts}) : _tts = tts ?? _createTtsSafe(), super(const NavigationSessionState()) { _initTts(); } static FlutterTts? _createTtsSafe() { try { return FlutterTts(); } catch (_) { return null; } } bool get isNavigating => state.isNavigating; Future _initTts() async { if (_tts == null) return; try { await _tts.setLanguage('en-US'); await _tts.setSpeechRate(0.5); // Clear motorcycle cadence await _tts.setVolume(1.0); await _tts.setPitch(1.0); _ttsInitialized = true; } catch (_) { _ttsInitialized = false; } } /// Begins turn-by-turn guidance along a calculated route. Future startNavigation({ required RouteCalculationResult calculationResult, ResolvedPlace? destination, MotorSignalRoute? route, RoutingOptions? options, }) async { final maneuvers = calculationResult.maneuvers; if (maneuvers.isEmpty) { return; } final initialDist = calculationResult.estimatedDistanceMeters ?? calculationResult.straightLineDistanceMeters; final initialDuration = calculationResult.estimatedDurationSeconds ?? (initialDist / 15.0).round(); // Default ~33 mph assumption if null final firstManeuverDist = maneuvers.first.distanceMeters > 0 ? maneuvers.first.distanceMeters : (maneuvers.length > 1 ? maneuvers[1].distanceMeters : initialDist); state = NavigationSessionState( status: NavigationStatus.navigating, route: route, destination: destination, calculationResult: calculationResult, maneuvers: maneuvers, currentManeuverIndex: 0, distanceToNextManeuverMeters: firstManeuverDist, remainingDistanceMeters: initialDist, remainingDurationSeconds: initialDuration, ttsEnabled: state.ttsEnabled, routingOptions: options ?? const RoutingOptions(), startedAt: DateTime.now(), ); // Speak initial maneuver final firstInstruction = maneuvers.first.instruction; _speakInstruction(firstInstruction); } /// Halts and clears active navigation. Future stopNavigation() async { if (_tts != null && _ttsInitialized) { try { await _tts.stop(); } catch (_) {} } state = const NavigationSessionState(); } /// Pauses turn-by-turn tracking (e.g. during a rest stop). void pauseNavigation() { if (state.isNavigating) { state = state.copyWith(status: NavigationStatus.paused); } } /// Resumes paused navigation. void resumeNavigation() { if (state.status == NavigationStatus.paused) { state = state.copyWith(status: NavigationStatus.navigating); final current = state.currentManeuver; if (current != null) { _speakInstruction('Resuming route. ${current.instruction}'); } } } /// Toggles spoken voice guidance. void toggleTts() { final newEnabled = !state.ttsEnabled; state = state.copyWith(ttsEnabled: newEnabled); if (!newEnabled && _tts != null && _ttsInitialized) { try { _tts.stop(); } catch (_) {} } } /// Updates navigation progress against rider's current location and speed. void updateLocation(AppLatLng riderPos, {double speedMph = 0.0}) { if (!state.isNavigating || state.maneuvers.isEmpty) return; final currIdx = state.currentManeuverIndex; if (currIdx >= state.maneuvers.length) { _completeArrival(); return; } final currentManeuver = state.maneuvers[currIdx]; final distToManeuver = _haversineMeters(riderPos, currentManeuver.location); // If within 25 meters of maneuver point, advance to next maneuver if (distToManeuver <= 25.0 && currIdx < state.maneuvers.length - 1) { final nextIdx = currIdx + 1; final nextManeuver = state.maneuvers[nextIdx]; final distToNext = _haversineMeters(riderPos, nextManeuver.location); state = state.copyWith( currentManeuverIndex: nextIdx, distanceToNextManeuverMeters: distToNext, ); _speakInstruction(nextManeuver.instruction); return; } // Recompute remaining distance along maneuvers double remainingMeters = distToManeuver; for (int i = currIdx + 1; i < state.maneuvers.length; i++) { remainingMeters += state.maneuvers[i].distanceMeters; } // Dynamic duration based on speed (or default 30 mph if stationary) final effectiveSpeedMps = speedMph > 5.0 ? (speedMph * 0.44704) : 13.4; final remainingSec = (remainingMeters / effectiveSpeedMps).round(); state = state.copyWith( distanceToNextManeuverMeters: distToManeuver, remainingDistanceMeters: remainingMeters, remainingDurationSeconds: remainingSec, ); } void _completeArrival() { state = state.copyWith( status: NavigationStatus.arrived, remainingDistanceMeters: 0.0, remainingDurationSeconds: 0, distanceToNextManeuverMeters: 0.0, ); _speakInstruction('You have arrived at your destination.'); } Future _speakInstruction(String instruction) async { if (!state.ttsEnabled || _tts == null || !_ttsInitialized) return; if (state.lastAnnouncedInstruction == instruction) return; state = state.copyWith(lastAnnouncedInstruction: instruction); try { await _tts.speak(instruction); } catch (_) {} } static double _haversineMeters(AppLatLng p1, AppLatLng p2) { const r = 6371000.0; // Earth radius in meters final dLat = (p2.latitude - p1.latitude) * (math.pi / 180.0); final dLon = (p2.longitude - p1.longitude) * (math.pi / 180.0); final lat1 = p1.latitude * (math.pi / 180.0); final lat2 = p2.latitude * (math.pi / 180.0); final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.sin(dLon / 2) * math.sin(dLon / 2) * math.cos(lat1) * math.cos(lat2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return r * c; } } /// Global shared navigation session provider across Map and Dashboard. final navigationSessionProvider = StateNotifierProvider( (ref) { final notifier = NavigationSessionNotifier(); // Connect live location updates when navigating ref.listen(locationStreamProvider, (_, next) { final pos = next.value; if (pos != null && notifier.isNavigating) { notifier.updateLocation( AppLatLng(pos.latitude, pos.longitude), speedMph: pos.speed * 2.23694, ); } }); return notifier; }, );