2026-05-12 00:14:11 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2026-06-16 14:12:44 +02:00
|
|
|
import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor.dart';
|
2026-05-12 00:14:11 +02:00
|
|
|
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';
|
2026-06-16 14:12:44 +02:00
|
|
|
import 'package:terepi_seged/enums/map_edit_tool.dart';
|
2026-05-27 15:04:46 +02:00
|
|
|
import 'package:terepi_seged/enums/map_survey_mode.dart';
|
2026-05-12 00:14:11 +02:00
|
|
|
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';
|
2026-06-11 01:20:55 +02:00
|
|
|
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
|
2026-06-20 00:15:41 +02:00
|
|
|
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
|
|
|
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
2026-05-12 00:14:11 +02:00
|
|
|
import 'package:terepi_seged/utils/rive_utils.dart';
|
|
|
|
|
import 'package:terepi_seged/widgets/coordinate_panel.dart';
|
2026-06-21 10:07:35 +02:00
|
|
|
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
|
2026-06-21 12:33:57 +02:00
|
|
|
import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart';
|
2026-05-17 00:35:21 +02:00
|
|
|
import 'package:terepi_seged/widgets/map_bottom_panel.dart';
|
2026-06-16 14:12:44 +02:00
|
|
|
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';
|
2026-06-07 11:45:15 +02:00
|
|
|
import 'package:terepi_seged/widgets/map_info_card_column.dart';
|
2026-05-12 00:14:11 +02:00
|
|
|
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: [
|
2026-05-24 14:50:31 +02:00
|
|
|
SharedMapWidget(
|
2026-06-20 00:15:41 +02:00
|
|
|
controls: const MapControls(),
|
2026-05-24 14:50:31 +02:00
|
|
|
mapController: controller.mapController,
|
2026-06-20 00:15:41 +02:00
|
|
|
isFollowing: controller.isMapMoveToCenter,
|
|
|
|
|
initialCenter: LatLng(
|
|
|
|
|
// ← nem fix érték
|
|
|
|
|
controller.currentLatitude.value != 0.0
|
|
|
|
|
? controller.currentLatitude.value
|
|
|
|
|
: 47.5,
|
|
|
|
|
controller.currentLongitude.value != 0.0
|
|
|
|
|
? controller.currentLongitude.value
|
|
|
|
|
: 19.0,
|
|
|
|
|
),
|
|
|
|
|
initialZoom: controller.currentZoom.value,
|
2026-05-24 14:50:31 +02:00
|
|
|
currentZoom: controller.currentZoom,
|
|
|
|
|
onZoomIn: controller.mapZoomIn,
|
|
|
|
|
onZoomOut: controller.mapZoomOut,
|
2026-06-20 00:15:41 +02:00
|
|
|
onPositionChanged: controller.onMapPositionChanged,
|
|
|
|
|
onCenterOnGps: controller.setIsMapMoveToCenter,
|
2026-06-16 14:12:44 +02:00
|
|
|
onLongPress: (tapPosition, point) {
|
|
|
|
|
if (controller.activeEditTool.value == MapEditTool.point) {
|
|
|
|
|
controller.saveEditedPoint(point: point);
|
|
|
|
|
}
|
|
|
|
|
if (controller.activeEditTool.value == MapEditTool.line ||
|
|
|
|
|
controller.activeEditTool.value == MapEditTool.polygon) {
|
|
|
|
|
controller.polygonEditorController.addPoint(point);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-19 12:53:50 +02:00
|
|
|
onTap: (tapPosition, point) {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) return;
|
|
|
|
|
if (controller.isMapEditing) return;
|
|
|
|
|
final polygonHit = controller.polygonHitNotifier.value;
|
|
|
|
|
if (polygonHit != null && polygonHit.hitValues.isNotEmpty) {
|
|
|
|
|
final id = polygonHit.hitValues.first;
|
|
|
|
|
controller.selectedNoteItem(id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final polylineHit = controller.polylineHitNotifier.value;
|
|
|
|
|
if (polylineHit != null && polylineHit.hitValues.isNotEmpty) {
|
|
|
|
|
final id = polylineHit.hitValues.first;
|
|
|
|
|
controller.selectedNoteItem(id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
controller.clearNoteItemSelection();
|
|
|
|
|
},
|
2026-05-28 13:24:16 +02:00
|
|
|
layers: [
|
2026-06-21 10:07:35 +02:00
|
|
|
const ImportedLayerOverlay(),
|
2026-06-11 01:20:55 +02:00
|
|
|
// Track polyline
|
|
|
|
|
Obx(() {
|
|
|
|
|
final isTracking = TrackingController.to.isRecording.value;
|
|
|
|
|
final inTrackMode = controller.mode.value == MapSurveyMode.track;
|
|
|
|
|
if (!isTracking && !inTrackMode) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
} else {
|
|
|
|
|
return _buildTrackLayer();
|
|
|
|
|
}
|
2026-06-16 14:12:44 +02:00
|
|
|
}),
|
2026-06-21 12:33:57 +02:00
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.measure) {
|
|
|
|
|
return SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
return MarkerLayer(markers: controller.pointNotesMarker.toList());
|
|
|
|
|
}),
|
2026-06-16 14:12:44 +02:00
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
2026-06-19 12:53:50 +02:00
|
|
|
|
|
|
|
|
return MarkerLayer(markers: [...controller.pointNotes]);
|
2026-06-16 14:12:44 +02:00
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 12:53:50 +02:00
|
|
|
return PolylineLayer(
|
|
|
|
|
hitNotifier: controller.polylineHitNotifier,
|
|
|
|
|
polylines: [...controller.polylineNotes]);
|
2026-06-16 14:12:44 +02:00
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 12:53:50 +02:00
|
|
|
return PolygonLayer(
|
|
|
|
|
hitNotifier: controller.polygonHitNotifier,
|
|
|
|
|
polygons: [...controller.polygonNotes],
|
|
|
|
|
useAltRendering: true);
|
2026-06-16 14:12:44 +02:00
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
2026-06-19 12:53:50 +02:00
|
|
|
final selectedId = controller.selectedNoteItemId.value;
|
|
|
|
|
if (selectedId == null) return const SizedBox.shrink();
|
2026-06-16 14:12:44 +02:00
|
|
|
|
2026-06-19 12:53:50 +02:00
|
|
|
// Polygon kiemelés
|
|
|
|
|
final selectedPolygon = controller.polygonNotes
|
|
|
|
|
.where((p) => p.hitValue == selectedId)
|
|
|
|
|
.firstOrNull;
|
|
|
|
|
if (selectedPolygon != null) {
|
|
|
|
|
return PolygonLayer(polygons: [
|
|
|
|
|
Polygon(
|
|
|
|
|
points: selectedPolygon.points,
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
borderColor: Colors.white,
|
|
|
|
|
borderStrokeWidth: selectedPolygon.borderStrokeWidth + 3,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Polyline kiemelés
|
|
|
|
|
final selectedPolyline = controller.polylineNotes
|
|
|
|
|
.where((p) => p.hitValue == selectedId)
|
|
|
|
|
.firstOrNull;
|
|
|
|
|
if (selectedPolyline != null) {
|
|
|
|
|
return PolylineLayer(polylines: [
|
|
|
|
|
Polyline(
|
|
|
|
|
points: selectedPolyline.points,
|
|
|
|
|
color: Colors.white.withOpacity(0.6),
|
|
|
|
|
strokeWidth: selectedPolyline.strokeWidth + 4,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
return PolygonEditor(
|
|
|
|
|
controller: controller.polygonEditorController,
|
|
|
|
|
throttleDuration: Duration.zero);
|
|
|
|
|
}),
|
2026-06-21 10:07:35 +02:00
|
|
|
Obx(() {
|
|
|
|
|
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
|
|
|
|
GnssConnectionType.none;
|
|
|
|
|
if (isGpsActive) {
|
|
|
|
|
return MarkerLayer(
|
|
|
|
|
markers: controller.currentLocationMarker.toList());
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}),
|
2026-05-28 13:24:16 +02:00
|
|
|
],
|
2026-05-24 14:50:31 +02:00
|
|
|
),
|
2026-05-12 00:14:11 +02:00
|
|
|
Positioned(
|
2026-06-07 11:45:15 +02:00
|
|
|
top: 12,
|
|
|
|
|
left: 12,
|
|
|
|
|
child: MapInfoCardColumn(controller: controller),
|
|
|
|
|
),
|
|
|
|
|
|
2026-06-16 14:12:44 +02:00
|
|
|
// Positioned(
|
|
|
|
|
// top: 390,
|
|
|
|
|
// right: 60,
|
|
|
|
|
// left: 8,
|
|
|
|
|
// child: CoordinatePanel(
|
|
|
|
|
// eovY: controller.eovY,
|
|
|
|
|
// eovX: controller.eovX,
|
|
|
|
|
// horError: controller.gpsLatitudeError,
|
|
|
|
|
// vertError: controller.gpsAltitudeError,
|
|
|
|
|
// altitudeMsl: controller.gpsAltitude,
|
|
|
|
|
// geoidSeparation: controller.gpsGeoidSeparation,
|
|
|
|
|
// ntripConnected: controller.ntripIsConnected,
|
|
|
|
|
// ntripBytes: controller.ntripReceivedData,
|
|
|
|
|
// ntripPackets: controller.ntripDataPacketNumbers,
|
|
|
|
|
// ggaPackets: controller.ggaSenDataPacketNumber,
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.fieldWalk) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
if (controller.activeEditTool.value == MapEditTool.none) {
|
|
|
|
|
return Positioned(
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
child: MapEditCompactToolbar(controller: controller));
|
|
|
|
|
}
|
|
|
|
|
return Positioned(
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
child: MapEditDrawingToolbar(controller: controller));
|
2026-06-21 12:33:57 +02:00
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (controller.mode.value != MapSurveyMode.measure) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
return Positioned(
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
child: MeasureBottomPanel(ctrl: controller));
|
2026-06-16 14:12:44 +02:00
|
|
|
})
|
2026-05-17 00:35:21 +02:00
|
|
|
// 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),
|
|
|
|
|
// ),
|
2026-05-27 15:04:46 +02:00
|
|
|
// Positioned(
|
|
|
|
|
// bottom: 0,
|
|
|
|
|
// left: 0,
|
|
|
|
|
// right: 0,
|
|
|
|
|
// child: MapBottomPanel(controller: controller))
|
2026-05-12 00:14:11 +02:00
|
|
|
]);
|
|
|
|
|
}
|
2026-06-11 01:20:55 +02:00
|
|
|
|
|
|
|
|
Widget _buildTrackLayer() {
|
|
|
|
|
// FutureBuilder helyett a controller livePoints-ból
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final ctrl = TrackingController.to;
|
|
|
|
|
if (ctrl.livePoints.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
return PolylineLayer(polylines: [
|
|
|
|
|
Polyline(
|
|
|
|
|
points: ctrl.livePoints
|
|
|
|
|
.map((p) => LatLng(p.latitude, p.longitude))
|
|
|
|
|
.toList(),
|
|
|
|
|
color: Colors.red.withOpacity(0.85),
|
|
|
|
|
strokeWidth: 3.0,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-12 00:14:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-16 14:47:34 +02:00
|
|
|
final dy = controller.deltaY;
|
|
|
|
|
final dx = controller.deltaX;
|
|
|
|
|
final dist = controller.distanceToTarget;
|
2026-05-12 00:14:11 +02:00
|
|
|
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',
|
2026-05-16 14:47:34 +02:00
|
|
|
value: dy,
|
2026-05-12 00:14:11 +02:00
|
|
|
unit: 'm',
|
|
|
|
|
),
|
|
|
|
|
_DeltaCell(
|
|
|
|
|
label: 'ΔX',
|
2026-05-16 14:47:34 +02:00
|
|
|
value: dx,
|
2026-05-12 00:14:11 +02:00
|
|
|
unit: 'm',
|
|
|
|
|
),
|
|
|
|
|
_DeltaCell(
|
|
|
|
|
label: 'Táv',
|
2026-05-16 14:47:34 +02:00
|
|
|
value: dist,
|
2026-05-12 00:14:11 +02:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|