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/core/geometry_measure.dart'; import 'package:terepi_seged/core/geometry_measure_formatter.dart'; import 'package:terepi_seged/core/geopackage_exporter.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/measured_point.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/imported_layer_overlay.dart'; import 'package:terepi_seged/widgets/map_edit_tools/color_row.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; import 'package:terepi_seged/widgets/map_edit_tools/note_item_list_sheet.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; import '../views/measured_points_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(); bool _isMapProgrammaticMove = false; final polylineHitNotifier = ValueNotifier?>(null); final polygonHitNotifier = ValueNotifier?>(null); final currentLocationMarker = [].obs; final pointNotesMarker = [].obs; final pointsToMeasureMarker = [].obs; final pointsToMeasureLabel = [].obs; final pointsToMeasureDropDownMenuItem = >[].obs; final allNoteItems = [].obs; final showGeometryLabels = false.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; final draftLengthMeters = 0.0.obs; final draftAreaSquareMeters = 0.0.obs; String get draftMeasureText { switch (activeEditTool.value) { case MapEditTool.line: return GeometryMeasureFormatter.length( draftLengthMeters.value, ); case MapEditTool.polygon: return GeometryMeasureFormatter.area( draftAreaSquareMeters.value, ); case MapEditTool.point: case MapEditTool.none: return ''; } } // 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; _updateDraftMeasurements(); }); await _setInitialPositionFromPhone(); mapIsInitialized.value = true; } @override void onReady() async { super.onReady(); await _initStorage(); gpsHeightController.text = '1.8'; ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); await _loadNoteItems(); await _loadMeasurePoints(); } @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(); } Future _setInitialPositionFromPhone() async { try { // Engedély ellenőrzés final permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { await Geolocator.requestPermission(); } // getLastKnownPosition → azonnal visszatér (cache) // Nem vár új GPS fixre → nem lassítja az indulást Position? pos = await Geolocator.getLastKnownPosition(); // Ha nincs cache → gyors egyszeri lekérés (max 3 mp) pos ??= await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, // gyors, kevésbé pontos timeLimit: Duration(seconds: 3), ), ).timeout( const Duration(seconds: 3), onTimeout: () => throw Exception('GPS timeout'), ); currentLatitude.value = pos.latitude; currentLongitude.value = pos.longitude; // Térkép mozgatása — csak ha már inicializálva van a widget // mapIsInitialized előtt hívjuk, de a controller már kész // A move() biztonságos ha a mapController már csatolt mapController.move( LatLng(pos.latitude, pos.longitude), currentZoom.value, ); } catch (e) { // Hiba esetén marad a default pozíció — nem kritikus debugPrint('Kezdő pozíció lekérés sikertelen: $e'); } } // ───────────────────────────────────────────────────────────────── // 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() { final newZoom = (mapController.camera.zoom + 1).clamp(0.0, maxZoomValue); // if (currentZoom.value >= maxZoomValue) return; // currentZoom.value++; currentZoom.value = newZoom; _isMapProgrammaticMove = true; _moveMap(); } void mapZoomOut() { final newZoom = (mapController.camera.zoom - 1).clamp(0.0, maxZoomValue); // if (currentZoom.value <= 0) return; // currentZoom.value--; currentZoom.value = newZoom; _isMapProgrammaticMove = true; _moveMap(); } void setIsMapMoveToCenter() { isMapMoveToCenter.value = !isMapMoveToCenter.value; // Bekapcsoláskor azonnal ugrik az aktuális pozícióra if (isMapMoveToCenter.value && currentLatitude.value != 0.0 && currentLongitude.value != 0.0) { _isMapProgrammaticMove = true; mapController.move( LatLng(currentLatitude.value, currentLongitude.value), currentZoom.value, ); // Ez MapEventSource.mapController → NEM kapcsolja ki a követést ✓ } } 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 onMapPositionChanged(MapCamera camera, bool hasGesture) { if (hasGesture) { currentZoom.value = camera.zoom; } if (_isMapProgrammaticMove) { _isMapProgrammaticMove = false; return; } if (hasGesture && isMapMoveToCenter.value) { isMapMoveToCenter.value = false; } // if (isMapMoveToCenter.value) { // isMapMoveToCenter.value = false; // } } void _updateCurrentLocationMarker() { // Koordináta validáció — 0,0 = nincs fix final lat = currentLatitude.value; final lon = currentLongitude.value; if (lat == 0.0 && lon == 0.0) { currentLocationMarker.clear(); return; } final color = _gnss.hasValidData ? getCurrentLocationMarkerColor(_gnss.gpsQuality.value) : Colors.blue; // telefon GPS: kék currentLocationMarker.assignAll([ Marker( point: LatLng(lat, lon), width: 16, height: 16, child: PulsingDot(color: color), // ← shared_map_widgets.dart-ból ), ]); // 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) { _isMapProgrammaticMove = true; 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, 'eovZ': eovHeight.value, 'poleHeight': double.tryParse(gpsHeightController.text), 'horizontalError': _gnss.horizontalAccuracy, 'verticalError': _gnss.altitudeError.value, 'description': pointDescriptionController.text, 'isDeleted': false, 'projectId': 2, }); await Supabase.instance.client .from('TerepiSeged_Receiver') .update({'isMeasured': true}).eq('pointNumber', pointId); // SQLite mentés final projectId = ProjectService.to.activeProjectId; if (projectId != null) { await AppDatabase.instance.insertMeasuredPoint(MeasuredPoint( projectId: projectId, name: pointId.toString(), eovY: eov.value.Y, eovX: eov.value.X, eovZ: eovHeight.value! - (double.tryParse(gpsHeightController.text) ?? 0.0), latitude: _gnss.latitude.value, longitude: _gnss.longitude.value, altitude: _gnss.altitude.value, accuracy: _gnss.horizontalAccuracy, fixQuality: _gnss.gpsQuality.value, timestamp: DateTime.now(), note: pointDescriptionController.text, )); } // 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 'Hosszan nyomj 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; activeEditLabel.value = ''; } void startLineTool() { polygonEditorController.clear(); polygonEditorController.setMode(PolygonEditorMode.line); activeEditTool.value = MapEditTool.line; activeEditLabel.value = ''; } void startPolygonTool() { polygonEditorController.clear(); polygonEditorController.setMode(PolygonEditorMode.polygon); activeEditTool.value = MapEditTool.polygon; activeEditLabel.value = ''; } void cancelEditing() { polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; editingNoteItemId = null; selectedNoteItemId.value = null; editorPointCount.value = 0; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.0; } 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); allNoteItems.assignAll(items); // 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 _loadMeasurePoints() async { final projectId = ProjectService.to.activeProject.value?.id; pointNotesMarker.clear(); pointWithDescriptionList.clear(); if (projectId == null) return; final points = await AppDatabase.instance.listMeasuredPoints(projectId); for (final pt in points) { if (pt.latitude != null && pt.longitude != null) { pointNotesMarker.add(Marker( point: LatLng(pt.latitude!, pt.longitude!), 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)), ))); } pointWithDescriptionList.add(PointWithDescription( int.tryParse(pt.name) ?? 0, pt.timestamp, pt.note, pt.eovY ?? 0.0, pt.eovX ?? 0.0, pt.latitude ?? 0.0, pt.longitude ?? 0.0, pt.accuracy ?? 0.0, 0.0)); } if (points.isNotEmpty) { final maxId = points .map((p) => int.tryParse(p.name) ?? 0) .fold(0, (prev, id) => id > prev ? id : prev); pointId = maxId + 1; } } 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); allNoteItems.removeWhere((i) => i.id == id); allNoteItems.add(updated); editingNoteItemId = null; activeEditTool.value = MapEditTool.none; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.0; } 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; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.0; } // 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)); allNoteItems.add(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 { allNoteItems.removeWhere((i) => i.id == item.id); 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; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.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()); allNoteItems.add(saved); } polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.0; activeEditLabel.value = ''; } 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; draftAreaSquareMeters.value = 0.0; draftLengthMeters.value = 0.0; } } int get _minPoints { if (polygonEditorController.mode == PolygonEditorMode.line) return 2; return 3; // polygon } void _updateDraftMeasurements() { switch (activeEditTool.value) { case MapEditTool.line: final points = List.from( polygonEditorController.points, ); draftLengthMeters.value = GeometryMeasure.lineLengthMeters(points); draftAreaSquareMeters.value = 0.0; break; case MapEditTool.polygon: final points = List.from( polygonEditorController.points, ); draftLengthMeters.value = GeometryMeasure.lineLengthMeters(points); draftAreaSquareMeters.value = GeometryMeasure.polygonAreaSquareMeters(points); break; case MapEditTool.point: case MapEditTool.none: draftLengthMeters.value = 0.0; draftAreaSquareMeters.value = 0.0; break; } } void openLayerPanel() { Get.bottomSheet( DraggableScrollableSheet( initialChildSize: 0.45, minChildSize: 0.3, maxChildSize: 0.85, snap: true, snapSizes: const [0.3, 0.45, 0.85], expand: false, builder: (_, scrollCtrl) => Container( decoration: BoxDecoration( color: Get.theme.colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 16, offset: const Offset(0, -4), ), ], ), child: CustomScrollView( controller: scrollCtrl, slivers: [ SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle Center( child: Container( margin: const EdgeInsets.symmetric(vertical: 10), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Importált rétegek', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 16), ImportLayerPanel( onFitBounds: fitImportedLayer, ), ], ), ), ], ), ), ], ), ), ), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false, ); } void fitImportedLayer(LatLngBounds bounds) { _isMapProgrammaticMove = true; mapController.fitCamera( CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.all(48), ), ); } void openMeasuredPointsList() { Get.bottomSheet( DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.4, maxChildSize: 0.92, snap: true, expand: false, builder: (_, __) => const MeasuredPointsSheet(), ), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false, ); } // ----- Terepbejárás pont mentése void showSavePointDialog({required LatLng point}) { final labelCtrl = TextEditingController(); Get.dialog( AlertDialog( title: const Text('Pont mentése'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ ColorRow(ctrl: this, circleSize: 34), SizedBox(height: 8), TextField( controller: labelCtrl, autofocus: true, decoration: const InputDecoration( hintText: 'Pont neve (opcionális)', border: OutlineInputBorder(), ), textCapitalization: TextCapitalization.sentences, onSubmitted: (_) => _doSavePoint(point, labelCtrl.text), ), ], ), actions: [ TextButton( onPressed: Get.back, child: const Text('Mégse'), ), FilledButton( onPressed: () => _doSavePoint(point, labelCtrl.text), child: const Text('Mentés'), ), ], ), barrierDismissible: false, ); } void _doSavePoint(LatLng point, String label) { activeEditLabel.value = label.trim(); Get.back(); saveEditedPoint(point: point); } void savePointFromCurrentPosition() { final lat = currentLatitude.value; final lon = currentLongitude.value; if (lat == 0.0 && lon == 0.0) { Get.snackbar('Hiba', 'GPS pozíció nem elérhető', snackPosition: SnackPosition.BOTTOM); return; } showSavePointDialog(point: LatLng(lat, lon)); } void openNoteItemList() { Get.bottomSheet( DraggableScrollableSheet( initialChildSize: 0.65, minChildSize: 0.4, maxChildSize: 0.92, snap: true, expand: false, builder: (_, __) => const NoteItemListSheet(), ), isScrollControlled: true, backgroundColor: Colors.transparent, ignoreSafeArea: false, ); } void toggleGeometryLabels() => showGeometryLabels.value = !showGeometryLabels.value; Future exportProject() async { Get.dialog( Center( child: Material( borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: const Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text( 'GeoPackage generálása...', style: TextStyle(fontSize: 16), ), ], ), ), ), ), barrierDismissible: false, ); try { await GeoPackageExporter().exportProject(); } catch (e) { Get.snackbar('Export hiba', e.toString(), snackPosition: SnackPosition.BOTTOM); } finally { Get.back(); // ← dialóg bezárása, akár sikerült akár nem } } }