Terepbejárás oldalon vonal és terület tulajdonságainak szerkesztése, mentés

This commit is contained in:
torok.istvan 2026-06-16 14:12:44 +02:00
parent 537897005c
commit 1276ac0610
19 changed files with 1206 additions and 17 deletions

View File

@ -0,0 +1,6 @@
enum MapEditTool {
none,
point,
line,
polygon,
}

View File

@ -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 = <Marker>[].obs;
final polylineNotes = <Polyline<Object>>[].obs;
final polygonNotes = <Polygon<Object>>[].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<LatLng>.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<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;
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;
}
}

View File

@ -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<MapSurveyController> {
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<MapSurveyController> {
} 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<MapSurveyController> {
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,

View File

@ -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(),
)),
],
);
}
}

View File

@ -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<LabelField> createState() => LabelFieldState();
}
class LabelFieldState extends State<LabelField> {
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),
),
),
],
);
}
}

View File

@ -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<double> 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()])),
),
]);
}

View File

@ -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),
),
),
),
);
});
}
}

View File

@ -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 '';
}
}
}

View File

@ -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'),
),
),
],
),
],
);
});
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -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,
),
),
),
);
}
}

View File

@ -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,
),
],
),
),
),
),
);
});
}
}

View File

@ -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),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -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,
),
),
],
),
),
),
),
);
}
}

View File

@ -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),
);
}
}

View File

@ -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,
));
}

View File

@ -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();
},
)),
),
]);
}
}

View File

@ -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),
),
),
),
);
}

View File

@ -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,
));
}