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'; 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() { return _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; }).toList(); } @override Widget build(BuildContext context) { if (_isLoadingHistory) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } final activeProviderType = ref.watch(mapProviderTypeNotifierProvider); final isFollowMode = ref.watch(mapFollowStateProvider); 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'), 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: Stack( children: [ UnifiedMapView( center: _initialCenter ?? _fallbackLocation, zoom: 15.0, polylines: polylines, markers: _getFilteredMarkers(), ), _buildEventFilterOverlay(), ], ), ); } // Live Tracking View final locationAsync = ref.watch(locationStreamProvider); 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, ), ); return Scaffold( appBar: AppBar( title: const Text('Live Map'), 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 (Primary)'), ), const PopupMenuItem( value: AppMapType.googleMaps, child: Text('Google Maps'), ), ], ), ], ), body: Stack( children: [ UnifiedMapView( center: currentPos, zoom: 17.0, markers: [motorcycleMarker], ), 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), ), ], ), ), ), ), ); } }