Jármű navigáció @3h
This commit is contained in:
@@ -30,6 +30,7 @@ import 'package:terepi_seged/services/stakeout_sync_service.dart';
|
|||||||
import 'package:terepi_seged/services/tilt_service.dart';
|
import 'package:terepi_seged/services/tilt_service.dart';
|
||||||
import 'package:terepi_seged/services/track_sync_service.dart';
|
import 'package:terepi_seged/services/track_sync_service.dart';
|
||||||
import 'package:terepi_seged/services/ts_sync_service.dart';
|
import 'package:terepi_seged/services/ts_sync_service.dart';
|
||||||
|
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
||||||
import 'package:terepi_seged/services/version_gate_service.dart';
|
import 'package:terepi_seged/services/version_gate_service.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
@@ -101,6 +102,7 @@ Future<void> main() async {
|
|||||||
Get.put(TiltService());
|
Get.put(TiltService());
|
||||||
Get.put(PermissionService());
|
Get.put(PermissionService());
|
||||||
Get.put(ContactService());
|
Get.put(ContactService());
|
||||||
|
Get.put(VehicleIdentityService());
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
/// Érzékelő-csatorna: a MŰSZER geometriája — melyik csatornaszám melyik
|
||||||
|
/// vonal/állomás fizikai pontjához tartozik.
|
||||||
|
///
|
||||||
|
/// SZÁNDÉKOSAN önálló modell (nem a StakeoutPoint kiterjesztése): az
|
||||||
|
/// adat forrása lehet SPS-import (a műszer geometria-exportja), de
|
||||||
|
/// származhat máshonnan is (kézi rögzítés, jövőbeli más formátum).
|
||||||
|
/// A kitűzési (GNSS-szel mért) pontokkal a (lineId, station) párossal
|
||||||
|
/// vetjük össze FUTÁSIDŐBEN (lásd StakeoutService), nem tárolunk
|
||||||
|
/// másolatot a mért pozícióból — így sosem megy szét a két adat.
|
||||||
|
class SensorChannel {
|
||||||
|
final int? id;
|
||||||
|
final String uuid;
|
||||||
|
final int projectId;
|
||||||
|
|
||||||
|
final int channel;
|
||||||
|
final String lineId;
|
||||||
|
final int station;
|
||||||
|
|
||||||
|
/// Terv-pozíció az SPS R-fájlból (ha volt hozzá tartozó vevőpont-sor).
|
||||||
|
/// Null, ha csak a csatorna-hozzárendelés ismert (pl. csak X-fájl volt),
|
||||||
|
/// ilyenkor a GNSS-mért kitűzési pont adja az egyetlen pozíciót.
|
||||||
|
final double? planEovY;
|
||||||
|
final double? planEovX;
|
||||||
|
final double? planLat;
|
||||||
|
final double? planLon;
|
||||||
|
|
||||||
|
final String source; // 'sps' | 'manual'
|
||||||
|
final String? importBatch; // egy import-menet azonosítója (törléshez)
|
||||||
|
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
SensorChannel({
|
||||||
|
this.id,
|
||||||
|
String? uuid,
|
||||||
|
required this.projectId,
|
||||||
|
required this.channel,
|
||||||
|
required this.lineId,
|
||||||
|
required this.station,
|
||||||
|
this.planEovY,
|
||||||
|
this.planEovX,
|
||||||
|
this.planLat,
|
||||||
|
this.planLon,
|
||||||
|
this.source = 'sps',
|
||||||
|
this.importBatch,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) : uuid = uuid ?? const Uuid().v4(),
|
||||||
|
createdAt = createdAt ?? DateTime.now(),
|
||||||
|
updatedAt = updatedAt ?? DateTime.now();
|
||||||
|
|
||||||
|
bool get hasPlanPosition => planEovY != null && planEovX != null;
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'uuid': uuid,
|
||||||
|
'project_id': projectId,
|
||||||
|
'channel': channel,
|
||||||
|
'line_id': lineId,
|
||||||
|
'station': station,
|
||||||
|
'plan_eov_y': planEovY,
|
||||||
|
'plan_eov_x': planEovX,
|
||||||
|
'plan_lat': planLat,
|
||||||
|
'plan_lon': planLon,
|
||||||
|
'source': source,
|
||||||
|
'import_batch': importBatch,
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory SensorChannel.fromMap(Map<String, dynamic> m) => SensorChannel(
|
||||||
|
id: m['id'] as int?,
|
||||||
|
uuid: m['uuid'] as String,
|
||||||
|
projectId: m['project_id'] as int,
|
||||||
|
channel: m['channel'] as int,
|
||||||
|
lineId: (m['line_id'] as String?) ?? '',
|
||||||
|
station: m['station'] as int,
|
||||||
|
planEovY: (m['plan_eov_y'] as num?)?.toDouble(),
|
||||||
|
planEovX: (m['plan_eov_x'] as num?)?.toDouble(),
|
||||||
|
planLat: (m['plan_lat'] as num?)?.toDouble(),
|
||||||
|
planLon: (m['plan_lon'] as num?)?.toDouble(),
|
||||||
|
source: (m['source'] as String?) ?? 'sps',
|
||||||
|
importBatch: m['import_batch'] as String?,
|
||||||
|
createdAt: DateTime.tryParse((m['created_at'] as String?) ?? '') ??
|
||||||
|
DateTime.now(),
|
||||||
|
updatedAt: DateTime.tryParse((m['updated_at'] as String?) ?? '') ??
|
||||||
|
DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
/// Forráspont (vibrátor-állomás) — az SPS S-fájlból importált TERV-pozíció.
|
||||||
|
///
|
||||||
|
/// Tisztán referencia-réteg: nincs "kijelölve/meglőve" mutálható állapota
|
||||||
|
/// (a döntés szerint a tényleges ellenőrzés a periodikus pozíciónapló és
|
||||||
|
/// a terv UTÓLAGOS összevetéséből adódik — lásd VibratorNavController).
|
||||||
|
class SourcePoint {
|
||||||
|
final int? id;
|
||||||
|
final String uuid;
|
||||||
|
final int projectId;
|
||||||
|
|
||||||
|
final String lineId;
|
||||||
|
final int station; // VP (vibration point) szám
|
||||||
|
|
||||||
|
final double planEovY;
|
||||||
|
final double planEovX;
|
||||||
|
final double planLat;
|
||||||
|
final double planLon;
|
||||||
|
|
||||||
|
final String source; // 'sps'
|
||||||
|
final String? importBatch;
|
||||||
|
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
SourcePoint({
|
||||||
|
this.id,
|
||||||
|
String? uuid,
|
||||||
|
required this.projectId,
|
||||||
|
required this.lineId,
|
||||||
|
required this.station,
|
||||||
|
required this.planEovY,
|
||||||
|
required this.planEovX,
|
||||||
|
required this.planLat,
|
||||||
|
required this.planLon,
|
||||||
|
this.source = 'sps',
|
||||||
|
this.importBatch,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) : uuid = uuid ?? const Uuid().v4(),
|
||||||
|
createdAt = createdAt ?? DateTime.now();
|
||||||
|
|
||||||
|
String get displayId => '$lineId · $station';
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'uuid': uuid,
|
||||||
|
'project_id': projectId,
|
||||||
|
'line_id': lineId,
|
||||||
|
'station': station,
|
||||||
|
'plan_eov_y': planEovY,
|
||||||
|
'plan_eov_x': planEovX,
|
||||||
|
'plan_lat': planLat,
|
||||||
|
'plan_lon': planLon,
|
||||||
|
'source': source,
|
||||||
|
'import_batch': importBatch,
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory SourcePoint.fromMap(Map<String, dynamic> m) => SourcePoint(
|
||||||
|
id: m['id'] as int?,
|
||||||
|
uuid: m['uuid'] as String,
|
||||||
|
projectId: m['project_id'] as int,
|
||||||
|
lineId: (m['line_id'] as String?) ?? '',
|
||||||
|
station: m['station'] as int,
|
||||||
|
planEovY: (m['plan_eov_y'] as num).toDouble(),
|
||||||
|
planEovX: (m['plan_eov_x'] as num).toDouble(),
|
||||||
|
planLat: (m['plan_lat'] as num).toDouble(),
|
||||||
|
planLon: (m['plan_lon'] as num).toDouble(),
|
||||||
|
source: (m['source'] as String?) ?? 'sps',
|
||||||
|
importBatch: m['import_batch'] as String?,
|
||||||
|
createdAt: DateTime.tryParse((m['created_at'] as String?) ?? '') ??
|
||||||
|
DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
/// Periodikus járműpozíció-minta — a navigációs oldal a beállított
|
||||||
|
/// időközönként ide ment egy sort, amíg a rögzítés fut. Ebből (és a
|
||||||
|
/// SourcePoint terv-pozíciókból) számítható utólag, mely forráspontok
|
||||||
|
/// mellett járt ténylegesen a jármű.
|
||||||
|
class VehiclePositionLog {
|
||||||
|
final int? id;
|
||||||
|
final String uuid;
|
||||||
|
final int projectId;
|
||||||
|
|
||||||
|
final String vehicleId; // pl. "V1" / "V2" / "V3"
|
||||||
|
|
||||||
|
final double eovY;
|
||||||
|
final double eovX;
|
||||||
|
final double lat;
|
||||||
|
final double lon;
|
||||||
|
final double? altitude;
|
||||||
|
final double? speedKmh;
|
||||||
|
final double? heading;
|
||||||
|
final int? fixQuality;
|
||||||
|
final double? accuracy;
|
||||||
|
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String? deviceId;
|
||||||
|
final String? appInstanceId;
|
||||||
|
|
||||||
|
VehiclePositionLog({
|
||||||
|
this.id,
|
||||||
|
String? uuid,
|
||||||
|
required this.projectId,
|
||||||
|
required this.vehicleId,
|
||||||
|
required this.eovY,
|
||||||
|
required this.eovX,
|
||||||
|
required this.lat,
|
||||||
|
required this.lon,
|
||||||
|
this.altitude,
|
||||||
|
this.speedKmh,
|
||||||
|
this.heading,
|
||||||
|
this.fixQuality,
|
||||||
|
this.accuracy,
|
||||||
|
DateTime? timestamp,
|
||||||
|
this.deviceId,
|
||||||
|
this.appInstanceId,
|
||||||
|
}) : uuid = uuid ?? const Uuid().v4(),
|
||||||
|
timestamp = timestamp ?? DateTime.now();
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'uuid': uuid,
|
||||||
|
'project_id': projectId,
|
||||||
|
'vehicle_id': vehicleId,
|
||||||
|
'eov_y': eovY,
|
||||||
|
'eov_x': eovX,
|
||||||
|
'lat': lat,
|
||||||
|
'lon': lon,
|
||||||
|
'altitude': altitude,
|
||||||
|
'speed_kmh': speedKmh,
|
||||||
|
'heading': heading,
|
||||||
|
'fix_quality': fixQuality,
|
||||||
|
'accuracy': accuracy,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'device_id': deviceId,
|
||||||
|
'app_instance_id': appInstanceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory VehiclePositionLog.fromMap(Map<String, dynamic> m) =>
|
||||||
|
VehiclePositionLog(
|
||||||
|
id: m['id'] as int?,
|
||||||
|
uuid: m['uuid'] as String,
|
||||||
|
projectId: m['project_id'] as int,
|
||||||
|
vehicleId: (m['vehicle_id'] as String?) ?? '',
|
||||||
|
eovY: (m['eov_y'] as num).toDouble(),
|
||||||
|
eovX: (m['eov_x'] as num).toDouble(),
|
||||||
|
lat: (m['lat'] as num).toDouble(),
|
||||||
|
lon: (m['lon'] as num).toDouble(),
|
||||||
|
altitude: (m['altitude'] as num?)?.toDouble(),
|
||||||
|
speedKmh: (m['speed_kmh'] as num?)?.toDouble(),
|
||||||
|
heading: (m['heading'] as num?)?.toDouble(),
|
||||||
|
fixQuality: m['fix_quality'] as int?,
|
||||||
|
accuracy: (m['accuracy'] as num?)?.toDouble(),
|
||||||
|
timestamp: DateTime.tryParse((m['timestamp'] as String?) ?? '') ??
|
||||||
|
DateTime.now(),
|
||||||
|
deviceId: m['device_id'] as String?,
|
||||||
|
appInstanceId: m['app_instance_id'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:get/get.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 = 60.obs; // felhasználó által állítható
|
||||||
|
Timer? _logTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
load();
|
||||||
|
if (Get.isRegistered<GnssService>()) {
|
||||||
|
ever(GnssService.to.lastGgaLine, (_) => _onPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_logTimer?.cancel();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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/gnss/gnss_service.dart';
|
||||||
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
|
import 'package:terepi_seged/services/sps_import_service.dart';
|
||||||
|
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
||||||
|
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
|
||||||
|
|
||||||
|
import '../controllers/vibro_nav_controller.dart';
|
||||||
|
|
||||||
|
/// Egyszerű navigációs oldal a vibrátor-járműhöz. ÖNÁLLÓ oldal, nem
|
||||||
|
/// MapSurveyMode — saját MapController kell a forgó (track-up)
|
||||||
|
/// térképhez, hogy ez semmilyen más módot ne érintsen.
|
||||||
|
class VibroNavView extends StatefulWidget {
|
||||||
|
const VibroNavView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VibroNavView> createState() => _VibroNavViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VibroNavViewState extends State<VibroNavView> {
|
||||||
|
late final VibroNavController controller;
|
||||||
|
final _mapController = MapController();
|
||||||
|
List<VehiclePositionLog> _logs = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller = Get.put(VibroNavController());
|
||||||
|
_refreshLogs();
|
||||||
|
// Forgatás + a naplók frissítése a pozíció-változásokra.
|
||||||
|
ever(controller.travelHeading, (_) => _applyRotation());
|
||||||
|
ever(controller.hasPosition, (_) => _followPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
Get.delete<VibroNavController>();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshLogs() async {
|
||||||
|
_logs = await controller.loadLogs();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyRotation() {
|
||||||
|
final h = controller.travelHeading.value;
|
||||||
|
if (h == null) return;
|
||||||
|
try {
|
||||||
|
_mapController.rotate(-h);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _followPosition() {
|
||||||
|
if (!controller.hasPosition.value ||
|
||||||
|
!Get.isRegistered<CoordConverterService>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final gnss = GnssService.to;
|
||||||
|
try {
|
||||||
|
_mapController.move(LatLng(gnss.latitude.value, gnss.longitude.value),
|
||||||
|
_mapController.camera.zoom);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Vibrátor navigáció'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.upload_file),
|
||||||
|
tooltip: 'Forráspontok importja (SPS S-fájl)',
|
||||||
|
onPressed: _importSourcePoints,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.local_shipping_outlined),
|
||||||
|
tooltip: 'Jármű kiválasztása',
|
||||||
|
onPressed: _pickVehicle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
_buildMap(),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
child: _VehicleBadge(),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
child: _NavPanel(
|
||||||
|
controller: controller, onLoggingToggled: _refreshLogs),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMap() {
|
||||||
|
return Obx(() {
|
||||||
|
final points = controller.sourcePoints;
|
||||||
|
final nearest = controller.nearestPoint.value;
|
||||||
|
|
||||||
|
return FlutterMap(
|
||||||
|
mapController: _mapController,
|
||||||
|
options: const MapOptions(
|
||||||
|
initialCenter: LatLng(47.5, 19.05),
|
||||||
|
initialZoom: 15,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
userAgentPackageName: 'hu.app_dev.terepi_seged',
|
||||||
|
),
|
||||||
|
// Tervezett útvonal + minden más importált háttérréteg — a
|
||||||
|
// MEGLÉVŐ rétegimport-mechanizmus, nincs hozzá új kód.
|
||||||
|
const ImportedLayerOverlay(),
|
||||||
|
MarkerLayer(markers: [
|
||||||
|
for (final sp in points)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(sp.planLat, sp.planLon),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: _SourcePointMarker(
|
||||||
|
point: sp,
|
||||||
|
isNearest: sp.uuid == nearest?.uuid,
|
||||||
|
isVerified: controller.isVerified(sp, _logs),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (controller.hasPosition.value)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(GnssService.to.latitude.value,
|
||||||
|
GnssService.to.longitude.value),
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: const _VehicleMarker(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Jármű-választás ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _pickVehicle() async {
|
||||||
|
final chosen = await Get.dialog<String>(AlertDialog(
|
||||||
|
title: const Text('Melyik járműben van ez a tablet?'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final v in VehicleIdentityService.availableVehicles)
|
||||||
|
RadioListTile<String>(
|
||||||
|
title: Text(v),
|
||||||
|
value: v,
|
||||||
|
groupValue: VehicleIdentityService.to.selectedVehicle.value,
|
||||||
|
onChanged: (val) => Get.back(result: val),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
if (chosen != null) {
|
||||||
|
await VehicleIdentityService.to.setVehicle(chosen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SPS S-fájl (forráspont) import — kompakt, egyetlen dialógus ────
|
||||||
|
|
||||||
|
Future<void> _importSourcePoints() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||||
|
final path = result?.files.single.path;
|
||||||
|
if (path == null) return;
|
||||||
|
|
||||||
|
final projectId = ProjectService.to.activeProjectId;
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final content = await File(path).readAsString();
|
||||||
|
final points = SpsParser.parseSourceFile(content);
|
||||||
|
if (points.isEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Import',
|
||||||
|
'Nem sikerült forráspontot beolvasni ebből a fájlból — '
|
||||||
|
'ellenőrizd, hogy valódi SPS S-fájl-e.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final withPos = points.where((p) => p.eovY != null && p.eovX != null);
|
||||||
|
final ok = await Get.dialog<bool>(AlertDialog(
|
||||||
|
title: const Text('Forráspontok importja'),
|
||||||
|
content: Text('${points.length} pont az S-fájlban, '
|
||||||
|
'${withPos.length} érvényes koordinátával.\n\n'
|
||||||
|
'Első néhány: ${points.take(3).map((p) => '${p.lineId}·${p.station}').join(', ')}…'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(result: false),
|
||||||
|
child: const Text('Mégse')),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Get.back(result: true),
|
||||||
|
child: const Text('Import')),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
if (ok != true) return;
|
||||||
|
|
||||||
|
final conv = CoordConverterService.to;
|
||||||
|
final batch = DateTime.now().toIso8601String();
|
||||||
|
final saved = <SourcePoint>[];
|
||||||
|
for (final p in withPos) {
|
||||||
|
final w = conv.eovToWgsPoint(p.eovY!, p.eovX!);
|
||||||
|
saved.add(SourcePoint(
|
||||||
|
projectId: projectId,
|
||||||
|
lineId: p.lineId,
|
||||||
|
station: p.station,
|
||||||
|
planEovY: p.eovY!,
|
||||||
|
planEovX: p.eovX!,
|
||||||
|
planLat: w.y,
|
||||||
|
planLon: w.x,
|
||||||
|
importBatch: batch,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final inserted = await AppDatabase.instance.insertSourcePoints(saved);
|
||||||
|
await controller.load();
|
||||||
|
Get.snackbar('Import kész', '$inserted forráspont importálva',
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar('Import hiba', e.toString(),
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// HUD panel: sebesség, táv, legközelebbi pont, rögzítés vezérlés
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class _NavPanel extends StatelessWidget {
|
||||||
|
final VibroNavController controller;
|
||||||
|
final VoidCallback onLoggingToggled;
|
||||||
|
const _NavPanel({required this.controller, required this.onLoggingToggled});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
final nearest = controller.nearestPoint.value;
|
||||||
|
return Card(
|
||||||
|
elevation: 6,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_Stat(
|
||||||
|
label: 'Sebesség',
|
||||||
|
value:
|
||||||
|
'${controller.speedKmh.value.toStringAsFixed(0)} km/h'),
|
||||||
|
_Stat(
|
||||||
|
label: 'Megtett táv',
|
||||||
|
value: _fmtDist(controller.sessionDistanceM.value)),
|
||||||
|
if (nearest != null)
|
||||||
|
_Stat(
|
||||||
|
label: 'Legközelebbi VP',
|
||||||
|
value: nearest.displayId,
|
||||||
|
highlight: true),
|
||||||
|
if (nearest != null)
|
||||||
|
_Stat(
|
||||||
|
label: 'Táv odáig',
|
||||||
|
value: _fmtDist(controller.nearestDistance.value)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: controller.isLogging.value
|
||||||
|
? OutlinedButton.icon(
|
||||||
|
icon: const Icon(Icons.stop_circle_outlined),
|
||||||
|
label: Text('Rögzítés leállítása '
|
||||||
|
'(${controller.logIntervalSec.value} mp)'),
|
||||||
|
onPressed: () {
|
||||||
|
controller.stopLogging();
|
||||||
|
onLoggingToggled();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: FilledButton.icon(
|
||||||
|
icon: const Icon(Icons.fiber_manual_record,
|
||||||
|
color: Colors.red),
|
||||||
|
label: Text('Rögzítés indítása '
|
||||||
|
'(${controller.logIntervalSec.value} mp-enként)'),
|
||||||
|
onPressed: () {
|
||||||
|
controller.startLogging();
|
||||||
|
onLoggingToggled();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.timer_outlined),
|
||||||
|
tooltip: 'Rögzítési időköz',
|
||||||
|
onPressed: controller.isLogging.value
|
||||||
|
? null
|
||||||
|
: () => _pickInterval(controller),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickInterval(VibroNavController c) async {
|
||||||
|
final opts = [15, 30, 60, 120, 300];
|
||||||
|
final chosen = await Get.dialog<int>(AlertDialog(
|
||||||
|
title: const Text('Rögzítési időköz'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final s in opts)
|
||||||
|
RadioListTile<int>(
|
||||||
|
title: Text(s < 60 ? '$s másodperc' : '${s ~/ 60} perc'),
|
||||||
|
value: s,
|
||||||
|
groupValue: c.logIntervalSec.value,
|
||||||
|
onChanged: (v) => Get.back(result: v),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
if (chosen != null) c.logIntervalSec.value = chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _fmtDist(double m) => m < 1000
|
||||||
|
? '${m.toStringAsFixed(0)} m'
|
||||||
|
: '${(m / 1000).toStringAsFixed(2)} km';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Stat extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final bool highlight;
|
||||||
|
const _Stat(
|
||||||
|
{required this.label, required this.value, this.highlight = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: highlight ? 16 : 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color:
|
||||||
|
highlight ? Theme.of(context).colorScheme.primary : null)),
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class _VehicleBadge extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
final v = VehicleIdentityService.to.selectedVehicle.value;
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: v == null
|
||||||
|
? Colors.orange.withOpacity(0.9)
|
||||||
|
: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
v == null ? 'Nincs jármű kiválasztva!' : 'Jármű: $v',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SourcePointMarker extends StatelessWidget {
|
||||||
|
final SourcePoint point;
|
||||||
|
final bool isNearest;
|
||||||
|
final bool isVerified;
|
||||||
|
const _SourcePointMarker(
|
||||||
|
{required this.point, required this.isNearest, required this.isVerified});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = isVerified ? Colors.green : Colors.grey.shade600;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isVerified ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||||
|
color: isNearest ? Colors.red : color,
|
||||||
|
size: isNearest ? 26 : 18,
|
||||||
|
),
|
||||||
|
Text(point.displayId,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: isNearest ? FontWeight.w700 : FontWeight.w400,
|
||||||
|
color: isNearest ? Colors.red : Colors.black87)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VehicleMarker extends StatelessWidget {
|
||||||
|
const _VehicleMarker();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Track-up módban a szimbólum mindig "felfelé" mutat — a világ
|
||||||
|
// forog körülötte, nem a szimbólum a világ körül.
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 3),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.blue.withOpacity(0.5), blurRadius: 8)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.navigation, color: Colors.white, size: 18),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import 'package:terepi_seged/pages/start/bindings/start_page_bindings.dart';
|
|||||||
import 'package:terepi_seged/pages/start/presentation/views/start_page.dart';
|
import 'package:terepi_seged/pages/start/presentation/views/start_page.dart';
|
||||||
import 'package:terepi_seged/pages/tracking/bindings/tracking_bindings.dart';
|
import 'package:terepi_seged/pages/tracking/bindings/tracking_bindings.dart';
|
||||||
import 'package:terepi_seged/pages/tracking/presentation/views/tracking_view.dart';
|
import 'package:terepi_seged/pages/tracking/presentation/views/tracking_view.dart';
|
||||||
|
import 'package:terepi_seged/pages/vibro_nav/presentation/views/vibro_nav_view.dart';
|
||||||
|
|
||||||
import '../pages/map_test/bindings/map_test_bindings.dart';
|
import '../pages/map_test/bindings/map_test_bindings.dart';
|
||||||
import '../pages/map_test/presentation/views/map_test_view.dart';
|
import '../pages/map_test/presentation/views/map_test_view.dart';
|
||||||
@@ -106,6 +107,7 @@ class AppPages {
|
|||||||
GetPage(name: Routes.SETTINGS, page: () => const SettingsView()),
|
GetPage(name: Routes.SETTINGS, page: () => const SettingsView()),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()),
|
name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()),
|
||||||
GetPage(name: Routes.CONTACTS, page: () => const ContactsView())
|
GetPage(name: Routes.CONTACTS, page: () => const ContactsView()),
|
||||||
|
GetPage(name: Routes.VIBRONAV, page: () => const VibroNavView())
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ abstract class Routes {
|
|||||||
|
|
||||||
static const SETTINGS = '/settings';
|
static const SETTINGS = '/settings';
|
||||||
static const STAKEOUT_IMPORT = '/stakeout_import';
|
static const STAKEOUT_IMPORT = '/stakeout_import';
|
||||||
|
static const VIBRONAV = '/vibro_nav';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import 'package:terepi_seged/models/measured_point.dart';
|
|||||||
import 'package:terepi_seged/models/note_item.dart';
|
import 'package:terepi_seged/models/note_item.dart';
|
||||||
import 'package:terepi_seged/models/note_item_audio.dart';
|
import 'package:terepi_seged/models/note_item_audio.dart';
|
||||||
import 'package:terepi_seged/models/note_item_photo.dart';
|
import 'package:terepi_seged/models/note_item_photo.dart';
|
||||||
|
import 'package:terepi_seged/models/source_point.dart';
|
||||||
import 'package:terepi_seged/models/stakeout_point.dart';
|
import 'package:terepi_seged/models/stakeout_point.dart';
|
||||||
import 'package:terepi_seged/models/track.dart';
|
import 'package:terepi_seged/models/track.dart';
|
||||||
|
import 'package:terepi_seged/models/vechicle_position_log.dart';
|
||||||
import 'package:terepi_seged/services/device_identity_service.dart';
|
import 'package:terepi_seged/services/device_identity_service.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import '../models/project.dart';
|
import '../models/project.dart';
|
||||||
@@ -44,7 +46,7 @@ class AppDatabase {
|
|||||||
final path = p.join(dbDir.path, 'terepi_seged.db');
|
final path = p.join(dbDir.path, 'terepi_seged.db');
|
||||||
|
|
||||||
return openDatabase(path,
|
return openDatabase(path,
|
||||||
version: 5,
|
version: 6,
|
||||||
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
onUpgrade: _onUpgrade);
|
onUpgrade: _onUpgrade);
|
||||||
@@ -271,6 +273,8 @@ class AppDatabase {
|
|||||||
await _createContactsOutbox(db);
|
await _createContactsOutbox(db);
|
||||||
await _addAppInstanceIdColumns(db);
|
await _addAppInstanceIdColumns(db);
|
||||||
|
|
||||||
|
await _createVibratorNavTables(db);
|
||||||
|
|
||||||
// Alap projekt létrehozása az első indításhoz
|
// Alap projekt létrehozása az első indításhoz
|
||||||
final now = DateTime.now().toIso8601String();
|
final now = DateTime.now().toIso8601String();
|
||||||
await db.insert('projects', {
|
await db.insert('projects', {
|
||||||
@@ -303,10 +307,11 @@ class AppDatabase {
|
|||||||
await _migrateToV4(db);
|
await _migrateToV4(db);
|
||||||
}
|
}
|
||||||
if (oldVersion < 5) {
|
if (oldVersion < 5) {
|
||||||
_createContactsOutbox(db);
|
await _createContactsOutbox(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _addAppInstanceIdColumns(db);
|
await _addAppInstanceIdColumns(db);
|
||||||
|
await _createVibratorNavTables(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _migrateToV4(Database db) async {
|
Future<void> _migrateToV4(Database db) async {
|
||||||
@@ -1392,4 +1397,84 @@ class AppDatabase {
|
|||||||
// await db.execute('CREATE INDEX IF NOT EXISTS idx_contacts_outbox_project '
|
// await db.execute('CREATE INDEX IF NOT EXISTS idx_contacts_outbox_project '
|
||||||
// 'ON contacts_outbox(project_id)');
|
// 'ON contacts_outbox(project_id)');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// ── Vibrátor navigáció: forráspontok + járműpozíció-napló ────────
|
||||||
|
|
||||||
|
Future<void> _createVibratorNavTables(Database db) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS source_points (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
line_id TEXT NOT NULL DEFAULT '',
|
||||||
|
station INTEGER NOT NULL,
|
||||||
|
plan_eov_y REAL NOT NULL,
|
||||||
|
plan_eov_x REAL NOT NULL,
|
||||||
|
plan_lat REAL NOT NULL,
|
||||||
|
plan_lon REAL NOT NULL,
|
||||||
|
source TEXT NOT NULL DEFAULT 'sps',
|
||||||
|
import_batch TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
await db.execute('CREATE INDEX IF NOT EXISTS idx_source_points_project '
|
||||||
|
'ON source_points(project_id, line_id, station)');
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS vehicle_position_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
vehicle_id TEXT NOT NULL,
|
||||||
|
eov_y REAL NOT NULL,
|
||||||
|
eov_x REAL NOT NULL,
|
||||||
|
lat REAL NOT NULL,
|
||||||
|
lon REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
speed_kmh REAL,
|
||||||
|
heading REAL,
|
||||||
|
fix_quality INTEGER,
|
||||||
|
accuracy REAL,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
device_id TEXT,
|
||||||
|
app_instance_id TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
await db.execute('CREATE INDEX IF NOT EXISTS idx_vehicle_logs_project '
|
||||||
|
'ON vehicle_position_logs(project_id, vehicle_id, timestamp)');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> insertSourcePoints(List<SourcePoint> points) async {
|
||||||
|
final db = await database;
|
||||||
|
var count = 0;
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
for (final p in points) {
|
||||||
|
await txn.insert('source_points', p.toMap());
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SourcePoint>> listSourcePoints(int projectId) async {
|
||||||
|
final db = await database;
|
||||||
|
final rows = await db.query('source_points',
|
||||||
|
where: 'project_id = ?',
|
||||||
|
whereArgs: [projectId],
|
||||||
|
orderBy: 'station ASC');
|
||||||
|
return rows.map(SourcePoint.fromMap).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertVehiclePositionLog(VehiclePositionLog log) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.insert('vehicle_position_logs', log.toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<VehiclePositionLog>> listVehiclePositionLogs(
|
||||||
|
int projectId) async {
|
||||||
|
final db = await database;
|
||||||
|
final rows = await db.query('vehicle_position_logs',
|
||||||
|
where: 'project_id = ?', whereArgs: [projectId]);
|
||||||
|
return rows.map(VehiclePositionLog.fromMap).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import 'package:terepi_seged/models/sensor_chanel.dart';
|
||||||
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||||
|
|
||||||
|
/// SPS (Shell Processing Support) fájlok beolvasása — a SEG 1993-as
|
||||||
|
/// "SPS Format for Land 3D Surveys" specifikációja szerint, fix
|
||||||
|
/// oszlop-pozíciókkal (1-alapú, záró oszlop is beleértve).
|
||||||
|
///
|
||||||
|
/// Csak azt olvassuk ki, ami a csatorna-geometria ellenőrzéséhez kell:
|
||||||
|
/// * R-fájl (Receiver "Point Record"): vonal, pontszám, EOV Y/X, magasság
|
||||||
|
/// * X-fájl (Relation Record): csatorna-tartomány → vonal + állomás-tartomány
|
||||||
|
///
|
||||||
|
/// A fix oszlopszélességű, évtizedes szabvány gyártónként kicsit eltérő
|
||||||
|
/// exportokat is szülhet — ezért import előtt MINDIG előnézet van
|
||||||
|
/// (lásd SpsImportPreview), soha nem mentünk vakon.
|
||||||
|
class SpsParser {
|
||||||
|
SpsParser._();
|
||||||
|
|
||||||
|
// ── Nyers sor-kivágás (1-alapú, záró oszlop is benne) ─────────────
|
||||||
|
static String _col(String line, int from, int to) {
|
||||||
|
if (line.length < from) return '';
|
||||||
|
final end = line.length < to ? line.length : to;
|
||||||
|
return line.substring(from - 1, end).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _isDataLine(String line, String expectedFirstChar) {
|
||||||
|
if (line.isEmpty) return false;
|
||||||
|
if (line.startsWith('EOF')) return false;
|
||||||
|
if (line[0] == 'H') return false; // fejléc/komment sor
|
||||||
|
return line[0].toUpperCase() == expectedFirstChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// R-fájl (vevőpont) — "Point Record", cols 1-80
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// 1 Rekord-azonosító 1-1 "R"
|
||||||
|
// 2 Vonalnév 2-17
|
||||||
|
// 3 Pontszám 18-25
|
||||||
|
// 4 Pont-index 26-26
|
||||||
|
// 11 EOV Y (easting) 47-55
|
||||||
|
// 12 EOV X (northing) 56-65
|
||||||
|
// 13 Magasság 66-71
|
||||||
|
|
||||||
|
static List<SpsPointRecord> parseReceiverFile(String content) =>
|
||||||
|
_parsePointFile(content, 'R');
|
||||||
|
|
||||||
|
/// Forráspontok (vibrátor-állomások) — az S-fájl UGYANAZT az
|
||||||
|
/// oszlop-elrendezést használja, mint az R-fájl, csak a rekord-jelölő
|
||||||
|
/// betű más.
|
||||||
|
static List<SpsPointRecord> parseSourceFile(String content) =>
|
||||||
|
_parsePointFile(content, 'S');
|
||||||
|
|
||||||
|
static List<SpsPointRecord> _parsePointFile(
|
||||||
|
String content, String recordType) {
|
||||||
|
final result = <SpsPointRecord>[];
|
||||||
|
for (final raw in content.split(RegExp(r'\r\n|\r|\n'))) {
|
||||||
|
if (!_isDataLine(raw, recordType)) continue;
|
||||||
|
final lineId = _col(raw, 2, 17);
|
||||||
|
final pointStr = _col(raw, 18, 25);
|
||||||
|
final indexStr = _col(raw, 26, 26);
|
||||||
|
final eastingStr = _col(raw, 47, 55);
|
||||||
|
final northingStr = _col(raw, 56, 65);
|
||||||
|
final elevStr = _col(raw, 66, 71);
|
||||||
|
|
||||||
|
// A pontszám ritkán tartalmazhat törtrészt — az egész részt vesszük
|
||||||
|
// állomásszámként.
|
||||||
|
final pointNum =
|
||||||
|
int.tryParse(pointStr.split('.').first.replaceAll(RegExp(r'\D'), ''));
|
||||||
|
if (pointNum == null) continue;
|
||||||
|
|
||||||
|
result.add(SpsPointRecord(
|
||||||
|
lineId: lineId,
|
||||||
|
station: pointNum,
|
||||||
|
pointIndex: int.tryParse(indexStr) ?? 1,
|
||||||
|
eovY: double.tryParse(eastingStr),
|
||||||
|
eovX: double.tryParse(northingStr),
|
||||||
|
elevation: double.tryParse(elevStr),
|
||||||
|
rawLine: raw,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// X-fájl (kapcsolat) — "Relation Record", cols 1-80
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// 9 From channel 39-42
|
||||||
|
// 10 To channel 43-46
|
||||||
|
// 11 Channel increment 47-47
|
||||||
|
// 12 Vevő-vonalnév 48-63
|
||||||
|
// 13 From receiver 64-71
|
||||||
|
// 14 To receiver 72-79
|
||||||
|
// 15 Receiver index 80-80
|
||||||
|
|
||||||
|
static List<SpsRelationRecord> parseRelationFile(String content) {
|
||||||
|
final result = <SpsRelationRecord>[];
|
||||||
|
for (final raw in content.split(RegExp(r'\r\n|\r|\n'))) {
|
||||||
|
if (!_isDataLine(raw, 'X')) continue;
|
||||||
|
|
||||||
|
final fromCh = int.tryParse(_col(raw, 39, 42));
|
||||||
|
final toCh = int.tryParse(_col(raw, 43, 46));
|
||||||
|
final chInc = int.tryParse(_col(raw, 47, 47)) ?? 1;
|
||||||
|
final recvLine = _col(raw, 48, 63);
|
||||||
|
final fromRecv = int.tryParse(_col(raw, 64, 71));
|
||||||
|
final toRecv = int.tryParse(_col(raw, 72, 79));
|
||||||
|
|
||||||
|
if (fromCh == null ||
|
||||||
|
toCh == null ||
|
||||||
|
fromRecv == null ||
|
||||||
|
toRecv == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(SpsRelationRecord(
|
||||||
|
fromChannel: fromCh,
|
||||||
|
toChannel: toCh,
|
||||||
|
channelIncrement: chInc <= 0 ? 1 : chInc,
|
||||||
|
recvLineId: recvLine,
|
||||||
|
fromReceiver: fromRecv,
|
||||||
|
toReceiver: toRecv,
|
||||||
|
rawLine: raw,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Egy X-rekord (csatorna-TARTOMÁNY) egyedi (csatorna, vonal, állomás)
|
||||||
|
/// hármasokra bontása. A normál (egykomponensű) esetben a csatorna- és
|
||||||
|
/// vevőszám párhuzamosan fut végig a tartományon. Többkomponensű
|
||||||
|
/// (channel increment > 1) esetet — ritka, pl. 3C geofonoknál — nem
|
||||||
|
/// bontunk szét channelenként külön állomásra, mert az UGYANAHHOZ az
|
||||||
|
/// egy fizikai ponthoz tartozna; ilyenkor a tartomány KEZDŐ csatornáját
|
||||||
|
/// társítjuk a ponthoz, a többit átugorjuk.
|
||||||
|
static List<({int channel, String lineId, int station})> expandRelation(
|
||||||
|
SpsRelationRecord r) {
|
||||||
|
final out = <({int channel, String lineId, int station})>[];
|
||||||
|
if (r.channelIncrement != 1) {
|
||||||
|
out.add((
|
||||||
|
channel: r.fromChannel,
|
||||||
|
lineId: r.recvLineId,
|
||||||
|
station: r.fromReceiver
|
||||||
|
));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
final chCount = r.toChannel - r.fromChannel;
|
||||||
|
final recvCount = r.toReceiver - r.fromReceiver;
|
||||||
|
if (chCount < 0) return out;
|
||||||
|
for (var i = 0; i <= chCount; i++) {
|
||||||
|
final station = chCount == 0
|
||||||
|
? r.fromReceiver
|
||||||
|
: r.fromReceiver + (recvCount * i / chCount).round();
|
||||||
|
out.add(
|
||||||
|
(channel: r.fromChannel + i, lineId: r.recvLineId, station: station));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// Előnézet összeállítása — R + X összefésülve
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// [receiverPoints] és/vagy [relations] közül legalább az egyik legyen
|
||||||
|
/// nem üres. Ha csak relations van, a csatorna-hozzárendelés megvan,
|
||||||
|
/// de terv-koordináta nélkül (a GNSS-mért kitűzési pont adja majd a
|
||||||
|
/// pozíciót). Ha csak receiverPoints van, nincs csatornaszám — ekkor
|
||||||
|
/// channel = null marad minden sorban (a UI jelzi, hogy ez hiányos).
|
||||||
|
static SpsImportPreview buildPreview({
|
||||||
|
List<SpsPointRecord> receiverPoints = const [],
|
||||||
|
List<SpsRelationRecord> relations = const [],
|
||||||
|
}) {
|
||||||
|
final byLineStation = <String, SpsPointRecord>{};
|
||||||
|
for (final p in receiverPoints) {
|
||||||
|
byLineStation['${p.lineId}|${p.station}'] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rows = <SpsPreviewRow>[];
|
||||||
|
var unmatchedChannels = 0;
|
||||||
|
|
||||||
|
if (relations.isNotEmpty) {
|
||||||
|
for (final rel in relations) {
|
||||||
|
for (final e in expandRelation(rel)) {
|
||||||
|
final match = byLineStation['${e.lineId}|${e.station}'];
|
||||||
|
if (match == null) unmatchedChannels++;
|
||||||
|
rows.add(SpsPreviewRow(
|
||||||
|
channel: e.channel,
|
||||||
|
lineId: e.lineId,
|
||||||
|
station: e.station,
|
||||||
|
eovY: match?.eovY,
|
||||||
|
eovX: match?.eovX,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Csak R-fájl: nincs csatornaszám, csak a vevőpontok listája.
|
||||||
|
for (final p in receiverPoints) {
|
||||||
|
rows.add(SpsPreviewRow(
|
||||||
|
channel: null,
|
||||||
|
lineId: p.lineId,
|
||||||
|
station: p.station,
|
||||||
|
eovY: p.eovY,
|
||||||
|
eovX: p.eovX,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort(
|
||||||
|
(a, b) => (a.channel ?? a.station).compareTo(b.channel ?? b.station));
|
||||||
|
|
||||||
|
return SpsImportPreview(
|
||||||
|
rows: rows,
|
||||||
|
totalReceiverPoints: receiverPoints.length,
|
||||||
|
totalRelations: relations.length,
|
||||||
|
unmatchedChannelCount: unmatchedChannels,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A jóváhagyott előnézetből SensorChannel lista építése (mentés előtt).
|
||||||
|
static List<SensorChannel> buildSensorChannels({
|
||||||
|
required SpsImportPreview preview,
|
||||||
|
required int projectId,
|
||||||
|
required String importBatch,
|
||||||
|
}) {
|
||||||
|
final conv = CoordConverterService.to;
|
||||||
|
final out = <SensorChannel>[];
|
||||||
|
for (final r in preview.rows) {
|
||||||
|
if (r.channel == null) continue; // csatornaszám nélkül nincs mit menteni
|
||||||
|
double? lat, lon;
|
||||||
|
if (r.eovY != null && r.eovX != null) {
|
||||||
|
final w = conv.eovToWgsPoint(r.eovY!, r.eovX!);
|
||||||
|
lon = w.x;
|
||||||
|
lat = w.y;
|
||||||
|
}
|
||||||
|
out.add(SensorChannel(
|
||||||
|
projectId: projectId,
|
||||||
|
channel: r.channel!,
|
||||||
|
lineId: r.lineId,
|
||||||
|
station: r.station,
|
||||||
|
planEovY: r.eovY,
|
||||||
|
planEovX: r.eovX,
|
||||||
|
planLat: lat,
|
||||||
|
planLon: lon,
|
||||||
|
source: 'sps',
|
||||||
|
importBatch: importBatch,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// Adatszerkezetek
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class SpsPointRecord {
|
||||||
|
final String lineId;
|
||||||
|
final int station;
|
||||||
|
final int pointIndex;
|
||||||
|
final double? eovY;
|
||||||
|
final double? eovX;
|
||||||
|
final double? elevation;
|
||||||
|
final String rawLine;
|
||||||
|
SpsPointRecord({
|
||||||
|
required this.lineId,
|
||||||
|
required this.station,
|
||||||
|
required this.pointIndex,
|
||||||
|
this.eovY,
|
||||||
|
this.eovX,
|
||||||
|
this.elevation,
|
||||||
|
required this.rawLine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpsRelationRecord {
|
||||||
|
final int fromChannel;
|
||||||
|
final int toChannel;
|
||||||
|
final int channelIncrement;
|
||||||
|
final String recvLineId;
|
||||||
|
final int fromReceiver;
|
||||||
|
final int toReceiver;
|
||||||
|
final String rawLine;
|
||||||
|
SpsRelationRecord({
|
||||||
|
required this.fromChannel,
|
||||||
|
required this.toChannel,
|
||||||
|
required this.channelIncrement,
|
||||||
|
required this.recvLineId,
|
||||||
|
required this.fromReceiver,
|
||||||
|
required this.toReceiver,
|
||||||
|
required this.rawLine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Egy előnézeti sor — ez jelenik meg a felhasználónak import előtt.
|
||||||
|
class SpsPreviewRow {
|
||||||
|
final int? channel;
|
||||||
|
final String lineId;
|
||||||
|
final int station;
|
||||||
|
final double? eovY;
|
||||||
|
final double? eovX;
|
||||||
|
SpsPreviewRow({
|
||||||
|
required this.channel,
|
||||||
|
required this.lineId,
|
||||||
|
required this.station,
|
||||||
|
this.eovY,
|
||||||
|
this.eovX,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get hasPosition => eovY != null && eovX != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpsImportPreview {
|
||||||
|
final List<SpsPreviewRow> rows;
|
||||||
|
final int totalReceiverPoints;
|
||||||
|
final int totalRelations;
|
||||||
|
final int unmatchedChannelCount;
|
||||||
|
SpsImportPreview({
|
||||||
|
required this.rows,
|
||||||
|
required this.totalReceiverPoints,
|
||||||
|
required this.totalRelations,
|
||||||
|
required this.unmatchedChannelCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Melyik járműben van EZ a tablet — eszköz-szintű, tartós beállítás
|
||||||
|
/// (nem projektfüggő). Több tabletnél/eszközcserénél is egyszerűen
|
||||||
|
/// újra beállítható, ha a tablet másik járműbe kerül.
|
||||||
|
class VehicleIdentityService extends GetxService {
|
||||||
|
static VehicleIdentityService get to => Get.find();
|
||||||
|
|
||||||
|
static const _key = 'vehicle_id';
|
||||||
|
|
||||||
|
/// Az alapértelmezett választható lista — igény szerint bővíthető.
|
||||||
|
static const availableVehicles = ['V1', 'V2', 'V3'];
|
||||||
|
|
||||||
|
final selectedVehicle = Rxn<String>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInit() async {
|
||||||
|
super.onInit();
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
selectedVehicle.value = prefs.getString(_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setVehicle(String vehicleId) async {
|
||||||
|
selectedVehicle.value = vehicleId;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_key, vehicleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,6 +145,14 @@ class AppDrawer extends StatelessWidget {
|
|||||||
// Get.to(() => const NtripSettingsView());
|
// Get.to(() => const NtripSettingsView());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.local_shipping_outlined),
|
||||||
|
title: const Text('Jármű navigáció'),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
Get.toNamed(Routes.VIBRONAV);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// ── 3. Beállítások ─────────────────────────────────
|
// ── 3. Beállítások ─────────────────────────────────
|
||||||
const _SectionLabel('Beállítások'),
|
const _SectionLabel('Beállítások'),
|
||||||
|
|||||||
Reference in New Issue
Block a user