import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:geolocator/geolocator.dart' as geolocator; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart' as ll; import '../Provider/RideProvider.dart'; import '../models/Ride.dart'; import '../utils/colors.dart'; extension L2Mapbox on ll.LatLng { Point get mb => Point(coordinates: Position(longitude, latitude)); } extension ListToMapbox on List { List get mb => map((p) => Position(p.longitude, p.latitude)).toList(); } class RideMapView extends StatefulWidget { final int? selectedRideId; final Ride? selectedRide; // Accept ride directly for better performance final geolocator.Position? driverPosition; const RideMapView({ Key? key, this.selectedRideId, this.selectedRide, this.driverPosition, }) : super(key: key); @override State createState() => _RideMapViewState(); } class _RideMapViewState extends State { MapboxMap? _controller; PointAnnotationManager? _pointAnnotationManager; PolylineAnnotationManager? _polylineAnnotationManager; // State management bool _mapReady = false; bool _isDisposing = false; bool _isLoading = true; bool _isFetchingRoute = false; // Current state tracking int? _currentRideId; ll.LatLng? _userLocation; ll.LatLng? _driverLocation; // Animation and timing bool _isAnimating = false; Timer? _debounceTimer; // Location tracking StreamSubscription? _positionStream; @override void initState() { super.initState(); log('RideMapView initState - selectedRideId: ${widget.selectedRideId}'); // Set driver location if provided if (widget.driverPosition != null) { _driverLocation = ll.LatLng( widget.driverPosition!.latitude, widget.driverPosition!.longitude, ); } _initializeLocation(); } @override void didUpdateWidget(covariant RideMapView oldWidget) { super.didUpdateWidget(oldWidget); // Update driver position if (widget.driverPosition != oldWidget.driverPosition && widget.driverPosition != null) { _driverLocation = ll.LatLng( widget.driverPosition!.latitude, widget.driverPosition!.longitude, ); if (_mapReady) { _updateMapContent(); } } // Handle ride change if (widget.selectedRideId != oldWidget.selectedRideId || widget.selectedRide != oldWidget.selectedRide) { log('Ride changed: ${oldWidget.selectedRideId} -> ${widget.selectedRideId}'); _currentRideId = widget.selectedRideId; if (_mapReady) { _debounceMapUpdate(); } } } void _debounceMapUpdate() { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 300), () { if (mounted && !_isDisposing) { _updateMapContent(); } }); } Future _initializeLocation() async { log('Starting location initialization...'); try { await _getUserLocation(); _startLocationUpdates(); } catch (e) { log('Location initialization failed: $e'); _setFallbackLocation(); } if (mounted) { setState(() { _isLoading = false; }); log('Location ready, map can be created'); } } Future _getUserLocation() async { try { final position = await geolocator.Geolocator.getCurrentPosition( desiredAccuracy: geolocator.LocationAccuracy.high, timeLimit: const Duration(seconds: 5), ).timeout(const Duration(seconds: 6)); if (mounted) { setState(() { _userLocation = ll.LatLng(position.latitude, position.longitude); }); log('Got current location: ${position.latitude}, ${position.longitude}'); } return; } catch (e) { log('Current location failed: $e, trying last known...'); } try { final lastKnown = await geolocator.Geolocator.getLastKnownPosition(); if (lastKnown != null && mounted) { setState(() { _userLocation = ll.LatLng(lastKnown.latitude, lastKnown.longitude); }); log('Got last known location: ${lastKnown.latitude}, ${lastKnown.longitude}'); return; } } catch (e) { log('Last known location failed: $e'); } _setFallbackLocation(); } void _setFallbackLocation() { // Use provided ride data first if (widget.selectedRide != null) { try { final rideProvider = context.read(); final lat = rideProvider.getLatitude(widget.selectedRide!.startLocation); final lng = rideProvider.getLongitude(widget.selectedRide!.startLocation); setState(() { _userLocation = ll.LatLng(lat, lng); }); log('Using selected ride location as fallback: $lat, $lng'); return; } catch (e) { log('Error using selected ride location: $e'); } } // Try cached ride if no selected ride provided if (widget.selectedRideId != null) { try { final rideProvider = context.read(); final ride = rideProvider.getCachedRide(widget.selectedRideId!); if (ride != null) { final lat = rideProvider.getLatitude(ride.startLocation); final lng = rideProvider.getLongitude(ride.startLocation); setState(() { _userLocation = ll.LatLng(lat, lng); }); log('Using cached ride location as fallback: $lat, $lng'); return; } } catch (e) { log('Error using cached ride location: $e'); } } // Default fallback to Tunisia center setState(() { _userLocation = ll.LatLng(35.8256, 10.6411); }); log('Using Tunisia center as fallback'); } void _startLocationUpdates() { const locationSettings = geolocator.LocationSettings( accuracy: geolocator.LocationAccuracy.high, distanceFilter: 15, // Increased to reduce updates ); _positionStream = geolocator.Geolocator.getPositionStream( locationSettings: locationSettings, ).listen( (position) { if (!mounted || _isDisposing) return; final newLocation = ll.LatLng(position.latitude, position.longitude); setState(() { _userLocation = newLocation; }); // Only update camera if not showing ride content if (_mapReady && !_isAnimating && widget.selectedRideId == null) { _smoothUpdateCamera(newLocation); } }, onError: (error) => log('Location stream error: $error'), ); } Future _smoothUpdateCamera(ll.LatLng location) async { if (_controller == null || _isAnimating) return; _isAnimating = true; try { await _controller!.easeTo( CameraOptions(center: location.mb), MapAnimationOptions(duration: 1000), ); } catch (e) { log('Error updating camera: $e'); } finally { _isAnimating = false; } } CameraOptions _getInitialCameraPosition() { if (_userLocation != null) { log('Setting initial camera to user location: ${_userLocation!.latitude}, ${_userLocation!.longitude}'); return CameraOptions( center: _userLocation!.mb, zoom: 13.0, ); } log('Using default camera position'); return CameraOptions( center: Point(coordinates: Position(10.6411, 35.8256)), zoom: 10.0, ); } Future _onMapCreated(MapboxMap mapboxMap) async { if (_isDisposing) return; _controller = mapboxMap; log('Map created, initializing managers...'); try { // Initialize managers _pointAnnotationManager = await _controller!.annotations.createPointAnnotationManager(); _polylineAnnotationManager = await _controller!.annotations.createPolylineAnnotationManager(); if (!mounted || _isDisposing) { _cleanupManagers(); return; } _mapReady = true; log('Map ready, updating content...'); // Update content immediately await _updateMapContent(); } catch (e) { log('Error setting up map: $e'); } } Future _updateMapContent() async { if (!_shouldUpdateMap()) { log('Skipping map update - conditions not met'); return; } log('Updating map content...'); try { // Clear existing content first await _clearAllAnnotations(); // Get the ride to display Ride? ride = widget.selectedRide; if (ride == null && widget.selectedRideId != null) { final rideProvider = context.read(); ride = rideProvider.getCachedRide(widget.selectedRideId!); } if (ride != null) { log('Processing ride ${ride.id}'); await _processRideContent(ride); } else { log('No ride to display'); } } catch (e) { log('Error updating map content: $e'); } } Future _processRideContent(Ride ride) async { try { final rideProvider = context.read(); // Create markers first await _createRideMarkers(ride, rideProvider); // Then create route await _createRideRoute(ride, rideProvider); // Finally, adjust camera to show the ride await _adjustCameraForRide(ride, rideProvider); } catch (e) { log('Error processing ride content: $e'); } } Future _createRideMarkers(Ride ride, RideProvider rideProvider) async { if (_pointAnnotationManager == null) return; final List markers = []; try { // Driver marker if available if (_driverLocation != null) { final driverMarker = await _createMarker( _driverLocation!, 'assets/icons/taxi_car_marker.png', Colors.blue.value, 2.0, ); if (driverMarker != null) { markers.add(driverMarker); } } // Start marker (pickup) final startPos = ll.LatLng( rideProvider.getLatitude(ride.startLocation), rideProvider.getLongitude(ride.startLocation), ); final startMarker = await _createMarker( startPos, 'assets/markers/markerA.png', Colors.green.value, 2.0, ); if (startMarker != null) { markers.add(startMarker); } // End marker (destination) final endPos = ll.LatLng( rideProvider.getLatitude(ride.endLocation), rideProvider.getLongitude(ride.endLocation), ); final endMarker = await _createMarker( endPos, 'assets/markers/markerB.png', Colors.red.value, 2.0, ); if (endMarker != null) { markers.add(endMarker); } // Create all markers at once if (markers.isNotEmpty) { await _pointAnnotationManager!.createMulti(markers); log('Created ${markers.length} markers'); } } catch (e) { log('Error creating ride markers: $e'); } } Future _createRideRoute(Ride ride, RideProvider rideProvider) async { if (_polylineAnnotationManager == null || _isFetchingRoute) return; _isFetchingRoute = true; try { final startPos = ll.LatLng( rideProvider.getLatitude(ride.startLocation), rideProvider.getLongitude(ride.startLocation), ); final endPos = ll.LatLng( rideProvider.getLatitude(ride.endLocation), rideProvider.getLongitude(ride.endLocation), ); // Route from driver to pickup (if driver position available) if (_driverLocation != null && _driverLocation != startPos) { try { final toPickupRoute = await rideProvider.fetchRouteInfoMapbox(_driverLocation!, startPos); final toPickupPoints = toPickupRoute.points.mb; if (toPickupPoints.isNotEmpty && toPickupPoints.length >= 2) { await _polylineAnnotationManager!.create( PolylineAnnotationOptions( geometry: LineString(coordinates: toPickupPoints), lineColor: Colors.deepOrange.value, lineWidth: 4.0, ), ); log('Created route to pickup with ${toPickupPoints.length} points'); } } catch (e) { log('Error creating route to pickup: $e'); } } // Route from pickup to destination if (startPos != endPos) { try { final rideRoute = await rideProvider.fetchRouteInfoMapbox(startPos, endPos); final ridePoints = rideRoute.points.mb; if (ridePoints.isNotEmpty && ridePoints.length >= 2) { await _polylineAnnotationManager!.create( PolylineAnnotationOptions( geometry: LineString(coordinates: ridePoints), lineColor: AppColors.primaryColor.value, lineWidth: 3.5, lineOpacity: 0.8, ), ); log('Created ride route with ${ridePoints.length} points'); } } catch (e) { log('Error creating ride route: $e'); } } } catch (e) { log('Error in _createRideRoute: $e'); } finally { _isFetchingRoute = false; } } Future _adjustCameraForRide(Ride ride, RideProvider rideProvider) async { if (_controller == null || _isAnimating) return; _isAnimating = true; try { final List points = []; // Add driver position if available if (_driverLocation != null) { points.add(_driverLocation!); } // Add start position points.add(ll.LatLng( rideProvider.getLatitude(ride.startLocation), rideProvider.getLongitude(ride.startLocation), )); // Add end position points.add(ll.LatLng( rideProvider.getLatitude(ride.endLocation), rideProvider.getLongitude(ride.endLocation), )); if (points.length == 1) { // Single point - just center on it await _controller!.flyTo( CameraOptions( center: points.first.mb, zoom: 15.0, ), MapAnimationOptions(duration: 1500), ); } else { // Multiple points - fit them in view await _fitPointsInView(points); } } catch (e) { log('Error adjusting camera for ride: $e'); } finally { _isAnimating = false; } } Future _fitPointsInView(List points) async { if (points.isEmpty || _controller == null) return; double minLat = points.first.latitude; double maxLat = points.first.latitude; double minLng = points.first.longitude; double maxLng = points.first.longitude; for (final point in points) { if (point.latitude < minLat) minLat = point.latitude; if (point.latitude > maxLat) maxLat = point.latitude; if (point.longitude < minLng) minLng = point.longitude; if (point.longitude > maxLng) maxLng = point.longitude; } const padding = 0.005; // Reduced padding for better view final bounds = CoordinateBounds( southwest: Point(coordinates: Position(minLng - padding, minLat - padding)), northeast: Point(coordinates: Position(maxLng + padding, maxLat + padding)), infiniteBounds: false, ); try { final cameraOptions = await _controller!.cameraForCoordinateBounds( bounds, MbxEdgeInsets(top: 80, left: 80, bottom: 200, right: 80), // More bottom padding for ride cards 0, 0, null, null, ); await _controller!.flyTo(cameraOptions, MapAnimationOptions(duration: 1500)); log('Camera adjusted to fit ${points.length} points'); } catch (e) { log('Error fitting points in view: $e'); } } bool _shouldUpdateMap() { return _mapReady && mounted && !_isDisposing && _pointAnnotationManager != null && _polylineAnnotationManager != null; } Future _clearAllAnnotations() async { try { if (_pointAnnotationManager != null) { await _pointAnnotationManager!.deleteAll(); } if (_polylineAnnotationManager != null) { await _polylineAnnotationManager!.deleteAll(); } await Future.delayed(const Duration(milliseconds: 50)); log('All annotations cleared'); } catch (e) { log('Error clearing annotations: $e'); } } Future _createMarker( ll.LatLng position, String assetPath, int fallbackColor, double iconSize, ) async { try { final bytes = await rootBundle.load(assetPath); final imageData = bytes.buffer.asUint8List(); return PointAnnotationOptions( geometry: position.mb, image: imageData, iconSize: iconSize, iconAnchor: IconAnchor.BOTTOM, ); } catch (e) { log('Marker image failed for $assetPath, using fallback: $e'); return PointAnnotationOptions( geometry: position.mb, iconSize: 1.0, iconColor: fallbackColor, ); } } void _cleanupManagers() { try { _pointAnnotationManager = null; _polylineAnnotationManager = null; } catch (e) { log('Error cleaning up managers: $e'); } } @override void dispose() { log('Disposing RideMapView'); _isDisposing = true; _debounceTimer?.cancel(); _positionStream?.cancel(); _cleanupManagers(); try { _controller?.dispose(); } catch (e) { log('Error disposing controller: $e'); } _controller = null; _mapReady = false; super.dispose(); } @override Widget build(BuildContext context) { if (_isLoading) { return Container( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[900] : Colors.grey[100], child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Loading map...'), ], ), ), ); } return MapWidget( key: ValueKey("rideMapWidget_${widget.selectedRideId}"), cameraOptions: _getInitialCameraPosition(), styleUri: Theme.of(context).brightness == Brightness.dark ? MapboxStyles.DARK : MapboxStyles.MAPBOX_STREETS, onMapCreated: _onMapCreated, gestureRecognizers: >{ Factory(() => EagerGestureRecognizer()), }, ); } }