200 lines
7.0 KiB
Dart
200 lines
7.0 KiB
Dart
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;
|
|||
|
|
}
|
|||
|
|
}
|