From 3651f53ef25087a5f9fb29bc38bcc50ae7a535f6 Mon Sep 17 00:00:00 2001 From: Janez T Date: Wed, 15 Oct 2025 22:04:23 +0200 Subject: [PATCH] feat: Add drawing functionality with line and rectangle support, including toolbar and layer integration --- .claude/settings.local.json | 3 +- lib/main.dart | 9 + lib/models/map_drawing.dart | 180 +++++++++++++++++ lib/providers/drawing_provider.dart | 259 ++++++++++++++++++++++++ lib/screens/map_tab.dart | 56 +++++- lib/widgets/map/drawing_layer.dart | 160 +++++++++++++++ lib/widgets/map/drawing_toolbar.dart | 281 +++++++++++++++++++++++++++ 7 files changed, 945 insertions(+), 3 deletions(-) create mode 100644 lib/models/map_drawing.dart create mode 100644 lib/providers/drawing_provider.dart create mode 100644 lib/widgets/map/drawing_layer.dart create mode 100644 lib/widgets/map/drawing_toolbar.dart diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e97d383..81634c2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,8 @@ "Bash(find:*)", "Read(//Users/dz0ny/meshcore-sar/**)", "Bash(git grep:*)", - "Bash(dart analyze:*)" + "Bash(dart analyze:*)", + "Bash(flutter logs --clear)" ], "deny": [], "ask": [] diff --git a/lib/main.dart b/lib/main.dart index 1d4a01e..8d694c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'providers/connection_provider.dart'; import 'providers/contacts_provider.dart'; import 'providers/messages_provider.dart'; import 'providers/map_provider.dart'; +import 'providers/drawing_provider.dart'; import 'providers/app_provider.dart'; import 'services/tile_cache_service.dart'; import 'screens/home_screen.dart'; @@ -84,6 +85,14 @@ class _MeshCoreSarAppState extends State { }, ), ChangeNotifierProvider(create: (_) => MapProvider()), + ChangeNotifierProvider( + create: (_) { + final provider = DrawingProvider(); + // Initialize drawing provider asynchronously + provider.initialize(); + return provider; + }, + ), // Tile cache service Provider(create: (_) => TileCacheService()), diff --git a/lib/models/map_drawing.dart b/lib/models/map_drawing.dart new file mode 100644 index 0000000..211b1ba --- /dev/null +++ b/lib/models/map_drawing.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + +/// Drawing shape type +enum DrawingShapeType { + line, + rectangle, +} + +/// Drawing colors available for user selection +class DrawingColors { + static const List palette = [ + Colors.red, + Colors.blue, + Colors.green, + Colors.yellow, + Colors.orange, + Colors.purple, + Colors.pink, + Colors.cyan, + ]; + + static String colorToName(Color color) { + if (color == Colors.red) return 'Red'; + if (color == Colors.blue) return 'Blue'; + if (color == Colors.green) return 'Green'; + if (color == Colors.yellow) return 'Yellow'; + if (color == Colors.orange) return 'Orange'; + if (color == Colors.purple) return 'Purple'; + if (color == Colors.pink) return 'Pink'; + if (color == Colors.cyan) return 'Cyan'; + return 'Unknown'; + } +} + +/// Base class for map drawings +abstract class MapDrawing { + final String id; + final DrawingShapeType type; + final Color color; + final DateTime createdAt; + + MapDrawing({ + required this.id, + required this.type, + required this.color, + required this.createdAt, + }); + + /// Convert to JSON for persistence + Map toJson(); + + /// Create from JSON + static MapDrawing? fromJson(Map json) { + final typeStr = json['type'] as String?; + if (typeStr == null) return null; + + try { + final type = DrawingShapeType.values.firstWhere( + (e) => e.toString() == 'DrawingShapeType.$typeStr', + ); + + switch (type) { + case DrawingShapeType.line: + return LineDrawing.fromJson(json); + case DrawingShapeType.rectangle: + return RectangleDrawing.fromJson(json); + } + } catch (e) { + return null; + } + } +} + +/// Line drawing on map +class LineDrawing extends MapDrawing { + final List points; + + LineDrawing({ + required super.id, + required super.color, + required super.createdAt, + required this.points, + }) : super(type: DrawingShapeType.line); + + @override + Map toJson() { + return { + 'id': id, + 'type': type.name, + 'color': color.toARGB32(), + 'createdAt': createdAt.toIso8601String(), + 'points': points.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(), + }; + } + + static LineDrawing fromJson(Map json) { + final pointsJson = json['points'] as List; + final points = pointsJson.map((p) => LatLng(p['lat'] as double, p['lon'] as double)).toList(); + + return LineDrawing( + id: json['id'] as String, + color: Color.fromARGB32(json['color'] as int), + createdAt: DateTime.parse(json['createdAt'] as String), + points: points, + ); + } + + /// Create a copy with updated points + LineDrawing copyWith({List? points}) { + return LineDrawing( + id: id, + color: color, + createdAt: createdAt, + points: points ?? this.points, + ); + } +} + +/// Rectangle drawing on map +class RectangleDrawing extends MapDrawing { + final LatLng topLeft; + final LatLng bottomRight; + + RectangleDrawing({ + required super.id, + required super.color, + required super.createdAt, + required this.topLeft, + required this.bottomRight, + }) : super(type: DrawingShapeType.rectangle); + + /// Get all corner points for rendering + List get corners => [ + topLeft, + LatLng(topLeft.latitude, bottomRight.longitude), // top right + bottomRight, + LatLng(bottomRight.latitude, topLeft.longitude), // bottom left + topLeft, // close the rectangle + ]; + + @override + Map toJson() { + return { + 'id': id, + 'type': type.name, + 'color': color.toARGB32(), + 'createdAt': createdAt.toIso8601String(), + 'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude}, + 'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude}, + }; + } + + static RectangleDrawing fromJson(Map json) { + final topLeftJson = json['topLeft'] as Map; + final bottomRightJson = json['bottomRight'] as Map; + + return RectangleDrawing( + id: json['id'] as String, + color: Color.fromARGB32(json['color'] as int), + createdAt: DateTime.parse(json['createdAt'] as String), + topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double), + bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double), + ); + } + + /// Create a copy with updated corners + RectangleDrawing copyWith({ + LatLng? topLeft, + LatLng? bottomRight, + }) { + return RectangleDrawing( + id: id, + color: color, + createdAt: createdAt, + topLeft: topLeft ?? this.topLeft, + bottomRight: bottomRight ?? this.bottomRight, + ); + } +} diff --git a/lib/providers/drawing_provider.dart b/lib/providers/drawing_provider.dart new file mode 100644 index 0000000..897c6b8 --- /dev/null +++ b/lib/providers/drawing_provider.dart @@ -0,0 +1,259 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/map_drawing.dart'; + +/// Drawing mode state +enum DrawingMode { + none, + line, + rectangle, +} + +/// Provider for managing map drawings +class DrawingProvider with ChangeNotifier { + static const String _storageKey = 'map_drawings'; + + // Drawing state + DrawingMode _drawingMode = DrawingMode.none; + Color _selectedColor = DrawingColors.palette[0]; + + // Completed drawings + final List _drawings = []; + + // In-progress drawing + MapDrawing? _currentDrawing; + List _currentLinePoints = []; + LatLng? _rectangleStartPoint; + + // Getters + DrawingMode get drawingMode => _drawingMode; + Color get selectedColor => _selectedColor; + List get drawings => List.unmodifiable(_drawings); + MapDrawing? get currentDrawing => _currentDrawing; + List get currentLinePoints => List.unmodifiable(_currentLinePoints); + LatLng? get rectangleStartPoint => _rectangleStartPoint; + bool get isDrawing => _drawingMode != DrawingMode.none; + + /// Initialize and load saved drawings + Future initialize() async { + await _loadDrawings(); + } + + /// Set drawing mode + void setDrawingMode(DrawingMode mode) { + if (_drawingMode != mode) { + // Cancel any in-progress drawing when switching modes + _cancelCurrentDrawing(); + _drawingMode = mode; + notifyListeners(); + } + } + + /// Set selected color + void setColor(Color color) { + _selectedColor = color; + notifyListeners(); + } + + /// Start drawing a line + void startLine(LatLng point) { + if (_drawingMode != DrawingMode.line) return; + + _currentLinePoints = [point]; + notifyListeners(); + } + + /// Add point to current line + void addLinePoint(LatLng point) { + if (_drawingMode != DrawingMode.line || _currentLinePoints.isEmpty) return; + + _currentLinePoints.add(point); + notifyListeners(); + } + + /// Complete current line drawing + void completeLine() { + if (_drawingMode != DrawingMode.line || _currentLinePoints.length < 2) { + _cancelCurrentDrawing(); + return; + } + + final drawing = LineDrawing( + id: DateTime.now().millisecondsSinceEpoch.toString(), + color: _selectedColor, + createdAt: DateTime.now(), + points: List.from(_currentLinePoints), + ); + + _drawings.add(drawing); + _currentLinePoints = []; + _saveDrawings(); + notifyListeners(); + } + + /// Start drawing a rectangle + void startRectangle(LatLng point) { + if (_drawingMode != DrawingMode.rectangle) return; + + _rectangleStartPoint = point; + notifyListeners(); + } + + /// Update rectangle end point (for preview) + void updateRectangleEndPoint(LatLng endPoint) { + if (_drawingMode != DrawingMode.rectangle || _rectangleStartPoint == null) return; + + // Create preview rectangle + _currentDrawing = RectangleDrawing( + id: 'preview', + color: _selectedColor, + createdAt: DateTime.now(), + topLeft: LatLng( + _rectangleStartPoint!.latitude > endPoint.latitude + ? endPoint.latitude + : _rectangleStartPoint!.latitude, + _rectangleStartPoint!.longitude < endPoint.longitude + ? _rectangleStartPoint!.longitude + : endPoint.longitude, + ), + bottomRight: LatLng( + _rectangleStartPoint!.latitude < endPoint.latitude + ? endPoint.latitude + : _rectangleStartPoint!.latitude, + _rectangleStartPoint!.longitude > endPoint.longitude + ? _rectangleStartPoint!.longitude + : endPoint.longitude, + ), + ); + notifyListeners(); + } + + /// Complete current rectangle drawing + void completeRectangle(LatLng endPoint) { + if (_drawingMode != DrawingMode.rectangle || _rectangleStartPoint == null) { + _cancelCurrentDrawing(); + return; + } + + // Calculate top-left and bottom-right corners + final topLeft = LatLng( + _rectangleStartPoint!.latitude > endPoint.latitude + ? endPoint.latitude + : _rectangleStartPoint!.latitude, + _rectangleStartPoint!.longitude < endPoint.longitude + ? _rectangleStartPoint!.longitude + : endPoint.longitude, + ); + + final bottomRight = LatLng( + _rectangleStartPoint!.latitude < endPoint.latitude + ? endPoint.latitude + : _rectangleStartPoint!.latitude, + _rectangleStartPoint!.longitude > endPoint.longitude + ? _rectangleStartPoint!.longitude + : endPoint.longitude, + ); + + final drawing = RectangleDrawing( + id: DateTime.now().millisecondsSinceEpoch.toString(), + color: _selectedColor, + createdAt: DateTime.now(), + topLeft: topLeft, + bottomRight: bottomRight, + ); + + _drawings.add(drawing); + _rectangleStartPoint = null; + _currentDrawing = null; + _saveDrawings(); + notifyListeners(); + } + + /// Cancel current drawing in progress + void _cancelCurrentDrawing() { + _currentLinePoints = []; + _rectangleStartPoint = null; + _currentDrawing = null; + } + + /// Clear current drawing (public method) + void cancelCurrentDrawing() { + _cancelCurrentDrawing(); + notifyListeners(); + } + + /// Remove a specific drawing + void removeDrawing(String id) { + _drawings.removeWhere((d) => d.id == id); + _saveDrawings(); + notifyListeners(); + } + + /// Clear all drawings + void clearAllDrawings() { + _drawings.clear(); + _cancelCurrentDrawing(); + _saveDrawings(); + notifyListeners(); + } + + /// Exit drawing mode + void exitDrawingMode() { + _cancelCurrentDrawing(); + _drawingMode = DrawingMode.none; + notifyListeners(); + } + + /// Save drawings to persistent storage + Future _saveDrawings() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonList = _drawings.map((d) => d.toJson()).toList(); + final jsonString = jsonEncode(jsonList); + await prefs.setString(_storageKey, jsonString); + } catch (e) { + debugPrint('Error saving drawings: $e'); + } + } + + /// Load drawings from persistent storage + Future _loadDrawings() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonString = prefs.getString(_storageKey); + if (jsonString == null) return; + + final jsonList = jsonDecode(jsonString) as List; + _drawings.clear(); + + for (final json in jsonList) { + final drawing = MapDrawing.fromJson(json as Map); + if (drawing != null) { + _drawings.add(drawing); + } + } + + notifyListeners(); + } catch (e) { + debugPrint('Error loading drawings: $e'); + } + } + + /// Get the current preview drawing for rendering + MapDrawing? getPreviewDrawing() { + if (_drawingMode == DrawingMode.line && _currentLinePoints.length >= 2) { + return LineDrawing( + id: 'preview', + color: _selectedColor, + createdAt: DateTime.now(), + points: _currentLinePoints, + ); + } else if (_drawingMode == DrawingMode.rectangle && _currentDrawing != null) { + return _currentDrawing; + } + return null; + } +} diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index e2ed316..2a849b2 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../providers/contacts_provider.dart'; import '../providers/messages_provider.dart'; import '../providers/map_provider.dart'; +import '../providers/drawing_provider.dart'; import '../providers/app_provider.dart'; import '../providers/connection_provider.dart'; import '../models/contact.dart'; @@ -24,6 +25,8 @@ import '../widgets/map_debug_info.dart'; import '../widgets/map/map_legend.dart'; import '../widgets/map/compass_widget.dart'; import '../widgets/map/detailed_compass_dialog.dart'; +import '../widgets/map/drawing_layer.dart'; +import '../widgets/map/drawing_toolbar.dart'; import '../widgets/messages/sar_update_sheet.dart'; import 'map_management_screen.dart'; @@ -781,8 +784,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); // Required for AutomaticKeepAliveClientMixin - return Consumer2( - builder: (context, contactsProvider, messagesProvider, child) { + return Consumer3( + builder: (context, contactsProvider, messagesProvider, drawingProvider, child) { final contactsWithLocation = contactsProvider.contactsWithLocation; final sarMarkers = messagesProvider.sarMarkers; final center = _calculateCenter(contactsWithLocation, sarMarkers); @@ -821,6 +824,9 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { } }, onLongPress: (tapPosition, point) { + // Skip if in drawing mode + if (drawingProvider.isDrawing) return; + // Drop a pin at long press location (if no pin exists) if (_droppedPinLocation == null) { setState(() { @@ -846,6 +852,13 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { } }, onPointerHover: (event, point) { + // Update rectangle preview while dragging + if (drawingProvider.drawingMode == DrawingMode.rectangle && + drawingProvider.rectangleStartPoint != null) { + drawingProvider.updateRectangleEndPoint(point); + return; + } + // Update pin location while dragging if (_isDraggingPin) { setState(() { @@ -862,6 +875,27 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { } }, onTap: (tapPosition, point) { + // Handle drawing mode taps + if (drawingProvider.drawingMode == DrawingMode.line) { + if (drawingProvider.currentLinePoints.isEmpty) { + // Start new line + drawingProvider.startLine(point); + } else { + // Add point to current line + drawingProvider.addLinePoint(point); + } + return; + } else if (drawingProvider.drawingMode == DrawingMode.rectangle) { + if (drawingProvider.rectangleStartPoint == null) { + // Start rectangle + drawingProvider.startRectangle(point); + } else { + // Complete rectangle + drawingProvider.completeRectangle(point); + } + return; + } + // Clear dropped pin if tapping elsewhere (not on the pin itself) if (_droppedPinLocation != null && !_isDraggingPin) { // Check if tap is far from the pin @@ -909,6 +943,11 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ); }, ), + // Drawing layer (rendered after paths, before markers) + DrawingLayer( + drawings: drawingProvider.drawings, + previewDrawing: drawingProvider.getPreviewDrawing(), + ), MarkerLayer( markers: [ // Contact markers @@ -1021,6 +1060,13 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ), ], ), + // Drawing markers layer (delete buttons on drawings) + DrawingMarkersLayer( + drawings: drawingProvider.drawings, + onDeleteDrawing: (drawingId) { + drawingProvider.removeDrawing(drawingId); + }, + ), ], ), ) @@ -1066,6 +1112,12 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { objectCount: messagesProvider.objectMarkers.length, ), ), + // Drawing toolbar - bottom left + Positioned( + bottom: 16, + left: 16, + child: const DrawingToolbar(), + ), // Map controls - right side Positioned( bottom: 16, diff --git a/lib/widgets/map/drawing_layer.dart b/lib/widgets/map/drawing_layer.dart new file mode 100644 index 0000000..3bdccf5 --- /dev/null +++ b/lib/widgets/map/drawing_layer.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import '../../models/map_drawing.dart'; + +/// Widget that renders map drawings as polylines +class DrawingLayer extends StatelessWidget { + final List drawings; + final MapDrawing? previewDrawing; + + const DrawingLayer({ + super.key, + required this.drawings, + this.previewDrawing, + }); + + @override + Widget build(BuildContext context) { + final List polylines = []; + + // Add completed drawings + for (final drawing in drawings) { + polylines.add(_createPolyline(drawing, isPreview: false)); + } + + // Add preview drawing (if any) + if (previewDrawing != null) { + polylines.add(_createPolyline(previewDrawing!, isPreview: true)); + } + + return PolylineLayer(polylines: polylines); + } + + /// Create a polyline from a drawing + Polyline _createPolyline(MapDrawing drawing, {required bool isPreview}) { + final points = _getPoints(drawing); + final opacity = isPreview ? 0.6 : 1.0; + + return Polyline( + points: points, + color: drawing.color.withOpacity(opacity), + strokeWidth: 4.0, + borderColor: Colors.white.withOpacity(opacity * 0.8), + borderStrokeWidth: 1.0, + ); + } + + /// Get points from a drawing based on its type + List _getPoints(MapDrawing drawing) { + if (drawing is LineDrawing) { + return drawing.points; + } else if (drawing is RectangleDrawing) { + return drawing.corners; + } + return []; + } +} + +/// Widget that shows drawing markers (start/end points) +class DrawingMarkersLayer extends StatelessWidget { + final List drawings; + final Function(String drawingId)? onDeleteDrawing; + + const DrawingMarkersLayer({ + super.key, + required this.drawings, + this.onDeleteDrawing, + }); + + @override + Widget build(BuildContext context) { + final List markers = []; + + // Add delete markers for each drawing (at the center point) + for (final drawing in drawings) { + final centerPoint = _getCenterPoint(drawing); + if (centerPoint != null) { + markers.add( + Marker( + point: centerPoint, + width: 40, + height: 40, + child: GestureDetector( + onTap: () { + if (onDeleteDrawing != null) { + _showDeleteDialog(context, drawing); + } + }, + child: Container( + decoration: BoxDecoration( + color: drawing.color.withOpacity(0.9), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 20, + ), + ), + ), + ), + ); + } + } + + return MarkerLayer(markers: markers); + } + + /// Get the center point of a drawing + LatLng? _getCenterPoint(MapDrawing drawing) { + if (drawing is LineDrawing && drawing.points.isNotEmpty) { + // Use the middle point of the line + final midIndex = drawing.points.length ~/ 2; + return drawing.points[midIndex]; + } else if (drawing is RectangleDrawing) { + // Use the center of the rectangle + return LatLng( + (drawing.topLeft.latitude + drawing.bottomRight.latitude) / 2, + (drawing.topLeft.longitude + drawing.bottomRight.longitude) / 2, + ); + } + return null; + } + + /// Show delete confirmation dialog + void _showDeleteDialog(BuildContext context, MapDrawing drawing) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Drawing'), + content: Text( + 'Delete this ${drawing.type.name}?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + onDeleteDrawing?.call(drawing.id); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Delete'), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/map/drawing_toolbar.dart b/lib/widgets/map/drawing_toolbar.dart new file mode 100644 index 0000000..89effe2 --- /dev/null +++ b/lib/widgets/map/drawing_toolbar.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/drawing_provider.dart'; +import '../../models/map_drawing.dart'; + +/// Toolbar for drawing controls on the map +class DrawingToolbar extends StatelessWidget { + const DrawingToolbar({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, drawingProvider, _) { + if (!drawingProvider.isDrawing) { + // Show compact floating button when not in drawing mode + return FloatingActionButton.small( + heroTag: 'drawing_tool', + onPressed: () => _showDrawingMenu(context, drawingProvider), + child: const Icon(Icons.edit), + ); + } + + // Show full toolbar when in drawing mode + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Title bar with close button + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getIconForMode(drawingProvider.drawingMode), + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + _getTitleForMode(drawingProvider.drawingMode), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => drawingProvider.exitDrawingMode(), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 12), + // Color picker + Wrap( + spacing: 8, + children: DrawingColors.palette.map((color) { + final isSelected = drawingProvider.selectedColor == color; + return GestureDetector( + onTap: () => drawingProvider.setColor(color), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.white : Colors.grey.shade300, + width: isSelected ? 3 : 2, + ), + boxShadow: [ + if (isSelected) + BoxShadow( + color: color.withOpacity(0.5), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: isSelected + ? const Icon( + Icons.check, + color: Colors.white, + size: 16, + ) + : null, + ), + ); + }).toList(), + ), + const SizedBox(height: 12), + // Action buttons + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Cancel current drawing + if (drawingProvider.currentLinePoints.isNotEmpty || + drawingProvider.rectangleStartPoint != null) + IconButton( + icon: const Icon(Icons.undo), + onPressed: () => drawingProvider.cancelCurrentDrawing(), + tooltip: 'Cancel', + ), + // Complete line drawing + if (drawingProvider.drawingMode == DrawingMode.line && + drawingProvider.currentLinePoints.length >= 2) + IconButton( + icon: const Icon(Icons.check), + onPressed: () => drawingProvider.completeLine(), + tooltip: 'Complete Line', + color: Colors.green, + ), + // Clear all drawings + if (drawingProvider.drawings.isNotEmpty) + IconButton( + icon: const Icon(Icons.delete_sweep), + onPressed: () => _showClearAllDialog(context, drawingProvider), + tooltip: 'Clear All', + color: Colors.red, + ), + ], + ), + // Instructions + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _getInstructions(drawingProvider.drawingMode), + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + }, + ); + } + + /// Show drawing mode selection menu + void _showDrawingMenu(BuildContext context, DrawingProvider drawingProvider) { + showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 12), + Text( + 'Drawing Tools', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.show_chart), + title: const Text('Draw Line'), + subtitle: const Text('Draw a freehand line on the map'), + onTap: () { + Navigator.pop(context); + drawingProvider.setDrawingMode(DrawingMode.line); + }, + ), + ListTile( + leading: const Icon(Icons.crop_square), + title: const Text('Draw Rectangle'), + subtitle: const Text('Draw a rectangular area on the map'), + onTap: () { + Navigator.pop(context); + drawingProvider.setDrawingMode(DrawingMode.rectangle); + }, + ), + if (drawingProvider.drawings.isNotEmpty) ...[ + const Divider(), + ListTile( + leading: const Icon(Icons.delete_sweep, color: Colors.red), + title: const Text('Clear All Drawings'), + subtitle: Text('Remove all ${drawingProvider.drawings.length} drawings'), + onTap: () { + Navigator.pop(context); + _showClearAllDialog(context, drawingProvider); + }, + ), + ], + ], + ), + ), + ); + } + + /// Show clear all confirmation dialog + void _showClearAllDialog(BuildContext context, DrawingProvider drawingProvider) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Clear All Drawings'), + content: Text( + 'Delete all ${drawingProvider.drawings.length} drawings from the map?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + drawingProvider.clearAllDrawings(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Clear All'), + ), + ], + ), + ); + } + + /// Get icon for drawing mode + IconData _getIconForMode(DrawingMode mode) { + switch (mode) { + case DrawingMode.line: + return Icons.show_chart; + case DrawingMode.rectangle: + return Icons.crop_square; + case DrawingMode.none: + return Icons.edit; + } + } + + /// Get title for drawing mode + String _getTitleForMode(DrawingMode mode) { + switch (mode) { + case DrawingMode.line: + return 'Draw Line'; + case DrawingMode.rectangle: + return 'Draw Rectangle'; + case DrawingMode.none: + return 'Drawing'; + } + } + + /// Get instructions for drawing mode + String _getInstructions(DrawingMode mode) { + switch (mode) { + case DrawingMode.line: + return 'Tap map to add points\nTap ✓ to finish'; + case DrawingMode.rectangle: + return 'Tap start point, then end point'; + case DrawingMode.none: + return ''; + } + } +}