fixes offline -> online transition for tile cache re-fetching, also display of camera profiles only when enabled

This commit is contained in:
stopflock
2025-08-22 23:27:01 -05:00
parent 257aefb2fc
commit 01f73322c7
3 changed files with 98 additions and 8 deletions
+32 -1
View File
@@ -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<OsmCameraNode> 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<CameraProfile> 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;
}
}
+46 -4
View File
@@ -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<MapView> {
LatLng? _currentLatLng;
late final CameraProviderWithCache _cameraProvider;
// Track profile changes to trigger camera refresh
List<CameraProfile>? _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<MapView> {
// 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<MapView> {
}
}
/// Helper to check if two profile lists are equal
bool _profileListsEqual(List<CameraProfile> list1, List<CameraProfile> 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<AppState>();
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 addmode target once, after first controller center is available.
if (session != null && session.target == null) {
@@ -193,7 +235,7 @@ class _MapViewState extends State<MapView> {
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<MapView> {
print('tileBuilder error: $e for tileImage: ${tileImage.toString()}');
return tileWidget;
}
}
},
),
cameraLayers,
// Built-in scale bar from flutter_map
+20 -3
View File
@@ -9,13 +9,30 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
static Map<String, Uint8List> 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
}
}