From b1d29e443dd5a7c0e66f0244dc67d0aae2bc6f67 Mon Sep 17 00:00:00 2001 From: stopflock Date: Thu, 7 Aug 2025 09:22:17 -0500 Subject: [PATCH] fix dup world area, storage estimate --- lib/screens/home_screen.dart | 2 +- lib/screens/settings_screen.dart | 27 +++++++++--- lib/services/offline_area_service.dart | 57 ++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 6049bdf..b7e173c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -135,7 +135,7 @@ class _DownloadAreaDialogState extends State { final worldTiles = OfflineAreaService().computeTileList( OfflineAreaService().globalWorldBounds(), 1, 4); final nTiles = allTiles.length + worldTiles.length; - const kbPerTile = 25; // Average PNG tile size + const kbPerTile = 6.5; // Empirically ~6.5kB average for OSM tiles at z=1-19 final totalMb = (nTiles * kbPerTile) / 1024.0; setState(() { _minZoom = minZoom; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 5f7b1fb..c47eaa3 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -420,10 +420,8 @@ class _OfflineAreasSectionState extends State<_OfflineAreasSection> { } else { subtitle += '\nTiles: ${area.tilesTotal}'; } - subtitle += ' | Size: $diskStr'; - if (area.status == OfflineAreaStatus.complete) { - subtitle += ' | Cameras: ${area.cameras.length}'; - } + subtitle += '\nSize: $diskStr'; + subtitle += '\nCameras: ${area.cameras.length}'; return Card( child: ListTile( leading: Icon(area.status == OfflineAreaStatus.complete @@ -477,7 +475,26 @@ class _OfflineAreasSectionState extends State<_OfflineAreasSection> { } }, ), - if (area.status != OfflineAreaStatus.downloading) + if (area.isPermanent) + IconButton( + icon: const Icon(Icons.refresh, color: Colors.blue), + tooltip: 'Refresh/re-download world tiles', + onPressed: () async { + // Trigger re-download for permanent area + await service.downloadArea( + id: area.id, + bounds: area.bounds, + minZoom: area.minZoom, + maxZoom: area.maxZoom, + directory: area.directory, + name: area.name, + onProgress: (progress) {}, + onComplete: (status) {}, + ); + setState(() {}); + }, + ) + else if (area.status != OfflineAreaStatus.downloading) IconButton( icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Delete offline area', diff --git a/lib/services/offline_area_service.dart b/lib/services/offline_area_service.dart index 833ee9d..0b73547 100644 --- a/lib/services/offline_area_service.dart +++ b/lib/services/offline_area_service.dart @@ -24,6 +24,7 @@ class OfflineArea { int tilesTotal; List cameras; int sizeBytes; // Disk size in bytes + final bool isPermanent; // Not user-deletable if true OfflineArea({ required this.id, @@ -38,6 +39,7 @@ class OfflineArea { this.tilesTotal = 0, this.cameras = const [], this.sizeBytes = 0, + this.isPermanent = false, }); Map toJson() => { @@ -56,6 +58,7 @@ class OfflineArea { 'tilesTotal': tilesTotal, 'cameras': cameras.map((c) => c.toJson()).toList(), 'sizeBytes': sizeBytes, + 'isPermanent': isPermanent, }; static OfflineArea fromJson(Map json) { @@ -78,6 +81,7 @@ class OfflineArea { cameras: (json['cameras'] as List? ?? []) .map((e) => OsmCameraNode.fromJson(e)).toList(), sizeBytes: json['sizeBytes'] ?? 0, + isPermanent: json['isPermanent'] ?? false, ); } } @@ -105,9 +109,33 @@ class OfflineAreaService { static final OfflineAreaService _instance = OfflineAreaService._(); factory OfflineAreaService() => _instance; OfflineAreaService._() { + _initPermanentWorldArea(); _loadAreasFromDisk(); } + // Ensure permanent world area exists at all times + Future _initPermanentWorldArea() async { + final dir = await getOfflineAreaDir(); + final worldDir = "${dir.path}/world_z1_4"; + final LatLngBounds worldBounds = globalWorldBounds(); + // Check if already present + final existing = _areas.where((a) => a.isPermanent).toList(); + if (existing.isEmpty) { + _areas.insert(0, OfflineArea( + id: 'permanent_world_z1_4', + name: 'World (zoom 1-4)', + bounds: worldBounds, + minZoom: 1, + maxZoom: 4, + directory: worldDir, + status: OfflineAreaStatus.complete, // Assume complete until proven otherwise on next app run + progress: 1.0, + isPermanent: true, + )); + await saveAreasToDisk(); + } + } + final List _areas = []; /// Where offline area data/metadata lives @@ -139,13 +167,27 @@ class OfflineAreaService { void Function(OfflineAreaStatus status)? onComplete, String? name, }) async { - final area = OfflineArea( + // If area with same id exists, replace its contents, else add. + OfflineArea? area; + for (final a in _areas) { + if (a.id == id) { area = a; break; } + } + if (area != null) { + // Remove area and its files before creating fresh + _areas.remove(area); + final dirObj = Directory(area.directory); + if (await dirObj.exists()) { + await dirObj.delete(recursive: true); + } + } + area = OfflineArea( id: id, - name: name ?? '', + name: name ?? area?.name ?? '', bounds: bounds, minZoom: minZoom, maxZoom: maxZoom, directory: directory, + isPermanent: area?.isPermanent ?? false, ); _areas.add(area); await saveAreasToDisk(); @@ -225,9 +267,16 @@ class OfflineAreaService { final file = await _getMetadataPath(); if (!(await file.exists())) return; final str = await file.readAsString(); - final data = jsonDecode(str); + if (str.trim().isEmpty) return; + late final List data; + try { + data = jsonDecode(str); + } catch (e) { + debugPrint('Failed to parse offline areas json: $e'); + return; + } _areas.clear(); - for (final areaJson in (data as List)) { + for (final areaJson in data) { final area = OfflineArea.fromJson(areaJson); // Check if directory still exists; adjust status if not if (!Directory(area.directory).existsSync()) {