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_service.dart'; import '../../services/alerts/widgets/quick_swarm_report_modal.dart'; import '../../services/map/map_layer_settings.dart'; import 'widgets/tactical_layers_bottom_sheet.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; 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() { _searchController.dispose(); super.dispose(); } 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) { 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: Colors.white10, borderRadius: BorderRadius.circular(12)), child: Text(alert.sourceType == RoadAlertSourceType.userReported ? 'User Reported' : 'Verified Provider', style: const TextStyle(fontSize: 10, color: Colors.white70))), ]), const SizedBox(height: 16), Text(alert.description, style: const TextStyle(fontSize: 15, height: 1.3)), const SizedBox(height: 16), 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)), ]), ]), ), ); } @override Widget build(BuildContext context) { if (_isLoadingHistory) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } 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), ), ], ), 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', ), ), ), ], ), ), ), // 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), ), ); } 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, markers: mapMarkers, ), ), // Top Floating Search Card (portrait and large screens) if (!isShortLandscape) Positioned( top: 10, left: 14, right: 14, child: _buildTopSearchCard(colors), ), // 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 Positioned( bottom: 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) { 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: [ 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) { setState(() => _destinationQuery = val); }, ), ), if (_destinationQuery.isNotEmpty) GestureDetector( key: const ValueKey('clear_search_button'), onTap: () { _searchController.clear(); setState(() => _destinationQuery = ''); }, 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 & Fuel', icon: Icons.local_gas_station, colors: colors, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Twisty Passes', icon: Icons.alt_route, colors: colors, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Scenic Rest', icon: Icons.photo_camera, colors: colors, ), const SizedBox(width: 6), _buildQuickDestinationChip( label: 'Repair/Moto', icon: Icons.build, colors: colors, ), ], ), ), ], ), ), ); } Widget _buildQuickDestinationChip({ required String label, required IconData icon, required MotorSignalThemeColors colors, }) { final isSelected = _destinationQuery.toLowerCase() == label.toLowerCase(); return InkWell( key: ValueKey('chip_${label.toLowerCase().replaceAll(' ', '_').replaceAll('/', '_').replaceAll('&', 'and')}'), borderRadius: BorderRadius.circular(20), onTap: () { setState(() { if (isSelected) { _destinationQuery = ''; _searchController.clear(); } else { _destinationQuery = label; _searchController.text = label; } }); }, 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 : Colors.white70, ), const SizedBox(width: 4), Text( label, style: TextStyle( fontFamily: 'Inter', fontSize: 11, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? colors.cyberCyan : Colors.white70, ), ), ], ), ), ); } } 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 button = Material( color: backgroundColor ?? colors.surfaceContainer.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; } }