import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; // import 'package:flutter_map_geojson/flutter_map_geojson.dart'; import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; // import 'package:location/location.dart'; import 'package:latlong2/latlong.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:terepi_seged/controls/geoid_grid.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart'; import 'package:terepi_seged/services/coord_converter_service.dart'; import 'package:terepi_seged/services/gnss/gnss_connection.dart'; import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/services/ntrip_service.dart'; enum MapSurveyMode { measure, // Bemérés — ahol vagyok, azt rögzítem stakeout, // Kitűzés — adott ponthoz navigálok, majd rögzítem } class MapSurveyController extends GetxController { static MapSurveyController get to => Get.find(); // ── Függőségek (service-ek) ─────────────────────────────────────── GnssService get _gnss => GnssService.to; NtripService get _ntrip => NtripService.to; final mode = MapSurveyMode.measure.obs; final targetName = ''.obs; final targetEovX = 0.0.obs; final targetEovY = 0.0.obs; double get deltaY => eov.value.Y - targetEovY.value; double get deltaX => eov.value.X - targetEovX.value; double get distanceToTarget => sqrt(deltaX * deltaX + deltaY * deltaY); bool get isOnTarget => distanceToTarget < 0.05; // ── GPS állapot — rövidítések a GnssService-hez ────────────────── // Ezeket a view-ban közvetlenül a service-ből is lehetne olvasni, // de a controller-en keresztül is hozzáférhetők a meglévő view kódhoz. RxDouble get gpsLatitude => _gnss.latitude; RxDouble get gpsLongitude => _gnss.longitude; RxDouble get gpsAltitude => _gnss.altitude; RxDouble get gpsGeoidSeparation => _gnss.geoidSeparation; RxInt get gpsQuality => _gnss.gpsQuality; RxDouble get gpsLatitudeError => _gnss.latitudeError; RxDouble get gpsLongitudeError => _gnss.longitudeError; RxDouble get gpsAltitudeError => _gnss.altitudeError; Rx get gpsDateTime => _gnss.gpsDateTime; bool get gpsIsConnected => _gnss.connectionState.value == GnssConnectionState.connected; // NTRIP állapot RxBool get ntripIsConnected => _ntrip.isConnected; RxInt get ntripDataPacketNumbers => _ntrip.packetCount; RxInt get ggaSenDataPacketNumber => _ntrip.ggaSentCount; RxString get ggaSendLastTimeStr => _ntrip.ggaLastSentTime; RxInt get ntripReceivedData => _ntrip.receivedBytes; // ── Számformátumok ──────────────────────────────────────────────── final formatEov = NumberFormat('##0,000.0', 'hu-HU'); final formatEovZ = NumberFormat('###0.0', 'hu-HU'); final formatAltitudeError = NumberFormat('####0.000', 'hu-HU'); final formatEovForFile = NumberFormat('#####0.0', 'hu-HU'); final formatWgs84Sec = NumberFormat('00.000', 'hu-HU'); // ── EOV koordináták (számítottak) ───────────────────────────────── Rx eov = Eov(0, 0).obs; Rx eovHeight = (0.0 as double?).obs; final eovY = 0.0.obs; final eovX = 0.0.obs; // DMS formátum RxInt latDegree = 0.obs; RxInt latMin = 0.obs; RxDouble latSec = 0.0.obs; RxInt longDegree = 0.obs; RxInt longMin = 0.obs; RxDouble longSec = 0.0.obs; // ── Térkép állapot ──────────────────────────────────────────────── static const double maxZoomValue = 25.0; RxDouble currentLongitude = 0.0.obs; RxDouble currentLatitude = 0.0.obs; RxDouble currentZoom = 12.0.obs; RxBool isMapMoveToCenter = true.obs; RxBool mapIsInitialized = false.obs; final MapController mapController = MapController(); final currentLocationMarker = [].obs; final pointNotesMarker = [].obs; final pointsToMeasureMarker = [].obs; final pointsToMeasureLabel = [].obs; final pointsToMeasureDropDownMenuItem = >[].obs; // ── Pont adatok ─────────────────────────────────────────────────── final RxList pointsToMeasure = [].obs; final RxList pointWithDescriptionList = [].obs; RxInt pointsToMeasureSelectedValue = (-1).obs; RxDouble distance = 0.0.obs; int pointId = 1; String pointIdPrefix = ''; String pointIdPostfix = ''; Rx pointMeasuringDirectionForward = true.obs; // ── Geoid grid ──────────────────────────────────────────────────── late GeoidGrid geoidGrid; // ── Fájlok ──────────────────────────────────────────────────────── late Directory? directory; late File dataFile; // ── Telefon GPS fallback ────────────────────────────────────────── StreamSubscription? _phoneLocationSub; StreamSubscription? _gnssUpdateSub; // ── UI controllerek ─────────────────────────────────────────────── final pointIdController = TextEditingController(); final pointDescriptionController = TextEditingController(); final gpsHeightController = TextEditingController(); final pointPrefixController = TextEditingController(); final pointPostfixController = TextEditingController(); late SharedPreferences prefs; Rx isShowPassword = false.obs; // ── Supabase ────────────────────────────────────────────────────── RealtimeChannel? _supaChannel; // ───────────────────────────────────────────────────────────────── // Lifecycle // ───────────────────────────────────────────────────────────────── @override void onInit() { super.onInit(); _initAsync(); } Future _initAsync() async { prefs = await SharedPreferences.getInstance(); geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx'); NtripService.to.onRtcmData = (data) => GnssService.to.sendToReceiver(data); _gnssUpdateSub = _gnss.onDataUpdated.listen((_) => _onGnssUpdate()); // ── Supabase realtime ───────────────────────────────────────── _supaChannel = Supabase.instance.client .channel('public:TerepiSeged_Receiver') .onPostgresChanges( event: PostgresChangeEvent.update, schema: 'public', table: 'TerepiSeged_Receiver', callback: (payload) { final id = payload.newRecord['pointNumber'] as int?; if (id != null) updatePointStatus(id); }, ) .subscribe(); mapIsInitialized.value = true; } @override void onReady() async { super.onReady(); await _initStorage(); gpsHeightController.text = '1.8'; } @override void onClose() { _phoneLocationSub?.cancel(); _gnssUpdateSub?.cancel(); final f = _supaChannel?.unsubscribe(); if (f != null) unawaited(f); pointIdController.dispose(); pointDescriptionController.dispose(); gpsHeightController.dispose(); pointPrefixController.dispose(); pointPostfixController.dispose(); super.onClose(); } // ───────────────────────────────────────────────────────────────── // GnssService frissítés kezelése // ───────────────────────────────────────────────────────────────── void _onGnssUpdate() { if (!_gnss.hasValidData) return; final lat = _gnss.latitude.value; final lon = _gnss.longitude.value; final alt = _gnss.altitude.value; final sep = _gnss.geoidSeparation.value; // EOV konverzió final converted = ConvertCoordinate.ConvertWgsToEov(lat, lon); eov.value = converted; eovY.value = converted.Y.toDouble(); eovX.value = converted.X.toDouble(); eovHeight.value = geoidGrid.toEovHeight(lat, lon, alt, sep); // DMS latDegree.value = ConvertCoordinate.toDegree(lat); latMin.value = ConvertCoordinate.toMinute(lat); latSec.value = ConvertCoordinate.toSecond(lat); longDegree.value = ConvertCoordinate.toDegree(lon); longMin.value = ConvertCoordinate.toMinute(lon); longSec.value = ConvertCoordinate.toSecond(lon); // Távolság a kiválasztott ponthoz if (pointsToMeasureSelectedValue.value >= 0 && pointsToMeasureSelectedValue.value < pointsToMeasure.length) { final pt = pointsToMeasure[pointsToMeasureSelectedValue.value]; final wgs = CoordConverterService.to.eovToWgsPoint(pt.coordX, pt.coordY); distance.value = calculateDistance( LatLng(lat, lon), LatLng(wgs.y, wgs.x), ); } // Térkép pozíció currentLatitude.value = lat; currentLongitude.value = lon; _updateCurrentLocationMarker(); // NTRIP GGA küldés NtripService.to.onGgaReceived( _gnss.lastGgaLine.value, _gnss.utcFix.value, ); } // ───────────────────────────────────────────────────────────────── // Térkép vezérlők // ───────────────────────────────────────────────────────────────── void mapZoomIn() { if (currentZoom.value >= maxZoomValue) return; currentZoom.value++; _moveMap(); } void mapZoomOut() { if (currentZoom.value <= 0) return; currentZoom.value--; _moveMap(); } void setIsMapMoveToCenter() => isMapMoveToCenter.value = !isMapMoveToCenter.value; void _moveMap() { final lat = isMapMoveToCenter.value ? currentLatitude.value : mapController.camera.center.latitude; final lon = isMapMoveToCenter.value ? currentLongitude.value : mapController.camera.center.longitude; mapController.move(LatLng(lat, lon), currentZoom.value); } void _updateCurrentLocationMarker() { currentLocationMarker.assignAll([ Marker( point: LatLng(currentLatitude.value, currentLongitude.value), width: 15.0, height: 15.0, child: Container( width: 15.0, height: 15.0, decoration: BoxDecoration( color: getCurrentLocationMarkerColor(_gnss.gpsQuality.value), shape: BoxShape.circle, border: Border.all(width: 1.5, color: Colors.white), ), ), ) ]); if (isMapMoveToCenter.value) { mapController.move( LatLng(currentLatitude.value, currentLongitude.value), currentZoom.value, ); } } Color getCurrentLocationMarkerColor(int quality) => switch (quality) { 0 => Colors.black, 1 => Colors.red, 2 => Colors.blue, 4 => Colors.green, 5 => Colors.orange, 6 => Colors.yellow, _ => Colors.white, }; String getGpsQualityIndicator({required int quality}) => switch (quality) { 0 => 'Invalid', 1 => 'Standard GPS', 2 => 'Differential GPS', 4 => 'RTK Fix', 5 => 'RTK Float', 6 => 'Estimated (DR)', _ => 'Invalid', }; // ───────────────────────────────────────────────────────────────── // Pont mentés dialóg // ───────────────────────────────────────────────────────────────── void showAddPointDialog() => onBottomNavigationBarTap(0); void onBottomNavigationBarTap(int index) async { if (index != 0) return; if (pointsToMeasureSelectedValue.value >= 0 && pointsToMeasureSelectedValue.value < pointsToMeasure.length) { pointIdController.text = '${pointsToMeasure[pointsToMeasureSelectedValue.value].id}'; } else { pointIdController.text = pointId.toString(); } pointDescriptionController.text = ''; Get.dialog( AlertDialog( title: const Text('Pont rögzítése'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: pointIdController, autofocus: true, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Azonosító'), ), const SizedBox(height: 20), TextField( controller: pointDescriptionController, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Leírás'), ), const SizedBox(height: 20), TextField( controller: gpsHeightController, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'GPS magasság'), ), ], ), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton( style: OutlinedButton.styleFrom(minimumSize: const Size(120, 40)), onPressed: () => Get.back(), child: const Text('Mégsem', style: TextStyle(color: Colors.red)), ), OutlinedButton( style: OutlinedButton.styleFrom(minimumSize: const Size(120, 40)), onPressed: _saveCurrentPoint, child: const Text('Ment', style: TextStyle( color: Colors.green, fontWeight: FontWeight.bold)), ), ], ), ], ), barrierDismissible: false, ); } Future _saveCurrentPoint() async { pointId = int.tryParse(pointIdController.text) ?? pointId; // Helyi lista pointWithDescriptionList.add(PointWithDescription( pointId, _gnss.gpsDateTime.value, pointDescriptionController.text, eov.value.Y, eov.value.X, _gnss.latitude.value, _gnss.longitude.value, max(_gnss.latitudeError.value, _gnss.longitudeError.value), _gnss.altitudeError.value, )); // Marker hozzáadása pointNotesMarker.add(Marker( point: LatLng(_gnss.latitude.value, _gnss.longitude.value), width: 15.0, height: 15.0, child: Container( width: 15.0, height: 15.0, decoration: BoxDecoration( color: Colors.amber[700], shape: BoxShape.circle, border: Border.all(width: 1.0, color: Colors.black), ), ), )); // Fájl mentés await dataFile.writeAsString( '$pointId;${_gnss.gpsDateTime.value};' '${pointDescriptionController.text};' '${formatEovForFile.format(eov.value.Y)};' '${formatEovForFile.format(eov.value.X)};' '${_gnss.latitude.value};${_gnss.longitude.value};' '${_gnss.altitude.value};' '${max(_gnss.latitudeError.value, _gnss.longitudeError.value)};' '${_gnss.altitudeError.value};' '${gpsHeightController.text}\r\n', mode: FileMode.append, ); // Supabase mentés await Supabase.instance.client.from('TerepiSeged_MeasuredPoints').insert({ 'pointNumber': pointId, 'gnssNumber': GnssDeviceService.to.selectedDevice.value?.name ?? '', 'latitude': _gnss.latitude.value, 'longitude': _gnss.longitude.value, 'altitude': _gnss.altitude.value, 'heightOfGeoid': _gnss.geoidSeparation.value, 'eovX': eov.value.X, 'eovY': eov.value.Y, 'poleHeight': double.tryParse(gpsHeightController.text), 'horizontalError': max(_gnss.latitudeError.value, _gnss.longitudeError.value), 'verticalError': _gnss.altitudeError.value, 'description': pointDescriptionController.text, 'isDeleted': false, 'projectId': 2, }); await Supabase.instance.client .from('TerepiSeged_Receiver') .update({'isMeasured': true}).eq('pointNumber', pointId); // Következő pont léptetése _advancePointSelection(); Get.back(); } void _advancePointSelection() { final len = pointsToMeasure.length; if (len == 0) return; if (pointMeasuringDirectionForward.isTrue) { if (pointsToMeasureSelectedValue.value < len - 1) { pointId++; pointsToMeasureSelectedValue.value++; } else { _showNoMorePoints(); } } else { if (pointsToMeasureSelectedValue.value > 0) { pointId--; pointsToMeasureSelectedValue.value--; } else { _showNoMorePoints(); } } } void _showNoMorePoints() { ScaffoldMessenger.of(Get.context!).showSnackBar( const SnackBar(content: Text('Nincs több bemérendő pont.')), ); } // ───────────────────────────────────────────────────────────────── // Pont betöltés // ───────────────────────────────────────────────────────────────── void ReadPointsFromFile() async { final result = await FilePicker.platform.pickFiles(); if (result == null) return; final file = File(result.files.single.path!); if (!await file.exists()) return; _clearPoints(); final content = await file.readAsLines(); for (final (index, item) in content.indexed) { if (index == 0) continue; _addPointFromCsv(item, index - 1); } if (pointsToMeasure.isNotEmpty) { pointsToMeasureSelectedValue.value = 0; } } void readPointsFromSupa() async { final data = await Supabase.instance.client.from('TerepiSeged_Receiver').select(); _clearPoints(); for (int i = 0; i < data.length; i++) { final item = data[i]; final id = item['pointNumber']; final coordX = (item['eovX'] as num?)?.toDouble(); final coordY = (item['eovY'] as num?)?.toDouble(); if (id == null || coordX == null || coordY == null) continue; _addPoint(id: id, coordX: coordX, coordY: coordY, listIndex: i); } if (pointsToMeasure.isNotEmpty) { pointsToMeasureSelectedValue.value = 0; } } void _clearPoints() { pointsToMeasure.clear(); pointsToMeasureMarker.clear(); pointsToMeasureLabel.clear(); pointsToMeasureDropDownMenuItem.clear(); } void _addPointFromCsv(String line, int listIndex) { final parts = line.split(';'); if (parts.length < 3) return; final id = int.tryParse(parts[0]); final coordX = double.tryParse(parts[1].replaceAll(',', '.')); final coordY = double.tryParse(parts[2].replaceAll(',', '.')); if (id == null || coordX == null || coordY == null) return; _addPoint(id: id, coordX: coordX, coordY: coordY, listIndex: listIndex); } void _addPoint({ required int id, required double coordX, required double coordY, required int listIndex, }) { pointsToMeasure.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY)); final wgs = CoordConverterService.to.eovToWgsPoint(coordX, coordY); pointsToMeasureMarker.add(Marker( point: LatLng(wgs.y, wgs.x), width: 15.0, height: 15.0, child: Container( width: 15.0, height: 15.0, decoration: BoxDecoration( color: Colors.purple, shape: BoxShape.circle, border: Border.all(width: 1.0, color: Colors.black), ), ), )); pointsToMeasureLabel.add(PolyWidget( center: LatLng(wgs.y + 0.0000075, wgs.x + 0.0000075), widthInMeters: 3, heightInMeters: 3, child: FittedBox( child: Padding( padding: const EdgeInsets.all(4), child: Text( ' $id ', style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.yellow, fontSize: 12, ), ), ), ), )); pointsToMeasureDropDownMenuItem.add(DropdownMenuItem( value: listIndex, child: Text('$id'), )); } void pointsToMeasureSelectedValueChanged(int value) => pointsToMeasureSelectedValue.value = value; // ───────────────────────────────────────────────────────────────── // Segédmetódusok // ───────────────────────────────────────────────────────────────── double calculateDistance(LatLng start, LatLng end) { const r = 6371000.0; final lat1 = start.latitude * (pi / 180); final lon1 = start.longitude * (pi / 180); final lat2 = end.latitude * (pi / 180); final lon2 = end.longitude * (pi / 180); final dLat = lat2 - lat1; final dLon = lon2 - lon1; final a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1) * cos(lat2) * sin(dLon / 2) * sin(dLon / 2); return r * 2 * atan2(sqrt(a), sqrt(1 - a)); } void updatePointStatus(int pointId) {} void showMeasuredPointsTableDialog() => Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn); // ───────────────────────────────────────────────────────────────── // Tárhely inicializálás // ───────────────────────────────────────────────────────────────── Future _initStorage() async { directory = await getExternalStorageDirectory(); if (directory != null && !await directory!.exists()) { await directory!.create(recursive: true); } dataFile = File('${directory!.path}/data.txt'); if (!await dataFile.exists()) { await dataFile.writeAsString( 'Id;DateTime;Description;EovX;EovY;Latitude;Longitude;Altitude;Hor.Err;Vert.Err\r\n', ); } } // ───────────────────────────────────────────────────────────────── // Export // ───────────────────────────────────────────────────────────────── Future readMeasuredPoints() async => Supabase.instance.client .from('TerepiSeged_MeasuredPoints') .select() .eq('projectId', 2) .order('created_at'); void SaveMeasuredPointsToFile() async { final dir = await getApplicationDocumentsDirectory(); final file = File('${dir.path}/measuredsPoints.csv'); if (await file.exists()) await file.delete(); await file.create(); await file.writeAsString( 'Id;DateTime;Description;EovX;EovY;Altitude;Hor.Err;Vert.Err\r\n'); final data = await readMeasuredPoints(); for (final d in data) { file.writeAsStringSync( '${d['id']};${d['created_at']};${d['description']};' '${formatEov.format(d['eovY'])};${formatEov.format(d['eovX'])};' '${formatEovZ.format((d['altitude'] as num) - (d['poleHeight'] as num))};' '${formatAltitudeError.format(d['horizontalError'])};' '${formatAltitudeError.format(d['verticalError'])}\r\n', mode: FileMode.append, encoding: utf8, ); } await SharePlus.instance.share(ShareParams( text: 'Mérési eredmények', files: [XFile('${dir.path}/measuredsPoints.csv')], subject: 'Mérési eredmények', )); } void switchMode(MapSurveyMode newMode) { mode.value = newMode; if (newMode == MapSurveyMode.measure) { targetName.value = ''; targetEovY.value = 0; targetEovX.value = 0; } } void setTarget(String name, double y, double x) { targetName.value = name; targetEovY.value = y; targetEovX.value = x; mode.value = MapSurveyMode.stakeout; } }