import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/drift.dart' as drift; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.dart'; import '../../database/database.dart'; import '../../core/responsive/responsive_layout.dart'; import '../../core/theme/theme_extensions.dart'; import '../../services/location_service.dart'; import '../../services/map/map_provider_interface.dart'; import '../../services/map/map_providers.dart'; import '../../services/map/unified_map_view.dart'; import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_providers.dart'; import '../../services/alerts/road_alert_service.dart'; import '../../services/alerts/widgets/quick_swarm_report_modal.dart'; import '../../services/map/map_layer_settings.dart'; import '../../services/routes/route_models.dart'; import '../../services/routes/route_provider.dart'; import '../../services/routes/route_providers.dart'; import '../../services/routes/navigation_handoff.dart'; import '../../services/navigation/navigation_session_service.dart'; import '../dashboard/widgets/navigation_maneuver_card.dart'; import 'widgets/tactical_layers_bottom_sheet.dart'; import '../replay/replay_screen.dart'; class MapScreen extends ConsumerStatefulWidget { final int? rideId; const MapScreen({super.key, this.rideId}); @override ConsumerState createState() => _MapScreenState(); } class _MapScreenState extends ConsumerState { bool _isLoadingHistory = true; List _telemetrySamples = []; List _routePoints = []; List _eventMarkers = []; AppLatLng? _initialCenter; AppLatLng _fallbackLocation = const AppLatLng(37.7749, -122.4194); bool _showRoute = true; bool _showSpeedHeatmap = false; bool _showHardAccel = true; bool _showHardBraking = true; bool _showCornering = true; double _currentZoom = 17.0; bool _isNorthUp = true; String _destinationQuery = ''; late final TextEditingController _searchController; Timer? _searchDebounce; bool _isSearching = false; List _searchResults = []; ResolvedPlace? _selectedDestination; RouteCalculationResult? _previewRouteResult; bool _isAddressExpanded = false; bool _isCalculatingRoute = false; int _routeCalculationRequestId = 0; RoutingOptions _routingOptions = const RoutingOptions(); static const double _hardAccelThresholdG = 0.35; static const double _hardBrakingThresholdG = -0.35; static const double _corneringLeanThresholdDeg = 20.0; static const double _corneringLateralGThreshold = 0.35; static const Duration _eventMergeWindow = Duration(milliseconds: 1500); @override void initState() { super.initState(); _searchController = TextEditingController(); if (widget.rideId != null) { _loadRideHistory(); } else { _isLoadingHistory = false; _initLiveLocation(); } } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); super.dispose(); } void _onSearchQueryChanged(String query, AppLatLng currentPos) { setState(() => _destinationQuery = query); _searchDebounce?.cancel(); final clean = query.trim(); if (clean.length < 2) { setState(() { _searchResults = []; _isSearching = false; }); return; } setState(() => _isSearching = true); _searchDebounce = Timer(const Duration(milliseconds: 350), () async { try { final results = await ref .read(activeRouteCalculationProvider) .searchPlaces(clean, nearLocation: currentPos); if (mounted && _destinationQuery.trim() == clean) { setState(() { _searchResults = results; _isSearching = false; }); } } catch (_) { if (mounted) setState(() => _isSearching = false); } }); } Future _onSelectPlace( PlaceSearchResult place, AppLatLng currentPos, ) async { _searchDebounce?.cancel(); FocusScope.of(context).unfocus(); setState(() { _searchResults = []; _isAddressExpanded = false; _searchController.text = place.primaryText; _destinationQuery = place.primaryText; }); try { final resolved = place.hasCoordinates ? place.toResolvedPlace()! : await ref .read(activeRouteCalculationProvider) .resolvePlace(place.providerId); if (!mounted) return; setState(() => _selectedDestination = resolved); await _calculateRouteToDestination(resolved, currentPos); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to resolve place: $e')), ); } } } Future _calculateRouteToDestination( ResolvedPlace dest, AppLatLng currentPos, ) async { final reqId = ++_routeCalculationRequestId; setState(() => _isCalculatingRoute = true); try { final origin = RouteStop( id: 'origin', order: 0, displayName: 'Current Location', latitude: currentPos.latitude, longitude: currentPos.longitude, ); final destinationStop = RouteStop( id: 'dest', order: 1, displayName: dest.displayName, latitude: dest.latitude, longitude: dest.longitude, ); final result = await ref .read(activeRouteCalculationProvider) .calculateRoute([origin, destinationStop], options: _routingOptions); if (mounted && reqId == _routeCalculationRequestId) { setState(() { _previewRouteResult = result; _isCalculatingRoute = false; }); } } catch (e) { if (mounted && reqId == _routeCalculationRequestId) { setState(() => _isCalculatingRoute = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Route calculation error: $e')), ); } } } void _clearDestination() { _searchDebounce?.cancel(); _searchController.clear(); setState(() { _destinationQuery = ''; _searchResults = []; _selectedDestination = null; _previewRouteResult = null; _isAddressExpanded = false; }); } void _startTurnByTurnNavigation() { if (_previewRouteResult == null || _selectedDestination == null) return; final preview = _previewRouteResult!; final dest = _selectedDestination!; setState(() { _previewRouteResult = null; }); ref.read(navigationSessionProvider.notifier).startNavigation( calculationResult: preview, destination: dest, options: _routingOptions, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Navigation started to ${dest.displayName}'), backgroundColor: const Color(0xFF00E5FF), ), ); } Future _initLiveLocation() async { final locationService = ref.read(locationServiceProvider); await locationService.requestPermissions(); try { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null && mounted) { setState(() => _fallbackLocation = AppLatLng(lastPos.latitude, lastPos.longitude)); } else { final currentPos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings(timeLimit: Duration(seconds: 3)), ).catchError((_) => Position( longitude: -122.4194, latitude: 37.7749, timestamp: DateTime.now(), accuracy: 0, altitude: 0, heading: 0, speed: 0, speedAccuracy: 0, altitudeAccuracy: 0, headingAccuracy: 0, )); if (mounted) setState(() => _fallbackLocation = AppLatLng(currentPos.latitude, currentPos.longitude)); } } catch (_) {} } Future _loadRideHistory() async { final db = ref.read(databaseProvider); final telemetry = await (db.select(db.telemetrySamples) ..where((t) => t.rideId.equals(widget.rideId!)) ..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp)])) .get(); final events = await (db.select(db.rideEvents) ..where((e) => e.rideId.equals(widget.rideId!))) .get(); _telemetrySamples = telemetry; _routePoints = telemetry .where((t) => t.latitude != null && t.longitude != null) .map((t) => AppLatLng(t.latitude!, t.longitude!)) .toList(); if (_routePoints.isNotEmpty) _initialCenter = _routePoints.first; _eventMarkers = _buildStoredEventMarkers(events); _eventMarkers.addAll(_buildTelemetryEventMarkers(telemetry)); if (mounted) setState(() => _isLoadingHistory = false); } List _buildStoredEventMarkers(List events) { final markers = []; for (final event in events) { if (event.latitude == null || event.longitude == null) continue; final typeLower = event.eventType.toLowerCase(); Color markerColor = Colors.redAccent; IconData markerIcon = Icons.warning_amber_rounded; if (typeLower.contains('corner') || typeLower.contains('lean')) { markerColor = const Color(0xFF00E5FF); markerIcon = Icons.cached; } else if (typeLower.contains('accel')) { markerColor = const Color(0xFF76FF03); markerIcon = Icons.arrow_upward; } else if (typeLower.contains('brak')) { markerColor = const Color(0xFFFF5252); markerIcon = Icons.arrow_downward; } else if (typeLower.contains('speed')) { markerColor = const Color(0xFFE040FB); markerIcon = Icons.speed; } markers.add(_eventMarker( id: 'event_${event.id}', position: AppLatLng(event.latitude!, event.longitude!), label: event.eventType, value: event.value, timestamp: event.timestamp, markerColor: markerColor, markerIcon: markerIcon, )); } return markers; } List _buildTelemetryEventMarkers(List samples) { final gps = samples.where((s) => s.latitude != null && s.longitude != null && (s.longitudinalG != null || s.accelX != null || s.lateralG != null || s.leanAngle != null)).toList(); if (gps.isEmpty) return []; final markers = []; DateTime? lastAccel; DateTime? lastBrake; DateTime? lastCorner; for (final sample in gps) { final longitudinalG = sample.longitudinalG ?? sample.accelX; final lateralG = sample.lateralG; final leanAngle = sample.leanAngle; if (longitudinalG != null && longitudinalG.isFinite) { if (longitudinalG >= _hardAccelThresholdG && (lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) { markers.add(_eventMarker( id: 'telemetry_accel_${sample.id}', position: AppLatLng(sample.latitude!, sample.longitude!), label: 'Hard Acceleration', value: longitudinalG, timestamp: sample.timestamp, markerColor: const Color(0xFF76FF03), markerIcon: Icons.arrow_upward, )); lastAccel = sample.timestamp; } else if (longitudinalG <= _hardBrakingThresholdG && (lastBrake == null || sample.timestamp.difference(lastBrake).abs() > _eventMergeWindow)) { markers.add(_eventMarker( id: 'telemetry_braking_${sample.id}', position: AppLatLng(sample.latitude!, sample.longitude!), label: 'Hard Braking', value: longitudinalG, timestamp: sample.timestamp, markerColor: const Color(0xFFFF5252), markerIcon: Icons.arrow_downward, )); lastBrake = sample.timestamp; } } final isCornering = (leanAngle != null && leanAngle.isFinite && leanAngle.abs() >= _corneringLeanThresholdDeg) || (lateralG != null && lateralG.isFinite && lateralG.abs() >= _corneringLateralGThreshold); if (isCornering && (lastCorner == null || sample.timestamp.difference(lastCorner).abs() > _eventMergeWindow)) { final cornerValue = leanAngle != null && leanAngle.isFinite ? leanAngle : lateralG!.abs(); markers.add(_eventMarker( id: 'telemetry_corner_${sample.id}', position: AppLatLng(sample.latitude!, sample.longitude!), label: 'Cornering', value: cornerValue, timestamp: sample.timestamp, markerColor: const Color(0xFF00E5FF), markerIcon: Icons.turn_right, )); lastCorner = sample.timestamp; } } return markers; } AppMapMarker _eventMarker({ required String id, required AppLatLng position, required String label, required double value, required DateTime timestamp, required Color markerColor, required IconData markerIcon, }) { return AppMapMarker( id: id, position: position, label: label, iconWidget: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: markerColor.withValues(alpha: 0.9), shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 4)], ), child: Icon(markerIcon, color: Colors.black, size: 18), ), onTap: () { if (!mounted) return; final timeStr = DateFormat('HH:mm:ss.SSS').format(timestamp.toLocal()); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF111111), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: markerColor.withValues(alpha: 0.6), width: 1.2), ), title: Row( children: [ Icon(markerIcon, color: markerColor, size: 20), const SizedBox(width: 8), Expanded(child: Text(label.toUpperCase(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: Colors.white))), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDialogRow('Timestamp:', timeStr), _buildDialogRow('Value:', value.toStringAsFixed(2)), _buildDialogRow('GPS Location:', '${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}'), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('CLOSE', style: TextStyle(color: Colors.white70))), ], ), ); }, ); } Widget _buildDialogRow(String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontSize: 11, color: Colors.white54)), Flexible(child: Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.white), textAlign: TextAlign.right)), ], ), ); List _buildPolylines({bool showTwistyHighlights = true}) { if (_routePoints.isEmpty) return []; final polylines = []; if (_showRoute) { polylines.add(AppPolyline( id: 'history_route', points: _routePoints, color: const Color(0xFF00E5FF), width: 4.5, )); } final validSamples = _telemetrySamples .where((t) => t.latitude != null && t.longitude != null) .toList(); if (validSamples.isEmpty) return polylines; if (_showSpeedHeatmap) { final step = (validSamples.length / 1500).ceil().clamp(1, 10); List currentSegment = []; Color currentColor = _getSpeedColor(validSamples.first.speed ?? 0.0); for (int i = 0; i < validSamples.length; i += step) { final s = validSamples[i]; final pt = AppLatLng(s.latitude!, s.longitude!); final segColor = _getSpeedColor(s.speed ?? 0.0); if (segColor != currentColor && currentSegment.isNotEmpty) { currentSegment.add(pt); polylines.add(AppPolyline(id: 'speed_seg_${polylines.length}', points: List.from(currentSegment), color: currentColor, width: 5.0)); currentSegment = [pt]; currentColor = segColor; } else { currentSegment.add(pt); } } if (currentSegment.length > 1) { polylines.add(AppPolyline(id: 'speed_seg_${polylines.length}', points: currentSegment, color: currentColor, width: 5.0)); } } if (_showHardAccel) { polylines.addAll(_buildLongitudinalEventPolylines( validSamples, predicate: (g) => g >= _hardAccelThresholdG, idPrefix: 'accel_layer', color: const Color(0xFF76FF03), )); } if (_showHardBraking) { polylines.addAll(_buildLongitudinalEventPolylines( validSamples, predicate: (g) => g <= _hardBrakingThresholdG, idPrefix: 'braking_layer', color: const Color(0xFFFF5252), )); } if (_showCornering && showTwistyHighlights) { polylines.addAll(_buildCorneringEventPolylines(validSamples)); } return polylines; } List _buildLongitudinalEventPolylines( List samples, { required bool Function(double g) predicate, required String idPrefix, required Color color, }) { if (samples.isEmpty) return []; final polylines = []; final eventIndices = []; for (var i = 0; i < samples.length; i++) { final g = samples[i].longitudinalG ?? samples[i].accelX; if (g != null && g.isFinite && predicate(g)) { eventIndices.add(i); } } if (eventIndices.isEmpty) return polylines; var runStart = eventIndices.first; var runEnd = eventIndices.first; var segmentIndex = 0; void emitRun(int start, int end) { final expandedStart = start > 0 ? start - 1 : start; final expandedEnd = end < samples.length - 1 ? end + 1 : end; final points = []; for (var i = expandedStart; i <= expandedEnd; i++) { final sample = samples[i]; points.add(AppLatLng(sample.latitude!, sample.longitude!)); } if (points.length >= 2) { polylines.add(AppPolyline( id: '${idPrefix}_$segmentIndex', points: points, color: color, width: 7.0, )); segmentIndex++; } } for (var i = 1; i < eventIndices.length; i++) { final index = eventIndices[i]; if (index - runEnd <= 2) { runEnd = index; } else { emitRun(runStart, runEnd); runStart = runEnd = index; } } emitRun(runStart, runEnd); return polylines; } List _buildCorneringEventPolylines(List samples) { if (samples.isEmpty) return []; final eventIndices = []; for (var i = 0; i < samples.length; i++) { final sample = samples[i]; final lean = sample.leanAngle; final lateral = sample.lateralG; final isCornering = (lean != null && lean.isFinite && lean.abs() >= _corneringLeanThresholdDeg) || (lateral != null && lateral.isFinite && lateral.abs() >= _corneringLateralGThreshold); if (isCornering) eventIndices.add(i); } if (eventIndices.isEmpty) return []; final polylines = []; var runStart = eventIndices.first; var runEnd = eventIndices.first; var segmentIndex = 0; void emitRun(int start, int end) { // Include a GPS breadcrumb before and after the detected cornering // samples. This makes a short threshold crossing visible as a route // segment rather than a point hidden beneath a marker. final expandedStart = start > 0 ? start - 1 : start; final expandedEnd = end < samples.length - 1 ? end + 1 : end; final points = []; for (var i = expandedStart; i <= expandedEnd; i++) { final sample = samples[i]; if (sample.latitude != null && sample.longitude != null) { points.add(AppLatLng(sample.latitude!, sample.longitude!)); } } if (points.length >= 2) { polylines.add(AppPolyline( id: 'cornering_layer_$segmentIndex', points: points, color: const Color(0xFF00E5FF), width: 7.0, )); segmentIndex++; } } for (var i = 1; i < eventIndices.length; i++) { final index = eventIndices[i]; if (index - runEnd <= 2) { runEnd = index; } else { emitRun(runStart, runEnd); runStart = runEnd = index; } } emitRun(runStart, runEnd); return polylines; } Color _getSpeedColor(double speedMph) { if (speedMph < 25) return const Color(0xFF00E5FF); if (speedMph < 50) return const Color(0xFF2979FF); if (speedMph < 75) return const Color(0xFFFFD600); if (speedMph < 100) return const Color(0xFFFF6D00); return const Color(0xFFE040FB); } List _getFilteredMarkers() { final markers = []; if (_routePoints.isNotEmpty) { markers.add(AppMapMarker( id: 'route_start', position: _routePoints.first, label: 'START', iconWidget: Container(padding: const EdgeInsets.all(5), decoration: const BoxDecoration(color: Colors.greenAccent, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 4)]), child: const Icon(Icons.flag, color: Colors.black, size: 18)), )); if (_routePoints.length > 1) { markers.add(AppMapMarker( id: 'route_end', position: _routePoints.last, label: 'FINISH', iconWidget: Container(padding: const EdgeInsets.all(5), decoration: const BoxDecoration(color: Colors.amberAccent, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 4)]), child: const Icon(Icons.sports_score, color: Colors.black, size: 18)), )); } } markers.addAll(_eventMarkers.where((m) { final label = (m.label ?? '').toLowerCase(); if (label.contains('accel') && !_showHardAccel) return false; if (label.contains('brak') && !_showHardBraking) return false; if ((label.contains('corner') || label.contains('lean')) && !_showCornering) return false; return true; })); return markers; } void _showRoadAlertDetails(RoadAlert alert) { final swarmService = ref.read(swarmRoadAlertServiceProvider); final canVote = alert.sourceType == RoadAlertSourceType.userReported && swarmService.canUserVote(alert.id); final hasVoted = swarmService.hasUserVoted(alert.id); final isConfirmed = alert.isCommunityConfirmed; showModalBottomSheet( context: context, backgroundColor: Colors.grey[900], shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: Icon(alert.category.icon, color: alert.category.color, size: 28), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( alert.category.displayName.toUpperCase(), style: TextStyle( color: alert.category.color, fontWeight: FontWeight.bold, fontSize: 12, letterSpacing: 1.1, ), ), if (alert.roadName != null && alert.roadName!.isNotEmpty) Text(alert.roadName!, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isConfirmed ? Colors.greenAccent.withValues(alpha: 0.2) : (alert.sourceType == RoadAlertSourceType.userReported ? Colors.orangeAccent.withValues(alpha: 0.2) : Colors.white10), borderRadius: BorderRadius.circular(12), border: Border.all( color: isConfirmed ? Colors.greenAccent : (alert.sourceType == RoadAlertSourceType.userReported ? Colors.orangeAccent : Colors.transparent), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isConfirmed ? Icons.verified : (alert.sourceType == RoadAlertSourceType.userReported ? Icons.schedule : Icons.shield_outlined), size: 12, color: isConfirmed ? Colors.greenAccent : (alert.sourceType == RoadAlertSourceType.userReported ? Colors.orangeAccent : Colors.white70), ), const SizedBox(width: 4), Text( isConfirmed ? 'COMMUNITY CONFIRMED' : (alert.sourceType == RoadAlertSourceType.userReported ? 'UNVERIFIED' : 'VERIFIED'), style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: isConfirmed ? Colors.greenAccent : (alert.sourceType == RoadAlertSourceType.userReported ? Colors.orangeAccent : Colors.white70), ), ), ], ), ), ], ), const SizedBox(height: 16), Text(alert.description, style: const TextStyle(fontSize: 15, height: 1.3)), const SizedBox(height: 12), if (alert.sourceType == RoadAlertSourceType.userReported) Text( 'Trust & Verification: ${alert.confirmations} confirmed · ${alert.dismissals} dismissed', style: const TextStyle(fontSize: 12, color: Colors.white70), ), const SizedBox(height: 12), const Divider(color: Colors.white24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (alert.distanceFromUser != null) Text( 'Distance: ${(alert.distanceFromUser! * 0.000621371).toStringAsFixed(2)} mi', style: const TextStyle( color: Colors.cyanAccent, fontWeight: FontWeight.bold, ), ), Text( 'Source: ${alert.source}', style: const TextStyle(color: Colors.white54, fontSize: 12), ), ], ), if (alert.sourceType == RoadAlertSourceType.userReported) ...[ const SizedBox(height: 16), if (hasVoted) Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: const Text( 'You have already verified this report.', style: TextStyle(color: Colors.white70, fontSize: 12), ), ) else Row( children: [ Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.greenAccent.withValues(alpha: 0.2), foregroundColor: Colors.greenAccent, side: const BorderSide( color: Colors.greenAccent, width: 1.5), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), icon: const Icon(Icons.thumb_up_alt_outlined, size: 18), label: const Text('STILL THERE', style: TextStyle(fontWeight: FontWeight.bold)), onPressed: canVote ? () async { final messenger = ScaffoldMessenger.of(context); Navigator.of(ctx).pop(); await swarmService.confirmAlert(alert.id); messenger.showSnackBar( const SnackBar( content: Text( 'Alert confirmed! Thank you for verifying.'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); } : null, ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent.withValues(alpha: 0.2), foregroundColor: Colors.redAccent, side: const BorderSide( color: Colors.redAccent, width: 1.5), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), icon: const Icon(Icons.thumb_down_alt_outlined, size: 18), label: const Text('NOT THERE', style: TextStyle(fontWeight: FontWeight.bold)), onPressed: canVote ? () async { final messenger = ScaffoldMessenger.of(context); Navigator.of(ctx).pop(); await swarmService.clearAlert(alert.id); messenger.showSnackBar( const SnackBar( content: Text( 'Report marked as clear. Thank you!'), backgroundColor: Colors.orange, duration: Duration(seconds: 2), ), ); } : null, ), ), ], ), ], ], ), ), ); } @override Widget build(BuildContext context) { if (_isLoadingHistory) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } ref.listen(navigationSessionProvider, (prev, next) { if (prev?.isNavigating == true && !next.isNavigating && mounted) { setState(() { _previewRouteResult = null; _selectedDestination = null; }); } }); final isFollowMode = ref.watch(mapFollowStateProvider); final layerSettings = ref.watch(mapLayerSettingsProvider); final colors = context.motorColors; final theme = Theme.of(context); return ResponsiveLayout( builder: (context, responsive) { final isShortLandscape = responsive.isLandscape && responsive.height < 450; if (widget.rideId != null) { final polylines = _buildPolylines(showTwistyHighlights: layerSettings.showTwistyHighlights); return Scaffold( appBar: AppBar( title: Text('Ride #${widget.rideId} Multi-Layer Map'), actions: [ IconButton( key: const ValueKey('preview_layers_button'), tooltip: 'Tactical Layers (MAP-02)', icon: Icon(Icons.layers_outlined, color: colors.cyberCyan), onPressed: () => TacticalLayersBottomSheet.show(context), ), IconButton( key: const ValueKey('play_replay_appbar_button'), tooltip: 'Fire Off Replay', icon: Icon(Icons.play_circle_fill, color: colors.cyberCyan), onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReplayScreen(rideId: widget.rideId!, autoPlay: true), ), ); }, ), ], ), body: Stack( fit: StackFit.expand, children: [ Positioned.fill( child: UnifiedMapView( key: ValueKey('preview_map_${widget.rideId}'), center: _initialCenter ?? _fallbackLocation, zoom: _currentZoom, polylines: polylines, markers: _getFilteredMarkers(), ), ), if (_routePoints.isNotEmpty) _buildMultiLayerFilterOverlay(colors), if (_showSpeedHeatmap) _buildSpeedLegend(colors), if (_routePoints.isEmpty) Positioned( top: 16, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: colors.surfaceContainer.withValues(alpha: 0.95), borderRadius: BorderRadius.circular(12), border: Border.all( color: colors.cyberCyan.withValues(alpha: 0.4), ), ), child: Row( children: [ Icon( Icons.info_outline, color: colors.cyberCyan, size: 20, ), const SizedBox(width: 10), Expanded( child: Text( 'No GPS breadcrumbs were recorded during this test session.', style: TextStyle( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 12, fontFamily: 'Inter', ), ), ), ], ), ), ), // Bottom Center: FIRE OFF REPLAY floating button if (_routePoints.isNotEmpty) Positioned( bottom: 24, left: 20, right: 80, child: Center( child: FilledButton.icon( key: const ValueKey('fire_off_ride_preview_button'), onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReplayScreen( rideId: widget.rideId!, autoPlay: true, ), ), ); }, icon: const Icon(Icons.play_arrow_rounded, size: 22), label: const Text( 'FIRE OFF REPLAY', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12.5, fontWeight: FontWeight.w800, letterSpacing: 0.6, ), ), style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), ), ), ), // Right HUD zoom controls on history preview Positioned( right: 14, bottom: 30, child: Column( mainAxisSize: MainAxisSize.min, children: [ _HudIconButton( key: const ValueKey('zoom_in_button'), icon: Icons.add, tooltip: 'Zoom In', onTap: () { setState(() => _currentZoom = (_currentZoom + 1.0).clamp(3.0, 19.0)); }, ), const SizedBox(height: 8), _HudIconButton( key: const ValueKey('zoom_out_button'), icon: Icons.remove, tooltip: 'Zoom Out', onTap: () { setState(() => _currentZoom = (_currentZoom - 1.0).clamp(3.0, 19.0)); }, ), ], ), ), ], ), ); } final locationAsync = ref.watch(locationStreamProvider); final liveAlertsAsync = ref.watch(activeAlertsStreamProvider); final currentPos = locationAsync.when( data: (pos) => AppLatLng(pos.latitude, pos.longitude), error: (_, _) => _fallbackLocation, loading: () => _fallbackLocation, ); final currentHeading = locationAsync.when( data: (pos) => pos.heading, error: (_, _) => 0.0, loading: () => 0.0, ); final motorcycleMarker = AppMapMarker( id: 'motorcycle_live', position: currentPos, heading: _isNorthUp ? currentHeading : 0.0, iconWidget: Icon( Icons.navigation, color: colors.cyberCyan, size: 40, ), ); final List mapMarkers = [motorcycleMarker]; final liveAlerts = layerSettings.showPoliceTraps ? (liveAlertsAsync.value ?? []) : []; for (var alert in liveAlerts) { mapMarkers.add( AppMapMarker( id: 'alert_${alert.providerId}_${alert.id}', position: AppLatLng(alert.latitude, alert.longitude), heading: alert.heading ?? 0.0, label: alert.category.displayName, iconWidget: Container( padding: const EdgeInsets.all(6), 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: 22, ), ), onTap: () => _showRoadAlertDetails(alert), ), ); } final navSession = ref.watch(navigationSessionProvider); if (_selectedDestination != null) { mapMarkers.add( AppMapMarker( id: 'destination_marker', position: _selectedDestination!.latLng, label: _selectedDestination!.displayName, iconWidget: Container( padding: const EdgeInsets.all(6), decoration: const BoxDecoration( color: Color(0xFFFF0055), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 4)], ), child: const Icon(Icons.flag, color: Colors.white, size: 20), ), ), ); } final livePolylines = []; if (_previewRouteResult != null && _previewRouteResult!.geometry.isNotEmpty) { livePolylines.add( AppPolyline( id: 'route_preview_polyline', points: _previewRouteResult!.geometry, color: colors.cyberCyan, width: 5.5, ), ); } else if (navSession.isNavigating && navSession.calculationResult?.geometry.isNotEmpty == true) { livePolylines.add( AppPolyline( id: 'nav_active_polyline', points: navSession.calculationResult!.geometry, color: colors.cyberCyan, width: 5.5, ), ); } return Scaffold( extendBodyBehindAppBar: isShortLandscape, appBar: AppBar( title: const Text('Live Map'), backgroundColor: isShortLandscape ? colors.surfaceContainer.withValues(alpha: 0.75) : null, elevation: isShortLandscape ? 0 : null, ), body: Stack( fit: StackFit.expand, children: [ Positioned.fill( child: UnifiedMapView( key: const ValueKey('live_map_view'), center: currentPos, zoom: _currentZoom, bearing: _isNorthUp ? 0.0 : currentHeading, markers: mapMarkers, polylines: livePolylines, ), ), // Active Navigation Maneuver HUD or Top Search & Preview Cards if (navSession.isNavigating) const Positioned( top: 10, left: 14, right: 14, child: NavigationManeuverCard(), ) else if (!isShortLandscape) ...[ Positioned( top: 10, left: 14, right: 14, child: _buildTopSearchCard(colors, currentPos), ), if (_searchResults.isNotEmpty) Positioned( top: 106, left: 14, right: 14, child: _buildSearchResultsOverlay(colors, currentPos), ), if (_previewRouteResult != null) Positioned( bottom: 24, left: 14, right: 14, child: _buildRoutePreviewCard(colors, currentPos), ), ], // Offline Cached Only Warning Pill if (layerSettings.offlineOnly) Positioned( top: isShortLandscape ? 8 : 106, left: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF1E1708), borderRadius: BorderRadius.circular(6), border: Border.all( color: colors.warningAmber.withValues(alpha: 0.6), width: 0.8, ), boxShadow: const [ BoxShadow(color: Colors.black45, blurRadius: 4), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.wifi_off, size: 12, color: colors.warningAmber), const SizedBox(width: 5), Text( 'OFFLINE TILES ONLY', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 9.5, fontWeight: FontWeight.bold, color: colors.warningAmber, ), ), ], ), ), ), // Right Vertical HUD Dock (Layers, Compass Orient, Zoom In, Zoom Out, Recenter) Positioned( right: 14, top: isShortLandscape ? 58 : 124, child: Column( mainAxisSize: MainAxisSize.min, children: [ _HudIconButton( key: const ValueKey('tactical_layers_button'), icon: Icons.layers_outlined, tooltip: 'Tactical Layers (MAP-02)', color: colors.cyberCyan, onTap: () => TacticalLayersBottomSheet.show(context), ), const SizedBox(height: 8), _HudIconButton( key: const ValueKey('compass_orient_button'), icon: _isNorthUp ? Icons.explore : Icons.navigation, tooltip: _isNorthUp ? 'North Up' : 'Heading Up', color: _isNorthUp ? Colors.white70 : colors.cyberCyan, onTap: () => setState(() => _isNorthUp = !_isNorthUp), ), const SizedBox(height: 8), _HudIconButton( key: const ValueKey('zoom_in_button'), icon: Icons.add, tooltip: 'Zoom In', onTap: () { ref.read(mapFollowStateProvider.notifier).userInteracted(); setState(() => _currentZoom = (_currentZoom + 1.0).clamp(3.0, 19.0)); }, ), const SizedBox(height: 8), _HudIconButton( key: const ValueKey('zoom_out_button'), icon: Icons.remove, tooltip: 'Zoom Out', onTap: () { ref.read(mapFollowStateProvider.notifier).userInteracted(); setState(() => _currentZoom = (_currentZoom - 1.0).clamp(3.0, 19.0)); }, ), if (!isFollowMode) ...[ const SizedBox(height: 8), _HudIconButton( key: const ValueKey('recenter_rider_button'), icon: Icons.my_location, tooltip: 'Recenter on Rider', color: colors.cyberCyan, backgroundColor: colors.cyberCyan.withValues(alpha: 0.25), onTap: () => ref.read(mapFollowStateProvider.notifier).recenter(), ), ], ], ), ), // Bottom-left Quick Swarm Report Button (Repositioned to prevent card collision and hidden during search/typing) if (MediaQuery.of(context).viewInsets.bottom == 0 && _searchResults.isEmpty) Positioned( bottom: _previewRouteResult != null ? 295 : 24, left: 20, child: FloatingActionButton.extended( key: const ValueKey('quick_swarm_report_button'), backgroundColor: const Color(0xFFFF9100), foregroundColor: Colors.black, icon: const Icon(Icons.campaign, size: 22), label: const Text( 'REPORT', style: TextStyle( fontWeight: FontWeight.bold, letterSpacing: 0.6, ), ), onPressed: () => QuickSwarmReportModal.show(context), ), ), ], ), ); }, ); } Widget _buildMultiLayerFilterOverlay(MotorSignalThemeColors colors) { return Positioned( top: 10, left: 10, right: 10, child: Card( color: colors.surfaceContainer.withValues(alpha: 0.85), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ FilterChip( label: const Text('Route'), selected: _showRoute, onSelected: (v) => setState(() => _showRoute = v), selectedColor: colors.cyberCyan.withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Speed'), selected: _showSpeedHeatmap, onSelected: (v) => setState(() => _showSpeedHeatmap = v), selectedColor: const Color(0xFFE040FB).withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Accel'), selected: _showHardAccel, onSelected: (v) => setState(() => _showHardAccel = v), selectedColor: colors.launchGreen.withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Braking'), selected: _showHardBraking, onSelected: (v) => setState(() => _showHardBraking = v), selectedColor: colors.raceRed.withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Cornering'), selected: _showCornering, onSelected: (v) => setState(() => _showCornering = v), selectedColor: colors.cyberCyan.withValues(alpha: 0.35), ), ], ), ), ), ), ); } Widget _buildSpeedLegend(MotorSignalThemeColors colors) => Positioned( bottom: 20, left: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: colors.surfaceContainer.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(8), border: Border.all(color: colors.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'SPEED RANGES (MPH)', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 8.5, fontWeight: FontWeight.bold, color: colors.cyberCyan, ), ), const SizedBox(height: 4), _buildLegendItem('0–25', const Color(0xFF00E5FF)), _buildLegendItem('25–50', const Color(0xFF2979FF)), _buildLegendItem('50–75', const Color(0xFFFFD600)), _buildLegendItem('75–100', const Color(0xFFFF6D00)), _buildLegendItem('100+', const Color(0xFFE040FB)), ], ), ), ); Widget _buildLegendItem(String label, Color color) => Padding( padding: const EdgeInsets.symmetric(vertical: 1.5), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( label, style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 9, fontWeight: FontWeight.w600, ), ), ], ), ); Widget _buildTopSearchCard(MotorSignalThemeColors colors, AppLatLng currentPos) { final theme = Theme.of(context); return Card( elevation: 6, shadowColor: Colors.black54, color: colors.surfaceContainer.withValues(alpha: 0.95), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: colors.cyberCyan.withValues(alpha: 0.25), width: 0.8, ), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (_isSearching || _isCalculatingRoute) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) else Icon(Icons.search, size: 20, color: colors.cyberCyan), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, style: TextStyle( fontFamily: 'Inter', fontSize: 13, color: theme.colorScheme.onSurface, ), decoration: InputDecoration( hintText: 'Search destinations, twisty passes...', hintStyle: TextStyle( fontFamily: 'Inter', fontSize: 12.5, color: theme.colorScheme.onSurface.withValues(alpha: 0.45), ), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 6), ), onChanged: (val) => _onSearchQueryChanged(val, currentPos), onSubmitted: (val) => _onSearchQueryChanged(val, currentPos), ), ), if (_destinationQuery.isNotEmpty) GestureDetector( key: const ValueKey('clear_search_button'), onTap: _clearDestination, child: Icon( Icons.close, size: 18, color: theme.colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), const SizedBox(height: 6), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildQuickDestinationChip( label: 'Gas Stations', customKey: 'chip_gas_and_fuel', icon: Icons.local_gas_station, colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Twisty Passes', icon: Icons.alt_route, colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Scenic Rest', icon: Icons.photo_camera, colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Repair/Moto', icon: Icons.build, colors: colors, currentPos: currentPos, ), ], ), ), ], ), ), ); } Widget _buildQuickDestinationChip({ required String label, required IconData icon, required MotorSignalThemeColors colors, required AppLatLng currentPos, String? customKey, }) { final isSelected = _destinationQuery.toLowerCase() == label.toLowerCase(); final keyString = customKey ?? 'chip_${label.toLowerCase().replaceAll(' ', '_').replaceAll('/', '_').replaceAll('&', 'and')}'; return InkWell( key: ValueKey(keyString), borderRadius: BorderRadius.circular(20), onTap: () { if (isSelected) { _clearDestination(); } else { _searchController.text = label; _onSearchQueryChanged(label, currentPos); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isSelected ? colors.cyberCyan.withValues(alpha: 0.25) : colors.surfaceContainerHigh.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? colors.cyberCyan : colors.outlineVariant.withValues(alpha: 0.6), width: isSelected ? 1.2 : 0.8, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 13, color: isSelected ? colors.cyberCyan : Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( label, style: TextStyle( fontFamily: 'Inter', fontSize: 11, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? colors.cyberCyan : Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), ); } Widget _buildSearchResultsOverlay( MotorSignalThemeColors colors, AppLatLng currentPos, ) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Card( key: const ValueKey('search_results_overlay'), elevation: 8, shadowColor: Colors.black87, color: theme.colorScheme.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: colors.cyberCyan.withValues(alpha: isDark ? 0.4 : 0.6), width: 1.0, ), ), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 280), child: ListView.separated( shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 6), itemCount: _searchResults.length, separatorBuilder: (_, _) => Divider( height: 1, color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35), ), itemBuilder: (context, index) { final item = _searchResults[index]; return ListTile( dense: true, leading: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.15), shape: BoxShape.circle, ), child: Icon( item.hasCoordinates ? Icons.pin_drop : Icons.place_outlined, size: 18, color: colors.cyberCyan, ), ), title: Text( item.primaryText, style: TextStyle( fontFamily: 'Inter', fontSize: 13, fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), subtitle: item.secondaryText.isNotEmpty ? Text( item.secondaryText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'Inter', fontSize: 11, color: theme.colorScheme.onSurfaceVariant, ), ) : null, selectedTileColor: colors.cyberCyan.withValues(alpha: 0.12), hoverColor: colors.cyberCyan.withValues(alpha: 0.08), onTap: () => _onSelectPlace(item, currentPos), ); }, ), ), ); } Widget _buildRoutePreviewCard( MotorSignalThemeColors colors, AppLatLng currentPos, ) { final preview = _previewRouteResult!; final dest = _selectedDestination; final confirmedPref = preview.options?.preference ?? _routingOptions.preference; return Card( key: const ValueKey('route_preview_card'), elevation: 8, color: colors.surfaceContainer.withValues(alpha: 0.95), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: colors.cyberCyan.withValues(alpha: 0.6), width: 1.2, ), ), child: Padding( padding: const EdgeInsets.all(14), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( key: const ValueKey('destination_address_expand_toggle'), borderRadius: BorderRadius.circular(6), onTap: () => setState(() => _isAddressExpanded = !_isAddressExpanded), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( dest?.displayName ?? 'Destination Route', maxLines: _isAddressExpanded ? 4 : 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface, ), ), ), const SizedBox(width: 4), Icon( _isAddressExpanded ? Icons.expand_less : Icons.expand_more, size: 18, color: colors.cyberCyan, ), ], ), ), const SizedBox(height: 4), Wrap( spacing: 8, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( '${preview.distanceMiles.toStringAsFixed(1)} mi • ${preview.formattedDuration}', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, fontWeight: FontWeight.w700, color: colors.cyberCyan, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: colors.cyberCyan.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: colors.cyberCyan, width: 0.8, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (_isCalculatingRoute) ...[ SizedBox( width: 8, height: 8, child: CircularProgressIndicator( strokeWidth: 1.5, color: colors.cyberCyan, ), ), const SizedBox(width: 4), ], Text( confirmedPref.name.toUpperCase(), style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 9.5, fontWeight: FontWeight.bold, color: colors.cyberCyan, ), ), ], ), ), if (preview.options?.avoidHighways == true) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFFF59E0B).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: const Color(0xFFF59E0B), width: 0.8, ), ), child: const Text( 'NO HIGHWAYS', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 9.5, fontWeight: FontWeight.bold, color: Color(0xFFF59E0B), ), ), ), if (preview.curvatureMetric != null && preview.curvatureMetric! >= 25.0) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFF10B981).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: const Color(0xFF10B981), width: 0.8, ), ), child: Text( 'CURVES: ${preview.twistinessRating}', style: const TextStyle( fontFamily: 'JetBrains Mono', fontSize: 9.5, fontWeight: FontWeight.bold, color: Color(0xFF10B981), ), ), ), ], ), ], ), ), IconButton( key: const ValueKey('clear_route_button'), icon: Icon(Icons.close, color: Theme.of(context).colorScheme.onSurfaceVariant), onPressed: _clearDestination, tooltip: 'Cancel Destination', ), ], ), const SizedBox(height: 10), // Routing Preferences Selector Row SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPreferenceChip( preference: RoutingPreference.fastest, label: 'Fastest', colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildPreferenceChip( preference: RoutingPreference.shortest, label: 'Shortest', colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildPreferenceChip( preference: RoutingPreference.twisty, label: 'Twisty (Curves)', colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), _buildPreferenceChip( preference: RoutingPreference.scenic, label: 'Scenic (No Hwy)', colors: colors, currentPos: currentPos, ), const SizedBox(width: 6), FilterChip( key: const ValueKey('preference_avoid_highways'), label: const Text('Avoid Highways'), selected: _routingOptions.avoidHighways, labelStyle: TextStyle( fontFamily: 'Inter', fontSize: 11, fontWeight: _routingOptions.avoidHighways ? FontWeight.bold : FontWeight.normal, color: _routingOptions.avoidHighways ? Colors.black : Theme.of(context).colorScheme.onSurfaceVariant, ), selectedColor: colors.cyberCyan, backgroundColor: colors.surfaceContainerHigh, onSelected: (selected) { setState(() { _routingOptions = _routingOptions.copyWith(avoidHighways: selected); }); if (_selectedDestination != null) { _calculateRouteToDestination(_selectedDestination!, currentPos); } }, ), ], ), ), const SizedBox(height: 12), // Navigation Actions: Start Turn-by-Turn & External Handoff Row( children: [ Expanded( child: FilledButton.icon( key: const ValueKey('start_navigation_button'), icon: const Icon(Icons.navigation, size: 18), label: const Text( 'START NAVIGATION', style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.5, ), ), style: FilledButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: _startTurnByTurnNavigation, ), ), const SizedBox(width: 8), OutlinedButton( key: const ValueKey('handoff_navigation_button'), style: OutlinedButton.styleFrom( foregroundColor: colors.cyberCyan, side: BorderSide(color: colors.cyberCyan), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), onPressed: () { if (dest != null) { NavigationHandoffService.showHandoffSheet( context: context, latitude: dest.latitude, longitude: dest.longitude, displayName: dest.displayName, ); } }, child: const Icon(Icons.open_in_new, size: 18), ), ], ), ], ), ), ); } Widget _buildPreferenceChip({ required RoutingPreference preference, required String label, required MotorSignalThemeColors colors, required AppLatLng currentPos, }) { final isSelected = _routingOptions.preference == preference; final onVariant = Theme.of(context).colorScheme.onSurfaceVariant; return ChoiceChip( key: ValueKey('preference_${preference.name}'), label: Text(label), selected: isSelected, labelStyle: TextStyle( fontFamily: 'Inter', fontSize: 11, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.black : onVariant, ), selectedColor: colors.cyberCyan, backgroundColor: colors.surfaceContainerHigh, onSelected: (selected) { if (selected) { setState(() { _routingOptions = _routingOptions.copyWith( preference: preference, avoidHighways: preference == RoutingPreference.twisty || preference == RoutingPreference.scenic, ); }); if (_selectedDestination != null) { _calculateRouteToDestination(_selectedDestination!, currentPos); } } }, ); } } class _HudIconButton extends StatelessWidget { final IconData icon; final String? tooltip; final VoidCallback onTap; final Color? color; final Color? backgroundColor; const _HudIconButton({ super.key, required this.icon, this.tooltip, required this.onTap, this.color, this.backgroundColor, }); @override Widget build(BuildContext context) { final colors = context.motorColors; final isDark = Theme.of(context).brightness == Brightness.dark; final defaultBg = isDark ? colors.surfaceContainer : const Color(0xFF1E293B); final button = Material( color: backgroundColor ?? defaultBg.withValues(alpha: 0.9), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide( color: colors.outlineVariant.withValues(alpha: 0.6), width: 0.8, ), ), elevation: 4, shadowColor: Colors.black54, child: InkWell( borderRadius: BorderRadius.circular(10), onTap: onTap, child: Container( width: 40, height: 40, alignment: Alignment.center, child: Icon( icon, size: 20, color: color ?? Colors.white, ), ), ), ); if (tooltip != null) { return Tooltip( message: tooltip!, child: button, ); } return button; } }