1840 lines
61 KiB
Dart
1840 lines
61 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/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 'Koppints a térképre a pont helyéhez.';
|
|
case MapEditTool.line:
|
|
return 'Hosszan nyomj a térképre a töréspontokhoz';
|
|
case MapEditTool.polygon:
|
|
return 'Hosszan nyomj a térképre a sarokpontokhoz.';
|
|
case MapEditTool.none:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
bool get canFinishGeometry {
|
|
switch (activeEditTool.value) {
|
|
case MapEditTool.point:
|
|
return editorPointCount == 1;
|
|
case MapEditTool.line:
|
|
return editorPointCount >= 2;
|
|
case MapEditTool.polygon:
|
|
return editorPointCount >= 3;
|
|
case MapEditTool.none:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String get finishButtonText {
|
|
switch (activeEditTool.value) {
|
|
case MapEditTool.point:
|
|
return 'Kész';
|
|
case MapEditTool.line:
|
|
return 'Kész';
|
|
case MapEditTool.polygon:
|
|
return 'Lezárás';
|
|
case MapEditTool.none:
|
|
return 'Kész';
|
|
}
|
|
}
|
|
|
|
void startPointTool() {
|
|
activeEditTool.value = MapEditTool.point;
|
|
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));
|
|
}
|
|
}
|