Kml, kmz és geojson import és térképi megjelnítésük.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
// 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<int>);
|
||||
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 = <Marker>[];
|
||||
final polylines = <Polyline>[];
|
||||
final polygons = <Polygon>[];
|
||||
|
||||
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<LatLng> _coordList(XmlElement geom) {
|
||||
final raw = geom.findElements('coordinates').firstOrNull?.innerText.trim();
|
||||
if (raw == null) return [];
|
||||
return raw
|
||||
.split(RegExp(r'\s+'))
|
||||
.map(_oneCoord)
|
||||
.whereType<LatLng>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<List<LatLng>> _polygonRings(XmlElement polygon) {
|
||||
final rings = <List<LatLng>>[];
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user