diff --git a/lib/models/measured_point.dart b/lib/models/measured_point.dart index def444e..1fa8ecd 100644 --- a/lib/models/measured_point.dart +++ b/lib/models/measured_point.dart @@ -1,13 +1,81 @@ class MeasuredPoint { + final int? id; + final int projectId; final String name; - final double latitude; - final double longitude; + final double? eovY; + final double? eovX; + final double? eovZ; + final double? latitude; + final double? longitude; final double? altitude; + final double? accuracy; + final int? fixQuality; + final DateTime timestamp; + final String note; const MeasuredPoint({ + this.id, + required this.projectId, required this.name, - required this.latitude, - required this.longitude, + this.eovY, + this.eovX, + this.eovZ, + this.latitude, + this.longitude, this.altitude, + this.accuracy, + this.fixQuality, + required this.timestamp, + this.note = '', }); + + Map toMap() => { + if (id != null) 'id': id, + 'project_id': projectId, + 'name': name, + 'eov_y': eovY, + 'eov_x': eovX, + 'eov_z': eovZ, + 'latitude': latitude, + 'longitude': longitude, + 'altitude': altitude, + 'accuracy': accuracy, + 'fix_quality': fixQuality, + 'timestamp': timestamp.toIso8601String(), + 'note': note, + }; + + factory MeasuredPoint.fromMap(Map m) => MeasuredPoint( + id: m['id'] as int?, + projectId: m['project_id'] as int, + name: m['name'] as String, + eovY: (m['eov_y'] as num?)?.toDouble(), + eovX: (m['eov_x'] as num?)?.toDouble(), + eovZ: (m['eov_z'] as num?)?.toDouble(), + latitude: (m['latitude'] as num?)?.toDouble(), + longitude: (m['longitude'] as num?)?.toDouble(), + altitude: (m['altitude'] as num?)?.toDouble(), + accuracy: (m['accuracy'] as num?)?.toDouble(), + fixQuality: m['fix_quality'] as int?, + timestamp: DateTime.parse(m['timestamp'] as String), + note: m['note'] as String? ?? '', + ); + + // ── CSV sor ─────────────────────────────────────────────────────── + + static String csvHeader() => 'Név;EOV Y;EOV X;EOV Z (tszf);' + 'Lat (WGS84);Lon (WGS84);Magasság (m);' + 'Pontosság (m);Fix minőség;Megjegyzés;Időbélyeg'; + + String toCsvRow() { + String fmt(double? v, int dec) => v == null ? '' : v.toStringAsFixed(dec); + return '${_csv(name)};' + '${fmt(eovY, 1)};${fmt(eovX, 1)};${fmt(eovZ, 3)};' + '${fmt(latitude, 8)};${fmt(longitude, 8)};${fmt(altitude, 3)};' + '${fmt(accuracy, 4)};${fixQuality ?? ""};' + '${_csv(note)};${timestamp.toIso8601String()}'; + } + + /// Pontosvesszőt tartalmazó mezőket idézőjelbe teszi + String _csv(String v) => v.contains(';') ? '"$v"' : v; } diff --git a/lib/models/point_with_description_model.dart b/lib/models/point_with_description_model.dart index c6888eb..426326f 100644 --- a/lib/models/point_with_description_model.dart +++ b/lib/models/point_with_description_model.dart @@ -1,5 +1,5 @@ class PointWithDescription { - final int Id; + final int pointId; final DateTime dateTime; final String description; final double eovX; @@ -10,7 +10,7 @@ class PointWithDescription { final double verticalError; PointWithDescription( - this.Id, + this.pointId, this.dateTime, this.description, this.eovX, diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index 8e4d0e3..ad3e647 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'dart:math' as math; +//import 'dart:math' as math; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -28,6 +28,7 @@ import 'package:terepi_seged/enums/map_survey_mode.dart'; import 'package:terepi_seged/enums/note_type.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; +import 'package:terepi_seged/models/measured_point.dart'; import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_with_description_model.dart'; @@ -47,6 +48,8 @@ import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; +import '../views/measured_points_sheet.dart'; + class MapSurveyController extends GetxController { static MapSurveyController get to => Get.find(); @@ -274,6 +277,7 @@ class MapSurveyController extends GetxController { ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); await _loadNoteItems(); + await _loadMeasurePoints(); } @override @@ -719,9 +723,9 @@ class MapSurveyController extends GetxController { 'heightOfGeoid': _gnss.geoidSeparation.value, 'eovX': eov.value.X, 'eovY': eov.value.Y, + 'eovZ': eovHeight.value, 'poleHeight': double.tryParse(gpsHeightController.text), - 'horizontalError': - max(_gnss.latitudeError.value, _gnss.longitudeError.value), + 'horizontalError': _gnss.horizontalAccuracy, 'verticalError': _gnss.altitudeError.value, 'description': pointDescriptionController.text, 'isDeleted': false, @@ -732,6 +736,26 @@ class MapSurveyController extends GetxController { .from('TerepiSeged_Receiver') .update({'isMeasured': true}).eq('pointNumber', pointId); +// SQLite mentés + final projectId = ProjectService.to.activeProjectId; + if (projectId != null) { + await AppDatabase.instance.insertMeasuredPoint(MeasuredPoint( + projectId: projectId, + name: pointId.toString(), + eovY: eov.value.Y, + eovX: eov.value.X, + eovZ: eovHeight.value! - + (double.tryParse(gpsHeightController.text) ?? 0.0), + latitude: _gnss.latitude.value, + longitude: _gnss.longitude.value, + altitude: _gnss.altitude.value, + accuracy: _gnss.horizontalAccuracy, + fixQuality: _gnss.gpsQuality.value, + timestamp: DateTime.now(), + note: pointDescriptionController.text, + )); + } + // Következő pont léptetése _advancePointSelection(); @@ -1243,6 +1267,51 @@ class MapSurveyController extends GetxController { } } + Future _loadMeasurePoints() async { + final projectId = ProjectService.to.activeProject.value?.id; + + pointNotesMarker.clear(); + pointWithDescriptionList.clear(); + + if (projectId == null) return; + + final points = await AppDatabase.instance.listMeasuredPoints(projectId); + + for (final pt in points) { + if (pt.latitude != null && pt.longitude != null) { + pointNotesMarker.add(Marker( + point: LatLng(pt.latitude!, pt.longitude!), + width: 15.0, + height: 15.0, + child: Container( + width: 15.0, + height: 15.0, + decoration: BoxDecoration( + color: Colors.amber[700], + shape: BoxShape.circle, + border: Border.all(width: 1.0, color: Colors.black)), + ))); + } + pointWithDescriptionList.add(PointWithDescription( + int.tryParse(pt.name) ?? 0, + pt.timestamp, + pt.note, + pt.eovY ?? 0.0, + pt.eovX ?? 0.0, + pt.latitude ?? 0.0, + pt.longitude ?? 0.0, + pt.accuracy ?? 0.0, + 0.0)); + } + + if (points.isNotEmpty) { + final maxId = points + .map((p) => int.tryParse(p.name) ?? 0) + .fold(0, (prev, id) => id > prev ? id : prev); + pointId = maxId + 1; + } + } + Future finishDraft() async { if (editingNoteItemId != null) { if (polygonEditorController.points.isEmpty) { @@ -1702,4 +1771,20 @@ class MapSurveyController extends GetxController { ), ); } + + void openMeasuredPointsList() { + Get.bottomSheet( + DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.92, + snap: true, + expand: false, + builder: (_, __) => const MeasuredPointsSheet(), + ), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ignoreSafeArea: false, + ); + } } diff --git a/lib/pages/map_survey/presentations/views/map_survey_view.dart b/lib/pages/map_survey/presentations/views/map_survey_view.dart index 4dc0ba1..753ff4b 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -14,6 +14,7 @@ import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/utils/rive_utils.dart'; import 'package:terepi_seged/widgets/coordinate_panel.dart'; import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; +import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart'; import 'package:terepi_seged/widgets/map_bottom_panel.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart'; @@ -86,6 +87,12 @@ class MapSurveyView extends GetView { return _buildTrackLayer(); } }), + Obx(() { + if (controller.mode.value != MapSurveyMode.measure) { + return SizedBox.shrink(); + } + return MarkerLayer(markers: controller.pointNotesMarker.toList()); + }), Obx(() { if (controller.mode.value != MapSurveyMode.fieldWalk) { return const SizedBox.shrink(); @@ -208,6 +215,16 @@ class MapSurveyView extends GetView { right: 0, bottom: 0, child: MapEditDrawingToolbar(controller: controller)); + }), + Obx(() { + if (controller.mode.value != MapSurveyMode.measure) { + return const SizedBox.shrink(); + } + return Positioned( + left: 0, + right: 0, + bottom: 0, + child: MeasureBottomPanel(ctrl: controller)); }) // Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), // Positioned( diff --git a/lib/pages/map_survey/presentations/views/measured_points_sheet.dart b/lib/pages/map_survey/presentations/views/measured_points_sheet.dart new file mode 100644 index 0000000..b97d15c --- /dev/null +++ b/lib/pages/map_survey/presentations/views/measured_points_sheet.dart @@ -0,0 +1,362 @@ +// Bemért pontok listája: +// - Pont neve + EOV Y/X +// - Törlés swipe-pal +// - CSV export (share_plus) +// - SQLite alapú (nem memória) + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../controllers/map_survey_controller.dart'; +import '../../../../../../models/measured_point.dart'; +import '../../../../../../services/app_database.dart'; +import '../../../../../../services/project_service.dart'; + +class MeasuredPointsSheet extends StatefulWidget { + const MeasuredPointsSheet({super.key}); + + @override + State createState() => _MeasuredPointsSheetState(); +} + +class _MeasuredPointsSheetState extends State { + List _points = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final projectId = ProjectService.to.activeProjectId; + if (projectId == null) { + setState(() => _loading = false); + return; + } + final pts = await AppDatabase.instance.listMeasuredPoints(projectId); + if (mounted) + setState(() { + _points = pts; + _loading = false; + }); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 16, + offset: const Offset(0, -4)), + ], + ), + child: Column( + children: [ + // ── Handle ─────────────────────────────────────────────── + Center( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 10), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2)), + ), + ), + + // ── Fejléc ─────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 12, 8), + child: Row(children: [ + const Icon(Icons.location_on, size: 20), + const SizedBox(width: 8), + Text('Bemért pontok', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + const SizedBox(width: 6), + if (!_loading) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: cs.primaryContainer, + borderRadius: BorderRadius.circular(10), + ), + child: Text('${_points.length}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: cs.onPrimaryContainer)), + ), + const Spacer(), + // CSV export + if (_points.isNotEmpty) + IconButton( + icon: const Icon(Icons.download_outlined), + tooltip: 'CSV export', + onPressed: _exportCsv, + ), + // Összes törlése + if (_points.isNotEmpty) + IconButton( + icon: Icon(Icons.delete_sweep_outlined, + color: Colors.red.shade300), + tooltip: 'Összes törlése', + onPressed: _deleteAll, + ), + ]), + ), + + const Divider(height: 1), + + // ── Lista ───────────────────────────────────────────────── + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : _points.isEmpty + ? _emptyState() + : Column(children: [ + // Fejléc sor + _TableHeader(), + const Divider(height: 1), + Expanded( + child: ListView.separated( + itemCount: _points.length, + separatorBuilder: (_, __) => + const Divider(height: 1), + itemBuilder: (_, i) => _PointTile( + point: _points[i], + onDelete: () => _deleteOne(_points[i]), + ), + ), + ), + ]), + ), + + // Alsó padding + SizedBox(height: MediaQuery.of(context).padding.bottom + 8), + ], + ), + ); + } + + // ── Törlés ──────────────────────────────────────────────────────── + + Future _deleteOne(MeasuredPoint pt) async { + final ok = await Get.dialog(AlertDialog( + title: const Text('Pont törlése'), + content: Text('"${pt.name}" törlése visszavonohatalan.'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('Mégse')), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Get.back(result: true), + child: const Text('Törlés'), + ), + ], + )); + if (ok != true) return; + await AppDatabase.instance.deleteMeasuredPoint(pt.id!); + setState(() => _points.remove(pt)); + + // Controller marker listából is eltávolítjuk + final ctrl = Get.find(); + + if (pt.latitude != null && pt.longitude != null) { + ctrl.pointNotesMarker.removeWhere((m) => + (m.point.latitude - pt.latitude!).abs() < 1e-9 && + (m.point.longitude - pt.longitude!).abs() < 1e-9); + } + + ctrl.pointWithDescriptionList + .removeWhere((p) => p.pointId.toString() == pt.name); + } + + Future _deleteAll() async { + final ok = await Get.dialog(AlertDialog( + title: const Text('Összes pont törlése'), + content: Text('${_points.length} pont törlése visszavonohatalan.'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('Mégse')), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Get.back(result: true), + child: const Text('Összes törlése'), + ), + ], + )); + if (ok != true) return; + + final projectId = ProjectService.to.activeProjectId; + if (projectId != null) { + await AppDatabase.instance.deleteAllMeasuredPoints(projectId); + } + final ctrl = Get.find(); + ctrl.pointWithDescriptionList.clear(); + ctrl.pointNotesMarker.clear(); + + setState(() => _points.clear()); + } + + // ── CSV export ──────────────────────────────────────────────────── + + Future _exportCsv() async { + final buf = StringBuffer(); + buf.writeln(MeasuredPoint.csvHeader()); + for (final pt in _points) { + buf.writeln(pt.toCsvRow()); + } + + final projectName = + ProjectService.to.activeProject.value?.name ?? 'projekt'; + final ts = + DateTime.now().toIso8601String().replaceAll(':', '-').substring(0, 16); + final name = '${projectName}_bemert_pontok_$ts.csv'; + final dir = await getTemporaryDirectory(); + final file = File('${dir.path}/$name'); + await file.writeAsString(buf.toString(), encoding: utf8); + await Share.shareXFiles( + [XFile(file.path, mimeType: 'text/csv; charset=utf-8')], + subject: 'Bemért pontok — $projectName', + ); + } + + Widget _emptyState() => Center( + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.location_off_outlined, + size: 56, color: Colors.grey.shade300), + const SizedBox(height: 12), + Text('Még nincs bemért pont', + style: TextStyle(color: Colors.grey.shade500)), + ]), + ); +} + +// ─── Fejléc sor ────────────────────────────────────────────────────────────── + +class _TableHeader extends StatelessWidget { + const _TableHeader(); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final style = TextStyle( + fontSize: 11, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant); + return Container( + color: cs.surfaceContainerHighest, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row(children: [ + SizedBox(width: 52, child: Text('Pont', style: style)), + Expanded(child: Text('EOV Y', style: style)), + Expanded(child: Text('EOV X', style: style)), + SizedBox( + width: 72, + child: Text('Leírás', style: style, textAlign: TextAlign.center)), + const SizedBox(width: 40), // törlés gomb helye + ]), + ); + } +} + +// ─── Egy pont sor ──────────────────────────────────────────────────────────── + +class _PointTile extends StatelessWidget { + final MeasuredPoint point; + final VoidCallback onDelete; + const _PointTile({required this.point, required this.onDelete}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final hasEov = point.eovY != null && point.eovX != null; + final accColor = _accColor(point.accuracy); + + return Dismissible( + key: ValueKey(point.id), + direction: DismissDirection.endToStart, + confirmDismiss: (_) async { + onDelete(); + return false; // mi kezeljük a törlést + }, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row(children: [ + // Pont neve + SizedBox( + width: 52, + child: Text(point.name, + style: + const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + overflow: TextOverflow.ellipsis), + ), + // EOV Y + Expanded( + child: Text( + hasEov ? point.eovY!.toStringAsFixed(1) : '–', + style: const TextStyle( + fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]), + ), + ), + // EOV X + Expanded( + child: Text( + hasEov ? point.eovX!.toStringAsFixed(1) : '–', + style: const TextStyle( + fontSize: 13, fontFeatures: [FontFeature.tabularFigures()]), + ), + ), + // Pontosság + SizedBox( + width: 72, + child: Text( + point.note.isEmpty ? '---' : point.note, + style: TextStyle( + fontSize: 12, + color: point.note.isEmpty ? Colors.grey.shade400 : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + // Törlés gomb + SizedBox( + width: 40, + child: IconButton( + icon: Icon(Icons.delete_outline, + size: 18, color: Colors.grey.shade400), + onPressed: onDelete, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + ), + ), + ]), + ), + ); + } + + Color _accColor(double? acc) { + if (acc == null) return Colors.grey; + if (acc <= 0.05) return Colors.green; + if (acc <= 0.20) return Colors.lightGreen.shade700; + if (acc <= 1.00) return Colors.orange; + return Colors.red; + } +} diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index 2c4a8d5..893a295 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -5,6 +5,7 @@ import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import 'package:terepi_seged/enums/note_type.dart'; import 'package:terepi_seged/models/imported_layer_meta.dart'; +import 'package:terepi_seged/models/measured_point.dart'; import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/note_item_audio.dart'; import 'package:terepi_seged/models/note_item_photo.dart'; @@ -605,4 +606,33 @@ class AppDatabase { ); return rows.map(ImportedLayerMeta.fromMap).toList(); } + + // ------------ Bemért pontok + + Future insertMeasuredPoint(MeasuredPoint point) async { + final db = await database; + return db.insert('measured_points', point.toMap()); + } + + Future deleteMeasuredPoint(int id) async { + final db = await database; + await db.delete('measured_points', where: 'id = ?', whereArgs: [id]); + } + + Future deleteAllMeasuredPoints(int projectId) async { + final db = await database; + await db.delete('measured_points', + where: 'project_id = ?', whereArgs: [projectId]); + } + + Future> listMeasuredPoints(int projectId) async { + final db = await database; + final rows = await db.query( + 'measured_points', + where: 'project_id = ?', + whereArgs: [projectId], + orderBy: 'timestamp ASC', + ); + return rows.map(MeasuredPoint.fromMap).toList(); + } } diff --git a/lib/services/gnss/gnss_service.dart b/lib/services/gnss/gnss_service.dart index 8c1ab1c..9baa2dc 100644 --- a/lib/services/gnss/gnss_service.dart +++ b/lib/services/gnss/gnss_service.dart @@ -255,6 +255,7 @@ class GnssService extends GetxService { vdop.value = 0; fixType.value = 0; lastGgaLine.value = ''; + gpsQuality.value = 0; } Future disconnect() => _disconnect(); diff --git a/lib/widgets/map/measure_bottom_panel.dart b/lib/widgets/map/measure_bottom_panel.dart new file mode 100644 index 0000000..b52e938 --- /dev/null +++ b/lib/widgets/map/measure_bottom_panel.dart @@ -0,0 +1,266 @@ +// Bemérés mód alsó panel: +// - Aktuális koordináták (WGS84 + EOV) +// - GPS minőség + pontosság +// - Rögzített pontok száma +// - Nagy "Pont rögzítése" gomb + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/services/gnss/gnss_connection.dart'; +import 'package:terepi_seged/services/gnss/gnss_service.dart'; + +import '../../pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class MeasureBottomPanel extends StatelessWidget { + final MapSurveyController ctrl; + const MeasureBottomPanel({super.key, required this.ctrl}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Container( + decoration: BoxDecoration( + color: cs.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 12, + offset: const Offset(0, -3), + ), + ], + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Koordináta sor ───────────────────────────────────── + Obx(() { + final lat = ctrl.gpsLatitude.value; + final lon = ctrl.gpsLongitude.value; + final eovY = ctrl.eovY.value; + final eovX = ctrl.eovX.value; + final hasGps = GnssService.to.connectionState.value == + GnssConnectionState.connected; + + if (!hasGps) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + 'GPS pozíció nem elérhető', + style: + TextStyle(color: cs.onSurfaceVariant, fontSize: 12), + ), + ); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + // WGS84 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('WGS84', + style: TextStyle( + fontSize: 10, + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500)), + Text( + '${lat.toStringAsFixed(7)}°', + style: const TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()]), + ), + Text( + '${lon.toStringAsFixed(7)}°', + style: const TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()]), + ), + ], + ), + ), + + Container( + width: 1, + height: 36, + color: cs.outlineVariant, + margin: const EdgeInsets.symmetric(horizontal: 10)), + + // EOV + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('EOV', + style: TextStyle( + fontSize: 10, + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500)), + Text( + 'Y: ${eovY == 0 ? "–" : eovY.toStringAsFixed(1)}', + style: const TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()]), + ), + Text( + 'X: ${eovX == 0 ? "–" : eovX.toStringAsFixed(1)}', + style: const TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()]), + ), + ], + ), + ), + + Container( + width: 1, + height: 36, + color: cs.outlineVariant, + margin: const EdgeInsets.symmetric(horizontal: 10)), + + // Pontosság + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pontosság', + style: TextStyle( + fontSize: 10, + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500)), + Obx(() => Text( + 'H: ${ctrl.horizontalAccuracyText}', + style: TextStyle( + fontSize: 12, + color: _accuracyColor( + ctrl.gpsLatitudeError.value, + ctrl.gpsLongitudeError.value), + fontWeight: FontWeight.w600, + fontFeatures: const [ + FontFeature.tabularFigures() + ], + ), + )), + Obx(() => Text( + 'V: ${ctrl.verticalAccuracyText}', + style: TextStyle( + fontSize: 12, + color: _accuracyColor( + ctrl.gpsAltitudeError.value, + ctrl.gpsAltitudeError.value), + fontWeight: FontWeight.w600, + fontFeatures: const [ + FontFeature.tabularFigures() + ], + ), + )), + ], + ), + ], + ), + ); + }), + + const Divider(height: 1), + const SizedBox(height: 8), + + // ── Minőség chip + rögzített pontok + gomb ───────────── + Row( + children: [ + // GPS minőség chip + Obx(() { + final q = ctrl.gpsQuality.value; + final color = ctrl.getCurrentLocationMarkerColor(q); + final label = ctrl.getGpsQualityIndicator(quality: q); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.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(label, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w600)), + ]), + ); + }), + + const SizedBox(width: 8), + + // Rögzített pontok száma + Obx(() { + final count = ctrl.pointWithDescriptionList.length; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(6), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.location_on, + size: 13, color: Colors.amber.shade700), + const SizedBox(width: 4), + Text('$count pont', + style: const TextStyle( + fontSize: 11, fontWeight: FontWeight.w500)), + ]), + ); + }), + + const Spacer(), + IconButton( + onPressed: ctrl.openMeasuredPointsList, + icon: const Icon(Icons.list_alt_outlined), + tooltip: 'Bemért pontok'), + const SizedBox(width: 4), + + // Pont rögzítése gomb + Obx(() { + final hasGps = GnssService.to.connectionState.value == + GnssConnectionState.connected; + return FilledButton.icon( + onPressed: hasGps ? ctrl.showAddPointDialog : null, + icon: const Icon(Icons.add_location_alt, size: 18), + label: const Text('Rögzítés'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 10), + ), + ); + }) + ], + ), + ], + ), + ), + ), + ); + } + + Color _accuracyColor(double e1, double e2) { + final e = e1 > e2 ? e1 : e2; + if (e <= 0.05) return Colors.green; + if (e <= 0.20) return Colors.lightGreen; + if (e <= 1.00) return Colors.orange; + return Colors.red; + } +} diff --git a/lib/widgets/map_info_card_column.dart b/lib/widgets/map_info_card_column.dart index 853f61b..db722b2 100644 --- a/lib/widgets/map_info_card_column.dart +++ b/lib/widgets/map_info_card_column.dart @@ -24,13 +24,16 @@ class MapInfoCardColumn extends StatelessWidget { return Obx(() { final cards = []; - if (controller.showWgs84Card.value) { + if (controller.showWgs84Card.value && + controller.mode.value == MapSurveyMode.browse) { cards.add(Wgs84CoordinateCard(controller: controller)); } - if (controller.showEovCard.value) { + if (controller.showEovCard.value && + controller.mode.value == MapSurveyMode.browse) { cards.add(EovCoordinateCard(controller: controller)); } - if (controller.showGnssQualityCard.value) { + if (controller.showGnssQualityCard.value && + controller.mode.value == MapSurveyMode.browse) { cards.add(GnssQualityCard(controller: controller)); } diff --git a/lib/widgets/shared_map_widgets.dart b/lib/widgets/shared_map_widgets.dart index 10a353f..c64d586 100644 --- a/lib/widgets/shared_map_widgets.dart +++ b/lib/widgets/shared_map_widgets.dart @@ -91,8 +91,8 @@ class SharedMapWidget extends StatelessWidget { ), if (controls.showZoomLevel && currentZoom != null) Positioned( - bottom: 8, - left: 8, + bottom: 150, + left: 4, child: Obx(() => _ZoomLabel(currentZoom!.value)), ), ], @@ -163,7 +163,7 @@ class _MapControlsOverlay extends StatelessWidget { Widget build(BuildContext context) { return Positioned( right: 10, - bottom: 80, + bottom: 150, child: Column( mainAxisSize: MainAxisSize.min, children: [