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 '../../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'; 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); // SF Default // Layer toggle states bool _showRoute = true; bool _showSpeedHeatmap = false; bool _showHardAccel = true; bool _showHardBraking = true; bool _showCornering = true; @override void initState() { super.initState(); if (widget.rideId != null) { _loadRideHistory(); } else { _isLoadingHistory = false; _initLiveLocation(); } } 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; if (telemetry.isNotEmpty) { _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 = []; for (final event in events) { if (event.latitude != null && event.longitude != null) { Color markerColor = Colors.redAccent; IconData markerIcon = Icons.warning_amber_rounded; final typeLower = event.eventType.toLowerCase(); 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; } _eventMarkers.add( AppMapMarker( id: 'event_${event.id}', position: AppLatLng(event.latitude!, event.longitude!), label: event.eventType, 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) { final timeStr = DateFormat('HH:mm:ss.SSS').format(event.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( event.eventType.toUpperCase(), style: const TextStyle( fontFamily: 'Sora', fontSize: 14, fontWeight: FontWeight.w900, color: Colors.white, ), ), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDialogRow('Timestamp:', timeStr), _buildDialogRow('Event Value:', event.value.toStringAsFixed(2)), _buildDialogRow('GPS Location:', '${event.latitude!.toStringAsFixed(5)}, ${event.longitude!.toStringAsFixed(5)}'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('CLOSE', style: TextStyle(color: Colors.white70, fontFamily: 'JetBrains Mono')), ), ], ), ); } }, ), ); } } if (mounted) { setState(() { _isLoadingHistory = false; }); } } Widget _buildDialogRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontFamily: 'Inter', fontSize: 11, color: Colors.white54)), Text(value, style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, fontWeight: FontWeight.bold, color: Colors.white)), ], ), ); } List _buildPolylines() { if (_routePoints.isEmpty) return []; if (!_showSpeedHeatmap) { if (!_showRoute) return []; // Standard cyan route polyline return [ AppPolyline( id: 'history_route', points: _routePoints, color: const Color(0xFF00E5FF), width: 4.5, ), ]; } // Multi-color segmented polyline according to speed brackets final polylines = []; final validSamples = _telemetrySamples.where((t) => t.latitude != null && t.longitude != null).toList(); if (validSamples.isEmpty) return []; // Step downsampling if large dataset 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, )); } return polylines; } Color _getSpeedColor(double speedMph) { if (speedMph < 25) return const Color(0xFF00E5FF); // 0 - 25 MPH (Cyan) if (speedMph < 50) return const Color(0xFF2979FF); // 25 - 50 MPH (Blue) if (speedMph < 75) return const Color(0xFFFFD600); // 50 - 75 MPH (Yellow) if (speedMph < 100) return const Color(0xFFFF6D00); // 75 - 100 MPH (Orange) return const Color(0xFFE040FB); // 100+ MPH (Magenta) } 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), ), ), ); } } final filteredEvents = _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; }); markers.addAll(filteredEvents); 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.0), 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 activeProviderType = ref.watch(mapProviderTypeNotifierProvider); final isFollowMode = ref.watch(mapFollowStateProvider); final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; if (widget.rideId != null) { // Historical Route View final polylines = _buildPolylines(); return Scaffold( appBar: AppBar( title: Text('Ride #${widget.rideId} Multi-Layer Map'), actions: [ IconButton( tooltip: 'Switch Map Provider', icon: Icon(activeProviderType == AppMapType.openStreetMap ? Icons.map : Icons.map_outlined), onPressed: () { final nextType = activeProviderType == AppMapType.openStreetMap ? AppMapType.googleMaps : AppMapType.openStreetMap; ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(nextType); }, ), ], ), body: LayoutBuilder( builder: (context, constraints) { return Stack( fit: StackFit.expand, children: [ Positioned.fill( child: SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: UnifiedMapView( key: ValueKey('preview_map_${constraints.maxWidth.toInt()}x${constraints.maxHeight.toInt()}'), center: _initialCenter ?? _fallbackLocation, zoom: 15.0, polylines: polylines, markers: _getFilteredMarkers(), ), ), ), if (_routePoints.isNotEmpty) _buildMultiLayerFilterOverlay(), if (_showSpeedHeatmap) _buildSpeedLegend(), if (_routePoints.isEmpty) Positioned( top: 16, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: const Color(0xFF0D0D0D).withValues(alpha: 0.9), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF00E5FF).withValues(alpha: 0.4)), ), child: const Row( children: [ Icon(Icons.info_outline, color: Color(0xFF00E5FF), size: 20), SizedBox(width: 10), Expanded( child: Text( 'No GPS breadcrumbs were recorded during this test session.', style: TextStyle(color: Colors.white70, fontSize: 12, fontFamily: 'Inter'), ), ), ], ), ), ), ], ); }, ), ); } // Live Tracking View 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: currentHeading, iconWidget: const Icon( Icons.navigation, color: Colors.cyanAccent, size: 40, ), ); final List mapMarkers = [motorcycleMarker]; final liveAlerts = 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: isLandscape, appBar: AppBar( title: const Text('Live Map'), backgroundColor: isLandscape ? Colors.black.withValues(alpha: 0.6) : null, elevation: isLandscape ? 0 : null, actions: [ PopupMenuButton( icon: const Icon(Icons.layers), onSelected: (type) { ref.read(mapProviderTypeNotifierProvider.notifier).setMapProviderType(type); }, itemBuilder: (context) => [ const PopupMenuItem( value: AppMapType.openStreetMap, child: Text('OpenStreetMap / Tactical (Primary)'), ), const PopupMenuItem( value: AppMapType.googleMaps, child: Text('Google Maps'), ), ], ), ], ), body: LayoutBuilder( builder: (context, constraints) { return Stack( fit: StackFit.expand, children: [ Positioned.fill( child: SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: UnifiedMapView( key: ValueKey('live_map_${constraints.maxWidth.toInt()}x${constraints.maxHeight.toInt()}'), center: currentPos, zoom: 17.0, markers: mapMarkers, ), ), ), if (!isFollowMode) Positioned( bottom: 24, right: 24, child: FloatingActionButton.extended( backgroundColor: Colors.cyanAccent, foregroundColor: Colors.black, icon: const Icon(Icons.my_location), label: const Text('RECENTER', style: TextStyle(fontWeight: FontWeight.bold)), onPressed: () { ref.read(mapFollowStateProvider.notifier).recenter(); }, ), ), ], ); }, ), ); } Widget _buildMultiLayerFilterOverlay() { return Positioned( top: 10, left: 10, right: 10, child: Card( color: Colors.black.withValues(alpha: 0.8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide(color: Color(0xFF1F1F1F)), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ FilterChip( label: const Text('Route'), selected: _showRoute, onSelected: (val) => setState(() => _showRoute = val), selectedColor: const Color(0xFF00E5FF).withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Speed'), selected: _showSpeedHeatmap, onSelected: (val) => setState(() => _showSpeedHeatmap = val), selectedColor: const Color(0xFFE040FB).withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Accel'), selected: _showHardAccel, onSelected: (val) => setState(() => _showHardAccel = val), selectedColor: const Color(0xFF76FF03).withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Braking'), selected: _showHardBraking, onSelected: (val) => setState(() => _showHardBraking = val), selectedColor: const Color(0xFFFF5252).withValues(alpha: 0.35), ), const SizedBox(width: 6), FilterChip( label: const Text('Cornering'), selected: _showCornering, onSelected: (val) => setState(() => _showCornering = val), selectedColor: const Color(0xFF00E5FF).withValues(alpha: 0.35), ), ], ), ), ), ), ); } Widget _buildSpeedLegend() { return Positioned( bottom: 20, left: 14, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF222222)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'SPEED RANGES (MPH)', style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 8.5, fontWeight: FontWeight.bold, color: Colors.white70), ), 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) { return 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, color: Colors.white)), ], ), ); } }