MobilApp/lib/services/layer_import_service.dart

275 lines
9.5 KiB
Dart

// 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,
};
}