import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'dart:async'; import '../../core/theme/map_style.dart'; import 'package:drift/drift.dart' hide Column; import '../../database/database.dart'; class ReplayScreen extends ConsumerStatefulWidget { final int rideId; const ReplayScreen({super.key, required this.rideId}); @override ConsumerState createState() => _ReplayScreenState(); } class _ReplayScreenState extends ConsumerState { GoogleMapController? _mapController; bool _isLoading = true; List _telemetry = []; final Set _polylines = {}; // Playback state bool _isPlaying = false; double _playbackSpeed = 1.0; int _currentIndex = 0; Timer? _playbackTimer; @override void initState() { super.initState(); _loadData(); } @override void dispose() { _playbackTimer?.cancel(); super.dispose(); } Future _loadData() async { final db = ref.read(databaseProvider); final telemetry = await (db.select(db.telemetrySamples) ..where((t) => t.rideId.equals(widget.rideId)) ..orderBy([(t) => OrderingTerm(expression: t.timestamp)]) ).get(); if (telemetry.isNotEmpty) { final List points = telemetry .where((t) => t.latitude != null && t.longitude != null) .map((t) => LatLng(t.latitude!, t.longitude!)) .toList(); if (points.isNotEmpty) { _polylines.add( Polyline( polylineId: const PolylineId('route'), points: points, color: Colors.cyanAccent, width: 5, ) ); } } if (mounted) { setState(() { _telemetry = telemetry; _isLoading = false; }); _fitRoute(); } } void _fitRoute() { if (_mapController == null || _polylines.isEmpty) return; final points = _polylines.first.points; if (points.isEmpty) return; double minLat = points.first.latitude; double maxLat = points.first.latitude; double minLng = points.first.longitude; double maxLng = points.first.longitude; for (final p in points) { if (p.latitude < minLat) minLat = p.latitude; if (p.latitude > maxLat) maxLat = p.latitude; if (p.longitude < minLng) minLng = p.longitude; if (p.longitude > maxLng) maxLng = p.longitude; } _mapController!.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds(southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng)), 50.0, ) ); } void _togglePlayback() { setState(() { _isPlaying = !_isPlaying; }); if (_isPlaying) { if (_currentIndex >= _telemetry.length - 1) { _currentIndex = 0; // restart } _scheduleNextFrame(); } else { _playbackTimer?.cancel(); } } void _scheduleNextFrame() { if (!_isPlaying || _currentIndex >= _telemetry.length - 1) { setState(() => _isPlaying = false); return; } final currentPoint = _telemetry[_currentIndex]; final nextPoint = _telemetry[_currentIndex + 1]; final diffMs = nextPoint.timestamp.difference(currentPoint.timestamp).inMilliseconds; // Cap at 1 second to avoid massive pauses if GPS dropped out final waitMs = (diffMs.clamp(0, 1000) / _playbackSpeed).round(); _playbackTimer = Timer(Duration(milliseconds: waitMs), () { if (mounted) { setState(() { _currentIndex++; }); // Follow camera if (_mapController != null) { final t = _telemetry[_currentIndex]; if (t.latitude != null && t.longitude != null) { _mapController!.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: LatLng(t.latitude!, t.longitude!), zoom: 18.0, bearing: t.heading ?? 0, ) ) ); } } _scheduleNextFrame(); } }); } void _changeSpeed() { setState(() { if (_playbackSpeed == 0.25) { _playbackSpeed = 0.5; } else if (_playbackSpeed == 0.5) { _playbackSpeed = 1.0; } else if (_playbackSpeed == 1.0) { _playbackSpeed = 2.0; } else if (_playbackSpeed == 2.0) { _playbackSpeed = 4.0; } else { _playbackSpeed = 0.25; } }); } @override Widget build(BuildContext context) { if (_isLoading) return const Scaffold(body: Center(child: CircularProgressIndicator())); if (_telemetry.isEmpty) return const Scaffold(body: Center(child: Text('No telemetry data to replay.'))); final currentSample = _telemetry[_currentIndex]; final Set markers = {}; if (currentSample.latitude != null && currentSample.longitude != null) { markers.add( Marker( markerId: const MarkerId('current'), position: LatLng(currentSample.latitude!, currentSample.longitude!), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan), rotation: currentSample.heading ?? 0, flat: true, ) ); } return Scaffold( appBar: AppBar(title: Text('Replay - Ride ${widget.rideId}')), body: Column( children: [ // Map Expanded( flex: 3, child: Stack( children: [ GoogleMap( initialCameraPosition: const CameraPosition(target: LatLng(0, 0), zoom: 15), polylines: _polylines, markers: markers, onMapCreated: (controller) { _mapController = controller; controller.setMapStyle(darkMapStyle); _fitRoute(); }, ), // Telemetry Overlay Positioned( top: 16, left: 16, right: 16, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildOverlayStat('SPEED', '${currentSample.speed?.toStringAsFixed(0) ?? 0} mph'), _buildOverlayStat('LEAN', '${currentSample.leanAngle?.abs().toStringAsFixed(0) ?? 0}°'), _buildOverlayStat('G', '${currentSample.longitudinalG?.abs().toStringAsFixed(2) ?? 0}G'), ], ), ), ], ), ), // Controls Container( padding: const EdgeInsets.all(16), color: Theme.of(context).colorScheme.surface, child: Column( children: [ Text(currentSample.timestamp.toLocal().toString().split('.')[0], style: const TextStyle(color: Colors.cyanAccent)), Slider( value: _currentIndex.toDouble(), min: 0, max: (_telemetry.length - 1).toDouble(), onChanged: (val) { setState(() { _currentIndex = val.toInt(); }); }, activeColor: Colors.cyanAccent, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.restart_alt), onPressed: () { setState(() { _currentIndex = 0; }); if (_mapController != null && _telemetry.isNotEmpty) { final t = _telemetry.first; if (t.latitude != null && t.longitude != null) { _mapController!.animateCamera(CameraUpdate.newLatLng(LatLng(t.latitude!, t.longitude!))); } } }, ), const SizedBox(width: 16), FloatingActionButton( onPressed: _togglePlayback, child: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), ), const SizedBox(width: 16), TextButton( onPressed: _changeSpeed, child: Text('${_playbackSpeed}x'), ) ], ) ], ), ) ], ), ); } Widget _buildOverlayStat(String label, String value) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white24), ), child: Column( children: [ Text(label, style: const TextStyle(fontSize: 10, color: Colors.white70)), Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.cyanAccent)), ], ), ); } }