From 1276ac0610c8d0ebeaa2b7943a971b96ea1dd5b7 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Tue, 16 Jun 2026 14:12:44 +0200 Subject: [PATCH] =?UTF-8?q?Terepbej=C3=A1r=C3=A1s=20oldalon=20vonal=20?= =?UTF-8?q?=C3=A9s=20ter=C3=BClet=20tulajdons=C3=A1gainak=20szerkeszt?= =?UTF-8?q?=C3=A9se,=20ment=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/enums/map_edit_tool.dart | 6 + .../controllers/map_survey_controller.dart | 228 ++++++++++++++++++ .../presentations/views/map_survey_view.dart | 94 ++++++-- lib/widgets/map_edit_tools/color_row.dart | 61 +++++ lib/widgets/map_edit_tools/label_field.dart | 60 +++++ .../map_edit_tools/labeled_slider.dart | 58 +++++ .../map_edit_drawing_toolbar.dart | 57 +++++ ..._edit_line_or_polygon_drawing_content.dart | 110 +++++++++ .../map_edit_point_drawing_content.dart | 103 ++++++++ .../map_edit_small_status_chip.dart | 37 +++ .../map_edit_square_toolbar_button.dart | 36 +++ .../map_edit_tools/map_edit_toolbar.dart | 89 +++++++ .../map_feature_save_sheet.dart | 78 ++++++ .../map_edit_tools/map_toolbar_action.dart | 79 ++++++ .../map_edit_tools/map_toolbar_divider.dart | 15 ++ .../map_edit_tools/opacity_slider.dart | 21 ++ .../map_edit_tools/save_sheet_actions.dart | 51 ++++ lib/widgets/map_edit_tools/sheet_handle.dart | 18 ++ lib/widgets/map_edit_tools/stroke_slider.dart | 22 ++ 19 files changed, 1206 insertions(+), 17 deletions(-) create mode 100644 lib/enums/map_edit_tool.dart create mode 100644 lib/widgets/map_edit_tools/color_row.dart create mode 100644 lib/widgets/map_edit_tools/label_field.dart create mode 100644 lib/widgets/map_edit_tools/labeled_slider.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_drawing_toolbar.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_point_drawing_content.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_small_status_chip.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_square_toolbar_button.dart create mode 100644 lib/widgets/map_edit_tools/map_edit_toolbar.dart create mode 100644 lib/widgets/map_edit_tools/map_feature_save_sheet.dart create mode 100644 lib/widgets/map_edit_tools/map_toolbar_action.dart create mode 100644 lib/widgets/map_edit_tools/map_toolbar_divider.dart create mode 100644 lib/widgets/map_edit_tools/opacity_slider.dart create mode 100644 lib/widgets/map_edit_tools/save_sheet_actions.dart create mode 100644 lib/widgets/map_edit_tools/sheet_handle.dart create mode 100644 lib/widgets/map_edit_tools/stroke_slider.dart diff --git a/lib/enums/map_edit_tool.dart b/lib/enums/map_edit_tool.dart new file mode 100644 index 0000000..06617f7 --- /dev/null +++ b/lib/enums/map_edit_tool.dart @@ -0,0 +1,6 @@ +enum MapEditTool { + none, + point, + line, + polygon, +} 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 1d875f7..803b16c 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -8,6 +8,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor_controller.dart'; // import 'package:flutter_map_geojson/flutter_map_geojson.dart'; import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; import 'package:geolocator/geolocator.dart'; @@ -20,6 +21,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; 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/eov/convert_coordinate.dart'; import 'package:terepi_seged/eov/eov.dart'; @@ -35,6 +37,7 @@ 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/widgets/map_edit_tools/map_feature_save_sheet.dart'; class MapSurveyController extends GetxController { static MapSurveyController get to => Get.find(); @@ -159,6 +162,26 @@ class MapSurveyController extends GetxController { // ── Supabase ────────────────────────────────────────────────────── RealtimeChannel? _supaChannel; + // ------- Map edit ---------------- + final activeEditTool = MapEditTool.none.obs; + final editorPointCount = 0.obs; + final pointNotes = [].obs; + final polylineNotes = >[].obs; + final polygonNotes = >[].obs; + + late final PolygonEditorController polygonEditorController; + + final activeEditColor = const Color(0xFF185FA5).obs; + final activeEditOpacity = 0.5.obs; + final activeEditStrokeWidth = 3.0.obs; + final activeEditStrokeColor = const Color(0xFFFFD700).obs; + final activeEditLabel = ''.obs; + + final PolygonLabelPlacementCalculator _labelPlacementCalculator = + const PolygonLabelPlacementCalculator.centroid(); + + bool get isMapEditing => activeEditTool.value != MapEditTool.none; + // ───────────────────────────────────────────────────────────────── // Lifecycle // ───────────────────────────────────────────────────────────────── @@ -190,6 +213,12 @@ class MapSurveyController extends GetxController { ) .subscribe(); + polygonEditorController = + PolygonEditorController(mode: PolygonEditorMode.polygon); + polygonEditorController.addListener(() { + editorPointCount.value = polygonEditorController.points.length; + }); + mapIsInitialized.value = true; } @@ -214,6 +243,7 @@ class MapSurveyController extends GetxController { gpsHeightController.dispose(); pointPrefixController.dispose(); pointPostfixController.dispose(); + polygonEditorController.dispose(); super.onClose(); } @@ -846,4 +876,202 @@ class MapSurveyController extends GetxController { return '${value.toStringAsFixed(1)}m'; } + + // --------- Térkép szerkesztési műveletek + IconData get activeEditToolIcon { + switch (activeEditTool.value) { + case MapEditTool.point: + return Icons.add_location_alt_outlined; + case MapEditTool.line: + return Icons.polyline_outlined; + case MapEditTool.polygon: + return Icons.border_outer_outlined; + case MapEditTool.none: + return Icons.edit_location_alt_outlined; + } + } + + String get activeEditToolTitle { + switch (activeEditTool.value) { + case MapEditTool.point: + return 'Pont hozzáadása'; + case MapEditTool.line: + return 'Vonal rögzítése'; + case MapEditTool.polygon: + return 'Terület rögzítése'; + case MapEditTool.none: + return ''; + } + } + + String get activeEditToolHint { + switch (activeEditTool.value) { + case MapEditTool.point: + return 'Koppints a térképre a pont helyéhez.'; + case MapEditTool.line: + return 'Hosszan nyomj a térképre a töréspontokhoz'; + case MapEditTool.polygon: + return 'Hosszan nyomj a térképre a sarokpontokhoz.'; + case MapEditTool.none: + return ''; + } + } + + bool get canFinishGeometry { + switch (activeEditTool.value) { + case MapEditTool.point: + return editorPointCount == 1; + case MapEditTool.line: + return editorPointCount >= 2; + case MapEditTool.polygon: + return editorPointCount >= 3; + case MapEditTool.none: + return false; + } + } + + String get finishButtonText { + switch (activeEditTool.value) { + case MapEditTool.point: + return 'Kész'; + case MapEditTool.line: + return 'Kész'; + case MapEditTool.polygon: + return 'Lezárás'; + case MapEditTool.none: + return 'Kész'; + } + } + + void startPointTool() { + activeEditTool.value = MapEditTool.point; + } + + void startLineTool() { + polygonEditorController.clear(); + polygonEditorController.setMode(PolygonEditorMode.line); + activeEditTool.value = MapEditTool.line; + } + + void startPolygonTool() { + polygonEditorController.clear(); + polygonEditorController.setMode(PolygonEditorMode.polygon); + activeEditTool.value = MapEditTool.polygon; + } + + void cancelEditing() { + polygonEditorController.clear(); + activeEditTool.value = MapEditTool.none; + } + + void openFeatureList() { + // TODO: DraggableScrollableSheet / bottom sheet lista + } + + void openLayerPanel() { + // TODO: rétegek sheet + } + + void finishGeometry() { + if (!canFinishGeometry) return; + + // Itt nyisd majd meg a szerkesztő sheetet: + // openFeatureEditorSheet(); + + // Példa: + // Get.bottomSheet( + // FeatureEditorSheet( + // tool: activeTool.value, + // points: List.from(draftPoints), + // ), + // isScrollControlled: true, + // ); + + 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); + + //activeEditTool.value = MapEditTool.none; + //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; + + Polyline polyline = Polyline( + points: List.from(polygonEditorController.points), + color: activeEditColor.value, + strokeWidth: activeEditStrokeWidth.value, + // hitValue: ( + // title: 'Purple Line', + // subtitle: 'Nothing really special here...', + // ), + ); + 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; + } + } + + 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); + activeEditTool.value = MapEditTool.none; + } } 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 ffee9f1..0cc219c 100644 --- a/lib/pages/map_survey/presentations/views/map_survey_view.dart +++ b/lib/pages/map_survey/presentations/views/map_survey_view.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map_polygon_editor/polygon_editor/polygon_editor.dart'; import 'package:flutter_map_polywidget/flutter_map_polywidget.dart'; import 'package:get/get.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/enums/map_survey_mode.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import 'package:terepi_seged/pages/map_survey/presentations/views/settings_dialog.dart'; @@ -10,6 +12,8 @@ import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_co import 'package:terepi_seged/utils/rive_utils.dart'; import 'package:terepi_seged/widgets/coordinate_panel.dart'; import 'package:terepi_seged/widgets/map_bottom_panel.dart'; +import 'package:terepi_seged/widgets/map_edit_tools/map_edit_drawing_toolbar.dart'; +import 'package:terepi_seged/widgets/map_edit_tools/map_edit_toolbar.dart'; import 'package:terepi_seged/widgets/map_info_card_column.dart'; import 'package:terepi_seged/widgets/save_point_fab.dart'; import 'package:terepi_seged/widgets/shared_map_widgets.dart'; @@ -28,6 +32,15 @@ class MapSurveyView extends GetView { onZoomIn: controller.mapZoomIn, onZoomOut: controller.mapZoomOut, onCenterOnGps: controller.isMapMoveToCenter, + onLongPress: (tapPosition, point) { + if (controller.activeEditTool.value == MapEditTool.point) { + controller.saveEditedPoint(point: point); + } + if (controller.activeEditTool.value == MapEditTool.line || + controller.activeEditTool.value == MapEditTool.polygon) { + controller.polygonEditorController.addPoint(point); + } + }, layers: [ Obx(() => MarkerLayer(markers: controller.currentLocationMarker.toList())), @@ -41,6 +54,36 @@ class MapSurveyView extends GetView { } else { 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(); + } + + return MarkerLayer(markers: [...controller.pointNotes]); + }), + Obx(() { + if (controller.mode.value != MapSurveyMode.fieldWalk) { + return const SizedBox.shrink(); + } + + return PolylineLayer(polylines: [...controller.polylineNotes]); + }), + Obx(() { + if (controller.mode.value != MapSurveyMode.fieldWalk) { + return const SizedBox.shrink(); + } + + return PolygonLayer( + polygons: [...controller.polygonNotes], useAltRendering: true); }) ], ), @@ -50,23 +93,40 @@ class MapSurveyView extends GetView { child: MapInfoCardColumn(controller: controller), ), - Positioned( - top: 390, - right: 60, - left: 8, - child: CoordinatePanel( - eovY: controller.eovY, - eovX: controller.eovX, - horError: controller.gpsLatitudeError, - vertError: controller.gpsAltitudeError, - altitudeMsl: controller.gpsAltitude, - geoidSeparation: controller.gpsGeoidSeparation, - ntripConnected: controller.ntripIsConnected, - ntripBytes: controller.ntripReceivedData, - ntripPackets: controller.ntripDataPacketNumbers, - ggaPackets: controller.ggaSenDataPacketNumber, - ), - ), + // Positioned( + // top: 390, + // right: 60, + // left: 8, + // child: CoordinatePanel( + // eovY: controller.eovY, + // eovX: controller.eovX, + // horError: controller.gpsLatitudeError, + // vertError: controller.gpsAltitudeError, + // altitudeMsl: controller.gpsAltitude, + // geoidSeparation: controller.gpsGeoidSeparation, + // ntripConnected: controller.ntripIsConnected, + // ntripBytes: controller.ntripReceivedData, + // ntripPackets: controller.ntripDataPacketNumbers, + // ggaPackets: controller.ggaSenDataPacketNumber, + // ), + // ), + Obx(() { + if (controller.mode.value != MapSurveyMode.fieldWalk) { + return const SizedBox.shrink(); + } + if (controller.activeEditTool.value == MapEditTool.none) { + return Positioned( + left: 0, + right: 0, + bottom: 0, + child: MapEditCompactToolbar(controller: controller)); + } + return Positioned( + left: 0, + right: 0, + bottom: 0, + child: MapEditDrawingToolbar(controller: controller)); + }) // Positioned(top: 8, left: 0, right: 0, child: _ModeSelector()), // Positioned( // bottom: 80, diff --git a/lib/widgets/map_edit_tools/color_row.dart b/lib/widgets/map_edit_tools/color_row.dart new file mode 100644 index 0000000..0f9e686 --- /dev/null +++ b/lib/widgets/map_edit_tools/color_row.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class ColorRow extends StatelessWidget { + final MapSurveyController ctrl; + static const _palette = [ + Color(0xFF6C63FF), + Color(0xFFE74C3C), + Color(0xFF27AE60), + Color(0xFFE91E8C), + Color(0xFFE67E22), + Color(0xFF3498DB), + Color(0xFF8BC34A), + ]; + const ColorRow({required this.ctrl}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Kitöltési szín', + style: TextStyle(fontSize: 13, color: Colors.grey.shade600)), + const SizedBox(height: 10), + Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _palette.map((color) { + final sel = ctrl.activeEditColor.value == color; + return GestureDetector( + onTap: () => ctrl.activeEditColor.value = color, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: 40, + height: 40, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: sel + ? Theme.of(context).colorScheme.onSurface + : Colors.transparent, + width: 3, + ), + boxShadow: sel + ? [ + BoxShadow( + color: color.withOpacity(0.45), + blurRadius: 8, + spreadRadius: 2) + ] + : null, + ), + ), + ); + }).toList(), + )), + ], + ); + } +} diff --git a/lib/widgets/map_edit_tools/label_field.dart b/lib/widgets/map_edit_tools/label_field.dart new file mode 100644 index 0000000..b83b82d --- /dev/null +++ b/lib/widgets/map_edit_tools/label_field.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:terepi_seged/enums/map_edit_tool.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class LabelField extends StatefulWidget { + final MapSurveyController ctrl; + const LabelField({required this.ctrl}); + + @override + State createState() => LabelFieldState(); +} + +class LabelFieldState extends State { + late final TextEditingController _text; + + @override + void initState() { + super.initState(); + _text = TextEditingController(text: widget.ctrl.activeEditLabel.value); + _text.addListener(() => widget.ctrl.activeEditLabel.value = _text.text); + } + + @override + void dispose() { + _text.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final hint = switch (widget.ctrl.activeEditTool.value) { + MapEditTool.point => 'Pont neve...', + MapEditTool.line => 'Vonal neve...', + MapEditTool.polygon => 'Terület neve...', + MapEditTool.none => 'Felirat...', + }; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Felirat', + style: TextStyle(fontSize: 13, color: Colors.grey.shade600)), + const SizedBox(height: 6), + TextField( + controller: _text, + decoration: InputDecoration( + hintText: hint, + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/map_edit_tools/labeled_slider.dart b/lib/widgets/map_edit_tools/labeled_slider.dart new file mode 100644 index 0000000..2729b00 --- /dev/null +++ b/lib/widgets/map_edit_tools/labeled_slider.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class LabeledSlider extends StatelessWidget { + final String label; + final double value; + final double min, max; + final int? divisions; + final String display; + final Color color; + final ValueChanged onChanged; + const LabeledSlider({ + required this.label, + required this.value, + required this.min, + required this.max, + required this.display, + required this.color, + required this.onChanged, + this.divisions, + }); + + @override + Widget build(BuildContext context) => Row(children: [ + SizedBox( + width: 90, + child: Text(label, + style: TextStyle(fontSize: 13, color: Colors.grey.shade600)), + ), + Expanded( + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: color, + thumbColor: color, + overlayColor: color.withOpacity(0.15), + inactiveTrackColor: Colors.grey.shade200, + trackHeight: 3.0, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), + ), + child: Slider( + value: value.clamp(min, max), + min: min, + max: max, + divisions: divisions, + onChanged: onChanged, + ), + ), + ), + SizedBox( + width: 48, + child: Text(display, + textAlign: TextAlign.right, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + fontFeatures: [FontFeature.tabularFigures()])), + ), + ]); +} diff --git a/lib/widgets/map_edit_tools/map_edit_drawing_toolbar.dart b/lib/widgets/map_edit_tools/map_edit_drawing_toolbar.dart new file mode 100644 index 0000000..4f1721e --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_drawing_toolbar.dart @@ -0,0 +1,57 @@ +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'; + +import 'map_edit_line_or_polygon_drawing_content.dart'; +import 'map_edit_point_drawing_content.dart'; + +class MapEditDrawingToolbar extends StatelessWidget { + final MapSurveyController controller; + + const MapEditDrawingToolbar({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + final tool = controller.activeEditTool.value; + + if (tool == MapEditTool.none) { + return const SizedBox.shrink(); + } + + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(10, 0, 10, 10), + child: Align( + alignment: Alignment.bottomCenter, + child: Material( + elevation: 8, + color: Theme.of(context).colorScheme.surface.withOpacity(0.97), + borderRadius: BorderRadius.circular(22), + clipBehavior: Clip.antiAlias, + child: Container( + constraints: const BoxConstraints( + maxWidth: 560, + ), + padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: + Theme.of(context).colorScheme.outline.withOpacity(0.22), + ), + ), + child: tool == MapEditTool.point + ? PointDrawingContent(controller: controller) + : LineOrPolygonDrawingContent(controller: controller), + ), + ), + ), + ); + }); + } +} diff --git a/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart b/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart new file mode 100644 index 0000000..9345050 --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_line_or_polygon_drawing_content.dart @@ -0,0 +1,110 @@ +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'; + +import 'map_edit_small_status_chip.dart'; +import 'map_edit_square_toolbar_button.dart'; + +class LineOrPolygonDrawingContent extends StatelessWidget { + final MapSurveyController controller; + + const LineOrPolygonDrawingContent({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Obx(() { + final pointCount = controller.editorPointCount.value; + final canFinish = controller.canFinishGeometry; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon( + controller.activeEditToolIcon, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + '${controller.activeEditToolTitle} · $pointCount pont', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + SmallStatusChip( + text: _requiredPointText(controller.activeEditTool.value), + color: canFinish + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + ], + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.centerLeft, + child: Text( + controller.activeEditToolHint, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.15, + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + // SquareToolbarButton( + // tooltip: 'Utolsó pont törlése', + // icon: Icons.undo, + // // onPressed: controller.canUndo ? controller.undoLastPoint : null, + // onPressed: () {}, + // ), + // const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: controller.cancelEditing, + icon: const Icon(Icons.close, size: 18), + label: const Text('Mégse'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: FilledButton.icon( + onPressed: canFinish ? controller.finishGeometry : null, + icon: const Icon(Icons.check, size: 18), + label: Text(controller.finishButtonText), + ), + ), + ], + ), + ], + ); + }); + } + + String _requiredPointText(MapEditTool tool) { + switch (tool) { + case MapEditTool.line: + return 'min. 2'; + case MapEditTool.polygon: + return 'min. 3'; + case MapEditTool.point: + return '1 pont'; + case MapEditTool.none: + return ''; + } + } +} diff --git a/lib/widgets/map_edit_tools/map_edit_point_drawing_content.dart b/lib/widgets/map_edit_tools/map_edit_point_drawing_content.dart new file mode 100644 index 0000000..ba5c00c --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_point_drawing_content.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +import 'map_edit_small_status_chip.dart'; + +class PointDrawingContent extends StatelessWidget { + final MapSurveyController controller; + + const PointDrawingContent({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Obx(() { + // final hasPoint = controller.draftPoints.isNotEmpty; + final hasPoint = true; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon( + controller.activeEditToolIcon, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + hasPoint + ? 'Pont kiválasztva' + : controller.activeEditToolTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + if (hasPoint) + SmallStatusChip( + text: '1 pont', + color: colorScheme.primary, + ), + ], + ), + const SizedBox(height: 5), + Align( + alignment: Alignment.centerLeft, + child: Text( + hasPoint + ? 'A pont helye kijelölve. A mentéshez nyomd meg a Kész gombot.' + : controller.activeEditToolHint, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.15, + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: controller.cancelEditing, + icon: const Icon(Icons.close, size: 18), + label: const Text('Mégse'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + //onPressed: controller.addPointFromCurrentPosition, + onPressed: () {}, + icon: const Icon(Icons.my_location, size: 18), + label: const Text('Saját hely'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: FilledButton.icon( + // onPressed: controller.canFinishGeometry + // ? controller.finishGeometry + // : null, + onPressed: () {}, + icon: const Icon(Icons.check, size: 18), + label: const Text('Kész'), + ), + ), + ], + ), + ], + ); + }); + } +} diff --git a/lib/widgets/map_edit_tools/map_edit_small_status_chip.dart b/lib/widgets/map_edit_tools/map_edit_small_status_chip.dart new file mode 100644 index 0000000..bb71f8d --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_small_status_chip.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class SmallStatusChip extends StatelessWidget { + final String text; + final Color color; + + const SmallStatusChip({ + required this.text, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 3, + ), + decoration: BoxDecoration( + color: color.withOpacity(0.10), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: color.withOpacity(0.55), + width: 1, + ), + ), + child: Text( + text, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w800, + height: 1.0, + ), + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/map_edit_square_toolbar_button.dart b/lib/widgets/map_edit_tools/map_edit_square_toolbar_button.dart new file mode 100644 index 0000000..c0bf347 --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_square_toolbar_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class SquareToolbarButton extends StatelessWidget { + final String tooltip; + final IconData icon; + final VoidCallback? onPressed; + + const SquareToolbarButton({ + required this.tooltip, + required this.icon, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: SizedBox( + width: 42, + height: 40, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(42, 40), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: onPressed, + child: Icon( + icon, + size: 20, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/map_edit_toolbar.dart b/lib/widgets/map_edit_tools/map_edit_toolbar.dart new file mode 100644 index 0000000..4b43071 --- /dev/null +++ b/lib/widgets/map_edit_tools/map_edit_toolbar.dart @@ -0,0 +1,89 @@ +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'; + +import 'map_toolbar_action.dart'; +import 'map_toolbar_divider.dart'; + +class MapEditCompactToolbar extends StatelessWidget { + final MapSurveyController controller; + + const MapEditCompactToolbar({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return Obx(() { + final activeTool = controller.activeEditTool.value; + + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(10, 0, 10, 10), + child: Align( + alignment: Alignment.bottomCenter, + child: Material( + elevation: 8, + color: Theme.of(context).colorScheme.surface.withOpacity(0.96), + borderRadius: BorderRadius.circular(22), + clipBehavior: Clip.antiAlias, + child: Container( + constraints: const BoxConstraints( + maxWidth: 520, + ), + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 6, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: + Theme.of(context).colorScheme.outline.withOpacity(0.22), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ToolbarAction( + icon: Icons.add_location_alt_outlined, + label: 'Pont', + selected: activeTool == MapEditTool.point, + onTap: controller.startPointTool, + ), + ToolbarAction( + icon: Icons.polyline_outlined, + label: 'Vonal', + selected: activeTool == MapEditTool.line, + onTap: controller.startLineTool, + ), + ToolbarAction( + icon: Icons.border_outer_outlined, + label: 'Terület', + selected: activeTool == MapEditTool.polygon, + onTap: controller.startPolygonTool, + ), + const ToolbarDivider(), + ToolbarAction( + icon: Icons.list_alt_outlined, + label: 'Lista', + selected: false, + onTap: controller.openFeatureList, + ), + ToolbarAction( + icon: Icons.layers_outlined, + label: 'Rétegek', + selected: false, + onTap: controller.openLayerPanel, + ), + ], + ), + ), + ), + ), + ); + }); + } +} diff --git a/lib/widgets/map_edit_tools/map_feature_save_sheet.dart b/lib/widgets/map_edit_tools/map_feature_save_sheet.dart new file mode 100644 index 0000000..8a4846b --- /dev/null +++ b/lib/widgets/map_edit_tools/map_feature_save_sheet.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:terepi_seged/enums/map_edit_tool.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +import 'color_row.dart'; +import 'label_field.dart'; +import 'opacity_slider.dart'; +import 'save_sheet_actions.dart'; +import 'sheet_handle.dart'; +import 'stroke_slider.dart'; + +class MapFeatureSaveSheet extends StatelessWidget { + final MapSurveyController ctrl; + final ScrollController scrollCtrl; + const MapFeatureSaveSheet({ + required this.ctrl, + required this.scrollCtrl, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 16, + offset: const Offset(0, -4), + ), + ], + ), + child: CustomScrollView( + controller: scrollCtrl, + slivers: [ + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle + SheetHandle(), + + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Stílus', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 16), + ColorRow(ctrl: ctrl), + const SizedBox(height: 18), + OpacitySlider(ctrl: ctrl), + const SizedBox(height: 10), + if (ctrl.activeEditTool.value != MapEditTool.point) ...[ + StrokeSlider(ctrl: ctrl), + const SizedBox(height: 10), + ], + LabelField(ctrl: ctrl), + const SizedBox(height: 24), + SaveSheetActions(ctrl: ctrl), + SizedBox( + height: MediaQuery.of(context).padding.bottom + 12), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/map_toolbar_action.dart b/lib/widgets/map_edit_tools/map_toolbar_action.dart new file mode 100644 index 0000000..859faa5 --- /dev/null +++ b/lib/widgets/map_edit_tools/map_toolbar_action.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class ToolbarAction extends StatelessWidget { + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + const ToolbarAction({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + final foreground = selected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant; + + final background = selected + ? colorScheme.primaryContainer.withOpacity(0.90) + : Colors.transparent; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 1), + child: Tooltip( + message: label, + waitDuration: const Duration(milliseconds: 500), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + curve: Curves.easeOut, + width: 58, + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(16), + border: selected + ? Border.all( + color: colorScheme.primary.withOpacity(0.35), + width: 1, + ) + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 21, + color: foreground, + ), + const SizedBox(height: 2), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: foreground, + fontWeight: + selected ? FontWeight.w800 : FontWeight.w600, + height: 1.0, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/map_edit_tools/map_toolbar_divider.dart b/lib/widgets/map_edit_tools/map_toolbar_divider.dart new file mode 100644 index 0000000..0993b3c --- /dev/null +++ b/lib/widgets/map_edit_tools/map_toolbar_divider.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ToolbarDivider extends StatelessWidget { + const ToolbarDivider(); + + @override + Widget build(BuildContext context) { + return Container( + width: 1, + height: 32, + margin: const EdgeInsets.symmetric(horizontal: 4), + color: Theme.of(context).colorScheme.outline.withOpacity(0.22), + ); + } +} diff --git a/lib/widgets/map_edit_tools/opacity_slider.dart b/lib/widgets/map_edit_tools/opacity_slider.dart new file mode 100644 index 0000000..fe7e4b3 --- /dev/null +++ b/lib/widgets/map_edit_tools/opacity_slider.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +import 'labeled_slider.dart'; + +class OpacitySlider extends StatelessWidget { + final MapSurveyController ctrl; + const OpacitySlider({required this.ctrl}); + + @override + Widget build(BuildContext context) => Obx(() => LabeledSlider( + label: 'Átlátszóság', + value: ctrl.activeEditOpacity.value, + min: 0.1, + max: 1.0, + display: '${(ctrl.activeEditOpacity.value * 100).round()}%', + color: ctrl.activeEditColor.value, + onChanged: (v) => ctrl.activeEditOpacity.value = v, + )); +} diff --git a/lib/widgets/map_edit_tools/save_sheet_actions.dart b/lib/widgets/map_edit_tools/save_sheet_actions.dart new file mode 100644 index 0000000..d251a3b --- /dev/null +++ b/lib/widgets/map_edit_tools/save_sheet_actions.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +class SaveSheetActions extends StatelessWidget { + final MapSurveyController ctrl; + const SaveSheetActions({required this.ctrl}); + + @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), + + // 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(); + }, + )), + ), + ]); + } +} diff --git a/lib/widgets/map_edit_tools/sheet_handle.dart b/lib/widgets/map_edit_tools/sheet_handle.dart new file mode 100644 index 0000000..301b573 --- /dev/null +++ b/lib/widgets/map_edit_tools/sheet_handle.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class SheetHandle extends StatelessWidget { + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.35), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ); +} diff --git a/lib/widgets/map_edit_tools/stroke_slider.dart b/lib/widgets/map_edit_tools/stroke_slider.dart new file mode 100644 index 0000000..6ed3c39 --- /dev/null +++ b/lib/widgets/map_edit_tools/stroke_slider.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; + +import 'labeled_slider.dart'; + +class StrokeSlider extends StatelessWidget { + final MapSurveyController ctrl; + const StrokeSlider({required this.ctrl}); + + @override + Widget build(BuildContext context) => Obx(() => LabeledSlider( + label: 'Körvonal', + value: ctrl.activeEditStrokeWidth.value, + min: 0.5, + max: 10.0, + divisions: 19, + display: '${ctrl.activeEditStrokeWidth.value.toStringAsFixed(1)} px', + color: ctrl.activeEditColor.value, + onChanged: (v) => ctrl.activeEditStrokeWidth.value = v, + )); +}