From f1c73a5e55dba4d5d971e24491f2767db7752988 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sun, 24 Aug 2025 15:31:02 -0500 Subject: [PATCH] settings area metadata, offline areas support for tile types --- .../offline_areas_section.dart | 5 +++ lib/services/offline_area_service.dart | 8 +++++ .../offline_areas/offline_area_models.dart | 35 +++++++++++++++++++ .../offline_areas/world_area_manager.dart | 14 +++++++- lib/widgets/download_area_dialog.dart | 9 +++++ lib/widgets/map/layer_selector_button.dart | 13 +++++++ 6 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/screens/settings_screen_sections/offline_areas_section.dart b/lib/screens/settings_screen_sections/offline_areas_section.dart index 0e7f741..b46defe 100644 --- a/lib/screens/settings_screen_sections/offline_areas_section.dart +++ b/lib/screens/settings_screen_sections/offline_areas_section.dart @@ -41,6 +41,7 @@ class _OfflineAreasSectionState extends State { : "${(area.sizeBytes / 1024).toStringAsFixed(1)} KB" : '--'; String subtitle = + 'Provider: ${area.tileProviderDisplay}\n' + 'Z${area.minZoom}-${area.maxZoom}\n' + 'Lat: ${area.bounds.southWest.latitude.toStringAsFixed(3)}, ${area.bounds.southWest.longitude.toStringAsFixed(3)}\n' + 'Lat: ${area.bounds.northEast.latitude.toStringAsFixed(3)}, ${area.bounds.northEast.longitude.toStringAsFixed(3)}'; @@ -121,6 +122,10 @@ class _OfflineAreasSectionState extends State { name: area.name, onProgress: (progress) {}, onComplete: (status) {}, + tileProviderId: area.tileProviderId, + tileProviderName: area.tileProviderName, + tileTypeId: area.tileTypeId, + tileTypeName: area.tileTypeName, ); setState(() {}); }, diff --git a/lib/services/offline_area_service.dart b/lib/services/offline_area_service.dart index 59da1df..f3f57bc 100644 --- a/lib/services/offline_area_service.dart +++ b/lib/services/offline_area_service.dart @@ -182,6 +182,10 @@ class OfflineAreaService { void Function(double progress)? onProgress, void Function(OfflineAreaStatus status)? onComplete, String? name, + String? tileProviderId, + String? tileProviderName, + String? tileTypeId, + String? tileTypeName, }) async { OfflineArea? area; for (final a in _areas) { @@ -202,6 +206,10 @@ class OfflineAreaService { maxZoom: maxZoom, directory: directory, isPermanent: area?.isPermanent ?? false, + tileProviderId: tileProviderId, + tileProviderName: tileProviderName, + tileTypeId: tileTypeId, + tileTypeName: tileTypeName, ); _areas.add(area); await saveAreasToDisk(); diff --git a/lib/services/offline_areas/offline_area_models.dart b/lib/services/offline_areas/offline_area_models.dart index 9dca081..76817c5 100644 --- a/lib/services/offline_areas/offline_area_models.dart +++ b/lib/services/offline_areas/offline_area_models.dart @@ -20,6 +20,12 @@ class OfflineArea { List cameras; int sizeBytes; // Disk size in bytes final bool isPermanent; // Not user-deletable if true + + // Tile provider metadata (null for legacy areas) + final String? tileProviderId; + final String? tileProviderName; + final String? tileTypeId; + final String? tileTypeName; OfflineArea({ required this.id, @@ -35,6 +41,10 @@ class OfflineArea { this.cameras = const [], this.sizeBytes = 0, this.isPermanent = false, + this.tileProviderId, + this.tileProviderName, + this.tileTypeId, + this.tileTypeName, }); Map toJson() => { @@ -54,6 +64,10 @@ class OfflineArea { 'cameras': cameras.map((c) => c.toJson()).toList(), 'sizeBytes': sizeBytes, 'isPermanent': isPermanent, + 'tileProviderId': tileProviderId, + 'tileProviderName': tileProviderName, + 'tileTypeId': tileTypeId, + 'tileTypeName': tileTypeName, }; static OfflineArea fromJson(Map json) { @@ -77,6 +91,27 @@ class OfflineArea { .map((e) => OsmCameraNode.fromJson(e)).toList(), sizeBytes: json['sizeBytes'] ?? 0, isPermanent: json['isPermanent'] ?? false, + tileProviderId: json['tileProviderId'], + tileProviderName: json['tileProviderName'], + tileTypeId: json['tileTypeId'], + tileTypeName: json['tileTypeName'], ); } + + /// Get display text for the tile provider used in this area + String get tileProviderDisplay { + if (tileProviderName != null && tileTypeName != null) { + return '$tileProviderName - $tileTypeName'; + } else if (tileTypeName != null) { + return tileTypeName!; + } else if (tileProviderName != null) { + return tileProviderName!; + } else { + // Legacy area - assume OSM + return 'OpenStreetMap (Legacy)'; + } + } + + /// Check if this area has tile provider metadata + bool get hasTileProviderInfo => tileProviderId != null && tileTypeId != null; } diff --git a/lib/services/offline_areas/world_area_manager.dart b/lib/services/offline_areas/world_area_manager.dart index 06ea8a3..879ecb3 100644 --- a/lib/services/offline_areas/world_area_manager.dart +++ b/lib/services/offline_areas/world_area_manager.dart @@ -23,6 +23,10 @@ class WorldAreaManager { required int maxZoom, required String directory, String? name, + String? tileProviderId, + String? tileProviderName, + String? tileTypeId, + String? tileTypeName, }) downloadArea, ) async { // Find existing world area @@ -66,6 +70,10 @@ class WorldAreaManager { required int maxZoom, required String directory, String? name, + String? tileProviderId, + String? tileProviderName, + String? tileTypeId, + String? tileTypeName, }) downloadArea, ) async { if (world.status == OfflineAreaStatus.complete) return; @@ -97,7 +105,7 @@ class WorldAreaManager { world.status = OfflineAreaStatus.downloading; debugPrint('WorldAreaManager: Starting world area download. ${world.tilesDownloaded}/${world.tilesTotal} tiles found.'); - // Start download (fire and forget) + // Start download (fire and forget) - use OSM for world areas downloadArea( id: world.id, bounds: world.bounds, @@ -105,6 +113,10 @@ class WorldAreaManager { maxZoom: world.maxZoom, directory: world.directory, name: world.name, + tileProviderId: 'openstreetmap', + tileProviderName: 'OpenStreetMap', + tileTypeId: 'osm_street', + tileTypeName: 'Street Map', ); } } diff --git a/lib/widgets/download_area_dialog.dart b/lib/widgets/download_area_dialog.dart index 1cb56e4..c39d8e4 100644 --- a/lib/widgets/download_area_dialog.dart +++ b/lib/widgets/download_area_dialog.dart @@ -236,6 +236,11 @@ class _DownloadAreaDialogState extends State { final appDocDir = await OfflineAreaService().getOfflineAreaDir(); final dir = "${appDocDir.path}/$id"; + // Get current tile provider info + final appState = context.read(); + final selectedProvider = appState.selectedTileProvider; + final selectedTileType = appState.selectedTileType; + // Fire and forget: don't await download, so dialog closes immediately // ignore: unawaited_futures OfflineAreaService().downloadArea( @@ -246,6 +251,10 @@ class _DownloadAreaDialogState extends State { directory: dir, onProgress: (progress) {}, onComplete: (status) {}, + tileProviderId: selectedProvider?.id, + tileProviderName: selectedProvider?.name, + tileTypeId: selectedTileType?.id, + tileTypeName: selectedTileType?.name, ); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/widgets/map/layer_selector_button.dart b/lib/widgets/map/layer_selector_button.dart index 740ff13..439275b 100644 --- a/lib/widgets/map/layer_selector_button.dart +++ b/lib/widgets/map/layer_selector_button.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import '../../app_state.dart'; import '../../models/tile_provider.dart'; +import '../../services/offline_area_service.dart'; class LayerSelectorButton extends StatelessWidget { const LayerSelectorButton({super.key}); @@ -17,6 +18,18 @@ class LayerSelectorButton extends StatelessWidget { } void _showLayerSelector(BuildContext context) { + // Check if any downloads are active + final offlineService = OfflineAreaService(); + if (offlineService.hasActiveDownloads) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Cannot change tile types while downloading offline areas'), + duration: Duration(seconds: 3), + ), + ); + return; + } + showDialog( context: context, builder: (context) => const _LayerSelectorDialog(),