import 'dart:async'; import 'dart:math' as math; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:terepi_seged/models/source_point.dart'; import 'package:terepi_seged/models/vechicle_position_log.dart'; import 'package:terepi_seged/services/app_database.dart'; import 'package:terepi_seged/services/coord_converter_service.dart'; import 'package:terepi_seged/services/device_identity_service.dart'; import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/services/vechicle_identity_service.dart'; /// Egyszerű navigációs panel a jelgerjesztő (vibrátor) járműhöz. /// /// A pozíció a KITŰZÉSSEL AZONOS forrásból jön (GnssService — külső /// BT/BLE GNSS-egység a járműben), semmilyen új eszköz-integráció nem /// kell hozzá. A forráspontok tisztán referencia-réteg (nincs mutálható /// "meglőve" állapotuk); az ellenőrzés a periodikus pozíciónapló és a /// terv UTÓLAGOS összevetéséből adódik (lásd isVerified). class VibroNavController extends GetxController { AppDatabase get _db => AppDatabase.instance; // ── Forráspontok ───────────────────────────────────────────────── final sourcePoints = [].obs; /// Egyezés-tűrés (m) a naplózott pozíció és a terv-forráspont között. final tolerance = 15.0.obs; // ── Élő pozíció (a GnssService-ből, EOV-ra konvertálva) ─────────── final hasPosition = false.obs; final curEovY = 0.0.obs; final curEovX = 0.0.obs; final speedKmh = 0.0.obs; final sessionDistanceM = 0.0.obs; /// Haladási irány (fok) — pozíció-előzményből számolva, ALACSONY /// SEBESSÉGNÉL BEFAGYASZTVA (a nyers GPS-heading álló/lassú helyzetben /// zajos/értelmetlen — ugyanez a minta, mint a Kitűzésnél). final travelHeading = Rxn(); static const _headingFreezeSpeedKmh = 5.0; double? _histY, _histX; DateTime? _histTime; // ── Legközelebbi forráspont ─────────────────────────────────────── final nearestPoint = Rxn(); final nearestDistance = 0.0.obs; // ── Periodikus napló ─────────────────────────────────────────────── final isLogging = false.obs; final logIntervalSec = 5.obs; // felhasználó által állítható final logs = [].obs; Timer? _logTimer; // ── Térkép — a MapController itt él, nem a View State-jében ──────── final mapController = MapController(); StreamSubscription? _gnssUpdateSub; /// A kezdő térképközéppont lekérdezése (gyors, telefon-GPS-alapú) /// befejeződött-e — a View ez alapján dönti el, mikor építse fel a /// térképet, hogy ne induljon rossz (pl. budapesti) középponttal. final mapReady = false.obs; @override void onInit() { super.onInit(); load(); refreshLogs(); if (Get.isRegistered()) { // FONTOS: a lastGgaLine csak NMEA (külső BT/BLE vevő) esetén // frissül — telefon GPS-nél sosem. Az onDataUpdated viszont // MINDKÉT forrásnál lefut, ez a helyes, egységes jelzés. _gnssUpdateSub = GnssService.to.onDataUpdated.listen((_) => _onPosition()); GnssService.to.determineInitialPosition().whenComplete(() { mapReady.value = true; }); } else { mapReady.value = true; } // Automatikus indítás: amint van pozíció ÉS ki van választva jármű, // induljon a rögzítés — korábban ez a bekötés hiányzott, a // _tryAutoStart() metódust semmi nem hívta meg. ever(hasPosition, (_) => _tryAutoStart()); ever(VehicleIdentityService.to.selectedVehicle, (_) => _tryAutoStart()); } @override void onClose() { _logTimer?.cancel(); _gnssUpdateSub?.cancel(); super.onClose(); } Future refreshLogs() async { logs.value = await loadLogs(); } Future load() async { final projectId = ProjectService.to.activeProjectId; if (projectId == null) { sourcePoints.clear(); return; } sourcePoints.value = await _db.listSourcePoints(projectId); } // ═════════════════════════════════════════════════════════════════ // Pozíció-feldolgozás // ═════════════════════════════════════════════════════════════════ void _onPosition() { final gnss = GnssService.to; if (gnss.gpsQuality.value <= 0 || gnss.latitude.value == 0 || !Get.isRegistered()) { return; } final p = CoordConverterService.to .wgsToEovPoint(gnss.longitude.value, gnss.latitude.value); final now = DateTime.now(); if (_histY != null && _histTime != null) { final dy = p.x - _histY!; final dx = p.y - _histX!; final dist = math.sqrt(dy * dy + dx * dx); final dtSec = now.difference(_histTime!).inMilliseconds / 1000.0; if (dtSec > 0) { final v = dist / dtSec; // m/s speedKmh.value = v * 3.6; sessionDistanceM.value += dist; // Irány csak akkor frissül, ha a sebesség a fagyasztási küszöb // felett van — álló/nagyon lassú helyzetben a nyers irány zajos. if (speedKmh.value >= _headingFreezeSpeedKmh && dist > 0.3) { travelHeading.value = (math.atan2(dy, dx) * 180 / math.pi + 360) % 360; } } } _histY = p.x; _histX = p.y; _histTime = now; curEovY.value = p.x; curEovX.value = p.y; hasPosition.value = true; _updateNearest(); } void _updateNearest() { if (sourcePoints.isEmpty) { nearestPoint.value = null; return; } SourcePoint? best; var bestDist = double.infinity; for (final sp in sourcePoints) { final dy = sp.planEovY - curEovY.value; final dx = sp.planEovX - curEovX.value; final d = math.sqrt(dy * dy + dx * dx); if (d < bestDist) { bestDist = d; best = sp; } } nearestPoint.value = best; nearestDistance.value = bestDist; } // ═════════════════════════════════════════════════════════════════ // Terv/napló összevetés — melyik forráspont "igazolt" // ═════════════════════════════════════════════════════════════════ /// Egyszerű, szinkron ellenőrzés a már betöltött [logs] lista alapján /// (a nézet előre lekéri, hogy ne fusson adatbázis-lekérdezés minden /// egyes marker kirajzolásakor). bool isVerified(SourcePoint sp, List logs) { for (final log in logs) { final dy = sp.planEovY - log.eovY; final dx = sp.planEovX - log.eovX; if (math.sqrt(dy * dy + dx * dx) <= tolerance.value) return true; } return false; } Future> loadLogs() async { final projectId = ProjectService.to.activeProjectId; if (projectId == null) return []; return _db.listVehiclePositionLogs(projectId); } // ═════════════════════════════════════════════════════════════════ // Periodikus rögzítés // ═════════════════════════════════════════════════════════════════ void startLogging() { if (isLogging.value) return; isLogging.value = true; _logTick(); // azonnali első pont _logTimer = Timer.periodic( Duration(seconds: logIntervalSec.value), (_) => _logTick()); } void stopLogging() { isLogging.value = false; _logTimer?.cancel(); _logTimer = null; } void _tryAutoStart() { if (isLogging.value) return; if (!hasPosition.value) return; if (VehicleIdentityService.to.selectedVehicle.value == null) return; startLogging(); } Future _logTick() async { if (!hasPosition.value) return; final projectId = ProjectService.to.activeProjectId; final vehicleId = VehicleIdentityService.to.selectedVehicle.value; if (projectId == null || vehicleId == null) return; final gnss = GnssService.to; final entry = VehiclePositionLog( projectId: projectId, vehicleId: vehicleId, eovY: curEovY.value, eovX: curEovX.value, lat: gnss.latitude.value, lon: gnss.longitude.value, altitude: gnss.altitude.value, speedKmh: speedKmh.value, heading: travelHeading.value, fixQuality: gnss.gpsQuality.value, accuracy: gnss.horizontalAccuracy, deviceId: Get.isRegistered() && DeviceIdentityService.to.isReady ? DeviceIdentityService.to.deviceId : null, appInstanceId: Get.isRegistered() && DeviceIdentityService.to.isReady ? DeviceIdentityService.to.appInstanceId : null, ); await _db.insertVehiclePositionLog(entry); logs.add(entry); } }