From deb9a4272b47d1598d5d9fcb524b9fcd03529686 Mon Sep 17 00:00:00 2001 From: stopflock Date: Thu, 28 Aug 2025 19:30:06 -0500 Subject: [PATCH] pull out the tile layer manager --- lib/widgets/map/tile_layer_manager.dart | 87 +++++++++++++++++++++++++ lib/widgets/map_view.dart | 63 +++++------------- 2 files changed, 105 insertions(+), 45 deletions(-) create mode 100644 lib/widgets/map/tile_layer_manager.dart diff --git a/lib/widgets/map/tile_layer_manager.dart b/lib/widgets/map/tile_layer_manager.dart new file mode 100644 index 0000000..155e134 --- /dev/null +++ b/lib/widgets/map/tile_layer_manager.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import '../../models/tile_provider.dart' as models; +import '../../services/simple_tile_service.dart'; + +/// Manages tile layer creation, caching, and provider switching. +/// Handles tile HTTP client lifecycle and cache invalidation. +class TileLayerManager { + late final SimpleTileHttpClient _tileHttpClient; + int _mapRebuildKey = 0; + String? _lastTileTypeId; + bool? _lastOfflineMode; + + /// Get the current map rebuild key for cache busting + int get mapRebuildKey => _mapRebuildKey; + + /// Initialize the tile layer manager + void initialize() { + _tileHttpClient = SimpleTileHttpClient(); + } + + /// Dispose of resources + void dispose() { + _tileHttpClient.close(); + } + + /// Check if cache should be cleared and increment rebuild key if needed. + /// Returns true if cache was cleared (map should be rebuilt). + bool checkAndClearCacheIfNeeded({ + required String? currentTileTypeId, + required bool currentOfflineMode, + }) { + bool shouldClear = false; + String? reason; + + if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId)) { + reason = 'tile type ($currentTileTypeId)'; + shouldClear = true; + } else if ((_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) { + reason = 'offline mode ($currentOfflineMode)'; + shouldClear = true; + } + + if (shouldClear) { + // Force map rebuild with new key to bust flutter_map cache + _mapRebuildKey++; + debugPrint('[TileLayerManager] *** CACHE CLEAR *** $reason changed - rebuilding map $_mapRebuildKey'); + } + + _lastTileTypeId = currentTileTypeId; + _lastOfflineMode = currentOfflineMode; + + return shouldClear; + } + + /// Clear the tile request queue (call after cache clear) + void clearTileQueue() { + debugPrint('[TileLayerManager] Post-frame: Clearing tile request queue'); + _tileHttpClient.clearTileQueue(); + } + + /// Clear tile queue immediately (for zoom changes, etc.) + void clearTileQueueImmediate() { + _tileHttpClient.clearTileQueue(); + } + + /// Build tile layer widget with current provider and type. + /// Uses fake domain that SimpleTileHttpClient can parse for cache separation. + Widget buildTileLayer({ + required models.TileProvider? selectedProvider, + required models.TileType? selectedTileType, + }) { + // Use fake domain with standard HTTPS scheme: https://tiles.local/provider/type/z/x/y + // This naturally separates cache entries by provider and type while being HTTP-compatible + final urlTemplate = 'https://tiles.local/${selectedProvider?.id ?? 'unknown'}/${selectedTileType?.id ?? 'unknown'}/{z}/{x}/{y}'; + + return TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: 'com.stopflock.flock_map_app', + tileProvider: NetworkTileProvider( + httpClient: _tileHttpClient, + // Enable flutter_map caching - cache busting handled by URL changes and FlutterMap key + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index cdbe9fc..02ee415 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -10,7 +10,6 @@ import 'package:collection/collection.dart'; import '../app_state.dart'; import '../services/offline_area_service.dart'; -import '../services/simple_tile_service.dart'; import '../services/network_status.dart'; import '../models/osm_camera_node.dart'; import '../models/camera_profile.dart'; @@ -21,6 +20,7 @@ import 'map/camera_markers.dart'; import 'map/direction_cones.dart'; import 'map/map_overlays.dart'; import 'map/map_position_manager.dart'; +import 'map/tile_layer_manager.dart'; import 'network_status_indicator.dart'; import '../dev_config.dart'; import '../screens/home_screen.dart' show FollowMeMode; @@ -51,27 +51,23 @@ class MapViewState extends State { LatLng? _currentLatLng; late final CameraProviderWithCache _cameraProvider; - late final SimpleTileHttpClient _tileHttpClient; late final MapPositionManager _positionManager; + late final TileLayerManager _tileManager; // Track profile changes to trigger camera refresh List? _lastEnabledProfiles; // Track zoom to clear queue on zoom changes double? _lastZoom; - - // Track changes that require cache clearing - String? _lastTileTypeId; - bool? _lastOfflineMode; - int _mapRebuildKey = 0; @override void initState() { super.initState(); OfflineAreaService(); _controller = widget.controller; - _tileHttpClient = SimpleTileHttpClient(); _positionManager = MapPositionManager(); + _tileManager = TileLayerManager(); + _tileManager.initialize(); // Load last map position before initializing GPS _positionManager.loadLastMapPosition().then((_) { @@ -103,7 +99,7 @@ class MapViewState extends State { _tileDebounce.dispose(); _mapPositionDebounce.dispose(); _cameraProvider.removeListener(_onCamerasUpdated); - _tileHttpClient.close(); + _tileManager.dispose(); super.dispose(); } @@ -255,24 +251,7 @@ class MapViewState extends State { return ids1.length == ids2.length && ids1.containsAll(ids2); } - /// Build tile layer - uses fake domain that SimpleTileHttpClient can parse - Widget _buildTileLayer(AppState appState) { - final selectedTileType = appState.selectedTileType; - final selectedProvider = appState.selectedTileProvider; - - // Use fake domain with standard HTTPS scheme: https://tiles.local/provider/type/z/x/y - // This naturally separates cache entries by provider and type while being HTTP-compatible - final urlTemplate = 'https://tiles.local/${selectedProvider?.id ?? 'unknown'}/${selectedTileType?.id ?? 'unknown'}/{z}/{x}/{y}'; - - return TileLayer( - urlTemplate: urlTemplate, - userAgentPackageName: 'com.stopflock.flock_map_app', - tileProvider: NetworkTileProvider( - httpClient: _tileHttpClient, - // Enable flutter_map caching - cache busting handled by URL changes and FlutterMap key - ), - ); - } + @@ -299,25 +278,16 @@ class MapViewState extends State { } // Check if tile type OR offline mode changed and clear cache if needed - final currentTileTypeId = appState.selectedTileType?.id; - final currentOfflineMode = appState.offlineMode; + final cacheCleared = _tileManager.checkAndClearCacheIfNeeded( + currentTileTypeId: appState.selectedTileType?.id, + currentOfflineMode: appState.offlineMode, + ); - if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) || - (_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) { - // Force map rebuild with new key to bust flutter_map cache - _mapRebuildKey++; - final reason = _lastTileTypeId != currentTileTypeId - ? 'tile type ($currentTileTypeId)' - : 'offline mode ($currentOfflineMode)'; - debugPrint('[MapView] *** CACHE CLEAR *** $reason changed - rebuilding map $_mapRebuildKey'); + if (cacheCleared) { WidgetsBinding.instance.addPostFrameCallback((_) { - debugPrint('[MapView] Post-frame: Clearing tile request queue'); - _tileHttpClient.clearTileQueue(); + _tileManager.clearTileQueue(); }); } - - _lastTileTypeId = currentTileTypeId; - _lastOfflineMode = currentOfflineMode; // Seed add‑mode target once, after first controller center is available. if (session != null && session.target == null) { @@ -383,7 +353,7 @@ class MapViewState extends State { return Stack( children: [ FlutterMap( - key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_$_mapRebuildKey'), + key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_${_tileManager.mapRebuildKey}'), mapController: _controller.mapController, options: MapOptions( initialCenter: _currentLatLng ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194), @@ -409,7 +379,7 @@ class MapViewState extends State { if (zoomChanged) { _tileDebounce(() { // Clear stale tile requests on zoom change (quietly) - _tileHttpClient.clearTileQueue(); + _tileManager.clearTileQueueImmediate(); }); } _lastZoom = currentZoom; @@ -426,7 +396,10 @@ class MapViewState extends State { }, ), children: [ - _buildTileLayer(appState), + _tileManager.buildTileLayer( + selectedProvider: appState.selectedTileProvider, + selectedTileType: appState.selectedTileType, + ), cameraLayers, // Built-in scale bar from flutter_map Scalebar(