Terepbejárás geometriák szerkesztése: kontrollpontok és tulajdonságok.

This commit is contained in:
torok.istvan 2026-06-19 12:53:50 +02:00
parent 1276ac0610
commit 0257beec38
8 changed files with 921 additions and 108 deletions

1
lib/enums/note_type.dart Normal file
View File

@ -0,0 +1 @@
enum NoteType { point, line, polygon }

183
lib/models/note_item.dart Normal file
View File

@ -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<LatLng> 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<LatLng>? 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<LatLng> _parsePoints(String json) {
final geom = jsonDecode(json) as Map<String, dynamic>;
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<dynamic> 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<String, dynamic> 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<String, dynamic> 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<int> toPolyline() => Polyline(
points: points,
color: color,
strokeWidth: strokeWidth,
hitValue: id!,
);
Polygon<int> toPolygon() => Polygon(
points: points,
color: color.withOpacity(opacity),
borderColor: strokeColor,
borderStrokeWidth: strokeWidth,
label: label.isEmpty ? null : label,
hitValue: id!,
);
}

View File

@ -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/controls/wgs84_coordinate_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/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.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_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart'; import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:shared_preferences/shared_preferences.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/controllers/ntrip_settings_controller.dart';
import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.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/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/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_connection.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_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/services/ntrip_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'; import 'package:terepi_seged/widgets/map_edit_tools/map_feature_save_sheet.dart';
class MapSurveyController extends GetxController { class MapSurveyController extends GetxController {
@ -120,6 +124,9 @@ class MapSurveyController extends GetxController {
final MapController mapController = MapController(); final MapController mapController = MapController();
final polylineHitNotifier = ValueNotifier<LayerHitResult<int>?>(null);
final polygonHitNotifier = ValueNotifier<LayerHitResult<int>?>(null);
final currentLocationMarker = <Marker>[].obs; final currentLocationMarker = <Marker>[].obs;
final pointNotesMarker = <Marker>[].obs; final pointNotesMarker = <Marker>[].obs;
final pointsToMeasureMarker = <Marker>[].obs; final pointsToMeasureMarker = <Marker>[].obs;
@ -166,8 +173,8 @@ class MapSurveyController extends GetxController {
final activeEditTool = MapEditTool.none.obs; final activeEditTool = MapEditTool.none.obs;
final editorPointCount = 0.obs; final editorPointCount = 0.obs;
final pointNotes = <Marker>[].obs; final pointNotes = <Marker>[].obs;
final polylineNotes = <Polyline<Object>>[].obs; final polylineNotes = <Polyline<int>>[].obs;
final polygonNotes = <Polygon<Object>>[].obs; final polygonNotes = <Polygon<int>>[].obs;
late final PolygonEditorController polygonEditorController; late final PolygonEditorController polygonEditorController;
@ -181,6 +188,13 @@ class MapSurveyController extends GetxController {
const PolygonLabelPlacementCalculator.centroid(); const PolygonLabelPlacementCalculator.centroid();
bool get isMapEditing => activeEditTool.value != MapEditTool.none; bool get isMapEditing => activeEditTool.value != MapEditTool.none;
final selectedNoteItemId = Rx<int?>(null);
final selectedNoteItemType = NoteType.line.obs;
int? _editingNoteItemId;
bool get isGeometryEditing => _editingNoteItemId != null;
// NoteItem? get selectedPoint =>
// pointNotes.firstWhereOrNull((n) => n.id == selectedNoteItemId.value);
// //
// Lifecycle // Lifecycle
@ -229,6 +243,10 @@ class MapSurveyController extends GetxController {
await _initStorage(); await _initStorage();
gpsHeightController.text = '1.8'; gpsHeightController.text = '1.8';
ever(ProjectService.to.activeProject, (_) => _loadNoteItems());
await _loadNoteItems();
} }
@override @override
@ -962,6 +980,9 @@ class MapSurveyController extends GetxController {
void cancelEditing() { void cancelEditing() {
polygonEditorController.clear(); polygonEditorController.clear();
activeEditTool.value = MapEditTool.none; activeEditTool.value = MapEditTool.none;
_editingNoteItemId = null;
selectedNoteItemId.value = null;
editorPointCount.value = 0;
} }
void openFeatureList() { void openFeatureList() {
@ -1005,73 +1026,437 @@ class MapSurveyController extends GetxController {
//draftPoints.clear(); //draftPoints.clear();
} }
Future<void> finishDraft() async { Marker _markerFromNoteItem(NoteItem item) {
if (polygonEditorController.mode == PolygonEditorMode.line) { return Marker(
print("Points number in line: ${polygonEditorController.points.length}"); key: ValueKey('note_point_${item.id}'),
print( point: item.points.first,
"1. point coords: ${polygonEditorController.points[0].latitude} - ${polygonEditorController.points[0].longitude}"); width: 32.0,
if (polygonEditorController.points.length < 2) return; 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( Future<NoteItem?> _saveItem({
points: List.from(polygonEditorController.points), required NoteType type,
color: activeEditColor.value, required List<LatLng> points,
strokeWidth: activeEditStrokeWidth.value, }) async {
// hitValue: ( final projectId = ProjectService.to.activeProject.value?.id;
// title: 'Purple Line',
// subtitle: 'Nothing really special here...', 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); } catch (e) {
// polylineNotes.refresh(); print('_saveItem hiba: $e');
return null;
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;
} }
} }
void saveEditedPoint({required LatLng point}) { // SQLite betöltés (onReady-ben hívandó)
Marker marker = Marker(
point: point, Future<void> _loadNoteItems() async {
width: 15.0, final projectId = ProjectService.to.activeProject.value?.id;
height: 15.0, final items = await AppDatabase.instance.listNoteItems(projectId);
child: Container(
width: 15.0, // Listák resetelése
height: 15.0, pointNotes.clear();
decoration: BoxDecoration( polylineNotes.clear();
color: Colors.amber[700], polygonNotes.clear();
shape: BoxShape.circle,
border: Border.all(width: 1.0, color: Colors.black)), for (final item in items) {
)); switch (item.type) {
pointNotes.add(marker); case NoteType.point:
pointNotes.add(_markerFromNoteItem(item));
case NoteType.line:
polylineNotes.add(item.toPolyline());
case NoteType.polygon:
polygonNotes.add(item.toPolygon());
}
}
}
Future<void> 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<void> _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<void> 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; activeEditTool.value = MapEditTool.none;
} }
// Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> _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<LatLng>.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<void> _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
}
} }

View File

@ -41,6 +41,23 @@ class MapSurveyView extends GetView<MapSurveyController> {
controller.polygonEditorController.addPoint(point); 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: [ layers: [
Obx(() => Obx(() =>
MarkerLayer(markers: controller.currentLocationMarker.toList())), MarkerLayer(markers: controller.currentLocationMarker.toList())),
@ -55,14 +72,6 @@ class MapSurveyView extends GetView<MapSurveyController> {
return _buildTrackLayer(); return _buildTrackLayer();
} }
}), }),
Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) {
return const SizedBox.shrink();
}
return PolygonEditor(
controller: controller.polygonEditorController,
throttleDuration: Duration.zero);
}),
Obx(() { Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) { if (controller.mode.value != MapSurveyMode.fieldWalk) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -75,7 +84,9 @@ class MapSurveyView extends GetView<MapSurveyController> {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return PolylineLayer(polylines: [...controller.polylineNotes]); return PolylineLayer(
hitNotifier: controller.polylineHitNotifier,
polylines: [...controller.polylineNotes]);
}), }),
Obx(() { Obx(() {
if (controller.mode.value != MapSurveyMode.fieldWalk) { if (controller.mode.value != MapSurveyMode.fieldWalk) {
@ -83,8 +94,56 @@ class MapSurveyView extends GetView<MapSurveyController> {
} }
return PolygonLayer( 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( Positioned(

View File

@ -3,6 +3,8 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p; 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:terepi_seged/models/track.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/project.dart'; import '../models/project.dart';
@ -116,7 +118,7 @@ class AppDatabase {
await db.execute(''' await db.execute('''
CREATE TABLE IF NOT EXISTS note_items ( CREATE TABLE IF NOT EXISTS note_items (
id INTEGER PRIMARY KEY AUTOINCREMENT, 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, type TEXT NOT NULL,
points_json TEXT NOT NULL, points_json TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#185FA5', color TEXT NOT NULL DEFAULT '#185FA5',
@ -381,4 +383,80 @@ class AppDatabase {
whereArgs: ['synced'], whereArgs: ['synced'],
); );
} }
// ------------------- Terepbejárás pontok, vonalak, területek
/// Elem mentése - visszaadja a kapott AQLite id-t
Future<int> 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<void> 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<void> 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<List<NoteItem>> listNoteItems(int? projectId, {NoteType? type}) async {
final db = await database;
String? where;
List<Object?> 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<NoteItem?> 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<void> deleteAllNoteItems(int projectId) async {
final db = await database;
await db.delete(
'note_items',
where: 'project_id = ?',
whereArgs: [projectId],
);
}
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/enums/map_edit_tool.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';
@ -56,10 +57,14 @@ class MapFeatureSaveSheet extends StatelessWidget {
const SizedBox(height: 18), const SizedBox(height: 18),
OpacitySlider(ctrl: ctrl), OpacitySlider(ctrl: ctrl),
const SizedBox(height: 10), const SizedBox(height: 10),
if (ctrl.activeEditTool.value != MapEditTool.point) ...[ Obx(() => ctrl.activeEditTool.value != MapEditTool.point
StrokeSlider(ctrl: ctrl), ? Column(children: [
const SizedBox(height: 10), StrokeSlider(ctrl: ctrl),
], const SizedBox(
height: 1,
)
])
: const SizedBox.shrink()),
LabelField(ctrl: ctrl), LabelField(ctrl: ctrl),
const SizedBox(height: 24), const SizedBox(height: 24),
SaveSheetActions(ctrl: ctrl), SaveSheetActions(ctrl: ctrl),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart';
class SaveSheetActions extends StatelessWidget { class SaveSheetActions extends StatelessWidget {
@ -8,44 +9,142 @@ class SaveSheetActions extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row(children: [ return Obx(() {
// Vissza bezárja a sheet-et, folytatja a rajzolást final isEditing = ctrl.isGeometryEditing;
Expanded( final tool = ctrl.activeEditTool.value;
child: OutlinedButton.icon( final isPoint = tool == MapEditTool.point;
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 return Column(mainAxisSize: MainAxisSize.min, children: [
Expanded( Row(children: [
flex: 2, Expanded(
child: Obx(() => FilledButton.icon( child: OutlinedButton.icon(
style: FilledButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: ctrl.activeEditColor.value, 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(
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), icon: const Icon(Icons.check, size: 18),
label: const Text( label: const Text(
'Mentés', 'Mentés',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15), style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
), ),
onPressed: () async { onPressed: () async {
//Navigator.pop(context); // style sheet bezárás
Get.back(); Get.back();
await ctrl.finishDraft(); 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'),
),
],
));
} }
} }

View File

@ -8,6 +8,7 @@ class SharedMapWidget extends StatelessWidget {
final MapController mapController; final MapController mapController;
final List<Widget> layers; final List<Widget> layers;
final void Function(TapPosition, LatLng)? onLongPress; final void Function(TapPosition, LatLng)? onLongPress;
final void Function(TapPosition, LatLng)? onTap;
final void Function(MapCamera, bool)? onPositionChanged; final void Function(MapCamera, bool)? onPositionChanged;
final MapControls controls; final MapControls controls;
@ -33,6 +34,7 @@ class SharedMapWidget extends StatelessWidget {
required this.mapController, required this.mapController,
this.layers = const [], this.layers = const [],
this.onLongPress, this.onLongPress,
this.onTap,
this.onPositionChanged, this.onPositionChanged,
this.controls = const MapControls(), this.controls = const MapControls(),
this.initialCenter = const LatLng(47.5, 19.0), this.initialCenter = const LatLng(47.5, 19.0),
@ -61,6 +63,7 @@ class SharedMapWidget extends StatelessWidget {
minZoom: minZoom, minZoom: minZoom,
maxZoom: maxZoom, maxZoom: maxZoom,
onLongPress: onLongPress, onLongPress: onLongPress,
onTap: onTap,
onPositionChanged: onPositionChanged, onPositionChanged: onPositionChanged,
interactionOptions: const InteractionOptions( interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all, flags: InteractiveFlag.all,