Új térkép widget, melyet több oldal is használ

This commit is contained in:
torok.istvan 2026-05-12 00:14:11 +02:00
parent 7dd642b299
commit f2457817b2
11 changed files with 1718 additions and 434 deletions

View File

@ -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,
});
}

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math' as math;
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:file_picker/file_picker.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/gngga.dart';
import 'package:terepi_seged/gnss_sentences/gngst.dart'; import 'package:terepi_seged/gnss_sentences/gngst.dart';
import 'package:terepi_seged/gnss_sentences/gnrmc.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_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:proj4dart/proj4dart.dart' as proj4; 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:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart';
import 'package:shared_preferences/shared_preferences.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 { class MapSurveyController extends GetxController {
// String gpsAddress = "E8:31:CD:14:8B:B2"; // String gpsAddress = "E8:31:CD:14:8B:B2";
// String gpsAddress = "98:CD:AC:62:FF:4E"; // String gpsAddress = "98:CD:AC:62:FF:4E";
@ -140,6 +146,32 @@ class MapSurveyController extends GetxController {
late Session? session; late Session? session;
late User? user; 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 = <MeasuredPoint>[].obs;
static MapSurveyController get to => Get.find();
@override @override
void onInit() async { void onInit() async {
super.onInit(); super.onInit();
@ -741,7 +773,8 @@ class MapSurveyController extends GetxController {
eov.value.X, eov.value.X,
gpsLatitude.value, gpsLatitude.value,
gpsLongitude.value, gpsLongitude.value,
max(gpsLatitudeError.value, gpsLongitudeError.value), math.max(
gpsLatitudeError.value, gpsLongitudeError.value),
gpsAltitudeError.value)); gpsAltitudeError.value));
print( print(
"pointWithDescriptionList -> ${pointWithDescriptionList.length}"); "pointWithDescriptionList -> ${pointWithDescriptionList.length}");
@ -760,7 +793,7 @@ class MapSurveyController extends GetxController {
Border.all(width: 1.0, color: Colors.black)), Border.all(width: 1.0, color: Colors.black)),
))); )));
await dataFile.writeAsString( 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); mode: FileMode.append);
_measuredPoints.add({ _measuredPoints.add({
@ -772,8 +805,8 @@ class MapSurveyController extends GetxController {
"eovY": formatEovForFile.format(eov.value.Y), "eovY": formatEovForFile.format(eov.value.Y),
"eovX": formatEovForFile.format(eov.value.X), "eovX": formatEovForFile.format(eov.value.X),
"description": pointDescriptionController.text, "description": pointDescriptionController.text,
"horizontalError": "horizontalError": math.max(
max(gpsLatitudeError.value, gpsLongitudeError.value), gpsLatitudeError.value, gpsLongitudeError.value),
"verticalError": gpsAltitudeError.value, "verticalError": gpsAltitudeError.value,
"gpsHeight": gpsHeightController.text "gpsHeight": gpsHeightController.text
}); });
@ -790,8 +823,8 @@ class MapSurveyController extends GetxController {
'eovX': eov.value.X, 'eovX': eov.value.X,
'eovY': eov.value.Y, 'eovY': eov.value.Y,
'poleHeight': double.tryParse(gpsHeightController.text), 'poleHeight': double.tryParse(gpsHeightController.text),
'horizontalError': 'horizontalError': math.max(
max(gpsLatitudeError.value, gpsLongitudeError.value), gpsLatitudeError.value, gpsLongitudeError.value),
'verticalError': gpsAltitudeError.value, 'verticalError': gpsAltitudeError.value,
'description': pointDescriptionController.text, 'description': pointDescriptionController.text,
'isDeleted': false, 'isDeleted': false,
@ -932,4 +965,37 @@ class MapSurveyController extends GetxController {
final result = await SharePlus.instance.share(params); 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<void> 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);
}
} }

View File

@ -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<MapSurveyController> {
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<MapSurveyController> {
const _ModeSelector();
@override
Widget build(BuildContext context) {
return Obx(() => SegmentedButton<MapSurveyMode>(
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<MapSurveyController> {
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,
),
),
],
);
}
}

View File

@ -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<MapSurveyController> {
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),
),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:terepi_seged/pages/home/presentation/controllers/home_controller.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/presentation/controllers/map_controller.dart';
import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
import '../presentations/controllers/shell_controller.dart'; import '../presentations/controllers/shell_controller.dart';
@ -11,5 +12,6 @@ class ShellBinding extends Bindings {
Get.put(ShellController()); Get.put(ShellController());
Get.put(HomeViewController()); Get.put(HomeViewController());
Get.put(MapViewController()); Get.put(MapViewController());
Get.put(MapSurveyController());
} }
} }

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:terepi_seged/pages/home/presentation/views/home_view.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 '../../../../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'; import '../controllers/shell_controller.dart';
class ShellView extends GetView<ShellController> { class ShellView extends GetView<ShellController> {
@ -24,8 +26,19 @@ class ShellView extends GetView<ShellController> {
// Cím reaktívan frissül tab váltáskor // Cím reaktívan frissül tab váltáskor
title: Obx(() => Text(controller.currentTitle)), title: Obx(() => Text(controller.currentTitle)),
actions: [ actions: [
//Obx(() => _GnssStatusChip()), const GnssStatusChip(),
const SizedBox(width: 8), 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(), drawer: const AppDrawer(),

View File

@ -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_add_point_dialog.dart';
import 'package:terepi_seged/pages/map/presentation/views/map_view.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/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/bindings/measured_data_bindings.dart';
import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart'; import 'package:terepi_seged/pages/measured_data/presentation/views/measured_data_view.dart';
import 'package:terepi_seged/pages/navigation/bindings/navigation_bindings.dart'; import 'package:terepi_seged/pages/navigation/bindings/navigation_bindings.dart';

View File

@ -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;
}

View File

@ -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,
),
),
]),
),
);
});
}
}

View File

@ -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: [
// 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<void> _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',
};

View File

@ -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<Widget> 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<SharedMapWidget> createState() => _SharedMapWidgetState();
}
class _SharedMapWidgetState extends State<SharedMapWidget> {
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<MapSurveyController>())
Obx(() => MarkerLayer(
markers: _buildMeasuredPointMarkers(),
)),
// 4. Kitűzési célpont + vonal
if (Get.isRegistered<MapSurveyController>())
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<Marker> _buildMeasuredPointMarkers() {
if (!Get.isRegistered<MapSurveyController>()) 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<MapSurveyController>())
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<double> _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)
],
),
),
);
}