From a2bc3309c0ca23c14a5a4a54171ff0422e700cdf Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 23 Aug 2025 12:27:04 -0500 Subject: [PATCH] chasing excess tile fetching and lack of correct cache clearing - NOT WORKING --- .../map_data_submodules/tiles_from_osm.dart | 54 +++++++++++++++++++ lib/services/network_status.dart | 18 +++---- lib/widgets/map_view.dart | 16 +++--- lib/widgets/tile_provider_with_cache.dart | 18 +++++-- 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/lib/services/map_data_submodules/tiles_from_osm.dart b/lib/services/map_data_submodules/tiles_from_osm.dart index 983fd8d..461ac7b 100644 --- a/lib/services/map_data_submodules/tiles_from_osm.dart +++ b/lib/services/map_data_submodules/tiles_from_osm.dart @@ -9,6 +9,17 @@ import '../network_status.dart'; /// Global semaphore to limit simultaneous tile fetches final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent +/// Cancellation token to invalidate all pending requests +int _globalCancelToken = 0; + +/// Clear queued tile requests and cancel all retries +void clearOSMTileQueue() { + final oldToken = _globalCancelToken; + _globalCancelToken++; // Invalidate all pending requests and retries + final clearedCount = _tileFetchSemaphore.clearQueue(); + debugPrint('[OSMTiles] Cancel token: $oldToken -> $_globalCancelToken, cleared $clearedCount queued'); +} + /// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit. /// Returns tile image bytes, or throws on persistent failure. Future> fetchOSMTile({ @@ -25,13 +36,31 @@ Future> fetchOSMTile({ kTileFetchSecondDelayMs + random.nextInt(kTileFetchJitter2Ms), kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms), ]; + + // Remember the cancel token when we start this request + final requestCancelToken = _globalCancelToken; + print('[fetchOSMTile] START $z/$x/$y with token $requestCancelToken (global: $_globalCancelToken)'); + while (true) { + // Check if this request was cancelled + if (requestCancelToken != _globalCancelToken) { + print('[fetchOSMTile] CANCELLED $z/$x/$y (token: $requestCancelToken vs $_globalCancelToken)'); + throw Exception('Tile request cancelled'); + } + await _tileFetchSemaphore.acquire(); try { print('[fetchOSMTile] FETCH $z/$x/$y'); attempt++; final resp = await http.get(Uri.parse(url)); print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}'); + + // Check cancellation after HTTP request completes - this is the key check! + if (requestCancelToken != _globalCancelToken) { + print('[fetchOSMTile] CANCELLED $z/$x/$y after HTTP (token: $requestCancelToken vs $_globalCancelToken)'); + throw Exception('Tile request cancelled'); + } + if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) { print('[fetchOSMTile] SUCCESS $z/$x/$y'); NetworkStatus.instance.reportOsmTileSuccess(); @@ -42,6 +71,11 @@ Future> fetchOSMTile({ throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}'); } } catch (e) { + // Don't retry cancelled requests + if (e.toString().contains('cancelled')) { + rethrow; + } + print('[fetchOSMTile] Exception $z/$x/$y: $e'); // Report network issues on connection errors @@ -55,9 +89,22 @@ Future> fetchOSMTile({ print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e"); rethrow; } + final delay = delays[attempt - 1].clamp(0, 60000); print("[fetchOSMTile] Attempt $attempt for $z/$x/$y failed: $e. Retrying in ${delay}ms."); + + // Check cancellation before and after delay + if (requestCancelToken != _globalCancelToken) { + print('[fetchOSMTile] CANCELLED $z/$x/$y before retry'); + throw Exception('Tile request cancelled'); + } + await Future.delayed(Duration(milliseconds: delay)); + + if (requestCancelToken != _globalCancelToken) { + print('[fetchOSMTile] CANCELLED $z/$x/$y after retry delay'); + throw Exception('Tile request cancelled'); + } } finally { _tileFetchSemaphore.release(); } @@ -90,4 +137,11 @@ class _SimpleSemaphore { _current--; } } + + /// Clear all queued requests (call when view changes significantly) + int clearQueue() { + final clearedCount = _queue.length; + _queue.clear(); + return clearedCount; + } } \ No newline at end of file diff --git a/lib/services/network_status.dart b/lib/services/network_status.dart index 78088d7..a36e73e 100644 --- a/lib/services/network_status.dart +++ b/lib/services/network_status.dart @@ -60,25 +60,21 @@ class NetworkStatus extends ChangeNotifier { /// Report successful operations to potentially clear issues faster void reportOsmTileSuccess() { - // Don't immediately clear on single success, but reduce recovery time + // Clear issues immediately on success (they were likely temporary) if (_osmTilesHaveIssues) { + debugPrint('[NetworkStatus] OSM tile server issues cleared after success'); + _osmTilesHaveIssues = false; _osmRecoveryTimer?.cancel(); - _osmRecoveryTimer = Timer(const Duration(seconds: 30), () { - _osmTilesHaveIssues = false; - notifyListeners(); - debugPrint('[NetworkStatus] OSM tile server issues cleared after success'); - }); + notifyListeners(); } } void reportOverpassSuccess() { if (_overpassHasIssues) { + debugPrint('[NetworkStatus] Overpass API issues cleared after success'); + _overpassHasIssues = false; _overpassRecoveryTimer?.cancel(); - _overpassRecoveryTimer = Timer(const Duration(seconds: 30), () { - _overpassHasIssues = false; - notifyListeners(); - debugPrint('[NetworkStatus] Overpass API issues cleared after success'); - }); + notifyListeners(); } } diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index c19a101..7654d6b 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -15,6 +15,7 @@ import 'camera_provider_with_cache.dart'; import 'map/camera_markers.dart'; import 'map/direction_cones.dart'; import 'map/map_overlays.dart'; +import 'network_status_indicator.dart'; import '../dev_config.dart'; class MapView extends StatefulWidget { @@ -95,6 +96,7 @@ class _MapViewState extends State { void _onTilesCached() { // When new tiles are cached, just trigger a widget rebuild // This should cause the TileLayer to re-render with cached tiles + debugPrint('[MapView] Tile cached callback triggered, calling setState'); if (mounted) { setState(() {}); } @@ -176,12 +178,6 @@ class _MapViewState extends State { return ids1.length == ids2.length && ids1.containsAll(ids2); } - - - - - - @override Widget build(BuildContext context) { final appState = context.watch(); @@ -270,6 +266,11 @@ class _MapViewState extends State { if (session != null) { appState.updateSession(target: pos.center); } + + // Simple approach: cancel tiles on ANY significant view change + final tileProvider = Provider.of(context, listen: false); + tileProvider.cancelAllTileRequests(); + // Request more cameras on any map movement/zoom at valid zoom level // This ensures cameras load even when zooming without panning (like with zoom buttons) if (pos.zoom >= 10) { @@ -323,6 +324,9 @@ class _MapViewState extends State { uploadMode: appState.uploadMode, session: session, ), + + // Network status indicator (top-left) + const NetworkStatusIndicator(), ], ); } diff --git a/lib/widgets/tile_provider_with_cache.dart b/lib/widgets/tile_provider_with_cache.dart index 0b304d7..56fb2a9 100644 --- a/lib/widgets/tile_provider_with_cache.dart +++ b/lib/widgets/tile_provider_with_cache.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import '../services/map_data_provider.dart'; +import '../services/map_data_submodules/tiles_from_osm.dart'; /// In-memory tile cache and async provider for custom tiles. class TileProviderWithCache extends TileProvider with ChangeNotifier { @@ -11,7 +12,7 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { bool _disposed = false; int _disposeCount = 0; VoidCallback? _onTilesCachedCallback; - + TileProviderWithCache(); /// Set a callback to be called when tiles are cached (used by MapView for refresh) @@ -19,6 +20,12 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { _onTilesCachedCallback = callback; } + /// Cancel ALL pending tile requests - delegates to OSM tile fetcher + void cancelAllTileRequests() { + clearOSMTileQueue(); // This handles all the cancellation logic + debugPrint('[TileProviderWithCache] Cancelled all tile requests'); + } + @override void dispose() { _disposeCount++; @@ -73,12 +80,17 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { if (!_disposed && hasListeners) { notifyListeners(); // This updates any listening widgets } - // Trigger map refresh callback to force tile re-rendering + // Trigger map refresh callback to force tile re-rendering + debugPrint('[TileProviderWithCache] Tile cached: $key, calling refresh callback'); _onTilesCachedCallback?.call(); } // If bytes were empty, don't cache (will re-attempt next time) } catch (e) { - // Do NOT cache a failed or empty tile! Placeholder tiles will be evicted on online transition. + // Cancelled requests will throw exceptions from fetchOSMTile(), just ignore them + if (e.toString().contains('cancelled')) { + debugPrint('[TileProviderWithCache] Tile request was cancelled: $key'); + } + // Don't cache failed tiles regardless of reason } } }