Jármű navigáció @3h

This commit is contained in:
2026-07-13 07:58:51 +02:00
parent 4bf73c6cc7
commit 0810c528ec
12 changed files with 1374 additions and 3 deletions
+87 -2
View File
@@ -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_audio.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/track.dart';
import 'package:terepi_seged/models/vechicle_position_log.dart';
import 'package:terepi_seged/services/device_identity_service.dart';
import 'package:uuid/uuid.dart';
import '../models/project.dart';
@@ -44,7 +46,7 @@ class AppDatabase {
final path = p.join(dbDir.path, 'terepi_seged.db');
return openDatabase(path,
version: 5,
version: 6,
onConfigure: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: _onCreate,
onUpgrade: _onUpgrade);
@@ -271,6 +273,8 @@ class AppDatabase {
await _createContactsOutbox(db);
await _addAppInstanceIdColumns(db);
await _createVibratorNavTables(db);
// Alap projekt létrehozása az első indításhoz
final now = DateTime.now().toIso8601String();
await db.insert('projects', {
@@ -303,10 +307,11 @@ class AppDatabase {
await _migrateToV4(db);
}
if (oldVersion < 5) {
_createContactsOutbox(db);
await _createContactsOutbox(db);
}
await _addAppInstanceIdColumns(db);
await _createVibratorNavTables(db);
}
Future<void> _migrateToV4(Database db) async {
@@ -1392,4 +1397,84 @@ class AppDatabase {
// await db.execute('CREATE INDEX IF NOT EXISTS idx_contacts_outbox_project '
// '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();
}
}
+320
View File
@@ -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);
}
}