Files
fforte/lib/services/tracking_service.dart
2025-06-06 19:46:29 +02:00

230 lines
6.9 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/services/notification_service.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service, with the Singleton design pattern, that runs the geolocator service that tracks the position of the device.
/// This is needed for excursions
///
/// Start the tracking service via [startTracking]
/// Manage the position stream via [pauseTracking], [stopTracking] and [resumeTracking]
class TrackingService {
// Singleton stuff
static TrackingService? _instance;
factory TrackingService() {
_instance ??= TrackingService._internal();
return _instance!;
}
TrackingService._internal();
/// Resets all values, making it possible to start tracking again.
static void resetInstance() {
if (_instance != null) {
_instance!.dispose();
_instance = null;
}
}
// Variables
// - Stores the tracked coordinates
List<LatLng> pathList = [];
// - Stores all gotten accuracies
List<double> accuracyList = [];
// - Stores timer so that is responsible vor the periodically tracking
Timer? _positionTimer;
bool isTracking = false;
// - Some more Singleton stuff (i guess. Vibecoded it because of lack of time)
BuildContext? _lastContext;
final _positionController = StreamController<Position>.broadcast();
final _statsController = StreamController<TrackingStats>.broadcast();
// - Getter
Stream<Position> get positionStream$ => _positionController.stream;
Stream<TrackingStats> get statsStream$ => _statsController.stream;
// - Stores the last measured accuracy so that it can be displayed in the excursions view double? currentAccuracy;
// Name says it all
double _calculateMedianAccuracy(List<double> accuracies) {
// if one or less values for accuracy are available return that accuracy or 0
if (accuracies.isEmpty) return 0;
if (accuracies.length == 1) return accuracies.first;
// Copy the list so that the original data doesnt get modified
var sorted = List<double>.from(accuracies)..sort();
// Calculates median (not arithmetic mean!!). That is because often the firsed tracked accuracy is about 9000m
if (sorted.length % 2 == 0) {
int midIndex = sorted.length ~/ 2;
return (sorted[midIndex - 1] + sorted[midIndex]) / 2;
} else {
return sorted[sorted.length ~/ 2];
}
}
/// Starts tracking
Future<void> startTracking(BuildContext context) async {
if (isTracking) return;
final LocationSettings locationSettings =
LocationSettings(accuracy: LocationAccuracy.high);
_lastContext = context;
await NotificationService().initNotification();
if (context.mounted) {
NotificationService().showNotification(
title: AppLocalizations.of(context)!.trackingRunningInBackground,
);
}
// Get tracking interval from settings
final prefs = await SharedPreferences.getInstance();
final intervalSeconds = prefs.getInt('trackingInterval') ?? 60;
// Create a timer that triggers position updates
_positionTimer =
Timer.periodic(Duration(seconds: intervalSeconds), (_) async {
try {
final Position position = await Geolocator.getCurrentPosition(
locationSettings: locationSettings);
pathList.add(LatLng(position.latitude, position.longitude));
accuracyList.add(position.accuracy);
currentAccuracy = position.accuracy;
_positionController.add(position);
_updateStats();
} catch (e) {
NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e");
}
});
// Get initial position immediately
try {
final Position position = await Geolocator.getCurrentPosition(
locationSettings: locationSettings);
pathList.add(LatLng(position.latitude, position.longitude));
accuracyList.add(position.accuracy);
currentAccuracy = position.accuracy;
_positionController.add(position);
_updateStats();
} catch (e) {
NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e");
}
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() {
_positionTimer?.cancel();
isTracking = false;
}
void resumeTracking() {
if (!isTracking && _lastContext != null) {
startTracking(_lastContext!);
}
isTracking = true;
}
void stopTracking() {
_positionTimer?.cancel();
NotificationService().deleteNotification();
isTracking = false;
accuracyList.clear();
currentAccuracy = null;
_lastContext = null;
}
void clearPath() {
pathList.clear();
accuracyList.clear();
currentAccuracy = null;
_updateStats();
}
String getPathAsString() {
return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";");
}
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,
});
}