import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:math' as 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_polygon_editor/polygon_editor/polygon_editor_controller.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/controls/wgs84_coordinate_formatter.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/enums/map_survey_mode.dart'; import 'package:terepi_seged/enums/note_type.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/models/note_item.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/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart'; import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart'; import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart'; import 'package:terepi_seged/services/app_database.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'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; 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; double? get horizontalAccuracy => _gnss.horizontalAccuracy; RxDouble get pdop => _gnss.pdop; RxDouble get hdop => _gnss.hdop; RxDouble get vdop => _gnss.vdop; final wgs84CoordinateFormat = Wgs84CoordinateFormat.decimalDegrees.obs; final showWgs84Card = true.obs; final showEovCard = true.obs; final showGnssQualityCard = true.obs; // 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 polylineHitNotifier = ValueNotifier?>(null); final polygonHitNotifier = ValueNotifier?>(null); 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; // ------- Map edit ---------------- final activeEditTool = MapEditTool.none.obs; final editorPointCount = 0.obs; final pointNotes = [].obs; final polylineNotes = >[].obs; final polygonNotes = >[].obs; late final PolygonEditorController polygonEditorController; final activeEditColor = const Color(0xFF185FA5).obs; final activeEditOpacity = 0.5.obs; final activeEditStrokeWidth = 3.0.obs; final activeEditStrokeColor = const Color(0xFFFFD700).obs; final activeEditLabel = ''.obs; final PolygonLabelPlacementCalculator _labelPlacementCalculator = const PolygonLabelPlacementCalculator.centroid(); bool get isMapEditing => activeEditTool.value != MapEditTool.none; final selectedNoteItemId = Rx(null); final selectedNoteItemType = NoteType.line.obs; int? _editingNoteItemId; bool get isGeometryEditing => _editingNoteItemId != null; // NoteItem? get selectedPoint => // pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value); // ───────────────────────────────────────────────────────────────── // 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(); polygonEditorController = PolygonEditorController(mode: PolygonEditorMode.polygon); polygonEditorController.addListener(() { editorPointCount.value = polygonEditorController.points.length; }); mapIsInitialized.value = true; } @override void onReady() async { super.onReady(); await _initStorage(); gpsHeightController.text = '1.8'; ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); await _loadNoteItems(); } @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(); polygonEditorController.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, ); } String _formatMeter(double? value, [int decimalSpace = 3]) { if (value == null || value.isNaN || value.isInfinite) { return '-'; } return '${value.toStringAsFixed(decimalSpace)} m'; } String get verticalAccuracyText { return _formatMeter(gpsAltitudeError.value); } String get horizontalAccuracyText { return _formatMeter(max(gpsLatitudeError.value, gpsLongitudeError.value)); } void setMode(MapSurveyMode newMode) { if (mode.value == MapSurveyMode.track && newMode != MapSurveyMode.track && TrackingController.to.isRecording.value) { Get.snackbar( 'Rögzítés folytatódik', 'A track rögzítés a háttérben aktív marad.', icon: const Icon(Icons.fiber_manual_record, color: Colors.red), duration: const Duration(seconds: 3), snackPosition: SnackPosition.TOP, ); } mode.value = newMode; // Itt lehet módhoz kötött állapotokat állítani: // - alsó panel tartalma // - térképi tap viselkedés // - aktív kártyák // - track indítás/leállítás figyelmeztetés stb. } String get currentModeLabel => switch (mode.value) { MapSurveyMode.browse => 'Térkép', MapSurveyMode.measure => 'Bemérés', MapSurveyMode.stakeout => 'Kitűzés', MapSurveyMode.fieldWalk => 'Bejárás', MapSurveyMode.track => 'Útvonal' }; IconData get currentModeIcon => switch (mode.value) { MapSurveyMode.browse => Icons.map, MapSurveyMode.measure => Icons.add_location_alt, MapSurveyMode.stakeout => Icons.gps_fixed, MapSurveyMode.fieldWalk => Icons.hiking, MapSurveyMode.track => Icons.route }; void openNtripsettings() { if (!Get.isRegistered()) { Get.put(NtripSettingsController()); } Get.bottomSheet(const NtripSettingsSheet(), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false); } void showNtripStatus() { // Get.bottomSheet( // const NtripStatusSheet(), // isScrollControlled: true, // backgroundColor: Colors.transparent, // ); } // ───────────────────────────────────────────────────────────────── // 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; } void setWgs84CoordinateFormat(Wgs84CoordinateFormat format) { wgs84CoordinateFormat.value = format; } void toggleShowWgs84Card() { showWgs84Card.value = !showWgs84Card.value; } void toggleShowEovCard() { showEovCard.value = !showEovCard.value; } void toggleShowGnssQualityCard() { showGnssQualityCard.value = !showGnssQualityCard.value; } String get latitudeText { return Wgs84CoordinateFormatter.formatLatitude( gpsLatitude.value, wgs84CoordinateFormat.value); } String get longitudeText { return Wgs84CoordinateFormatter.formatLongitude( gpsLongitude.value, wgs84CoordinateFormat.value); } String formatDop(double? value) { if (value == null || value.isNaN || value.isInfinite) { return '-'; } return value.toStringAsFixed(2); } String formatMeterCompact(double? value) { if (value == null || value.isNaN || value.isInfinite) { return '-'; } if (value < 1.0) { return '${value.toStringAsFixed(3)}m'; } if (value < 10.0) { return '${value.toStringAsFixed(2)}m'; } return '${value.toStringAsFixed(1)}m'; } // --------- Térkép szerkesztési műveletek IconData get activeEditToolIcon { switch (activeEditTool.value) { case MapEditTool.point: return Icons.add_location_alt_outlined; case MapEditTool.line: return Icons.polyline_outlined; case MapEditTool.polygon: return Icons.border_outer_outlined; case MapEditTool.none: return Icons.edit_location_alt_outlined; } } String get activeEditToolTitle { switch (activeEditTool.value) { case MapEditTool.point: return 'Pont hozzáadása'; case MapEditTool.line: return 'Vonal rögzítése'; case MapEditTool.polygon: return 'Terület rögzítése'; case MapEditTool.none: return ''; } } String get activeEditToolHint { switch (activeEditTool.value) { case MapEditTool.point: return 'Koppints a térképre a pont helyéhez.'; case MapEditTool.line: return 'Hosszan nyomj a térképre a töréspontokhoz'; case MapEditTool.polygon: return 'Hosszan nyomj a térképre a sarokpontokhoz.'; case MapEditTool.none: return ''; } } bool get canFinishGeometry { switch (activeEditTool.value) { case MapEditTool.point: return editorPointCount == 1; case MapEditTool.line: return editorPointCount >= 2; case MapEditTool.polygon: return editorPointCount >= 3; case MapEditTool.none: return false; } } String get finishButtonText { switch (activeEditTool.value) { case MapEditTool.point: return 'Kész'; case MapEditTool.line: return 'Kész'; case MapEditTool.polygon: return 'Lezárás'; case MapEditTool.none: return 'Kész'; } } void startPointTool() { activeEditTool.value = MapEditTool.point; } void startLineTool() { polygonEditorController.clear(); polygonEditorController.setMode(PolygonEditorMode.line); activeEditTool.value = MapEditTool.line; } void startPolygonTool() { polygonEditorController.clear(); polygonEditorController.setMode(PolygonEditorMode.polygon); activeEditTool.value = MapEditTool.polygon; } void cancelEditing() { polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; _editingNoteItemId = null; selectedNoteItemId.value = null; editorPointCount.value = 0; } void openFeatureList() { // TODO: DraggableScrollableSheet / bottom sheet lista } void openLayerPanel() { // TODO: rétegek sheet } void finishGeometry() { if (!canFinishGeometry) return; // Itt nyisd majd meg a szerkesztő sheetet: // openFeatureEditorSheet(); // Példa: // Get.bottomSheet( // FeatureEditorSheet( // tool: activeTool.value, // points: List.from(draftPoints), // ), // isScrollControlled: true, // ); Get.bottomSheet( DraggableScrollableSheet( initialChildSize: 0.52, minChildSize: 0.35, maxChildSize: 0.85, snap: true, snapSizes: const [0.35, 0.52, 0.85], expand: false, builder: (_, scrollCtrl) => MapFeatureSaveSheet(ctrl: this, scrollCtrl: scrollCtrl)), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false); //activeEditTool.value = MapEditTool.none; //draftPoints.clear(); } Marker _markerFromNoteItem(NoteItem item) { return Marker( key: ValueKey('note_point_${item.id}'), point: item.points.first, width: 32.0, height: 32.0, child: GestureDetector( onTap: () => selectedNoteItem(item.id!), child: Obx(() { final isSelected = selectedNoteItemId.value == item.id; return Center( child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: isSelected ? 26.0 : 20.0, height: isSelected ? 26.0 : 20.0, decoration: BoxDecoration( color: item.color, shape: BoxShape.circle, border: Border.all( width: isSelected ? 3.0 : 1.5, color: isSelected ? Colors.white : item.strokeColor), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: isSelected ? 8 : 3) ]), )); }), ), ); } // ── SQLite mentés ───────────────────────────────────────────────── Future _saveItem({ required NoteType type, required List points, }) async { final projectId = ProjectService.to.activeProject.value?.id; final item = NoteItem( projectId: projectId, // projekt nélkül is mentődik type: type, points: points, color: activeEditColor.value, opacity: activeEditOpacity.value, strokeWidth: activeEditStrokeWidth.value, strokeColor: activeEditStrokeColor.value, label: activeEditLabel.value, createdAt: DateTime.now(), ); try { final id = await AppDatabase.instance.insertNoteItem(item); // id-vel visszaadott NoteItem — a hitValue ehhez az id-hez kötődik return NoteItem( id: id, projectId: item.projectId, type: item.type, points: item.points, color: item.color, opacity: item.opacity, strokeWidth: item.strokeWidth, strokeColor: item.strokeColor, label: item.label, createdAt: item.createdAt, ); } catch (e) { print('_saveItem hiba: $e'); return null; } } // ── SQLite betöltés (onReady-ben hívandó) ───────────────────────── Future _loadNoteItems() async { final projectId = ProjectService.to.activeProject.value?.id; final items = await AppDatabase.instance.listNoteItems(projectId); // Listák resetelése pointNotes.clear(); polylineNotes.clear(); polygonNotes.clear(); for (final item in items) { switch (item.type) { case NoteType.point: pointNotes.add(_markerFromNoteItem(item)); case NoteType.line: polylineNotes.add(item.toPolyline()); case NoteType.polygon: polygonNotes.add(item.toPolygon()); } } } Future finishDraft() async { if (_editingNoteItemId != null) { if (polygonEditorController.points.isEmpty) { await _finishStyleUpdate(); } else { // ── FRISSÍTÉSI MÓD ────────────────────────────────────────── await _finishGeometryUpdate(); } } else { // ── LÉTREHOZÁSI MÓD (eredeti logika) ──────────────────────── await _finishCreate(); } } Future _finishStyleUpdate() async { final id = _editingNoteItemId!; final existing = await AppDatabase.instance.getNoteItem(id); if (existing == null) return; final updated = existing.copyWith( color: activeEditColor.value, opacity: activeEditOpacity.value, strokeWidth: activeEditStrokeWidth.value, strokeColor: activeEditStrokeColor.value, label: activeEditLabel.value, ); await updateNoteItem(updated); _editingNoteItemId = null; } Future deleteEditingItem() async { final id = _editingNoteItemId; if (id == null) return; final item = await AppDatabase.instance.getNoteItem(id); if (item != null) await deleteNoteItem(item); _editingNoteItemId = null; selectedNoteItemId.value = null; activeEditTool.value = MapEditTool.none; } // Future finishDraft() async { // if (polygonEditorController.mode == PolygonEditorMode.line) { // print("Points number in line: ${polygonEditorController.points.length}"); // print( // "1. point coords: ${polygonEditorController.points[0].latitude} - ${polygonEditorController.points[0].longitude}"); // if (polygonEditorController.points.length < 2) return; // final saved = await _saveItem( // type: NoteType.line, // points: List.from(polygonEditorController.points)); // if (saved != null) { // polylineNotes.add(saved.toPolyline()); // } // polygonEditorController.clear(); // activeEditTool.value = MapEditTool.none; // } // if (polygonEditorController.mode == PolygonEditorMode.polygon) { // if (polygonEditorController.points.length < 3) return; // final saved = await _saveItem( // type: NoteType.polygon, // points: List.from(polygonEditorController.points)); // if (saved != null) { // polygonNotes.add(saved.toPolygon()); // } // polygonEditorController.clear(); // activeEditTool.value = MapEditTool.none; // } // } void saveEditedPoint({required LatLng point}) { if (_editingNoteItemId != null) { // ── PONT FRISSÍTÉSI MÓD ────────────────────────────────────── final id = _editingNoteItemId!; _editingNoteItemId = null; activeEditTool.value = MapEditTool.none; AppDatabase.instance.getNoteItem(id).then((existing) async { if (existing == null) return; final updated = existing.copyWith(points: [point]); await updateNoteItem(updated); }); } else { // ── ÚJ PONT LÉTREHOZÁS (eredeti logika) ───────────────────── _saveItem( type: NoteType.point, points: [point], ).then((saved) { if (saved == null) return; pointNotes.add(_markerFromNoteItem(saved)); }); activeEditTool.value = MapEditTool.none; } } // void saveEditedPoint({required LatLng point}) { // _saveItem(type: NoteType.point, points: [point]).then((saved) { // if (saved == null) return; // pointNotes.add(_markerFromNoteItem(saved)); // }); // // Marker marker = Marker( // // point: point, // // 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)), // // )); // //pointNotes.add(marker); // activeEditTool.value = MapEditTool.none; // } Future selectedNoteItem(int id) async { selectedNoteItemId.value = id; final item = await AppDatabase.instance.getNoteItem(id); if (item == null) return; _editingNoteItemId = item.id; activeEditColor.value = item.color; activeEditOpacity.value = item.opacity; activeEditStrokeWidth.value = item.strokeWidth; activeEditStrokeColor.value = item.strokeColor; activeEditLabel.value = item.label; activeEditTool.value = switch (item.type) { NoteType.point => MapEditTool.point, NoteType.line => MapEditTool.line, NoteType.polygon => MapEditTool.polygon }; Get.bottomSheet( DraggableScrollableSheet( initialChildSize: 0.52, minChildSize: 0.35, maxChildSize: 0.85, snap: true, snapSizes: const [0.35, 0.52, 0.85], expand: false, builder: (_, scrollCtrl) => MapFeatureSaveSheet(ctrl: this, scrollCtrl: scrollCtrl)), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false) .whenComplete(() { selectedNoteItemId.value = null; //_editingNoteItemId = null; if (!isGeometryEditing) { activeEditTool.value = MapEditTool.none; } }); } void clearNoteItemSelection() { selectedNoteItemId.value = null; } Future updateNoteItem(NoteItem updated) async { await AppDatabase.instance.updateNoteItem(updated); switch (updated.type) { case NoteType.line: final idx = polylineNotes.indexWhere((p) => p.hitValue == updated.id); if (idx >= 0) { polylineNotes[idx] = updated.toPolyline(); polylineNotes.refresh(); } case NoteType.polygon: final idx = polygonNotes.indexWhere((p) => p.hitValue == updated.id); if (idx >= 0) { polygonNotes[idx] = updated.toPolygon(); polygonNotes.refresh(); } case NoteType.point: final idx = _findPointMarkerIndex(updated.id!); if (idx >= 0) { pointNotes[idx] = _markerFromNoteItem(updated); pointNotes.refresh(); } } } Future deleteNoteItem(NoteItem item) async { await AppDatabase.instance.deleteNoteItem(item.id!); switch (item.type) { case NoteType.line: polylineNotes.removeWhere((p) => p.hitValue == item.id); case NoteType.polygon: polygonNotes.removeWhere((p) => p.hitValue == item.id); case NoteType.point: // Pontot tag-elt Key alapján keresünk final idx = _findPointMarkerIndex(item.id!); if (idx >= 0) pointNotes.removeAt(idx); } selectedNoteItemId.value = null; } /// Pont marker indexének megkeresése. /// A marker Key-je hordozza a NoteItem id-t. int _findPointMarkerIndex(int noteId) { return pointNotes.indexWhere( (m) => m.key == ValueKey('note_point_$noteId'), ); } // ── Geometria szerkesztés indítása ──────────────────────────────────── Future startGeometryEdit() async { final id = _editingNoteItemId; if (id == null) return; final item = await AppDatabase.instance.getNoteItem(id); if (item == null) return; if (item.type == NoteType.point) { // Pontnál: régi törlése, új elhelyezés vár activeEditTool.value = MapEditTool.point; return; } // Editor mód beállítása a típus szerint final editorMode = item.type == NoteType.line ? PolygonEditorMode.line : PolygonEditorMode.polygon; // PolygonEditorController újraindítása a meglévő pontokkal // Dispose → újra létrehozás szükséges ha a controller már él polygonEditorController.clear(); polygonEditorController.setMode(editorMode); // Meglévő pontok betöltése az editorba for (final point in item.points) { polygonEditorController.addPoint(point); } editorPointCount.value = item.points.length; // Szerkesztési mód aktiválása activeEditTool.value = item.type == NoteType.line ? MapEditTool.line : MapEditTool.polygon; } // ── Geometria szerkesztés megszakítása ──────────────────────────────── void cancelGeometryEdit() { _editingNoteItemId = null; polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; editorPointCount.value = 0; } Future _finishGeometryUpdate() async { final id = _editingNoteItemId!; if (polygonEditorController.points.length < _minPoints) { Get.snackbar( 'Figyelem', 'Nincs elég pont a geometriához.', snackPosition: SnackPosition.BOTTOM, ); return; } // Meglévő elem lekérése az adatbázisból final existing = await AppDatabase.instance.getNoteItem(id); if (existing == null) return; // Frissítés: csak a pontok változnak, a stílus marad final updated = existing.copyWith( points: List.from(polygonEditorController.points), // Ha a stílus is változott a szerkesztés közben: color: activeEditColor.value, opacity: activeEditOpacity.value, strokeWidth: activeEditStrokeWidth.value, strokeColor: activeEditStrokeColor.value, label: activeEditLabel.value, ); // SQLite + display lista frissítése await updateNoteItem(updated); // Reset _editingNoteItemId = null; polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; editorPointCount.value = 0; Get.snackbar( 'Geometria frissítve', updated.label.isNotEmpty ? updated.label : '', snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), ); } Future _finishCreate() async { if (polygonEditorController.mode == PolygonEditorMode.line) { if (polygonEditorController.points.length < 2) return; final saved = await _saveItem( type: NoteType.line, points: List.from(polygonEditorController.points), ); if (saved != null) { polylineNotes.add(saved.toPolyline()); } polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; } if (polygonEditorController.mode == PolygonEditorMode.polygon) { if (polygonEditorController.points.length < 3) return; final saved = await _saveItem( type: NoteType.polygon, points: List.from(polygonEditorController.points), ); if (saved != null) { polygonNotes.add(saved.toPolygon()); } polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; } } int get _minPoints { if (polygonEditorController.mode == PolygonEditorMode.line) return 2; return 3; // polygon } }