Files
MobilApp/lib/services/parcel_geometry_service.dart
T
2026-08-31 20:38:18 +02:00

242 lines
9.4 KiB
Dart

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<String> 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<String, dynamic> props, List<String> 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<GeoJsonImportResult> 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
: <dynamic>[];
var imported = 0;
final skipped = <String>[];
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<String, dynamic>() ?? {};
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<List<Map<String, dynamic>>> fetchGeometriesFor(
List<FieldProperty> 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 = <String, Set<String>>{};
for (final p in properties) {
bySettlement
.putIfAbsent(p.settlement, () => {})
.add(p.normalizedParcelNumber);
}
final result = <Map<String, dynamic>>[];
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<Map<String, dynamic>>.from(rows));
}
return result;
}
// ═════════════════════════════════════════════════════════════════
// flutter_map segédek
// ═════════════════════════════════════════════════════════════════
List<Polygon> parsePolygons(List<Map<String, dynamic>> rows,
{Color color = Colors.deepOrange}) {
final polygons = <Polygon>[];
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<Map<String, dynamic>> 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,
});
}