diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e7c4ef6..b14c7ba 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -171,5 +171,6 @@ "trackingAnAusschalten": "Tracking an/aus-schalten", "trackingStart": "Tracking starten", "trackingStop": "Tracking stoppen", - "trackingPause": "Tracking pausieren" + "trackingPause": "Tracking pausieren", + "accuracy": "Genauigkeit" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 940c915..9e0afc8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -681,5 +681,10 @@ "trackingStop": "Stop tracking", - "trackingPause": "Pause Tracking" + "trackingPause": "Pause Tracking", + + "accuracy": "Accuracy", + "@accuracy": { + "description": "Accuracy of GPS position" + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 67d3c70..8ce4691 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1126,6 +1126,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Pause Tracking'** String get trackingPause; + + /// Accuracy of GPS position + /// + /// In en, this message translates to: + /// **'Accuracy'** + String get accuracy; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3eb2bf9..af325aa 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -523,4 +523,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get trackingPause => 'Tracking pausieren'; + + @override + String get accuracy => 'Genauigkeit'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c9fabb7..2cee217 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -523,4 +523,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackingPause => 'Pause Tracking'; + + @override + String get accuracy => 'Accuracy'; } diff --git a/lib/screens/excursion/widgets/tracking.dart b/lib/screens/excursion/widgets/tracking.dart index 0795586..c846fe6 100644 --- a/lib/screens/excursion/widgets/tracking.dart +++ b/lib/screens/excursion/widgets/tracking.dart @@ -25,6 +25,8 @@ class _TrackingState extends State { LocationMarkerPosition? locationMarkerPosition; MapController mapController = MapController(); StreamSubscription? _positionSubscription; + StreamSubscription? _statsSubscription; + TrackingStats? _currentStats; @override void initState() { @@ -52,6 +54,12 @@ class _TrackingState extends State { accuracy: widget.startPosition.accuracy, ); + // Initialisiere die Statistiken sofort + setState(() { + _currentStats = _trackingService.currentStats; + }); + _trackingService.requestStatsUpdate(); + _positionSubscription = _trackingService.positionStream$.listen((position) { setState(() { locationMarkerPosition = LocationMarkerPosition( @@ -62,6 +70,12 @@ class _TrackingState extends State { }); }); + _statsSubscription = _trackingService.statsStream$.listen((stats) { + setState(() { + _currentStats = stats; + }); + }); + GeolocatorService.alwaysPositionEnabled().then((value) { if (!value && mounted) { Navigator.of(context).pop(); @@ -73,15 +87,45 @@ class _TrackingState extends State { @override void dispose() { _positionSubscription?.cancel(); + _statsSubscription?.cancel(); widget.weg.text = _trackingService.getPathAsString(); super.dispose(); } + 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: Text(AppLocalizations.of(context)!.tracking), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.tracking), + 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); diff --git a/lib/services/tracking_service.dart b/lib/services/tracking_service.dart index 9f48c55..0de03db 100644 --- a/lib/services/tracking_service.dart +++ b/lib/services/tracking_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/services/notification_service.dart'; @@ -12,35 +13,58 @@ class TrackingService { TrackingService._internal(); List pathList = []; + List accuracyList = []; StreamSubscription? positionStream; bool isTracking = false; final _positionController = StreamController.broadcast(); + final _statsController = StreamController.broadcast(); Stream get positionStream$ => _positionController.stream; + Stream get statsStream$ => _statsController.stream; + double? currentAccuracy; + + double _calculateMedianAccuracy(List accuracies) { + if (accuracies.isEmpty) return 0; + if (accuracies.length == 1) return accuracies.first; + + // Kopiere die Liste, um die Originaldaten nicht zu verändern + var sorted = List.from(accuracies)..sort(); + + if (sorted.length % 2 == 0) { + // Bei gerader Anzahl: Durchschnitt der beiden mittleren Werte + int midIndex = sorted.length ~/ 2; + return (sorted[midIndex - 1] + sorted[midIndex]) / 2; + } else { + // Bei ungerader Anzahl: Der mittlere Wert + return sorted[sorted.length ~/ 2]; + } + } + Future startTracking(BuildContext context) async { if (isTracking) return; await NotificationService().initNotification(); - if (context.mounted) { NotificationService().showNotification( title: AppLocalizations.of(context)!.trackingRunningInBackground, ); - } positionStream = Geolocator.getPositionStream( locationSettings: AndroidSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, foregroundNotificationConfig: ForegroundNotificationConfig( - notificationTitle: context.mounted ? AppLocalizations.of(context)!.trackingRunningInBackground : "Tracking", + notificationTitle: AppLocalizations.of(context)!.trackingRunningInBackground, notificationText: "", ), ), ).listen((Position? position) { if (position != null) { pathList.add(LatLng(position.latitude, position.longitude)); + accuracyList.add(position.accuracy); + currentAccuracy = position.accuracy; _positionController.add(position); + _updateStats(); } }); @@ -52,6 +76,60 @@ class TrackingService { isTracking = true; } + TrackingStats? _lastStats; + TrackingStats? get currentStats => _lastStats; + + void _updateStats() { + if (pathList.isEmpty) { + _lastStats = TrackingStats( + currentAccuracy: currentAccuracy ?? 0, + averageAccuracy: 0, + totalDistanceMeters: 0 + ); + _statsController.add(_lastStats!); + return; + } + + double totalDistance = 0; + for (int i = 1; i < pathList.length; i++) { + totalDistance += _calculateDistance( + pathList[i-1].latitude, + pathList[i-1].longitude, + pathList[i].latitude, + pathList[i].longitude + ); + } + + double medianAccuracy = _calculateMedianAccuracy(accuracyList); + + _lastStats = TrackingStats( + currentAccuracy: currentAccuracy ?? 0, + averageAccuracy: medianAccuracy, + totalDistanceMeters: totalDistance + ); + _statsController.add(_lastStats!); + } + + void requestStatsUpdate() { + _updateStats(); + } + + double _calculateDistance(double lat1, double lon1, double lat2, double lon2) { + const double earthRadius = 6371000; // Erdradius in Metern + + double lat1Rad = lat1 * math.pi / 180; + double lat2Rad = lat2 * math.pi / 180; + double deltaLat = (lat2 - lat1) * math.pi / 180; + double deltaLon = (lon2 - lon1) * math.pi / 180; + + double a = math.sin(deltaLat/2) * math.sin(deltaLat/2) + + math.cos(lat1Rad) * math.cos(lat2Rad) * + math.sin(deltaLon/2) * math.sin(deltaLon/2); + + double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)); + return earthRadius * c; + } + void pauseTracking() { positionStream?.pause(); isTracking = false; @@ -66,10 +144,15 @@ class TrackingService { positionStream?.cancel(); NotificationService().deleteNotification(); isTracking = false; + accuracyList.clear(); + currentAccuracy = null; } void clearPath() { pathList.clear(); + accuracyList.clear(); + currentAccuracy = null; + _updateStats(); } String getPathAsString() { @@ -79,5 +162,18 @@ class TrackingService { void dispose() { stopTracking(); _positionController.close(); + _statsController.close(); } +} + +class TrackingStats { + final double currentAccuracy; + final double averageAccuracy; + final double totalDistanceMeters; + + TrackingStats({ + required this.currentAccuracy, + required this.averageAccuracy, + required this.totalDistanceMeters, + }); } \ No newline at end of file