mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 08:50:30 +00:00
feat: Enhance map functionality with download area selection and user location pointer
- Added download area selection feature in MapProvider with methods to enter, exit, and update bounds. - Implemented DownloadAreaOverlay widget for user interface during download area selection. - Integrated download area preview in MapManagementScreen with visual feedback. - Introduced LocationPointer widget to indicate user location and heading on the map. - Updated localization strings in app_sl.arb for improved clarity and accuracy. - Refactored map marker generation to utilize the new LocationPointer for user location representation. - Improved error handling in LocationTrackingService for initial position setting.
This commit is contained in:
@@ -887,7 +887,44 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert zoom level to user-friendly description
|
||||
String _getZoomDescription(int zoom) {
|
||||
if (zoom <= 5) {
|
||||
return 'Continental view (very low detail)';
|
||||
} else if (zoom <= 8) {
|
||||
return 'Country view (low detail)';
|
||||
} else if (zoom <= 10) {
|
||||
return 'Regional view (basic detail)';
|
||||
} else if (zoom <= 12) {
|
||||
return 'City view (moderate detail)';
|
||||
} else if (zoom <= 15) {
|
||||
return 'Neighborhood view (good detail)';
|
||||
} else if (zoom <= 17) {
|
||||
return 'Street view (high detail)';
|
||||
} else {
|
||||
return 'Building view (very high detail)';
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDownloadCard() {
|
||||
// Calculate current bounds for preview
|
||||
LatLngBounds? previewBounds;
|
||||
try {
|
||||
final north = double.tryParse(_northController.text);
|
||||
final south = double.tryParse(_southController.text);
|
||||
final east = double.tryParse(_eastController.text);
|
||||
final west = double.tryParse(_westController.text);
|
||||
|
||||
if (north != null && south != null && east != null && west != null) {
|
||||
previewBounds = LatLngBounds(
|
||||
LatLng(south, west),
|
||||
LatLng(north, east),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid bounds, preview will be null
|
||||
}
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -900,6 +937,80 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Preview map (if bounds are valid)
|
||||
if (previewBounds != null) ...[
|
||||
Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: previewBounds.center,
|
||||
initialZoom: 12.0,
|
||||
minZoom: 1,
|
||||
maxZoom: 19,
|
||||
interactionOptions: const InteractionOptions(
|
||||
flags: InteractiveFlag.none, // Static preview
|
||||
),
|
||||
onMapReady: () {
|
||||
// Fit bounds after map is ready would require MapController
|
||||
// For now, center on bounds center
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: _selectedLayer.urlTemplate,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
),
|
||||
// Blue rectangle showing download area
|
||||
PolygonLayer(
|
||||
polygons: [
|
||||
Polygon(
|
||||
points: [
|
||||
LatLng(previewBounds.north, previewBounds.west),
|
||||
LatLng(previewBounds.north, previewBounds.east),
|
||||
LatLng(previewBounds.south, previewBounds.east),
|
||||
LatLng(previewBounds.south, previewBounds.west),
|
||||
],
|
||||
color: Colors.blue.withValues(alpha: 0.2),
|
||||
borderColor: Colors.blue,
|
||||
borderStrokeWidth: 3.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Label overlay
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Download Area Preview',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Map Layer Selection
|
||||
DropdownButtonFormField<MapLayer>(
|
||||
initialValue: _selectedLayer,
|
||||
@@ -1010,13 +1121,22 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.minZoom(_minZoom)),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.minZoom(_minZoom),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
_getZoomDescription(_minZoom),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: _minZoom.toDouble(),
|
||||
min: 1,
|
||||
max: 19,
|
||||
divisions: 18,
|
||||
label: '$_minZoom',
|
||||
label: '$_minZoom - ${_getZoomDescription(_minZoom)}',
|
||||
onChanged: _isDownloading
|
||||
? null
|
||||
: (value) {
|
||||
@@ -1036,13 +1156,22 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.maxZoom(_maxZoom)),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.maxZoom(_maxZoom),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
_getZoomDescription(_maxZoom),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: _maxZoom.toDouble(),
|
||||
min: 1,
|
||||
max: 19,
|
||||
divisions: 18,
|
||||
label: '$_maxZoom',
|
||||
label: '$_maxZoom - ${_getZoomDescription(_maxZoom)}',
|
||||
onChanged: _isDownloading
|
||||
? null
|
||||
: (value) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import '../widgets/map/drawing_toolbar.dart';
|
||||
import '../widgets/map/location_trail_layer.dart';
|
||||
import '../widgets/map/trail_controls.dart';
|
||||
import '../widgets/map/map_message_overlay.dart';
|
||||
import '../widgets/map/download_area_overlay.dart';
|
||||
import '../widgets/messages/sar_update_sheet.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'map_management_screen.dart';
|
||||
@@ -708,21 +709,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
try {
|
||||
// Get current map bounds
|
||||
final bounds = _mapController.camera.visibleBounds;
|
||||
final currentZoom = _mapController.camera.zoom.round();
|
||||
|
||||
// Navigate to Map Management screen with pre-populated data
|
||||
final appProvider = context.read<AppProvider>();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapManagementScreen(
|
||||
tileCacheService: appProvider.tileCacheService,
|
||||
initialLayer: _currentLayer,
|
||||
initialBounds: bounds,
|
||||
initialZoom: currentZoom,
|
||||
),
|
||||
),
|
||||
);
|
||||
// Enter download area selection mode (show preview overlay)
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
mapProvider.enterDownloadAreaMode(bounds);
|
||||
} catch (e) {
|
||||
debugPrint('Error accessing map camera: $e');
|
||||
}
|
||||
@@ -1550,13 +1540,15 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
widget.onNavigateToMessages?.call();
|
||||
},
|
||||
),
|
||||
// User location marker
|
||||
// User location marker with directional pointer
|
||||
if (_markerService.generateUserLocationMarker(
|
||||
position: _locationService.currentPosition,
|
||||
heading: _currentHeading,
|
||||
context: context,
|
||||
) != null)
|
||||
_markerService.generateUserLocationMarker(
|
||||
position: _locationService.currentPosition,
|
||||
heading: _currentHeading,
|
||||
context: context,
|
||||
)!,
|
||||
// Measurement point 1 marker
|
||||
@@ -1745,6 +1737,36 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
},
|
||||
),
|
||||
// Download area selection polygon (rendered on top when in selection mode)
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, _) {
|
||||
if (!mapProvider.isSelectingDownloadArea || mapProvider.downloadAreaBounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final bounds = mapProvider.downloadAreaBounds!;
|
||||
|
||||
// Add padding to the bounds so the rectangle is visible within the screen
|
||||
// Calculate 5% padding on each side
|
||||
final latPadding = (bounds.north - bounds.south) * 0.05;
|
||||
final lonPadding = (bounds.east - bounds.west) * 0.05;
|
||||
|
||||
return PolygonLayer(
|
||||
polygons: [
|
||||
Polygon(
|
||||
points: [
|
||||
LatLng(bounds.north - latPadding, bounds.west + lonPadding), // Top-left
|
||||
LatLng(bounds.north - latPadding, bounds.east - lonPadding), // Top-right
|
||||
LatLng(bounds.south + latPadding, bounds.east - lonPadding), // Bottom-right
|
||||
LatLng(bounds.south + latPadding, bounds.west + lonPadding), // Bottom-left
|
||||
],
|
||||
color: Colors.blue.withValues(alpha: 0.2),
|
||||
borderColor: Colors.blue,
|
||||
borderStrokeWidth: 3.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -1761,6 +1783,52 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Download area overlay (shown when in download area selection mode)
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, _) {
|
||||
if (!mapProvider.isSelectingDownloadArea || mapProvider.downloadAreaBounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return DownloadAreaOverlay(
|
||||
bounds: mapProvider.downloadAreaBounds!,
|
||||
onConfirm: () {
|
||||
// Navigate to map management screen with selected bounds
|
||||
final bounds = mapProvider.downloadAreaBounds!;
|
||||
final zoom = _mapController.camera.zoom.round();
|
||||
|
||||
// Find matching layer from MapLayer.allLayers to avoid instance mismatch
|
||||
// Only pass initialLayer if it's in allLayers (standard layers only)
|
||||
MapLayer? initialLayer;
|
||||
try {
|
||||
initialLayer = MapLayer.allLayers.firstWhere(
|
||||
(layer) => layer.type == _currentLayer.type,
|
||||
);
|
||||
} catch (e) {
|
||||
// Current layer not in allLayers (WMS/vector), don't pass it
|
||||
initialLayer = null;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapManagementScreen(
|
||||
tileCacheService: _tileCache,
|
||||
initialBounds: bounds,
|
||||
initialLayer: initialLayer,
|
||||
initialZoom: zoom,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Exit download area selection mode
|
||||
mapProvider.exitDownloadAreaMode();
|
||||
},
|
||||
onCancel: () {
|
||||
// Exit download area selection mode
|
||||
mapProvider.exitDownloadAreaMode();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// Exit fullscreen button - top left (only shown in fullscreen mode)
|
||||
if (_isFullscreen)
|
||||
Positioned(
|
||||
|
||||
Reference in New Issue
Block a user