feat: Add drawing functionality with line and rectangle support, including toolbar and layer integration

This commit is contained in:
Janez T
2025-10-15 22:04:23 +02:00
parent aab79a56ea
commit 3651f53ef2
7 changed files with 945 additions and 3 deletions

View File

@@ -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": []

View File

@@ -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<MeshCoreSarApp> {
},
),
ChangeNotifierProvider(create: (_) => MapProvider()),
ChangeNotifierProvider(
create: (_) {
final provider = DrawingProvider();
// Initialize drawing provider asynchronously
provider.initialize();
return provider;
},
),
// Tile cache service
Provider(create: (_) => TileCacheService()),

180
lib/models/map_drawing.dart Normal file
View File

@@ -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<Color> 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<String, dynamic> toJson();
/// Create from JSON
static MapDrawing? fromJson(Map<String, dynamic> 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<LatLng> points;
LineDrawing({
required super.id,
required super.color,
required super.createdAt,
required this.points,
}) : super(type: DrawingShapeType.line);
@override
Map<String, dynamic> 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<String, dynamic> json) {
final pointsJson = json['points'] as List<dynamic>;
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<LatLng>? 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<LatLng> get corners => [
topLeft,
LatLng(topLeft.latitude, bottomRight.longitude), // top right
bottomRight,
LatLng(bottomRight.latitude, topLeft.longitude), // bottom left
topLeft, // close the rectangle
];
@override
Map<String, dynamic> 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<String, dynamic> json) {
final topLeftJson = json['topLeft'] as Map<String, dynamic>;
final bottomRightJson = json['bottomRight'] as Map<String, dynamic>;
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,
);
}
}

View File

@@ -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<MapDrawing> _drawings = [];
// In-progress drawing
MapDrawing? _currentDrawing;
List<LatLng> _currentLinePoints = [];
LatLng? _rectangleStartPoint;
// Getters
DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor;
List<MapDrawing> get drawings => List.unmodifiable(_drawings);
MapDrawing? get currentDrawing => _currentDrawing;
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
bool get isDrawing => _drawingMode != DrawingMode.none;
/// Initialize and load saved drawings
Future<void> 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<void> _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<void> _loadDrawings() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_storageKey);
if (jsonString == null) return;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
_drawings.clear();
for (final json in jsonList) {
final drawing = MapDrawing.fromJson(json as Map<String, dynamic>);
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;
}
}

View File

@@ -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<MapTab> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
return Consumer2<ContactsProvider, MessagesProvider>(
builder: (context, contactsProvider, messagesProvider, child) {
return Consumer3<ContactsProvider, MessagesProvider, DrawingProvider>(
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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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<MapTab> 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,

View File

@@ -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<MapDrawing> drawings;
final MapDrawing? previewDrawing;
const DrawingLayer({
super.key,
required this.drawings,
this.previewDrawing,
});
@override
Widget build(BuildContext context) {
final List<Polyline> 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<LatLng> _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<MapDrawing> drawings;
final Function(String drawingId)? onDeleteDrawing;
const DrawingMarkersLayer({
super.key,
required this.drawings,
this.onDeleteDrawing,
});
@override
Widget build(BuildContext context) {
final List<Marker> 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'),
),
],
),
);
}
}

View File

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