import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../../../map/providers/map_provider.dart'; import '../../providers/routing_provider.dart'; import '../widgets/route_summary.dart'; import '../widgets/step_list.dart'; class RouteScreen extends ConsumerStatefulWidget { final double? destinationLat; final double? destinationLon; final String? destinationName; const RouteScreen({ super.key, this.destinationLat, this.destinationLon, this.destinationName, }); @override ConsumerState createState() => _RouteScreenState(); } class _RouteScreenState extends ConsumerState { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _initializeRoute(); }); } void _initializeRoute() { final notifier = ref.read(routingProvider.notifier); final mapState = ref.read(mapProvider); // Set origin from current location if available. if (mapState.currentLocation != null) { notifier.setOrigin( mapState.currentLocation!.latitude, mapState.currentLocation!.longitude, 'My Location', ); } // Set destination from route params. if (widget.destinationLat != null && widget.destinationLon != null) { notifier.setDestination( widget.destinationLat!, widget.destinationLon!, widget.destinationName ?? 'Destination', ); notifier.calculateRoute(); } } @override Widget build(BuildContext context) { final routingState = ref.watch(routingProvider); return Scaffold( appBar: AppBar( title: const Text('Route'), ), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Origin and destination fields Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( readOnly: true, decoration: InputDecoration( labelText: 'From', prefixIcon: const Icon(Icons.trip_origin), hintText: routingState.originName, ), controller: TextEditingController(text: routingState.originName), ), const SizedBox(height: 8), TextField( readOnly: true, decoration: InputDecoration( labelText: 'To', prefixIcon: const Icon(Icons.flag), hintText: routingState.destName, ), controller: TextEditingController(text: routingState.destName), ), ], ), ), // Profile selector Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ _ProfileChip( icon: Icons.directions_car, label: 'Drive', isSelected: routingState.profile == 'driving', onTap: () { ref.read(routingProvider.notifier).setProfile('driving'); ref.read(routingProvider.notifier).calculateRoute(); }, ), const SizedBox(width: 8), _ProfileChip( icon: Icons.directions_walk, label: 'Walk', isSelected: routingState.profile == 'walking', onTap: () { ref.read(routingProvider.notifier).setProfile('walking'); ref.read(routingProvider.notifier).calculateRoute(); }, ), const SizedBox(width: 8), _ProfileChip( icon: Icons.directions_bike, label: 'Cycle', isSelected: routingState.profile == 'cycling', onTap: () { ref.read(routingProvider.notifier).setProfile('cycling'); ref.read(routingProvider.notifier).calculateRoute(); }, ), ], ), ), const SizedBox(height: 16), // Loading if (routingState.isLoading) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ), // Error if (routingState.error != null) Padding( padding: const EdgeInsets.all(16), child: Card( color: Theme.of(context).colorScheme.errorContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.error_outline, color: Theme.of(context) .colorScheme .onErrorContainer), const SizedBox(width: 8), Expanded(child: Text(routingState.error!)), ], ), ), ), ), // Map with route if (routingState.routes.isNotEmpty) _buildRouteMap(context, routingState), // Route summaries if (routingState.routes.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ for (var i = 0; i < routingState.routes.length; i++) RouteSummary( route: routingState.routes[i], profile: routingState.profile, isSelected: i == routingState.selectedRouteIndex, onTap: () => ref.read(routingProvider.notifier).selectRoute(i), ), ], ), ), // Step-by-step instructions if (routingState.selectedRoute != null && routingState.selectedRoute!.legs.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( 'Turn-by-turn directions', style: Theme.of(context).textTheme.titleSmall, ), ), StepList( steps: routingState.selectedRoute!.legs .expand((leg) => leg.steps) .toList(), ), ], const SizedBox(height: 32), ], ), ), ); } Widget _buildRouteMap(BuildContext context, RoutingState routingState) { final selectedRoute = routingState.selectedRoute; if (selectedRoute == null) return const SizedBox.shrink(); final routePoints = selectedRoute.geometry .map((c) => LatLng(c[1], c[0])) .toList(); // Calculate bounds. double minLat = 90, maxLat = -90, minLon = 180, maxLon = -180; for (final p in routePoints) { if (p.latitude < minLat) minLat = p.latitude; if (p.latitude > maxLat) maxLat = p.latitude; if (p.longitude < minLon) minLon = p.longitude; if (p.longitude > maxLon) maxLon = p.longitude; } return Container( height: 250, margin: const EdgeInsets.all(16), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), ), child: FlutterMap( options: MapOptions( initialCameraFit: CameraFit.bounds( bounds: LatLngBounds( LatLng(minLat, minLon), LatLng(maxLat, maxLon), ), padding: const EdgeInsets.all(32), ), interactionOptions: const InteractionOptions( flags: InteractiveFlag.none, ), ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', tileProvider: CancellableNetworkTileProvider(), userAgentPackageName: 'com.privacymaps.app', ), // Draw all routes, non-selected as faint, selected as bold. for (var i = routingState.routes.length - 1; i >= 0; i--) PolylineLayer( polylines: [ Polyline( points: routingState.routes[i].geometry .map((c) => LatLng(c[1], c[0])) .toList(), strokeWidth: i == routingState.selectedRouteIndex ? 5.0 : 3.0, color: i == routingState.selectedRouteIndex ? Theme.of(context).colorScheme.primary : Theme.of(context) .colorScheme .outline .withValues(alpha: 0.5), ), ], ), // Origin and destination markers MarkerLayer( markers: [ if (routingState.originLat != null && routingState.originLon != null) Marker( point: LatLng( routingState.originLat!, routingState.originLon!, ), width: 20, height: 20, child: Container( decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), if (routingState.destLat != null && routingState.destLon != null) Marker( point: LatLng( routingState.destLat!, routingState.destLon!, ), width: 20, height: 20, child: Container( decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), ], ), ], ), ); } } class _ProfileChip extends StatelessWidget { final IconData icon; final String label; final bool isSelected; final VoidCallback onTap; const _ProfileChip({ required this.icon, required this.label, required this.isSelected, required this.onTap, }); @override Widget build(BuildContext context) { return ChoiceChip( avatar: Icon(icon, size: 18), label: Text(label), selected: isSelected, onSelected: (_) => onTap(), ); } }