267 lines
7.9 KiB
Dart
267 lines
7.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 that runs the geolocator service to track the device's position
|
|
/// Uses the Singleton design pattern to ensure only one tracking instance exists
|
|
/// This service is essential for excursion tracking functionality
|
|
///
|
|
/// Start tracking via [startTracking]
|
|
/// Control tracking via [pauseTracking], [stopTracking], and [resumeTracking]
|
|
class TrackingService {
|
|
// Singleton implementation
|
|
static TrackingService? _instance;
|
|
|
|
factory TrackingService() {
|
|
_instance ??= TrackingService._internal();
|
|
return _instance!;
|
|
}
|
|
|
|
TrackingService._internal();
|
|
|
|
/// Reset all tracking values for a fresh start
|
|
static void resetInstance() {
|
|
if (_instance != null) {
|
|
_instance!.dispose();
|
|
_instance = null;
|
|
}
|
|
}
|
|
|
|
// Core tracking variables
|
|
// Stores tracked GPS coordinates
|
|
List<LatLng> pathList = [];
|
|
// Stores GPS accuracy values
|
|
List<double> accuracyList = [];
|
|
// Timer for periodic tracking
|
|
Timer? _positionTimer;
|
|
// Current tracking status
|
|
bool isTracking = false;
|
|
// Context for UI interactions
|
|
BuildContext? _lastContext;
|
|
// Stream controllers for position and stats updates
|
|
final _positionController = StreamController<Position>.broadcast();
|
|
final _statsController = StreamController<TrackingStats>.broadcast();
|
|
|
|
// Stream getters
|
|
Stream<Position> get positionStream$ => _positionController.stream;
|
|
Stream<TrackingStats> get statsStream$ => _statsController.stream;
|
|
|
|
// Last measured GPS accuracy for display
|
|
double? currentAccuracy;
|
|
|
|
/// Calculate median accuracy from a list of accuracy values
|
|
/// This is preferred over mean because initial accuracy values can be very high (~9000m)
|
|
double _calculateMedianAccuracy(List<double> accuracies) {
|
|
if (accuracies.isEmpty) return 0;
|
|
if (accuracies.length == 1) return accuracies.first;
|
|
|
|
// Copy list to preserve original data
|
|
var sorted = List<double>.from(accuracies)..sort();
|
|
|
|
// Calculate median
|
|
if (sorted.length % 2 == 0) {
|
|
int midIndex = sorted.length ~/ 2;
|
|
return (sorted[midIndex - 1] + sorted[midIndex]) / 2;
|
|
} else {
|
|
return sorted[sorted.length ~/ 2];
|
|
}
|
|
}
|
|
|
|
/// Start position tracking
|
|
/// - Initializes high-accuracy GPS tracking
|
|
/// - Sets up periodic position updates
|
|
/// - Shows tracking notification
|
|
Future<void> startTracking(BuildContext context) async {
|
|
if (isTracking) return;
|
|
|
|
// Configure high-accuracy GPS settings
|
|
final LocationSettings locationSettings =
|
|
LocationSettings(accuracy: LocationAccuracy.high);
|
|
|
|
_lastContext = context;
|
|
|
|
// Initialize and show tracking notification
|
|
await NotificationService().initNotification();
|
|
if (context.mounted) {
|
|
NotificationService().showNotification(
|
|
title: AppLocalizations.of(context)!.trackingRunningInBackground,
|
|
);
|
|
}
|
|
|
|
// Load tracking interval from settings
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final intervalSeconds = prefs.getInt('trackingInterval') ?? 60;
|
|
|
|
// Set up periodic position updates
|
|
_positionTimer =
|
|
Timer.periodic(Duration(seconds: intervalSeconds), (_) async {
|
|
try {
|
|
final Position position = await Geolocator.getCurrentPosition(
|
|
locationSettings: locationSettings);
|
|
|
|
// Store position and accuracy data
|
|
pathList.add(LatLng(position.latitude, position.longitude));
|
|
accuracyList.add(position.accuracy);
|
|
currentAccuracy = position.accuracy;
|
|
_positionController.add(position);
|
|
_updateStats();
|
|
} catch (e) {
|
|
// Handle errors with notification
|
|
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;
|
|
}
|
|
|
|
// Last calculated tracking statistics
|
|
TrackingStats? _lastStats;
|
|
TrackingStats? get currentStats => _lastStats;
|
|
|
|
/// Update tracking statistics
|
|
/// Calculates:
|
|
/// - Current GPS accuracy
|
|
/// - Average accuracy
|
|
/// - Total distance traveled
|
|
void _updateStats() {
|
|
if (pathList.isEmpty) {
|
|
_lastStats = TrackingStats(
|
|
currentAccuracy: currentAccuracy ?? 0,
|
|
averageAccuracy: 0,
|
|
totalDistanceMeters: 0);
|
|
_statsController.add(_lastStats!);
|
|
return;
|
|
}
|
|
|
|
// Calculate total distance
|
|
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!);
|
|
}
|
|
|
|
/// Request a manual stats update
|
|
void requestStatsUpdate() {
|
|
_updateStats();
|
|
}
|
|
|
|
/// Calculate distance between two GPS coordinates using the Haversine formula
|
|
/// @return Distance in meters
|
|
double _calculateDistance(
|
|
double lat1, double lon1, double lat2, double lon2) {
|
|
const double earthRadius = 6371000; // Earth radius in meters
|
|
|
|
// Convert coordinates to radians
|
|
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;
|
|
|
|
// Haversine formula calculation
|
|
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;
|
|
}
|
|
|
|
/// Temporarily pause tracking
|
|
void pauseTracking() {
|
|
_positionTimer?.cancel();
|
|
isTracking = false;
|
|
}
|
|
|
|
/// Resume paused tracking
|
|
void resumeTracking() {
|
|
if (!isTracking && _lastContext != null) {
|
|
startTracking(_lastContext!);
|
|
}
|
|
isTracking = true;
|
|
}
|
|
|
|
/// Stop tracking completely and clear current state
|
|
void stopTracking() {
|
|
_positionTimer?.cancel();
|
|
NotificationService().deleteNotification();
|
|
isTracking = false;
|
|
accuracyList.clear();
|
|
currentAccuracy = null;
|
|
_lastContext = null;
|
|
}
|
|
|
|
/// Clear all recorded position data
|
|
void clearPath() {
|
|
pathList.clear();
|
|
accuracyList.clear();
|
|
currentAccuracy = null;
|
|
_updateStats();
|
|
}
|
|
|
|
/// Convert tracked path to string format
|
|
/// Format: "latitude,longitude;latitude,longitude;..."
|
|
String getPathAsString() {
|
|
return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";");
|
|
}
|
|
|
|
/// Clean up resources
|
|
void dispose() {
|
|
stopTracking();
|
|
_positionController.close();
|
|
_statsController.close();
|
|
}
|
|
}
|
|
|
|
/// Data class for tracking statistics
|
|
/// Contains:
|
|
/// - Current GPS accuracy
|
|
/// - Average accuracy
|
|
/// - Total distance in meters
|
|
class TrackingStats {
|
|
final double currentAccuracy;
|
|
final double averageAccuracy;
|
|
final double totalDistanceMeters;
|
|
|
|
TrackingStats({
|
|
required this.currentAccuracy,
|
|
required this.averageAccuracy,
|
|
required this.totalDistanceMeters,
|
|
});
|
|
}
|