258 lines
9.7 KiB
Dart
258 lines
9.7 KiB
Dart
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 = <SourcePoint>[].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<double>();
|
|
static const _headingFreezeSpeedKmh = 5.0;
|
|
|
|
double? _histY, _histX;
|
|
DateTime? _histTime;
|
|
|
|
// ── Legközelebbi forráspont ───────────────────────────────────────
|
|
final nearestPoint = Rxn<SourcePoint>();
|
|
final nearestDistance = 0.0.obs;
|
|
|
|
// ── Periodikus napló ───────────────────────────────────────────────
|
|
final isLogging = false.obs;
|
|
final logIntervalSec = 5.obs; // felhasználó által állítható
|
|
final logs = <VehiclePositionLog>[].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<GnssService>()) {
|
|
// 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<void> refreshLogs() async {
|
|
logs.value = await loadLogs();
|
|
}
|
|
|
|
Future<void> 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<CoordConverterService>()) {
|
|
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<VehiclePositionLog> 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<List<VehiclePositionLog>> 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<void> _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>() &&
|
|
DeviceIdentityService.to.isReady
|
|
? DeviceIdentityService.to.deviceId
|
|
: null,
|
|
appInstanceId: Get.isRegistered<DeviceIdentityService>() &&
|
|
DeviceIdentityService.to.isReady
|
|
? DeviceIdentityService.to.appInstanceId
|
|
: null,
|
|
);
|
|
await _db.insertVehiclePositionLog(entry);
|
|
logs.add(entry);
|
|
}
|
|
}
|