Files
MobilApp/lib/models/vibro_nav_route.dart
T

200 lines
7.0 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:get/get.dart';
/// Egy navigációs útvonal (kerülő, megközelítés…) a
/// `terepi_seged_vibro_nav_routes` táblából.
///
/// A `geometry` az EREDETI GeoJSON (wgs84 vagy eov), a `lines` pedig a
/// BETÖLTÉSKOR egyszer, WGS84-re átváltott, kirajzolásra kész vonalak —
/// a térkép így nem számol újra GNSS-frissítéskor.
class VibroNavRoute {
final String id;
final String projectId;
final String name;
final Map<String, dynamic> geometry;
final String crs; // 'auto' | 'wgs84' | 'eov'
final DateTime? validDate;
final String? vehicle;
final String routeKind; // 'detour' | 'access' | 'restricted'
final String? color;
final bool isActive;
final List<List<LatLng>> lines;
const VibroNavRoute({
required this.id,
required this.projectId,
required this.name,
required this.geometry,
required this.crs,
required this.validDate,
required this.vehicle,
required this.routeKind,
required this.color,
required this.isActive,
required this.lines,
});
factory VibroNavRoute.fromMap(Map<String, dynamic> m) {
final geometry = Map<String, dynamic>.from(m['geometry'] as Map);
final crs = (m['crs'] as String?) ?? 'auto';
final dateStr = m['valid_date'] as String?;
return VibroNavRoute(
id: m['id'] as String,
projectId: m['project_id'] as String,
name: (m['name'] as String?) ?? '',
geometry: geometry,
crs: crs,
validDate: dateStr == null ? null : DateTime.parse(dateStr),
vehicle: (m['vehicle'] as String?)?.trim().isEmpty ?? true
? null
: (m['vehicle'] as String).trim(),
routeKind: (m['route_kind'] as String?) ?? 'detour',
color: m['color'] as String?,
isActive: (m['is_active'] as bool?) ?? true,
lines: toWgsLines(geometry, crs),
);
}
int get pointCount => lines.fold(0, (s, l) => s + l.length);
String get dateLabel => validDate == null
? 'minden nap'
: '${validDate!.year}-${validDate!.month.toString().padLeft(2, '0')}-'
'${validDate!.day.toString().padLeft(2, '0')}';
String get vehicleLabel => vehicle ?? 'minden jármű';
// ═════════════════════════════════════════════════════════════════
// Fajták / színek
// ═════════════════════════════════════════════════════════════════
static const kinds = <String, String>{
'detour': 'Kerülő',
'access': 'Megközelítés',
'restricted': 'Tiltott / veszélyes',
};
static Color colorFor(String kind, String? hex) {
final custom = _parseHex(hex);
if (custom != null) return custom;
switch (kind) {
case 'access':
return const Color(0xFF2979FF);
case 'restricted':
return const Color(0xFFD50000);
case 'detour':
default:
return const Color(0xFFFFAB00);
}
}
static Color? _parseHex(String? hex) {
if (hex == null) return null;
var h = hex.trim().replaceFirst('#', '');
if (h.length == 6) h = 'FF$h';
if (h.length != 8) return null;
final v = int.tryParse(h, radix: 16);
return v == null ? null : Color(v);
}
// ═════════════════════════════════════════════════════════════════
// Geometria → WGS84 vonalak
// ═════════════════════════════════════════════════════════════════
/// Megállapítja a koordinátarendszert az ELSŐ koordináta alapján
/// (egy geometrián belül egységes vetületet feltételezünk).
/// WGS84: |x| ≤ 180 és |y| ≤ 90. EOV: több tízezres–százezres értékek.
static String detectCrs(Map<String, dynamic> geometry) {
final first = _firstCoordinate(geometry);
if (first == null) return 'wgs84';
return (first[0].abs() <= 180 && first[1].abs() <= 90) ? 'wgs84' : 'eov';
}
static List<double>? _firstCoordinate(Map<String, dynamic> geometry) {
final lines = _rawLines(geometry);
for (final l in lines) {
if (l.isNotEmpty) return l.first;
}
return null;
}
/// A GeoJSON geometria nyers vonalai ([x, y] párok listájának listája).
static List<List<List<double>>> _rawLines(Map<String, dynamic> geometry) {
final type = geometry['type'];
final coords = geometry['coordinates'];
if (coords is! List) return const [];
List<List<double>> parseLine(dynamic raw) {
final out = <List<double>>[];
if (raw is! List) return out;
for (final c in raw) {
if (c is List && c.length >= 2 && c[0] is num && c[1] is num) {
out.add([(c[0] as num).toDouble(), (c[1] as num).toDouble()]);
}
}
return out;
}
if (type == 'LineString') return [parseLine(coords)];
if (type == 'MultiLineString') return coords.map(parseLine).toList();
return const [];
}
/// WGS84 [LatLng] vonalak. A nem véges vagy érvénytelen pontokat
/// eldobja (a LatLng NaN összeomlasztja a térképet), a 2 pontnál
/// rövidebb vonalakat kihagyja.
static List<List<LatLng>> toWgsLines(
Map<String, dynamic> geometry, String crs) {
final raw = _rawLines(geometry);
if (raw.isEmpty) return const [];
final effective = crs == 'auto' ? detectCrs(geometry) : crs;
final out = <List<LatLng>>[];
for (final line in raw) {
final pts = <LatLng>[];
for (final c in line) {
final ll = effective == 'eov' ? _eovToLatLng(c[0], c[1]) : _wgs(c);
if (ll != null) pts.add(ll);
}
if (pts.length >= 2) out.add(pts);
}
return out;
}
static LatLng? _wgs(List<double> c) {
var lon = c[0];
var lat = c[1];
// Gyakori hiba: felcserélt sorrend [szélesség, hosszúság]. Magyarország
// területén a szélesség 45–49, a hosszúság 16–23, ezért egyértelmű.
if (lon > 44 && lon < 50 && lat > 15 && lat < 24) {
final t = lon;
lon = lat;
lat = t;
}
if (!lon.isFinite || !lat.isFinite) return null;
if (lon.abs() > 180 || lat.abs() > 90) return null;
return LatLng(lat, lon);
}
static LatLng? _eovToLatLng(double a, double b) {
if (!Get.isRegistered<CoordConverterService>()) return null;
// Szabvány: [EOV Y (kelet ~400k–950k), EOV X (észak ~45k–350k)].
// Ha felcserélve érkezett, javítjuk.
var y = a;
var x = b;
if (y < 400000 && x > 400000) {
final t = y;
y = x;
x = t;
}
try {
final w = CoordConverterService.to.eovToWgsPoint(y, x);
if (w.x.isFinite && w.y.isFinite) return LatLng(w.y, w.x);
} catch (_) {}
return null;
}
}