275 lines
9.5 KiB
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,
|
|
};
|
|
}
|