Kml, kmz és geojson import és térképi megjelnítésük.
This commit is contained in:
parent
0828630a5b
commit
7c29bc7ae5
272
lib/core/kml_parser.dart
Normal file
272
lib/core/kml_parser.dart
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
1
lib/enums/layer_import_source_type.dart
Normal file
1
lib/enums/layer_import_source_type.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
enum LayerImportSourceType { geoJson, kml, kmz }
|
||||||
@ -9,6 +9,7 @@ import 'package:terepi_seged/services/app_database.dart';
|
|||||||
import 'package:terepi_seged/services/coord_converter_service.dart';
|
import 'package:terepi_seged/services/coord_converter_service.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
|
import 'package:terepi_seged/services/layer_import_service.dart';
|
||||||
import 'package:terepi_seged/services/note_audio_service.dart';
|
import 'package:terepi_seged/services/note_audio_service.dart';
|
||||||
import 'package:terepi_seged/services/note_photo_service.dart';
|
import 'package:terepi_seged/services/note_photo_service.dart';
|
||||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||||
@ -34,6 +35,7 @@ Future<void> main() async {
|
|||||||
Get.put(TrackingController(), permanent: true);
|
Get.put(TrackingController(), permanent: true);
|
||||||
Get.put(NotePhotoService(), permanent: true);
|
Get.put(NotePhotoService(), permanent: true);
|
||||||
Get.put(NoteAudioService(), permanent: true);
|
Get.put(NoteAudioService(), permanent: true);
|
||||||
|
Get.put(LayerImportService(), permanent: true);
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|||||||
96
lib/models/imported_layer.dart
Normal file
96
lib/models/imported_layer.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Importált réteg modellje.
|
||||||
|
// A GeoJsonParser közvetlenül flutter_map objektumokat gyárt,
|
||||||
|
// ezeket csomagoljuk egy kezelhető rétegbe.
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
class ImportedLayer {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final LayerImportSourceType sourceType;
|
||||||
|
|
||||||
|
// flutter_map objektumok — közvetlenül a réteg widgetekbe kerülnek
|
||||||
|
final List<Marker> markers;
|
||||||
|
final List<Polyline> polylines;
|
||||||
|
final List<Polygon> polygons;
|
||||||
|
|
||||||
|
final bool isVisible;
|
||||||
|
final DateTime importedAt;
|
||||||
|
|
||||||
|
const ImportedLayer({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.sourceType,
|
||||||
|
required this.markers,
|
||||||
|
required this.polylines,
|
||||||
|
required this.polygons,
|
||||||
|
this.isVisible = true,
|
||||||
|
required this.importedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Statisztikák ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
int get featureCount => markers.length + polylines.length + polygons.length;
|
||||||
|
|
||||||
|
bool get isEmpty => featureCount == 0;
|
||||||
|
|
||||||
|
// ── Bounding box — zoom a rétegre ─────────────────────────────────
|
||||||
|
|
||||||
|
LatLngBounds? get bounds {
|
||||||
|
final points = <LatLng>[
|
||||||
|
...markers.map((m) => m.point),
|
||||||
|
...polylines.expand((p) => p.points),
|
||||||
|
...polygons.expand((p) => p.points),
|
||||||
|
];
|
||||||
|
if (points.isEmpty) return null;
|
||||||
|
|
||||||
|
var minLat = points.first.latitude;
|
||||||
|
var maxLat = points.first.latitude;
|
||||||
|
var minLon = points.first.longitude;
|
||||||
|
var maxLon = points.first.longitude;
|
||||||
|
|
||||||
|
for (final p in points) {
|
||||||
|
if (p.latitude < minLat) minLat = p.latitude;
|
||||||
|
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||||
|
if (p.longitude < minLon) minLon = p.longitude;
|
||||||
|
if (p.longitude > maxLon) maxLon = p.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LatLngBounds(
|
||||||
|
LatLng(minLat, minLon),
|
||||||
|
LatLng(maxLat, maxLon),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedLayer copyWith({bool? isVisible}) => ImportedLayer(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
sourceType: sourceType,
|
||||||
|
markers: markers,
|
||||||
|
polylines: polylines,
|
||||||
|
polygons: polygons,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
importedAt: importedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Gyártó: GeoJsonParser kimenetéből ─────────────────────────────
|
||||||
|
|
||||||
|
static ImportedLayer fromGeoJsonParser({
|
||||||
|
required dynamic parser, // GeoJsonParser
|
||||||
|
required String name,
|
||||||
|
LayerImportSourceType source = LayerImportSourceType.geoJson,
|
||||||
|
}) {
|
||||||
|
return ImportedLayer(
|
||||||
|
id: const Uuid().v4(),
|
||||||
|
name: name,
|
||||||
|
sourceType: source,
|
||||||
|
markers: List.from(parser.markers as List),
|
||||||
|
polylines: List.from(parser.polylines as List),
|
||||||
|
polygons: List.from(parser.polygons as List),
|
||||||
|
importedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
lib/models/imported_layer_meta.dart
Normal file
72
lib/models/imported_layer_meta.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||||
|
|
||||||
|
class ImportedLayerMeta {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final LayerImportSourceType sourceType;
|
||||||
|
final String localPath; // fájl elérési út
|
||||||
|
final String? storagePath; // Supabase Storage (megosztáskor)
|
||||||
|
final bool isVisible;
|
||||||
|
final int? projectId;
|
||||||
|
final DateTime importedAt;
|
||||||
|
final DateTime? syncedAt;
|
||||||
|
|
||||||
|
const ImportedLayerMeta({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.sourceType,
|
||||||
|
required this.localPath,
|
||||||
|
this.storagePath,
|
||||||
|
this.isVisible = true,
|
||||||
|
this.projectId,
|
||||||
|
required this.importedAt,
|
||||||
|
this.syncedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isSynced => storagePath != null;
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'source_type': sourceType.name,
|
||||||
|
'local_path': localPath,
|
||||||
|
'storage_path': storagePath,
|
||||||
|
'is_visible': isVisible ? 1 : 0,
|
||||||
|
'project_id': projectId,
|
||||||
|
'imported_at': importedAt.toIso8601String(),
|
||||||
|
'synced_at': syncedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ImportedLayerMeta.fromMap(Map<String, dynamic> m) =>
|
||||||
|
ImportedLayerMeta(
|
||||||
|
id: m['id'] as String,
|
||||||
|
name: m['name'] as String,
|
||||||
|
sourceType:
|
||||||
|
LayerImportSourceType.values.byName(m['source_type'] as String),
|
||||||
|
localPath: m['local_path'] as String,
|
||||||
|
storagePath: m['storage_path'] as String?,
|
||||||
|
isVisible: (m['is_visible'] as int) == 1,
|
||||||
|
projectId: m['project_id'] as int?,
|
||||||
|
importedAt: DateTime.parse(m['imported_at'] as String),
|
||||||
|
syncedAt: m['synced_at'] != null
|
||||||
|
? DateTime.parse(m['synced_at'] as String)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
ImportedLayerMeta copyWith({
|
||||||
|
bool? isVisible,
|
||||||
|
String? storagePath,
|
||||||
|
DateTime? syncedAt,
|
||||||
|
}) =>
|
||||||
|
ImportedLayerMeta(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
sourceType: sourceType,
|
||||||
|
localPath: localPath,
|
||||||
|
storagePath: storagePath ?? this.storagePath,
|
||||||
|
isVisible: isVisible ?? this.isVisible,
|
||||||
|
projectId: projectId,
|
||||||
|
importedAt: importedAt,
|
||||||
|
syncedAt: syncedAt ?? this.syncedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -43,6 +43,7 @@ import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
|||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
import 'package:terepi_seged/services/ntrip_service.dart';
|
import 'package:terepi_seged/services/ntrip_service.dart';
|
||||||
import 'package:terepi_seged/services/project_service.dart';
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
|
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
|
||||||
import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart';
|
import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart';
|
||||||
import 'package:terepi_seged/widgets/shared_map_widgets.dart';
|
import 'package:terepi_seged/widgets/shared_map_widgets.dart';
|
||||||
|
|
||||||
@ -1623,4 +1624,82 @@ class MapSurveyController extends GetxController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void openLayerPanel() {
|
||||||
|
Get.bottomSheet(
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.45,
|
||||||
|
minChildSize: 0.3,
|
||||||
|
maxChildSize: 0.85,
|
||||||
|
snap: true,
|
||||||
|
snapSizes: const [0.3, 0.45, 0.85],
|
||||||
|
expand: false,
|
||||||
|
builder: (_, scrollCtrl) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Get.theme.colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.15),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: scrollCtrl,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Handle
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Importált rétegek',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ImportLayerPanel(
|
||||||
|
onFitBounds: fitImportedLayer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
ignoreSafeArea: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fitImportedLayer(LatLngBounds bounds) {
|
||||||
|
_isMapProgrammaticMove = true;
|
||||||
|
mapController.fitCamera(
|
||||||
|
CameraFit.bounds(
|
||||||
|
bounds: bounds,
|
||||||
|
padding: const EdgeInsets.all(48),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
|||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
import 'package:terepi_seged/utils/rive_utils.dart';
|
import 'package:terepi_seged/utils/rive_utils.dart';
|
||||||
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
||||||
|
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
|
||||||
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
||||||
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart';
|
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart';
|
||||||
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart';
|
import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart';
|
||||||
@ -74,16 +75,7 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
controller.clearNoteItemSelection();
|
controller.clearNoteItemSelection();
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
Obx(() {
|
const ImportedLayerOverlay(),
|
||||||
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
|
||||||
GnssConnectionType.none;
|
|
||||||
if (isGpsActive) {
|
|
||||||
return MarkerLayer(
|
|
||||||
markers: controller.currentLocationMarker.toList());
|
|
||||||
}
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Track polyline
|
// Track polyline
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final isTracking = TrackingController.to.isRecording.value;
|
final isTracking = TrackingController.to.isRecording.value;
|
||||||
@ -166,6 +158,15 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
controller: controller.polygonEditorController,
|
controller: controller.polygonEditorController,
|
||||||
throttleDuration: Duration.zero);
|
throttleDuration: Duration.zero);
|
||||||
}),
|
}),
|
||||||
|
Obx(() {
|
||||||
|
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
||||||
|
GnssConnectionType.none;
|
||||||
|
if (isGpsActive) {
|
||||||
|
return MarkerLayer(
|
||||||
|
markers: controller.currentLocationMarker.toList());
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:terepi_seged/enums/note_type.dart';
|
import 'package:terepi_seged/enums/note_type.dart';
|
||||||
|
import 'package:terepi_seged/models/imported_layer_meta.dart';
|
||||||
import 'package:terepi_seged/models/note_item.dart';
|
import 'package:terepi_seged/models/note_item.dart';
|
||||||
import 'package:terepi_seged/models/note_item_audio.dart';
|
import 'package:terepi_seged/models/note_item_audio.dart';
|
||||||
import 'package:terepi_seged/models/note_item_photo.dart';
|
import 'package:terepi_seged/models/note_item_photo.dart';
|
||||||
@ -190,6 +191,22 @@ class AppDatabase {
|
|||||||
'ON pending_points(sync_status)',
|
'ON pending_points(sync_status)',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS imported_layers (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
source_type TEXT NOT NULL,
|
||||||
|
local_path TEXT NOT NULL,
|
||||||
|
storage_path TEXT,
|
||||||
|
is_visible INTEGER NOT NULL DEFAULT 1,
|
||||||
|
project_id INTEGER,
|
||||||
|
imported_at TEXT NOT NULL,
|
||||||
|
synced_at TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_imp_layers_project ON imported_layers(project_id)');
|
||||||
|
|
||||||
// Alap projekt létrehozása az első indításhoz
|
// Alap projekt létrehozása az első indításhoz
|
||||||
final now = DateTime.now().toIso8601String();
|
final now = DateTime.now().toIso8601String();
|
||||||
await db.insert('projects', {
|
await db.insert('projects', {
|
||||||
@ -559,4 +576,33 @@ class AppDatabase {
|
|||||||
);
|
);
|
||||||
return rows.map(NoteItemAudio.fromMap).toList();
|
return rows.map(NoteItemAudio.fromMap).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------- Layer meta adatok
|
||||||
|
Future<void> insertImportedLayer(ImportedLayerMeta meta) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.insert('imported_layers', meta.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateImportedLayer(ImportedLayerMeta meta) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.update('imported_layers', meta.toMap(),
|
||||||
|
where: 'id = ?', whereArgs: [meta.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteImportedLayer(String id) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete('imported_layers', where: 'id = ?', whereArgs: [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ImportedLayerMeta>> listImportedLayers({int? projectId}) async {
|
||||||
|
final db = await database;
|
||||||
|
final rows = await db.query(
|
||||||
|
'imported_layers',
|
||||||
|
where: projectId != null ? 'project_id = ?' : null,
|
||||||
|
whereArgs: projectId != null ? [projectId] : null,
|
||||||
|
orderBy: 'imported_at DESC',
|
||||||
|
);
|
||||||
|
return rows.map(ImportedLayerMeta.fromMap).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
274
lib/services/layer_import_service.dart
Normal file
274
lib/services/layer_import_service.dart
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
// Geo fájl import service — perzisztens verzió
|
||||||
|
//
|
||||||
|
// TÁRHELY STRATÉGIA:
|
||||||
|
// Fájlrendszer → /files/layers/{id}.geojson|kml|kmz (nyers forrás)
|
||||||
|
// SQLite → imported_layers tábla (metadata)
|
||||||
|
// Supabase → Storage (megosztás, később)
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../models/imported_layer.dart';
|
||||||
|
import '../models/imported_layer_meta.dart';
|
||||||
|
import '../services/app_database.dart';
|
||||||
|
import '../controls/geojson_parser.dart';
|
||||||
|
import '../core/kml_parser.dart';
|
||||||
|
import '../services/project_service.dart';
|
||||||
|
|
||||||
|
class LayerImportService extends GetxService {
|
||||||
|
static LayerImportService get to => Get.find();
|
||||||
|
|
||||||
|
final layers = <ImportedLayer>[].obs;
|
||||||
|
final isLoading = false.obs;
|
||||||
|
final isSyncing = false.obs;
|
||||||
|
final lastError = Rxn<String>();
|
||||||
|
|
||||||
|
final _kmlParser = const KmlParser(
|
||||||
|
defaultPolylineColor: Color(0xCC1565C0),
|
||||||
|
defaultPolylineStroke: 3.0,
|
||||||
|
defaultPolygonFillColor: Color(0x4D1565C0),
|
||||||
|
defaultPolygonBorderColor: Color(0xCC1565C0),
|
||||||
|
defaultPolygonBorderStroke: 1.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
String? _layerDir;
|
||||||
|
static const _uuid = Uuid();
|
||||||
|
|
||||||
|
// ── Inicializálás ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onReady() async {
|
||||||
|
super.onReady();
|
||||||
|
await _initLayerDir();
|
||||||
|
await _loadPersistedLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initLayerDir() async {
|
||||||
|
final ext = await getExternalStorageDirectory();
|
||||||
|
final dir = Directory(p.join(ext!.path, 'layers'));
|
||||||
|
if (!await dir.exists()) await dir.create(recursive: true);
|
||||||
|
_layerDir = dir.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fájl import ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<ImportedLayer?> importFile() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
lastError.value = null;
|
||||||
|
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
// type: FileType.custom,
|
||||||
|
// allowedExtensions: ['geojson', 'json', 'kml', 'kmz'],
|
||||||
|
type: FileType.any,
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
if (result == null || result.files.isEmpty) return null;
|
||||||
|
|
||||||
|
final file = result.files.first;
|
||||||
|
final fileName = file.name;
|
||||||
|
final ext = fileName.split('.').last.toLowerCase();
|
||||||
|
final bytes = file.bytes!;
|
||||||
|
final id = _uuid.v4();
|
||||||
|
|
||||||
|
if (!['geojson', 'json', 'kml', 'kmz'].contains(ext)) {
|
||||||
|
throw Exception('Nem támogatott formátum: .$ext');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Fájl mentése tartós tárhelyre
|
||||||
|
final localPath = await _saveFile(id, ext, bytes);
|
||||||
|
|
||||||
|
// 2. Parse
|
||||||
|
final layer = _parse(bytes, fileName, id, ext);
|
||||||
|
if (layer.isEmpty) {
|
||||||
|
await File(localPath).delete();
|
||||||
|
throw Exception('Nem tartalmaz feldolgozható geometriát.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SQLite metadata
|
||||||
|
final meta = ImportedLayerMeta(
|
||||||
|
id: id,
|
||||||
|
name: fileName,
|
||||||
|
sourceType: _sourceType(ext),
|
||||||
|
localPath: localPath,
|
||||||
|
isVisible: true,
|
||||||
|
projectId: ProjectService.to.activeProjectId,
|
||||||
|
importedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
await AppDatabase.instance.insertImportedLayer(meta);
|
||||||
|
|
||||||
|
layers.add(layer);
|
||||||
|
return layer;
|
||||||
|
} catch (e) {
|
||||||
|
lastError.value = e.toString();
|
||||||
|
Get.snackbar('Import hiba', e.toString(),
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Perzisztens rétegek betöltése induláskor ──────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _loadPersistedLayers() async {
|
||||||
|
try {
|
||||||
|
final projectId = ProjectService.to.activeProjectId;
|
||||||
|
final metas =
|
||||||
|
await AppDatabase.instance.listImportedLayers(projectId: projectId);
|
||||||
|
|
||||||
|
final loaded = <ImportedLayer>[];
|
||||||
|
for (final meta in metas) {
|
||||||
|
final file = File(meta.localPath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
// Fájl törlődött — SQLite rekord is törlendő
|
||||||
|
await AppDatabase.instance.deleteImportedLayer(meta.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final ext = meta.localPath.split('.').last;
|
||||||
|
loaded.add(_parse(bytes, meta.name, meta.id, ext,
|
||||||
|
isVisible: meta.isVisible));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Réteg betöltés hiba (${meta.name}): $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layers.assignAll(loaded);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Perzisztens rétegek betöltési hiba: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Réteg kezelés ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> toggleLayer(String id) async {
|
||||||
|
final idx = layers.indexWhere((l) => l.id == id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
final newVisible = !layers[idx].isVisible;
|
||||||
|
layers[idx] = layers[idx].copyWith(isVisible: newVisible);
|
||||||
|
layers.refresh();
|
||||||
|
|
||||||
|
// SQLite szinkron
|
||||||
|
final metas = await AppDatabase.instance.listImportedLayers();
|
||||||
|
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||||
|
if (meta != null) {
|
||||||
|
await AppDatabase.instance
|
||||||
|
.updateImportedLayer(meta.copyWith(isVisible: newVisible));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeLayer(String id) async {
|
||||||
|
final metas = await AppDatabase.instance.listImportedLayers();
|
||||||
|
final meta = metas.firstWhereOrNull((m) => m.id == id);
|
||||||
|
if (meta != null) {
|
||||||
|
final f = File(meta.localPath);
|
||||||
|
if (await f.exists()) await f.delete();
|
||||||
|
await AppDatabase.instance.deleteImportedLayer(id);
|
||||||
|
}
|
||||||
|
layers.removeWhere((l) => l.id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeAll() async {
|
||||||
|
for (final l in List.from(layers)) {
|
||||||
|
await removeLayer(l.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ImportedLayer> get visibleLayers =>
|
||||||
|
layers.where((l) => l.isVisible).toList();
|
||||||
|
|
||||||
|
// ── Supabase megosztás (előkészítve) ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Implementáció a megosztási feladatnál:
|
||||||
|
//
|
||||||
|
// Future<void> syncLayerToSupabase(String id) async {
|
||||||
|
// final meta = ...
|
||||||
|
// final bytes = await File(meta.localPath).readAsBytes();
|
||||||
|
// final ext = meta.localPath.split('.').last;
|
||||||
|
// final path = 'layers/$projectUuid/${meta.id}.$ext';
|
||||||
|
//
|
||||||
|
// await supabase.storage.from('geo_layers').uploadBinary(path, bytes);
|
||||||
|
//
|
||||||
|
// await AppDatabase.instance.updateImportedLayer(
|
||||||
|
// meta.copyWith(storagePath: path, syncedAt: DateTime.now()));
|
||||||
|
//
|
||||||
|
// await supabase.from('terepi_seged_shared_layers').upsert({
|
||||||
|
// 'id': meta.id, 'name': meta.name, 'project_uuid': projectUuid,
|
||||||
|
// 'storage_path': path, 'source_type': meta.sourceType.name,
|
||||||
|
// 'created_at': DateTime.now().toIso8601String(),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ── Belső segédek ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<String> _saveFile(String id, String ext, Uint8List bytes) async {
|
||||||
|
final path = p.join(_layerDir!, '$id.$ext');
|
||||||
|
await File(path).writeAsBytes(bytes);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedLayer _parse(Uint8List bytes, String name, String id, String ext,
|
||||||
|
{bool isVisible = true}) {
|
||||||
|
switch (ext.toLowerCase()) {
|
||||||
|
case 'geojson':
|
||||||
|
case 'json':
|
||||||
|
return _parseGeoJson(String.fromCharCodes(bytes), name, id,
|
||||||
|
isVisible: isVisible);
|
||||||
|
case 'kml':
|
||||||
|
return _kmlParser.parseKml(String.fromCharCodes(bytes), name,
|
||||||
|
id: id, isVisible: isVisible);
|
||||||
|
case 'kmz':
|
||||||
|
return _kmlParser.parseKmz(bytes, name, id: id, isVisible: isVisible);
|
||||||
|
default:
|
||||||
|
throw Exception('Nem támogatott formátum: .$ext');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedLayer _parseGeoJson(String content, String name, String id,
|
||||||
|
{bool isVisible = true}) {
|
||||||
|
final parser = GeoJsonParser(
|
||||||
|
defaultMarkerColor: Colors.red.withOpacity(0.9),
|
||||||
|
defaultMarkerIcon: Icons.location_pin,
|
||||||
|
defaultPolylineColor: const Color(0xCC1565C0),
|
||||||
|
defaultPolylineStroke: 3.0,
|
||||||
|
defaultPolygonFillColor: const Color(0x4D1565C0),
|
||||||
|
defaultPolygonBorderColor: const Color(0xCC1565C0),
|
||||||
|
defaultPolygonBorderStroke: 1.5,
|
||||||
|
defaultPolygonIsFilled: true,
|
||||||
|
onMarkerTapCallback: (props) {
|
||||||
|
final label = props['name'] ?? props['title'] ?? '';
|
||||||
|
if (label.toString().isNotEmpty) {
|
||||||
|
Get.snackbar(label.toString(), props['description']?.toString() ?? '',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
duration: const Duration(seconds: 3));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
parser.parseGeoJsonAsString(content);
|
||||||
|
|
||||||
|
return ImportedLayer(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
sourceType: LayerImportSourceType.geoJson,
|
||||||
|
markers: parser.markers,
|
||||||
|
polylines: parser.polylines,
|
||||||
|
polygons: parser.polygons,
|
||||||
|
isVisible: isVisible,
|
||||||
|
importedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerImportSourceType _sourceType(String ext) => switch (ext.toLowerCase()) {
|
||||||
|
'kml' => LayerImportSourceType.kml,
|
||||||
|
'kmz' => LayerImportSourceType.kmz,
|
||||||
|
_ => LayerImportSourceType.geoJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -87,7 +87,35 @@ class AppDrawer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
const Divider(height: 24),
|
//const Divider(height: 24),
|
||||||
|
const Divider(height: 12),
|
||||||
|
|
||||||
|
// ----------- További funkciók
|
||||||
|
const _SectionLabel('Funkciók'),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.message_outlined),
|
||||||
|
title: const Text('Üzenetek'),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
// Get.to(() => const NtripSettingsView());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.phone_outlined),
|
||||||
|
title: const Text('Kapcsolatok'),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
// Get.to(() => const NtripSettingsView());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.data_exploration_outlined),
|
||||||
|
title: const Text('Mérés'),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
// Get.to(() => const NtripSettingsView());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// ── 3. Beállítások ─────────────────────────────────
|
// ── 3. Beállítások ─────────────────────────────────
|
||||||
const _SectionLabel('Beállítások'),
|
const _SectionLabel('Beállítások'),
|
||||||
|
|||||||
@ -156,7 +156,11 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
tooltip: 'További funkciók',
|
tooltip: 'További funkciók',
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onSelected: null,
|
onSelected: (value) {
|
||||||
|
if (value == 1) {
|
||||||
|
controller.openLayerPanel();
|
||||||
|
}
|
||||||
|
},
|
||||||
itemBuilder: (context) => const [
|
itemBuilder: (context) => const [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 1,
|
value: 1,
|
||||||
|
|||||||
200
lib/widgets/map/imported_layer_overlay.dart
Normal file
200
lib/widgets/map/imported_layer_overlay.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// Térkép overlay és panel widget az importált rétegekhez.
|
||||||
|
// Az ImportedLayer már kész flutter_map objektumokat tartalmaz —
|
||||||
|
// itt csak megjelenítjük őket.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||||
|
|
||||||
|
import '../../models/imported_layer.dart';
|
||||||
|
import '../../services/layer_import_service.dart';
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// Térkép réteg — a flutter_map layers listájába kerül
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class ImportedLayerOverlay extends StatelessWidget {
|
||||||
|
const ImportedLayerOverlay({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!Get.isRegistered<LayerImportService>()) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
final visible = LayerImportService.to.visibleLayers;
|
||||||
|
if (visible.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
// Az összes látható réteg objektumait összegyűjtjük
|
||||||
|
final polylines = visible.expand((l) => l.polylines).toList();
|
||||||
|
final polygons = visible.expand((l) => l.polygons).toList();
|
||||||
|
final markers = visible.expand((l) => l.markers).toList();
|
||||||
|
|
||||||
|
return Stack(children: [
|
||||||
|
if (polygons.isNotEmpty) PolygonLayer(polygons: polygons),
|
||||||
|
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
|
||||||
|
if (markers.isNotEmpty) MarkerLayer(markers: markers),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
// Réteg panel — import gomb + betöltött rétegek listája
|
||||||
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class ImportLayerPanel extends StatelessWidget {
|
||||||
|
/// Ha meg van adva, a rétegre zoom gomb ezt hívja
|
||||||
|
final void Function(LatLngBounds bounds)? onFitBounds;
|
||||||
|
|
||||||
|
const ImportLayerPanel({super.key, this.onFitBounds});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final svc = LayerImportService.to;
|
||||||
|
|
||||||
|
return Obx(() => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Import gomb
|
||||||
|
_ImportButton(svc: svc),
|
||||||
|
|
||||||
|
// Réteg lista
|
||||||
|
if (svc.layers.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...svc.layers.map((layer) => _LayerTile(
|
||||||
|
layer: layer,
|
||||||
|
onToggle: () => svc.toggleLayer(layer.id),
|
||||||
|
onRemove: () => svc.removeLayer(layer.id),
|
||||||
|
onZoomTo: onFitBounds != null
|
||||||
|
? () {
|
||||||
|
final b = layer.bounds;
|
||||||
|
if (b != null) onFitBounds!(b);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Import gomb ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _ImportButton extends StatelessWidget {
|
||||||
|
final LayerImportService svc;
|
||||||
|
const _ImportButton({required this.svc});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() => svc.isLoading.value
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Betöltés...', style: TextStyle(fontSize: 13)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: OutlinedButton.icon(
|
||||||
|
onPressed: () => svc.importFile(),
|
||||||
|
icon: const Icon(Icons.file_open_outlined, size: 18),
|
||||||
|
label: const Text('GeoJSON / KML / KMZ'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Egy réteg sor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _LayerTile extends StatelessWidget {
|
||||||
|
final ImportedLayer layer;
|
||||||
|
final VoidCallback onToggle;
|
||||||
|
final VoidCallback onRemove;
|
||||||
|
final VoidCallback? onZoomTo;
|
||||||
|
|
||||||
|
const _LayerTile({
|
||||||
|
required this.layer,
|
||||||
|
required this.onToggle,
|
||||||
|
required this.onRemove,
|
||||||
|
this.onZoomTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final icon = switch (layer.sourceType) {
|
||||||
|
LayerImportSourceType.geoJson => Icons.data_object,
|
||||||
|
LayerImportSourceType.kml => Icons.map_outlined,
|
||||||
|
LayerImportSourceType.kmz => Icons.folder_zip_outlined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: Row(children: [
|
||||||
|
// Láthatóság
|
||||||
|
Transform.scale(
|
||||||
|
scale: 0.85,
|
||||||
|
child: Switch.adaptive(
|
||||||
|
value: layer.isVisible,
|
||||||
|
onChanged: (_) => onToggle(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Ikon
|
||||||
|
Icon(icon, size: 15, color: Colors.grey.shade500),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
// Név + statisztika
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(layer.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, fontWeight: FontWeight.w500),
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
Text(_stats(),
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Zoom gomb
|
||||||
|
if (onZoomTo != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.fit_screen, size: 16),
|
||||||
|
onPressed: onZoomTo,
|
||||||
|
tooltip: 'Ráközelítés',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
// Törlés
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
onPressed: onRemove,
|
||||||
|
tooltip: 'Eltávolítás',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _stats() {
|
||||||
|
final parts = <String>[];
|
||||||
|
if (layer.markers.isNotEmpty) parts.add('${layer.markers.length} pont');
|
||||||
|
if (layer.polylines.isNotEmpty)
|
||||||
|
parts.add('${layer.polylines.length} vonal');
|
||||||
|
if (layer.polygons.isNotEmpty)
|
||||||
|
parts.add('${layer.polygons.length} terület');
|
||||||
|
return parts.isEmpty ? 'Üres réteg' : parts.join(' · ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -79,7 +79,12 @@ class MapModeMenuAnchor extends StatelessWidget {
|
|||||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||||
const Icon(Icons.arrow_drop_down, size: 22),
|
const Icon(Icons.arrow_drop_down, size: 22),
|
||||||
]),
|
]),
|
||||||
Text(p == null ? 'Projekt' : p.name,
|
Text(
|
||||||
|
p == null
|
||||||
|
? 'Projekt'
|
||||||
|
: (p.name.length > 18
|
||||||
|
? '${p.name.substring(0, 18)}...'
|
||||||
|
: p.name),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
|
|||||||
@ -75,6 +75,8 @@ dependencies:
|
|||||||
image_picker: ^1.2.2
|
image_picker: ^1.2.2
|
||||||
record: ^7.1.0
|
record: ^7.1.0
|
||||||
audioplayers: ^6.7.1
|
audioplayers: ^6.7.1
|
||||||
|
archive: ^4.0.9
|
||||||
|
xml: ^7.0.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user