From d50a324e44d8d2fcbe0be7c2fbcedebabdcd6461 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Sat, 16 May 2026 14:47:34 +0200 Subject: [PATCH] =?UTF-8?q?SharedMapWidget=20=C3=A9s=20t=C3=A9rk=C3=A9p=20?= =?UTF-8?q?n=C3=A9zet=20refraktor=C3=A1l=C3=A1sa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 2 + .../controllers/map_survey_controller.dart | 1447 +++++++---------- .../presentations/views/map_survey_view.dart | 9 +- .../presentations/views/settings_dialog.dart | 285 ++-- lib/pages/shell/bindings/shell_binding.dart | 8 +- .../shell/presentations/views/shell_view.dart | 10 +- .../gnss/bt_serial_gnss_connection.dart | 4 + lib/services/gnss/gnss_service.dart | 156 +- lib/services/ntrip_service.dart | 248 +++ lib/widgets/coordinate_panel.dart | 9 +- lib/widgets/shared_map_widgets.dart | 39 +- 11 files changed, 1167 insertions(+), 1050 deletions(-) create mode 100644 lib/services/ntrip_service.dart diff --git a/lib/main.dart b/lib/main.dart index 1d94d09..a5a3e4e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:terepi_seged/routes/app_pages.dart'; import 'package:terepi_seged/services/coord_converter_service.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'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -21,6 +22,7 @@ Future main() async { () => CoordConverterService().init()); Get.put(GnssDeviceService()); Get.put(GnssService()); + Get.put(NtripService()); runApp(const MyApp()); } diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index 9670e71..58f5a43 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math' as math; +import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:file_picker/file_picker.dart'; @@ -11,9 +11,10 @@ import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:flutter_map/flutter_map.dart'; // import 'package:flutter_map_geojson/flutter_map_geojson.dart'; import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:location/location.dart'; +// import 'package:location/location.dart'; import 'package:latlong2/latlong.dart'; import 'package:nmea/nmea.dart'; import 'package:path_provider/path_provider.dart'; @@ -21,6 +22,7 @@ import 'package:permission_handler/permission_handler.dart' as permission_handler; import 'package:share_plus/share_plus.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:terepi_seged/controls/geoid_grid.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/gnss_sentences/gngga.dart'; @@ -33,6 +35,11 @@ import 'package:proj4dart/proj4dart.dart' as proj4; import 'package:shared_preferences/shared_preferences.dart'; import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:terepi_seged/services/coord_converter_service.dart'; +import 'package:terepi_seged/services/gnss/gnss_connection.dart'; +import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; +import 'package:terepi_seged/services/gnss/gnss_service.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; enum MapSurveyMode { measure, // Bemérés — ahol vagyok, azt rögzítem @@ -40,51 +47,56 @@ enum MapSurveyMode { } class MapSurveyController extends GetxController { - // String gpsAddress = "E8:31:CD:14:8B:B2"; - // String gpsAddress = "98:CD:AC:62:FF:4E"; - RxString gpsAddress = "98:CD:AC:62:FF:36".obs; - RxString gpsName = "TiGNSS Rover-FF4E".obs; - // String gpsName = "TiGNSS Rover-8BB2"; - static const double maxZoomValue = 25.0; - Rx gpsIsConnected = false.obs; - RxBool ntripIsConnected = false.obs; - RxInt ntripDataPacketNumbers = 0.obs; - RxInt ggaSenDataPacketNumber = 0.obs; - RxString ggaSendLastTimeStr = "".obs; - String _messageBuffer = ""; - late BluetoothConnection connection; - late Socket socket; - late StreamSubscription socketStreamSubscription; - RxInt gpsReceivedData = 0.obs; - RxBool hasGpsValidData = false.obs; - RxBool isMapMoveToCenter = true.obs; - DateTime lastGpsRefreshTime = DateTime.now(); - String utcOfPositionFix = ""; - String utcDateOfPositionFix = ""; - RxInt ntripReceivedData = 0.obs; - final NmeaDecoder nmeaDecoder = NmeaDecoder(); - String lastGgaMessage = ''; - DateTime lastSendTimeGgaMessage = - DateTime.now().add(const Duration(seconds: -30)); + static MapSurveyController get to => Get.find(); - NumberFormat formatEov = NumberFormat("##0,000.0", "hu-HU"); - NumberFormat formatEovZ = NumberFormat("###0.0", "hu-HU"); - NumberFormat formatAltitudeError = NumberFormat("####0.000", "hu-HU"); - NumberFormat formatEovForFile = NumberFormat("#####0.0", "hu-HU"); - NumberFormat formatWgs84Sec = NumberFormat('00.000', 'hu-HU'); + // ── Függőségek (service-ek) ─────────────────────────────────────── + GnssService get _gnss => GnssService.to; + NtripService get _ntrip => NtripService.to; - RxDouble gpsLatitude = 0.0.obs; - RxString gpsLatitudeDirection = "".obs; - RxDouble gpsLongitude = 0.0.obs; - RxString gpsLongitudeDirection = "".obs; - RxDouble gpsAltitude = 0.0.obs; - RxDouble gpsGeoidSeparation = 0.0.obs; - RxInt gpsQuality = 0.obs; - RxDouble gpsLatitudeError = 0.0.obs; - RxDouble gpsLongitudeError = 0.0.obs; - RxDouble gpsAltitudeError = 0.0.obs; - Rx gpsDateTime = DateTime(2000).obs; + final mode = MapSurveyMode.measure.obs; + final targetName = ''.obs; + final targetEovX = 0.0.obs; + final targetEovY = 0.0.obs; + + double get deltaY => eov.value.Y - targetEovY.value; + double get deltaX => eov.value.X - targetEovX.value; + double get distanceToTarget => sqrt(deltaX * deltaX + deltaY * deltaY); + bool get isOnTarget => distanceToTarget < 0.05; + + // ── GPS állapot — rövidítések a GnssService-hez ────────────────── + // Ezeket a view-ban közvetlenül a service-ből is lehetne olvasni, + // de a controller-en keresztül is hozzáférhetők a meglévő view kódhoz. + RxDouble get gpsLatitude => _gnss.latitude; + RxDouble get gpsLongitude => _gnss.longitude; + RxDouble get gpsAltitude => _gnss.altitude; + RxDouble get gpsGeoidSeparation => _gnss.geoidSeparation; + RxInt get gpsQuality => _gnss.gpsQuality; + RxDouble get gpsLatitudeError => _gnss.latitudeError; + RxDouble get gpsLongitudeError => _gnss.longitudeError; + RxDouble get gpsAltitudeError => _gnss.altitudeError; + Rx get gpsDateTime => _gnss.gpsDateTime; + RxBool get gpsIsConnected => + (_gnss.connectionState.value == GnssConnectionState.connected).obs; + + // NTRIP állapot + RxBool get ntripIsConnected => _ntrip.isConnected; + RxInt get ntripDataPacketNumbers => _ntrip.packetCount; + RxInt get ggaSenDataPacketNumber => _ntrip.ggaSentCount; + RxString get ggaSendLastTimeStr => _ntrip.ggaLastSentTime; + RxInt get ntripReceivedData => _ntrip.receivedBytes; + + // ── Számformátumok ──────────────────────────────────────────────── + final formatEov = NumberFormat('##0,000.0', 'hu-HU'); + final formatEovZ = NumberFormat('###0.0', 'hu-HU'); + final formatAltitudeError = NumberFormat('####0.000', 'hu-HU'); + final formatEovForFile = NumberFormat('#####0.0', 'hu-HU'); + final formatWgs84Sec = NumberFormat('00.000', 'hu-HU'); + + // ── EOV koordináták (számítottak) ───────────────────────────────── Rx eov = Eov(0, 0).obs; + Rx eovHeight = (0.0 as double?).obs; + + // DMS formátum RxInt latDegree = 0.obs; RxInt latMin = 0.obs; RxDouble latSec = 0.0.obs; @@ -92,884 +104,673 @@ class MapSurveyController extends GetxController { 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; + late final MapController mapController; + final currentLocationMarker = []; final pointNotesMarker = []; - - List pointsToMeasure = []; final pointsToMeasureMarker = []; final pointsToMeasureLabel = []; + final pointsToMeasureDropDownMenuItem = >[]; - TextEditingController pointIdController = TextEditingController(); - TextEditingController pointDescriptionController = TextEditingController(); - TextEditingController gpsHeightController = TextEditingController(); - TextEditingController pointPrefixController = TextEditingController(); - TextEditingController pointPostfixController = TextEditingController(); - TextEditingController ntripUsernameController = TextEditingController(); - TextEditingController ntripPasswordController = TextEditingController(); + // ── Pont adatok ─────────────────────────────────────────────────── + List pointsToMeasure = []; + List pointWithDescriptionList = []; + RxInt pointsToMeasureSelectedValue = (-1).obs; + RxDouble distance = 0.0.obs; int pointId = 1; - String pointIdPrefix = ""; - String pointIdPostfix = ""; - RxString ntripUserName = "".obs; - RxString ntripPassword = "".obs; - + String pointIdPrefix = ''; + String pointIdPostfix = ''; Rx pointMeasuringDirectionForward = true.obs; - Rx isShowPassword = false.obs; - final passwordFieldFocusNode = FocusNode(); + // ── Geoid grid ──────────────────────────────────────────────────── + late GeoidGrid geoidGrid; - List? measuredPoints; - - List pointWithDescriptionList = []; + // ── Fájlok ──────────────────────────────────────────────────────── late Directory? directory; late File dataFile; - late proj4.Projection eovProj, wgsProj; - RxBool mapIsInitialized = false.obs; + // ── Telefon GPS fallback ────────────────────────────────────────── + StreamSubscription? _phoneLocationSub; - // GeoJsonParser parser = - // GeoJsonParser(defaultMarkerColor: const Color.fromARGB(255, 85, 34, 49)); - - final CollectionReference _measuredPoints = - FirebaseFirestore.instance.collection('measuredPoints'); - - // late SMIBool gpsTrigger; - // late StateMachineController riveGpsIconController; + // ── UI controllerek ─────────────────────────────────────────────── + final pointIdController = TextEditingController(); + final pointDescriptionController = TextEditingController(); + final gpsHeightController = TextEditingController(); + final pointPrefixController = TextEditingController(); + final pointPostfixController = TextEditingController(); late SharedPreferences prefs; + Rx isShowPassword = false.obs; - late AuthResponse authResponse; - late Session? session; - late User? user; + // ── Supabase ────────────────────────────────────────────────────── + RealtimeChannel? _supaChannel; - final mode = MapSurveyMode.measure.obs; - - // ── Közös adatok ────────────────────────────────────────────── - final currentEovY = 0.0.obs; - final currentEovX = 0.0.obs; - final currentEovZ = 0.0.obs; - final accuracy = 0.0.obs; - //final gpsQuality = 0.obs; - - // ── Kitűzés adatok — csak stakeout módban aktív ─────────────── - final targetEovY = 0.0.obs; - final targetEovX = 0.0.obs; - final targetName = ''.obs; - - // Eltérés — valós időben számolódik - double get deltaY => currentEovY.value - targetEovY.value; - double get deltaX => currentEovX.value - targetEovX.value; - double get distanceToTarget => math.sqrt(deltaY * deltaY + deltaX * deltaX); - - // Belül a célpont határán vagyunk-e (pl. 0.05 m) - bool get isOnTarget => distanceToTarget < 0.05; - - final measuredPoints1 = [].obs; - - static MapSurveyController get to => Get.find(); + // ───────────────────────────────────────────────────────────────── + // Lifecycle + // ───────────────────────────────────────────────────────────────── @override void onInit() async { super.onInit(); - final bytes = (await rootBundle.load('assets/Grids/etrs2eov_notowgs.gsb')) - .buffer - .asUint8List(); - proj4.Projection.nadgrid('Etrs2Eov', bytes); - // var def = - // '+proj=somerc +lat_0=47.14439372222222 +lon_0=19.04857177777778 +k_0=0.99993 +x_0=650000 +y_0=200000 +ellps=GRS67 +towgs84=52.17,-71.82,-14.9,0,0,0,0 +units=m +nadgrids=@ignorable,Etrs2Eov,null +no_defs'; - var def = - '+proj=somerc +lat_0=47.14439372222222 +lon_0=19.04857177777778 +k_0=0.99993 +x_0=650000 +y_0=200000 +ellps=GRS67 +towgs84=52.17,-71.82,-14.9,0,0,0,0 +units=m +nadgrids=Etrs2Eov +no_defs'; - -// Named Projection signature, later find it from anywhere via Projection.get('EPSG:23700') - eovProj = proj4.Projection.add('EPSG:23700', def); - wgsProj = proj4.Projection.WGS84; - //Loading, Success, Error handle with 1 line of code - nmeaDecoder - ..registerTalkerSentence("GGA", (line) => Gngga(raw: line)) - ..registerTalkerSentence("RMC", (line) => Gnrmc(raw: line)) - ..registerTalkerSentence("GST", (line) => Gngst(raw: line)); prefs = await SharedPreferences.getInstance(); - - authResponse = await Supabase.instance.client.auth - .signInWithPassword(email: 'test.elek.1@email.hu', password: 'demo'); - session = authResponse.session; - user = authResponse.user; - - Supabase.instance.client - .channel('public:TerepiSeged_Receiver') - .onPostgresChanges( - event: PostgresChangeEvent.update, - schema: 'public', - table: 'TerepiSeged_Receiver', - callback: (playload) { - var id = playload.newRecord['pointNumber'] as int; - print('Change received: ${id}'); - updatePointStatus(id); - }) - .subscribe(); - + geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx'); mapController = MapController(); - // riveGpsIconController = RiveUtils.getRiveController(Artboard(), - // stateMachineName: "gps_Interactivity"); - // gpsTrigger = riveGpsIconController.findSMI("active"); + // ── NTRIP RTCM adat → GNSS vevő ────────────────────────────── + NtripService.to.onRtcmData = (data) { + GnssService.to.sendToReceiver(data); + }; + + // ── GnssService pozíció változás → EOV, marker, NTRIP GGA ──── + ever(_gnss.gpsQuality, (_) => _onGnssUpdate()); + + // ── Supabase realtime ───────────────────────────────────────── + _supaChannel = Supabase.instance.client + .channel('public:TerepiSeged_Receiver') + .onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + table: 'TerepiSeged_Receiver', + callback: (payload) { + final id = payload.newRecord['pointNumber'] as int?; + if (id != null) updatePointStatus(id); + }, + ) + .subscribe(); mapIsInitialized.value = true; } - @override - void onClose() async { - super.onClose(); - FlutterBluetoothSerial.instance.setPairingRequestHandler(null); - if (gpsIsConnected.value) { - connection.close(); - // connection = null; - print("BluetoothTestController dispose ...."); - } - pointDescriptionController.dispose(); - pointIdController.dispose(); - gpsHeightController.dispose(); - pointPrefixController.dispose(); - pointPostfixController.dispose(); - ntripUsernameController.dispose(); - ntripPasswordController.dispose(); - - await Supabase.instance.client - .channel('public:TerepiSeged_Receiver') - .unsubscribe(); - } - @override void onReady() async { super.onReady(); - _getInitialLocation(); - - // String data = await rootBundle.loadString('assets/Files/transzekt.geojson'); - // parser.parseGeoJsonAsString(data); - - if (await permission_handler.Permission.storage.isGranted) { - print("Storage permission is ok ..."); - } else { - var result = await permission_handler.Permission.storage.request(); - if (result == permission_handler.PermissionStatus.granted) { - print("Storage permission request is ok ...."); - } else { - print("No storage permission"); - } - } - if (await permission_handler.Permission.manageExternalStorage.isGranted) { - print("External storage permission is ok ..."); - } else { - var result = - await permission_handler.Permission.manageExternalStorage.request(); - if (result == permission_handler.PermissionStatus.granted) { - print("External storage permission request is ok ...."); - } else { - print("No external storage permission"); - } - if (prefs.containsKey('gpsAddress')) { - var address = prefs.getString('gpsAddress'); - if (address != null) { - gpsAddress.value = address; - } - } - if (prefs.containsKey('gpsName')) { - var name = prefs.getString('gpsName'); - if (name != null) { - gpsName.value = name; - } - } - if (prefs.containsKey('ntripUserName')) { - var userName = prefs.getString('ntripUserName'); - if (userName != null) { - ntripUserName.value = userName; - } - } - if (prefs.containsKey('ntripPassword')) { - var password = prefs.getString('ntripPassword'); - if (password != null) { - ntripPassword.value = password; - } - } - } - - directory = await getExternalStorageDirectory(); - print(directory!.path); - // String newPath = ''; - // List folders = directory!.path.split("/"); - // for (int i = 1; i < folders.length; i++) { - // String folder = folders[i]; - // if (folder != "Android") { - // newPath += "/" + folder; - // } else { - // break; - // } - // } - // newPath = newPath + "/TerepisSegedApp"; - // directory = Directory(newPath); - if (!await directory!.exists()) { - await directory!.create(recursive: true); - } - dataFile = File("${directory!.path}/data.txt"); - - if (await directory!.exists()) { - if (!await dataFile.exists()) { - dataFile.writeAsString( - "Id;DateTime;Description;EovX;EovY;Latitude;Longitude;Altitude;Hor.Err;Vert.Err\r\n"); - } - } + await _initPhoneGps(); + await _initStorage(); gpsHeightController.text = '1.8'; } - void _getInitialLocation() async { - bool servicedEnabled; - PermissionStatus permissionGranted; - LocationData locationData; - Location location = Location(); + @override + void onClose() async { + super.onClose(); - servicedEnabled = await location.serviceEnabled(); - if (!servicedEnabled) { - servicedEnabled = await location.requestService(); - if (!servicedEnabled) { - return; - } + _phoneLocationSub?.cancel(); + await _supaChannel?.unsubscribe(); + + pointIdController.dispose(); + pointDescriptionController.dispose(); + gpsHeightController.dispose(); + pointPrefixController.dispose(); + pointPostfixController.dispose(); + } + + // ───────────────────────────────────────────────────────────────── + // 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ó + eov.value = ConvertCoordinate.ConvertWgsToEov(lat, lon); + 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), + ); } - permissionGranted = await location.hasPermission(); - if (permissionGranted == PermissionStatus.denied) { - permissionGranted = await location.requestPermission(); - if (permissionGranted != PermissionStatus.granted) { - return; - } - } - - locationData = await location.getLocation(); - currentLatitude.value = locationData.latitude ?? 0.0; - currentLongitude.value = locationData.longitude ?? 0.0; - print("Current location initialized -> $currentLatitude $currentLongitude"); - mapController.move(LatLng(currentLatitude.value, currentLongitude.value), - currentZoom.value); + // Térkép pozíció + currentLatitude.value = lat; + currentLongitude.value = lon; _updateCurrentLocationMarker(); - } - void connectToGps() { - if (gpsIsConnected == false) { - BluetoothConnection.toAddress(gpsAddress.value).then((value) { - connection = value; - gpsIsConnected.value = true; - print("GPS is connected ..."); - - connection.input!.listen(_onDataReceived); - }); + // Telefon GPS leállítása ha külső GPS van + if (_phoneLocationSub != null) { + _stopPhoneGps(); } - } - void disconnectFromGps() { - if (gpsIsConnected.value) { - connection.close(); - gpsIsConnected.value = false; - print("GPS is disconnected ...."); - } - if (ntripIsConnected.value) { - disconnectFromNtripServer(); - } - } - - void connectToNtripServer() async { - // socket = await Socket.connect(InternetAddress('3.23.52.207'), 2101, - // timeout: const Duration(seconds: 5)); - socket = await Socket.connect(InternetAddress('84.206.45.44'), 2101, - timeout: const Duration(seconds: 5)); - socket.asBroadcastStream; - ntripIsConnected.value = true; - - socket.encoding = ascii; - print("Connected to ntrip server ...."); - - // String header = "GET /KOVARIK HTTP/1.1\r\n"; - String header = "GET /SGO_RTK3.2 HTTP/1.1\r\n"; - - header += "User-Agent: SharpGps iter.dk\r\n"; - header += "Accept: */*\r\nConnection: close\r\n"; - // header += "Authorization: Basic ${_toBase64("info@mail.app-dev.hu:")}\r\n"; - //header += "Authorization: Basic ${_toBase64("elgi08:Laszl0stef1")}\r\n"; - header += - "Authorization: Basic ${_toBase64("${ntripUserName.value}:${ntripPassword.value}")}\r\n"; - // header += "Host:rtk2go.com:2101\r\n"; - header += "Host:gnssnet.hu:2101\r\n"; - header += "Ntrip-Vesrsion:Ntrip/2.0\r\n"; - header += "\r\n"; - - // listen for responses from the server - socketStreamSubscription = socket.listen( - // handle data from the server - (Uint8List data) { - // "ICY 200 OK" - first response - final serverResponse = String.fromCharCodes(data); - ntripReceivedData.value = data.length; - ntripDataPacketNumbers.value++; - if (gpsIsConnected.value) { - if (data.length > 14) { - connection.output.add(data); - } - } - print('Server: $ntripReceivedData'); - }, - - // handle errors - onError: (error) { - print(error); - socket.destroy(); - }, - - // handle server ending connection - onDone: () async { - print('Server left.'); - socketStreamSubscription.cancel(); - await socket.flush(); - ntripIsConnected.value = false; - ntripReceivedData.value = 0; - socket.destroy(); - }, + // NTRIP GGA küldés + NtripService.to.onGgaReceived( + _gnss.lastGgaLine.value, + _gnss.utcFix.value, ); - - socket.add(_convertStringToUint8List(header)); - - // sendGgaMessage(lastGgaMessage); } - void disconnectFromNtripServer() async { - socketStreamSubscription.cancel(); - await socket.flush(); - socket.close(); - socket.destroy(); - ntripReceivedData.value = 0; - ntripIsConnected.value = false; - print("Disconnect from ntrip server...."); - } + // ───────────────────────────────────────────────────────────────── + // Telefon GPS fallback + // ───────────────────────────────────────────────────────────────── - void _onDataReceived(Uint8List data) { - String sentence = ""; - - print("Bluetooth received -> ${data.length} byte(s)"); - String dataString = String.fromCharCodes(data); - int index = data.indexOf(13); - - if (~index != 0) { - sentence = _messageBuffer + dataString.substring(0, index); - _messageBuffer = dataString.substring(index); - } else { - _messageBuffer = _messageBuffer + dataString; - } - - // print("Message ($count): $sentence"); - _processGnssMessage(sentence); - } - - String _toBase64(String str) { - final bytes = ascii.encode(str); - final base64Str = base64.encode(bytes); - - return base64Str; - } - - Uint8List _convertStringToUint8List(String str) { - final List codeUnits = str.codeUnits; - final Uint8List unit8List = Uint8List.fromList(codeUnits); - return unit8List; - } - - void _processGnssMessage(String message) { - LineSplitter lineSplitter = const LineSplitter(); - List lines = lineSplitter.convert(message); - - if (lines.isEmpty) { - return; - } - - for (String line in lines) { - if (line.trim().isEmpty) { - continue; + Future _initPhoneGps() async { + // Egyszeri kezdő pozíció + try { + final last = await Geolocator.getLastKnownPosition(); + if (last != null) { + currentLatitude.value = last.latitude; + currentLongitude.value = last.longitude; + GnssService.to.latitude.value = last.latitude; + GnssService.to.longitude.value = last.longitude; + GnssService.to.gpsQuality.value = 1; + mapController.move( + LatLng(last.latitude, last.longitude), + currentZoom.value, + ); + _updateCurrentLocationMarker(); } - if (line.startsWith("\$GNGGA")) { - final sentence = nmeaDecoder.decode(line); - if (sentence!.valid && sentence is Gngga) { - hasGpsValidData.value = sentence.gpsQualityIndicator > 0; - if (hasGpsValidData.value) { - if (DateTime.now().difference(lastGpsRefreshTime).inSeconds >= 0) { - lastGpsRefreshTime = DateTime.now(); - utcOfPositionFix = sentence.utcOfPositionFix; - gpsLatitude.value = sentence.latitude; - gpsLatitudeDirection.value = sentence.latitudeDirection; - gpsLongitude.value = sentence.longitude; - gpsLongitudeDirection.value = sentence.longitudeDirection; - gpsAltitude.value = sentence.altitudeAboveMeanSeaLevel; - gpsGeoidSeparation.value = sentence.geoidSeparation; - gpsQuality.value = sentence.gpsQualityIndicator; - eov.value = ConvertCoordinate.ConvertWgsToEov( - gpsLatitude.value, gpsLongitude.value); + } catch (_) {} - latDegree.value = ConvertCoordinate.toDegree(gpsLatitude.value); - latMin.value = ConvertCoordinate.toMinute(gpsLatitude.value); - latSec.value = ConvertCoordinate.toSecond(gpsLatitude.value); - longDegree.value = ConvertCoordinate.toDegree(gpsLongitude.value); - longMin.value = ConvertCoordinate.toMinute(gpsLongitude.value); - longSec.value = ConvertCoordinate.toSecond(gpsLongitude.value); - currentLatitude.value = gpsLatitude.value; - currentLongitude.value = gpsLongitude.value; - _updateCurrentLocationMarker(); - lastGgaMessage = line; - if (ntripIsConnected.value && - (DateTime.now() - .difference(lastSendTimeGgaMessage) - .inSeconds >= - 5)) { - sendGgaMessage(lastGgaMessage); - print("Send GGA message: $lastGgaMessage"); - ggaSenDataPacketNumber.value++; - ggaSendLastTimeStr.value = utcOfPositionFix; - lastSendTimeGgaMessage = DateTime.now(); - } - } - } - } - } - if (line.startsWith("\$GNGST") && hasGpsValidData.value) { - final sentence = nmeaDecoder.decode(line); - if (sentence!.valid && sentence is Gngst) { - gpsLatitudeError.value = sentence.latitudeError; - gpsLongitudeError.value = sentence.longitudeError; - gpsAltitudeError.value = sentence.heightError; - } - } - if (line.startsWith("\$GNRMC") && hasGpsValidData.value) { - final sentence = nmeaDecoder.decode(line); - if (sentence!.valid && sentence is Gnrmc) { - utcDateOfPositionFix = sentence.date; - if (utcDateOfPositionFix.isNotEmpty && utcOfPositionFix.isNotEmpty) { - gpsDateTime.value = DateTime( - 2000 + - int.parse( - "${utcDateOfPositionFix[4]}${utcDateOfPositionFix[5]}"), - int.parse( - "${utcDateOfPositionFix[2]}${utcDateOfPositionFix[3]}"), - int.parse( - "${utcDateOfPositionFix[0]}${utcDateOfPositionFix[1]}"), - int.parse("${utcOfPositionFix[0]}${utcOfPositionFix[1]}"), - int.parse("${utcOfPositionFix[2]}${utcOfPositionFix[3]}"), - int.parse("${utcOfPositionFix[4]}${utcOfPositionFix[5]}")); - } - } - } + // Folyamatos stream ha nincs külső GPS + if (_gnss.connectionState.value != GnssConnectionState.connected) { + await _startPhoneGps(); } } - String getGpsQualityIndicator({required int quality}) { - String qualityStr = "Invalid"; - switch (quality) { - case 0: - { - qualityStr = "Invalid"; - } - break; - case 1: - { - qualityStr = "Standard GPS (2D/3D)"; - } - break; - case 2: - { - qualityStr = "Differential GPS"; - } - break; - case 4: - { - qualityStr = "RTK Fix"; - } - break; - case 5: - { - qualityStr = "RTK Float"; - } - break; - case 6: - { - qualityStr = "Estimated (DR) Fix"; - } - break; - default: - { - qualityStr = "Invalid (-1)"; - } - } - return qualityStr; + Future _startPhoneGps() async { + if (_phoneLocationSub != null) return; + + final permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) return; + + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return; + + _phoneLocationSub = Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + ), + ).listen((pos) { + // Leáll ha közben csatlakozott a külső GPS + if (_gnss.connectionState.value == GnssConnectionState.connected) { + _stopPhoneGps(); + return; + } + currentLatitude.value = pos.latitude; + currentLongitude.value = pos.longitude; + GnssService.to.latitude.value = pos.latitude; + GnssService.to.longitude.value = pos.longitude; + GnssService.to.gpsQuality.value = 1; + _updateCurrentLocationMarker(); + }); + } + + void _stopPhoneGps() { + _phoneLocationSub?.cancel(); + _phoneLocationSub = null; + GnssService.to.gpsQuality.value = 0; + } + + // ───────────────────────────────────────────────────────────────── + // Térkép vezérlők + // ───────────────────────────────────────────────────────────────── + + void mapZoomIn() { + if (currentZoom.value >= maxZoomValue) return; + currentZoom.value++; + _moveMap(); } void mapZoomOut() { - double cLat, cLong; - if (currentZoom.value > 0) { - currentZoom.value = currentZoom.value - 1; - cLat = isMapMoveToCenter.value - ? currentLatitude.value - : mapController.camera.center.latitude; - cLong = isMapMoveToCenter.value - ? currentLongitude.value - : mapController.camera.center.longitude; - mapController.move(LatLng(cLat, cLong), currentZoom.value); - } + if (currentZoom.value <= 0) return; + currentZoom.value--; + _moveMap(); } - void mapZoomIn() { - double cLat, cLong; - if (currentZoom.value < maxZoomValue) { - currentZoom.value++; - cLat = isMapMoveToCenter.value - ? currentLatitude.value - : mapController.camera.center.latitude; - cLong = isMapMoveToCenter.value - ? currentLongitude.value - : mapController.camera.center.longitude; - mapController.move(LatLng(cLat, cLong), currentZoom.value); - } + void setIsMapMoveToCenter() => + isMapMoveToCenter.value = !isMapMoveToCenter.value; + + void _moveMap() { + final lat = isMapMoveToCenter.value + ? currentLatitude.value + : mapController.camera.center.latitude; + final lon = isMapMoveToCenter.value + ? currentLongitude.value + : mapController.camera.center.longitude; + mapController.move(LatLng(lat, lon), currentZoom.value); } void _updateCurrentLocationMarker() { currentLocationMarker.clear(); currentLocationMarker.add(Marker( - point: LatLng(currentLatitude.value, currentLongitude.value), + point: LatLng(currentLatitude.value, currentLongitude.value), + width: 15.0, + height: 15.0, + child: Container( width: 15.0, height: 15.0, - child: Container( - width: 15.0, - height: 15.0, - decoration: BoxDecoration( - color: getCurrentLocationMarkerColor(quality: gpsQuality.value), - shape: BoxShape.circle, - border: Border.all(width: 1.5, color: Colors.white)), - ))); - if (isMapMoveToCenter.value) { - mapController.move(LatLng(currentLatitude.value, currentLongitude.value), - currentZoom.value); - } - } - - Color getCurrentLocationMarkerColor({required int quality}) { - Color qualityStr = Colors.black; - switch (quality) { - case 0: - { - qualityStr = Colors.black; - } - break; - case 1: - { - qualityStr = Colors.red; - } - break; - case 2: - { - qualityStr = Colors.blue; - } - break; - case 4: - { - qualityStr = Colors.green; - } - break; - case 5: - { - qualityStr = Colors.orange; - } - break; - case 6: - { - qualityStr = Colors.yellow; - } - break; - default: - { - qualityStr = Colors.white; - } - } - return qualityStr; - } - - void onBottomNavigationBarTap(int index) async { - bool noMorePoint = false; - print("OnBottomNavTap -> $index"); - if (index == 0) { - 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.0, - ), - TextField( - controller: pointDescriptionController, - decoration: const InputDecoration( - border: OutlineInputBorder(), labelText: 'Leírás'), - ), - const SizedBox( - height: 20, - ), - TextField( - controller: gpsHeightController, - autofocus: true, - 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.0, 40.0)), - child: const Text( - "Mégsem", - style: TextStyle(color: Colors.red), - ), - onPressed: () { - Get.back(); - }, - ), - OutlinedButton( - style: OutlinedButton.styleFrom( - minimumSize: const Size(120.0, 40.0)), - child: const Text( - "Ment", - style: TextStyle( - color: Colors.green, fontWeight: FontWeight.bold), - ), - onPressed: () async { - pointId = int.parse(pointIdController.text); - pointWithDescriptionList.add(PointWithDescription( - pointId, - gpsDateTime.value, - pointDescriptionController.text, - eov.value.Y, - eov.value.X, - gpsLatitude.value, - gpsLongitude.value, - math.max( - gpsLatitudeError.value, gpsLongitudeError.value), - gpsAltitudeError.value)); - print( - "pointWithDescriptionList -> ${pointWithDescriptionList.length}"); - - pointNotesMarker.add(Marker( - point: LatLng(gpsLatitude.value, gpsLongitude.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)), - ))); - await dataFile.writeAsString( - "$pointId;$gpsDateTime;${pointDescriptionController.text};${formatEovForFile.format(eov.value.Y)};${formatEovForFile.format(eov.value.X)};$gpsLatitude;$gpsLongitude;$gpsAltitude;${math.max(gpsLatitudeError.value, gpsLongitudeError.value)};$gpsAltitudeError;${gpsHeightController.text}\r\n", - mode: FileMode.append); - - _measuredPoints.add({ - "id": pointId, - "dateAndTime": gpsDateTime.value, - "latitude": gpsLatitude.value, - "longitude": gpsLongitude.value, - "altitude": gpsAltitude.value, - "eovY": formatEovForFile.format(eov.value.Y), - "eovX": formatEovForFile.format(eov.value.X), - "description": pointDescriptionController.text, - "horizontalError": math.max( - gpsLatitudeError.value, gpsLongitudeError.value), - "verticalError": gpsAltitudeError.value, - "gpsHeight": gpsHeightController.text - }); - - await Supabase.instance.client - .from('TerepiSeged_MeasuredPoints') - .insert({ - 'pointNumber': pointId, - 'gnssNumber': gpsName.value, - 'latitude': gpsLatitude.value, - 'longitude': gpsLongitude.value, - 'altitude': gpsAltitude.value, - 'heightOfGeoid': gpsGeoidSeparation.value, - 'eovX': eov.value.X, - 'eovY': eov.value.Y, - 'poleHeight': double.tryParse(gpsHeightController.text), - 'horizontalError': math.max( - gpsLatitudeError.value, gpsLongitudeError.value), - 'verticalError': gpsAltitudeError.value, - 'description': pointDescriptionController.text, - 'isDeleted': false, - 'projectId': 2 - }); - - await Supabase.instance.client - .from('TerepiSeged_Receiver') - .update({'isMeasured': true}).eq( - 'pointNumber', pointId); - - pointId++; - - Get.back(); - }, - ), - ], - ) - ], + decoration: BoxDecoration( + color: getCurrentLocationMarkerColor(_gnss.gpsQuality.value), + shape: BoxShape.circle, + border: Border.all(width: 1.5, color: Colors.white), ), - barrierDismissible: false, + ), + )); + if (isMapMoveToCenter.value) { + mapController.move( + LatLng(currentLatitude.value, currentLongitude.value), + currentZoom.value, ); } } - void sendGgaMessage(String ggaMessage) { - if (ntripIsConnected.value && ggaMessage.isNotEmpty) { - socket.add(_convertStringToUint8List("$ggaMessage\r\n")); + Color getCurrentLocationMarkerColor(int quality) => switch (quality) { + 0 => Colors.black, + 1 => Colors.red, + 2 => Colors.blue, + 4 => Colors.green, + 5 => Colors.orange, + 6 => Colors.yellow, + _ => Colors.white, + }; + + String getGpsQualityIndicator({required int quality}) => switch (quality) { + 0 => 'Invalid', + 1 => 'Standard GPS', + 2 => 'Differential GPS', + 4 => 'RTK Fix', + 5 => 'RTK Float', + 6 => 'Estimated (DR)', + _ => 'Invalid', + }; + + // ───────────────────────────────────────────────────────────────── + // Pont mentés dialóg + // ───────────────────────────────────────────────────────────────── + + void showAddPointDialog() => onBottomNavigationBarTap(0); + + void onBottomNavigationBarTap(int index) async { + if (index != 0) return; + + if (pointsToMeasureSelectedValue.value >= 0 && + pointsToMeasureSelectedValue.value < pointsToMeasure.length) { + pointIdController.text = + '${pointsToMeasure[pointsToMeasureSelectedValue.value].id}'; + } else { + pointIdController.text = pointId.toString(); + } + pointDescriptionController.text = ''; + + Get.dialog( + AlertDialog( + title: const Text('Pont rögzítése'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: pointIdController, + autofocus: true, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + border: OutlineInputBorder(), labelText: 'Azonosító'), + ), + const SizedBox(height: 20), + TextField( + controller: pointDescriptionController, + decoration: const InputDecoration( + border: OutlineInputBorder(), labelText: 'Leírás'), + ), + const SizedBox(height: 20), + TextField( + controller: gpsHeightController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + border: OutlineInputBorder(), labelText: 'GPS magasság'), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton( + style: + OutlinedButton.styleFrom(minimumSize: const Size(120, 40)), + onPressed: () => Get.back(), + child: + const Text('Mégsem', style: TextStyle(color: Colors.red)), + ), + OutlinedButton( + style: + OutlinedButton.styleFrom(minimumSize: const Size(120, 40)), + onPressed: _saveCurrentPoint, + child: const Text('Ment', + style: TextStyle( + color: Colors.green, fontWeight: FontWeight.bold)), + ), + ], + ), + ], + ), + barrierDismissible: false, + ); + } + + Future _saveCurrentPoint() async { + pointId = int.tryParse(pointIdController.text) ?? pointId; + + // Helyi lista + pointWithDescriptionList.add(PointWithDescription( + pointId, + _gnss.gpsDateTime.value, + pointDescriptionController.text, + eov.value.Y, + eov.value.X, + _gnss.latitude.value, + _gnss.longitude.value, + max(_gnss.latitudeError.value, _gnss.longitudeError.value), + _gnss.altitudeError.value, + )); + + // Marker hozzáadása + pointNotesMarker.add(Marker( + point: LatLng(_gnss.latitude.value, _gnss.longitude.value), + width: 15.0, + height: 15.0, + child: Container( + width: 15.0, + height: 15.0, + decoration: BoxDecoration( + color: Colors.amber[700], + shape: BoxShape.circle, + border: Border.all(width: 1.0, color: Colors.black), + ), + ), + )); + + // Fájl mentés + await dataFile.writeAsString( + '$pointId;${_gnss.gpsDateTime.value};' + '${pointDescriptionController.text};' + '${formatEovForFile.format(eov.value.Y)};' + '${formatEovForFile.format(eov.value.X)};' + '${_gnss.latitude.value};${_gnss.longitude.value};' + '${_gnss.altitude.value};' + '${max(_gnss.latitudeError.value, _gnss.longitudeError.value)};' + '${_gnss.altitudeError.value};' + '${gpsHeightController.text}\r\n', + mode: FileMode.append, + ); + + // Supabase mentés + await Supabase.instance.client.from('TerepiSeged_MeasuredPoints').insert({ + 'pointNumber': pointId, + 'gnssNumber': GnssDeviceService.to.selectedDevice.value?.name ?? '', + 'latitude': _gnss.latitude.value, + 'longitude': _gnss.longitude.value, + 'altitude': _gnss.altitude.value, + 'heightOfGeoid': _gnss.geoidSeparation.value, + 'eovX': eov.value.X, + 'eovY': eov.value.Y, + 'poleHeight': double.tryParse(gpsHeightController.text), + 'horizontalError': + max(_gnss.latitudeError.value, _gnss.longitudeError.value), + 'verticalError': _gnss.altitudeError.value, + 'description': pointDescriptionController.text, + 'isDeleted': false, + 'projectId': 2, + }); + + await Supabase.instance.client + .from('TerepiSeged_Receiver') + .update({'isMeasured': true}).eq('pointNumber', pointId); + + // Következő pont léptetése + _advancePointSelection(); + + Get.back(); + } + + void _advancePointSelection() { + final len = pointsToMeasure.length; + if (len == 0) return; + + if (pointMeasuringDirectionForward.isTrue) { + if (pointsToMeasureSelectedValue.value < len - 1) { + pointId++; + pointsToMeasureSelectedValue.value++; + } else { + _showNoMorePoints(); + } + } else { + if (pointsToMeasureSelectedValue.value > 0) { + pointId--; + pointsToMeasureSelectedValue.value--; + } else { + _showNoMorePoints(); + } } } - void setIsMapMoveToCenter() { - isMapMoveToCenter.value = !isMapMoveToCenter.value; - // onBottomNavigationBarTap(0); + void _showNoMorePoints() { + ScaffoldMessenger.of(Get.context!).showSnackBar( + const SnackBar(content: Text('Nincs több bemérendő pont.')), + ); } - void addMeasuredPoint() { - _measuredPoints.add({"id": 4001, "latitude": 46.3455, "longitude": 19.652}); + // ───────────────────────────────────────────────────────────────── + // 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 showAddPointDialog() { - onBottomNavigationBarTap(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 saveGpsAddress(String address) { - prefs.setString('gpsAddress', address); + void _clearPoints() { + pointsToMeasure.clear(); + pointsToMeasureMarker.clear(); + pointsToMeasureLabel.clear(); + pointsToMeasureDropDownMenuItem.clear(); } - void saveGpsName(String name) { - prefs.setString('gpsName', name); + 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 saveNtripUserName(String username) { - prefs.setString('ntripUserName', username); + void _addPoint({ + required int id, + required double coordX, + required double coordY, + required int listIndex, + }) { + pointsToMeasure.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY)); + + final wgs = CoordConverterService.to.eovToWgsPoint(coordX, coordY); + + pointsToMeasureMarker.add(Marker( + point: LatLng(wgs.y, wgs.x), + width: 15.0, + height: 15.0, + child: Container( + width: 15.0, + height: 15.0, + decoration: BoxDecoration( + color: Colors.purple, + shape: BoxShape.circle, + border: Border.all(width: 1.0, color: Colors.black), + ), + ), + )); + + pointsToMeasureLabel.add(PolyWidget( + center: LatLng(wgs.y + 0.0000075, wgs.x + 0.0000075), + widthInMeters: 3, + heightInMeters: 3, + child: FittedBox( + child: Padding( + padding: const EdgeInsets.all(4), + child: Text( + ' $id ', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.yellow, + fontSize: 12, + ), + ), + ), + ), + )); + + pointsToMeasureDropDownMenuItem.add(DropdownMenuItem( + value: listIndex, + child: Text('$id'), + )); } - void saveNtripPassword(String password) { - prefs.setString('ntripPassword', password); + void pointsToMeasureSelectedValueChanged(int value) => + pointsToMeasureSelectedValue.value = value; + + // ───────────────────────────────────────────────────────────────── + // Segédmetódusok + // ───────────────────────────────────────────────────────────────── + + double calculateDistance(LatLng start, LatLng end) { + const r = 6371.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 toggleShowPassword() { - isShowPassword.value = !isShowPassword.value; - if (passwordFieldFocusNode.hasPrimaryFocus) return; - passwordFieldFocusNode.canRequestFocus = false; + void showMeasuredPointsTableDialog() => + Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn); + + // ───────────────────────────────────────────────────────────────── + // Tárhely inicializálás + // ───────────────────────────────────────────────────────────────── + + Future _initStorage() async { + directory = await getExternalStorageDirectory(); + if (directory != null && !await directory!.exists()) { + await directory!.create(recursive: true); + } + dataFile = File('${directory!.path}/data.txt'); + if (!await dataFile.exists()) { + await dataFile.writeAsString( + 'Id;DateTime;Description;EovX;EovY;Latitude;Longitude;Altitude;Hor.Err;Vert.Err\r\n', + ); + } } - void showMeasuredPointsTableDialog() { - Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn); - } + // ───────────────────────────────────────────────────────────────── + // Export + // ───────────────────────────────────────────────────────────────── - Future readMeasuredPoints() async { - var response = await Supabase.instance.client - .from('TerepiSeged_MeasuredPoints') - .select() - .eq('projectId', 2) - .order('created_at'); - - print(response); - - return response; - } + Future readMeasuredPoints() async => Supabase.instance.client + .from('TerepiSeged_MeasuredPoints') + .select() + .eq('projectId', 2) + .order('created_at'); void SaveMeasuredPointsToFile() async { - // var pointsDirectory = await getExternalStorageDirectory(); - var pointsDirectory = await getApplicationDocumentsDirectory(); - print(directory!.path); - // String newPath = ''; - // List folders = directory!.path.split("/"); - // for (int i = 1; i < folders.length; i++) { - // String folder = folders[i]; - // if (folder != "Android") { - // newPath += "/" + folder; - // } else { - // break; - // } - // } - // newPath = newPath + "/TerepisSegedApp"; - // directory = Directory(newPath); - if (!await pointsDirectory!.exists()) { - await pointsDirectory.create(recursive: true); - } - var oldMeasuredPointsFile = File("${directory!.path}/measuredsPoints.csv"); + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/measuredsPoints.csv'); - if (await oldMeasuredPointsFile.exists()) { - await oldMeasuredPointsFile.delete(); + 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, + ); } - var measuredPointsFile = - await File("${directory!.path}/measuredsPoints.csv").create(); - - if (await pointsDirectory.exists()) { - if (await measuredPointsFile.exists()) { - await measuredPointsFile.writeAsString( - "Id;DateTime;Description;EovX;EovY;Altitude;Hor.Err;Vert.Err\r\n"); - } - } - - var data = await readMeasuredPoints(); - - data.forEach((d) { - measuredPointsFile.writeAsStringSync( - "${d['id']};${d['created_at']};${d['description']};${formatEov.format(d['eovY'])};${formatEov.format(d['eovX'])};${formatEovZ.format(d['altitude'] - d['poleHeight'])};${formatAltitudeError.format(d['horizontalError'])};${formatAltitudeError.format(d['verticalError'])}\r\n", - flush: true, - mode: FileMode.append, - encoding: utf8); - }); - - print('Number of data: ${data.length}'); - - final params = ShareParams( - text: "Mérési eredmények", - // files: [XFile('$measuredPointsFile')], - files: [XFile("${directory!.path}/measuredsPoints.csv")], - subject: 'Mérési eredmények', - title: 'Mérési eredmények'); - - final result = await SharePlus.instance.share(params); + 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) { - // Kitűzési célpont törlése targetName.value = ''; targetEovY.value = 0; targetEovX.value = 0; @@ -982,20 +783,4 @@ class MapSurveyController extends GetxController { targetEovX.value = x; mode.value = MapSurveyMode.stakeout; } - - Future savePoint(String name, String note) async { - // Mindkét módban ugyanaz a mentési logika - // await AppDatabase.instance.insertMeasuredPoint(MeasuredPoint( - // projectId: ProjectService.to.activeProjectId!, - // name: name, - // eovY: currentEovY.value, - // eovX: currentEovX.value, - // eovZ: currentEovZ.value, - // accuracy: accuracy.value, - // fixQuality: gpsQuality.value, - // note: note, - // timestamp: DateTime.now(), - // )); - // await FeedbackService.to.announcePointSaved(name); - } } diff --git a/lib/pages/map_survey/presentations/views/map_survey_view.dart b/lib/pages/map_survey/presentations/views/map_survey_view.dart index 92d74d7..1433e20 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -84,6 +84,9 @@ class _StakeoutPanel extends GetView { padding: const EdgeInsets.all(12), child: Obx(() { final onTarget = controller.isOnTarget; + final dy = controller.deltaY; + final dx = controller.deltaX; + final dist = controller.distanceToTarget; return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -108,17 +111,17 @@ class _StakeoutPanel extends GetView { children: [ _DeltaCell( label: 'ΔY', - value: controller.deltaY, + value: dy, unit: 'm', ), _DeltaCell( label: 'ΔX', - value: controller.deltaX, + value: dx, unit: 'm', ), _DeltaCell( label: 'Táv', - value: controller.distanceToTarget, + value: dist, unit: 'm', alwaysPositive: true, ), diff --git a/lib/pages/map_survey/presentations/views/settings_dialog.dart b/lib/pages/map_survey/presentations/views/settings_dialog.dart index 5754750..775c9fc 100644 --- a/lib/pages/map_survey/presentations/views/settings_dialog.dart +++ b/lib/pages/map_survey/presentations/views/settings_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; class SettingsDialog extends StatelessWidget { final controller = Get.find(); @@ -17,19 +18,19 @@ class SettingsDialog extends StatelessWidget { children: [ IconButton( onPressed: () { - if (controller.ntripUsernameController.text.isNotEmpty) { - controller.ntripUserName.value = - controller.ntripUsernameController.text; - controller.saveNtripUserName( - controller.ntripUsernameController.text); - if (controller - .ntripPasswordController.text.isNotEmpty) { - controller.ntripPassword.value = - controller.ntripPasswordController.text; - controller.saveNtripPassword( - controller.ntripPasswordController.text); - } - } + // if (controller.ntripUsernameController.text.isNotEmpty) { + // controller.ntripUserName.value = + // controller.ntripUsernameController.text; + // controller.saveNtripUserName( + // controller.ntripUsernameController.text); + // if (controller + // .ntripPasswordController.text.isNotEmpty) { + // controller.ntripPassword.value = + // controller.ntripPasswordController.text; + // controller.saveNtripPassword( + // controller.ntripPasswordController.text); + // } + // } Get.back(); }, icon: const Icon(Icons.close)), @@ -38,19 +39,19 @@ class SettingsDialog extends StatelessWidget { overlayColor: MaterialStateProperty.all(Colors.transparent)), onPressed: () { - if (controller.ntripUsernameController.text.isNotEmpty) { - controller.ntripUserName.value = - controller.ntripUsernameController.text; - controller.saveNtripUserName( - controller.ntripUsernameController.text); - if (controller - .ntripPasswordController.text.isNotEmpty) { - controller.ntripPassword.value = - controller.ntripPasswordController.text; - controller.saveNtripPassword( - controller.ntripPasswordController.text); - } - } + // if (controller.ntripUsernameController.text.isNotEmpty) { + // controller.ntripUserName.value = + // controller.ntripUsernameController.text; + // controller.saveNtripUserName( + // controller.ntripUsernameController.text); + // if (controller + // .ntripPasswordController.text.isNotEmpty) { + // controller.ntripPassword.value = + // controller.ntripPasswordController.text; + // controller.saveNtripPassword( + // controller.ntripPasswordController.text); + // } + // } Get.back(); }, child: const Text( @@ -74,118 +75,118 @@ class SettingsDialog extends StatelessWidget { style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500), ), ), - Obx(() => Column(children: [ - RadioListTile( - title: Text('TiGNSS Rover-BE6A'), - value: '10:06:1C:97:BE:6A', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-BE6A'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-BE6A'); - }), - RadioListTile( - title: Text('TiGNSS Rover-1DC6'), - value: 'E8:31:CD:16:1D:C6', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-1DC6'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-1DC6'); - }), - RadioListTile( - title: Text('TiGNSS Rover-9C3A'), - value: '08:3A:8D:14:9C:3A', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-9C3A'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-9C3A'); - }), - RadioListTile( - title: Text('TiGNSS Rover-72C2'), - value: '10:06:1C:97:72:C2', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-72C2'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-72C2'); - }), - RadioListTile( - title: Text('TiGNSS Rover-FE16'), - value: '10:06:1C:9F:FE:16', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-FE16'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-FE16'); - }), - RadioListTile( - title: Text('TiGNSS Rover-3B0A'), - value: '10:C6:1C:9E:3B:0A', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-3B0A'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-3B0A'); - }), - RadioListTile( - title: Text('TiGNSS Rover-7FEA'), - value: '10:06:1C:9C:7F:EA', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-7FEA'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-7FEA'); - }), - RadioListTile( - title: Text('TiGNSS Rover-A39E'), - value: '10:06:1C:97:A3:9E', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-A39E'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-A39E'); - }), - RadioListTile( - title: Text('TiGNSS Rover-FF4E'), - value: '98:CD:AC:62:FF:4E', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-FF4E'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-FF4E'); - }), - RadioListTile( - title: Text('TiGNSS Rover-8BB2'), - value: 'E8:31:CD:14:8B:B2', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-8BB2'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-8BB2'); - }), - RadioListTile( - title: Text('TiGNSS Rover-FF36'), - value: '98:CD:AC:62:FF:36', - groupValue: controller.gpsAddress.value, - onChanged: (value) { - controller.gpsAddress.value = value!; - controller.gpsName.value = 'TiGNSS Rover-FF36'; - controller.saveGpsAddress(value); - controller.saveGpsName('TiGNSS Rover-FF36'); - }) - ])), + // Obx(() => Column(children: [ + // RadioListTile( + // title: Text('TiGNSS Rover-BE6A'), + // value: '10:06:1C:97:BE:6A', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-BE6A'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-BE6A'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-1DC6'), + // value: 'E8:31:CD:16:1D:C6', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-1DC6'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-1DC6'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-9C3A'), + // value: '08:3A:8D:14:9C:3A', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-9C3A'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-9C3A'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-72C2'), + // value: '10:06:1C:97:72:C2', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-72C2'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-72C2'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-FE16'), + // value: '10:06:1C:9F:FE:16', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-FE16'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-FE16'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-3B0A'), + // value: '10:C6:1C:9E:3B:0A', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-3B0A'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-3B0A'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-7FEA'), + // value: '10:06:1C:9C:7F:EA', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-7FEA'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-7FEA'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-A39E'), + // value: '10:06:1C:97:A3:9E', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-A39E'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-A39E'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-FF4E'), + // value: '98:CD:AC:62:FF:4E', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-FF4E'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-FF4E'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-8BB2'), + // value: 'E8:31:CD:14:8B:B2', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-8BB2'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-8BB2'); + // }), + // RadioListTile( + // title: Text('TiGNSS Rover-FF36'), + // value: '98:CD:AC:62:FF:36', + // groupValue: controller.gpsAddress.value, + // onChanged: (value) { + // controller.gpsAddress.value = value!; + // controller.gpsName.value = 'TiGNSS Rover-FF36'; + // controller.saveGpsAddress(value); + // controller.saveGpsName('TiGNSS Rover-FF36'); + // }) + // ])), const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: Divider( @@ -329,7 +330,7 @@ class SettingsDialog extends StatelessWidget { SizedBox( height: 40, child: TextField( - controller: controller.ntripUsernameController, + controller: NtripService.to.usernameController, enableSuggestions: false, autocorrect: false, decoration: InputDecoration( @@ -351,10 +352,10 @@ class SettingsDialog extends StatelessWidget { child: TextField( keyboardType: TextInputType.visiblePassword, obscureText: controller.isShowPassword.value, - focusNode: controller.passwordFieldFocusNode, + //focusNode: controller.passwordFieldFocusNode, enableSuggestions: false, autocorrect: false, - controller: controller.ntripPasswordController, + controller: NtripService.to.passwordController, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never, isDense: true, @@ -371,7 +372,7 @@ class SettingsDialog extends StatelessWidget { suffixIcon: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 4, 0), child: GestureDetector( - onTap: controller.toggleShowPassword, + //onTap: controller.toggleShowPassword, child: Icon( controller.isShowPassword.value ? Icons.visibility_rounded diff --git a/lib/pages/shell/bindings/shell_binding.dart b/lib/pages/shell/bindings/shell_binding.dart index 59f55a8..1bc5bfa 100644 --- a/lib/pages/shell/bindings/shell_binding.dart +++ b/lib/pages/shell/bindings/shell_binding.dart @@ -11,7 +11,11 @@ class ShellBinding extends Bindings { // TODO: implement dependencies Get.put(ShellController()); Get.put(HomeViewController()); - Get.put(MapViewController()); - Get.put(MapSurveyController()); + // Get.put(MapViewController()); + // Get.put(MapSurveyController()); + Get.lazyPut( + () => MapSurveyController(), + fenix: true, + ); } } diff --git a/lib/pages/shell/presentations/views/shell_view.dart b/lib/pages/shell/presentations/views/shell_view.dart index e12eead..07e1f93 100644 --- a/lib/pages/shell/presentations/views/shell_view.dart +++ b/lib/pages/shell/presentations/views/shell_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:terepi_seged/pages/home/presentation/views/home_view.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/widgets/gnss_status_chip.dart'; import '../../../../widgets/app_drawer.dart'; @@ -30,12 +31,11 @@ class ShellView extends GetView { const SizedBox(width: 6), Obx(() => controller.currentIndex.value == 1 ? NtripStatusChip( - isConnected: MapSurveyController.to.ntripIsConnected, + isConnected: NtripService.to.isConnected, onToggle: () { - final c = MapSurveyController.to; - c.ntripIsConnected.value - ? c.disconnectFromNtripServer() - : c.connectToNtripServer(); + NtripService.to.isConnected.value + ? NtripService.to.disconnect() + : NtripService.to.connect(); }, ) : const SizedBox.shrink()) diff --git a/lib/services/gnss/bt_serial_gnss_connection.dart b/lib/services/gnss/bt_serial_gnss_connection.dart index bbd8980..b569f19 100644 --- a/lib/services/gnss/bt_serial_gnss_connection.dart +++ b/lib/services/gnss/bt_serial_gnss_connection.dart @@ -92,4 +92,8 @@ class BtSerialGnssConnection implements GnssConnection { _nmeaController.close(); _stateController.close(); } + + void sendData(Uint8List data) { + _connection?.output.add(data); + } } diff --git a/lib/services/gnss/gnss_service.dart b/lib/services/gnss/gnss_service.dart index 7e2fd54..95006ef 100644 --- a/lib/services/gnss/gnss_service.dart +++ b/lib/services/gnss/gnss_service.dart @@ -1,43 +1,68 @@ // lib/services/gnss/gnss_service.dart import 'dart:async'; +import 'dart:typed_data'; + import 'package:get/get.dart'; import 'package:nmea/nmea.dart'; +import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; + import '../../gnss_sentences/gngga.dart'; -import 'gnss_connection.dart'; +import '../../gnss_sentences/gngst.dart'; +import '../../gnss_sentences/gnrmc.dart'; import 'bt_serial_gnss_connection.dart'; import 'ble_gnss_connection.dart'; -import 'gnss_device_service.dart'; +import 'gnss_connection.dart'; class GnssService extends GetxService { static GnssService get to => Get.find(); GnssConnection? _connection; - // Reaktív állapot — a controllerek Obx-szel figyelhetik + // ── Kapcsolat állapot ───────────────────────────────────────────── final connectionState = GnssConnectionState.disconnected.obs; final activeConnectionType = Rxn(); - // Parsed NMEA adatok + // ── GGA adatok ──────────────────────────────────────────────────── final latitude = 0.0.obs; final longitude = 0.0.obs; - final altitude = 0.0.obs; - final geoidSeparation = 0.0.obs; + final altitude = 0.0.obs; // MSL (ortometrikus) + final geoidSeparation = 0.0.obs; // N — geoid undulációja final gpsQuality = 0.obs; final utcFix = ''.obs; final satelliteCount = 0.obs; final hdop = 0.0.obs; + // Utolsó nyers GGA sor — NtripService küldi vissza a casternek + final lastGgaLine = ''.obs; + + // ── GST adatok (pontossági hibák) ───────────────────────────────── + final latitudeError = 0.0.obs; + final longitudeError = 0.0.obs; + final altitudeError = 0.0.obs; + + // ── RMC adatok (dátum/idő) ──────────────────────────────────────── + final gpsDateTime = DateTime(2000).obs; + + // Segédmező: van-e érvényes adat + bool get hasValidData => gpsQuality.value > 0; + + // ── Belső ───────────────────────────────────────────────────────── final NmeaDecoder _decoder = NmeaDecoder(); StreamSubscription? _nmeaSub; StreamSubscription? _stateSub; + String _utcTime = ''; + String _utcDate = ''; @override void onInit() { super.onInit(); - _decoder.registerTalkerSentence('GGA', (l) => Gngga(raw: l)); + _decoder + ..registerTalkerSentence('GGA', (l) => Gngga(raw: l)) + ..registerTalkerSentence('GST', (l) => Gngst(raw: l)) + ..registerTalkerSentence('RMC', (l) => Gnrmc(raw: l)); } - // ── Kapcsolódás ────────────────────────────────────────────────── + // ── Kapcsolódás ─────────────────────────────────────────────────── Future connectBtSerial(String macAddress) async { await _disconnect(); @@ -60,6 +85,27 @@ class GnssService extends GetxService { await _doConnect(deviceId); } + /// Eszközváltás — GnssDevicePicker hívja. + Future onDeviceChanged(GnssDevice device) async { + switch (device.type) { + case GnssConnectionType.btSerial: + await connectBtSerial(device.address); + case GnssConnectionType.ble: + await connectBle(device.address); + case GnssConnectionType.phoneGps: + await _disconnect(); + connectionState.value = GnssConnectionState.disconnected; + activeConnectionType.value = GnssConnectionType.phoneGps; + } + } + + Future reconnect() async { + final device = GnssDeviceService.to.selectedDevice.value; + if (device == null) return; + await _disconnect(); + await onDeviceChanged(device); + } + Future _doConnect(String address) async { _stateSub = _connection!.connectionState.listen((s) { connectionState.value = s; @@ -74,26 +120,74 @@ class GnssService extends GetxService { await _connection?.disconnect(); _connection?.dispose(); _connection = null; + connectionState.value = GnssConnectionState.disconnected; } - // ── NMEA parsing — egy helyen, nem háromban ────────────────────── + Future disconnect() => _disconnect(); + + /// RTCM adat továbbítása a GNSS vevőnek (NtripService hívja). + void sendToReceiver(Uint8List data) { + if (_connection == null) return; + if (connectionState.value != GnssConnectionState.connected) return; + (_connection as BtSerialGnssConnection?)?.sendData(data); + } + + // ── NMEA parsing ────────────────────────────────────────────────── void _parseNmea(String line) { - if (!line.startsWith('\$GNGGA') && !line.startsWith('\$GPGGA')) return; + if (line.startsWith('\$GNGGA') || line.startsWith('\$GPGGA')) { + _parseGga(line); + } else if (line.startsWith('\$GNGST') && hasValidData) { + _parseGst(line); + } else if (line.startsWith('\$GNRMC') && hasValidData) { + _parseRmc(line); + } + } + void _parseGga(String line) { try { - final sentence = _decoder.decode(line); - if (sentence == null || !sentence.valid || sentence is! Gngga) return; - if (sentence.gpsQualityIndicator == 0) return; + final s = _decoder.decode(line); + if (s == null || !s.valid || s is! Gngga) return; + if (s.gpsQualityIndicator == 0) return; - latitude.value = sentence.latitude; - longitude.value = sentence.longitude; - altitude.value = sentence.altitudeAboveMeanSeaLevel; - geoidSeparation.value = sentence.geoidSeparation; - gpsQuality.value = sentence.gpsQualityIndicator; - utcFix.value = sentence.utcOfPositionFix; - satelliteCount.value = sentence.numberOfSvsInUse; - hdop.value = sentence.hdop; + latitude.value = s.latitude; + longitude.value = s.longitude; + altitude.value = s.altitudeAboveMeanSeaLevel; + geoidSeparation.value = s.geoidSeparation; + gpsQuality.value = s.gpsQualityIndicator; + utcFix.value = s.utcOfPositionFix; + satelliteCount.value = s.numberOfSvsInUse; + hdop.value = s.hdop; + lastGgaLine.value = line; + _utcTime = s.utcOfPositionFix; + } catch (_) {} + } + + void _parseGst(String line) { + try { + final s = _decoder.decode(line); + if (s == null || !s.valid || s is! Gngst) return; + latitudeError.value = s.latitudeError; + longitudeError.value = s.longitudeError; + altitudeError.value = s.heightError; + } catch (_) {} + } + + void _parseRmc(String line) { + try { + final s = _decoder.decode(line); + if (s == null || !s.valid || s is! Gnrmc) return; + _utcDate = s.date; + if (_utcDate.length >= 6 && _utcTime.length >= 6) { + gpsDateTime.value = DateTime( + 2000 + int.parse('${_utcDate[4]}${_utcDate[5]}'), + int.parse('${_utcDate[2]}${_utcDate[3]}'), + int.parse('${_utcDate[0]}${_utcDate[1]}'), + int.parse('${_utcTime[0]}${_utcTime[1]}'), + int.parse('${_utcTime[2]}${_utcTime[3]}'), + int.parse('${_utcTime[4]}${_utcTime[5]}'), + ); + } } catch (_) {} } @@ -102,24 +196,4 @@ class GnssService extends GetxService { _disconnect(); super.onClose(); } - -// ── Eszközváltás — a GnssDevicePickerDialog hívja ─────────────── - - Future onDeviceChanged(GnssDevice device) async { - switch (device.type) { - case GnssConnectionType.btSerial: - await connectBtSerial(device.address); - - case GnssConnectionType.ble: - await connectBle(device.address); - - case GnssConnectionType.phoneGps: - // Külső GPS kapcsolat lezárása ha volt - await _disconnect(); - connectionState.value = GnssConnectionState.disconnected; - activeConnectionType.value = GnssConnectionType.phoneGps; - // A telefon GPS kezelése a map_controller _startPhoneGps()-ben van - // itt csak jelezzük hogy phone módra váltottunk - } - } } diff --git a/lib/services/ntrip_service.dart b/lib/services/ntrip_service.dart new file mode 100644 index 0000000..68f4ad0 --- /dev/null +++ b/lib/services/ntrip_service.dart @@ -0,0 +1,248 @@ +// lib/services/ntrip_service.dart + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// NTRIP kapcsolatot kezelő singleton service. +/// +/// Felelőssége: +/// - Socket kapcsolat az NTRIP casterhez +/// - RTCM adatok fogadása és továbbítása a GNSS vevőnek +/// - GGA mondatok küldése a casternek (5 másodpercenként) +/// - Beállítások tárolása SharedPreferences-ben +/// +/// Használat: +/// ```dart +/// // Csatlakozás előtt add meg a callback-et: +/// NtripService.to.onRtcmData = (data) => connection.output.add(data); +/// await NtripService.to.connect(); +/// ``` +class NtripService extends GetxService { + static NtripService get to => Get.find(); + + // ── Reaktív állapot ─────────────────────────────────────────────── + final isConnected = false.obs; + final receivedBytes = 0.obs; + final packetCount = 0.obs; + final ggaSentCount = 0.obs; + final ggaLastSentTime = ''.obs; + + // ── Beállítások ─────────────────────────────────────────────────── + final host = '84.206.45.44'.obs; // gnssnet.hu IP + final port = 2101.obs; + final mountpoint = 'SGO_RTK3.2'.obs; + final username = ''.obs; + final password = ''.obs; + + // ── UI controllerek (beállítás dialóghoz) ──────────────────────── + final hostController = TextEditingController(); + final portController = TextEditingController(); + final mountpointController = TextEditingController(); + final usernameController = TextEditingController(); + final passwordController = TextEditingController(); + + // ── Belső állapot ──────────────────────────────────────────────── + Socket? _socket; + StreamSubscription? _socketSub; + String _lastGgaMessage = ''; + DateTime _lastGgaSentTime = + DateTime.now().subtract(const Duration(seconds: 30)); + + /// Callback: RTCM adat érkezett → a controller továbbítja a GNSS vevőnek. + /// Beállítás: `NtripService.to.onRtcmData = (data) => connection.output.add(data);` + Function(Uint8List)? onRtcmData; + + // ── Inicializálás ──────────────────────────────────────────────── + + @override + Future onInit() async { + super.onInit(); + await _loadSettings(); + _syncControllersFromValues(); + } + + @override + void onClose() { + disconnect(); + hostController.dispose(); + portController.dispose(); + mountpointController.dispose(); + usernameController.dispose(); + passwordController.dispose(); + super.onClose(); + } + + // ── Kapcsolat ──────────────────────────────────────────────────── + + Future connect() async { + if (isConnected.value) return; + + try { + _socket = await Socket.connect( + InternetAddress(host.value), + port.value, + timeout: const Duration(seconds: 5), + ); + + _socket!.encoding = ascii; + isConnected.value = true; + receivedBytes.value = 0; + packetCount.value = 0; + + // HTTP fejléc összeállítása + final header = _buildNtripHeader(); + _socket!.add(_toUint8List(header)); + + // Adatfogadás + _socketSub = _socket!.listen( + _onData, + onError: _onError, + onDone: _onDone, + ); + } catch (e) { + isConnected.value = false; + Get.snackbar( + 'NTRIP hiba', + 'Nem sikerült csatlakozni: $e', + backgroundColor: const Color(0xFFB71C1C), + colorText: const Color(0xFFFFFFFF), + ); + } + } + + Future disconnect() async { + if (!isConnected.value && _socket == null) return; + await _socketSub?.cancel(); + await _socket?.flush(); + _socket?.close(); + _socket?.destroy(); + _socket = null; + isConnected.value = false; + receivedBytes.value = 0; + } + + void reconnect() async { + await disconnect(); + await Future.delayed(const Duration(seconds: 1)); + await connect(); + } + + // ── GGA küldés ─────────────────────────────────────────────────── + + /// Az NMEA feldolgozó hívja minden GGA mondatnál. + /// 5 másodpercenként küld egyet az NTRIP casternek. + void onGgaReceived(String ggaLine, String utcTime) { + _lastGgaMessage = ggaLine; + + if (!isConnected.value) return; + if (ggaLine.isEmpty) return; + + final elapsed = DateTime.now().difference(_lastGgaSentTime).inSeconds; + if (elapsed < 5) return; + + _sendGga(ggaLine); + ggaSentCount.value++; + ggaLastSentTime.value = utcTime; + _lastGgaSentTime = DateTime.now(); + } + + void _sendGga(String ggaMessage) { + if (_socket == null || !isConnected.value) return; + _socket!.add(_toUint8List('$ggaMessage\r\n')); + } + + // ── Belső adatfogadás ──────────────────────────────────────────── + + void _onData(Uint8List data) { + receivedBytes.value = data.length; + packetCount.value++; + + // Csak RTCM adat (>14 byte) kerül a GNSS vevőhöz + if (data.length > 14) { + onRtcmData?.call(data); + } + } + + void _onError(dynamic error) { + _socket?.destroy(); + isConnected.value = false; + Get.snackbar( + 'NTRIP kapcsolat hiba', + error.toString(), + backgroundColor: const Color(0xFFB71C1C), + colorText: const Color(0xFFFFFFFF), + ); + } + + void _onDone() async { + await _socketSub?.cancel(); + await _socket?.flush(); + _socket?.destroy(); + _socket = null; + isConnected.value = false; + receivedBytes.value = 0; + } + + // ── HTTP fejléc összeállítás ───────────────────────────────────── + + String _buildNtripHeader() { + final auth = _toBase64('${username.value}:${password.value}'); + final host = '${this.host.value}:${port.value}'; + + return 'GET /${mountpoint.value} HTTP/1.1\r\n' + 'User-Agent: SharpGps iter.dk\r\n' + 'Accept: */*\r\n' + 'Connection: close\r\n' + 'Authorization: Basic $auth\r\n' + 'Host:$host\r\n' + 'Ntrip-Version:Ntrip/2.0\r\n' + '\r\n'; + } + + // ── Beállítások mentése / betöltése ────────────────────────────── + + Future saveSettings() async { + // Szinkronizálás a controllerektől az Rx értékekbe + host.value = hostController.text.trim(); + port.value = int.tryParse(portController.text) ?? 2101; + mountpoint.value = mountpointController.text.trim(); + username.value = usernameController.text.trim(); + password.value = passwordController.text; + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('ntrip_host', host.value); + await prefs.setInt('ntrip_port', port.value); + await prefs.setString('ntrip_mountpoint', mountpoint.value); + await prefs.setString('ntrip_username', username.value); + await prefs.setString('ntrip_password', password.value); + } + + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + host.value = prefs.getString('ntrip_host') ?? '84.206.45.44'; + port.value = prefs.getInt('ntrip_port') ?? 2101; + mountpoint.value = prefs.getString('ntrip_mountpoint') ?? 'SGO_RTK3.2'; + username.value = prefs.getString('ntrip_username') ?? ''; + password.value = prefs.getString('ntrip_password') ?? ''; + } + + void _syncControllersFromValues() { + hostController.text = host.value; + portController.text = port.value.toString(); + mountpointController.text = mountpoint.value; + usernameController.text = username.value; + // Jelszót nem pre-töltjük biztonsági okokból + } + + // ── Segédfüggvények ────────────────────────────────────────────── + + String _toBase64(String str) => base64.encode(ascii.encode(str)); + + Uint8List _toUint8List(String str) => Uint8List.fromList(str.codeUnits); +} diff --git a/lib/widgets/coordinate_panel.dart b/lib/widgets/coordinate_panel.dart index b7761b6..3484b89 100644 --- a/lib/widgets/coordinate_panel.dart +++ b/lib/widgets/coordinate_panel.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; import '../services/gnss/gnss_service.dart'; @@ -49,10 +50,10 @@ class CoordinatePanel extends StatelessWidget { vertError: ctrl.gpsAltitudeError as RxDouble, altitudeMsl: ctrl.gpsAltitude as RxDouble, geoidSeparation: ctrl.gpsGeoidSeparation as RxDouble, - ntripConnected: ctrl.ntripIsConnected as RxBool, - ntripBytes: ctrl.ntripReceivedData as RxInt, - ntripPackets: ctrl.ntripDataPacketNumbers as RxInt, - ggaPackets: ctrl.ggaSenDataPacketNumber as RxInt, + ntripConnected: NtripService.to.isConnected, + ntripBytes: NtripService.to.receivedBytes, + ntripPackets: NtripService.to.packetCount, + ggaPackets: NtripService.to.ggaSentCount, ); } diff --git a/lib/widgets/shared_map_widgets.dart b/lib/widgets/shared_map_widgets.dart index 46912fe..33cfd15 100644 --- a/lib/widgets/shared_map_widgets.dart +++ b/lib/widgets/shared_map_widgets.dart @@ -1,5 +1,6 @@ // lib/widgets/shared_map_widget.dart import 'dart:math' as math; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; @@ -139,13 +140,17 @@ class _SharedMapWidgetState extends State { // 2. Extra rétegek (terepbejárás elemei) ...widget.extraLayers, // 3. Bemért pontok - if (Get.isRegistered()) - Obx(() => MarkerLayer( - markers: _buildMeasuredPointMarkers(), - )), + // if (Get.isRegistered()) + // Obx(() => MarkerLayer( + // markers: _buildMeasuredPointMarkers(), + // )), // 4. Kitűzési célpont + vonal if (Get.isRegistered()) - Obx(() => _buildStakeoutLayer(lat, lon)), + Obx(() { + final lat = GnssService.to.latitude.value; + final lon = GnssService.to.longitude.value; + return _buildStakeoutLayer(lat, lon); + }), // 5. GPS pozíció Obx(() { final lat = GnssService.to.latitude.value; @@ -187,19 +192,7 @@ class _SharedMapWidgetState extends State { List _buildMeasuredPointMarkers() { if (!Get.isRegistered()) return []; - return MapSurveyController.to.measuredPoints1 - .map((point) => Marker( - point: LatLng(point.latitude, point.longitude), - width: 100, - height: 56, - alignment: Alignment.bottomCenter, - child: _LabeledMarker( - label: point.name, - icon: Icons.location_on, - color: Colors.blue, - ), - )) - .toList(); + return MapSurveyController.to.pointNotesMarker; } Widget _buildStakeoutLayer(double lat, double lon) { @@ -214,6 +207,10 @@ class _SharedMapWidgetState extends State { final wgs = CoordConverterService.to .eovToWgsPoint(ctrl.targetEovY.value, ctrl.targetEovX.value); final targetLatLng = LatLng(wgs.y, wgs.x); + final dx = ctrl.eov.value.X - ctrl.targetEovX.value; + final dy = ctrl.eov.value.Y - ctrl.targetEovY.value; + final dist = sqrt(dx * dx + dy * dy); + final onTarget = dist < 0.05; return Stack(children: [ // Szaggatott vonal @@ -235,10 +232,8 @@ class _SharedMapWidgetState extends State { label: ctrl.targetName.value, icon: Icons.flag, color: Colors.orange, - activeColor: ctrl.isOnTarget ? Colors.green : null, - sublabel: ctrl.isOnTarget - ? '✓ Célponton' - : '${ctrl.distanceToTarget.toStringAsFixed(3)} m', + activeColor: onTarget ? Colors.green : null, + sublabel: onTarget ? '✓ Célponton' : '${dist.toStringAsFixed(3)} m', ), ), ]),