import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../services/location/location_service.dart'; import '../../data/models/route_model.dart'; import '../../data/models/stop_model.dart'; class MapViewPage extends StatefulWidget { final RouteModel route; const MapViewPage({super.key, required this.route}); @override State createState() => _MapViewPageState(); } class _MapViewPageState extends State { final Completer _mapController = Completer(); final LocationService _locationService = LocationService(); final Set _markers = {}; final Set _polylines = {}; LatLng? _currentPosition; StreamSubscription? _locationSubscription; @override void initState() { super.initState(); _initializeMap(); _startLocationTracking(); } @override void dispose() { _locationSubscription?.cancel(); super.dispose(); } Future _initializeMap() async { await _createMarkers(); await _fitBounds(); } Future _createMarkers() async { final markers = {}; for (int i = 0; i < widget.route.stops.length; i++) { final stop = widget.route.stops[i]; final position = LatLng( stop.location.latitude, stop.location.longitude, ); markers.add( Marker( markerId: MarkerId(stop.id), position: position, icon: await _getMarkerIcon(stop), infoWindow: InfoWindow( title: '${i + 1}. ${stop.customerName}', snippet: stop.type == StopType.pickup ? 'Pickup' : 'Dropoff', ), onTap: () => _onMarkerTapped(stop), ), ); } if (mounted) { setState(() { _markers.addAll(markers); }); } } Future _getMarkerIcon(StopModel stop) async { // Determine color based on status final hue = switch (stop.status) { StopStatus.completed => BitmapDescriptor.hueGreen, StopStatus.inProgress => BitmapDescriptor.hueBlue, StopStatus.failed => BitmapDescriptor.hueRed, _ => BitmapDescriptor.hueOrange, }; return BitmapDescriptor.defaultMarkerWithHue(hue); } Future _fitBounds() async { if (widget.route.stops.isEmpty) return; final controller = await _mapController.future; double minLat = widget.route.stops.first.location.latitude; double maxLat = widget.route.stops.first.location.latitude; double minLng = widget.route.stops.first.location.longitude; double maxLng = widget.route.stops.first.location.longitude; for (final stop in widget.route.stops) { if (stop.location.latitude < minLat) minLat = stop.location.latitude; if (stop.location.latitude > maxLat) maxLat = stop.location.latitude; if (stop.location.longitude < minLng) minLng = stop.location.longitude; if (stop.location.longitude > maxLng) maxLng = stop.location.longitude; } final bounds = LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ); controller.animateCamera( CameraUpdate.newLatLngBounds(bounds, 100), ); } void _startLocationTracking() { _locationSubscription = _locationService.getLocationStream().listen( (position) { final newPosition = LatLng(position.latitude, position.longitude); if (mounted) { setState(() { _currentPosition = newPosition; // Add or update current location marker _markers.removeWhere( (marker) => marker.markerId.value == 'current_location', ); _markers.add( Marker( markerId: const MarkerId('current_location'), position: newPosition, icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), infoWindow: const InfoWindow(title: 'Your Location'), ), ); }); } }, onError: (error) { print('Error tracking location: $error'); }, ); } void _onMarkerTapped(StopModel stop) { showModalBottomSheet( context: context, builder: (context) => Container( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( stop.customerName, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( stop.type == StopType.pickup ? 'Pickup' : 'Dropoff', style: Theme.of(context).textTheme.bodyMedium, ), if (stop.location.address != null) ...[ const SizedBox(height: 8), Text( stop.location.address!, style: Theme.of(context).textTheme.bodySmall, ), ], const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); _navigateToStop(stop); }, icon: const Icon(Icons.navigation), label: const Text('Navigate to this stop'), ), ), ], ), ), ); } Future _navigateToStop(StopModel stop) async { // This will use the NavigationService to open external navigation final navigationService = (() => throw UnimplementedError('Add NavigationService'))(); } Future _centerOnCurrentLocation() async { if (_currentPosition != null) { final controller = await _mapController.future; controller.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: _currentPosition!, zoom: AppConstants.defaultZoom, ), ), ); } else { final position = await _locationService.getCurrentLocation(); if (position != null) { final newPosition = LatLng(position.latitude, position.longitude); final controller = await _mapController.future; controller.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: newPosition, zoom: AppConstants.defaultZoom, ), ), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Route ${widget.route.id} Map'), actions: [ IconButton( icon: const Icon(Icons.layers), onPressed: () { // Toggle map type }, ), ], ), body: Stack( children: [ GoogleMap( onMapCreated: (controller) { _mapController.complete(controller); }, initialCameraPosition: CameraPosition( target: widget.route.stops.isNotEmpty ? LatLng( widget.route.stops.first.location.latitude, widget.route.stops.first.location.longitude, ) : const LatLng(0, 0), zoom: AppConstants.defaultZoom, ), markers: _markers, polylines: _polylines, myLocationEnabled: true, myLocationButtonEnabled: false, mapToolbarEnabled: true, zoomControlsEnabled: false, ), Positioned( top: 16, left: 16, right: 16, child: Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Route Progress', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildLegendItem( 'Completed', AppTheme.completedColor, ), _buildLegendItem( 'In Progress', AppTheme.inProgressColor, ), _buildLegendItem( 'Pending', AppTheme.pendingColor, ), ], ), ], ), ), ), ), ], ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( heroTag: 'center', mini: true, onPressed: _centerOnCurrentLocation, child: const Icon(Icons.my_location), ), const SizedBox(height: 12), FloatingActionButton( heroTag: 'fit', mini: true, onPressed: _fitBounds, child: const Icon(Icons.zoom_out_map), ), ], ), ); } Widget _buildLegendItem(String label, Color color) { return Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( label, style: const TextStyle(fontSize: 12), ), ], ); } }