import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/theme/theme_extensions.dart'; import 'route_models.dart'; /// Target navigation application for handoff. enum NavigationApp { googleMaps, waze, platformDefault; String get label { switch (this) { case NavigationApp.googleMaps: return 'Google Maps'; case NavigationApp.waze: return 'Waze'; case NavigationApp.platformDefault: return 'System Map'; } } } /// Service handling truthful handoff to external navigation applications. /// MotorSignal delegates road-following turn-by-turn navigation to trusted /// navigation apps without claiming internal turn-by-turn guidance. class NavigationHandoffService { const NavigationHandoffService(); /// Builds a standard Google Maps turn-by-turn navigation URI. static Uri buildGoogleMapsUri({ required double latitude, required double longitude, String? displayName, }) { final dest = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; return Uri.parse( 'https://www.google.com/maps/dir/?api=1&destination=$dest', ); } /// Builds a standard Waze navigation URI. static Uri buildWazeUri({ required double latitude, required double longitude, }) { final ll = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; return Uri.parse('https://waze.com/ul?ll=$ll&navigate=yes'); } /// Builds a standard RFC 5870 system geo: navigation URI. static Uri buildSystemGeoUri({ required double latitude, required double longitude, required String displayName, }) { final coords = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; return Uri.parse('geo:$coords?q=$coords($displayName)'); } /// Displays an interactive navigation handoff bottom sheet with direct links /// to Google Maps, Waze, and native system mapping. static void showHandoffSheet({ required BuildContext context, required double latitude, required double longitude, required String displayName, }) { final colors = context.motorColors; showModalBottomSheet( context: context, backgroundColor: colors.surfaceContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (sheetContext) { return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon( Icons.navigation, color: colors.cyberCyan, size: 22, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'NAVIGATION HANDOFF', style: TextStyle( fontSize: 11, letterSpacing: 1.2, fontWeight: FontWeight.w700, color: colors.cyberCyan, ), ), const SizedBox(height: 2), Text( displayName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), const SizedBox(height: 20), ListTile( key: const ValueKey('handoff_google_maps_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.map, color: Colors.white), ), title: const Text( 'Google Maps', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), subtitle: const Text( 'Open turn-by-turn in Google Maps', style: TextStyle(color: Colors.white54, fontSize: 12), ), trailing: const Icon( Icons.open_in_new, color: Colors.white38, size: 18, ), onTap: () async { Navigator.pop(sheetContext); final uri = buildGoogleMapsUri( latitude: latitude, longitude: longitude, displayName: displayName, ); if (await canLaunchUrl(uri)) { await launchUrl( uri, mode: LaunchMode.externalApplication, ); } }, ), const Divider(color: Colors.white12, height: 1), ListTile( key: const ValueKey('handoff_waze_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.directions_car, color: colors.cyberCyan, ), ), title: const Text( 'Waze', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), subtitle: const Text( 'Open road directions in Waze', style: TextStyle(color: Colors.white54, fontSize: 12), ), trailing: const Icon( Icons.open_in_new, color: Colors.white38, size: 18, ), onTap: () async { Navigator.pop(sheetContext); final uri = buildWazeUri( latitude: latitude, longitude: longitude, ); if (await canLaunchUrl(uri)) { await launchUrl( uri, mode: LaunchMode.externalApplication, ); } }, ), const Divider(color: Colors.white12, height: 1), ListTile( key: const ValueKey('handoff_system_map_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.place, color: Colors.white70), ), title: const Text( 'System Map', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), subtitle: const Text( 'Open in default platform mapping app', style: TextStyle(color: Colors.white54, fontSize: 12), ), trailing: const Icon( Icons.open_in_new, color: Colors.white38, size: 18, ), onTap: () async { Navigator.pop(sheetContext); final uri = buildSystemGeoUri( latitude: latitude, longitude: longitude, displayName: displayName, ); if (await canLaunchUrl(uri)) { await launchUrl( uri, mode: LaunchMode.externalApplication, ); } }, ), const SizedBox(height: 8), ], ), ), ); }, ); } /// Launches external navigation to a single target [stop]. Future launchNavigationToStop({ required RouteStop stop, NavigationApp app = NavigationApp.googleMaps, }) async { Uri uri; switch (app) { case NavigationApp.googleMaps: uri = buildGoogleMapsUri( latitude: stop.latitude, longitude: stop.longitude, displayName: stop.displayName, ); break; case NavigationApp.waze: uri = buildWazeUri(latitude: stop.latitude, longitude: stop.longitude); break; case NavigationApp.platformDefault: uri = buildSystemGeoUri( latitude: stop.latitude, longitude: stop.longitude, displayName: stop.displayName, ); break; } try { if (await canLaunchUrl(uri)) { return await launchUrl(uri, mode: LaunchMode.externalApplication); } final fallbackUri = Uri.parse( 'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}', ); if (await canLaunchUrl(fallbackUri)) { return await launchUrl( fallbackUri, mode: LaunchMode.externalApplication, ); } return false; } catch (e) { debugPrint('[NavigationHandoffService] Failed to launch navigation: $e'); return false; } } /// Launches multi-stop road route navigation in external maps (up to provider limit). Future launchFullRouteNavigation({ required MotorSignalRoute route, NavigationApp app = NavigationApp.googleMaps, }) async { final stops = route.orderedStops; if (stops.isEmpty) return false; if (stops.length == 1) { return launchNavigationToStop(stop: stops.first, app: app); } final origin = '${stops.first.latitude},${stops.first.longitude}'; final dest = '${stops.last.latitude},${stops.last.longitude}'; if (app == NavigationApp.googleMaps) { final intermediateStops = stops.sublist(1, stops.length - 1); final cappedIntermediates = intermediateStops.take(9); final waypointsStr = cappedIntermediates .map((s) => '${s.latitude},${s.longitude}') .join('|'); final waypointsParam = waypointsStr.isNotEmpty ? '&waypoints=$waypointsStr' : ''; final uri = Uri.parse( 'https://www.google.com/maps/dir/?api=1&origin=$origin&destination=$dest$waypointsParam', ); try { if (await canLaunchUrl(uri)) { return await launchUrl(uri, mode: LaunchMode.externalApplication); } } catch (e) { debugPrint( '[NavigationHandoffService] Failed to launch multi-stop route: $e', ); } } return launchNavigationToStop(stop: stops.last, app: app); } }