From 4660873350dc5a924b2c52cbb8e05e98251b3f0a Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Wed, 15 Jul 2026 04:21:30 +0200 Subject: [PATCH] =?UTF-8?q?Navig=C3=A1ci=C3=B3s=20t=C3=A9rk=C3=A9p=20?= =?UTF-8?q?=C3=A9s=20=C3=BAtvoanlr=C3=B6gz=C3=ADt=C3=A9s=20hib=C3=A1inak?= =?UTF-8?q?=20jav=C3=ADt=C3=A1sa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/vibro_nav_controller.dart | 33 ++- .../presentation/views/vibro_nav_view.dart | 214 ++++++++++++++---- lib/routes/app_pages.dart | 3 + lib/services/layer_import_service.dart | 74 +++++- 4 files changed, 276 insertions(+), 48 deletions(-) diff --git a/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart b/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart index a300d0f..a9b9f5b 100644 --- a/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart +++ b/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart @@ -51,26 +51,48 @@ class VibroNavController extends GetxController { // ── Periodikus napló ─────────────────────────────────────────────── final isLogging = false.obs; - final logIntervalSec = 60.obs; // felhasználó által állítható + final logIntervalSec = 5.obs; // felhasználó által állítható final logs = [].obs; Timer? _logTimer; // ── Térkép — a MapController itt él, nem a View State-jében ──────── final mapController = MapController(); + StreamSubscription? _gnssUpdateSub; + + /// A kezdő térképközéppont lekérdezése (gyors, telefon-GPS-alapú) + /// befejeződött-e — a View ez alapján dönti el, mikor építse fel a + /// térképet, hogy ne induljon rossz (pl. budapesti) középponttal. + final mapReady = false.obs; + @override void onInit() { super.onInit(); load(); refreshLogs(); if (Get.isRegistered()) { - ever(GnssService.to.lastGgaLine, (_) => _onPosition()); + // FONTOS: a lastGgaLine csak NMEA (külső BT/BLE vevő) esetén + // frissül — telefon GPS-nél sosem. Az onDataUpdated viszont + // MINDKÉT forrásnál lefut, ez a helyes, egységes jelzés. + _gnssUpdateSub = + GnssService.to.onDataUpdated.listen((_) => _onPosition()); + GnssService.to.determineInitialPosition().whenComplete(() { + mapReady.value = true; + }); + } else { + mapReady.value = true; } + // Automatikus indítás: amint van pozíció ÉS ki van választva jármű, + // induljon a rögzítés — korábban ez a bekötés hiányzott, a + // _tryAutoStart() metódust semmi nem hívta meg. + ever(hasPosition, (_) => _tryAutoStart()); + ever(VehicleIdentityService.to.selectedVehicle, (_) => _tryAutoStart()); } @override void onClose() { _logTimer?.cancel(); + _gnssUpdateSub?.cancel(); super.onClose(); } @@ -194,6 +216,13 @@ class VibroNavController extends GetxController { _logTimer = null; } + void _tryAutoStart() { + if (isLogging.value) return; + if (!hasPosition.value) return; + if (VehicleIdentityService.to.selectedVehicle.value == null) return; + startLogging(); + } + Future _logTick() async { if (!hasPosition.value) return; final projectId = ProjectService.to.activeProjectId; diff --git a/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart b/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart index 5ab3ae0..b9a416f 100644 --- a/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart +++ b/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart @@ -9,7 +9,10 @@ import 'package:terepi_seged/models/source_point.dart'; import 'package:terepi_seged/models/vechicle_position_log.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/layer_import_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/services/sps_import_service.dart'; import 'package:terepi_seged/services/vechicle_identity_service.dart'; @@ -27,50 +30,80 @@ class VibroNavView extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Vibrátor navigáció'), - actions: [ - IconButton( - icon: const Icon(Icons.upload_file), - tooltip: 'Forráspontok importja (SPS S-fájl)', - onPressed: _importSourcePoints, - ), - IconButton( - icon: const Icon(Icons.local_shipping_outlined), - tooltip: 'Jármű kiválasztása', - onPressed: _pickVehicle, - ), - ], - ), - body: Stack( - children: [ - _buildMap(), - Obx(() { - final target = - controller.hasPosition.value && Get.isRegistered() + appBar: AppBar( + title: const Text('Navigáció'), + actions: [ + IconButton( + icon: const Icon(Icons.bluetooth_connected), + tooltip: "Gyors újracsatlakozás a GNSS vevőhöz", + onPressed: _quickReconnectGnss, + ), + IconButton( + icon: const Icon(Icons.layers_outlined), + tooltip: 'Réteg importja (GeoJSON/KML/KMZ)', + onPressed: _importLayer, + ), + IconButton( + icon: const Icon(Icons.upload_file), + tooltip: 'Forráspontok importja (SPS S-fájl)', + onPressed: _importSourcePoints, + ), + IconButton( + icon: const Icon(Icons.local_shipping_outlined), + tooltip: 'Jármű kiválasztása', + onPressed: _pickVehicle, + ), + ], + ), + body: Obx(() { + if (!controller.mapReady.value) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text('Pozíció lekérése…'), + ], + ), + ); + } + return Stack( + children: [ + _buildMap(), + Builder(builder: (context) { + final target = controller.hasPosition.value && + Get.isRegistered() ? LatLng(GnssService.to.latitude.value, GnssService.to.longitude.value) : null; - return AnimatedMapFollow( - mapController: controller.mapController, - target: target, - heading: controller.travelHeading.value); - }), - Positioned( - left: 8, - right: 8, - top: 8, - child: _VehicleBadge(), - ), - Positioned( - left: 8, - right: 8, - bottom: 8, - child: _NavPanel(controller: controller), - ), - ], - ), - ); + return AnimatedMapFollow( + mapController: controller.mapController, + target: target, + heading: controller.travelHeading.value, + ); + }), + Positioned( + left: 8, + right: 8, + top: 8, + child: Row( + children: [ + Expanded(child: _VehicleBadge()), + const SizedBox(width: 8), + const _GnssStatusBadge(), + ], + ), + ), + Positioned( + left: 8, + right: 8, + bottom: 8, + child: _NavPanel(controller: controller), + ), + ], + ); + })); } Widget _buildMap() { @@ -78,10 +111,17 @@ class VibroNavView extends GetView { final points = controller.sourcePoints; final nearest = controller.nearestPoint.value; + final gnssPos = + Get.isRegistered() && GnssService.to.latitude.value != 0 + ? LatLng( + GnssService.to.latitude.value, GnssService.to.longitude.value) + : null; + final center = gnssPos ?? const LatLng(47.5, 19.05); + return FlutterMap( mapController: controller.mapController, - options: const MapOptions( - initialCenter: LatLng(47.5, 19.05), + options: MapOptions( + initialCenter: center, initialZoom: 15, ), children: [ @@ -147,6 +187,38 @@ class VibroNavView extends GetView { } } + Future _quickReconnectGnss() async { + final device = GnssDeviceService.to.selectedDevice.value; + if (device == null) { + Get.snackbar( + 'Nincs korábbi GNSS-eszköz', + 'Előbb válassz ki egyet a Kitűzés/Mérés beállításaiban.', + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + Get.snackbar( + 'Újracsatlakozás', + '„${device.name}" (${device.typeLabel}) — folyamatban…', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + ); + await GnssService.to.reconnect(); + } + + Future _importLayer() async { + try { + final layer = await LayerImportService.to.importFile(); + if (layer != null) { + Get.snackbar('Réteg importálva', layer.name, + snackPosition: SnackPosition.BOTTOM); + } + } catch (e) { + Get.snackbar('Import hiba', e.toString(), + snackPosition: SnackPosition.BOTTOM); + } + } + // ── SPS S-fájl (forráspont) import — kompakt, egyetlen dialógus ──── Future _importSourcePoints() async { @@ -419,3 +491,59 @@ class _VehicleMarker extends StatelessWidget { ); } } + +class _GnssStatusBadge extends StatelessWidget { + const _GnssStatusBadge(); + + @override + Widget build(BuildContext context) { + if (!Get.isRegistered()) return const SizedBox.shrink(); + + return Obx(() { + final gnss = GnssService.to; + final state = gnss.connectionState.value; + final quality = gnss.gpsQuality.value; + + Color color; + IconData icon; + String label; + + if (state == GnssConnectionState.connected && quality > 0) { + color = Colors.green; + icon = Icons.gps_fixed; + label = quality >= 4 ? 'RTK fix' : (quality == 2 ? 'RTK float' : 'GPS'); + } else if (state == GnssConnectionState.connected) { + color = Colors.orange; + icon = Icons.gps_not_fixed; + label = 'Nincs fix'; + } else if (state == GnssConnectionState.connecting) { + color = Colors.orange; + icon = Icons.gps_not_fixed; + label = 'Csatlakozás…'; + } else { + color = Colors.red; + icon = Icons.gps_off; + label = 'Nincs GNSS'; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color, width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text(label, + style: TextStyle( + fontSize: 11, fontWeight: FontWeight.w600, color: color)), + ], + ), + ); + }); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 508b424..ebf7416 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -113,6 +113,9 @@ class AppPages { name: Routes.VIBRONAV, page: () => const VibroNavView(), binding: BindingsBuilder(() { + if (Get.isRegistered()) { + Get.delete(force: true); + } Get.put(VibroNavController()); })) ]; diff --git a/lib/services/layer_import_service.dart b/lib/services/layer_import_service.dart index bb33cb9..7e31983 100644 --- a/lib/services/layer_import_service.dart +++ b/lib/services/layer_import_service.dart @@ -10,7 +10,9 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:terepi_seged/enums/layer_import_source_type.dart'; @@ -300,10 +302,11 @@ class LayerImportService extends GetxService { defaultPolygonBorderColor: const Color(0xCC1565C0), defaultPolygonBorderStroke: 1.5, defaultPolygonIsFilled: true, + markerCreationCallback: _pointMarkerWithLabel, onMarkerTapCallback: (props) { - final label = props['name'] ?? props['title'] ?? ''; - if (label.toString().isNotEmpty) { - Get.snackbar(label.toString(), props['description']?.toString() ?? '', + final label = _extractFeatureName(props); + if (label != null) { + Get.snackbar(label, props['description']?.toString() ?? '', snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3)); } @@ -323,6 +326,71 @@ class LayerImportService extends GetxService { ); } + // ── Rugalmas névkinyerés — az attribútum neve forrásonként eltérhet ── + + static const _nameKeys = [ + 'name', + 'nev', + 'név', + 'label', + 'title', + 'megnevezes', + 'megnevezés', + 'ref', + 'point' + ]; + + /// A properties-ből megpróbál egy "nevet" kinyerni, a kulcs pontos + /// nevétől (kis/nagybetű) függetlenül, több elterjedt változatot is + /// figyelembe véve. Null, ha semelyik ismert kulcs alatt nincs + /// érdemi (nem üres) érték. + static String? _extractFeatureName(Map properties) { + final lower = { + for (final e in properties.entries) e.key.toLowerCase(): e.value, + }; + for (final key in _nameKeys) { + final v = lower[key]; + if (v != null && v.toString().trim().isNotEmpty) { + return v.toString().trim(); + } + } + return null; + } + + /// Pont-marker, ami — ha talál nevet az attribútumok közt — egy kis + /// feliratot is mutat a jelölő fölött, nem csak koppintásra. + static Marker _pointMarkerWithLabel( + LatLng point, Map properties) { + final label = _extractFeatureName(properties); + return Marker( + point: point, + width: 100, + height: 46, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (label != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.red.withOpacity(0.7)), + ), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + const TextStyle(fontSize: 10, fontWeight: FontWeight.w600), + ), + ), + const Icon(Icons.location_pin, color: Colors.red, size: 26), + ], + ), + ); + } + LayerImportSourceType _sourceType(String ext) => switch (ext.toLowerCase()) { 'kml' => LayerImportSourceType.kml, 'kmz' => LayerImportSourceType.kmz,