import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../services/tile_cache_service.dart'; import '../services/validation_service.dart'; import '../models/map_layer.dart'; class MapManagementScreen extends StatefulWidget { final TileCacheService tileCacheService; final MapLayer? initialLayer; final LatLngBounds? initialBounds; final int? initialZoom; const MapManagementScreen({ super.key, required this.tileCacheService, this.initialLayer, this.initialBounds, this.initialZoom, }); @override State createState() => _MapManagementScreenState(); } class _MapManagementScreenState extends State { bool _isLoading = false; String? _statusMessage; Map? _cacheStats; // Download parameters late MapLayer _selectedLayer; late TextEditingController _northController; late TextEditingController _southController; late TextEditingController _eastController; late TextEditingController _westController; late int _minZoom; late int _maxZoom; double _downloadProgress = 0.0; bool _isDownloading = false; @override void initState() { super.initState(); // Initialize with provided values or defaults _selectedLayer = widget.initialLayer ?? MapLayer.openStreetMap; if (widget.initialBounds != null) { _northController = TextEditingController( text: widget.initialBounds!.north.toStringAsFixed(4), ); _southController = TextEditingController( text: widget.initialBounds!.south.toStringAsFixed(4), ); _eastController = TextEditingController( text: widget.initialBounds!.east.toStringAsFixed(4), ); _westController = TextEditingController( text: widget.initialBounds!.west.toStringAsFixed(4), ); } else { _northController = TextEditingController(text: '46.1'); _southController = TextEditingController(text: '46.0'); _eastController = TextEditingController(text: '14.6'); _westController = TextEditingController(text: '14.4'); } // Set zoom levels if (widget.initialZoom != null) { _minZoom = (widget.initialZoom! - 2).clamp(1, 19); _maxZoom = (widget.initialZoom! + 2).clamp(1, 19); } else { _minZoom = 10; _maxZoom = 16; } _loadCacheStats(); } @override void dispose() { _northController.dispose(); _southController.dispose(); _eastController.dispose(); _westController.dispose(); super.dispose(); } Future _loadCacheStats() async { if (!mounted) return; setState(() => _isLoading = true); try { final stats = await widget.tileCacheService.getStoreStats(); if (!mounted) return; setState(() { _cacheStats = stats; _isLoading = false; }); } catch (e) { if (!mounted) return; setState(() { _statusMessage = 'Error loading stats: $e'; _isLoading = false; }); } } Future _downloadRegion() async { final validator = ValidationService(); try { // Parse coordinates final north = double.tryParse(_northController.text); final south = double.tryParse(_southController.text); final east = double.tryParse(_eastController.text); final west = double.tryParse(_westController.text); // Validate bounds final boundsResult = validator.validateBounds( north: north, south: south, east: east, west: west, ); if (!boundsResult.isValid) { _showError(boundsResult.errorMessage!); return; } // Validate zoom levels final minZoomResult = validator.validateZoomLevel(_minZoom); if (!minZoomResult.isValid) { _showError('Min zoom: ${minZoomResult.errorMessage}'); return; } final maxZoomResult = validator.validateZoomLevel(_maxZoom); if (!maxZoomResult.isValid) { _showError('Max zoom: ${maxZoomResult.errorMessage}'); return; } if (_minZoom > _maxZoom) { _showError('Minimum zoom must be less than or equal to maximum zoom'); return; } final bounds = LatLngBounds( LatLng(south!, west!), LatLng(north!, east!), ); if (!mounted) return; setState(() { _isDownloading = true; _downloadProgress = 0.0; _statusMessage = 'Starting download...'; }); await widget.tileCacheService.downloadRegion( layer: _selectedLayer, bounds: bounds, minZoom: _minZoom, maxZoom: _maxZoom, onProgress: (progress) { print('UI received progress update: $progress%'); if (!mounted) return; setState(() { _downloadProgress = progress; _statusMessage = 'Downloading map tiles...'; }); }, ); if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = 'Download completed successfully!'; }); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Map download completed!'), backgroundColor: Colors.green, ), ); } } catch (e) { if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = 'Download failed: $e'; }); _showError('Download failed: $e'); } } Future _cancelDownload() async { try { if (!mounted) return; setState(() => _statusMessage = 'Cancelling download...'); await widget.tileCacheService.cancelDownload(); if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = 'Download cancelled'; }); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Download cancelled'), backgroundColor: Colors.orange, ), ); } } catch (e) { if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = 'Cancel failed: $e'; }); _showError('Cancel failed: $e'); } } Future _clearCache() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Clear Cache'), content: const Text( 'Are you sure you want to delete all downloaded maps? This action cannot be undone.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Clear'), ), ], ), ); if (confirmed != true) return; if (!mounted) return; setState(() => _isLoading = true); try { await widget.tileCacheService.clearCache(); if (!mounted) return; setState(() => _isLoading = false); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Cache cleared successfully!'), backgroundColor: Colors.green, ), ); } } catch (e) { setState(() => _isLoading = false); _showError('Clear cache failed: $e'); } } void _showError(String message) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, duration: const Duration(seconds: 4), ), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Map Management'), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Cache Statistics _buildStatisticsCard(), const SizedBox(height: 16), // Download Region _buildDownloadCard(), const SizedBox(height: 16), // Clear Cache _buildActionsCard(), ], ), ), ); } Widget _buildStatisticsCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Cache Statistics', style: Theme.of(context).textTheme.titleLarge, ), IconButton( icon: const Icon(Icons.refresh), onPressed: _loadCacheStats, ), ], ), const SizedBox(height: 16), if (_cacheStats != null) ...[ _buildStatRow( 'Total Tiles', '${_cacheStats!['tileCount'] ?? 0}', Icons.grid_on, ), _buildStatRow( 'Cache Size', '${(_cacheStats!['sizeMB'] ?? 0.0).toStringAsFixed(2)} MB', Icons.storage, ), _buildStatRow( 'Store Name', _cacheStats!['storeName'] ?? 'Unknown', Icons.folder, ), ] else const Text('No cache statistics available'), ], ), ), ); } Widget _buildStatRow(String label, String value, IconData icon) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Icon(icon, size: 20, color: Colors.grey[600]), const SizedBox(width: 12), Expanded( child: Text(label, style: const TextStyle(fontWeight: FontWeight.w500)), ), Text(value, style: TextStyle(color: Colors.grey[600])), ], ), ); } Widget _buildDownloadCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Download Region', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Map Layer Selection DropdownButtonFormField( value: _selectedLayer, decoration: const InputDecoration( labelText: 'Map Layer', border: OutlineInputBorder(), ), items: MapLayer.allLayers.map((layer) { return DropdownMenuItem( value: layer, child: Text(layer.name), ); }).toList(), onChanged: _isDownloading ? null : (layer) { if (layer != null) { setState(() => _selectedLayer = layer); } }, ), const SizedBox(height: 16), // Coordinates Text( 'Region Bounds', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _northController, decoration: const InputDecoration( labelText: 'North', border: OutlineInputBorder(), hintText: '46.1', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _southController, decoration: const InputDecoration( labelText: 'South', border: OutlineInputBorder(), hintText: '46.0', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _eastController, decoration: const InputDecoration( labelText: 'East', border: OutlineInputBorder(), hintText: '14.6', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _westController, decoration: const InputDecoration( labelText: 'West', border: OutlineInputBorder(), hintText: '14.4', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), ], ), const SizedBox(height: 16), // Zoom Levels Text( 'Zoom Levels', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Min: $_minZoom'), Slider( value: _minZoom.toDouble(), min: 1, max: 19, divisions: 18, label: '$_minZoom', onChanged: _isDownloading ? null : (value) { setState(() { _minZoom = value.toInt(); if (_minZoom > _maxZoom) { _maxZoom = _minZoom; } }); }, ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Max: $_maxZoom'), Slider( value: _maxZoom.toDouble(), min: 1, max: 19, divisions: 18, label: '$_maxZoom', onChanged: _isDownloading ? null : (value) { setState(() { _maxZoom = value.toInt(); if (_maxZoom < _minZoom) { _minZoom = _maxZoom; } }); }, ), ], ), ), ], ), // Download Progress if (_isDownloading) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.primary.withOpacity(0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _statusMessage ?? 'Downloading...', style: TextStyle( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), Text( '${_downloadProgress.toStringAsFixed(1)}%', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: _downloadProgress / 100, minHeight: 8, backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ), ], ), ), ], const SizedBox(height: 16), // Download/Cancel Button if (_isDownloading) ElevatedButton.icon( onPressed: _cancelDownload, icon: const Icon(Icons.cancel), label: const Text('Cancel Download'), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), backgroundColor: Colors.red, foregroundColor: Colors.white, ), ) else ElevatedButton.icon( onPressed: _downloadRegion, icon: const Icon(Icons.download), label: const Text('Download Region'), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), ), const SizedBox(height: 8), Text( 'Note: Large regions or high zoom levels may take significant time and storage.', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), ), ); } Widget _buildActionsCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Cache Management', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Clear Cache Button OutlinedButton.icon( onPressed: _isDownloading ? null : _clearCache, icon: const Icon(Icons.delete_forever), label: const Text('Clear All Maps'), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, minimumSize: const Size.fromHeight(48), ), ), ], ), ), ); } }