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

314 lines
10 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:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
import 'package:share_plus/share_plus.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 {
// ── Á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;
// ── Belső állapot ──────────────────────────────────────────────────────────
LocationSource? _source;
StreamSubscription<SourcePosition>? _positionSub;
Timer? _elapsedTimer;
TrackPoint? _lastPoint;
DateTime? _sessionStart;
double _accumulatedDistance = 0;
final _db = TrackDatabase.instance;
final _exporter = GpxExporter();
// ── Inicializálás ──────────────────────────────────────────────────────────
@override
void onInit() {
super.onInit();
_initForegroundTask();
loadSavedTracks();
}
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}) async {
if (isRecording.value) return;
_source = source ?? PhoneGpsSource(intervalMs: 1000, distanceFilter: 2.0);
// Track létrehozása az adatbázisban
final now = DateTime.now();
final name = DateFormat('yyyy-MM-dd HH:mm').format(now);
final trackId = await _db.insertTrack(Track(
name: name,
startTime: now,
source: _source!.displayName,
));
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: name,
callback: startTrackingCallback,
);
// GPS stream feliratkozás
_positionSub = _source!.positionStream.listen(
_onPosition,
onError: (e) {
Get.snackbar('GPS hiba', e.toString(),
backgroundColor: Colors.red, colorText: Colors.white);
stopRecording();
},
);
// Időmérő
_startElapsedTimer();
isRecording.value = true;
isPaused.value = false;
}
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(),
);
await _db.updateTrack(finished);
currentTrack.value = finished;
}
await FlutterForegroundTask.stopService();
await _source?.dispose();
_source = null;
isRecording.value = false;
isPaused.value = false;
await loadSavedTracks();
}
Future<void> loadSavedTracks() async {
savedTracks.value = await _db.listTracks();
}
Future<void> deleteTrack(int id) async {
await _db.deleteTrack(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}');
} 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 {
if (isPaused.value) return;
final trackId = currentTrack.value?.id;
if (trackId == null) return;
// 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: ugrásszerű változás (pl. GPS lock elvesztése) ignorálása
if (segmentDist > 100) return;
}
_accumulatedDistance += segmentDist;
sessionDistance.value = _accumulatedDistance;
// Sebesség km/h-ban
currentSpeedKmh.value = (pos.speed ?? 0) * 3.6;
// Pont mentése
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,
);
await _db.addPoint(point, _accumulatedDistance);
_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,
);
}
}
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();
}
}