Terepbejárás: geometriák hosszának és területének számítása és jelzése
This commit is contained in:
parent
a9316b9b8d
commit
9656bbd332
105
lib/core/geometry_measure.dart
Normal file
105
lib/core/geometry_measure.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
class GeometryMeasure {
|
||||||
|
static final Distance _distance = const Distance();
|
||||||
|
|
||||||
|
static double lineLengthMeters(List<LatLng> points) {
|
||||||
|
if (points.length < 2) return 0.0;
|
||||||
|
|
||||||
|
double sum = 0.0;
|
||||||
|
|
||||||
|
for (var i = 1; i < points.length; i++) {
|
||||||
|
sum += _distance.as(
|
||||||
|
LengthUnit.Meter,
|
||||||
|
points[i - 1],
|
||||||
|
points[i],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double polygonAreaSquareMeters(List<LatLng> points) {
|
||||||
|
final polygon = _normalizedPolygon(points);
|
||||||
|
|
||||||
|
if (polygon.length < 3) return 0.0;
|
||||||
|
|
||||||
|
final origin = _centroid(polygon);
|
||||||
|
final projected =
|
||||||
|
polygon.map((p) => _projectToLocalMeters(p, origin)).toList();
|
||||||
|
|
||||||
|
double area = 0.0;
|
||||||
|
|
||||||
|
for (var i = 0; i < projected.length; i++) {
|
||||||
|
final j = (i + 1) % projected.length;
|
||||||
|
|
||||||
|
area += projected[i].x * projected[j].y - projected[j].x * projected[i].y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return area.abs() / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LatLng> _normalizedPolygon(List<LatLng> source) {
|
||||||
|
final points = List<LatLng>.from(source);
|
||||||
|
|
||||||
|
if (points.length < 2) return points;
|
||||||
|
|
||||||
|
final first = points.first;
|
||||||
|
final last = points.last;
|
||||||
|
|
||||||
|
final closeDistance = _distance.as(
|
||||||
|
LengthUnit.Meter,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (closeDistance < 0.01) {
|
||||||
|
points.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LatLng _centroid(List<LatLng> points) {
|
||||||
|
double lat = 0.0;
|
||||||
|
double lon = 0.0;
|
||||||
|
|
||||||
|
for (final p in points) {
|
||||||
|
lat += p.latitude;
|
||||||
|
lon += p.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LatLng(
|
||||||
|
lat / points.length,
|
||||||
|
lon / points.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _Point2D _projectToLocalMeters(
|
||||||
|
LatLng point,
|
||||||
|
LatLng origin,
|
||||||
|
) {
|
||||||
|
const earthRadius = 6378137.0;
|
||||||
|
|
||||||
|
final latRad = point.latitude * pi / 180.0;
|
||||||
|
final lonRad = point.longitude * pi / 180.0;
|
||||||
|
|
||||||
|
final originLatRad = origin.latitude * pi / 180.0;
|
||||||
|
final originLonRad = origin.longitude * pi / 180.0;
|
||||||
|
|
||||||
|
final x = earthRadius * (lonRad - originLonRad) * cos(originLatRad);
|
||||||
|
|
||||||
|
final y = earthRadius * (latRad - originLatRad);
|
||||||
|
|
||||||
|
return _Point2D(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Point2D {
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
|
||||||
|
const _Point2D(this.x, this.y);
|
||||||
|
}
|
||||||
17
lib/core/geometry_measure_formatter.dart
Normal file
17
lib/core/geometry_measure_formatter.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
class GeometryMeasureFormatter {
|
||||||
|
static String length(double meters) {
|
||||||
|
if (meters < 1000.0) {
|
||||||
|
return '${meters.toStringAsFixed(0)} m';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${(meters / 1000.0).toStringAsFixed(2)} km';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String area(double squareMeters) {
|
||||||
|
if (squareMeters < 10000.0) {
|
||||||
|
return '${squareMeters.toStringAsFixed(0)} m²';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${(squareMeters / 10000.0).toStringAsFixed(2)} ha';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,8 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:terepi_seged/controls/geoid_grid.dart';
|
import 'package:terepi_seged/controls/geoid_grid.dart';
|
||||||
import 'package:terepi_seged/controls/wgs84_coordinate_formatter.dart';
|
import 'package:terepi_seged/controls/wgs84_coordinate_formatter.dart';
|
||||||
|
import 'package:terepi_seged/core/geometry_measure.dart';
|
||||||
|
import 'package:terepi_seged/core/geometry_measure_formatter.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_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';
|
||||||
@ -194,6 +196,26 @@ class MapSurveyController extends GetxController {
|
|||||||
final selectedNoteItemType = NoteType.line.obs;
|
final selectedNoteItemType = NoteType.line.obs;
|
||||||
int? _editingNoteItemId;
|
int? _editingNoteItemId;
|
||||||
bool get isGeometryEditing => _editingNoteItemId != null;
|
bool get isGeometryEditing => _editingNoteItemId != null;
|
||||||
|
final draftLengthMeters = 0.0.obs;
|
||||||
|
final draftAreaSquareMeters = 0.0.obs;
|
||||||
|
|
||||||
|
String get draftMeasureText {
|
||||||
|
switch (activeEditTool.value) {
|
||||||
|
case MapEditTool.line:
|
||||||
|
return GeometryMeasureFormatter.length(
|
||||||
|
draftLengthMeters.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
case MapEditTool.polygon:
|
||||||
|
return GeometryMeasureFormatter.area(
|
||||||
|
draftAreaSquareMeters.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
case MapEditTool.point:
|
||||||
|
case MapEditTool.none:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NoteItem? get selectedPoint =>
|
// NoteItem? get selectedPoint =>
|
||||||
// pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value);
|
// pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value);
|
||||||
@ -233,6 +255,7 @@ class MapSurveyController extends GetxController {
|
|||||||
PolygonEditorController(mode: PolygonEditorMode.polygon);
|
PolygonEditorController(mode: PolygonEditorMode.polygon);
|
||||||
polygonEditorController.addListener(() {
|
polygonEditorController.addListener(() {
|
||||||
editorPointCount.value = polygonEditorController.points.length;
|
editorPointCount.value = polygonEditorController.points.length;
|
||||||
|
_updateDraftMeasurements();
|
||||||
});
|
});
|
||||||
await _setInitialPositionFromPhone();
|
await _setInitialPositionFromPhone();
|
||||||
|
|
||||||
@ -1064,18 +1087,21 @@ class MapSurveyController extends GetxController {
|
|||||||
|
|
||||||
void startPointTool() {
|
void startPointTool() {
|
||||||
activeEditTool.value = MapEditTool.point;
|
activeEditTool.value = MapEditTool.point;
|
||||||
|
activeEditLabel.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void startLineTool() {
|
void startLineTool() {
|
||||||
polygonEditorController.clear();
|
polygonEditorController.clear();
|
||||||
polygonEditorController.setMode(PolygonEditorMode.line);
|
polygonEditorController.setMode(PolygonEditorMode.line);
|
||||||
activeEditTool.value = MapEditTool.line;
|
activeEditTool.value = MapEditTool.line;
|
||||||
|
activeEditLabel.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void startPolygonTool() {
|
void startPolygonTool() {
|
||||||
polygonEditorController.clear();
|
polygonEditorController.clear();
|
||||||
polygonEditorController.setMode(PolygonEditorMode.polygon);
|
polygonEditorController.setMode(PolygonEditorMode.polygon);
|
||||||
activeEditTool.value = MapEditTool.polygon;
|
activeEditTool.value = MapEditTool.polygon;
|
||||||
|
activeEditLabel.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelEditing() {
|
void cancelEditing() {
|
||||||
@ -1084,14 +1110,8 @@ class MapSurveyController extends GetxController {
|
|||||||
_editingNoteItemId = null;
|
_editingNoteItemId = null;
|
||||||
selectedNoteItemId.value = null;
|
selectedNoteItemId.value = null;
|
||||||
editorPointCount.value = 0;
|
editorPointCount.value = 0;
|
||||||
}
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
void openFeatureList() {
|
|
||||||
// TODO: DraggableScrollableSheet / bottom sheet lista
|
|
||||||
}
|
|
||||||
|
|
||||||
void openLayerPanel() {
|
|
||||||
// TODO: rétegek sheet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishGeometry() {
|
void finishGeometry() {
|
||||||
@ -1252,6 +1272,8 @@ class MapSurveyController extends GetxController {
|
|||||||
await updateNoteItem(updated);
|
await updateNoteItem(updated);
|
||||||
_editingNoteItemId = null;
|
_editingNoteItemId = null;
|
||||||
activeEditTool.value = MapEditTool.none;
|
activeEditTool.value = MapEditTool.none;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteEditingItem() async {
|
Future<void> deleteEditingItem() async {
|
||||||
@ -1262,6 +1284,8 @@ class MapSurveyController extends GetxController {
|
|||||||
_editingNoteItemId = null;
|
_editingNoteItemId = null;
|
||||||
selectedNoteItemId.value = null;
|
selectedNoteItemId.value = null;
|
||||||
activeEditTool.value = MapEditTool.none;
|
activeEditTool.value = MapEditTool.none;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> finishDraft() async {
|
// Future<void> finishDraft() async {
|
||||||
@ -1518,6 +1542,8 @@ class MapSurveyController extends GetxController {
|
|||||||
polygonEditorController.clear();
|
polygonEditorController.clear();
|
||||||
activeEditTool.value = MapEditTool.none;
|
activeEditTool.value = MapEditTool.none;
|
||||||
editorPointCount.value = 0;
|
editorPointCount.value = 0;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Geometria frissítve',
|
'Geometria frissítve',
|
||||||
@ -1540,6 +1566,9 @@ class MapSurveyController extends GetxController {
|
|||||||
}
|
}
|
||||||
polygonEditorController.clear();
|
polygonEditorController.clear();
|
||||||
activeEditTool.value = MapEditTool.none;
|
activeEditTool.value = MapEditTool.none;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
|
activeEditLabel.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polygonEditorController.mode == PolygonEditorMode.polygon) {
|
if (polygonEditorController.mode == PolygonEditorMode.polygon) {
|
||||||
@ -1554,6 +1583,8 @@ class MapSurveyController extends GetxController {
|
|||||||
}
|
}
|
||||||
polygonEditorController.clear();
|
polygonEditorController.clear();
|
||||||
activeEditTool.value = MapEditTool.none;
|
activeEditTool.value = MapEditTool.none;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1561,4 +1592,35 @@ class MapSurveyController extends GetxController {
|
|||||||
if (polygonEditorController.mode == PolygonEditorMode.line) return 2;
|
if (polygonEditorController.mode == PolygonEditorMode.line) return 2;
|
||||||
return 3; // polygon
|
return 3; // polygon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateDraftMeasurements() {
|
||||||
|
switch (activeEditTool.value) {
|
||||||
|
case MapEditTool.line:
|
||||||
|
final points = List<LatLng>.from(
|
||||||
|
polygonEditorController.points,
|
||||||
|
);
|
||||||
|
|
||||||
|
draftLengthMeters.value = GeometryMeasure.lineLengthMeters(points);
|
||||||
|
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MapEditTool.polygon:
|
||||||
|
final points = List<LatLng>.from(
|
||||||
|
polygonEditorController.points,
|
||||||
|
);
|
||||||
|
|
||||||
|
draftLengthMeters.value = GeometryMeasure.lineLengthMeters(points);
|
||||||
|
|
||||||
|
draftAreaSquareMeters.value =
|
||||||
|
GeometryMeasure.polygonAreaSquareMeters(points);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MapEditTool.point:
|
||||||
|
case MapEditTool.none:
|
||||||
|
draftLengthMeters.value = 0.0;
|
||||||
|
draftAreaSquareMeters.value = 0.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class LineOrPolygonDrawingContent extends StatelessWidget {
|
|||||||
return Obx(() {
|
return Obx(() {
|
||||||
final pointCount = controller.editorPointCount.value;
|
final pointCount = controller.editorPointCount.value;
|
||||||
final canFinish = controller.canFinishGeometry;
|
final canFinish = controller.canFinishGeometry;
|
||||||
|
final measureText = controller.draftMeasureText;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -42,6 +43,18 @@ class LineOrPolygonDrawingContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (measureText.isNotEmpty) ...[
|
||||||
|
const SizedBox(
|
||||||
|
width: 6,
|
||||||
|
),
|
||||||
|
SmallStatusChip(
|
||||||
|
text: measureText,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 6,
|
||||||
|
),
|
||||||
|
],
|
||||||
SmallStatusChip(
|
SmallStatusChip(
|
||||||
text: _requiredPointText(controller.activeEditTool.value),
|
text: _requiredPointText(controller.activeEditTool.value),
|
||||||
color: canFinish
|
color: canFinish
|
||||||
|
|||||||
@ -70,13 +70,13 @@ class MapEditCompactToolbar extends StatelessWidget {
|
|||||||
icon: Icons.list_alt_outlined,
|
icon: Icons.list_alt_outlined,
|
||||||
label: 'Lista',
|
label: 'Lista',
|
||||||
selected: false,
|
selected: false,
|
||||||
onTap: controller.openFeatureList,
|
onTap: () {},
|
||||||
),
|
),
|
||||||
ToolbarAction(
|
ToolbarAction(
|
||||||
icon: Icons.layers_outlined,
|
icon: Icons.layers_outlined,
|
||||||
label: 'Rétegek',
|
label: 'Rétegek',
|
||||||
selected: false,
|
selected: false,
|
||||||
onTap: controller.openLayerPanel,
|
onTap: () {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -18,17 +18,19 @@ class SaveSheetActions extends StatelessWidget {
|
|||||||
Row(children: [
|
Row(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10))),
|
borderRadius: BorderRadius.circular(10))),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isEditing ? Icons.close : Icons.arrow_back,
|
isEditing ? Icons.close : Icons.arrow_back,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
label: Text(isEditing ? 'Mégse' : 'Vissza'),
|
label: Text(isEditing ? 'Mégse' : 'Vissza'),
|
||||||
onPressed: () => Get.back(),
|
onPressed: () {
|
||||||
),
|
Get.back();
|
||||||
|
ctrl.cancelEditing();
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user