Files
fforte/lib/screens/excursion/widgets/tracking.dart

278 lines
8.9 KiB
Dart

// * Widget for GPS tracking during wildlife monitoring excursions
// * Features:
// * - Real-time location tracking
// * - Track visualization on map
// * - Distance calculation
// * - Location accuracy monitoring
// * - Track recording controls (start/pause/stop)
// * - Track data persistence
import 'dart:async';
import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/screens/addCam/services/geolocator_service.dart';
import 'package:fforte/screens/helper/add_entries_dialog_helper.dart';
import 'package:fforte/screens/helper/snack_bar_helper.dart';
import 'package:fforte/services/tracking_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
/// Widget for managing GPS tracking functionality
/// Provides map visualization and tracking controls
class Tracking extends StatefulWidget {
/// Initial position for the tracking session
final Position startPosition;
/// Controller for storing the tracked path
final TextEditingController weg;
const Tracking({super.key, required this.startPosition, required this.weg});
@override
State<Tracking> createState() => _TrackingState();
}
/// State class for the tracking widget
class _TrackingState extends State<Tracking> {
/// Service for managing tracking functionality
final TrackingService _trackingService = TrackingService();
/// Current GPS position
Position? currentPosition;
/// Controller for the map widget
MapController mapController = MapController();
/// Subscription for position updates
StreamSubscription? _positionSubscription;
/// Subscription for tracking statistics updates
StreamSubscription? _statsSubscription;
/// Current tracking statistics
TrackingStats? _currentStats;
@override
void initState() {
super.initState();
// Load existing track if available
if (widget.weg.text.isNotEmpty) {
for (var element in widget.weg.text.split(";")) {
List<String> posSplit = element.split(",");
try {
posSplit[0] = posSplit[0].substring(0, 9);
posSplit[1] = posSplit[1].substring(0, 9);
} on RangeError {
// ignore because the double is short enough then
}
_trackingService.pathList.add(
LatLng(double.parse(posSplit.first), double.parse(posSplit[1])),
);
}
}
currentPosition = widget.startPosition;
// Initialize tracking statistics
setState(() {
_currentStats = _trackingService.currentStats;
});
_trackingService.requestStatsUpdate();
// Subscribe to position updates
_positionSubscription = _trackingService.positionStream$.listen((position) {
setState(() {
currentPosition = position;
});
});
// Subscribe to statistics updates
_statsSubscription = _trackingService.statsStream$.listen((stats) {
setState(() {
_currentStats = stats;
});
});
// Check location permissions
GeolocatorService.alwaysPositionEnabled().then((value) {
if (!value && mounted) {
Navigator.of(context).pop();
SnackBarHelper.showSnackBarMessage(context, "${AppLocalizations.of(context)!.locationForbidden} ${AppLocalizations.of(context)!.oder} ${AppLocalizations.of(context)!.locationDisabled}");
}
});
}
@override
void dispose() {
_positionSubscription?.cancel();
_statsSubscription?.cancel();
widget.weg.text = _trackingService.getPathAsString();
super.dispose();
}
/// Format distance for display
/// @param meters Distance in meters
/// @return Formatted distance string with appropriate unit
String _formatDistance(double meters) {
if (meters >= 1000) {
return '${(meters / 1000).toStringAsFixed(2)} km';
}
return '${meters.toStringAsFixed(1)} m';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context)!.tracking),
// Display tracking statistics if available
if (_currentStats != null)
DefaultTextStyle(
style: Theme.of(context).textTheme.bodySmall!,
child: Row(
children: [
Expanded(
child: Text(
'${AppLocalizations.of(context)!.accuracy}: ${_currentStats!.currentAccuracy.toStringAsFixed(1)}m (∅ ${_currentStats!.averageAccuracy.toStringAsFixed(1)}m)',
overflow: TextOverflow.ellipsis,
),
),
Text(
_formatDistance(_currentStats!.totalDistanceMeters),
),
],
),
),
],
),
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(Icons.arrow_back_rounded)
),
actions: [
// Delete track button (only when not tracking)
if (!_trackingService.isTracking)
IconButton(
onPressed: () async {
bool delete = await AddEntriesDialogHelper.deleteCompleteRouteDialog(context);
if (delete) {
setState(() {
_trackingService.clearPath();
});
}
},
icon: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.errorContainer,
),
),
// Stop tracking button (only when tracking)
if (_trackingService.isTracking)
TextButton(
onPressed: () {
setState(() {
_trackingService.stopTracking();
});
},
child: Text(AppLocalizations.of(context)!.trackingStop),
),
// Start/Pause tracking button
TextButton(
onPressed: () {
setState(() {
if (_trackingService.isTracking) {
_trackingService.pauseTracking();
} else {
_trackingService.startTracking(context);
}
});
},
child: _trackingService.isTracking
? Text(AppLocalizations.of(context)!.trackingPause)
: Text(AppLocalizations.of(context)!.trackingStart),
),
],
),
// Center on current location button
floatingActionButton: FloatingActionButton(
onPressed: () {
mapController.move(
LatLng(
currentPosition!.latitude,
currentPosition!.longitude,
),
16,
);
},
child: Icon(Icons.my_location),
),
// Map display
body: FlutterMap(
mapController: mapController,
options: MapOptions(
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.pinchZoom |
InteractiveFlag.drag |
InteractiveFlag.pinchMove,
),
initialCenter: LatLng(
widget.startPosition.latitude,
widget.startPosition.longitude,
),
initialZoom: 16.0,
),
children: [
// Base map layer
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'de.lupus.apps',
),
// Track path layer
if (_trackingService.pathList.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
strokeWidth: 2.0,
points: _trackingService.pathList,
color: Colors.red
),
],
),
// Current position accuracy circle
if (currentPosition != null)
CircleLayer(
circles: [
CircleMarker(
point: LatLng(
currentPosition!.latitude,
currentPosition!.longitude,
),
radius: currentPosition!.accuracy,
color: Colors.blue.withAlpha(2),
borderColor: Colors.blue,
borderStrokeWidth: 2,
),
CircleMarker(
point: LatLng(
currentPosition!.latitude,
currentPosition!.longitude,
),
radius: 5,
color: Colors.blue,
borderColor: Colors.white,
borderStrokeWidth: 2,
),
],
),
// Current location marker
CurrentLocationLayer(),
],
),
);
}
}