482 lines
17 KiB
Dart
482 lines
17 KiB
Dart
import 'dart:convert';
|
||||
|
|
import 'dart:io';
|
|||
|
|
|
|||
|
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
|||
|
|
|
|||
|
|
import '../../models/stakeout_point.dart';
|
|||
|
|
|
|||
|
|
/// Oszlop-szerepek a CSV-megfeleltetéshez.
|
|||
|
|
enum ColumnRole {
|
|||
|
|
ignore('—'),
|
|||
|
|
station('Állomás / pontszám'),
|
|||
|
|
line('Vonal'),
|
|||
|
|
name('Megnevezés'),
|
|||
|
|
eovY('EOV Y (kelet)'),
|
|||
|
|
eovX('EOV X (észak)'),
|
|||
|
|
lat('Szélesség (lat)'),
|
|||
|
|
lon('Hosszúság (lon)'),
|
|||
|
|
elevation('Magasság');
|
|||
|
|
|
|||
|
|
final String label;
|
|||
|
|
const ColumnRole(this.label);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Az elemzés eredménye — ebből épül az előnézeti képernyő.
|
|||
|
|
class CsvPreview {
|
|||
|
|
final String fileName;
|
|||
|
|
final String delimiter;
|
|||
|
|
final bool hasHeader;
|
|||
|
|
final bool decimalComma;
|
|||
|
|
final List<String> headers; // fejléc vagy "1. oszlop" ...
|
|||
|
|
final List<List<String>> sampleRows; // első max. 6 adatsor
|
|||
|
|
final List<List<String>> allRows; // minden adatsor (nyers)
|
|||
|
|
final List<ColumnRole> guessedRoles;
|
|||
|
|
final String detectedCrs; // 'EOV' | 'WGS84' | '?'
|
|||
|
|
final int badRowCount;
|
|||
|
|
|
|||
|
|
CsvPreview({
|
|||
|
|
required this.fileName,
|
|||
|
|
required this.delimiter,
|
|||
|
|
required this.hasHeader,
|
|||
|
|
required this.decimalComma,
|
|||
|
|
required this.headers,
|
|||
|
|
required this.sampleRows,
|
|||
|
|
required this.allRows,
|
|||
|
|
required this.guessedRoles,
|
|||
|
|
required this.detectedCrs,
|
|||
|
|
required this.badRowCount,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
int get rowCount => allRows.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// CSV / GeoJSON kitűzési pont import — magyar terepi sajátosságokkal:
|
|||
|
|
/// * elválasztó-felismerés (';' a magyar Excel alapértelmezése, ',', tab)
|
|||
|
|
/// * tizedesVESSZŐ kezelése
|
|||
|
|
/// * kódolás: UTF-8, hibánál Latin-1 visszaesés (a koordináták így is
|
|||
|
|
/// hibátlanok, legfeljebb az ő/ű torzulhat a nevekben)
|
|||
|
|
/// * oszlopszerep-felismerés ÉRTÉKTARTOMÁNY alapján — Magyarországon az
|
|||
|
|
/// EOV Y (421–960 ezer), EOV X (48–384 ezer), lat (45,5–48,8) és
|
|||
|
|
/// lon (16–23) tartományok páronként diszjunktak, így a felismerés
|
|||
|
|
/// nagyon megbízható; a fejlécnevek csak megerősítésként számítanak.
|
|||
|
|
class StakeoutImportService {
|
|||
|
|
StakeoutImportService._();
|
|||
|
|
|
|||
|
|
// ── Magyar értéktartományok ──────────────────────────────────────
|
|||
|
|
static bool _isEovY(double v) => v >= 421000 && v <= 960000;
|
|||
|
|
static bool _isEovX(double v) => v >= 48000 && v <= 384000;
|
|||
|
|
static bool _isLat(double v) => v >= 45.5 && v <= 48.8;
|
|||
|
|
static bool _isLon(double v) => v >= 16.0 && v <= 23.0;
|
|||
|
|
static bool _isElev(double v) => v >= -50 && v <= 3000;
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
// CSV elemzés
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
static Future<CsvPreview> analyzeCsv(File file) async {
|
|||
|
|
final bytes = await file.readAsBytes();
|
|||
|
|
String text;
|
|||
|
|
try {
|
|||
|
|
text = utf8.decode(bytes);
|
|||
|
|
} catch (_) {
|
|||
|
|
text = latin1.decode(bytes); // Windows-1250 közelítése
|
|||
|
|
}
|
|||
|
|
// BOM eltávolítás
|
|||
|
|
if (text.isNotEmpty && text.codeUnitAt(0) == 0xFEFF) {
|
|||
|
|
text = text.substring(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
final lines = text
|
|||
|
|
.split(RegExp(r'\r\n|\r|\n'))
|
|||
|
|
.where((l) => l.trim().isNotEmpty)
|
|||
|
|
.toList();
|
|||
|
|
if (lines.isEmpty) {
|
|||
|
|
throw const FormatException('A fájl üres.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. Elválasztó: amelyikből a legtöbb van KONZISZTENSEN a sorokban.
|
|||
|
|
final delimiter = _detectDelimiter(lines.take(20).toList());
|
|||
|
|
|
|||
|
|
// 2. Sorok felbontása.
|
|||
|
|
var rows = lines.map((l) => _splitLine(l, delimiter)).toList();
|
|||
|
|
final colCount = rows.map((r) => r.length).reduce((a, b) => a > b ? a : b);
|
|||
|
|
// Rövid sorok kipótlása üres cellákkal, hogy a táblázat téglalap legyen.
|
|||
|
|
rows = rows
|
|||
|
|
.map((r) => [...r, ...List.filled(colCount - r.length, '')])
|
|||
|
|
.toList();
|
|||
|
|
|
|||
|
|
// 3. Tizedesvessző? Ha ';' az elválasztó és sok "123,45" mintájú cella
|
|||
|
|
// van, akkor a vessző tizedesjel.
|
|||
|
|
final decimalComma = _detectDecimalComma(rows.take(30).toList());
|
|||
|
|
|
|||
|
|
// 4. Fejléc: az első sor akkor fejléc, ha a cellái NEM számok, de a
|
|||
|
|
// második soréi többségében igen.
|
|||
|
|
final hasHeader = rows.length > 1 &&
|
|||
|
|
_numericRatio(rows.first, decimalComma) < 0.5 &&
|
|||
|
|
_numericRatio(rows[1], decimalComma) >= 0.5;
|
|||
|
|
|
|||
|
|
final headers = hasHeader
|
|||
|
|
? rows.first.map((h) => h.trim()).toList()
|
|||
|
|
: List.generate(colCount, (i) => '${i + 1}. oszlop');
|
|||
|
|
final dataRows = hasHeader ? rows.sublist(1) : rows;
|
|||
|
|
|
|||
|
|
// 5. Oszlopszerepek felismerése.
|
|||
|
|
final roles = _guessRoles(headers, dataRows, decimalComma);
|
|||
|
|
|
|||
|
|
final crs =
|
|||
|
|
roles.contains(ColumnRole.eovY) && roles.contains(ColumnRole.eovX)
|
|||
|
|
? 'EOV'
|
|||
|
|
: roles.contains(ColumnRole.lat) && roles.contains(ColumnRole.lon)
|
|||
|
|
? 'WGS84'
|
|||
|
|
: '?';
|
|||
|
|
|
|||
|
|
return CsvPreview(
|
|||
|
|
fileName: file.uri.pathSegments.last,
|
|||
|
|
delimiter: delimiter,
|
|||
|
|
hasHeader: hasHeader,
|
|||
|
|
decimalComma: decimalComma,
|
|||
|
|
headers: headers,
|
|||
|
|
sampleRows: dataRows.take(6).toList(),
|
|||
|
|
allRows: dataRows,
|
|||
|
|
guessedRoles: roles,
|
|||
|
|
detectedCrs: crs,
|
|||
|
|
badRowCount: 0, // a tényleges építéskor derül ki
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static String _detectDelimiter(List<String> lines) {
|
|||
|
|
var best = ';';
|
|||
|
|
var bestScore = -1;
|
|||
|
|
for (final d in [';', ',', '\t']) {
|
|||
|
|
final counts = lines.map((l) => d.allMatches(l).length).toList();
|
|||
|
|
final min = counts.reduce((a, b) => a < b ? a : b);
|
|||
|
|
// Pontszám: minden sorban legyen legalább 1, és konzisztens legyen.
|
|||
|
|
final score = min > 0 ? min * 10 - (counts.toSet().length - 1) : -1;
|
|||
|
|
if (score > bestScore) {
|
|||
|
|
bestScore = score;
|
|||
|
|
best = d;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return best;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Egyszerű felbontás idézőjel-kezeléssel ("a;b" egyben marad).
|
|||
|
|
static List<String> _splitLine(String line, String delimiter) {
|
|||
|
|
final cells = <String>[];
|
|||
|
|
final sb = StringBuffer();
|
|||
|
|
var inQuotes = false;
|
|||
|
|
for (var i = 0; i < line.length; i++) {
|
|||
|
|
final c = line[i];
|
|||
|
|
if (c == '"') {
|
|||
|
|
inQuotes = !inQuotes;
|
|||
|
|
} else if (c == delimiter && !inQuotes) {
|
|||
|
|
cells.add(sb.toString().trim());
|
|||
|
|
sb.clear();
|
|||
|
|
} else {
|
|||
|
|
sb.write(c);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
cells.add(sb.toString().trim());
|
|||
|
|
return cells;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static final _decimalCommaRe = RegExp(r'^-?\d+,\d+$');
|
|||
|
|
static final _decimalDotRe = RegExp(r'^-?\d+\.\d+$');
|
|||
|
|
|
|||
|
|
static bool _detectDecimalComma(List<List<String>> rows) {
|
|||
|
|
var comma = 0, dot = 0;
|
|||
|
|
for (final row in rows) {
|
|||
|
|
for (final cell in row) {
|
|||
|
|
if (_decimalCommaRe.hasMatch(cell)) comma++;
|
|||
|
|
if (_decimalDotRe.hasMatch(cell)) dot++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return comma > dot;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Cella → szám, a tizedesjel-beállítás figyelembevételével.
|
|||
|
|
static double? parseNum(String cell, bool decimalComma) {
|
|||
|
|
var s = cell.trim();
|
|||
|
|
if (s.isEmpty) return null;
|
|||
|
|
if (decimalComma) {
|
|||
|
|
s = s.replaceAll(' ', '').replaceAll(',', '.');
|
|||
|
|
}
|
|||
|
|
return double.tryParse(s);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static double _numericRatio(List<String> row, bool decimalComma) {
|
|||
|
|
if (row.isEmpty) return 0;
|
|||
|
|
final n = row.where((c) => parseNum(c, decimalComma) != null).length;
|
|||
|
|
return n / row.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Szerepfelismerés ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
static List<ColumnRole> _guessRoles(
|
|||
|
|
List<String> headers, List<List<String>> rows, bool decimalComma) {
|
|||
|
|
final n = headers.length;
|
|||
|
|
final sample = rows.take(200).toList();
|
|||
|
|
final roles = List<ColumnRole>.filled(n, ColumnRole.ignore);
|
|||
|
|
|
|||
|
|
// Oszloponkénti statisztika.
|
|||
|
|
final stats = List.generate(n, (c) {
|
|||
|
|
final values = <double>[];
|
|||
|
|
final raw = <String>[];
|
|||
|
|
for (final row in sample) {
|
|||
|
|
if (c >= row.length || row[c].isEmpty) continue;
|
|||
|
|
raw.add(row[c]);
|
|||
|
|
final v = parseNum(row[c], decimalComma);
|
|||
|
|
if (v != null) values.add(v);
|
|||
|
|
}
|
|||
|
|
final distinct = raw.toSet().length;
|
|||
|
|
final allInt =
|
|||
|
|
values.isNotEmpty && values.every((v) => v == v.roundToDouble());
|
|||
|
|
double share(bool Function(double) test) =>
|
|||
|
|
values.isEmpty ? 0 : values.where(test).length / values.length;
|
|||
|
|
return (
|
|||
|
|
numericRatio: raw.isEmpty ? 0.0 : values.length / raw.length,
|
|||
|
|
values: values,
|
|||
|
|
distinct: distinct,
|
|||
|
|
count: raw.length,
|
|||
|
|
allInt: allInt,
|
|||
|
|
eovY: share(_isEovY),
|
|||
|
|
eovX: share(_isEovX),
|
|||
|
|
lat: share(_isLat),
|
|||
|
|
lon: share(_isLon),
|
|||
|
|
elev: share(_isElev),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
String h(int c) => headers[c].toLowerCase();
|
|||
|
|
bool hHas(int c, List<String> keys) => keys.any((k) => h(c).contains(k));
|
|||
|
|
|
|||
|
|
// 1. Koordináták — tartomány alapján (95% feletti találat kell),
|
|||
|
|
// fejlécnév csak döntetlennél számít.
|
|||
|
|
int pick(double Function(int) score, List<String> headerKeys) {
|
|||
|
|
var best = -1;
|
|||
|
|
var bestScore = 0.94;
|
|||
|
|
for (var c = 0; c < n; c++) {
|
|||
|
|
if (roles[c] != ColumnRole.ignore) continue;
|
|||
|
|
var s = score(c);
|
|||
|
|
if (s > 0.94 && hHas(c, headerKeys)) s += 0.05;
|
|||
|
|
if (s > bestScore) {
|
|||
|
|
bestScore = s;
|
|||
|
|
best = c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return best;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
final cEovY = pick((c) => stats[c].eovY, ['y', 'kelet', 'east']);
|
|||
|
|
if (cEovY >= 0) roles[cEovY] = ColumnRole.eovY;
|
|||
|
|
final cEovX = pick((c) => stats[c].eovX, ['x', 'észak', 'eszak', 'north']);
|
|||
|
|
if (cEovX >= 0) roles[cEovX] = ColumnRole.eovX;
|
|||
|
|
final cLat = pick((c) => stats[c].lat, ['lat', 'fi', 'szél', 'szel']);
|
|||
|
|
if (cLat >= 0) roles[cLat] = ColumnRole.lat;
|
|||
|
|
final cLon = pick((c) => stats[c].lon, ['lon', 'lambda', 'hossz']);
|
|||
|
|
if (cLon >= 0) roles[cLon] = ColumnRole.lon;
|
|||
|
|
|
|||
|
|
// 2. Magasság: numerikus, elfogadható tartomány, NEM egész-azonosító
|
|||
|
|
// jellegű; fejléc segít.
|
|||
|
|
for (var c = 0; c < n; c++) {
|
|||
|
|
if (roles[c] != ColumnRole.ignore) continue;
|
|||
|
|
final s = stats[c];
|
|||
|
|
if (s.numericRatio > 0.9 &&
|
|||
|
|
s.elev > 0.94 &&
|
|||
|
|
(hHas(c, ['z', 'mag', 'elev', 'h']) || !s.allInt)) {
|
|||
|
|
roles[c] = ColumnRole.elevation;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Állomás/pontszám: egész, (közel) egyedi értékek.
|
|||
|
|
var bestStation = -1;
|
|||
|
|
var bestUnique = 0.9;
|
|||
|
|
for (var c = 0; c < n; c++) {
|
|||
|
|
if (roles[c] != ColumnRole.ignore) continue;
|
|||
|
|
final s = stats[c];
|
|||
|
|
if (s.numericRatio > 0.95 && s.allInt && s.count > 0) {
|
|||
|
|
var unique = s.distinct / s.count;
|
|||
|
|
if (hHas(c, ['psz', 'pont', 'station', 'áll', 'all', 'id'])) {
|
|||
|
|
unique += 0.05;
|
|||
|
|
}
|
|||
|
|
if (unique > bestUnique) {
|
|||
|
|
bestUnique = unique;
|
|||
|
|
bestStation = c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (bestStation >= 0) roles[bestStation] = ColumnRole.station;
|
|||
|
|
|
|||
|
|
// 4. Vonal: kevés egyedi értékű oszlop (a pontszám sokszorosa tartozik
|
|||
|
|
// egy vonalhoz); fejléc segít.
|
|||
|
|
for (var c = 0; c < n; c++) {
|
|||
|
|
if (roles[c] != ColumnRole.ignore) continue;
|
|||
|
|
final s = stats[c];
|
|||
|
|
final lowCardinality =
|
|||
|
|
s.count >= 10 && s.distinct <= (s.count / 4).ceil();
|
|||
|
|
if (hHas(c, ['line', 'vonal', 'ln']) || lowCardinality) {
|
|||
|
|
roles[c] = ColumnRole.line;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. Név: az első még szabad, többségében szöveges oszlop.
|
|||
|
|
for (var c = 0; c < n; c++) {
|
|||
|
|
if (roles[c] != ColumnRole.ignore) continue;
|
|||
|
|
if (stats[c].numericRatio < 0.5 && stats[c].count > 0) {
|
|||
|
|
roles[c] = ColumnRole.name;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return roles;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
// Pontok építése a megfeleltetés alapján
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// A (kézzel jóváhagyott) szerep-kiosztás alapján felépíti a pontokat.
|
|||
|
|
/// Visszaadja a pontokat és a kihagyott (hibás) sorok számát.
|
|||
|
|
static ({List<StakeoutPoint> points, int skipped}) buildPoints({
|
|||
|
|
required CsvPreview preview,
|
|||
|
|
required List<ColumnRole> roles,
|
|||
|
|
required int projectId,
|
|||
|
|
}) {
|
|||
|
|
int col(ColumnRole r) => roles.indexOf(r);
|
|||
|
|
final cStation = col(ColumnRole.station);
|
|||
|
|
final cLine = col(ColumnRole.line);
|
|||
|
|
final cName = col(ColumnRole.name);
|
|||
|
|
final cEovY = col(ColumnRole.eovY);
|
|||
|
|
final cEovX = col(ColumnRole.eovX);
|
|||
|
|
final cLat = col(ColumnRole.lat);
|
|||
|
|
final cLon = col(ColumnRole.lon);
|
|||
|
|
final cElev = col(ColumnRole.elevation);
|
|||
|
|
|
|||
|
|
final isEov = cEovY >= 0 && cEovX >= 0;
|
|||
|
|
if (!isEov && !(cLat >= 0 && cLon >= 0)) {
|
|||
|
|
throw const FormatException(
|
|||
|
|
'Hiányzó koordináta-oszlopok: EOV Y+X vagy lat+lon kell.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
final conv = CoordConverterService.to;
|
|||
|
|
final points = <StakeoutPoint>[];
|
|||
|
|
var skipped = 0;
|
|||
|
|
var autoStation = 1;
|
|||
|
|
|
|||
|
|
for (final row in preview.allRows) {
|
|||
|
|
double? get(int c) => c >= 0 && c < row.length
|
|||
|
|
? parseNum(row[c], preview.decimalComma)
|
|||
|
|
: null;
|
|||
|
|
|
|||
|
|
double eovY, eovX, lat, lon;
|
|||
|
|
if (isEov) {
|
|||
|
|
final y = get(cEovY), x = get(cEovX);
|
|||
|
|
if (y == null || x == null || !_isEovY(y) || !_isEovX(x)) {
|
|||
|
|
skipped++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
eovY = y;
|
|||
|
|
eovX = x;
|
|||
|
|
final p = conv.eovToWgsPoint(y, x);
|
|||
|
|
lon = p.x;
|
|||
|
|
lat = p.y;
|
|||
|
|
} else {
|
|||
|
|
final la = get(cLat), lo = get(cLon);
|
|||
|
|
if (la == null || lo == null || !_isLat(la) || !_isLon(lo)) {
|
|||
|
|
skipped++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
lat = la;
|
|||
|
|
lon = lo;
|
|||
|
|
final p = conv.wgsToEovPoint(lo, la);
|
|||
|
|
eovY = p.x;
|
|||
|
|
eovX = p.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
final stationVal = get(cStation);
|
|||
|
|
final station = stationVal?.round() ?? autoStation++;
|
|||
|
|
final line = cLine >= 0 && cLine < row.length ? row[cLine].trim() : '';
|
|||
|
|
final name = cName >= 0 && cName < row.length && row[cName].isNotEmpty
|
|||
|
|
? row[cName].trim()
|
|||
|
|
: station.toString();
|
|||
|
|
|
|||
|
|
points.add(StakeoutPoint(
|
|||
|
|
projectId: projectId,
|
|||
|
|
lineId: line,
|
|||
|
|
station: station,
|
|||
|
|
name: name,
|
|||
|
|
source: 'csv',
|
|||
|
|
planEovY: eovY,
|
|||
|
|
planEovX: eovX,
|
|||
|
|
planEovZ: get(cElev),
|
|||
|
|
planLat: lat,
|
|||
|
|
planLon: lon,
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
return (points: points, skipped: skipped);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
// GeoJSON
|
|||
|
|
// ═════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// GeoJSON → CsvPreview-kompatibilis táblázat: a property-k az oszlopok,
|
|||
|
|
/// plusz a kinyert lon/lat. Így ugyanaz az előnézeti/megfeleltetési
|
|||
|
|
/// képernyő szolgálja ki, mint a CSV-t.
|
|||
|
|
static Future<CsvPreview> analyzeGeojson(File file) async {
|
|||
|
|
final json = jsonDecode(await file.readAsString());
|
|||
|
|
final features = (json['features'] as List?) ?? [];
|
|||
|
|
|
|||
|
|
final propKeys = <String>{};
|
|||
|
|
final rows = <List<String>>[];
|
|||
|
|
for (final f in features) {
|
|||
|
|
final geom = f['geometry'];
|
|||
|
|
if (geom == null || geom['type'] != 'Point') continue;
|
|||
|
|
propKeys.addAll(
|
|||
|
|
((f['properties'] as Map?) ?? {}).keys.map((k) => k.toString()));
|
|||
|
|
}
|
|||
|
|
final keys = propKeys.toList();
|
|||
|
|
|
|||
|
|
for (final f in features) {
|
|||
|
|
final geom = f['geometry'];
|
|||
|
|
if (geom == null || geom['type'] != 'Point') continue;
|
|||
|
|
final coords = geom['coordinates'] as List;
|
|||
|
|
final props = (f['properties'] as Map?) ?? {};
|
|||
|
|
rows.add([
|
|||
|
|
for (final k in keys) '${props[k] ?? ''}',
|
|||
|
|
'${coords[0]}', // lon
|
|||
|
|
'${coords[1]}', // lat
|
|||
|
|
if (coords.length > 2) '${coords[2]}' else '',
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
if (rows.isEmpty) {
|
|||
|
|
throw const FormatException('A GeoJSON nem tartalmaz Point elemet.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
final headers = [...keys, 'lon', 'lat', 'z'];
|
|||
|
|
final roles = _guessRoles(headers, rows, false);
|
|||
|
|
// A GeoJSON szabvány szerint mindig WGS84 — a koordináta-oszlopokat
|
|||
|
|
// ismerjük, kényszerítjük.
|
|||
|
|
roles[headers.length - 3] = ColumnRole.lon;
|
|||
|
|
roles[headers.length - 2] = ColumnRole.lat;
|
|||
|
|
if (rows.any((r) => r.last.isNotEmpty)) {
|
|||
|
|
roles[headers.length - 1] = ColumnRole.elevation;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return CsvPreview(
|
|||
|
|
fileName: file.uri.pathSegments.last,
|
|||
|
|
delimiter: ',',
|
|||
|
|
hasHeader: true,
|
|||
|
|
decimalComma: false,
|
|||
|
|
headers: headers,
|
|||
|
|
sampleRows: rows.take(6).toList(),
|
|||
|
|
allRows: rows,
|
|||
|
|
guessedRoles: roles,
|
|||
|
|
detectedCrs: 'WGS84',
|
|||
|
|
badRowCount: 0,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|