215 lines
6.3 KiB
Dart
215 lines
6.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:drift/drift.dart';
|
|
import '../../database/database.dart';
|
|
import '../../core/theme/map_style.dart';
|
|
import '../../services/location_service.dart';
|
|
|
|
class MapScreen extends ConsumerStatefulWidget {
|
|
final int? rideId;
|
|
const MapScreen({super.key, this.rideId});
|
|
|
|
@override
|
|
ConsumerState<MapScreen> createState() => _MapScreenState();
|
|
}
|
|
|
|
class _MapScreenState extends ConsumerState<MapScreen> {
|
|
GoogleMapController? _mapController;
|
|
MapType _currentMapType = MapType.normal;
|
|
|
|
bool _isLoadingHistory = true;
|
|
final Set<Polyline> _polylines = {};
|
|
final Set<Marker> _markers = {};
|
|
LatLng? _initialCameraPosition;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.rideId != null) {
|
|
_loadRideHistory();
|
|
} else {
|
|
_isLoadingHistory = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _loadRideHistory() async {
|
|
final db = ref.read(databaseProvider);
|
|
|
|
// Fetch telemetry for route
|
|
final telemetry = await (db.select(db.telemetrySamples)
|
|
..where((t) => t.rideId.equals(widget.rideId!))
|
|
..orderBy([(t) => OrderingTerm(expression: t.timestamp)])
|
|
).get();
|
|
|
|
// Fetch events for markers
|
|
final events = await (db.select(db.rideEvents)
|
|
..where((e) => e.rideId.equals(widget.rideId!))
|
|
).get();
|
|
|
|
if (telemetry.isNotEmpty) {
|
|
final List<LatLng> points = telemetry
|
|
.where((t) => t.latitude != null && t.longitude != null)
|
|
.map((t) => LatLng(t.latitude!, t.longitude!))
|
|
.toList();
|
|
|
|
if (points.isNotEmpty) {
|
|
_initialCameraPosition = points.first;
|
|
_polylines.add(
|
|
Polyline(
|
|
polylineId: const PolylineId('route'),
|
|
points: points,
|
|
color: Colors.cyanAccent,
|
|
width: 5,
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
for (final event in events) {
|
|
if (event.latitude != null && event.longitude != null) {
|
|
BitmapDescriptor icon = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed);
|
|
if (event.eventType == 'Extreme Lean') icon = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueOrange);
|
|
if (event.eventType == 'Hard Acceleration') icon = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen);
|
|
if (event.eventType == 'High Speed') icon = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueViolet);
|
|
|
|
_markers.add(
|
|
Marker(
|
|
markerId: MarkerId(event.id.toString()),
|
|
position: LatLng(event.latitude!, event.longitude!),
|
|
icon: icon,
|
|
infoWindow: InfoWindow(
|
|
title: event.eventType,
|
|
snippet: 'Value: ${event.value.toStringAsFixed(2)}',
|
|
),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
_isLoadingHistory = false;
|
|
});
|
|
|
|
// Adjust camera to fit route if map is ready
|
|
_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, // padding
|
|
)
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoadingHistory) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
|
|
Widget mapContent;
|
|
|
|
if (widget.rideId != null) {
|
|
// Historical Ride View
|
|
mapContent = GoogleMap(
|
|
initialCameraPosition: CameraPosition(
|
|
target: _initialCameraPosition ?? const LatLng(0, 0),
|
|
zoom: 15.0,
|
|
),
|
|
mapType: _currentMapType,
|
|
polylines: _polylines,
|
|
markers: _markers,
|
|
myLocationEnabled: false,
|
|
onMapCreated: (controller) {
|
|
_mapController = controller;
|
|
_fitRoute();
|
|
},
|
|
);
|
|
} else {
|
|
// Live Tracking View
|
|
final locationAsync = ref.watch(locationStreamProvider);
|
|
|
|
mapContent = locationAsync.when(
|
|
data: (position) {
|
|
final currentLatLng = LatLng(position.latitude, position.longitude);
|
|
|
|
if (_mapController != null) {
|
|
_mapController!.animateCamera(
|
|
CameraUpdate.newCameraPosition(
|
|
CameraPosition(
|
|
target: currentLatLng,
|
|
zoom: 17.0,
|
|
bearing: position.heading,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return GoogleMap(
|
|
initialCameraPosition: CameraPosition(
|
|
target: currentLatLng,
|
|
zoom: 17.0,
|
|
),
|
|
mapType: _currentMapType,
|
|
myLocationEnabled: true,
|
|
myLocationButtonEnabled: true,
|
|
compassEnabled: true,
|
|
onMapCreated: (controller) {
|
|
_mapController = controller;
|
|
_mapController?.setMapStyle(darkMapStyle);
|
|
},
|
|
);
|
|
},
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.rideId != null ? 'Ride ${widget.rideId} Map' : 'Map'),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(
|
|
_currentMapType == MapType.normal
|
|
? Icons.satellite
|
|
: Icons.map,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
_currentMapType = _currentMapType == MapType.normal
|
|
? MapType.satellite
|
|
: MapType.normal;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: mapContent,
|
|
);
|
|
}
|
|
}
|