From f2457817b2e882251233e8e986e346a71a393b76 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Tue, 12 May 2026 00:14:11 +0200 Subject: [PATCH] =?UTF-8?q?=C3=9Aj=20t=C3=A9rk=C3=A9p=20widget,=20melyet?= =?UTF-8?q?=20t=C3=B6bb=20oldal=20is=20haszn=C3=A1l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/measured_point.dart | 13 + .../controllers/map_survey_controller.dart | 80 ++- .../presentations/views/map_survey_view.dart | 203 ++++++ .../presentations/views/mapsurvey_view.dart | 423 ------------- lib/pages/shell/bindings/shell_binding.dart | 2 + .../shell/presentations/views/shell_view.dart | 19 +- lib/routes/app_pages.dart | 2 +- lib/widgets/coordinate_panel.dart | 319 ++++++++++ lib/widgets/gnss_status_chip.dart | 188 ++++++ lib/widgets/save_point_fab.dart | 324 ++++++++++ lib/widgets/shared_map_widgets.dart | 579 ++++++++++++++++++ 11 files changed, 1718 insertions(+), 434 deletions(-) create mode 100644 lib/models/measured_point.dart create mode 100644 lib/pages/map_survey/presentations/views/map_survey_view.dart delete mode 100644 lib/pages/map_survey/presentations/views/mapsurvey_view.dart create mode 100644 lib/widgets/coordinate_panel.dart create mode 100644 lib/widgets/gnss_status_chip.dart create mode 100644 lib/widgets/save_point_fab.dart create mode 100644 lib/widgets/shared_map_widgets.dart diff --git a/lib/models/measured_point.dart b/lib/models/measured_point.dart new file mode 100644 index 0000000..def444e --- /dev/null +++ b/lib/models/measured_point.dart @@ -0,0 +1,13 @@ +class MeasuredPoint { + final String name; + final double latitude; + final double longitude; + final double? altitude; + + const MeasuredPoint({ + required this.name, + required this.latitude, + required this.longitude, + this.altitude, + }); +} 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 c5fe6de..9670e71 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'; +import 'dart:math' as math; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:file_picker/file_picker.dart'; @@ -26,6 +26,7 @@ import 'package:terepi_seged/eov/eov.dart'; import 'package:terepi_seged/gnss_sentences/gngga.dart'; import 'package:terepi_seged/gnss_sentences/gngst.dart'; import 'package:terepi_seged/gnss_sentences/gnrmc.dart'; +import 'package:terepi_seged/models/measured_point.dart'; import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:proj4dart/proj4dart.dart' as proj4; @@ -33,6 +34,11 @@ 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'; +enum MapSurveyMode { + measure, // Bemérés — ahol vagyok, azt rögzítem + stakeout, // Kitűzés — adott ponthoz navigálok, majd rögzítem +} + class MapSurveyController extends GetxController { // String gpsAddress = "E8:31:CD:14:8B:B2"; // String gpsAddress = "98:CD:AC:62:FF:4E"; @@ -140,6 +146,32 @@ class MapSurveyController extends GetxController { late Session? session; late User? user; + 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(); + @override void onInit() async { super.onInit(); @@ -741,7 +773,8 @@ class MapSurveyController extends GetxController { eov.value.X, gpsLatitude.value, gpsLongitude.value, - max(gpsLatitudeError.value, gpsLongitudeError.value), + math.max( + gpsLatitudeError.value, gpsLongitudeError.value), gpsAltitudeError.value)); print( "pointWithDescriptionList -> ${pointWithDescriptionList.length}"); @@ -760,7 +793,7 @@ class MapSurveyController extends GetxController { 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;${max(gpsLatitudeError.value, gpsLongitudeError.value)};$gpsAltitudeError;${gpsHeightController.text}\r\n", + "$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({ @@ -772,8 +805,8 @@ class MapSurveyController extends GetxController { "eovY": formatEovForFile.format(eov.value.Y), "eovX": formatEovForFile.format(eov.value.X), "description": pointDescriptionController.text, - "horizontalError": - max(gpsLatitudeError.value, gpsLongitudeError.value), + "horizontalError": math.max( + gpsLatitudeError.value, gpsLongitudeError.value), "verticalError": gpsAltitudeError.value, "gpsHeight": gpsHeightController.text }); @@ -790,8 +823,8 @@ class MapSurveyController extends GetxController { 'eovX': eov.value.X, 'eovY': eov.value.Y, 'poleHeight': double.tryParse(gpsHeightController.text), - 'horizontalError': - max(gpsLatitudeError.value, gpsLongitudeError.value), + 'horizontalError': math.max( + gpsLatitudeError.value, gpsLongitudeError.value), 'verticalError': gpsAltitudeError.value, 'description': pointDescriptionController.text, 'isDeleted': false, @@ -932,4 +965,37 @@ class MapSurveyController extends GetxController { final result = await SharePlus.instance.share(params); } + + 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; + } + } + + void setTarget(String name, double y, double x) { + targetName.value = name; + targetEovY.value = y; + 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 new file mode 100644 index 0000000..92d74d7 --- /dev/null +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -0,0 +1,203 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; +import 'package:get/get.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:rive/rive.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart'; +import 'package:terepi_seged/utils/rive_utils.dart'; +import 'package:terepi_seged/widgets/coordinate_panel.dart'; +import 'package:terepi_seged/widgets/save_point_fab.dart'; +import 'package:terepi_seged/widgets/shared_map_widgets.dart'; + +import 'map_add_point_dialog.dart'; + +class MapSurveyView extends GetView { + const MapSurveyView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack(children: [ + const SharedMapWidget(), + Positioned( + top: 8, + right: 8, + left: 8, + child: CoordinatePanel.fromController(controller), + ), + Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), + Positioned( + bottom: 80, + left: 8, + right: 8, + child: Obx( + () => controller.mode.value == MapSurveyMode.stakeout + ? _StakeoutPanel() // ΔY, ΔX, távolság, irányszög + : const SizedBox.shrink(), + ), + ), + Positioned( + bottom: 16, + right: 16, + child: SavePointFab(controller: controller), + ), + ]); + } +} + +class _ModeSelector extends GetView { + const _ModeSelector(); + + @override + Widget build(BuildContext context) { + return Obx(() => SegmentedButton( + segments: const [ + ButtonSegment( + value: MapSurveyMode.measure, + icon: Icon(Icons.gps_fixed, size: 16), + label: Text('Bemérés'), + ), + ButtonSegment( + value: MapSurveyMode.stakeout, + icon: Icon(Icons.my_location, size: 16), + label: Text('Kitűzés'), + ), + ], + selected: {controller.mode.value}, + onSelectionChanged: (s) => controller.switchMode(s.first), + )); + } +} + +class _StakeoutPanel extends GetView { + const _StakeoutPanel(); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Obx(() { + final onTarget = controller.isOnTarget; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Célpont neve + Row(children: [ + const Icon(Icons.flag, size: 16, color: Colors.orange), + const SizedBox(width: 6), + Text(controller.targetName.value, + style: const TextStyle(fontWeight: FontWeight.w600)), + const Spacer(), + TextButton( + onPressed: _showTargetPicker, + child: const Text('Változtat'), + ), + ]), + + const Divider(height: 16), + + // Eltérések + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _DeltaCell( + label: 'ΔY', + value: controller.deltaY, + unit: 'm', + ), + _DeltaCell( + label: 'ΔX', + value: controller.deltaX, + unit: 'm', + ), + _DeltaCell( + label: 'Táv', + value: controller.distanceToTarget, + unit: 'm', + alwaysPositive: true, + ), + ], + ), + + const SizedBox(height: 8), + + // Státusz + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: onTarget + ? Colors.green.withOpacity(0.12) + : Colors.orange.withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + onTarget + ? '✓ Célponton — pont rögzíthető' + : '${controller.distanceToTarget.toStringAsFixed(3)} m a céltól', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w500, + color: onTarget ? Colors.green : Colors.orange, + fontSize: 13, + ), + ), + ), + ], + ); + }), + ), + ); + } + + void _showTargetPicker() { + // Lista a korábban bemért vagy tervezett pontokból + //Get.bottomSheet(const _TargetPickerSheet()); + } +} + +class _DeltaCell extends StatelessWidget { + final String label; + final double value; + final String unit; + final bool alwaysPositive; + + const _DeltaCell({ + required this.label, + required this.value, + required this.unit, + this.alwaysPositive = false, + }); + + @override + Widget build(BuildContext context) { + final display = alwaysPositive ? value.abs() : value; + final prefix = (!alwaysPositive && value > 0) ? '+' : ''; + final color = value.abs() < 0.05 + ? Colors.green + : value.abs() < 0.5 + ? Colors.orange + : Colors.red; + + return Column( + children: [ + Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)), + const SizedBox(height: 2), + Text( + '$prefix${display.toStringAsFixed(3)} $unit', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ], + ); + } +} diff --git a/lib/pages/map_survey/presentations/views/mapsurvey_view.dart b/lib/pages/map_survey/presentations/views/mapsurvey_view.dart deleted file mode 100644 index a3a66f7..0000000 --- a/lib/pages/map_survey/presentations/views/mapsurvey_view.dart +++ /dev/null @@ -1,423 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; -import 'package:get/get.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:rive/rive.dart'; -import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; -import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart'; -import 'package:terepi_seged/utils/rive_utils.dart'; - -import 'map_add_point_dialog.dart'; - -class MapSurveyView extends GetView { - const MapSurveyView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - late SMIBool gpsTrigger; - - return Scaffold( - resizeToAvoidBottomInset: false, - extendBody: true, - appBar: AppBar( - title: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Térkép'), - Text( - "", - style: TextStyle(fontSize: 12.0), - ) - ], - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Obx( - () => GestureDetector( - onTap: () { - if (controller.gpsIsConnected.value) { - controller.disconnectFromGps(); - } else { - controller.connectToGps(); - } - }, - child: Icon(Icons.gps_fixed, - size: 26.0, - color: controller.gpsIsConnected.value - ? Colors.green - : Colors.red), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Obx( - () => GestureDetector( - onTap: () { - if (controller.ntripIsConnected.value) { - controller.disconnectFromNtripServer(); - } else { - controller.connectToNtripServer(); - } - }, - child: Icon( - Icons.assistant, - size: 26.0, - color: controller.ntripIsConnected.value - ? Colors.green - : Colors.red, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: GestureDetector( - onTap: () { - if (controller.ntripUserName.value.isNotEmpty) { - controller.ntripUsernameController.text = - controller.ntripUserName.value; - } - if (controller.ntripPassword.value.isNotEmpty) { - controller.ntripPasswordController.text = - controller.ntripPassword.value; - } - Get.to(() => SettingsDialog(), transition: Transition.downToUp); - }, - child: const Icon( - Icons.more_vert, - size: 26.0, - ), - ), - ) - ], - ), - body: Column( - children: [ - Obx( - () => controller.gpsIsConnected.value - ? Padding( - padding: const EdgeInsets.all(4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 170.0, - // height: 160.0, - decoration: BoxDecoration( - border: Border.all( - width: 2.0, - color: const Color.fromARGB( - 130, 255, 255, 255)), - color: - const Color.fromARGB(130, 255, 255, 255)), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${controller.gpsDateTime} (UTC)", - style: const TextStyle(fontSize: 10.0), - ), - const Divider( - height: 5.0, - color: Colors.black, - ), - Text( - "EovX: ${controller.formatEov.format(controller.eov.value.Y)}"), - Text( - "EovY: ${controller.formatEov.format(controller.eov.value.X)}"), - Text("Alt: ${controller.gpsAltitude} (m)"), - const Divider( - height: 5.0, - color: Colors.black, - ), - Text( - "Hor. error: ${max(controller.gpsLatitudeError.value, controller.gpsLongitudeError.value)} (m)", - style: const TextStyle(fontSize: 14.0), - ), - Text( - "Vert. error: ${controller.gpsAltitudeError} (m)", - style: const TextStyle(fontSize: 14.0), - ), - const Divider( - height: 5.0, - color: Colors.black, - ), - Text( - "GPS quality -> ${controller.getGpsQualityIndicator(quality: controller.gpsQuality.value)} ", - style: const TextStyle(fontSize: 8.0), - ), - controller.ntripIsConnected.value - ? Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Divider( - height: 5.0, - color: Colors.black, - ), - Text( - "Ntrip ${controller.ntripReceivedData} byte(s) (${controller.ntripDataPacketNumbers})", - style: - const TextStyle(fontSize: 10.0), - ), - Text( - "GGA last send: ${controller.ggaSendLastTimeStr} (${controller.ggaSenDataPacketNumber})", - style: - const TextStyle(fontSize: 10.0), - ), - ], - ) - : const SizedBox( - width: 0.0, - height: 0.0, - ) - ], - )), - ], - ), - ) - : const SizedBox(), - ), - Expanded( - child: Stack( - children: [ - Obx( - () => controller.mapIsInitialized.value - ? FlutterMap( - mapController: controller.mapController, - options: MapOptions( - initialCenter: LatLng( - controller.currentLatitude.value, - controller.currentLongitude.value), - maxZoom: 25, - initialZoom: controller.currentZoom.value, - ), - children: [ - TileLayer( - urlTemplate: - 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', - subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'], - maxNativeZoom: 18, - // urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - // userAgentPackageName: 'hu.app_dev.terepi_seged', - ), - MarkerLayer( - markers: controller.pointNotesMarker, - ), - MarkerLayer( - markers: controller.currentLocationMarker), - // MarkerLayer(markers: controller.parser.markers), - MarkerLayer( - markers: controller.pointsToMeasureMarker), - // PolylineLayer( - // polylines: controller.parser.polylines), - PolyWidgetLayer( - polyWidgets: controller.pointsToMeasureLabel) - ], - ) - : const Center(child: CircularProgressIndicator()), - ), - // Obx( - // () => controller.gpsIsConnected.value - // ? Positioned( - // left: 4.0, - // top: 4.0, - // child: Container( - // width: 170.0, - // // height: 160.0, - // decoration: BoxDecoration( - // border: Border.all( - // width: 2.0, - // color: const Color.fromARGB( - // 130, 255, 255, 255)), - // color: - // const Color.fromARGB(130, 255, 255, 255)), - // child: Column( - // mainAxisAlignment: MainAxisAlignment.start, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // "${controller.gpsDateTime} (UTC)", - // style: const TextStyle(fontSize: 10.0), - // ), - // const Divider( - // height: 5.0, - // color: Colors.black, - // ), - // Text( - // "EovX: ${controller.formatEov.format(controller.eov.value.Y)}"), - // Text( - // "EovY: ${controller.formatEov.format(controller.eov.value.X)}"), - // Text("Alt: ${controller.gpsAltitude} (m)"), - // const Divider( - // height: 5.0, - // color: Colors.black, - // ), - // Text( - // "Hor. error: ${max(controller.gpsLatitudeError.value, controller.gpsLongitudeError.value)} (m)", - // style: const TextStyle(fontSize: 14.0), - // ), - // Text( - // "Vert. error: ${controller.gpsAltitudeError} (m)", - // style: const TextStyle(fontSize: 14.0), - // ), - // const Divider( - // height: 5.0, - // color: Colors.black, - // ), - // Text( - // "GPS quality -> ${controller.getGpsQualityIndicator(quality: controller.gpsQuality.value)} ", - // style: const TextStyle(fontSize: 8.0), - // ), - // controller.ntripIsConnected.value - // ? Column( - // crossAxisAlignment: - // CrossAxisAlignment.start, - // children: [ - // const Divider( - // height: 5.0, - // color: Colors.black, - // ), - // Text( - // "Ntrip ${controller.ntripReceivedData} byte(s) (${controller.ntripDataPacketNumbers})", - // style: const TextStyle( - // fontSize: 10.0), - // ), - // Text( - // "GGA last send: ${controller.ggaSendLastTimeStr} (${controller.ggaSenDataPacketNumber})", - // style: const TextStyle( - // fontSize: 10.0), - // ), - // ], - // ) - // : const SizedBox( - // width: 0.0, - // height: 0.0, - // ) - // ], - // )), - // ) - // : const Positioned( - // child: SizedBox( - // width: 0.0, - // height: 0.0, - // )), - // ) - ], - ), - ), - ], - ), - // bottomNavigationBar: Container( - // padding: const EdgeInsets.all(12), - // margin: const EdgeInsets.symmetric(horizontal: 24), - // decoration: BoxDecoration( - // color: Colors.black.withOpacity(0.5), - // borderRadius: const BorderRadius.all(Radius.circular(24))), - // child: Row( - // children: [ - // GestureDetector( - // onTap: () { - // gpsTrigger.change(true); - // Future.delayed(const Duration(seconds: 1), () { - // gpsTrigger.change(false); - // }); - // // controller.onBottomNavigationBarTap(0); - // Get.to(() => const MapAddPointDialog(), - // fullscreenDialog: true, - // transition: Transition.downToUp, - // duration: const Duration(milliseconds: 600)); - // }, - // child: SizedBox( - // height: 36, - // width: 36, - // child: RiveAnimation.asset( - // "assets/RiveAssets/travel_icons_pack.riv", - // artboard: "GPS", onInit: (artboard) { - // StateMachineController controllerRive = - // RiveUtils.getRiveController(artboard, - // stateMachineName: "gps_interactivity"); - // gpsTrigger = controllerRive.findSMI("active") as SMIBool; - // }), - // ), - // ) - // ], - // ), - // ), - floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, - floatingActionButton: Wrap( - direction: Axis.horizontal, - children: [ - FloatingActionButton( - onPressed: () { - controller.mapZoomOut(); - }, - heroTag: 'ZoomOut', - tooltip: 'Zoom out', - child: const Icon(Icons.remove_circle_outline_rounded), - ), - const SizedBox( - width: 15, - ), - FloatingActionButton( - onPressed: () { - controller.mapZoomIn(); - }, - heroTag: 'ZoomIn', - tooltip: 'Zoom in', - child: const Icon(Icons.add_circle_outline_rounded), - ), - const SizedBox(width: 15.0), - FloatingActionButton( - onPressed: () { - // controller.isMapMoveToCenter(); - controller.setIsMapMoveToCenter(); - }, - heroTag: 'Center map', - tooltip: 'Center map', - child: Obx( - () => Icon(Icons.center_focus_strong, - color: controller.isMapMoveToCenter.value - ? Colors.green - : Colors.red), - ), - ), - const SizedBox( - width: 15.0, - ), - FloatingActionButton( - onPressed: () { - // controller.addMeasuredPoint(); - // controller.onBottomNavigationBarTap(0); - controller.showAddPointDialog(); - }, - heroTag: 'Pont bemérése', - tooltip: 'Pont bemérése', - child: const Icon(Icons.flag_sharp), - ), - const SizedBox( - width: 15.0, - ), - FloatingActionButton( - onPressed: () { - // controller.isMapMoveToCenter(); - controller.showMeasuredPointsTableDialog(); - }, - heroTag: 'Database test', - tooltip: 'Pont bemérése', - child: const Icon(Icons.dataset), - ), - ], - ), - ); - } -} diff --git a/lib/pages/shell/bindings/shell_binding.dart b/lib/pages/shell/bindings/shell_binding.dart index e71376f..59f55a8 100644 --- a/lib/pages/shell/bindings/shell_binding.dart +++ b/lib/pages/shell/bindings/shell_binding.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:terepi_seged/pages/home/presentation/controllers/home_controller.dart'; import 'package:terepi_seged/pages/map/presentation/controllers/map_controller.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import '../presentations/controllers/shell_controller.dart'; @@ -11,5 +12,6 @@ class ShellBinding extends Bindings { Get.put(ShellController()); Get.put(HomeViewController()); Get.put(MapViewController()); + Get.put(MapSurveyController()); } } diff --git a/lib/pages/shell/presentations/views/shell_view.dart b/lib/pages/shell/presentations/views/shell_view.dart index 6827a3c..e12eead 100644 --- a/lib/pages/shell/presentations/views/shell_view.dart +++ b/lib/pages/shell/presentations/views/shell_view.dart @@ -1,9 +1,11 @@ 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/widgets/gnss_status_chip.dart'; import '../../../../widgets/app_drawer.dart'; -import '../../../map_survey/presentations/views/mapsurvey_view.dart'; +import '../../../map_survey/presentations/views/map_survey_view.dart'; import '../controllers/shell_controller.dart'; class ShellView extends GetView { @@ -24,8 +26,19 @@ class ShellView extends GetView { // Cím reaktívan frissül tab váltáskor title: Obx(() => Text(controller.currentTitle)), actions: [ - //Obx(() => _GnssStatusChip()), - const SizedBox(width: 8), + const GnssStatusChip(), + const SizedBox(width: 6), + Obx(() => controller.currentIndex.value == 1 + ? NtripStatusChip( + isConnected: MapSurveyController.to.ntripIsConnected, + onToggle: () { + final c = MapSurveyController.to; + c.ntripIsConnected.value + ? c.disconnectFromNtripServer() + : c.connectToNtripServer(); + }, + ) + : const SizedBox.shrink()) ], ), drawer: const AppDrawer(), diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index f5ddf1b..5b140ff 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -9,7 +9,7 @@ import 'package:terepi_seged/pages/map/bindings/map_bindings.dart'; import 'package:terepi_seged/pages/map/presentation/views/map_add_point_dialog.dart'; import 'package:terepi_seged/pages/map/presentation/views/map_view.dart'; import 'package:terepi_seged/pages/map_survey/bindings/map_survey_bindings.dart'; -import 'package:terepi_seged/pages/map_survey/presentations/views/mapsurvey_view.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/views/map_survey_view.dart'; import 'package:terepi_seged/pages/measured_data/bindings/measured_data_bindings.dart'; import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart'; import 'package:terepi_seged/pages/navigation/bindings/navigation_bindings.dart'; diff --git a/lib/widgets/coordinate_panel.dart b/lib/widgets/coordinate_panel.dart new file mode 100644 index 0000000..b7761b6 --- /dev/null +++ b/lib/widgets/coordinate_panel.dart @@ -0,0 +1,319 @@ +// lib/widgets/coordinate_panel.dart + +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +import '../services/gnss/gnss_service.dart'; + +class CoordinatePanel extends StatelessWidget { + final RxDouble eovY; + final RxDouble eovX; + final RxDouble horError; + final RxDouble vertError; + final RxDouble? altitudeMsl; + final RxDouble? geoidSeparation; + final RxBool? ntripConnected; + final RxInt? ntripBytes; + final RxInt? ntripPackets; + final RxInt? ggaPackets; + + const CoordinatePanel({ + super.key, + required this.eovY, + required this.eovX, + required this.horError, + required this.vertError, + this.altitudeMsl, + this.geoidSeparation, + this.ntripConnected, + this.ntripBytes, + this.ntripPackets, + this.ggaPackets, + }); + + /// Gyors factory — MapSurveyController mezőiből. + factory CoordinatePanel.fromController(dynamic ctrl) { + final y = RxDouble(0.0); + final x = RxDouble(0.0); + // Figyeli az eov változást és frissíti Y/X-et + ctrl.eov.listen((eov) { + y.value = (eov.Y as num).toDouble(); + x.value = (eov.X as num).toDouble(); + }); + return CoordinatePanel( + eovY: y, + eovX: x, + horError: ctrl.gpsLatitudeError as RxDouble, + 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, + ); + } + + static final _fmtEov = NumberFormat('###,###,##0.000', 'hu-HU'); + static final _fmtEovZ = NumberFormat('###,##0.000', 'hu-HU'); + + @override + Widget build(BuildContext context) { + final gnss = GnssService.to; + + return Obx(() { + final quality = gnss.gpsQuality.value; + final hasData = quality > 0; + final color = _qualityColor(quality); + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.78), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: color.withOpacity(0.6), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Fejléc ────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 6), + child: Row(children: [ + // Fix chip + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(5), + border: Border.all(color: color.withOpacity(0.5)), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Container( + width: 7, + height: 7, + decoration: + BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 5), + Text(_qualityLabel(quality), + style: TextStyle( + color: color, + fontSize: 11, + fontWeight: FontWeight.w700, + )), + ]), + ), + const Spacer(), + // Műholdak + if (hasData) ...[ + Icon(Icons.satellite_alt, + size: 11, color: Colors.white.withOpacity(0.5)), + const SizedBox(width: 3), + Text('${gnss.satelliteCount.value}', + style: TextStyle( + color: Colors.white.withOpacity(0.55), fontSize: 11)), + const SizedBox(width: 10), + ], + // UTC + Icon(Icons.access_time, + size: 11, color: Colors.white.withOpacity(0.4)), + const SizedBox(width: 3), + Text( + gnss.utcFix.value.isNotEmpty + ? '${gnss.utcFix.value} UTC' + : '--:--:-- UTC', + style: TextStyle( + color: Colors.white.withOpacity(0.45), fontSize: 11)), + ]), + ), + + if (hasData) ...[ + _Div(), + + // ── EOV koordináták ────────────────────────────── + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: Obx(() { + final alt = altitudeMsl?.value ?? gnss.altitude.value; + final geoSep = + geoidSeparation?.value ?? gnss.geoidSeparation.value; + return Column(children: [ + _Row('EOV Y', _fmtEov.format(eovY.value), 'm', bold: true), + const SizedBox(height: 3), + _Row('EOV X', _fmtEov.format(eovX.value), 'm', bold: true), + const SizedBox(height: 6), + _Row('H (MSL)', _fmtEovZ.format(alt), 'm'), + const SizedBox(height: 2), + _Row('h (WGS84)', _fmtEovZ.format(alt + geoSep), 'm', + dimmed: true), + ]); + }), + ), + + _Div(), + + // ── Pontosság ──────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(10, 4, 10, 6), + child: Obx(() { + final hor = max(horError.value, 0.0); + final vert = vertError.value; + final geo = + geoidSeparation?.value ?? gnss.geoidSeparation.value; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _Chip(Icons.open_in_full, 'Vízszintes', + '±${hor.toStringAsFixed(3)} m', _errorColor(hor)), + _Chip(Icons.height, 'Függőleges', + '±${vert.toStringAsFixed(3)} m', _errorColor(vert)), + _Chip(Icons.water, 'Geoid N', + '${geo.toStringAsFixed(2)} m', Colors.white54), + ], + ); + }), + ), + + // ── NTRIP ──────────────────────────────────────── + if (ntripConnected != null) + Obx( + () => ntripConnected!.value + ? Column(children: [ + _Div(), + Padding( + padding: const EdgeInsets.fromLTRB(10, 4, 10, 6), + child: Row(children: [ + Container( + width: 7, + height: 7, + decoration: const BoxDecoration( + color: Colors.greenAccent, + shape: BoxShape.circle), + ), + const SizedBox(width: 6), + const Text('NTRIP', + style: TextStyle( + color: Colors.greenAccent, + fontSize: 11, + fontWeight: FontWeight.w600, + )), + const Spacer(), + Text( + '${ntripBytes?.value ?? 0} byte' + ' · ${ntripPackets?.value ?? 0} cs.', + style: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 10, + )), + const SizedBox(width: 8), + Text('GGA: ${ggaPackets?.value ?? 0}', + style: TextStyle( + color: Colors.white.withOpacity(0.4), + fontSize: 10, + )), + ]), + ), + ]) + : const SizedBox.shrink(), + ), + ], + ], + ), + ); + }); + } +} + +// ── Mini widgetek ───────────────────────────────────────────────────── + +class _Div extends StatelessWidget { + @override + Widget build(BuildContext context) => + Container(height: 1, color: Colors.white.withOpacity(0.08)); +} + +class _Row extends StatelessWidget { + final String label, value, unit; + final bool bold, dimmed; + const _Row(this.label, this.value, this.unit, + {this.bold = false, this.dimmed = false}); + + @override + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + color: Colors.white.withOpacity(0.5), fontSize: 11)), + Row(children: [ + Text(value, + style: TextStyle( + color: Colors.white.withOpacity(dimmed ? 0.45 : 1.0), + fontSize: bold ? 15 : 13, + fontWeight: bold ? FontWeight.w600 : FontWeight.w400, + fontFeatures: const [FontFeature.tabularFigures()], + )), + const SizedBox(width: 3), + Text(unit, + style: TextStyle( + color: Colors.white.withOpacity(0.35), fontSize: 10)), + ]), + ], + ); +} + +class _Chip extends StatelessWidget { + final IconData icon; + final String label, value; + final Color color; + const _Chip(this.icon, this.label, this.value, this.color); + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: Colors.white.withOpacity(0.4)), + const SizedBox(height: 2), + Text(value, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + fontFeatures: const [FontFeature.tabularFigures()], + )), + Text(label, + style: + TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 9)), + ], + ); +} + +// ── Segédfüggvények ─────────────────────────────────────────────────── + +Color _qualityColor(int q) => switch (q) { + 4 => Colors.greenAccent, + 5 => Colors.lightGreen, + 2 => Colors.blue, + 1 => Colors.orange, + _ => Colors.red, + }; + +String _qualityLabel(int q) => switch (q) { + 4 => 'RTK Fix', + 5 => 'RTK Float', + 2 => 'DGPS', + 1 => 'GPS', + _ => 'Nincs fix', + }; + +Color _errorColor(double e) { + if (e < 0.05) return Colors.greenAccent; + if (e < 0.2) return Colors.green; + if (e < 1.0) return Colors.orange; + return Colors.red; +} diff --git a/lib/widgets/gnss_status_chip.dart b/lib/widgets/gnss_status_chip.dart new file mode 100644 index 0000000..b6bf637 --- /dev/null +++ b/lib/widgets/gnss_status_chip.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../services/gnss/gnss_connection.dart'; +import '../services/gnss/gnss_device_service.dart'; +import '../services/gnss/gnss_service.dart'; +import 'gnss_device_picker_dialog.dart'; + +/// GNSS kapcsolat + fix minőség chip az AppBar-ban. +/// +/// Tapintásra megnyitja az eszközkiválasztó dialógot. +/// Kompakt: csak az AppBar-ban lévő kis helyet foglalja el. +/// +/// Állapotok: +/// - Szürke: nincs eszköz kiválasztva +/// - Piros: eszköz kiválasztva, nincs kapcsolat +/// - Narancs: csatlakozva, nincs GPS fix +/// - Sárga: autonóm GPS (quality 1) +/// - Kék: DGPS (quality 2) +/// - Világoszöld: RTK Float (quality 5) +/// - Zöld: RTK Fix (quality 4) ← ideális állapot +class GnssStatusChip extends StatelessWidget { + const GnssStatusChip({super.key}); + + @override + Widget build(BuildContext context) { + return Obx(() { + final connState = GnssService.to.connectionState.value; + final quality = GnssService.to.gpsQuality.value; + final device = GnssDeviceService.to.selectedDevice.value; + final sats = GnssService.to.satelliteCount.value; + + final isConnected = connState == GnssConnectionState.connected; + final isConnecting = connState == GnssConnectionState.connecting; + final color = _chipColor(isConnected, quality); + final label = _chipLabel(connState, quality, device?.name); + + return GestureDetector( + onTap: () => GnssDevicePickerDialog.show(), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.5)), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + // Állapot ikon + if (isConnecting) + SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: color, + ), + ) + else + Icon( + _chipIcon(isConnected, quality), + size: 12, + color: color, + ), + + const SizedBox(width: 4), + + // Felirat + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + + // Műholdak száma (csak ha van fix) + if (isConnected && quality > 0) ...[ + const SizedBox(width: 4), + Text( + '($sats)', + style: TextStyle( + fontSize: 10, + color: color.withOpacity(0.8), + ), + ), + ], + ]), + ), + ); + }); + } + + Color _chipColor(bool connected, int quality) { + if (!connected) return Colors.grey; + return switch (quality) { + 4 => Colors.greenAccent, + 5 => Colors.lightGreen, + 2 => Colors.blue, + 1 => Colors.orange, + _ => Colors.orange.shade300, + }; + } + + IconData _chipIcon(bool connected, int quality) { + if (!connected) return Icons.gps_off; + if (quality == 0) return Icons.gps_not_fixed; + return Icons.gps_fixed; + } + + String _chipLabel( + GnssConnectionState state, int quality, String? deviceName) { + return switch (state) { + GnssConnectionState.connecting => 'Kapcsolódás...', + GnssConnectionState.disconnected => + deviceName != null ? 'Nincs kapcsolat' : 'Nincs eszköz', + GnssConnectionState.error => 'Hiba', + GnssConnectionState.connected => switch (quality) { + 4 => 'RTK Fix', + 5 => 'RTK Float', + 2 => 'DGPS', + 1 => 'GPS', + _ => 'Fix nélkül', + }, + }; + } +} + +/// NTRIP kapcsolat chip az AppBar-ban. +/// +/// Tapintásra az NTRIP beállítások oldalra navigál. +/// Csak akkor látható, ha van kiválasztott GNSS eszköz. +class NtripStatusChip extends StatelessWidget { + /// NTRIP csatlakozva van-e — a controllerből kapja. + final RxBool isConnected; + + /// Csatlakozás / leválasztás callback. + final VoidCallback? onToggle; + + /// NTRIP beállítások megnyitása. + final VoidCallback? onSettings; + + const NtripStatusChip({ + super.key, + required this.isConnected, + this.onToggle, + this.onSettings, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + final connected = isConnected.value; + final color = connected ? Colors.greenAccent : Colors.grey; + + return GestureDetector( + onTap: onToggle, + onLongPress: onSettings, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.45)), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon( + Icons.cell_tower, + size: 12, + color: color, + ), + const SizedBox(width: 4), + Text( + connected ? 'NTRIP' : 'NTRIP off', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ]), + ), + ); + }); + } +} diff --git a/lib/widgets/save_point_fab.dart b/lib/widgets/save_point_fab.dart new file mode 100644 index 0000000..4492775 --- /dev/null +++ b/lib/widgets/save_point_fab.dart @@ -0,0 +1,324 @@ +// lib/widgets/save_point_fab.dart + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../services/gnss/gnss_service.dart'; + +/// Pont mentése FAB + dialóg widget. +/// +/// A meglévő [MapSurveyController] mentési logikájára épül. +/// GPS fix nélkül letiltva — kesztyűvel is jól tapintható méret. +/// +/// Használat: +/// ```dart +/// // Stack-ben, jobb alulra pozicionálva: +/// Positioned( +/// right: 16, +/// bottom: 16, +/// child: SavePointFab(controller: controller), +/// ) +/// ``` +class SavePointFab extends StatelessWidget { + /// A controller dynamic-ként fogadva — így nincs + /// közvetlen import kényszer a MapSurveyController-re. + final dynamic controller; + + const SavePointFab({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + final quality = GnssService.to.gpsQuality.value; + final hasfix = quality > 0; + final color = _fixColor(quality); + + return Stack( + clipBehavior: Clip.none, + children: [ + // ── Fő FAB ──────────────────────────────────────────── + FloatingActionButton.large( + heroTag: 'save_point_fab', + tooltip: hasfix ? 'Pont rögzítése' : 'GPS fix szükséges', + backgroundColor: hasfix + ? Theme.of(context).colorScheme.primaryContainer + : Colors.grey.shade700, + onPressed: hasfix ? () => _showSaveDialog(context) : null, + child: Icon( + Icons.flag, + size: 34, + color: hasfix + ? Theme.of(context).colorScheme.onPrimaryContainer + : Colors.grey.shade400, + ), + ), + + // ── Fix minőség jelző — jobb felső sarok ───────────── + Positioned( + top: -4, + right: -4, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + width: 14, + height: 14, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.5), + blurRadius: 6, + spreadRadius: 1, + ), + ], + ), + ), + ), + ], + ); + }); + } + + // ── Dialóg ──────────────────────────────────────────────────────── + + void _showSaveDialog(BuildContext context) { + // A controller mezőit frissítjük az aktuális sorszámra + controller.pointIdController.text = controller.pointId.toString(); + controller.pointDescriptionController.text = ''; + controller.gpsHeightController.text = ''; + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Fejléc ─────────────────────────────────────── + Row(children: [ + Obx(() { + final q = GnssService.to.gpsQuality.value; + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _fixColor(q).withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: _fixColor(q).withOpacity(0.5)), + ), + child: Text( + _fixLabel(q), + style: TextStyle( + color: _fixColor(q), + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ); + }), + const SizedBox(width: 10), + const Expanded( + child: Text( + 'Pont rögzítése', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => Get.back(), + ), + ]), + + const SizedBox(height: 8), + + // ── EOV előnézet ───────────────────────────────── + Obx(() { + final fmt = controller.formatEov; + final eovY = fmt.format(controller.eov.value.Y); + final eovX = fmt.format(controller.eov.value.X); + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceVariant + .withOpacity(0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + _PreviewRow('EOV Y', eovY, 'm'), + const SizedBox(height: 2), + _PreviewRow('EOV X', eovX, 'm'), + const SizedBox(height: 2), + _PreviewRow( + 'H (MSL)', + '${(controller.gpsAltitude.value as double).toStringAsFixed(3)}', + 'm', + ), + ], + ), + ); + }), + + const SizedBox(height: 16), + + // ── Pont azonosító ─────────────────────────────── + TextField( + controller: controller.pointIdController, + autofocus: true, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Pont azonosító', + prefixIcon: Icon(Icons.tag, size: 18), + ), + ), + + const SizedBox(height: 12), + + // ── Leírás ─────────────────────────────────────── + TextField( + controller: controller.pointDescriptionController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Leírás (opcionális)', + prefixIcon: Icon(Icons.notes, size: 18), + ), + ), + + const SizedBox(height: 12), + + // ── Pólus magasság ─────────────────────────────── + TextField( + controller: controller.gpsHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Pólus magasság (m)', + prefixIcon: Icon(Icons.height, size: 18), + hintText: '0.00', + ), + ), + + const SizedBox(height: 20), + + // ── Gombok ─────────────────────────────────────── + Row(children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Get.back(), + child: const Text('Mégsem'), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton.icon( + icon: const Icon(Icons.save_alt, size: 18), + label: const Text('Mentés'), + onPressed: () async { + await _savePoint(); + }, + ), + ), + ]), + ], + ), + ), + ), + barrierDismissible: false, + ); + } + + // ── Mentési logika ──────────────────────────────────────────────── + + Future _savePoint() async { + // A meglévő controller logikájának meghívása + // (onBottomNavigationBarTap belső logikájából kiemelve) + try { + controller.pointId = + int.tryParse(controller.pointIdController.text) ?? controller.pointId; + + // A controller meglévő mentési metódusának hívása + // Ez tartalmazza: lista, fájl, Supabase mentés, marker + await controller.saveCurrentPoint(); + + Get.back(); + + Get.snackbar( + 'Pont mentve', + '${controller.pointIdController.text} sikeresen rögzítve.', + backgroundColor: Colors.green.withOpacity(0.9), + colorText: Colors.white, + duration: const Duration(seconds: 2), + snackPosition: SnackPosition.TOP, + ); + } catch (e) { + Get.snackbar( + 'Hiba', + 'Mentés sikertelen: $e', + backgroundColor: Colors.red.withOpacity(0.9), + colorText: Colors.white, + ); + } + } +} + +// ── EOV előnézet sor ────────────────────────────────────────────────── + +class _PreviewRow extends StatelessWidget { + final String label, value, unit; + const _PreviewRow(this.label, this.value, this.unit); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + fontSize: 11, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withOpacity(0.7), + )), + Text( + '$value $unit', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +// ── Segédfüggvények ─────────────────────────────────────────────────── + +Color _fixColor(int q) => switch (q) { + 4 => Colors.greenAccent, + 5 => Colors.lightGreen, + 2 => Colors.blue, + 1 => Colors.orange, + _ => Colors.grey, + }; + +String _fixLabel(int q) => switch (q) { + 4 => 'RTK Fix', + 5 => 'RTK Float', + 2 => 'DGPS', + 1 => 'GPS', + _ => 'Nincs fix', + }; diff --git a/lib/widgets/shared_map_widgets.dart b/lib/widgets/shared_map_widgets.dart new file mode 100644 index 0000000..46912fe --- /dev/null +++ b/lib/widgets/shared_map_widgets.dart @@ -0,0 +1,579 @@ +// lib/widgets/shared_map_widget.dart +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; + +import '../services/gnss/gnss_service.dart'; +import '../services/coord_converter_service.dart'; +import '../pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +// ─── SharedMapWidget ────────────────────────────────────────────────────────── + +class SharedMapWidget extends StatefulWidget { + /// Extra flutter_map rétegek (pl. PolylineLayer, PolygonLayer). + final List extraLayers; + + /// Külső MapController — ha az oldal saját maga is mozgatja a térképet. + final MapController? mapController; + + /// Hosszú nyomás callback (terepbejárás pont hozzáadáshoz). + final void Function(TapPosition, LatLng)? onLongPress; + + /// Megjelenítendő vezérlők. + final MapControls controls; + + /// Kezdeti zoom szint. + final double initialZoom; + + const SharedMapWidget({ + super.key, + this.extraLayers = const [], + this.mapController, + this.onLongPress, + this.controls = const MapControls(), + this.initialZoom = 18.0, + }); + + @override + State createState() => _SharedMapWidgetState(); +} + +class _SharedMapWidgetState extends State { + late final MapController _mapController; + + // Reaktív belső állapot + final _isFollowing = true.obs; // GPS követés be/ki + final _currentZoom = 18.0.obs; + final _isNorthUp = true.obs; // forgó térkép vs. É-up + + @override + void initState() { + super.initState(); + _mapController = widget.mapController ?? MapController(); + _currentZoom.value = widget.initialZoom; + } + + @override + void dispose() { + // Csak akkor disposoljuk, ha belső controller + if (widget.mapController == null) { + _mapController.dispose(); + } + super.dispose(); + } + + // ── GPS pozíció követése ──────────────────────────────────────────── + + void _onPositionChanged(MapCamera camera, bool hasGesture) { + _currentZoom.value = camera.zoom; + // Ha a felhasználó manuálisan mozgatja → kikapcsol a követés + if (hasGesture && _isFollowing.value) { + _isFollowing.value = false; + } + } + + void _centerOnPosition() { + final gnss = GnssService.to; + if (gnss.latitude.value == 0) return; + _mapController.move( + LatLng(gnss.latitude.value, gnss.longitude.value), + _currentZoom.value, + ); + _isFollowing.value = true; + } + + void _zoomIn() => + _mapController.move(_mapController.camera.center, _currentZoom.value + 1); + + void _zoomOut() => + _mapController.move(_mapController.camera.center, _currentZoom.value - 1); + + void _resetNorth() { + _mapController.rotate(0); + _isNorthUp.value = true; + } + + // ── GPS stream → térkép mozgatás ─────────────────────────────────── + + @override + Widget build(BuildContext context) { + return Obx(() { + final gnss = GnssService.to; + final lat = gnss.latitude.value; + final lon = gnss.longitude.value; + + if (_isFollowing.value && lat != 0 && lon != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _mapController.move(LatLng(lat, lon), _currentZoom.value); + }); + } + + return Stack( + children: [ + // ── Térkép ──────────────────────────────────────────────── + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: + lat != 0 ? LatLng(lat, lon) : const LatLng(47.5, 19.0), + initialZoom: widget.initialZoom, + maxZoom: 25, + minZoom: 3, + onLongPress: widget.onLongPress, + onPositionChanged: _onPositionChanged, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all, + ), + ), + children: [ + // 1. Alaptérkép + TileLayer( + urlTemplate: + 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', + subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'], + maxNativeZoom: 18, + ), + // 2. Extra rétegek (terepbejárás elemei) + ...widget.extraLayers, + // 3. Bemért pontok + if (Get.isRegistered()) + Obx(() => MarkerLayer( + markers: _buildMeasuredPointMarkers(), + )), + // 4. Kitűzési célpont + vonal + if (Get.isRegistered()) + Obx(() => _buildStakeoutLayer(lat, lon)), + // 5. GPS pozíció + Obx(() { + final lat = GnssService.to.latitude.value; + final lon = GnssService.to.longitude.value; + return MarkerLayer( + markers: lat == 0 && lon == 0 + ? [] + : [_buildCurrentPositionMarker(lat, lon)], + ); + }), + ], + ), + + // ── Vezérlők ────────────────────────────────────────────── + _MapControlsOverlay( + controls: widget.controls, + isFollowing: _isFollowing, + isNorthUp: _isNorthUp, + currentZoom: _currentZoom, + onZoomIn: _zoomIn, + onZoomOut: _zoomOut, + onCenterOnGps: _centerOnPosition, + onResetNorth: _resetNorth, + ), + + // ── Zoom szint jelzés (opcionális) ──────────────────────── + if (widget.controls.showZoomLevel) + Positioned( + bottom: 8, + left: 8, + child: Obx(() => _ZoomLabel(_currentZoom.value)), + ), + ], + ); + }); + } + + // ── Marker builder metódusok ──────────────────────────────────────── + + 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(); + } + + Widget _buildStakeoutLayer(double lat, double lon) { + if (!Get.isRegistered()) + return const SizedBox.shrink(); + final ctrl = MapSurveyController.to; + if (ctrl.mode.value != MapSurveyMode.stakeout || + ctrl.targetEovY.value == 0) { + return const SizedBox.shrink(); + } + + final wgs = CoordConverterService.to + .eovToWgsPoint(ctrl.targetEovY.value, ctrl.targetEovX.value); + final targetLatLng = LatLng(wgs.y, wgs.x); + + return Stack(children: [ + // Szaggatott vonal + PolylineLayer(polylines: [ + Polyline( + points: [LatLng(lat, lon), targetLatLng], + color: Colors.orange.withOpacity(0.85), + strokeWidth: 2.5, + ), + ]), + // Célpont marker + MarkerLayer(markers: [ + Marker( + point: targetLatLng, + width: 130, + height: 72, + alignment: Alignment.bottomCenter, + child: _LabeledMarker( + 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', + ), + ), + ]), + ]); + } + + Marker _buildCurrentPositionMarker(double lat, double lon) { + final color = switch (GnssService.to.gpsQuality.value) { + 4 => Colors.green, + 5 => Colors.lightGreen, + 2 => Colors.blue, + 1 => Colors.orange, + _ => Colors.grey, + }; + return Marker( + point: LatLng(lat, lon), + width: 24, + height: 24, + child: _PulsingDot(color: color), + ); + } +} + +// ─── Vezérlők konfigurációja ────────────────────────────────────────────────── + +class MapControls { + final bool showZoomButtons; + final bool showFollowButton; + final bool showNorthButton; + final bool showZoomLevel; + final bool showCompass; + + const MapControls({ + this.showZoomButtons = true, + this.showFollowButton = true, + this.showNorthButton = true, + this.showZoomLevel = true, + this.showCompass = false, + }); + + /// Terepbejárás módhoz — nincs follow (rajzolás közben szabad mozgás) + const MapControls.fieldTrip() + : showZoomButtons = true, + showFollowButton = false, + showNorthButton = true, + showZoomLevel = true, + showCompass = false; + + /// Navigáció módhoz — minden vezérlő + const MapControls.navigation() + : showZoomButtons = true, + showFollowButton = true, + showNorthButton = true, + showZoomLevel = false, + showCompass = true; + + /// Minimális — csak zoom + const MapControls.minimal() + : showZoomButtons = true, + showFollowButton = false, + showNorthButton = false, + showZoomLevel = false, + showCompass = false; +} + +// ─── Vezérlők overlay ──────────────────────────────────────────────────────── + +class _MapControlsOverlay extends StatelessWidget { + final MapControls controls; + final RxBool isFollowing; + final RxBool isNorthUp; + final RxDouble currentZoom; + final VoidCallback onZoomIn; + final VoidCallback onZoomOut; + final VoidCallback onCenterOnGps; + final VoidCallback onResetNorth; + + const _MapControlsOverlay({ + required this.controls, + required this.isFollowing, + required this.isNorthUp, + required this.currentZoom, + required this.onZoomIn, + required this.onZoomOut, + required this.onCenterOnGps, + required this.onResetNorth, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + right: 10, + bottom: 80, // BottomNav felett + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Iránytű / É-ra forgat ───────────────────────────── + if (controls.showNorthButton) + Obx(() => _ControlButton( + icon: Icons.navigation, + tooltip: 'Észak felfelé', + active: isNorthUp.value, + // A gomb elfordul ahogy a térkép forog — vizuális jelzés + child: Transform.rotate( + angle: 0, + child: const Icon(Icons.navigation, size: 20), + ), + onTap: onResetNorth, + )), + + if (controls.showNorthButton) const SizedBox(height: 6), + + // ── GPS követés ─────────────────────────────────────── + if (controls.showFollowButton) + Obx(() => _ControlButton( + tooltip: isFollowing.value + ? 'GPS követés aktív' + : 'GPS követés kikapcsolva', + active: isFollowing.value, + icon: + isFollowing.value ? Icons.gps_fixed : Icons.gps_not_fixed, + onTap: onCenterOnGps, + )), + + if (controls.showFollowButton) const SizedBox(height: 6), + + // ── Zoom gombok ─────────────────────────────────────── + if (controls.showZoomButtons) ...[ + _ControlButton( + icon: Icons.add, + tooltip: 'Nagyítás', + onTap: onZoomIn, + ), + const SizedBox(height: 2), + _ControlButton( + icon: Icons.remove, + tooltip: 'Kicsinyítés', + onTap: onZoomOut, + ), + ], + ], + ), + ); + } +} + +class _ControlButton extends StatelessWidget { + final IconData? icon; + final Widget? child; + final String tooltip; + final bool active; + final VoidCallback onTap; + + const _ControlButton({ + this.icon, + this.child, + required this.tooltip, + this.active = false, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Tooltip( + message: tooltip, + child: Material( + color: active + ? Theme.of(context).colorScheme.primaryContainer + : isDark + ? Colors.grey.shade800.withOpacity(0.9) + : Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + elevation: 3, + shadowColor: Colors.black38, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: SizedBox( + width: 42, + height: 42, + child: Center( + child: child ?? + Icon( + icon, + size: 20, + color: active + ? Theme.of(context).colorScheme.primary + : isDark + ? Colors.white70 + : Colors.black87, + ), + ), + ), + ), + ), + ); + } +} + +// ─── Zoom szint label ───────────────────────────────────────────────────────── + +class _ZoomLabel extends StatelessWidget { + final double zoom; + const _ZoomLabel(this.zoom); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + 'Z ${zoom.toStringAsFixed(1)}', + style: const TextStyle(color: Colors.white70, fontSize: 11), + ), + ); + } +} + +// ─── Feliratozott marker ────────────────────────────────────────────────────── + +class _LabeledMarker extends StatelessWidget { + final String label; + final IconData icon; + final Color color; + final Color? activeColor; + final String? sublabel; + + const _LabeledMarker({ + required this.label, + required this.icon, + required this.color, + this.activeColor, + this.sublabel, + }); + + @override + Widget build(BuildContext context) { + final c = activeColor ?? color; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: c, + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + if (sublabel != null) + Text(sublabel!, + style: TextStyle( + color: Colors.white.withOpacity(0.9), fontSize: 9)), + ], + ), + ), + Container(width: 2, height: 6, color: c), + Icon(icon, color: c, size: 22, shadows: const [ + Shadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)) + ]), + ], + ); + } +} + +// ─── Pulzáló GPS pont ───────────────────────────────────────────────────────── + +class _PulsingDot extends StatefulWidget { + final Color color; + const _PulsingDot({required this.color}); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + late Animation _scale; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 1000)) + ..repeat(reverse: true); + _scale = Tween(begin: 0.8, end: 1.2) + .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => ScaleTransition( + scale: _scale, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: widget.color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: widget.color.withOpacity(0.5), + blurRadius: 8, + spreadRadius: 2) + ], + ), + ), + ); +}