From 17c9ee0c5c7ed5d1828c18ef6b9238b4fac1de2b Mon Sep 17 00:00:00 2001 From: stopflock Date: Sun, 24 Aug 2025 19:38:42 -0500 Subject: [PATCH] idk but it's better - cache busting works. --- lib/screens/tile_provider_editor_screen.dart | 25 ++++++++ lib/services/map_data_provider.dart | 15 +++-- .../cameras_from_overpass.dart | 11 +++- .../tiles_from_remote.dart | 15 ++--- lib/services/network_status.dart | 14 ++--- lib/widgets/camera_tag_sheet.dart | 10 +-- lib/widgets/map_view.dart | 62 ++++++++++++++++--- 7 files changed, 114 insertions(+), 38 deletions(-) diff --git a/lib/screens/tile_provider_editor_screen.dart b/lib/screens/tile_provider_editor_screen.dart index 0525301..2a10759 100644 --- a/lib/screens/tile_provider_editor_screen.dart +++ b/lib/screens/tile_provider_editor_screen.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; +import 'package:collection/collection.dart'; import '../app_state.dart'; import '../models/tile_provider.dart'; @@ -158,9 +159,33 @@ class _TileProviderEditorScreenState extends State { void _deleteTileType(int index) { if (_tileTypes.length <= 1) return; + final tileTypeToDelete = _tileTypes[index]; + final appState = context.read(); + setState(() { _tileTypes.removeAt(index); }); + + // If we're deleting the currently selected tile type, switch to another one + if (appState.selectedTileType?.id == tileTypeToDelete.id) { + // Find first remaining tile type in this provider or any other provider + TileType? replacement; + if (_tileTypes.isNotEmpty) { + replacement = _tileTypes.first; + } else { + // Look in other providers + for (final provider in appState.tileProviders) { + if (provider.availableTileTypes.isNotEmpty) { + replacement = provider.availableTileTypes.first; + break; + } + } + } + + if (replacement != null) { + appState.setSelectedTileType(replacement.id); + } + } } void _showTileTypeDialog({TileType? tileType, int? index}) { diff --git a/lib/services/map_data_provider.dart b/lib/services/map_data_provider.dart index a6ac431..8f76a91 100644 --- a/lib/services/map_data_provider.dart +++ b/lib/services/map_data_provider.dart @@ -79,7 +79,7 @@ class MapDataProvider { pageSize: AppState.instance.maxCameras, ); } catch (e) { - print('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.'); + debugPrint('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.'); return fetchLocalCameras( bounds: bounds, profiles: profiles, @@ -152,14 +152,13 @@ class MapDataProvider { final selectedTileType = appState.selectedTileType; final selectedProvider = appState.selectedTileProvider; - if (selectedTileType != null && selectedProvider != null) { - // Use current provider - final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey); - return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl); - } else { - // Fallback to OSM if no provider selected - return fetchOSMTile(z: z, x: x, y: y); + // We guarantee that a provider and tile type are always selected + if (selectedTileType == null || selectedProvider == null) { + throw Exception('No tile provider selected - this should never happen'); } + + final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey); + return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl); } /// Clear any queued tile requests (call when map view changes significantly) diff --git a/lib/services/map_data_submodules/cameras_from_overpass.dart b/lib/services/map_data_submodules/cameras_from_overpass.dart index 1cfe70a..9e94d15 100644 --- a/lib/services/map_data_submodules/cameras_from_overpass.dart +++ b/lib/services/map_data_submodules/cameras_from_overpass.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -43,15 +44,19 @@ Future> camerasFromOverpass({ print('[camerasFromOverpass] Querying Overpass...'); print('[camerasFromOverpass] Query:\n$query'); final resp = await http.post(Uri.parse(prodEndpoint), body: {'data': query.trim()}); - print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}'); + // Only log errors if (resp.statusCode != 200) { - print('[camerasFromOverpass] Overpass failed: ${resp.body}'); + debugPrint('[camerasFromOverpass] Overpass failed: ${resp.body}'); NetworkStatus.instance.reportOverpassIssue(); return []; } final data = jsonDecode(resp.body) as Map; final elements = data['elements'] as List; - print('[camerasFromOverpass] Retrieved elements: ${elements.length}'); + + // Only log if many cameras found or if it's a bulk download + if (elements.length > 20 || fetchAllPages) { + debugPrint('[camerasFromOverpass] Retrieved ${elements.length} cameras'); + } NetworkStatus.instance.reportOverpassSuccess(); return elements.whereType>().map((e) { return OsmCameraNode( diff --git a/lib/services/map_data_submodules/tiles_from_remote.dart b/lib/services/map_data_submodules/tiles_from_remote.dart index e22b14d..49a52f2 100644 --- a/lib/services/map_data_submodules/tiles_from_remote.dart +++ b/lib/services/map_data_submodules/tiles_from_remote.dart @@ -12,12 +12,13 @@ final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent /// Clear queued tile requests when map view changes significantly void clearRemoteTileQueue() { final clearedCount = _tileFetchSemaphore.clearQueue(); - debugPrint('[RemoteTiles] Cleared $clearedCount queued tile requests'); + // Only log if we actually cleared something significant + if (clearedCount > 5) { + debugPrint('[RemoteTiles] Cleared $clearedCount queued tile requests'); + } } -/// Legacy alias for backward compatibility -@Deprecated('Use clearRemoteTileQueue instead') -void clearOSMTileQueue() => clearRemoteTileQueue(); + /// Fetches a tile from any remote provider, with in-memory retries/backoff, and global concurrency limit. /// Returns tile image bytes, or throws on persistent failure. @@ -50,11 +51,11 @@ Future> fetchRemoteTile({ if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) { // Success - no logging for normal operation - NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now + NetworkStatus.instance.reportOsmTileSuccess(); // Generic tile server reporting return resp.bodyBytes; } else { debugPrint('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}'); - NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now + NetworkStatus.instance.reportOsmTileIssue(); // Generic tile server reporting throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}'); } } catch (e) { @@ -62,7 +63,7 @@ Future> fetchRemoteTile({ if (e.toString().contains('Connection refused') || e.toString().contains('Connection timed out') || e.toString().contains('Connection reset')) { - NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now + NetworkStatus.instance.reportOsmTileIssue(); // Generic tile server reporting } if (attempt >= maxAttempts) { diff --git a/lib/services/network_status.dart b/lib/services/network_status.dart index e5fa97a..0680a2c 100644 --- a/lib/services/network_status.dart +++ b/lib/services/network_status.dart @@ -44,12 +44,12 @@ class NetworkStatus extends ChangeNotifier { return null; } - /// Report OSM tile server issues + /// Report tile server issues (for any provider) void reportOsmTileIssue() { if (!_osmTilesHaveIssues) { _osmTilesHaveIssues = true; notifyListeners(); - debugPrint('[NetworkStatus] OSM tile server issues detected'); + debugPrint('[NetworkStatus] Tile server issues detected'); } // Reset recovery timer - if we keep getting errors, keep showing indicator @@ -57,7 +57,7 @@ class NetworkStatus extends ChangeNotifier { _osmRecoveryTimer = Timer(const Duration(minutes: 2), () { _osmTilesHaveIssues = false; notifyListeners(); - debugPrint('[NetworkStatus] OSM tile server issues cleared'); + debugPrint('[NetworkStatus] Tile server issues cleared'); }); } @@ -82,7 +82,7 @@ class NetworkStatus extends ChangeNotifier { void reportOsmTileSuccess() { // Clear issues immediately on success (they were likely temporary) if (_osmTilesHaveIssues) { - debugPrint('[NetworkStatus] OSM tile server issues cleared after success'); + // Quietly clear - don't log routine success _osmTilesHaveIssues = false; _osmRecoveryTimer?.cancel(); notifyListeners(); @@ -91,7 +91,7 @@ class NetworkStatus extends ChangeNotifier { void reportOverpassSuccess() { if (_overpassHasIssues) { - debugPrint('[NetworkStatus] Overpass API issues cleared after success'); + // Quietly clear - don't log routine success _overpassHasIssues = false; _overpassRecoveryTimer?.cancel(); notifyListeners(); @@ -109,7 +109,7 @@ class NetworkStatus extends ChangeNotifier { if (!_isWaitingForData) { _isWaitingForData = true; notifyListeners(); - debugPrint('[NetworkStatus] Waiting for data...'); + // Don't log routine waiting - only log if we stay waiting too long } // Set timeout to show appropriate status after reasonable time @@ -140,7 +140,7 @@ class NetworkStatus extends ChangeNotifier { _waitingTimer?.cancel(); _noDataResetTimer?.cancel(); notifyListeners(); - debugPrint('[NetworkStatus] Waiting/timeout/no-data status cleared - data arrived'); + // Quietly clear waiting status - don't log routine data arrival } } diff --git a/lib/widgets/camera_tag_sheet.dart b/lib/widgets/camera_tag_sheet.dart index bee8657..69980cf 100644 --- a/lib/widgets/camera_tag_sheet.dart +++ b/lib/widgets/camera_tag_sheet.dart @@ -11,10 +11,11 @@ class CameraTagSheet extends StatelessWidget { return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text('Camera #${node.id}', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 12), @@ -55,6 +56,7 @@ class CameraTagSheet extends StatelessWidget { ), ], ), + ), ), ); } diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 41615cc..9acc440 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -55,8 +55,11 @@ class MapViewState extends State { // Track zoom to clear queue on zoom changes double? _lastZoom; - // Track tile type changes to clear cache + // Track changes that require cache clearing String? _lastTileTypeId; + bool? _lastOfflineMode; + int _mapRebuildKey = 0; + bool _shouldClearCache = false; @override void initState() { @@ -126,6 +129,10 @@ class MapViewState extends State { ); } + + + + @override void didUpdateWidget(covariant MapView oldWidget) { super.didUpdateWidget(oldWidget); @@ -176,13 +183,38 @@ class MapViewState extends State { /// Build tile layer - uses standard URL that SimpleTileHttpClient can parse Widget _buildTileLayer(AppState appState) { + final selectedTileType = appState.selectedTileType; + final selectedProvider = appState.selectedTileProvider; + final offlineMode = appState.offlineMode; + + // Create a unique cache key that includes provider, tile type, and offline mode + // This ensures different providers/modes have separate cache entries + String generateTileKey(String url) { + final providerKey = selectedProvider?.id ?? 'unknown'; + final typeKey = selectedTileType?.id ?? 'unknown'; + final modeKey = offlineMode ? 'offline' : 'online'; + return '$providerKey-$typeKey-$modeKey-$url'; + } + // Use a generic URL template that SimpleTileHttpClient recognizes // The actual provider URL will be built by MapDataProvider using current AppState + // Create a completely fresh HTTP client when providers change + // This should bypass any caching at the HTTP client level + final httpClient = _shouldClearCache + ? SimpleTileHttpClient() // Fresh instance + : _tileHttpClient; // Reuse existing + + if (_shouldClearCache) { + debugPrint('[MapView] Creating fresh HTTP client to bypass cache'); + } + return TileLayer( - urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png', + urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png?provider=${selectedProvider?.id}&type=${selectedTileType?.id}&mode=${offlineMode ? 'offline' : 'online'}', userAgentPackageName: 'com.stopflock.flock_map_app', tileProvider: NetworkTileProvider( - httpClient: _tileHttpClient, + httpClient: httpClient, + // Also disable flutter_map caching + cachingProvider: const DisabledMapCachingProvider(), ), ); } @@ -208,16 +240,28 @@ class MapViewState extends State { }); } - // Check if tile type changed and clear cache if needed + // Check if tile type OR offline mode changed and clear cache if needed final currentTileTypeId = appState.selectedTileType?.id; - if (_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) { + final currentOfflineMode = appState.offlineMode; + + if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) || + (_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) { + // Force map rebuild with new key and destroy cache + _mapRebuildKey++; + _shouldClearCache = true; + final reason = _lastTileTypeId != currentTileTypeId + ? 'tile type ($currentTileTypeId)' + : 'offline mode ($currentOfflineMode)'; + debugPrint('[MapView] *** CACHE CLEAR *** $reason changed - destroying cache $_mapRebuildKey'); WidgetsBinding.instance.addPostFrameCallback((_) { - // Clear our tile request queue + debugPrint('[MapView] Post-frame: Clearing tile request queue'); _tileHttpClient.clearTileQueue(); - // Note: The ValueKey on FlutterMap will cause flutter_map to rebuild and clear its cache + _shouldClearCache = false; }); } + _lastTileTypeId = currentTileTypeId; + _lastOfflineMode = currentOfflineMode; // Seed add‑mode target once, after first controller center is available. if (session != null && session.target == null) { @@ -267,7 +311,7 @@ class MapViewState extends State { return Stack( children: [ FlutterMap( - key: ValueKey('map_offline_${appState.offlineMode}_tiletype_${appState.selectedTileType?.id ?? 'none'}'), + key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_$_mapRebuildKey'), mapController: _controller, options: MapOptions( initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194), @@ -289,7 +333,7 @@ class MapViewState extends State { if (zoomChanged) { _tileDebounce(() { - debugPrint('[MapView] Zoom change detected - clearing stale tile requests'); + // Clear stale tile requests on zoom change (quietly) _tileHttpClient.clearTileQueue(); }); }