From 01f73322c721daf7b8f4df73fcba82e646f81cf7 Mon Sep 17 00:00:00 2001 From: stopflock Date: Fri, 22 Aug 2025 23:27:01 -0500 Subject: [PATCH] fixes offline -> online transition for tile cache re-fetching, also display of camera profiles only when enabled --- lib/widgets/camera_provider_with_cache.dart | 33 +++++++++++++- lib/widgets/map_view.dart | 50 +++++++++++++++++++-- lib/widgets/tile_provider_with_cache.dart | 23 ++++++++-- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/lib/widgets/camera_provider_with_cache.dart b/lib/widgets/camera_provider_with_cache.dart index a3f8dd7..e5139ab 100644 --- a/lib/widgets/camera_provider_with_cache.dart +++ b/lib/widgets/camera_provider_with_cache.dart @@ -19,8 +19,18 @@ class CameraProviderWithCache extends ChangeNotifier { Timer? _debounceTimer; /// Call this to get (quickly) all cached overlays for the given view. + /// Filters by currently enabled profiles. List getCachedCamerasForBounds(LatLngBounds bounds) { - return CameraCache.instance.queryByBounds(bounds); + final allCameras = CameraCache.instance.queryByBounds(bounds); + final enabledProfiles = AppState.instance.enabledProfiles; + + // If no profiles are enabled, show no cameras + if (enabledProfiles.isEmpty) return []; + + // Filter cameras to only show those matching enabled profiles + return allCameras.where((camera) { + return _matchesAnyProfile(camera, enabledProfiles); + }).toList(); } /// Call this when the map view changes (bounds/profiles), triggers async fetch @@ -59,4 +69,25 @@ class CameraProviderWithCache extends ChangeNotifier { CameraCache.instance.clear(); notifyListeners(); } + + /// Force refresh the display (useful when filters change but cache doesn't) + void refreshDisplay() { + notifyListeners(); + } + + /// Check if a camera matches any of the provided profiles + bool _matchesAnyProfile(OsmCameraNode camera, List profiles) { + for (final profile in profiles) { + if (_cameraMatchesProfile(camera, profile)) return true; + } + return false; + } + + /// Check if a camera matches a specific profile (all profile tags must match) + bool _cameraMatchesProfile(OsmCameraNode camera, CameraProfile profile) { + for (final entry in profile.tags.entries) { + if (camera.tags[entry.key] != entry.value) return false; + } + return true; + } } diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 448696b..e76d686 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import '../app_state.dart'; import '../services/offline_area_service.dart'; import '../models/osm_camera_node.dart'; +import '../models/camera_profile.dart'; import 'debouncer.dart'; import 'tile_provider_with_cache.dart'; import 'camera_provider_with_cache.dart'; @@ -40,6 +41,13 @@ class _MapViewState extends State { LatLng? _currentLatLng; late final CameraProviderWithCache _cameraProvider; + + // Track profile changes to trigger camera refresh + List? _lastEnabledProfiles; + + // Track offline mode changes to trigger tile refresh + bool? _lastOfflineMode; + int _mapRebuildCounter = 0; @override void initState() { @@ -52,6 +60,7 @@ class _MapViewState extends State { // Set up camera overlay caching _cameraProvider = CameraProviderWithCache.instance; _cameraProvider.addListener(_onCamerasUpdated); + // Ensure initial overlays are fetched WidgetsBinding.instance.addPostFrameCallback((_) { _refreshCamerasFromProvider(); @@ -137,13 +146,46 @@ class _MapViewState extends State { } } + /// Helper to check if two profile lists are equal + bool _profileListsEqual(List list1, List list2) { + if (list1.length != list2.length) return false; + // Compare by profile IDs since profiles are value objects + final ids1 = list1.map((p) => p.id).toSet(); + final ids2 = list2.map((p) => p.id).toSet(); + return ids1.length == ids2.length && ids1.containsAll(ids2); + } + + + + + @override Widget build(BuildContext context) { final appState = context.watch(); final session = appState.session; - // Only update cameras when map moves or profiles/mode actually change (not every build!) - // _refreshCamerasFromProvider() is now only called from map movement and relevant change handlers. + // Check if enabled profiles changed and refresh cameras if needed + final currentEnabledProfiles = appState.enabledProfiles; + if (_lastEnabledProfiles == null || + !_profileListsEqual(_lastEnabledProfiles!, currentEnabledProfiles)) { + _lastEnabledProfiles = List.from(currentEnabledProfiles); + // Refresh cameras when profiles change + WidgetsBinding.instance.addPostFrameCallback((_) { + // Force display refresh first (for immediate UI update) + _cameraProvider.refreshDisplay(); + // Then fetch new cameras for newly enabled profiles + _refreshCamerasFromProvider(); + }); + } + + // Check if offline mode changed and force complete map rebuild + final currentOfflineMode = appState.offlineMode; + if (_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode) { + // Offline mode changed - increment counter to force FlutterMap rebuild + _mapRebuildCounter++; + debugPrint('[MapView] Offline mode changed, forcing map rebuild #$_mapRebuildCounter'); + } + _lastOfflineMode = currentOfflineMode; // Seed add‑mode target once, after first controller center is available. if (session != null && session.target == null) { @@ -193,7 +235,7 @@ class _MapViewState extends State { return Stack( children: [ FlutterMap( - key: ValueKey(appState.offlineMode), + key: ValueKey('map_rebuild_$_mapRebuildCounter'), mapController: _controller, options: MapOptions( initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194), @@ -237,7 +279,7 @@ class _MapViewState extends State { print('tileBuilder error: $e for tileImage: ${tileImage.toString()}'); return tileWidget; } - } + }, ), cameraLayers, // Built-in scale bar from flutter_map diff --git a/lib/widgets/tile_provider_with_cache.dart b/lib/widgets/tile_provider_with_cache.dart index 0ac02ff..4f3afcb 100644 --- a/lib/widgets/tile_provider_with_cache.dart +++ b/lib/widgets/tile_provider_with_cache.dart @@ -9,13 +9,30 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { static Map get tileCache => _tileCache; bool _disposed = false; + int _disposeCount = 0; TileProviderWithCache(); @override void dispose() { + _disposeCount++; + + // If already disposed, just silently return (common during FlutterMap rebuilds) + if (_disposed) { + debugPrint('[TileProviderWithCache] Already disposed (call #$_disposeCount) - ignoring'); + return; + } + + debugPrint('[TileProviderWithCache] Disposing (call #$_disposeCount)'); _disposed = true; - super.dispose(); + + // Safely call super.dispose() with error handling + try { + super.dispose(); + } catch (e) { + debugPrint('[TileProviderWithCache] Error during disposal: $e'); + // Continue execution - disposal errors shouldn't crash the app + } } @override @@ -46,8 +63,8 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { ); if (bytes.isNotEmpty) { _tileCache[key] = Uint8List.fromList(bytes); - // Only notify listeners if not disposed - if (!_disposed) { + // Only notify listeners if not disposed and still mounted + if (!_disposed && hasListeners) { notifyListeners(); // This updates any listening widgets } }