import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; import '../../../services/alerts/road_alert_models.dart'; import '../../../services/alerts/road_alert_service.dart'; import '../../../services/map/map_provider_interface.dart'; import '../../../services/map/map_providers.dart'; import '../../../services/map/unified_map_view.dart'; import '../../../services/navigation/navigation_session_service.dart'; import '../../../services/telemetry_service.dart'; import '../../rides/ride_state_machine.dart'; /// Embedded tactical map panel for Touring mode and responsive dashboard views. /// /// Subscribes to vehicle coordinates and heading via [telemetryStreamProvider] /// and alert pins via [activeAlertsStreamProvider], maintains camera follow /// state, and provides a recenter control after manual pan. class DashboardMapPanel extends ConsumerStatefulWidget { final double zoom; const DashboardMapPanel({ super.key, this.zoom = 16.5, }); @override ConsumerState createState() => _DashboardMapPanelState(); } class _DashboardMapPanelState extends ConsumerState { static const AppLatLng _defaultFallbackCenter = AppLatLng(37.7749, -122.4194); AppLatLng? _lastKnownRiderPos; @override void initState() { super.initState(); // Ensure follow mode is active whenever the Touring map panel is mounted. WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ref.read(mapFollowStateProvider.notifier).recenter(); } }); } bool _isValidCoord(double? lat, double? lng) { return lat != null && lng != null && lat.isFinite && lng.isFinite && (lat != 0.0 || lng != 0.0); } @override Widget build(BuildContext context) { // Re-enable camera follow automatically when a ride session starts. ref.listen(rideStateMachineProvider, (previous, next) { if (previous?.state != RideState.riding && next.state == RideState.riding && mounted) { ref.read(mapFollowStateProvider.notifier).recenter(); } }); final vehicleCoords = ref.watch( telemetryStreamProvider.select( (s) => ( lat: s.value?.latitude, lng: s.value?.longitude, heading: s.value?.heading ?? 0.0, isGpsFresh: s.value?.isGpsFresh ?? false, ), ), ); final hasCoordinates = _isValidCoord(vehicleCoords.lat, vehicleCoords.lng); if (hasCoordinates) { _lastKnownRiderPos = AppLatLng(vehicleCoords.lat!, vehicleCoords.lng!); } final activeCenter = _lastKnownRiderPos ?? _defaultFallbackCenter; final isFresh = vehicleCoords.isGpsFresh; final isFollowMode = ref.watch(mapFollowStateProvider); final orientation = ref.watch(mapOrientationProvider); final isNorthUp = orientation == MapOrientation.northUp; final effectiveBearing = isNorthUp ? 0.0 : vehicleCoords.heading; final vehicleMarker = AppMapMarker( id: 'dash_vehicle', position: activeCenter, heading: isNorthUp ? vehicleCoords.heading : 0.0, iconWidget: Icon( Icons.navigation, color: isFresh ? AppColors.cyberCyan : AppColors.cyberCyan.withValues(alpha: 0.38), size: 36, ), ); final activeAlertsAsync = ref.watch(activeAlertsStreamProvider); final activeAlerts = activeAlertsAsync.value ?? []; final mapMarkers = [vehicleMarker]; for (final alert in activeAlerts) { mapMarkers.add( AppMapMarker( id: 'dash_alert_${alert.id}', position: AppLatLng(alert.latitude, alert.longitude), heading: alert.heading ?? 0.0, label: alert.category.displayName, iconWidget: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: 0.9), shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)], ), child: Icon(alert.category.icon, color: Colors.black, size: 16), ), ), ); } final navSession = ref.watch(navigationSessionProvider); final polylines = []; if (navSession.isNavigating && navSession.calculationResult?.geometry.isNotEmpty == true) { polylines.add( AppPolyline( id: 'dash_active_nav_route', points: navSession.calculationResult!.geometry, color: const Color(0xFF00E5FF), width: 5.0, ), ); } return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all( color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkOutline : AppColors.lightOutline, width: 1.2, ), ), clipBehavior: Clip.antiAlias, child: LayoutBuilder( builder: (context, constraints) { return SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: Stack( children: [ Positioned.fill( child: UnifiedMapView( key: const ValueKey('dash_map_x'), center: activeCenter, zoom: widget.zoom, bearing: effectiveBearing, markers: mapMarkers, polylines: polylines, ), ), Positioned( top: 10, right: 10, child: Material( color: Colors.black.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(20), child: InkWell( key: const ValueKey('dashboard_map_orientation_button'), borderRadius: BorderRadius.circular(20), onTap: () { ref.read(mapOrientationProvider.notifier).toggle(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all( color: isNorthUp ? Colors.white24 : AppColors.cyberCyan, width: 1.2, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isNorthUp ? Icons.explore : Icons.navigation, size: 14, color: isNorthUp ? Colors.white70 : AppColors.cyberCyan, ), const SizedBox(width: 5), Text( isNorthUp ? 'NORTH UP' : 'HEADING UP', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 0.6, color: isNorthUp ? Colors.white70 : AppColors.cyberCyan, ), ), ], ), ), ), ), ), if (!isFresh && (hasCoordinates || _lastKnownRiderPos != null)) Positioned( top: 10, left: 10, child: Container( key: const ValueKey('stale_gps_pill'), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.75), borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.warningAmber, width: 1.0), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.location_off, size: 13, color: AppColors.warningAmber), SizedBox(width: 5), Text( 'LAST KNOWN LOCATION', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 0.5, color: AppColors.warningAmber, ), ), ], ), ), ), if (!isFollowMode) Positioned( bottom: 10, right: 10, child: Material( color: Colors.black.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(20), child: InkWell( key: const ValueKey('dashboard_map_recenter_button'), borderRadius: BorderRadius.circular(20), onTap: () { ref.read(mapFollowStateProvider.notifier).recenter(); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all( color: AppColors.cyberCyan, width: 1.2, ), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.my_location, size: 14, color: AppColors.cyberCyan, ), SizedBox(width: 5), Text( 'RECENTER', style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 0.6, color: AppColors.cyberCyan, ), ), ], ), ), ), ), ), ], ), ); }, ), ); } }