// 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 = [].obs; final isLoading = false.obs; final isSyncing = false.obs; final lastError = Rxn(); 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 onReady() async { super.onReady(); await _initLayerDir(); await _loadPersistedLayers(); } Future _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 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 _loadPersistedLayers() async { try { final projectId = ProjectService.to.activeProjectId; final metas = await AppDatabase.instance.listImportedLayers(projectId: projectId); final loaded = []; 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 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 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 removeAll() async { for (final l in List.from(layers)) { await removeLayer(l.id); } } List get visibleLayers => layers.where((l) => l.isVisible).toList(); // ── Supabase megosztás (előkészítve) ────────────────────────────────────── // // Implementáció a megosztási feladatnál: // // Future 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 _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, }; }