diff --git a/README.md b/README.md index 8c516da..1a3c606 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,13 @@ A Flutter app for mapping and tagging ALPR-style cameras (and other surveillance ## Offline Areas *(IN PROGRESS)* - Download any map area for offline use! Uses OSM raster tile cache and Overpass-surveillance cameras. - Each area download: - - Selects the current map region with a dynamic minimum zoom. + - Selects the current map region with a dynamic minimum zoom (calculated as the highest zoom level at which a single tile covers the selected area) - Lets user pick the max zoom, shows real tile/storage estimate. - Always includes world tiles for zoom 1–4 for seamless context. - Downloads **all** camera points in area (not just top 250) for offline visibility. - Status, progress, and detailed area management appear in Settings: - Cancel, retry, and delete areas (in UI now) - Storage/camera breakdown per area (coming soon) -- After area download: future updates will allow full offline map and camera viewing/queuing. --- diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 581d7bb..56b59e4 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import '../app_state.dart'; import '../widgets/map_view.dart'; +import 'package:flutter_map/flutter_map.dart'; +import '../services/offline_area_service.dart'; import '../widgets/add_camera_sheet.dart'; class HomeScreen extends StatefulWidget { @@ -14,6 +16,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { final GlobalKey _scaffoldKey = GlobalKey(); + final MapController _mapController = MapController(); bool _followMe = true; void _openAddCameraSheet() { @@ -47,6 +50,7 @@ class _HomeScreenState extends State { ], ), body: MapView( + controller: _mapController, followMe: _followMe, onUserGesture: () { if (_followMe) setState(() => _followMe = false); @@ -67,7 +71,7 @@ class _HomeScreenState extends State { FloatingActionButton.extended( onPressed: () => showDialog( context: context, - builder: (ctx) => const DownloadAreaDialog(), + builder: (ctx) => DownloadAreaDialog(controller: _mapController), ), icon: const Icon(Icons.download_for_offline), label: const Text('Download'), @@ -82,9 +86,9 @@ class _HomeScreenState extends State { } // --- Download area dialog --- - class DownloadAreaDialog extends StatefulWidget { - const DownloadAreaDialog({super.key}); + final MapController controller; + const DownloadAreaDialog({super.key, required this.controller}); @override State createState() => _DownloadAreaDialogState(); @@ -147,19 +151,47 @@ class _DownloadAreaDialogState extends State { child: const Text('Cancel'), ), ElevatedButton( - onPressed: () { - // Real download to be implemented in later stages. - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Download started (stub only)'), - ), - ); + onPressed: () async { + try { + final bounds = widget.controller.camera.visibleBounds; + final maxZoom = _zoom.toInt(); + final minZoom = _findDynamicMinZoom(bounds); + final id = DateTime.now().toIso8601String().replaceAll(':', '-'); + final dir = '/tmp/offline_areas/$id'; + + await OfflineAreaService().downloadArea( + id: id, + bounds: bounds, + minZoom: minZoom, + maxZoom: maxZoom, + directory: dir, + onProgress: (progress) {}, + onComplete: (status) {}, + ); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Download started!'), + ), + ); + } catch (e) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to start download: $e'), + ), + ); + } }, child: const Text('Download'), ), ], ); } + + int _findDynamicMinZoom(LatLngBounds bounds) { + // For now, just pick 12 as min; can implement dynamic min‑zoom by area + return 12; + } } diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index b335321..01a54c2 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -16,8 +16,10 @@ import 'debouncer.dart'; import 'camera_tag_sheet.dart'; class MapView extends StatefulWidget { + final MapController controller; const MapView({ super.key, + required this.controller, required this.followMe, required this.onUserGesture, }); @@ -30,7 +32,7 @@ class MapView extends StatefulWidget { } class _MapViewState extends State { - final MapController _controller = MapController(); + late final MapController _controller; final OverpassService _overpass = OverpassService(); final Debouncer _debounce = Debouncer(const Duration(milliseconds: 500)); @@ -58,6 +60,7 @@ class _MapViewState extends State { @override void initState() { super.initState(); + _controller = widget.controller; _initLocation(); }