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:
Janez T
2025-10-28 19:09:11 +01:00
parent fed4aa19f1
commit da3302b030
14 changed files with 769 additions and 272 deletions

View File

@@ -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) {

View File

@@ -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(