diff --git a/lib/widgets/map/camera_refresh_controller.dart b/lib/widgets/map/camera_refresh_controller.dart new file mode 100644 index 0000000..ae94771 --- /dev/null +++ b/lib/widgets/map/camera_refresh_controller.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +import '../../models/camera_profile.dart'; +import '../../app_state.dart' show UploadMode; +import '../camera_provider_with_cache.dart'; +import '../../dev_config.dart'; + +/// Manages camera data refreshing, profile change detection, and camera cache operations. +/// Handles debounced camera fetching and profile-based cache invalidation. +class CameraRefreshController { + late final CameraProviderWithCache _cameraProvider; + List? _lastEnabledProfiles; + VoidCallback? _onCamerasUpdated; + + /// Initialize the camera refresh controller + void initialize({required VoidCallback onCamerasUpdated}) { + _cameraProvider = CameraProviderWithCache.instance; + _onCamerasUpdated = onCamerasUpdated; + _cameraProvider.addListener(_onCamerasUpdated!); + } + + /// Dispose of resources and listeners + void dispose() { + if (_onCamerasUpdated != null) { + _cameraProvider.removeListener(_onCamerasUpdated!); + } + } + + /// Check if camera profiles changed and handle cache clearing if needed. + /// Returns true if profiles changed (triggering a refresh). + bool checkAndHandleProfileChanges({ + required List currentEnabledProfiles, + required VoidCallback onProfilesChanged, + }) { + if (_lastEnabledProfiles == null || + !_profileListsEqual(_lastEnabledProfiles!, currentEnabledProfiles)) { + _lastEnabledProfiles = List.from(currentEnabledProfiles); + + // Handle profile change with cache clearing and refresh + WidgetsBinding.instance.addPostFrameCallback((_) { + // Clear camera cache to ensure fresh data for new profile combination + _cameraProvider.clearCache(); + // Force display refresh first (for immediate UI update) + _cameraProvider.refreshDisplay(); + // Notify that profiles changed (triggers camera refresh) + onProfilesChanged(); + }); + + return true; + } + return false; + } + + /// Refresh cameras from provider for the current map view + void refreshCamerasFromProvider({ + required AnimatedMapController controller, + required List enabledProfiles, + required UploadMode uploadMode, + required BuildContext context, + }) { + LatLngBounds? bounds; + try { + bounds = controller.mapController.camera.visibleBounds; + } catch (_) { + return; + } + + final zoom = controller.mapController.camera.zoom; + if (zoom < kCameraMinZoomLevel) { + // Show a snackbar-style bubble warning + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Cameras not drawn below zoom level $kCameraMinZoomLevel'), + duration: const Duration(seconds: 2), + ), + ); + } + return; + } + + _cameraProvider.fetchAndUpdate( + bounds: bounds, + profiles: enabledProfiles, + uploadMode: uploadMode, + ); + } + + /// Get the camera provider instance for external access + CameraProviderWithCache get cameraProvider => _cameraProvider; + + /// Helper to check if two profile lists are equal by comparing IDs + 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); + } +} \ No newline at end of file diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 02ee415..4438a2e 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -21,6 +21,7 @@ import 'map/direction_cones.dart'; import 'map/map_overlays.dart'; import 'map/map_position_manager.dart'; import 'map/tile_layer_manager.dart'; +import 'map/camera_refresh_controller.dart'; import 'network_status_indicator.dart'; import '../dev_config.dart'; import '../screens/home_screen.dart' show FollowMeMode; @@ -50,12 +51,9 @@ class MapViewState extends State { StreamSubscription? _positionSub; LatLng? _currentLatLng; - late final CameraProviderWithCache _cameraProvider; late final MapPositionManager _positionManager; late final TileLayerManager _tileManager; - - // Track profile changes to trigger camera refresh - List? _lastEnabledProfiles; + late final CameraRefreshController _cameraController; // Track zoom to clear queue on zoom changes double? _lastZoom; @@ -68,6 +66,8 @@ class MapViewState extends State { _positionManager = MapPositionManager(); _tileManager = TileLayerManager(); _tileManager.initialize(); + _cameraController = CameraRefreshController(); + _cameraController.initialize(onCamerasUpdated: _onCamerasUpdated); // Load last map position before initializing GPS _positionManager.loadLastMapPosition().then((_) { @@ -78,10 +78,6 @@ class MapViewState extends State { }); _initLocation(); - // Set up camera overlay caching - _cameraProvider = CameraProviderWithCache.instance; - _cameraProvider.addListener(_onCamerasUpdated); - // Fetch initial cameras WidgetsBinding.instance.addPostFrameCallback((_) { _refreshCamerasFromProvider(); @@ -98,7 +94,7 @@ class MapViewState extends State { _cameraDebounce.dispose(); _tileDebounce.dispose(); _mapPositionDebounce.dispose(); - _cameraProvider.removeListener(_onCamerasUpdated); + _cameraController.dispose(); _tileManager.dispose(); super.dispose(); } @@ -127,29 +123,11 @@ class MapViewState extends State { void _refreshCamerasFromProvider() { final appState = context.read(); - LatLngBounds? bounds; - try { - bounds = _controller.mapController.camera.visibleBounds; - } catch (_) { - return; - } - final zoom = _controller.mapController.camera.zoom; - if (zoom < kCameraMinZoomLevel) { - // Show a snackbar-style bubble, if desired - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Cameras not drawn below zoom level $kCameraMinZoomLevel'), - duration: const Duration(seconds: 2), - ), - ); - } - return; - } - _cameraProvider.fetchAndUpdate( - bounds: bounds, - profiles: appState.enabledProfiles, + _cameraController.refreshCamerasFromProvider( + controller: _controller, + enabledProfiles: appState.enabledProfiles, uploadMode: appState.uploadMode, + context: context, ); } @@ -242,14 +220,7 @@ 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); - } + @@ -262,20 +233,10 @@ class MapViewState extends State { final editSession = appState.editSession; // 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((_) { - // Clear camera cache to ensure fresh data for new profile combination - _cameraProvider.clearCache(); - // Force display refresh first (for immediate UI update) - _cameraProvider.refreshDisplay(); - // Then fetch new cameras for newly enabled profiles - _refreshCamerasFromProvider(); - }); - } + _cameraController.checkAndHandleProfileChanges( + currentEnabledProfiles: appState.enabledProfiles, + onProfilesChanged: _refreshCamerasFromProvider, + ); // Check if tile type OR offline mode changed and clear cache if needed final cacheCleared = _tileManager.checkAndClearCacheIfNeeded(