MobilApp/lib/pages/map_survey/presentations/views/map_survey_view.dart

576 lines
19 KiB
Dart
Raw Permalink Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor.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:terepi_seged/enums/map_edit_tool.dart';
2026-05-27 15:04:46 +02:00
import 'package:terepi_seged/enums/map_survey_mode.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';
2026-06-11 01:20:55 +02:00
import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
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/note_item_label_layer.dart';
2026-05-17 00:35:21 +02:00
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';
import 'package:terepi_seged/widgets/map_info_card_column.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: [
2026-05-24 14:50:31 +02:00
SharedMapWidget(
controls: const MapControls(),
2026-05-24 14:50:31 +02:00
mapController: controller.mapController,
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,
onPositionChanged: controller.onMapPositionChanged,
onCenterOnGps: controller.setIsMapMoveToCenter,
onLongPress: (tapPosition, point) {
if (controller.activeEditTool.value == MapEditTool.point) {
if (controller.isGeometryEditing) {
controller.saveEditedPoint(point: point);
} else {
//controller.saveEditedPoint(point: point);
controller.showSavePointDialog(point: point);
}
return;
}
if (controller.activeEditTool.value == MapEditTool.line ||
controller.activeEditTool.value == MapEditTool.polygon) {
controller.polygonEditorController.addPoint(point);
}
},
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();
},
layers: [
const ImportedLayerOverlay(),
2026-06-11 01:20:55 +02:00
// Track polyline
2026-06-23 15:21:20 +02:00
Obx(() {
final inTrackMode = controller.mode.value == MapSurveyMode.track ||
controller.mode.value == MapSurveyMode.fieldWalk;
2026-06-23 15:21:20 +02:00
if (!inTrackMode) return const SizedBox.shrink();
final ids = TrackingController.to.overlayTrackIds;
if (ids.isEmpty) return const SizedBox.shrink();
final polylines = ids
.map((id) {
final pts = TrackingController.to.getCoordsFor(id);
if (pts.isEmpty) return null;
return Polyline(
points: pts,
color: Colors.blue.withOpacity(0.75),
strokeWidth: 2.5,
borderColor: Colors.white.withOpacity(0.4),
borderStrokeWidth: 1.0,
);
})
.whereType<Polyline>()
.toList();
if (polylines.isEmpty) return const SizedBox.shrink();
return PolylineLayer(polylines: polylines);
}),
2026-06-11 01:20:55 +02:00
Obx(() {
final isTracking = TrackingController.to.isRecording.value;
final inTrackMode = controller.mode.value == MapSurveyMode.track;
if (!isTracking && !inTrackMode) {
return const SizedBox.shrink();
} else {
2026-06-23 15:21:20 +02:00
return _buildTrackLayer1();
2026-06-11 01:20:55 +02:00
}
}),
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();
}
return MarkerLayer(markers: [...controller.pointNotes]);
}),
Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) {
return const SizedBox.shrink();
}
return PolylineLayer(
hitNotifier: controller.polylineHitNotifier,
polylines: [...controller.polylineNotes]);
}),
Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) {
return const SizedBox.shrink();
}
return PolygonLayer(
hitNotifier: controller.polygonHitNotifier,
polygons: [...controller.polygonNotes],
useAltRendering: true);
}),
Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) {
return const SizedBox.shrink();
}
final selectedId = controller.selectedNoteItemId.value;
if (selectedId == null) return const SizedBox.shrink();
// 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);
}),
NoteItemLabelLayer(
controller: controller,
),
2026-06-23 15:21:20 +02:00
Obx(() {
final tracks = controller.teamTrackPoints;
if (tracks.isEmpty) return const SizedBox.shrink();
return PolylineLayer(
polylines: tracks.entries.map((e) {
final colors = [
Colors.blue,
Colors.purple,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.deepPurple
];
final color = colors[e.key.hashCode.abs() % colors.length];
return Polyline(
points: e.value,
color: color.withValues(alpha: 0.7),
strokeWidth: 2.5);
}).toList(),
);
}),
Obx(() {
final markers =
List<Marker>.from(controller.teamTrackMarkers.values);
if (markers.isEmpty) return const SizedBox.shrink();
return MarkerLayer(markers: markers);
}),
Obx(() {
final isGpsActive = GnssService.to.activeConnectionType.value !=
GnssConnectionType.none;
2026-06-23 15:21:20 +02:00
if (isGpsActive && controller.mode.value != MapSurveyMode.track) {
return MarkerLayer(
markers: controller.currentLocationMarker.toList());
}
return const SizedBox.shrink();
}),
],
2026-05-24 14:50:31 +02:00
),
Positioned(
top: 12,
left: 12,
child: MapInfoCardColumn(controller: controller),
),
// 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));
}),
Obx(() {
if (controller.mode.value != MapSurveyMode.measure) {
return const SizedBox.shrink();
}
return Positioned(
left: 0,
right: 0,
bottom: 0,
child: MeasureBottomPanel(ctrl: controller));
})
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-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-06-23 15:21:20 +02:00
Widget _buildTrackLayer1() {
return Obx(() {
final ctrl = TrackingController.to;
final points = ctrl.livePoints.toList();
if (points.isEmpty) return const SizedBox.shrink();
return Stack(children: [
// 1. Track vonal
PolylineLayer(polylines: [
Polyline(
points: points,
color: Colors.red.withOpacity(0.85),
strokeWidth: 3.0,
),
]),
// 2. Markerek a vonal felett
MarkerLayer(markers: [
// Kezdőpont — zöld
Marker(
point: points.first,
child: const Icon(Icons.flag, color: Colors.green, size: 28),
),
// Utolsó pont — piros (ha van legalább 2 pont)
if (points.length > 1)
Marker(
point: points.last,
child: _PulsingDot(
color: TrackingController.to.isPaused.value
? Colors.orange
: Colors.blue,
),
),
]),
]);
});
}
}
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),
));
}
}
2026-06-23 15:21:20 +02:00
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 _anim;
late Animation<double> _scale;
@override
void initState() {
super.initState();
_anim = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000))
..repeat(reverse: true);
_scale = Tween(begin: 0.8, end: 1.1)
.animate(CurvedAnimation(parent: _anim, curve: Curves.easeInOut));
}
@override
void dispose() {
_anim.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scale,
child: Container(
width: 8,
height: 8,
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: 4,
spreadRadius: 2)
],
),
),
);
}
}
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;
final dy = controller.deltaY;
final dx = controller.deltaX;
final dist = controller.distanceToTarget;
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: dy,
unit: 'm',
),
_DeltaCell(
label: 'ΔX',
value: dx,
unit: 'm',
),
_DeltaCell(
label: 'Táv',
value: dist,
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,
),
),
],
);
}
}