From 0257beec3869a1dbc76b7a34cca25dcbd9828b7a Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Fri, 19 Jun 2026 12:53:50 +0200 Subject: [PATCH] =?UTF-8?q?Terepbej=C3=A1r=C3=A1s=20geometri=C3=A1k=20szer?= =?UTF-8?q?keszt=C3=A9se:=20kontrollpontok=20=C3=A9s=20tulajdons=C3=A1gok.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/enums/note_type.dart | 1 + lib/models/note_item.dart | 183 +++++++ .../controllers/map_survey_controller.dart | 513 +++++++++++++++--- .../presentations/views/map_survey_view.dart | 81 ++- lib/services/app_database.dart | 80 ++- .../map_feature_save_sheet.dart | 13 +- .../map_edit_tools/save_sheet_actions.dart | 155 +++++- lib/widgets/shared_map_widgets.dart | 3 + 8 files changed, 921 insertions(+), 108 deletions(-) create mode 100644 lib/enums/note_type.dart create mode 100644 lib/models/note_item.dart diff --git a/lib/enums/note_type.dart b/lib/enums/note_type.dart new file mode 100644 index 0000000..99abd04 --- /dev/null +++ b/lib/enums/note_type.dart @@ -0,0 +1 @@ +enum NoteType { point, line, polygon } diff --git a/lib/models/note_item.dart b/lib/models/note_item.dart new file mode 100644 index 0000000..7630783 --- /dev/null +++ b/lib/models/note_item.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +import '../enums/note_type.dart'; + +class NoteItem { + final int? id; + final int? projectId; + final NoteType type; + final List points; // ← szerkesztéshez mindig elérhető + final Color color; + final double opacity; + final double strokeWidth; + final Color strokeColor; + final String label; + final DateTime createdAt; + + const NoteItem( + {this.id, + this.projectId, + required this.type, + required this.points, + required this.color, + this.opacity = 0.5, + this.strokeWidth = 3.0, + required this.strokeColor, + this.label = '', + required this.createdAt}); + + NoteItem copyWith({ + List? points, + Color? color, + double? opacity, + double? strokeWidth, + Color? strokeColor, + String? label, + }) => + NoteItem( + id: id, + projectId: projectId, + type: type, + points: points ?? this.points, + color: color ?? this.color, + opacity: opacity ?? this.opacity, + strokeWidth: strokeWidth ?? this.strokeWidth, + strokeColor: strokeColor ?? this.strokeColor, + label: label ?? this.label, + createdAt: createdAt, + ); + // ── Koordináta ↔ GeoJSON ──────────────────────────────────────── + + static List _parsePoints(String json) { + final geom = jsonDecode(json) as Map; + final gType = geom['type'] as String; + final coords = geom['coordinates']; + + switch (gType) { + case 'Point': + final c = coords as List; + return [ + LatLng( + (c[1] as num).toDouble(), + (c[0] as num).toDouble(), + ) + ]; + + case 'LineString': + return (coords as List) + .map((c) => LatLng( + (c[1] as num).toDouble(), + (c[0] as num).toDouble(), + )) + .toList(); + + case 'Polygon': + // Külső gyűrű (index 0) + return ((coords as List)[0] as List) + .map((c) => LatLng( + (c[1] as num).toDouble(), + (c[0] as num).toDouble(), + )) + .toList(); + + default: + return []; + } + } + + String _toPointsJson() { + List coords; + String gType; + + switch (type) { + case NoteType.point: + // GeoJSON: [lon, lat] + gType = 'Point'; + coords = [points.first.longitude, points.first.latitude]; + + case NoteType.line: + gType = 'LineString'; + coords = points.map((p) => [p.longitude, p.latitude]).toList(); + + case NoteType.polygon: + gType = 'Polygon'; + final ring = points.map((p) => [p.longitude, p.latitude]).toList(); + // Polygon zárt: első = utolsó pont + if (ring.isNotEmpty) { + final first = ring.first; + final last = ring.last; + final isClosed = first[0] == last[0] && first[1] == last[1]; + + if (!isClosed) ring.add(List.from(first)); + } + coords = [ring]; + } + + return jsonEncode({'type': gType, 'coordinates': coords}); + } + // ── SQLite ↔ Map ──────────────────────────────────────────────── + + static Color _parseColor(String hex) { + final h = hex.replaceFirst('#', ''); + return Color(int.parse(h.length == 6 ? 'FF$h' : h, radix: 16)); + } + + static String _colorHex(Color c) => + '#${c.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; + + Map toMap() => { + if (id != null) 'id': id, + if (projectId != null) 'project_id': projectId, + 'type': type.name, + 'points_json': _toPointsJson(), + 'color': _colorHex(color), + 'opacity': opacity, + 'stroke_width': strokeWidth, + 'stroke_color': _colorHex(strokeColor), + 'label': label, + 'created_at': createdAt.toIso8601String(), + }; + + factory NoteItem.fromMap(Map m) => NoteItem( + id: m['id'] as int?, + projectId: m['project_id'] as int?, + type: NoteType.values.firstWhere((t) => t.name == (m['type'] as String), + orElse: () => NoteType.point), + points: _parsePoints(m['points_json'] as String), + color: _parseColor(m['color'] as String? ?? '#185FA5'), + opacity: (m['opacity'] as num?)?.toDouble() ?? 0.5, + strokeWidth: (m['stroke_width'] as num?)?.toDouble() ?? 3.0, + strokeColor: _parseColor(m['stroke_color'] as String? ?? '#FFD700'), + label: m['label'] as String? ?? '', + createdAt: DateTime.parse(m['created_at'] as String), + ); + + /// Kitöltési szín az opacity-val alkalmazva. + Color get fillColor => Color.fromARGB( + (opacity * 255).round(), + color.red, + color.green, + color.blue, + ); + + // Rendereléshez — a hitValue hordozza az id-t a tap detektáláshoz + Polyline toPolyline() => Polyline( + points: points, + color: color, + strokeWidth: strokeWidth, + hitValue: id!, + ); + + Polygon toPolygon() => Polygon( + points: points, + color: color.withOpacity(opacity), + borderColor: strokeColor, + borderStrokeWidth: strokeWidth, + label: label.isEmpty ? null : label, + hitValue: id!, + ); +} 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 803b16c..bd54c81 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -23,8 +23,10 @@ import 'package:terepi_seged/controls/geoid_grid.dart'; import 'package:terepi_seged/controls/wgs84_coordinate_formatter.dart'; import 'package:terepi_seged/enums/map_edit_tool.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'; import 'package:terepi_seged/eov/eov.dart'; +import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/point_to_measure.dart'; import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,11 +34,13 @@ import 'package:terepi_seged/pages/map_survey/presentations/views/measured_point import 'package:terepi_seged/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart'; import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart'; import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_controller.dart'; +import 'package:terepi_seged/services/app_database.dart'; import 'package:terepi_seged/services/coord_converter_service.dart'; import 'package:terepi_seged/services/gnss/gnss_connection.dart'; import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/services/ntrip_service.dart'; +import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart'; class MapSurveyController extends GetxController { @@ -120,6 +124,9 @@ class MapSurveyController extends GetxController { final MapController mapController = MapController(); + final polylineHitNotifier = ValueNotifier?>(null); + final polygonHitNotifier = ValueNotifier?>(null); + final currentLocationMarker = [].obs; final pointNotesMarker = [].obs; final pointsToMeasureMarker = [].obs; @@ -166,8 +173,8 @@ class MapSurveyController extends GetxController { final activeEditTool = MapEditTool.none.obs; final editorPointCount = 0.obs; final pointNotes = [].obs; - final polylineNotes = >[].obs; - final polygonNotes = >[].obs; + final polylineNotes = >[].obs; + final polygonNotes = >[].obs; late final PolygonEditorController polygonEditorController; @@ -181,6 +188,13 @@ class MapSurveyController extends GetxController { const PolygonLabelPlacementCalculator.centroid(); bool get isMapEditing => activeEditTool.value != MapEditTool.none; + final selectedNoteItemId = Rx(null); + final selectedNoteItemType = NoteType.line.obs; + int? _editingNoteItemId; + bool get isGeometryEditing => _editingNoteItemId != null; + + // NoteItem? get selectedPoint => + // pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value); // ───────────────────────────────────────────────────────────────── // Lifecycle @@ -229,6 +243,10 @@ class MapSurveyController extends GetxController { await _initStorage(); gpsHeightController.text = '1.8'; + + ever(ProjectService.to.activeProject, (_) => _loadNoteItems()); + + await _loadNoteItems(); } @override @@ -962,6 +980,9 @@ class MapSurveyController extends GetxController { void cancelEditing() { polygonEditorController.clear(); activeEditTool.value = MapEditTool.none; + _editingNoteItemId = null; + selectedNoteItemId.value = null; + editorPointCount.value = 0; } void openFeatureList() { @@ -1005,73 +1026,437 @@ class MapSurveyController extends GetxController { //draftPoints.clear(); } - Future finishDraft() async { - if (polygonEditorController.mode == PolygonEditorMode.line) { - print("Points number in line: ${polygonEditorController.points.length}"); - print( - "1. point coords: ${polygonEditorController.points[0].latitude} - ${polygonEditorController.points[0].longitude}"); - if (polygonEditorController.points.length < 2) return; + Marker _markerFromNoteItem(NoteItem item) { + return Marker( + key: ValueKey('note_point_${item.id}'), + point: item.points.first, + width: 32.0, + height: 32.0, + child: GestureDetector( + onTap: () => selectedNoteItem(item.id!), + child: Obx(() { + final isSelected = selectedNoteItemId.value == item.id; + return Center( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: isSelected ? 26.0 : 20.0, + height: isSelected ? 26.0 : 20.0, + decoration: BoxDecoration( + color: item.color, + shape: BoxShape.circle, + border: Border.all( + width: isSelected ? 3.0 : 1.5, + color: isSelected ? Colors.white : item.strokeColor), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: isSelected ? 8 : 3) + ]), + )); + }), + ), + ); + } + // ── SQLite mentés ───────────────────────────────────────────────── - Polyline polyline = Polyline( - points: List.from(polygonEditorController.points), - color: activeEditColor.value, - strokeWidth: activeEditStrokeWidth.value, - // hitValue: ( - // title: 'Purple Line', - // subtitle: 'Nothing really special here...', - // ), + Future _saveItem({ + required NoteType type, + required List points, + }) async { + final projectId = ProjectService.to.activeProject.value?.id; + + final item = NoteItem( + projectId: projectId, // projekt nélkül is mentődik + type: type, + points: points, + color: activeEditColor.value, + opacity: activeEditOpacity.value, + strokeWidth: activeEditStrokeWidth.value, + strokeColor: activeEditStrokeColor.value, + label: activeEditLabel.value, + createdAt: DateTime.now(), + ); + + try { + final id = await AppDatabase.instance.insertNoteItem(item); + // id-vel visszaadott NoteItem — a hitValue ehhez az id-hez kötődik + return NoteItem( + id: id, + projectId: item.projectId, + type: item.type, + points: item.points, + color: item.color, + opacity: item.opacity, + strokeWidth: item.strokeWidth, + strokeColor: item.strokeColor, + label: item.label, + createdAt: item.createdAt, ); - polylineNotes.add(polyline); - // polylineNotes.refresh(); - - print("Points number in polylineNotes: ${polylineNotes.length}"); - print( - "1. point coords of polyline: ${polyline.points[0].latitude} - ${polyline.points[0].longitude}"); - - polygonEditorController.clear(); - activeEditTool.value = MapEditTool.none; - } - if (polygonEditorController.mode == PolygonEditorMode.polygon) { - print( - "Points number in polygon: ${polygonEditorController.points.length}"); - - Polygon polygon = Polygon( - points: List.from(polygonEditorController.points), - color: activeEditColor.value.withValues(alpha: activeEditOpacity.value), - borderColor: activeEditStrokeColor.value, - borderStrokeWidth: activeEditStrokeWidth.value, - label: activeEditLabel.value, - labelPlacementCalculator: _labelPlacementCalculator, - // hitValue: ( - // title: 'Basic Filled Polygon', - // subtitle: 'Nothing really special here...', - ); - - polygonNotes.add(polygon); - //polygonNotes.refresh(); - //update(); - - print("Points number in polygonNotes: ${polygonNotes.length}"); - - polygonEditorController.clear(); - activeEditTool.value = MapEditTool.none; + } catch (e) { + print('_saveItem hiba: $e'); + return null; } } - void saveEditedPoint({required LatLng point}) { - Marker marker = Marker( - point: point, - width: 15.0, - height: 15.0, - child: Container( - width: 15.0, - height: 15.0, - decoration: BoxDecoration( - color: Colors.amber[700], - shape: BoxShape.circle, - border: Border.all(width: 1.0, color: Colors.black)), - )); - pointNotes.add(marker); +// ── SQLite betöltés (onReady-ben hívandó) ───────────────────────── + + Future _loadNoteItems() async { + final projectId = ProjectService.to.activeProject.value?.id; + final items = await AppDatabase.instance.listNoteItems(projectId); + + // Listák resetelése + pointNotes.clear(); + polylineNotes.clear(); + polygonNotes.clear(); + + for (final item in items) { + switch (item.type) { + case NoteType.point: + pointNotes.add(_markerFromNoteItem(item)); + case NoteType.line: + polylineNotes.add(item.toPolyline()); + case NoteType.polygon: + polygonNotes.add(item.toPolygon()); + } + } + } + + Future finishDraft() async { + if (_editingNoteItemId != null) { + if (polygonEditorController.points.isEmpty) { + await _finishStyleUpdate(); + } else { + // ── FRISSÍTÉSI MÓD ────────────────────────────────────────── + await _finishGeometryUpdate(); + } + } else { + // ── LÉTREHOZÁSI MÓD (eredeti logika) ──────────────────────── + await _finishCreate(); + } + } + + Future _finishStyleUpdate() async { + final id = _editingNoteItemId!; + final existing = await AppDatabase.instance.getNoteItem(id); + if (existing == null) return; + + final updated = existing.copyWith( + color: activeEditColor.value, + opacity: activeEditOpacity.value, + strokeWidth: activeEditStrokeWidth.value, + strokeColor: activeEditStrokeColor.value, + label: activeEditLabel.value, + ); + + await updateNoteItem(updated); + _editingNoteItemId = null; + } + + Future deleteEditingItem() async { + final id = _editingNoteItemId; + if (id == null) return; + final item = await AppDatabase.instance.getNoteItem(id); + if (item != null) await deleteNoteItem(item); + _editingNoteItemId = null; + selectedNoteItemId.value = null; activeEditTool.value = MapEditTool.none; } + + // Future finishDraft() async { + // if (polygonEditorController.mode == PolygonEditorMode.line) { + // print("Points number in line: ${polygonEditorController.points.length}"); + // print( + // "1. point coords: ${polygonEditorController.points[0].latitude} - ${polygonEditorController.points[0].longitude}"); + // if (polygonEditorController.points.length < 2) return; + + // final saved = await _saveItem( + // type: NoteType.line, + // points: List.from(polygonEditorController.points)); + + // if (saved != null) { + // polylineNotes.add(saved.toPolyline()); + // } + + // polygonEditorController.clear(); + // activeEditTool.value = MapEditTool.none; + // } + // if (polygonEditorController.mode == PolygonEditorMode.polygon) { + // if (polygonEditorController.points.length < 3) return; + + // final saved = await _saveItem( + // type: NoteType.polygon, + // points: List.from(polygonEditorController.points)); + + // if (saved != null) { + // polygonNotes.add(saved.toPolygon()); + // } + + // polygonEditorController.clear(); + // activeEditTool.value = MapEditTool.none; + // } + // } + + void saveEditedPoint({required LatLng point}) { + if (_editingNoteItemId != null) { + // ── PONT FRISSÍTÉSI MÓD ────────────────────────────────────── + final id = _editingNoteItemId!; + _editingNoteItemId = null; + activeEditTool.value = MapEditTool.none; + + AppDatabase.instance.getNoteItem(id).then((existing) async { + if (existing == null) return; + final updated = existing.copyWith(points: [point]); + await updateNoteItem(updated); + }); + } else { + // ── ÚJ PONT LÉTREHOZÁS (eredeti logika) ───────────────────── + _saveItem( + type: NoteType.point, + points: [point], + ).then((saved) { + if (saved == null) return; + pointNotes.add(_markerFromNoteItem(saved)); + }); + activeEditTool.value = MapEditTool.none; + } + } + + // void saveEditedPoint({required LatLng point}) { + // _saveItem(type: NoteType.point, points: [point]).then((saved) { + // if (saved == null) return; + // pointNotes.add(_markerFromNoteItem(saved)); + // }); + + // // Marker marker = Marker( + // // point: point, + // // width: 15.0, + // // height: 15.0, + // // child: Container( + // // width: 15.0, + // // height: 15.0, + // // decoration: BoxDecoration( + // // color: Colors.amber[700], + // // shape: BoxShape.circle, + // // border: Border.all(width: 1.0, color: Colors.black)), + // // )); + // //pointNotes.add(marker); + // activeEditTool.value = MapEditTool.none; + // } + + Future selectedNoteItem(int id) async { + selectedNoteItemId.value = id; + + final item = await AppDatabase.instance.getNoteItem(id); + if (item == null) return; + + _editingNoteItemId = item.id; + activeEditColor.value = item.color; + activeEditOpacity.value = item.opacity; + activeEditStrokeWidth.value = item.strokeWidth; + activeEditStrokeColor.value = item.strokeColor; + activeEditLabel.value = item.label; + activeEditTool.value = switch (item.type) { + NoteType.point => MapEditTool.point, + NoteType.line => MapEditTool.line, + NoteType.polygon => MapEditTool.polygon + }; + + Get.bottomSheet( + DraggableScrollableSheet( + initialChildSize: 0.52, + minChildSize: 0.35, + maxChildSize: 0.85, + snap: true, + snapSizes: const [0.35, 0.52, 0.85], + expand: false, + builder: (_, scrollCtrl) => + MapFeatureSaveSheet(ctrl: this, scrollCtrl: scrollCtrl)), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ignoreSafeArea: false) + .whenComplete(() { + selectedNoteItemId.value = null; + //_editingNoteItemId = null; + if (!isGeometryEditing) { + activeEditTool.value = MapEditTool.none; + } + }); + } + + void clearNoteItemSelection() { + selectedNoteItemId.value = null; + } + + Future updateNoteItem(NoteItem updated) async { + await AppDatabase.instance.updateNoteItem(updated); + + switch (updated.type) { + case NoteType.line: + final idx = polylineNotes.indexWhere((p) => p.hitValue == updated.id); + if (idx >= 0) { + polylineNotes[idx] = updated.toPolyline(); + polylineNotes.refresh(); + } + case NoteType.polygon: + final idx = polygonNotes.indexWhere((p) => p.hitValue == updated.id); + if (idx >= 0) { + polygonNotes[idx] = updated.toPolygon(); + polygonNotes.refresh(); + } + case NoteType.point: + final idx = _findPointMarkerIndex(updated.id!); + if (idx >= 0) { + pointNotes[idx] = _markerFromNoteItem(updated); + pointNotes.refresh(); + } + } + } + + Future deleteNoteItem(NoteItem item) async { + await AppDatabase.instance.deleteNoteItem(item.id!); + + switch (item.type) { + case NoteType.line: + polylineNotes.removeWhere((p) => p.hitValue == item.id); + case NoteType.polygon: + polygonNotes.removeWhere((p) => p.hitValue == item.id); + case NoteType.point: + // Pontot tag-elt Key alapján keresünk + final idx = _findPointMarkerIndex(item.id!); + if (idx >= 0) pointNotes.removeAt(idx); + } + selectedNoteItemId.value = null; + } + + /// Pont marker indexének megkeresése. + /// A marker Key-je hordozza a NoteItem id-t. + int _findPointMarkerIndex(int noteId) { + return pointNotes.indexWhere( + (m) => m.key == ValueKey('note_point_$noteId'), + ); + } + // ── Geometria szerkesztés indítása ──────────────────────────────────── + + Future startGeometryEdit() async { + final id = _editingNoteItemId; + if (id == null) return; + + final item = await AppDatabase.instance.getNoteItem(id); + if (item == null) return; + + if (item.type == NoteType.point) { + // Pontnál: régi törlése, új elhelyezés vár + activeEditTool.value = MapEditTool.point; + return; + } + + // Editor mód beállítása a típus szerint + final editorMode = item.type == NoteType.line + ? PolygonEditorMode.line + : PolygonEditorMode.polygon; + + // PolygonEditorController újraindítása a meglévő pontokkal + // Dispose → újra létrehozás szükséges ha a controller már él + polygonEditorController.clear(); + polygonEditorController.setMode(editorMode); + + // Meglévő pontok betöltése az editorba + for (final point in item.points) { + polygonEditorController.addPoint(point); + } + + editorPointCount.value = item.points.length; + + // Szerkesztési mód aktiválása + activeEditTool.value = + item.type == NoteType.line ? MapEditTool.line : MapEditTool.polygon; + } + +// ── Geometria szerkesztés megszakítása ──────────────────────────────── + + void cancelGeometryEdit() { + _editingNoteItemId = null; + polygonEditorController.clear(); + activeEditTool.value = MapEditTool.none; + editorPointCount.value = 0; + } + + Future _finishGeometryUpdate() async { + final id = _editingNoteItemId!; + + if (polygonEditorController.points.length < _minPoints) { + Get.snackbar( + 'Figyelem', + 'Nincs elég pont a geometriához.', + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + // Meglévő elem lekérése az adatbázisból + final existing = await AppDatabase.instance.getNoteItem(id); + if (existing == null) return; + + // Frissítés: csak a pontok változnak, a stílus marad + final updated = existing.copyWith( + points: List.from(polygonEditorController.points), + // Ha a stílus is változott a szerkesztés közben: + color: activeEditColor.value, + opacity: activeEditOpacity.value, + strokeWidth: activeEditStrokeWidth.value, + strokeColor: activeEditStrokeColor.value, + label: activeEditLabel.value, + ); + + // SQLite + display lista frissítése + await updateNoteItem(updated); + + // Reset + _editingNoteItemId = null; + polygonEditorController.clear(); + activeEditTool.value = MapEditTool.none; + editorPointCount.value = 0; + + Get.snackbar( + 'Geometria frissítve', + updated.label.isNotEmpty ? updated.label : '', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + ); + } + + Future _finishCreate() async { + if (polygonEditorController.mode == PolygonEditorMode.line) { + if (polygonEditorController.points.length < 2) return; + + final saved = await _saveItem( + type: NoteType.line, + points: List.from(polygonEditorController.points), + ); + if (saved != null) { + polylineNotes.add(saved.toPolyline()); + } + polygonEditorController.clear(); + activeEditTool.value = MapEditTool.none; + } + + if (polygonEditorController.mode == PolygonEditorMode.polygon) { + if (polygonEditorController.points.length < 3) return; + + final saved = await _saveItem( + type: NoteType.polygon, + points: List.from(polygonEditorController.points), + ); + if (saved != null) { + polygonNotes.add(saved.toPolygon()); + } + polygonEditorController.clear(); + activeEditTool.value = MapEditTool.none; + } + } + + int get _minPoints { + if (polygonEditorController.mode == PolygonEditorMode.line) return 2; + return 3; // polygon + } } 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 0cc219c..cb8c587 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -41,6 +41,23 @@ class MapSurveyView extends GetView { 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: [ Obx(() => MarkerLayer(markers: controller.currentLocationMarker.toList())), @@ -55,14 +72,6 @@ class MapSurveyView extends GetView { return _buildTrackLayer(); } }), - Obx(() { - if (controller.mode.value != MapSurveyMode.fieldWalk) { - return const SizedBox.shrink(); - } - return PolygonEditor( - controller: controller.polygonEditorController, - throttleDuration: Duration.zero); - }), Obx(() { if (controller.mode.value != MapSurveyMode.fieldWalk) { return const SizedBox.shrink(); @@ -75,7 +84,9 @@ class MapSurveyView extends GetView { return const SizedBox.shrink(); } - return PolylineLayer(polylines: [...controller.polylineNotes]); + return PolylineLayer( + hitNotifier: controller.polylineHitNotifier, + polylines: [...controller.polylineNotes]); }), Obx(() { if (controller.mode.value != MapSurveyMode.fieldWalk) { @@ -83,8 +94,56 @@ class MapSurveyView extends GetView { } return PolygonLayer( - polygons: [...controller.polygonNotes], useAltRendering: true); - }) + 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); + }), ], ), Positioned( diff --git a/lib/services/app_database.dart b/lib/services/app_database.dart index a8d086c..b40edf0 100644 --- a/lib/services/app_database.dart +++ b/lib/services/app_database.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; +import 'package:terepi_seged/enums/note_type.dart'; +import 'package:terepi_seged/models/note_item.dart'; import 'package:terepi_seged/models/track.dart'; import 'package:uuid/uuid.dart'; import '../models/project.dart'; @@ -116,7 +118,7 @@ class AppDatabase { await db.execute(''' CREATE TABLE IF NOT EXISTS note_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, - project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, type TEXT NOT NULL, points_json TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#185FA5', @@ -381,4 +383,80 @@ class AppDatabase { whereArgs: ['synced'], ); } + + // ------------------- Terepbejárás pontok, vonalak, területek + + /// Elem mentése - visszaadja a kapott AQLite id-t + Future insertNoteItem(NoteItem item) async { + final db = await database; + return db.insert('note_items', item.toMap()); + } + + /// Elem frissítése (szín, label, koordináták módosítása után). + Future updateNoteItem(NoteItem item) async { + final db = await database; + await db.update( + 'note_items', + item.toMap(), + where: 'id = ?', + whereArgs: [item.id], + ); + } + + /// Egy elem törlése. + Future deleteNoteItem(int id) async { + final db = await database; + await db.delete( + 'note_items', + where: 'id = ?', + whereArgs: [id], + ); + } + + /// Projekt összes eleme — opcionálisan típus szerint szűrve. + Future> listNoteItems(int? projectId, {NoteType? type}) async { + final db = await database; + + String? where; + List whereArgs = []; + + if (projectId != null) { + where = type != null ? 'project_id = ? AND type = ?' : 'project_id = ?'; + whereArgs = type != null ? [projectId, type.name] : [projectId]; + } else { + // Projekt nélküli elemek + where = type != null ? 'type = ?' : null; + whereArgs = type != null ? [type.name] : []; + } + + final rows = await db.query( + 'note_items', + where: where, + whereArgs: whereArgs, + orderBy: 'created_at ASC', + ); + return rows.map(NoteItem.fromMap).toList(); + } + + /// Egyetlen elem lekérése id alapján. + Future getNoteItem(int id) async { + final db = await database; + final rows = await db.query( + 'note_items', + where: 'id = ?', + whereArgs: [id], + limit: 1, + ); + return rows.isEmpty ? null : NoteItem.fromMap(rows.first); + } + + /// Projekt összes elemének törlése. + Future deleteAllNoteItems(int projectId) async { + final db = await database; + await db.delete( + 'note_items', + where: 'project_id = ?', + whereArgs: [projectId], + ); + } } diff --git a/lib/widgets/map_edit_tools/map_feature_save_sheet.dart b/lib/widgets/map_edit_tools/map_feature_save_sheet.dart index 8a4846b..855d3cf 100644 --- a/lib/widgets/map_edit_tools/map_feature_save_sheet.dart +++ b/lib/widgets/map_edit_tools/map_feature_save_sheet.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; @@ -56,10 +57,14 @@ class MapFeatureSaveSheet extends StatelessWidget { const SizedBox(height: 18), OpacitySlider(ctrl: ctrl), const SizedBox(height: 10), - if (ctrl.activeEditTool.value != MapEditTool.point) ...[ - StrokeSlider(ctrl: ctrl), - const SizedBox(height: 10), - ], + Obx(() => ctrl.activeEditTool.value != MapEditTool.point + ? Column(children: [ + StrokeSlider(ctrl: ctrl), + const SizedBox( + height: 1, + ) + ]) + : const SizedBox.shrink()), LabelField(ctrl: ctrl), const SizedBox(height: 24), SaveSheetActions(ctrl: ctrl), diff --git a/lib/widgets/map_edit_tools/save_sheet_actions.dart b/lib/widgets/map_edit_tools/save_sheet_actions.dart index d251a3b..1a290d5 100644 --- a/lib/widgets/map_edit_tools/save_sheet_actions.dart +++ b/lib/widgets/map_edit_tools/save_sheet_actions.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; class SaveSheetActions extends StatelessWidget { @@ -8,44 +9,142 @@ class SaveSheetActions extends StatelessWidget { @override Widget build(BuildContext context) { - return Row(children: [ - // ← Vissza — bezárja a sheet-et, folytatja a rajzolást - Expanded( - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - icon: const Icon(Icons.arrow_back, size: 18), - label: const Text('Vissza'), - onPressed: () => Navigator.pop(context), - ), - ), - const SizedBox(width: 12), + return Obx(() { + final isEditing = ctrl.isGeometryEditing; + final tool = ctrl.activeEditTool.value; + final isPoint = tool == MapEditTool.point; - // Mentés — végleges mentés, mindkét sheet bezárása - Expanded( - flex: 2, - child: Obx(() => FilledButton.icon( - style: FilledButton.styleFrom( - backgroundColor: ctrl.activeEditColor.value, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), + return Column(mainAxisSize: MainAxisSize.min, children: [ + Row(children: [ + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), + icon: Icon( + isEditing ? Icons.close : Icons.arrow_back, + size: 18, ), + label: Text(isEditing ? 'Mégse' : 'Vissza'), + onPressed: () => Get.back(), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: ctrl.activeEditColor.value, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), icon: const Icon(Icons.check, size: 18), label: const Text( 'Mentés', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15), ), onPressed: () async { - //Navigator.pop(context); // style sheet bezárás Get.back(); await ctrl.finishDraft(); }, - )), - ), - ]); + ), + ) + ]), + if (isEditing && !isPoint) ...[ + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + icon: const Icon(Icons.edit_outlined, size: 18), + label: const Text('Geometria szerkesztése'), + onPressed: () { + Get.back(); + ctrl.startGeometryEdit(); + }, + ), + ), + ], + if (isEditing) ...[ + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), + icon: const Icon(Icons.delete_outline, size: 18), + label: const Text('Törlés'), + onPressed: () => _confirmDelete(context, ctrl), + ), + ) + ] + ]); + }); + } + // return Row(children: [ + // // ← Vissza — bezárja a sheet-et, folytatja a rajzolást + // Expanded( + // child: OutlinedButton.icon( + // style: OutlinedButton.styleFrom( + // padding: const EdgeInsets.symmetric(vertical: 14), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10)), + // ), + // icon: const Icon(Icons.arrow_back, size: 18), + // label: const Text('Vissza'), + // onPressed: () => Navigator.pop(context), + // ), + // ), + // const SizedBox(width: 12), + + // // Mentés — végleges mentés, mindkét sheet bezárása + // Expanded( + // flex: 2, + // child: Obx(() => FilledButton.icon( + // style: FilledButton.styleFrom( + // backgroundColor: ctrl.activeEditColor.value, + // padding: const EdgeInsets.symmetric(vertical: 14), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10)), + // ), + // icon: const Icon(Icons.check, size: 18), + // label: const Text( + // 'Mentés', + // style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15), + // ), + // onPressed: () async { + // //Navigator.pop(context); // style sheet bezárás + // Get.back(); + // await ctrl.finishDraft(); + // }, + // )), + // ), + // ]); + void _confirmDelete(BuildContext context, MapSurveyController ctrl) { + Get.dialog(AlertDialog( + title: const Text('Elem törlése'), + content: const Text('Ez az elem véglegesen törlődik.'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('Mégse')), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.red), + onPressed: () async { + Get.back(); // dialóg + Get.back(); // sheet + await ctrl.deleteEditingItem(); + }, + child: const Text('Törlés'), + ), + ], + )); } } diff --git a/lib/widgets/shared_map_widgets.dart b/lib/widgets/shared_map_widgets.dart index d74b13b..10a353f 100644 --- a/lib/widgets/shared_map_widgets.dart +++ b/lib/widgets/shared_map_widgets.dart @@ -8,6 +8,7 @@ class SharedMapWidget extends StatelessWidget { final MapController mapController; final List layers; final void Function(TapPosition, LatLng)? onLongPress; + final void Function(TapPosition, LatLng)? onTap; final void Function(MapCamera, bool)? onPositionChanged; final MapControls controls; @@ -33,6 +34,7 @@ class SharedMapWidget extends StatelessWidget { required this.mapController, this.layers = const [], this.onLongPress, + this.onTap, this.onPositionChanged, this.controls = const MapControls(), this.initialCenter = const LatLng(47.5, 19.0), @@ -61,6 +63,7 @@ class SharedMapWidget extends StatelessWidget { minZoom: minZoom, maxZoom: maxZoom, onLongPress: onLongPress, + onTap: onTap, onPositionChanged: onPositionChanged, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all,