import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/field_property.dart'; class GeoJsonImportResult { final int imported; final List skipped; const GeoJsonImportResult({required this.imported, required this.skipped}); } /// Ingatlan-parcellák geometriája (GeoJSON-import + térképi lekérdezés). /// /// SZÁNDÉKOSAN tisztán online, nincs helyi tükör-tábla. A tábla /// PROJEKT-FÜGGETLEN, közös katalógus — egy fizikai telek geometriája /// nem egyetlen projekthez tartozik. class ParcelGeometryService extends GetxService { static ParcelGeometryService get to => Get.find(); SupabaseClient get _client => Supabase.instance.client; // ═════════════════════════════════════════════════════════════════ // Normalizálás — PONTOSAN a levél-pipeline szabálya (ellenőrzött // minta alapján): minden "/"-lel elválasztott számjegy-szegmensről // levágja a vezető nullákat. // "0173/4" → "173/4", "0620" → "620", "011/9" → "11/9" // ═════════════════════════════════════════════════════════════════ static String normalizeParcelNumber(String raw) { final trimmed = raw.trim(); final parts = trimmed.split('/'); final normalized = parts.map((p) { final stripped = p.replaceFirst(RegExp(r'^0+(?=\d)'), ''); return stripped.isEmpty ? '0' : stripped; }).join('/'); return normalized; } // ═════════════════════════════════════════════════════════════════ // Import // ═════════════════════════════════════════════════════════════════ static const _settlementKeys = ['telepules', 'település', 'settlement']; static const _parcelKeys = [ 'hrsz', 'helyrajziszam', 'helyrajzi_szam', 'parcel_number', ]; static String? _extractByKeys(Map props, List keys) { final lower = {for (final e in props.entries) e.key.toLowerCase(): e.value}; for (final k in keys) { final v = lower[k]; if (v != null && v.toString().trim().isNotEmpty) return v.toString().trim(); } return null; } /// A GeoJSON tartalmának importja — Feature-önként upsert, egy hibás /// elem nem akasztja meg a többit. Future importGeoJson(String content, {required String projectId, String? sourceFileName}) async { final dynamic json = jsonDecode(content); final features = (json is Map && json['features'] is List) ? json['features'] as List : []; var imported = 0; final skipped = []; for (final f in features) { try { if (f is! Map) { skipped.add('érvénytelen elem'); continue; } final geometry = f['geometry']; if (geometry is! Map) { skipped.add('geometria nélküli elem'); continue; } final geomType = geometry['type'] as String?; List coords; if (geomType == 'MultiPolygon') { coords = geometry['coordinates'] as List; } else if (geomType == 'Polygon') { // Egységesítjük MultiPolygon-alakúra tároláshoz, hogy a // megjelenítés mindig ugyanazt a szerkezetet olvassa. coords = [geometry['coordinates']]; } else { skipped.add('nem támogatott geometria-típus: $geomType'); continue; } final props = (f['properties'] as Map?)?.cast() ?? {}; final settlement = _extractByKeys(props, _settlementKeys); final rawParcel = _extractByKeys(props, _parcelKeys); if (settlement == null || rawParcel == null) { skipped.add( 'hiányzó település/helyrajzi szám (talált kulcsok: ${props.keys.join(", ")})'); continue; } final normalized = normalizeParcelNumber(rawParcel); final bbox = _computeBbox(coords); await _client.from('terepi_seged_field_parcel_geometries').upsert({ 'project_id': projectId, 'settlement': settlement, 'normalized_parcel_number': normalized, 'parcel_number_raw': rawParcel, 'geometry': {'type': 'MultiPolygon', 'coordinates': coords}, 'bbox_min_lat': bbox.minLat, 'bbox_max_lat': bbox.maxLat, 'bbox_min_lon': bbox.minLon, 'bbox_max_lon': bbox.maxLon, 'source_file': sourceFileName, }, onConflict: 'project_id,settlement,normalized_parcel_number'); imported++; } catch (e) { skipped.add('hiba: $e'); } } return GeoJsonImportResult(imported: imported, skipped: skipped); } _Bbox _computeBbox(List multiPolygonCoords) { double minLat = double.infinity, maxLat = -double.infinity; double minLon = double.infinity, maxLon = -double.infinity; for (final polygon in multiPolygonCoords) { for (final ring in polygon as List) { for (final point in ring as List) { final lon = (point[0] as num).toDouble(); final lat = (point[1] as num).toDouble(); if (lat < minLat) minLat = lat; if (lat > maxLat) maxLat = lat; if (lon < minLon) minLon = lon; if (lon > maxLon) maxLon = lon; } } } return _Bbox( minLat: minLat, maxLat: maxLat, minLon: minLon, maxLon: maxLon); } // ═════════════════════════════════════════════════════════════════ // Lekérdezés a térképi megjelenítéshez // ═════════════════════════════════════════════════════════════════ /// Egy vagy több ingatlan geometriája (settlement + normalized_parcel_number /// párok alapján) — a property-lista bármely részhalmazára hívható. Future>> fetchGeometriesFor( List properties) async { if (properties.isEmpty) return []; // Minden átadott property ugyanahhoz az (aktív) projekthez tartozik — // a geometria is erre a projektre szűkül. final projectId = properties.first.projectId; final bySettlement = >{}; for (final p in properties) { bySettlement .putIfAbsent(p.settlement, () => {}) .add(p.normalizedParcelNumber); } final result = >[]; for (final entry in bySettlement.entries) { final rows = await _client .from('terepi_seged_field_parcel_geometries') .select() .eq('project_id', projectId) .eq('settlement', entry.key) .inFilter('normalized_parcel_number', entry.value.toList()); result.addAll(List>.from(rows)); } return result; } // ═════════════════════════════════════════════════════════════════ // flutter_map segédek // ═════════════════════════════════════════════════════════════════ List parsePolygons(List> rows, {Color color = Colors.deepOrange}) { final polygons = []; for (final row in rows) { final geom = row['geometry'] as Map; final coords = geom['coordinates'] as List; // MultiPolygon for (final polygon in coords) { final outerRing = (polygon as List).first as List; final points = outerRing .map((p) => LatLng((p[1] as num).toDouble(), (p[0] as num).toDouble())) .toList(); polygons.add(Polygon( points: points, color: color.withOpacity(0.35), borderColor: color, borderStrokeWidth: 2, )); } } return polygons; } LatLngBounds? computeBounds(List> rows) { if (rows.isEmpty) return null; double minLat = double.infinity, maxLat = -double.infinity; double minLon = double.infinity, maxLon = -double.infinity; for (final row in rows) { minLat = math.min(minLat, (row['bbox_min_lat'] as num).toDouble()); maxLat = math.max(maxLat, (row['bbox_max_lat'] as num).toDouble()); minLon = math.min(minLon, (row['bbox_min_lon'] as num).toDouble()); maxLon = math.max(maxLon, (row['bbox_max_lon'] as num).toDouble()); } return LatLngBounds(LatLng(minLat, minLon), LatLng(maxLat, maxLon)); } } class _Bbox { final double minLat, maxLat, minLon, maxLon; const _Bbox({ required this.minLat, required this.maxLat, required this.minLon, required this.maxLon, }); }