// KML és KMZ → flutter_map objektumok // // FONTOS: KML koordináta sorrend: lon,lat,alt // Flutter LatLng(lat, lon) — tehát fordítva kell olvasni! // // KMZ = ZIP archív, benne doc.kml import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:terepi_seged/enums/layer_import_source_type.dart'; import 'package:uuid/uuid.dart'; import 'package:xml/xml.dart'; import '../models/imported_layer.dart'; class KmlParser { // Alapértelmezett stílusok — felülírhatók konstruktorban final Color defaultPolylineColor; final double defaultPolylineStroke; final Color defaultPolygonFillColor; final Color defaultPolygonBorderColor; final double defaultPolygonBorderStroke; final Color defaultMarkerColor; const KmlParser({ this.defaultPolylineColor = const Color(0xCC1565C0), // kék this.defaultPolylineStroke = 3.0, this.defaultPolygonFillColor = const Color(0x4D1565C0), this.defaultPolygonBorderColor = const Color(0xCC1565C0), this.defaultPolygonBorderStroke = 1.5, this.defaultMarkerColor = Colors.red, }); // ── KMZ (ZIP → KML) ────────────────────────────────────────────── ImportedLayer parseKmz(Uint8List bytes, String fileName, {String? id, bool isVisible = true}) { final archive = ZipDecoder().decodeBytes(bytes); ArchiveFile? kmlFile = archive.findFile('doc.kml'); kmlFile ??= archive.files.firstWhere( (f) => f.name.toLowerCase().endsWith('.kml'), orElse: () => throw Exception('Nem található .kml fájl a KMZ-ben'), ); final kmlString = String.fromCharCodes(kmlFile.content as List); return _parse(kmlString, fileName, LayerImportSourceType.kmz, id: id, isVisible: isVisible); } // ── KML (szöveg) ────────────────────────────────────────────────── ImportedLayer parseKml(String kmlString, String fileName, {String? id, bool isVisible = true}) => _parse(kmlString, fileName, LayerImportSourceType.kml, id: id, isVisible: isVisible); // ── Belső parse ─────────────────────────────────────────────────── ImportedLayer _parse( String kmlString, String fileName, LayerImportSourceType source, {String? id, bool isVisible = true}) { final doc = XmlDocument.parse(kmlString); final markers = []; final polylines = []; final polygons = []; for (final pm in doc.findAllElements('Placemark')) { final style = _style(pm); final label = pm.findElements('name').firstOrNull?.innerText.trim() ?? ''; // Point final point = pm.findElements('Point').firstOrNull; if (point != null) { final pt = _singleCoord(point); if (pt != null) { markers.add(Marker( point: pt, width: label.isNotEmpty ? 120 : 24, height: label.isNotEmpty ? 40 : 24, alignment: Alignment.bottomCenter, child: _markerWidget(label, style.lineColor), )); } } // LineString final line = pm.findElements('LineString').firstOrNull; if (line != null) { final pts = _coordList(line); if (pts.isNotEmpty) { polylines.add(Polyline( points: pts, color: style.lineColor, strokeWidth: style.lineWidth, borderColor: style.lineColor.withOpacity(0.3), borderStrokeWidth: 1.0, )); } } // Polygon final poly = pm.findElements('Polygon').firstOrNull; if (poly != null) { final rings = _polygonRings(poly); if (rings.isNotEmpty) { polygons.add(Polygon( points: rings[0], holePointsList: rings.length > 1 ? rings.sublist(1) : null, color: style.fillColor, borderColor: style.lineColor, borderStrokeWidth: style.lineWidth, label: label.isNotEmpty ? label : null, )); } } // MultiGeometry — rekurzív feldolgozás az első egyszerű elemre final multi = pm.findElements('MultiGeometry').firstOrNull; if (multi != null) { for (final ls in multi.findElements('LineString')) { final pts = _coordList(ls); if (pts.isNotEmpty) { polylines.add(Polyline( points: pts, color: style.lineColor, strokeWidth: style.lineWidth, )); } } } } return ImportedLayer( id: id ?? const Uuid().v4(), isVisible: isVisible, name: fileName, sourceType: source, markers: markers, polylines: polylines, polygons: polygons, importedAt: DateTime.now(), ); } // ── Koordináta parserek ─────────────────────────────────────────── LatLng? _singleCoord(XmlElement geom) { final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim(); if (raw == null) return null; return _oneCoord(raw.split(RegExp(r'\s+')).first); } List _coordList(XmlElement geom) { final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim(); if (raw == null) return []; return raw .split(RegExp(r'\s+')) .map(_oneCoord) .whereType() .toList(); } List> _polygonRings(XmlElement polygon) { final rings = >[]; final outer = polygon .findElements('outerBoundaryIs') .firstOrNull ?.findElements('LinearRing') .firstOrNull; if (outer != null) rings.add(_coordList(outer)); for (final inner in polygon.findElements('innerBoundaryIs')) { final ring = inner.findElements('LinearRing').firstOrNull; if (ring != null) rings.add(_coordList(ring)); } return rings.where((r) => r.isNotEmpty).toList(); } /// KML: lon,lat,alt → LatLng(lat, lon) LatLng? _oneCoord(String s) { final p = s.split(','); if (p.length < 2) return null; try { return LatLng( double.parse(p[1].trim()), // lat double.parse(p[0].trim()), // lon ); } catch (_) { return null; } } // ── Stílus kiolvasás ────────────────────────────────────────────── _Style _style(XmlElement pm) { final style = pm.findElements('Style').firstOrNull; if (style == null) return _Style.defaults(this); Color lineColor = defaultPolylineColor; double lineWidth = defaultPolylineStroke; Color fillColor = defaultPolygonFillColor; final ls = style.findElements('LineStyle').firstOrNull; if (ls != null) { final c = ls.findElements('color').firstOrNull?.innerText; if (c != null) lineColor = _kmlColor(c); final w = ls.findElements('width').firstOrNull?.innerText; if (w != null) lineWidth = double.tryParse(w) ?? lineWidth; } final ps = style.findElements('PolyStyle').firstOrNull; if (ps != null) { final c = ps.findElements('color').firstOrNull?.innerText; if (c != null) fillColor = _kmlColor(c); } return _Style( lineColor: lineColor, lineWidth: lineWidth, fillColor: fillColor); } /// KML szín formátum: aabbggrr → Flutter Color(argb) Color _kmlColor(String s) { if (s.length != 8) return defaultPolylineColor; try { final a = int.parse(s.substring(0, 2), radix: 16); final b = int.parse(s.substring(2, 4), radix: 16); final g = int.parse(s.substring(4, 6), radix: 16); final r = int.parse(s.substring(6, 8), radix: 16); return Color.fromARGB(a, r, g, b); } catch (_) { return defaultPolylineColor; } } // ── Marker widget ───────────────────────────────────────────────── Widget _markerWidget(String label, Color color) { if (label.isEmpty) { return Icon(Icons.location_pin, color: color, size: 24); } return Column(mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)), child: Text(label, style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis), ), Icon(Icons.location_pin, color: color, size: 16), ]); } } class _Style { final Color lineColor; final double lineWidth; final Color fillColor; const _Style( {required this.lineColor, required this.lineWidth, required this.fillColor}); factory _Style.defaults(KmlParser p) => _Style( lineColor: p.defaultPolylineColor, lineWidth: p.defaultPolylineStroke, fillColor: p.defaultPolygonFillColor, ); }