MobilApp/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart

1857 lines
62 KiB
Dart

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/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/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<DateTime> 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 = Eov(0, 0).obs;
Rx<double?> 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<LayerHitResult<int>?>(null);
final polygonHitNotifier = ValueNotifier<LayerHitResult<int>?>(null);
final currentLocationMarker = <Marker>[].obs;
final pointNotesMarker = <Marker>[].obs;
final pointsToMeasureMarker = <Marker>[].obs;
final pointsToMeasureLabel = <PolyWidget>[].obs;
final pointsToMeasureDropDownMenuItem = <DropdownMenuItem<int>>[].obs;
// ── Pont adatok ───────────────────────────────────────────────────
final RxList<PointToMeasure> pointsToMeasure = <PointToMeasure>[].obs;
final RxList<PointWithDescription> pointWithDescriptionList =
<PointWithDescription>[].obs;
RxInt pointsToMeasureSelectedValue = (-1).obs;
RxDouble distance = 0.0.obs;
int pointId = 1;
String pointIdPrefix = '';
String pointIdPostfix = '';
Rx<bool> pointMeasuringDirectionForward = true.obs;
// ── Geoid grid ────────────────────────────────────────────────────
late GeoidGrid geoidGrid;
// ── Fájlok ────────────────────────────────────────────────────────
late Directory? directory;
late File dataFile;
// ── Telefon GPS fallback ──────────────────────────────────────────
StreamSubscription<Position>? _phoneLocationSub;
StreamSubscription<void>? _gnssUpdateSub;
// ── UI controllerek ───────────────────────────────────────────────
final pointIdController = TextEditingController();
final pointDescriptionController = TextEditingController();
final gpsHeightController = TextEditingController();
final pointPrefixController = TextEditingController();
final pointPostfixController = TextEditingController();
late SharedPreferences prefs;
Rx<bool> isShowPassword = false.obs;
// ── Supabase ──────────────────────────────────────────────────────
RealtimeChannel? _supaChannel;
// ------- Map edit ----------------
final activeEditTool = MapEditTool.none.obs;
final editorPointCount = 0.obs;
final pointNotes = <Marker>[].obs;
final polylineNotes = <Polyline<int>>[].obs;
final polygonNotes = <Polygon<int>>[].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<int?>(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<void> _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<void> _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<NtripSettingsController>()) {
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<void> _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<int>(
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<void> _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<List> 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<LatLng>.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<NoteItem?> _saveItem({
required NoteType type,
required List<LatLng> 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<void> _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<void> _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<void> 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<void> _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;
activeEditTool.value = MapEditTool.none;
draftAreaSquareMeters.value = 0.0;
draftLengthMeters.value = 0.0;
}
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> _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<LatLng>.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<void> _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;
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<LatLng>.from(
polygonEditorController.points,
);
draftLengthMeters.value = GeometryMeasure.lineLengthMeters(points);
draftAreaSquareMeters.value = 0.0;
break;
case MapEditTool.polygon:
final points = List<LatLng>.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: 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,
);
}
}