diff --git a/lib/services/map_data_provider.dart b/lib/services/map_data_provider.dart index 5cb6c8d..6dea030 100644 --- a/lib/services/map_data_provider.dart +++ b/lib/services/map_data_provider.dart @@ -5,7 +5,7 @@ import '../models/camera_profile.dart'; import '../models/osm_camera_node.dart'; import '../app_state.dart'; import 'map_data_submodules/cameras_from_overpass.dart'; -import 'map_data_submodules/tiles_from_osm.dart'; +import 'map_data_submodules/tiles_from_remote.dart'; import 'map_data_submodules/cameras_from_local.dart'; import 'map_data_submodules/tiles_from_local.dart'; @@ -147,6 +147,6 @@ class MapDataProvider { /// Clear any queued tile requests (call when map view changes significantly) void clearTileQueue() { - clearOSMTileQueue(); + clearRemoteTileQueue(); } } \ No newline at end of file diff --git a/lib/services/map_data_submodules/tiles_from_osm.dart b/lib/services/map_data_submodules/tiles_from_remote.dart similarity index 58% rename from lib/services/map_data_submodules/tiles_from_osm.dart rename to lib/services/map_data_submodules/tiles_from_remote.dart index c472932..c742846 100644 --- a/lib/services/map_data_submodules/tiles_from_osm.dart +++ b/lib/services/map_data_submodules/tiles_from_remote.dart @@ -10,19 +10,23 @@ import '../network_status.dart'; final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent /// Clear queued tile requests when map view changes significantly -void clearOSMTileQueue() { +void clearRemoteTileQueue() { final clearedCount = _tileFetchSemaphore.clearQueue(); - debugPrint('[OSMTiles] Cleared $clearedCount queued tile requests'); + debugPrint('[RemoteTiles] Cleared $clearedCount queued tile requests'); } -/// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit. +/// 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. -Future> fetchOSMTile({ +Future> fetchRemoteTile({ required int z, required int x, required int y, + required String url, }) async { - final url = 'https://tile.openstreetmap.org/$z/$x/$y.png'; const int maxAttempts = kTileFetchMaxAttempts; int attempt = 0; final random = Random(); @@ -32,40 +36,42 @@ Future> fetchOSMTile({ kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms), ]; + final hostInfo = Uri.parse(url).host; // For logging + while (true) { await _tileFetchSemaphore.acquire(); try { - print('[fetchOSMTile] FETCH $z/$x/$y'); + print('[fetchRemoteTile] FETCH $z/$x/$y from $hostInfo'); attempt++; final resp = await http.get(Uri.parse(url)); - print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}'); + print('[fetchRemoteTile] HTTP ${resp.statusCode} for $z/$x/$y from $hostInfo, length=${resp.bodyBytes.length}'); if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) { - print('[fetchOSMTile] SUCCESS $z/$x/$y'); - NetworkStatus.instance.reportOsmTileSuccess(); + print('[fetchRemoteTile] SUCCESS $z/$x/$y from $hostInfo'); + NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now return resp.bodyBytes; } else { - print('[fetchOSMTile] FAIL $z/$x/$y: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}'); - NetworkStatus.instance.reportOsmTileIssue(); - throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}'); + print('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}'); + NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now + throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}'); } } catch (e) { - print('[fetchOSMTile] Exception $z/$x/$y: $e'); + print('[fetchRemoteTile] Exception $z/$x/$y from $hostInfo: $e'); // Report network issues on connection errors if (e.toString().contains('Connection refused') || e.toString().contains('Connection timed out') || e.toString().contains('Connection reset')) { - NetworkStatus.instance.reportOsmTileIssue(); + NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now } if (attempt >= maxAttempts) { - print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e"); + print("[fetchRemoteTile] Failed for $z/$x/$y from $hostInfo 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."); + print("[fetchRemoteTile] Attempt $attempt for $z/$x/$y from $hostInfo failed: $e. Retrying in ${delay}ms."); await Future.delayed(Duration(milliseconds: delay)); } finally { _tileFetchSemaphore.release(); @@ -73,6 +79,21 @@ Future> fetchOSMTile({ } } +/// Legacy function for backward compatibility +@Deprecated('Use fetchRemoteTile instead') +Future> fetchOSMTile({ + required int z, + required int x, + required int y, +}) async { + return fetchRemoteTile( + z: z, + x: x, + y: y, + url: 'https://tile.openstreetmap.org/$z/$x/$y.png', + ); +} + /// Simple counting semaphore, suitable for single-thread Flutter concurrency class _SimpleSemaphore { final int _max; diff --git a/lib/services/simple_tile_service.dart b/lib/services/simple_tile_service.dart index 1ff7a56..6939622 100644 --- a/lib/services/simple_tile_service.dart +++ b/lib/services/simple_tile_service.dart @@ -13,35 +13,65 @@ class SimpleTileHttpClient extends http.BaseClient { @override Future send(http.BaseRequest request) async { - // Only intercept tile requests to OSM (for now - other providers pass through) - if (request.url.host == 'tile.openstreetmap.org') { - return _handleTileRequest(request); + // Try to parse as a tile request from any provider + final tileInfo = _parseTileRequest(request.url); + if (tileInfo != null) { + return _handleTileRequest(request, tileInfo); } - // Pass through all other requests (Google, Mapbox, etc.) + // Pass through non-tile requests return _inner.send(request); } - Future _handleTileRequest(http.BaseRequest request) async { - final pathSegments = request.url.pathSegments; + /// Parse URL to extract tile coordinates if it looks like a tile request + Map? _parseTileRequest(Uri url) { + final pathSegments = url.pathSegments; - // Parse z/x/y from URL like: /15/5242/12666.png - if (pathSegments.length == 3) { - final z = int.tryParse(pathSegments[0]); - final x = int.tryParse(pathSegments[1]); - final yPng = pathSegments[2]; - final y = int.tryParse(yPng.replaceAll('.png', '')); - - if (z != null && x != null && y != null) { - return _getTile(z, x, y); + // Common patterns for tile URLs: + // OSM: /z/x/y.png + // Google: /vt/lyrs=y&x=x&y=y&z=z (query params) + // Mapbox: /styles/v1/mapbox/streets-v12/tiles/z/x/y + // ArcGIS: /tile/z/y/x.png + + // Try query parameters first (Google style) + final query = url.queryParameters; + if (query.containsKey('x') && query.containsKey('y') && query.containsKey('z')) { + final x = int.tryParse(query['x']!); + final y = int.tryParse(query['y']!); + final z = int.tryParse(query['z']!); + if (x != null && y != null && z != null) { + return {'z': z, 'x': x, 'y': y, 'originalUrl': url.toString()}; } } - // Malformed tile URL - pass through to OSM - return _inner.send(request); + // Try path-based patterns + if (pathSegments.length >= 3) { + // Try z/x/y pattern (OSM style) - can be at different positions + for (int i = 0; i <= pathSegments.length - 3; i++) { + final z = int.tryParse(pathSegments[i]); + final x = int.tryParse(pathSegments[i + 1]); + final yWithExt = pathSegments[i + 2]; + final y = int.tryParse(yWithExt.replaceAll(RegExp(r'\.[^.]*$'), '')); // Remove file extension + + if (z != null && x != null && y != null) { + return {'z': z, 'x': x, 'y': y, 'originalUrl': url.toString()}; + } + } + } + + return null; // Not a recognizable tile request } - Future _getTile(int z, int x, int y) async { + Future _handleTileRequest(http.BaseRequest request, Map tileInfo) async { + final z = tileInfo['z'] as int; + final x = tileInfo['x'] as int; + final y = tileInfo['y'] as int; + final originalUrl = tileInfo['originalUrl'] as String; + + return _getTile(z, x, y, originalUrl, request.url.host); + } + + Future _getTile(int z, int x, int y, String originalUrl, String providerHost) async { try { // First try to get tile from offline storage final localTileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.local); @@ -69,7 +99,7 @@ class SimpleTileHttpClient extends http.BaseClient { // Check if we're in offline mode if (AppState.instance.offlineMode) { - debugPrint('[SimpleTileService] Offline mode - not attempting OSM fetch for $z/$x/$y'); + debugPrint('[SimpleTileService] Offline mode - not attempting $providerHost fetch for $z/$x/$y'); // Report that we couldn't serve this tile offline NetworkStatus.instance.reportOfflineMiss(); return http.StreamedResponse( @@ -79,17 +109,17 @@ class SimpleTileHttpClient extends http.BaseClient { ); } - // We're online - try OSM with proper error handling - debugPrint('[SimpleTileService] Online mode - trying OSM for $z/$x/$y'); + // We're online - try the original provider with proper error handling + debugPrint('[SimpleTileService] Online mode - trying $providerHost for $z/$x/$y'); try { - final response = await _inner.send(http.Request('GET', Uri.parse('https://tile.openstreetmap.org/$z/$x/$y.png'))); + final response = await _inner.send(http.Request('GET', Uri.parse(originalUrl))); // Clear waiting status on successful network tile if (response.statusCode == 200) { NetworkStatus.instance.clearWaiting(); } return response; } catch (networkError) { - debugPrint('[SimpleTileService] OSM request failed for $z/$x/$y: $networkError'); + debugPrint('[SimpleTileService] $providerHost request failed for $z/$x/$y: $networkError'); // Return 404 instead of throwing - let flutter_map handle gracefully return http.StreamedResponse( Stream.value([]),