From 80f7c8d57176ce5c2cb5da2f232e04615df9c32f Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Sun, 5 Jul 2026 01:41:15 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A1vols=C3=A1g=20vagy=20ter=C3=BClet=20m?= =?UTF-8?q?=C3=A9r=C3=A9se=20a=20t=C3=A9rk=C3=A9pen=20terepbej=C3=A1r?= =?UTF-8?q?=C3=A1s=20n=C3=A9zetben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/enums/map_measure_type.dart | 1 + .../controllers/map_survey_controller.dart | 33 +++ .../presentations/views/map_survey_view.dart | 90 +++++++ .../map/distance_or_area_measure_layer.dart | 245 ++++++++++++++++++ 4 files changed, 369 insertions(+) create mode 100644 lib/enums/map_measure_type.dart create mode 100644 lib/widgets/map/distance_or_area_measure_layer.dart diff --git a/lib/enums/map_measure_type.dart b/lib/enums/map_measure_type.dart new file mode 100644 index 0000000..784cfa2 --- /dev/null +++ b/lib/enums/map_measure_type.dart @@ -0,0 +1 @@ +enum MapMeasureType { none, distance, area } 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 f1d1be8..5c49cdd 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -26,6 +26,7 @@ import 'package:terepi_seged/core/geometry_measure_formatter.dart'; import 'package:terepi_seged/core/geopackage_exporter.dart'; import 'package:terepi_seged/core/style_editable.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; +import 'package:terepi_seged/enums/map_measure_type.dart'; 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'; @@ -240,6 +241,11 @@ class MapSurveyController extends GetxController implements StyleEditable { } } + final mapMeasureType = MapMeasureType.none.obs; + final distanceOrAreaMeasurePoints = [].obs; + bool get isDistanceOrAreaMeasuring => + mapMeasureType.value != MapMeasureType.none; + // NoteItem? get selectedPoint => // pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value); @@ -1985,4 +1991,31 @@ class MapSurveyController extends GetxController implements StyleEditable { void toggleShowLines() => showLines.value = !showLines.value; void toggleShowPolygons() => showPolygons.value = !showPolygons.value; + + void toggleMeasureDistance() { + if (mapMeasureType.value == MapMeasureType.distance) { + mapMeasureType.value = MapMeasureType.none; + } else { + mapMeasureType.value = MapMeasureType.distance; + } + distanceOrAreaMeasurePoints.clear(); + } + + void toggleMeasureArea() { + if (mapMeasureType.value == MapMeasureType.area) { + mapMeasureType.value = MapMeasureType.none; + } else { + mapMeasureType.value = MapMeasureType.area; + } + distanceOrAreaMeasurePoints.clear(); + } + + void addDistanceOrAreaMeasuredPoint(LatLng point) => + distanceOrAreaMeasurePoints.add(point); + + void removeLastDistanceOrAreaMeasuredPoint() { + if (distanceOrAreaMeasurePoints.isNotEmpty) { + distanceOrAreaMeasurePoints.removeLast(); + } + } } 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 86ae288..4ce637d 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -5,6 +5,7 @@ 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'; +import 'package:terepi_seged/enums/map_measure_type.dart'; 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'; @@ -13,6 +14,7 @@ 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/distance_or_area_measure_layer.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'; @@ -66,6 +68,10 @@ class MapSurveyView extends GetView { } }, onTap: (tapPosition, point) { + if (controller.isDistanceOrAreaMeasuring) { + controller.addDistanceOrAreaMeasuredPoint(point); + return; + } if (controller.mode.value != MapSurveyMode.fieldWalk) return; if (controller.isMapEditing) return; final polygonHit = controller.polygonHitNotifier.value; @@ -233,6 +239,7 @@ class MapSurveyView extends GetView { if (markers.isEmpty) return const SizedBox.shrink(); return MarkerLayer(markers: markers); }), + DistanceOrAreaMeasureLayer(controller: controller), Obx(() { final isGpsActive = GnssService.to.activeConnectionType.value != GnssConnectionType.none; @@ -293,6 +300,41 @@ class MapSurveyView extends GetView { right: 0, bottom: 0, child: MeasureBottomPanel(ctrl: controller)); + }), + Obx(() { + if (controller.mode.value != MapSurveyMode.fieldWalk) + return SizedBox.shrink(); + return Positioned( + right: 10, + bottom: 300, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // Távolság mérés + _MeasureButton( + icon: Icons.straighten, + tooltip: 'Távolságmérés', + active: + controller.mapMeasureType.value == MapMeasureType.distance, + onTap: controller.toggleMeasureDistance, + ), + const SizedBox(height: 4), + // Területmérés + _MeasureButton( + icon: Icons.pentagon_outlined, + tooltip: 'Területmérés', + active: controller.mapMeasureType.value == MapMeasureType.area, + onTap: controller.toggleMeasureArea, + ), + // Visszavonás (ha van pont) + if (controller.distanceOrAreaMeasurePoints.isNotEmpty) ...[ + const SizedBox(height: 4), + _MeasureButton( + icon: Icons.undo, + tooltip: 'Utolsó pont törlése', + active: false, + onTap: controller.removeLastDistanceOrAreaMeasuredPoint, + ), + ] + ])); }) // Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), // Positioned( @@ -579,3 +621,51 @@ class _DeltaCell extends StatelessWidget { ); } } + +class _MeasureButton extends StatelessWidget { + final IconData icon; + final String tooltip; + final bool active; + final VoidCallback onTap; + const _MeasureButton({ + required this.icon, + required this.tooltip, + required this.active, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Tooltip( + message: tooltip, + child: Material( + color: active + ? Colors.deepOrange + : 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: Icon(icon, + size: 20, + color: active + ? Colors.white + : isDark + ? Colors.white70 + : Colors.black87), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/map/distance_or_area_measure_layer.dart b/lib/widgets/map/distance_or_area_measure_layer.dart new file mode 100644 index 0000000..f2336b0 --- /dev/null +++ b/lib/widgets/map/distance_or_area_measure_layer.dart @@ -0,0 +1,245 @@ +// lib/widgets/map/measure_layer.dart +// +// Távolság és terület mérés — rögzítés nélkül, csak memóriában +// +// - Pontok: koppintásra kerülnek a térképre +// - Vonalak: azonnal megjelennek a szakaszok +// - Hossz: minden szakasz felezőpontján buborékban +// - Terület: centroidon m² / ha (+ kerület zárójelben) + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:terepi_seged/enums/map_measure_type.dart'; + +import '../../pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class DistanceOrAreaMeasureLayer extends StatelessWidget { + final MapSurveyController controller; + const DistanceOrAreaMeasureLayer({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + final type = controller.mapMeasureType.value; + final points = controller.distanceOrAreaMeasurePoints.toList(); + + if (type == MapMeasureType.none || points.isEmpty) { + return const SizedBox.shrink(); + } + + final markers = []; + final polylines = []; + + // ── Vonalak ─────────────────────────────────────────────────── + if (points.length >= 2) { + // Fővonal + polylines.add(Polyline( + points: points, + color: Colors.deepOrange, + strokeWidth: 2.5, + borderColor: Colors.white.withOpacity(0.6), + borderStrokeWidth: 1.0, + )); + + // Szakaszhosszak a felezőpontokon + for (int i = 0; i < points.length - 1; i++) { + final mid = _midpoint(points[i], points[i + 1]); + final dist = _haversine(points[i], points[i + 1]); + markers.add(Marker( + point: mid, + width: 80, + height: 22, + alignment: Alignment.center, + child: _LabelBubble( + text: _fmtDist(dist), + color: Colors.deepOrange, + ), + )); + } + } + + // ── Záró szár (terület módban: vissza az első ponthoz) ──────── + if (type == MapMeasureType.area && points.length >= 3) { + polylines.add(Polyline( + points: [points.last, points.first], + color: Colors.deepOrange.withOpacity(0.5), + strokeWidth: 2.0, + //isDotted: true, + )); + } + + // ── Összesítő buborék ───────────────────────────────────────── + if (points.length >= 2) { + final anchor = type == MapMeasureType.area && points.length >= 3 + ? _centroid(points) + : points.last; + + final label = type == MapMeasureType.distance + ? _totalDistLabel(points) + : _areaLabel(points); + + markers.add(Marker( + point: anchor, + width: 140, + height: 36, + alignment: type == MapMeasureType.area + ? Alignment.center + : Alignment.topCenter, + child: _LabelBubble( + text: label, + color: Colors.indigo, + large: true, + ), + )); + } + + // ── Mérési pontok ───────────────────────────────────────────── + for (int i = 0; i < points.length; i++) { + final isLast = i == points.length - 1; + final isFirst = i == 0; + markers.add(Marker( + point: points[i], + width: 20, + height: 20, + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + color: isFirst + ? Colors.green + : isLast + ? Colors.deepOrange + : Colors.white, + shape: BoxShape.circle, + border: Border.all(color: Colors.deepOrange, width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 3), + ], + ), + ), + )); + } + + return Stack(children: [ + if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), + if (markers.isNotEmpty) MarkerLayer(markers: markers), + ]); + }); + } + + // ── Számítások ──────────────────────────────────────────────────── + + static const _distCalc = Distance(); + + double _haversine(LatLng a, LatLng b) => _distCalc.as(LengthUnit.Meter, a, b); + + LatLng _midpoint(LatLng a, LatLng b) => LatLng( + (a.latitude + b.latitude) / 2, + (a.longitude + b.longitude) / 2, + ); + + LatLng _centroid(List pts) => LatLng( + pts.map((p) => p.latitude).reduce((a, b) => a + b) / pts.length, + pts.map((p) => p.longitude).reduce((a, b) => a + b) / pts.length, + ); + + double _totalDist(List pts) { + double total = 0; + for (int i = 0; i < pts.length - 1; i++) { + total += _haversine(pts[i], pts[i + 1]); + } + return total; + } + + /// Gömbháromszög módszer — pontos terület m²-ben + double _area(List pts) { + if (pts.length < 3) return 0; + const r = 6371000.0; + double area = 0; + final n = pts.length; + for (int i = 0; i < n; i++) { + final j = (i + 1) % n; + final xi = pts[i].longitude * pi / 180; + final xj = pts[j].longitude * pi / 180; + final yi = pts[i].latitude * pi / 180; + final yj = pts[j].latitude * pi / 180; + area += (xj - xi) * (2 + sin(yi) + sin(yj)); + } + return (area.abs() * r * r / 2); + } + + double _perimeter(List pts) { + double p = _totalDist(pts); + if (pts.length >= 3) p += _haversine(pts.last, pts.first); + return p; + } + + // ── Formázás ────────────────────────────────────────────────────── + + String _fmtDist(double m) { + if (m < 1000) return '${m.toStringAsFixed(0)} m'; + return '${(m / 1000).toStringAsFixed(2)} km'; + } + + String _fmtArea(double m2) { + if (m2 < 10000) return '${m2.toStringAsFixed(0)} m²'; + if (m2 < 1000000) return '${(m2 / 10000).toStringAsFixed(2)} ha'; + return '${(m2 / 1000000).toStringAsFixed(3)} km²'; + } + + String _totalDistLabel(List pts) => '⟷ ${_fmtDist(_totalDist(pts))}'; + + String _areaLabel(List pts) { + final a = _area(pts); + final p = _perimeter(pts); + return '${_fmtArea(a)}\n(${_fmtDist(p)})'; + } +} + +// ─── Felirat buborék ────────────────────────────────────────────────────────── + +class _LabelBubble extends StatelessWidget { + final String text; + final Color color; + final bool large; + const _LabelBubble({ + required this.text, + required this.color, + this.large = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: large ? 8 : 5, vertical: large ? 4 : 2), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.93), + borderRadius: BorderRadius.circular(6), + border: Border(left: BorderSide(color: color, width: 3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + text, + style: TextStyle( + fontSize: large ? 12 : 10, + fontWeight: FontWeight.w700, + color: color, + height: 1.2, + fontFeatures: const [FontFeature.tabularFigures()], + ), + textAlign: TextAlign.center, + maxLines: 2, + ), + ); + } +}