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 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 parseSourceFile(String content) => _parsePointFile(content, 'S'); static List _parsePointFile( String content, String recordType) { final result = []; 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 parseRelationFile(String content) { final result = []; 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 receiverPoints = const [], List relations = const [], }) { final byLineStation = {}; for (final p in receiverPoints) { byLineStation['${p.lineId}|${p.station}'] = p; } final rows = []; 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 buildSensorChannels({ required SpsImportPreview preview, required int projectId, required String importBatch, }) { final conv = CoordConverterService.to; final out = []; 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 rows; final int totalReceiverPoints; final int totalRelations; final int unmatchedChannelCount; SpsImportPreview({ required this.rows, required this.totalReceiverPoints, required this.totalRelations, required this.unmatchedChannelCount, }); }