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 '../../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 _routePoints = []; List _eventMarkers = []; AppLatLng? _initialCenter; AppLatLng _fallbackLocation = const AppLatLng(37.7749, -122.4194); // SF Default // Event category filter states bool _showHardAccel = true; bool _showHardBraking = true; bool _showCornering = true; bool _showSpeedMilestones = 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(); 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; if (event.eventType.contains('Corner')) markerColor = Colors.orangeAccent; if (event.eventType.contains('Acceleration')) markerColor = Colors.greenAccent; if (event.eventType.contains('Speed')) markerColor = Colors.purpleAccent; _eventMarkers.add( AppMapMarker( id: event.id.toString(), position: AppLatLng(event.latitude!, event.longitude!), label: event.eventType, iconWidget: Icon(Icons.location_on, color: markerColor, size: 36), onTap: () { if (mounted) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(event.eventType), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Time: ${event.timestamp.toIso8601String().substring(11, 19)}'), Text('Value: ${event.value.toStringAsFixed(2)}'), if (event.latitude != null && event.longitude != null) Text('Lat/Lng: ${event.latitude!.toStringAsFixed(4)}, ${event.longitude!.toStringAsFixed(4)}'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Close'), ), ], ), ); } }, ), ); } } setState(() { _isLoadingHistory = false; }); } 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: 20), ), ), ); 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: 20), ), ), ); } } final filteredEvents = _eventMarkers.where((m) { final label = m.label ?? ''; if (label.contains('Acceleration') && !_showHardAccel) return false; if (label.contains('Braking') && !_showHardBraking) return false; if (label.contains('Corner') && !_showCornering) return false; if (label.contains('Speed') && !_showSpeedMilestones) 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(10), decoration: BoxDecoration( color: alert.category.color.withValues(alpha: 0.2), shape: BoxShape.circle, border: Border.all(color: alert.category.color), ), child: Icon(alert.category.icon, color: alert.category.color, size: 28), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( alert.category.displayName.toUpperCase(), style: TextStyle( color: alert.category.color, fontWeight: FontWeight.w900, fontSize: 16, 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 = _routePoints.isNotEmpty ? [AppPolyline(id: 'history_route', points: _routePoints, color: Colors.cyanAccent, width: 5.0)] : []; return Scaffold( appBar: AppBar( title: Text('Ride #${widget.rideId} Route Preview'), 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) _buildEventFilterOverlay(), 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 _buildEventFilterOverlay() { return Positioned( top: 10, left: 10, right: 10, child: Card( color: Colors.black.withValues(alpha: 0.7), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ FilterChip( label: const Text('Accel'), selected: _showHardAccel, onSelected: (val) => setState(() => _showHardAccel = val), selectedColor: Colors.green.withValues(alpha: 0.4), ), const SizedBox(width: 6), FilterChip( label: const Text('Braking'), selected: _showHardBraking, onSelected: (val) => setState(() => _showHardBraking = val), selectedColor: Colors.red.withValues(alpha: 0.4), ), const SizedBox(width: 6), FilterChip( label: const Text('Cornering'), selected: _showCornering, onSelected: (val) => setState(() => _showCornering = val), selectedColor: Colors.orange.withValues(alpha: 0.4), ), const SizedBox(width: 6), FilterChip( label: const Text('Speed'), selected: _showSpeedMilestones, onSelected: (val) => setState(() => _showSpeedMilestones = val), selectedColor: Colors.purple.withValues(alpha: 0.4), ), ], ), ), ), ), ); } }