Új térkép widget, melyet több oldal is használ
This commit is contained in:
parent
7dd642b299
commit
f2457817b2
13
lib/models/measured_point.dart
Normal file
13
lib/models/measured_point.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
lib/pages/map_survey/presentations/views/map_survey_view.dart
Normal file
203
lib/pages/map_survey/presentations/views/map_survey_view.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
319
lib/widgets/coordinate_panel.dart
Normal file
319
lib/widgets/coordinate_panel.dart
Normal 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;
|
||||||
|
}
|
||||||
188
lib/widgets/gnss_status_chip.dart
Normal file
188
lib/widgets/gnss_status_chip.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
324
lib/widgets/save_point_fab.dart
Normal file
324
lib/widgets/save_point_fab.dart
Normal 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: [
|
||||||
|
// ── 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<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',
|
||||||
|
};
|
||||||
579
lib/widgets/shared_map_widgets.dart
Normal file
579
lib/widgets/shared_map_widgets.dart
Normal 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)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user