Távolság vagy terület mérése a térképen terepbejárás nézetben
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
enum MapMeasureType { none, distance, area }
|
||||||
@@ -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/geopackage_exporter.dart';
|
||||||
import 'package:terepi_seged/core/style_editable.dart';
|
import 'package:terepi_seged/core/style_editable.dart';
|
||||||
import 'package:terepi_seged/enums/map_edit_tool.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/map_survey_mode.dart';
|
||||||
import 'package:terepi_seged/enums/note_type.dart';
|
import 'package:terepi_seged/enums/note_type.dart';
|
||||||
import 'package:terepi_seged/eov/convert_coordinate.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 = <LatLng>[].obs;
|
||||||
|
bool get isDistanceOrAreaMeasuring =>
|
||||||
|
mapMeasureType.value != MapMeasureType.none;
|
||||||
|
|
||||||
// NoteItem? get selectedPoint =>
|
// NoteItem? get selectedPoint =>
|
||||||
// pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value);
|
// pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value);
|
||||||
|
|
||||||
@@ -1985,4 +1991,31 @@ class MapSurveyController extends GetxController implements StyleEditable {
|
|||||||
void toggleShowLines() => showLines.value = !showLines.value;
|
void toggleShowLines() => showLines.value = !showLines.value;
|
||||||
|
|
||||||
void toggleShowPolygons() => showPolygons.value = !showPolygons.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:terepi_seged/enums/map_edit_tool.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/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/controllers/map_survey_controller.dart';
|
||||||
import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.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/services/gnss/gnss_service.dart';
|
||||||
import 'package:terepi_seged/utils/rive_utils.dart';
|
import 'package:terepi_seged/utils/rive_utils.dart';
|
||||||
import 'package:terepi_seged/widgets/coordinate_panel.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/imported_layer_overlay.dart';
|
||||||
import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart';
|
import 'package:terepi_seged/widgets/map/measure_bottom_panel.dart';
|
||||||
import 'package:terepi_seged/widgets/map/note_item_label_layer.dart';
|
import 'package:terepi_seged/widgets/map/note_item_label_layer.dart';
|
||||||
@@ -66,6 +68,10 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap: (tapPosition, point) {
|
onTap: (tapPosition, point) {
|
||||||
|
if (controller.isDistanceOrAreaMeasuring) {
|
||||||
|
controller.addDistanceOrAreaMeasuredPoint(point);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controller.mode.value != MapSurveyMode.fieldWalk) return;
|
if (controller.mode.value != MapSurveyMode.fieldWalk) return;
|
||||||
if (controller.isMapEditing) return;
|
if (controller.isMapEditing) return;
|
||||||
final polygonHit = controller.polygonHitNotifier.value;
|
final polygonHit = controller.polygonHitNotifier.value;
|
||||||
@@ -233,6 +239,7 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
if (markers.isEmpty) return const SizedBox.shrink();
|
if (markers.isEmpty) return const SizedBox.shrink();
|
||||||
return MarkerLayer(markers: markers);
|
return MarkerLayer(markers: markers);
|
||||||
}),
|
}),
|
||||||
|
DistanceOrAreaMeasureLayer(controller: controller),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
final isGpsActive = GnssService.to.activeConnectionType.value !=
|
||||||
GnssConnectionType.none;
|
GnssConnectionType.none;
|
||||||
@@ -293,6 +300,41 @@ class MapSurveyView extends GetView<MapSurveyController> {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: MeasureBottomPanel(ctrl: controller));
|
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(top: 8, left: 0, right: 0, child: _ModeSelector()),
|
||||||
// Positioned(
|
// 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 = <Marker>[];
|
||||||
|
final polylines = <Polyline>[];
|
||||||
|
|
||||||
|
// ── 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<LatLng> 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<LatLng> 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<LatLng> 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<LatLng> 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<LatLng> pts) => '⟷ ${_fmtDist(_totalDist(pts))}';
|
||||||
|
|
||||||
|
String _areaLabel(List<LatLng> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user