2026-07-06 11:47:41 +02:00
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
import 'package:get/get.dart';
|
2026-07-06 14:26:16 +02:00
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
|
import 'package:terepi_seged/controls/geoid_grid.dart';
|
2026-07-06 11:47:41 +02:00
|
|
|
|
import 'package:terepi_seged/services/app_database.dart';
|
|
|
|
|
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
|
|
|
|
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
|
|
|
|
|
import 'package:terepi_seged/services/project_service.dart';
|
2026-07-07 02:48:37 +02:00
|
|
|
|
import 'package:terepi_seged/services/tilt_service.dart';
|
2026-07-06 14:26:16 +02:00
|
|
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
2026-07-06 11:47:41 +02:00
|
|
|
|
|
|
|
|
|
|
import '../../models/stakeout_point.dart';
|
|
|
|
|
|
|
|
|
|
|
|
/// Kitűzési sorrend.
|
|
|
|
|
|
enum StakeoutOrder { idAsc, idDesc, nearest }
|
|
|
|
|
|
|
|
|
|
|
|
/// Eltérés-kijelzési mód a közeli fázisban.
|
|
|
|
|
|
enum DeviationMode {
|
|
|
|
|
|
line('Inline / crossline'),
|
|
|
|
|
|
travel('Haladási irány'),
|
|
|
|
|
|
northEast('Észak / kelet');
|
|
|
|
|
|
|
|
|
|
|
|
final String label;
|
|
|
|
|
|
const DeviationMode(this.label);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A kitűzés "agya": célpont-kezelés, sorrend, vonal-geometria,
|
|
|
|
|
|
/// eltérés-számítás, tárolás, haptikus visszajelzés.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Az egyetlen térképnézetbe illeszkedik: a MapSurveyController a
|
|
|
|
|
|
/// setMode()-ban hívja a [setActive]-ot (mode == MapSurveyMode.stakeout),
|
|
|
|
|
|
/// a service pedig csak aktív állapotban dolgozik (haptika, fázis).
|
|
|
|
|
|
/// Minden adatbázisművelet az AppDatabase-en keresztül megy.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Geometria: minden számítás EOV-SÍKBAN, méterben. A crossline irány a
|
|
|
|
|
|
/// szomszédos állomások szakaszaiból SZÁMÍTÓDIK — a vonal első pontjánál
|
|
|
|
|
|
/// az első szakasz hátrafelé, az utolsónál az utolsó szakasz előre
|
|
|
|
|
|
/// extrapolálásával, belső töréspontnál a két irány átlagával; virtuális
|
|
|
|
|
|
/// segédpontot nem tárolunk.
|
|
|
|
|
|
class StakeoutService extends GetxService {
|
|
|
|
|
|
static StakeoutService get to => Get.find();
|
|
|
|
|
|
|
|
|
|
|
|
AppDatabase get _db => AppDatabase.instance;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Mód-aktiválás ────────────────────────────────────────────────
|
|
|
|
|
|
final active = false.obs;
|
|
|
|
|
|
|
|
|
|
|
|
/// A MapSurveyController hívja módváltáskor.
|
|
|
|
|
|
Future<void> setActive(bool value) async {
|
|
|
|
|
|
if (active.value == value) return;
|
|
|
|
|
|
active.value = value;
|
|
|
|
|
|
if (value) {
|
2026-07-06 14:26:16 +02:00
|
|
|
|
WakelockPlus.enable();
|
2026-07-07 02:48:37 +02:00
|
|
|
|
if (Get.isRegistered<TiltService>()) TiltService.to.setActive(true);
|
2026-07-06 11:47:41 +02:00
|
|
|
|
await load();
|
|
|
|
|
|
_applyPhase();
|
|
|
|
|
|
_scheduleHaptic();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_hapticTimer?.cancel();
|
2026-07-06 14:26:16 +02:00
|
|
|
|
WakelockPlus.disable();
|
2026-07-07 02:48:37 +02:00
|
|
|
|
if (Get.isRegistered<TiltService>()) TiltService.to.setActive(false);
|
2026-07-06 11:47:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Állapot ──────────────────────────────────────────────────────
|
|
|
|
|
|
final points = <StakeoutPoint>[].obs;
|
|
|
|
|
|
final target = Rxn<StakeoutPoint>();
|
|
|
|
|
|
final orderMode = StakeoutOrder.idAsc.obs;
|
|
|
|
|
|
final deviationMode = DeviationMode.line.obs;
|
|
|
|
|
|
|
|
|
|
|
|
/// Tűrés (m) — tárolásnál és a céltábla belső körénél.
|
|
|
|
|
|
final toleranceXY = 0.03.obs;
|
|
|
|
|
|
|
|
|
|
|
|
/// Közeli fázis (céltábla-nézet); automatikus 5 m alatt, kézzel
|
|
|
|
|
|
/// felülbírálható.
|
|
|
|
|
|
final nearPhase = false.obs;
|
|
|
|
|
|
static const nearPhaseDistance = 5.0;
|
|
|
|
|
|
bool? _manualPhase;
|
|
|
|
|
|
|
|
|
|
|
|
final hapticsEnabled = true.obs;
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
final soundEnabled = true.obs;
|
|
|
|
|
|
|
|
|
|
|
|
/// Átlagolás tároláskor
|
|
|
|
|
|
final averagingSeconds = 0.obs;
|
|
|
|
|
|
final isAveraging = false.obs;
|
|
|
|
|
|
final averagingElapsed = 0.obs;
|
|
|
|
|
|
|
|
|
|
|
|
/// Max. megengedett crossline eltolás (m; 0=nincs korlát)
|
|
|
|
|
|
final maxOffset = 0.0.obs;
|
|
|
|
|
|
|
|
|
|
|
|
late GeoidGrid geoidGrid;
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
// ── Aktuális pozíció (EOV) és navigációs értékek ────────────────
|
|
|
|
|
|
final hasPosition = false.obs;
|
|
|
|
|
|
final curEovY = 0.0.obs;
|
|
|
|
|
|
final curEovX = 0.0.obs;
|
|
|
|
|
|
final curAlt = 0.0.obs;
|
|
|
|
|
|
|
|
|
|
|
|
final distance = 0.0.obs;
|
|
|
|
|
|
final bearingToTarget = 0.0.obs; // fok, EOV-észak = 0, óramutató
|
|
|
|
|
|
final travelHeading = Rxn<double>(); // null = állunk
|
|
|
|
|
|
|
|
|
|
|
|
/// Eltérés az aktuális [deviationMode] szerint: előre(+)/hátra(−),
|
|
|
|
|
|
/// jobbra(+)/balra(−); dz: fel(+)/le(−).
|
|
|
|
|
|
final devForward = 0.0.obs;
|
|
|
|
|
|
final devRight = 0.0.obs;
|
|
|
|
|
|
final devDz = Rxn<double>();
|
|
|
|
|
|
final withinTolerance = false.obs;
|
|
|
|
|
|
|
|
|
|
|
|
double? _histY, _histX;
|
|
|
|
|
|
Timer? _hapticTimer;
|
|
|
|
|
|
bool _toleranceAnnounced = false;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
2026-07-06 14:26:16 +02:00
|
|
|
|
void onInit() async {
|
2026-07-06 11:47:41 +02:00
|
|
|
|
super.onInit();
|
2026-07-06 14:26:16 +02:00
|
|
|
|
await _loadSettings();
|
|
|
|
|
|
geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx');
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
if (Get.isRegistered<GnssService>()) {
|
|
|
|
|
|
ever(GnssService.to.lastGgaLine, (_) => _onPosition());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void onClose() {
|
|
|
|
|
|
_hapticTimer?.cancel();
|
|
|
|
|
|
super.onClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
Future<void> _loadSettings() async {
|
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
|
toleranceXY.value = prefs.getDouble('stakeout_tolerance') ?? 0.03;
|
|
|
|
|
|
averagingSeconds.value = prefs.getInt('stakeout_avg_seconds') ?? 0;
|
|
|
|
|
|
hapticsEnabled.value = prefs.getBool('stakeout_haptics') ?? true;
|
|
|
|
|
|
soundEnabled.value = prefs.getBool('stakeout_sound') ?? true;
|
|
|
|
|
|
maxOffset.value = prefs.getDouble('stakeout_max_offset') ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> saveSettings() async {
|
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
|
await prefs.setDouble('stakeout_tolerance', toleranceXY.value);
|
|
|
|
|
|
await prefs.setInt('stakeout_avg_seconds', averagingSeconds.value);
|
|
|
|
|
|
await prefs.setBool('stakeout_haptics', hapticsEnabled.value);
|
|
|
|
|
|
await prefs.setBool('stakeout_sound', soundEnabled.value);
|
|
|
|
|
|
await prefs.setDouble('stakeout_max_offset', maxOffset.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Betöltés / cél-kezelés
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> load() async {
|
|
|
|
|
|
final projectId = ProjectService.to.activeProjectId;
|
|
|
|
|
|
if (projectId == null) {
|
|
|
|
|
|
points.clear();
|
|
|
|
|
|
target.value = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
points.value = await _db.listStakeoutPoints(projectId);
|
|
|
|
|
|
if (target.value == null ||
|
|
|
|
|
|
!points.any((p) => p.uuid == target.value!.uuid)) {
|
|
|
|
|
|
target.value = _firstPending();
|
|
|
|
|
|
}
|
|
|
|
|
|
_recompute();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<String> get lines =>
|
|
|
|
|
|
points.map((p) => p.lineId).toSet().toList()..sort();
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, ({int total, int staked})> get lineProgress {
|
|
|
|
|
|
final m = <String, ({int total, int staked})>{};
|
|
|
|
|
|
for (final p in points) {
|
|
|
|
|
|
final cur = m[p.lineId] ?? (total: 0, staked: 0);
|
|
|
|
|
|
m[p.lineId] = (
|
|
|
|
|
|
total: cur.total + 1,
|
|
|
|
|
|
staked: cur.staked + (p.status == StakeoutStatus.staked ? 1 : 0),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void setTarget(StakeoutPoint p) {
|
|
|
|
|
|
target.value = p;
|
|
|
|
|
|
_manualPhase = null; // új célnál vissza automatikus fázisra
|
|
|
|
|
|
_toleranceAnnounced = false;
|
|
|
|
|
|
_recompute();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void togglePhase() {
|
|
|
|
|
|
_manualPhase = !nearPhase.value;
|
|
|
|
|
|
_applyPhase();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
StakeoutPoint? _firstPending() {
|
|
|
|
|
|
final pending =
|
|
|
|
|
|
points.where((p) => p.status == StakeoutStatus.pending).toList();
|
|
|
|
|
|
if (pending.isEmpty) return null;
|
|
|
|
|
|
pending.sort((a, b) => a.lineId != b.lineId
|
|
|
|
|
|
? a.lineId.compareTo(b.lineId)
|
|
|
|
|
|
: a.station.compareTo(b.station));
|
|
|
|
|
|
return orderMode.value == StakeoutOrder.idDesc
|
|
|
|
|
|
? pending.last
|
|
|
|
|
|
: pending.first;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Következő cél a sorrend-mód szerint — a CÉL VONALÁN belül lép,
|
|
|
|
|
|
/// elfogyva a következő vonalra.
|
|
|
|
|
|
StakeoutPoint? nextTarget({bool backwards = false}) {
|
|
|
|
|
|
final cur = target.value;
|
|
|
|
|
|
if (cur == null) return _firstPending();
|
|
|
|
|
|
final pending = points
|
|
|
|
|
|
.where((p) => p.status == StakeoutStatus.pending && p.uuid != cur.uuid)
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
if (pending.isEmpty) return null;
|
|
|
|
|
|
|
|
|
|
|
|
final sameLine = pending.where((p) => p.lineId == cur.lineId).toList()
|
|
|
|
|
|
..sort((a, b) => a.station.compareTo(b.station));
|
|
|
|
|
|
|
|
|
|
|
|
switch (orderMode.value) {
|
|
|
|
|
|
case StakeoutOrder.nearest:
|
|
|
|
|
|
final pool = sameLine.isNotEmpty ? sameLine : pending;
|
|
|
|
|
|
pool.sort((a, b) => _distTo(a).compareTo(_distTo(b)));
|
|
|
|
|
|
return pool.first;
|
|
|
|
|
|
|
|
|
|
|
|
case StakeoutOrder.idAsc:
|
|
|
|
|
|
case StakeoutOrder.idDesc:
|
|
|
|
|
|
final asc = (orderMode.value == StakeoutOrder.idAsc) != backwards;
|
|
|
|
|
|
final candidates = sameLine.where(
|
|
|
|
|
|
(p) => asc ? p.station > cur.station : p.station < cur.station);
|
|
|
|
|
|
if (candidates.isNotEmpty) {
|
|
|
|
|
|
return asc
|
|
|
|
|
|
? candidates.reduce((a, b) => a.station < b.station ? a : b)
|
|
|
|
|
|
: candidates.reduce((a, b) => a.station > b.station ? a : b);
|
|
|
|
|
|
}
|
|
|
|
|
|
final others = pending.where((p) => p.lineId != cur.lineId).toList();
|
|
|
|
|
|
if (others.isEmpty) return null;
|
|
|
|
|
|
others.sort((a, b) => a.lineId != b.lineId
|
|
|
|
|
|
? a.lineId.compareTo(b.lineId)
|
|
|
|
|
|
: a.station.compareTo(b.station));
|
|
|
|
|
|
return asc ? others.first : others.last;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void advance({bool backwards = false}) {
|
|
|
|
|
|
final n = nextTarget(backwards: backwards);
|
|
|
|
|
|
if (n != null) setTarget(n);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double _distTo(StakeoutPoint p) {
|
|
|
|
|
|
final dy = p.planEovY - curEovY.value;
|
|
|
|
|
|
final dx = p.planEovX - curEovX.value;
|
|
|
|
|
|
return math.sqrt(dy * dy + dx * dx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Pozíció + eltérés
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
void _onPosition() {
|
|
|
|
|
|
final gnss = GnssService.to;
|
|
|
|
|
|
if (gnss.gpsQuality.value <= 0 ||
|
|
|
|
|
|
gnss.latitude.value == 0 ||
|
|
|
|
|
|
!Get.isRegistered<CoordConverterService>()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-06 14:26:16 +02:00
|
|
|
|
final lat = gnss.latitude.value;
|
|
|
|
|
|
final lon = gnss.longitude.value;
|
|
|
|
|
|
final alt = gnss.altitude.value;
|
|
|
|
|
|
final sep = gnss.geoidSeparation.value;
|
2026-07-06 11:47:41 +02:00
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
final p = CoordConverterService.to.wgsToEovPoint(lon, lat);
|
2026-07-06 11:47:41 +02:00
|
|
|
|
curEovY.value = p.x;
|
|
|
|
|
|
curEovX.value = p.y;
|
2026-07-06 14:26:16 +02:00
|
|
|
|
|
|
|
|
|
|
curAlt.value = geoidGrid.toEovHeight(lat, lon, alt, sep)!;
|
|
|
|
|
|
|
|
|
|
|
|
//curAlt.value = gnss.altitude.value;
|
2026-07-06 11:47:41 +02:00
|
|
|
|
hasPosition.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Haladási irány: legalább 0,5 m elmozdulásból (állva zajos lenne).
|
|
|
|
|
|
if (_histY != null) {
|
|
|
|
|
|
final dy = curEovY.value - _histY!;
|
|
|
|
|
|
final dx = curEovX.value - _histX!;
|
|
|
|
|
|
if (math.sqrt(dy * dy + dx * dx) >= 0.5) {
|
|
|
|
|
|
travelHeading.value = _bearingDeg(dy, dx);
|
|
|
|
|
|
_histY = curEovY.value;
|
|
|
|
|
|
_histX = curEovX.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_histY = curEovY.value;
|
|
|
|
|
|
_histX = curEovX.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_recompute();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _recompute() {
|
|
|
|
|
|
final t = target.value;
|
|
|
|
|
|
if (t == null || !hasPosition.value) {
|
|
|
|
|
|
withinTolerance.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final dy = t.planEovY - curEovY.value;
|
|
|
|
|
|
final dx = t.planEovX - curEovX.value;
|
|
|
|
|
|
distance.value = math.sqrt(dy * dy + dx * dx);
|
|
|
|
|
|
bearingToTarget.value = _bearingDeg(dy, dx);
|
|
|
|
|
|
|
|
|
|
|
|
final double fwdBearing;
|
|
|
|
|
|
switch (deviationMode.value) {
|
|
|
|
|
|
case DeviationMode.line:
|
|
|
|
|
|
fwdBearing = lineBearingAt(t) ?? travelHeading.value ?? 0;
|
|
|
|
|
|
case DeviationMode.travel:
|
|
|
|
|
|
fwdBearing = travelHeading.value ?? 0;
|
|
|
|
|
|
case DeviationMode.northEast:
|
|
|
|
|
|
fwdBearing = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final rad = fwdBearing * math.pi / 180;
|
|
|
|
|
|
devForward.value = dy * math.sin(rad) + dx * math.cos(rad);
|
|
|
|
|
|
devRight.value = dy * math.cos(rad) - dx * math.sin(rad);
|
|
|
|
|
|
devDz.value = t.planEovZ != null ? t.planEovZ! - curAlt.value : null;
|
|
|
|
|
|
|
|
|
|
|
|
final wasWithin = withinTolerance.value;
|
|
|
|
|
|
withinTolerance.value = distance.value <= toleranceXY.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (active.value) {
|
|
|
|
|
|
_applyPhase();
|
|
|
|
|
|
if (withinTolerance.value && !wasWithin && !_toleranceAnnounced) {
|
|
|
|
|
|
_toleranceAnnounced = true;
|
|
|
|
|
|
if (hapticsEnabled.value) HapticFeedback.heavyImpact();
|
2026-07-06 14:26:16 +02:00
|
|
|
|
if (soundEnabled.value) SystemSound.play(SystemSoundType.alert);
|
2026-07-06 11:47:41 +02:00
|
|
|
|
} else if (!withinTolerance.value) {
|
|
|
|
|
|
_toleranceAnnounced = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
_scheduleHaptic();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _applyPhase() {
|
|
|
|
|
|
nearPhase.value = _manualPhase ??
|
|
|
|
|
|
(hasPosition.value &&
|
|
|
|
|
|
target.value != null &&
|
|
|
|
|
|
distance.value <= nearPhaseDistance);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Haptikus "geiger": közeledve sűrűsödő pulzus, tűrésen belül gyors.
|
|
|
|
|
|
void _scheduleHaptic() {
|
|
|
|
|
|
_hapticTimer?.cancel();
|
|
|
|
|
|
if (!active.value ||
|
|
|
|
|
|
!hapticsEnabled.value ||
|
|
|
|
|
|
!hasPosition.value ||
|
|
|
|
|
|
target.value == null) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
final d = distance.value;
|
|
|
|
|
|
if (d > 30) return;
|
|
|
|
|
|
|
|
|
|
|
|
final ms = withinTolerance.value
|
|
|
|
|
|
? 150
|
|
|
|
|
|
: d <= 1
|
|
|
|
|
|
? 250
|
|
|
|
|
|
: d <= 3
|
|
|
|
|
|
? 450
|
|
|
|
|
|
: d <= 10
|
|
|
|
|
|
? 800
|
|
|
|
|
|
: 1500;
|
|
|
|
|
|
|
|
|
|
|
|
_hapticTimer = Timer(Duration(milliseconds: ms), () {
|
|
|
|
|
|
if (withinTolerance.value) {
|
|
|
|
|
|
HapticFeedback.mediumImpact();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
HapticFeedback.lightImpact();
|
|
|
|
|
|
}
|
2026-07-06 14:26:16 +02:00
|
|
|
|
if (soundEnabled.value) SystemSound.play(SystemSoundType.click);
|
2026-07-06 11:47:41 +02:00
|
|
|
|
_scheduleHaptic();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static double _bearingDeg(double dy, double dx) {
|
|
|
|
|
|
final b = math.atan2(dy, dx) * 180 / math.pi;
|
|
|
|
|
|
return (b + 360) % 360;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Vonal-geometria ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
List<StakeoutPoint> _linePoints(String lineId) {
|
|
|
|
|
|
final lp = points.where((p) => p.lineId == lineId && !p.isOffset).toList()
|
|
|
|
|
|
..sort((a, b) => a.station.compareTo(b.station));
|
|
|
|
|
|
return lp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A vonal iránya (fok) az adott pontnál, a station-növekedés felé.
|
|
|
|
|
|
double? lineBearingAt(StakeoutPoint p) {
|
|
|
|
|
|
final lp = _linePoints(p.lineId);
|
|
|
|
|
|
if (lp.length < 2) return null;
|
|
|
|
|
|
var idx = lp.indexWhere((e) => e.uuid == p.uuid);
|
|
|
|
|
|
if (idx < 0) {
|
|
|
|
|
|
// Eltolt pont: a legközelebbi vonalpont szerint.
|
|
|
|
|
|
var bestD = double.infinity;
|
|
|
|
|
|
for (var k = 0; k < lp.length; k++) {
|
|
|
|
|
|
final d = math.pow(lp[k].planEovY - p.planEovY, 2) +
|
|
|
|
|
|
math.pow(lp[k].planEovX - p.planEovX, 2);
|
|
|
|
|
|
if (d < bestD) {
|
|
|
|
|
|
bestD = d.toDouble();
|
|
|
|
|
|
idx = k;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double segBearing(StakeoutPoint a, StakeoutPoint b) =>
|
|
|
|
|
|
_bearingDeg(b.planEovY - a.planEovY, b.planEovX - a.planEovX);
|
|
|
|
|
|
|
|
|
|
|
|
if (idx == 0) return segBearing(lp[0], lp[1]);
|
|
|
|
|
|
if (idx == lp.length - 1) return segBearing(lp[idx - 1], lp[idx]);
|
|
|
|
|
|
final b1 = segBearing(lp[idx - 1], lp[idx]) * math.pi / 180;
|
|
|
|
|
|
final b2 = segBearing(lp[idx], lp[idx + 1]) * math.pi / 180;
|
|
|
|
|
|
return _bearingDeg(
|
|
|
|
|
|
math.sin(b1) + math.sin(b2), math.cos(b1) + math.cos(b2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Műveletek (adatbázis: AppDatabase)
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
/// Tárolás a jelenlegi (vagy átlagolt) mért pozícióval. Ha az
|
|
|
|
|
|
/// [averagingSeconds] > 0, a megadott ideig gyűjti és átlagolja a
|
|
|
|
|
|
/// pozíciókat — csökkenti a pillanatnyi zaj hatását. A rekordba az
|
|
|
|
|
|
/// eltérés mindig vonal-relatívan (inline/crossline) kerül; vonal
|
|
|
|
|
|
/// híján É/K bontásban.
|
2026-07-06 11:47:41 +02:00
|
|
|
|
Future<StakeoutPoint?> storeCurrent() async {
|
|
|
|
|
|
final t = target.value;
|
2026-07-06 14:26:16 +02:00
|
|
|
|
if (t == null || !hasPosition.value || isAveraging.value) return null;
|
2026-07-06 11:47:41 +02:00
|
|
|
|
final gnss = GnssService.to;
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
double useY = curEovY.value;
|
|
|
|
|
|
double useX = curEovX.value;
|
|
|
|
|
|
double useAlt = curAlt.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (averagingSeconds.value > 0) {
|
|
|
|
|
|
final avg = await _collectAverage(averagingSeconds.value);
|
|
|
|
|
|
useY = avg.y;
|
|
|
|
|
|
useX = avg.x;
|
|
|
|
|
|
useAlt = avg.alt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final dy = t.planEovY - useY;
|
|
|
|
|
|
final dx = t.planEovX - useX;
|
2026-07-06 11:47:41 +02:00
|
|
|
|
final lineBearing = lineBearingAt(t);
|
|
|
|
|
|
final rad = (lineBearing ?? 0) * math.pi / 180;
|
|
|
|
|
|
final inline =
|
|
|
|
|
|
lineBearing != null ? dy * math.sin(rad) + dx * math.cos(rad) : dx;
|
|
|
|
|
|
final crossline =
|
|
|
|
|
|
lineBearing != null ? dy * math.cos(rad) - dx * math.sin(rad) : dy;
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
final w = CoordConverterService.to.eovToWgsPoint(useY, useX);
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
final updated = t.copyWith(
|
|
|
|
|
|
status: StakeoutStatus.staked,
|
2026-07-06 14:26:16 +02:00
|
|
|
|
measuredEovY: useY,
|
|
|
|
|
|
measuredEovX: useX,
|
|
|
|
|
|
measuredEovZ: useAlt,
|
|
|
|
|
|
measuredLat: w.y,
|
|
|
|
|
|
measuredLon: w.x,
|
2026-07-06 11:47:41 +02:00
|
|
|
|
devInline: inline,
|
|
|
|
|
|
devCrossline: crossline,
|
2026-07-06 14:26:16 +02:00
|
|
|
|
devDz: t.planEovZ != null ? t.planEovZ! - useAlt : null,
|
2026-07-07 02:48:37 +02:00
|
|
|
|
tiltDeg: Get.isRegistered<TiltService>() &&
|
|
|
|
|
|
TiltService.to.isCalibrated.value &&
|
|
|
|
|
|
!TiltService.to.isMoving.value
|
|
|
|
|
|
? TiltService.to.tiltDeg.value
|
|
|
|
|
|
: null,
|
2026-07-06 11:47:41 +02:00
|
|
|
|
fixQuality: gnss.gpsQuality.value,
|
|
|
|
|
|
accuracy: gnss.horizontalAccuracy,
|
|
|
|
|
|
stakedAt: DateTime.now(),
|
|
|
|
|
|
);
|
|
|
|
|
|
await _db.updateStakeoutPoint(updated);
|
|
|
|
|
|
final i = points.indexWhere((p) => p.uuid == t.uuid);
|
|
|
|
|
|
if (i >= 0) points[i] = updated;
|
|
|
|
|
|
points.refresh();
|
|
|
|
|
|
return updated;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
/// Pozíció-átlagolás: [seconds] másodpercig minden GGA-pozíciót
|
|
|
|
|
|
/// gyűjt, majd átlagol. Ha nem jött minta, a pillanatnyi értéket adja.
|
|
|
|
|
|
Future<({double y, double x, double alt})> _collectAverage(
|
|
|
|
|
|
int seconds) async {
|
|
|
|
|
|
isAveraging.value = true;
|
|
|
|
|
|
averagingElapsed.value = 0;
|
|
|
|
|
|
final ys = <double>[], xs = <double>[], alts = <double>[];
|
|
|
|
|
|
|
|
|
|
|
|
final worker = ever(curEovY, (_) {
|
|
|
|
|
|
ys.add(curEovY.value);
|
|
|
|
|
|
xs.add(curEovX.value);
|
|
|
|
|
|
alts.add(curAlt.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
final ticker = Timer.periodic(
|
|
|
|
|
|
const Duration(seconds: 1), (_) => averagingElapsed.value++);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Future.delayed(Duration(seconds: seconds));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
worker.dispose();
|
|
|
|
|
|
ticker.cancel();
|
|
|
|
|
|
isAveraging.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ys.isEmpty) {
|
|
|
|
|
|
return (y: curEovY.value, x: curEovX.value, alt: curAlt.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
double mean(List<double> l) => l.reduce((a, b) => a + b) / l.length;
|
|
|
|
|
|
return (y: mean(ys), x: mean(xs), alt: mean(alts));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
Future<void> skipCurrent() async {
|
|
|
|
|
|
final t = target.value;
|
|
|
|
|
|
if (t == null) return;
|
|
|
|
|
|
final updated = t.copyWith(status: StakeoutStatus.skipped);
|
|
|
|
|
|
await _db.updateStakeoutPoint(updated);
|
|
|
|
|
|
final i = points.indexWhere((p) => p.uuid == t.uuid);
|
|
|
|
|
|
if (i >= 0) points[i] = updated;
|
|
|
|
|
|
points.refresh();
|
|
|
|
|
|
advance();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 14:26:16 +02:00
|
|
|
|
/// Státusz-állítás a listából (kihagyás / visszaállítás).
|
|
|
|
|
|
Future<void> setPointStatus(StakeoutPoint p, StakeoutStatus status) async {
|
|
|
|
|
|
final updated = p.copyWith(status: status);
|
|
|
|
|
|
await _db.updateStakeoutPoint(updated);
|
|
|
|
|
|
final i = points.indexWhere((e) => e.uuid == p.uuid);
|
|
|
|
|
|
if (i >= 0) points[i] = updated;
|
|
|
|
|
|
points.refresh();
|
|
|
|
|
|
if (target.value?.uuid == p.uuid && status != StakeoutStatus.pending) {
|
|
|
|
|
|
advance();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Pont törlése (soft delete — a szinkron viszi a többi eszközre is).
|
|
|
|
|
|
Future<void> deletePoint(StakeoutPoint p) async {
|
|
|
|
|
|
await _db.softDeleteStakeoutPoint(p.id!);
|
|
|
|
|
|
points.removeWhere((e) => e.uuid == p.uuid);
|
|
|
|
|
|
if (target.value?.uuid == p.uuid) {
|
|
|
|
|
|
target.value = _firstPending();
|
|
|
|
|
|
_recompute();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
/// Transzverzális eltolt pont: a vonalra merőlegesen [dist] méterre
|
|
|
|
|
|
/// ([toRight] = jobbra a station-növekedés irányából nézve). Az új
|
|
|
|
|
|
/// pont lesz a cél; az eltolás-vektor a rekordba kerül.
|
|
|
|
|
|
Future<StakeoutPoint?> createOffset(
|
|
|
|
|
|
{required double dist, required bool toRight}) async {
|
|
|
|
|
|
final t = target.value;
|
|
|
|
|
|
if (t == null) return null;
|
|
|
|
|
|
|
|
|
|
|
|
final base = lineBearingAt(t) ?? travelHeading.value ?? 0;
|
|
|
|
|
|
final bearing = (base + (toRight ? 90 : -90) + 360) % 360;
|
|
|
|
|
|
final rad = bearing * math.pi / 180;
|
|
|
|
|
|
final eovY = t.planEovY + dist * math.sin(rad);
|
|
|
|
|
|
final eovX = t.planEovX + dist * math.cos(rad);
|
|
|
|
|
|
final w = CoordConverterService.to.eovToWgsPoint(eovY, eovX);
|
|
|
|
|
|
|
|
|
|
|
|
final offset = StakeoutPoint(
|
|
|
|
|
|
projectId: t.projectId,
|
|
|
|
|
|
lineId: t.lineId,
|
|
|
|
|
|
station: t.station,
|
|
|
|
|
|
name: '${t.name}/E',
|
|
|
|
|
|
pointType: t.pointType,
|
|
|
|
|
|
source: 'offset',
|
|
|
|
|
|
planEovY: eovY,
|
|
|
|
|
|
planEovX: eovX,
|
|
|
|
|
|
planEovZ: t.planEovZ,
|
|
|
|
|
|
planLat: w.y,
|
|
|
|
|
|
planLon: w.x,
|
|
|
|
|
|
isOffset: true,
|
|
|
|
|
|
parentUuid: t.uuid,
|
|
|
|
|
|
offsetDist: dist,
|
|
|
|
|
|
offsetBearing: bearing,
|
|
|
|
|
|
);
|
|
|
|
|
|
final id = await _db.insertStakeoutPoint(offset);
|
|
|
|
|
|
final saved = offset.copyWith(id: id);
|
|
|
|
|
|
points.add(saved);
|
|
|
|
|
|
setTarget(saved);
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|