Files
MobilApp/lib/pages/tracking/presentation/controllers/tracking_controller.dart
T

508 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.dart';
import 'package:terepi_seged/services/app_logger.dart';
import 'package:terepi_seged/services/firebase_logger.dart';
import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/services/track_sync_service.dart';
import '../../../../services/location_source.dart';
import '../../../../services/phone_gps_source.dart';
import '../../../../services/track_database.dart';
import '../../../../services/gpx_exporter.dart';
import '../../../../models/track.dart';
// ─── Foreground task handler ─────────────────────────────────────────────────
// Ez a függvény a háttér-isolate-ban fut. Csak a meglevő pozíció-streamet
// tartja fenn, a tényleges adatfeldolgozás a főszálban történik.
@pragma('vm:entry-point')
void startTrackingCallback() {
FlutterForegroundTask.setTaskHandler(_TrackingTaskHandler());
}
class _TrackingTaskHandler extends TaskHandler {
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
@override
void onRepeatEvent(DateTime timestamp) {
// A Geolocator stream a főizoláton folyik tovább, itt nincs teendő.
}
@override
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
}
// ─── TrackingController ──────────────────────────────────────────────────────
class TrackingController extends GetxController {
static TrackingController get to => Get.find();
// ── Állapot ────────────────────────────────────────────────────────────────
final isRecording = false.obs;
final isPaused = false.obs;
final currentTrack = Rxn<Track>();
/// Aktuális session pontjai a térképhez (csak a memóriában).
final livePoints = <LatLng>[].obs;
/// Aktuális sebesség [km/h].
final currentSpeedKmh = 0.0.obs;
/// Megtett távolság [m] az aktuális sessionben.
final sessionDistance = 0.0.obs;
/// Eltelt idő formátuma óó:pp:mm.
final elapsedFormatted = '00:00:00'.obs;
/// Előtér/háttér állapot jelzése.
final isInBackground = false.obs;
// ── Mentett track-ek listája ────────────────────────────────────────────────
final savedTracks = <Track>[].obs;
// Melyik track-ek látszanak overlay-ként a térképen
final overlayTrackIds = <int>[].obs;
// Betöltött koordináták cache
final Map<int, List<LatLng>> _trackCoords = {};
int get livePointCount => livePoints.length;
// ── Belső állapot ──────────────────────────────────────────────────────────
LocationSource? _source;
StreamSubscription<SourcePosition>? _positionSub;
Timer? _elapsedTimer;
TrackPoint? _lastPoint;
DateTime? _sessionStart;
double _accumulatedDistance = 0;
final _db = TrackDatabase.instance;
final _exporter = GpxExporter();
// Timer
DateTime? _lastPositionTime;
Timer? _watchdogTimer;
final gpsSignalLost = false.obs;
static const _gpsTimeoutSec = 10;
// ── Inicializálás ──────────────────────────────────────────────────────────
@override
void onInit() {
super.onInit();
_initForegroundTask();
loadSavedTracks();
// A TrackingController permanent, egyszer indul az app életciklusában —
// enélkül projektváltáskor a lista a réginél ragadna.
ever(ProjectService.to.activeProject, (_) {
loadSavedTracks();
// Ne maradjon a térképen egy MÁSIK projekt track-overlaya.
overlayTrackIds.clear();
_trackCoords.clear();
});
}
void _initForegroundTask() {
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'tracking_channel',
channelName: 'Nyomvonal rögzítés',
channelDescription: 'Aktív track rögzítése folyamatban',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: true,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(1000),
autoRunOnBoot: false,
allowWakeLock: true,
allowWifiLock: false,
),
);
}
// ── Publikus API ───────────────────────────────────────────────────────────
/// Elindítja a rögzítést a megadott forrással (alapértelmezett: telefon GPS).
Future<void> startRecording(
{LocationSource? source, String? name, TrackSyncMode? syncMode}) async {
if (isRecording.value) return;
// Alapértelmezés: a track kövesse az AKTÍV PROJEKT online/lokális
// állapotát — eddig ez mindig 'online' volt, függetlenül a projekt
// beállításától. Explicit paraméterrel felülírható.
final activeProject = ProjectService.to.activeProject.value;
final effectiveSyncMode = syncMode ??
(activeProject?.isLocalOnly ?? true
? TrackSyncMode.localOnly
: TrackSyncMode.online);
_source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0);
// Track létrehozása az adatbázisban
final now = DateTime.now();
final trackName = name ?? DateFormat('yyyy-MM-dd HH:mm').format(now);
final trackId = await _db.insertTrack(Track(
name: trackName,
startTime: now,
source: _source!.displayName,
projectId: activeProject?.id,
syncMode: effectiveSyncMode));
currentTrack.value = await _db.getTrack(trackId);
// Állapot reset
livePoints.clear();
sessionDistance.value = 0;
_accumulatedDistance = 0;
_lastPoint = null;
_sessionStart = now;
// Foreground service indítása (háttér-működéshez)
await FlutterForegroundTask.startService(
notificationTitle: 'Track rögzítése',
notificationText: trackName,
callback: startTrackingCallback,
);
// A TrackSyncService.startSession() maga dönti el (a track
// isLocalOnly-ja alapján), hogy indítja-e a szinkron-időzítőket —
// nincs többé külön "előbb hozzuk létre szerveroldalon" lépés, mert
// az egységes (uuid-alapú) push maga hozza létre a sort a szerveren.
TrackSyncService.to.startSession(currentTrack.value!);
// GPS stream feliratkozás
_positionSub = _source!.positionStream.listen(
_onPosition,
onError: (e) {
AppLogger.e('positionStream', 'Stream hiba', error: e);
FirebaseLogger.e('positionStream', 'Stream hiba', error: e);
if (e is LocationServiceDisabledException ||
e is PermissionDeniedException) {
Get.snackbar('GPS hiba', e.toString(),
backgroundColor: Colors.red, colorText: Colors.white);
stopRecording();
}
},
);
AppLogger.e('track_started', 'name:$trackName');
FirebaseLogger.event('track_started', {'name': trackName});
// Időmérő
_startElapsedTimer();
isRecording.value = true;
isPaused.value = false;
_startWatchdog();
}
void pauseRecording() {
if (!isRecording.value || isPaused.value) return;
_positionSub?.pause();
_elapsedTimer?.cancel();
isPaused.value = true;
FlutterForegroundTask.updateService(
notificationTitle: 'Track szüneteltetve',
notificationText: currentTrack.value?.name ?? '',
);
}
void resumeRecording() {
if (!isPaused.value) return;
_positionSub?.resume();
_startElapsedTimer();
isPaused.value = false;
FlutterForegroundTask.updateService(
notificationTitle: 'Track rögzítése',
notificationText: currentTrack.value?.name ?? '',
);
}
Future<void> stopRecording() async {
if (!isRecording.value) return;
await _positionSub?.cancel();
_positionSub = null;
_elapsedTimer?.cancel();
_elapsedTimer = null;
// Track lezárása az adatbázisban
final track = currentTrack.value;
if (track?.id != null) {
final finished = track!.copyWith(
status: TrackStatus.finished,
endTime: DateTime.now(),
distanceMeters: _accumulatedDistance,
pointCount: livePoints.length,
);
await _db.updateTrack(finished);
await TrackSyncService.to.stopSession(finished);
currentTrack.value = finished;
}
await FlutterForegroundTask.stopService();
await _source?.dispose();
_source = null;
isRecording.value = false;
isPaused.value = false;
await loadSavedTracks();
_watchdogTimer?.cancel();
_watchdogTimer = null;
gpsSignalLost.value = false;
}
Future<void> loadSavedTracks() async {
savedTracks.value =
await _db.listTracks(projectId: ProjectService.to.activeProjectId);
}
Future<void> deleteTrack(int id) async {
await _db.deleteTrack(id);
overlayTrackIds.remove(id);
_trackCoords.remove(id);
await loadSavedTracks();
}
/// GPX export + rendszer megosztás dialóg.
Future<void> exportTrack(Track track) async {
try {
final path = await _exporter.export(track);
await Share.shareXFiles([XFile(path)],
subject: 'Nyomvonal: ${track.name}.gpx');
} catch (e) {
Get.snackbar('Export hiba', e.toString(),
backgroundColor: Colors.red, colorText: Colors.white);
}
}
/// Visszatölt egy mentett track pontjait a térképre (megtekintés).
Future<List<LatLng>> loadTrackPoints(int trackId) async {
final pts = await _db.getLatLons(trackId);
return pts.map((p) => LatLng(p.lat, p.lon)).toList();
}
// ── Belső logika ───────────────────────────────────────────────────────────
Future<void> _onPosition(SourcePosition pos) async {
try {
if (isPaused.value) return;
_lastPositionTime = DateTime.now();
gpsSignalLost.value = false;
final sw = Stopwatch()..start();
final trackId = currentTrack.value?.id;
if (trackId == null) return;
final point = TrackPoint(
trackId: trackId,
latitude: pos.latitude,
longitude: pos.longitude,
altitude: pos.altitude,
accuracy: pos.accuracy,
speed: pos.speed,
heading: pos.heading,
timestamp: pos.timestamp,
);
// Távolság a legutóbbi ponttól
double segmentDist = 0;
if (_lastPoint != null) {
segmentDist = haversineMeters(
_lastPoint!.latitude,
_lastPoint!.longitude,
pos.latitude,
pos.longitude,
);
// Szűrés: valóban LEHETETLEN ugrás kiszűrése (pl. GPS lock-
// vesztés utáni téves fix) — az ELTELT IDŐT is figyelembe véve.
// A korábbi, sima 100 m-es távolság-küszöb gyorsabb haladásnál
// (pl. földúton autóval) egy rövid, teljesen normál jel-
// kimaradás után érkező, VALÓS pontokat is kiszűrt, mert
// néhány másodperc alatt egy autó simán megtesz 100+ métert.
final elapsedSec =
point.timestamp.difference(_lastPoint!.timestamp).inMilliseconds /
1000.0;
final impliedSpeedMs =
elapsedSec > 0 ? segmentDist / elapsedSec : double.infinity;
// ~50 m/s = 180 km/h — terepen, földúton ennél gyorsabban senki
// nem halad, tehát ami ennél nagyobb sebességet implikálna, az
// GPS-hiba, nem valódi mozgás.
const maxPlausibleSpeedMs = 50.0;
if (impliedSpeedMs > maxPlausibleSpeedMs) {
AppLogger.w(
'_onPosition',
'GPS ugrás kiszűrve: ${segmentDist.toStringAsFixed(0)}m / '
'${elapsedSec.toStringAsFixed(1)}s '
'(${(impliedSpeedMs * 3.6).toStringAsFixed(0)} km/h, '
'pts: ${livePoints.length})');
_lastPoint = point;
return;
}
}
_accumulatedDistance += segmentDist;
sessionDistance.value = _accumulatedDistance;
// Sebesség km/h-ban
currentSpeedKmh.value = (pos.speed ?? 0) * 3.6;
// Pont mentése
await _db.addPoint(point, _accumulatedDistance);
sw.stop();
if (sw.elapsedMilliseconds > 300) {
AppLogger.w(
'on_Position',
'Lassú addPoint: ${sw.elapsedMilliseconds}ms, '
'pts: ${livePoints.length}');
FirebaseLogger.w(
'on_Position',
'Lassú addPoint: ${sw.elapsedMilliseconds}ms, '
'pts: ${livePoints.length}');
}
TrackSyncService.to.onNewPoint(point);
_lastPoint = point;
// UI frissítés
livePoints.add(LatLng(pos.latitude, pos.longitude));
// Értesítés frissítése
if (livePoints.length % 10 == 0) {
final dist = _formatDistance(_accumulatedDistance);
FlutterForegroundTask.updateService(
notificationTitle: 'Track rögzítése $dist',
notificationText: elapsedFormatted.value,
);
}
} catch (e, stack) {
AppLogger.e('_onPosition', 'pont feldolgozási hiba - track folytatódik',
error: e, stack: stack);
}
}
void _startElapsedTimer() {
_elapsedTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (_sessionStart == null) return;
final d = DateTime.now().difference(_sessionStart!);
final h = d.inHours.toString().padLeft(2, '0');
final m = (d.inMinutes % 60).toString().padLeft(2, '0');
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
elapsedFormatted.value = '$h:$m:$s';
});
}
String _formatDistance(double m) {
if (m < 1000) return '${m.toStringAsFixed(0)} m';
return '${(m / 1000).toStringAsFixed(2)} km';
}
@override
void onClose() {
stopRecording();
if (!isRecording.value) {
_positionSub?.cancel();
_elapsedTimer?.cancel();
}
super.onClose();
}
// Overlay ki/be kapcsolása — ha bekapcsol, betölti a koordinátákat
void toggleTrackOverlay(int trackId) {
if (overlayTrackIds.contains(trackId)) {
overlayTrackIds.remove(trackId);
overlayTrackIds.refresh();
} else {
overlayTrackIds.add(trackId);
overlayTrackIds.refresh();
_loadTrackCoords(trackId);
}
}
/// Koordináták visszaadása — üres lista ha még nincs betöltve
List<LatLng> getCoordsFor(int trackId) => _trackCoords[trackId] ?? [];
Future<void> _loadTrackCoords(int trackId) async {
if (_trackCoords.containsKey(trackId)) return;
final pts = await _db.getLatLons(trackId);
_trackCoords[trackId] = pts.map((p) => LatLng(p.lat, p.lon)).toList();
overlayTrackIds.refresh(); // térkép frissítés
}
void _startWatchdog() {
_watchdogTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => _checkGpsTimeout(),
);
}
void _checkGpsTimeout() {
if (!isRecording.value || isPaused.value) return;
final last = _lastPositionTime;
if (last == null) return;
final elapsedSec = DateTime.now().difference(last).inSeconds;
if (elapsedSec < _gpsTimeoutSec) return;
if (!gpsSignalLost.value) {
gpsSignalLost.value = true;
AppLogger.w(
'watchdog',
'GPS jel elveszett: ${elapsedSec}s óta nincs pozíció '
'(pts: ${livePoints.length}, '
'táv: ${_formatDistance(_accumulatedDistance)})');
AppLogger.w(
'gps_signal_lost',
'elapsed_sec: ${elapsedSec}'
'point_count: ${livePoints.length}'
'distance_m: ${_accumulatedDistance.round()}');
// Foreground notification frissítése
FlutterForegroundTask.updateService(
notificationTitle: '⚠ GPS jel elveszett',
notificationText: 'Track szünetel — folytatódik ha visszajön a jel',
);
// Snackbar ha előtérben van az app
if (!isInBackground.value) {
Get.snackbar(
'GPS jel elveszett',
'Valószínűleg alagút vagy lefedettség hiány.\n'
'A rögzítés automatikusan folytatódik.',
duration: const Duration(seconds: 6),
backgroundColor: Colors.orange.shade700,
colorText: Colors.white,
icon: const Icon(Icons.gps_off, color: Colors.white),
snackPosition: SnackPosition.TOP,
);
}
}
// Folyamatosan logol amíg nincs jel (percenként egyszer)
if (elapsedSec % 60 == 0) {
AppLogger.w('watchdog', 'GPS még mindig hiányzik: ${elapsedSec}s');
}
}
}