import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.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, }) { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF1B1E26), 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: [ const Icon(Icons.navigation, color: Color(0xFF00E5FF), size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'NAVIGATION HANDOFF', style: TextStyle( fontSize: 11, letterSpacing: 1.2, fontWeight: FontWeight.w700, color: Color(0xFF00E5FF), ), ), 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( 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( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.directions_car, color: Color(0xFF00E5FF)), ), 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( 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); } }