mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-09-28 12:02:02 +02:00
genericize tiles_from submodule, simpletileservice.
This commit is contained in:
@@ -5,7 +5,7 @@ import '../models/camera_profile.dart';
|
|||||||
import '../models/osm_camera_node.dart';
|
import '../models/osm_camera_node.dart';
|
||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
import 'map_data_submodules/cameras_from_overpass.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/cameras_from_local.dart';
|
||||||
import 'map_data_submodules/tiles_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)
|
/// Clear any queued tile requests (call when map view changes significantly)
|
||||||
void clearTileQueue() {
|
void clearTileQueue() {
|
||||||
clearOSMTileQueue();
|
clearRemoteTileQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+37
-16
@@ -10,19 +10,23 @@ import '../network_status.dart';
|
|||||||
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
||||||
|
|
||||||
/// Clear queued tile requests when map view changes significantly
|
/// Clear queued tile requests when map view changes significantly
|
||||||
void clearOSMTileQueue() {
|
void clearRemoteTileQueue() {
|
||||||
final clearedCount = _tileFetchSemaphore.clearQueue();
|
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.
|
/// Returns tile image bytes, or throws on persistent failure.
|
||||||
Future<List<int>> fetchOSMTile({
|
Future<List<int>> fetchRemoteTile({
|
||||||
required int z,
|
required int z,
|
||||||
required int x,
|
required int x,
|
||||||
required int y,
|
required int y,
|
||||||
|
required String url,
|
||||||
}) async {
|
}) async {
|
||||||
final url = 'https://tile.openstreetmap.org/$z/$x/$y.png';
|
|
||||||
const int maxAttempts = kTileFetchMaxAttempts;
|
const int maxAttempts = kTileFetchMaxAttempts;
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
final random = Random();
|
final random = Random();
|
||||||
@@ -32,40 +36,42 @@ Future<List<int>> fetchOSMTile({
|
|||||||
kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms),
|
kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
final hostInfo = Uri.parse(url).host; // For logging
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await _tileFetchSemaphore.acquire();
|
await _tileFetchSemaphore.acquire();
|
||||||
try {
|
try {
|
||||||
print('[fetchOSMTile] FETCH $z/$x/$y');
|
print('[fetchRemoteTile] FETCH $z/$x/$y from $hostInfo');
|
||||||
attempt++;
|
attempt++;
|
||||||
final resp = await http.get(Uri.parse(url));
|
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) {
|
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||||
print('[fetchOSMTile] SUCCESS $z/$x/$y');
|
print('[fetchRemoteTile] SUCCESS $z/$x/$y from $hostInfo');
|
||||||
NetworkStatus.instance.reportOsmTileSuccess();
|
NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now
|
||||||
return resp.bodyBytes;
|
return resp.bodyBytes;
|
||||||
} else {
|
} else {
|
||||||
print('[fetchOSMTile] FAIL $z/$x/$y: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
print('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
||||||
NetworkStatus.instance.reportOsmTileIssue();
|
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
||||||
throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
|
throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[fetchOSMTile] Exception $z/$x/$y: $e');
|
print('[fetchRemoteTile] Exception $z/$x/$y from $hostInfo: $e');
|
||||||
|
|
||||||
// Report network issues on connection errors
|
// Report network issues on connection errors
|
||||||
if (e.toString().contains('Connection refused') ||
|
if (e.toString().contains('Connection refused') ||
|
||||||
e.toString().contains('Connection timed out') ||
|
e.toString().contains('Connection timed out') ||
|
||||||
e.toString().contains('Connection reset')) {
|
e.toString().contains('Connection reset')) {
|
||||||
NetworkStatus.instance.reportOsmTileIssue();
|
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt >= maxAttempts) {
|
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;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
final delay = delays[attempt - 1].clamp(0, 60000);
|
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));
|
await Future.delayed(Duration(milliseconds: delay));
|
||||||
} finally {
|
} finally {
|
||||||
_tileFetchSemaphore.release();
|
_tileFetchSemaphore.release();
|
||||||
@@ -73,6 +79,21 @@ Future<List<int>> fetchOSMTile({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Legacy function for backward compatibility
|
||||||
|
@Deprecated('Use fetchRemoteTile instead')
|
||||||
|
Future<List<int>> 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
|
/// Simple counting semaphore, suitable for single-thread Flutter concurrency
|
||||||
class _SimpleSemaphore {
|
class _SimpleSemaphore {
|
||||||
final int _max;
|
final int _max;
|
||||||
@@ -13,35 +13,65 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||||
// Only intercept tile requests to OSM (for now - other providers pass through)
|
// Try to parse as a tile request from any provider
|
||||||
if (request.url.host == 'tile.openstreetmap.org') {
|
final tileInfo = _parseTileRequest(request.url);
|
||||||
return _handleTileRequest(request);
|
if (tileInfo != null) {
|
||||||
|
return _handleTileRequest(request, tileInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass through all other requests (Google, Mapbox, etc.)
|
// Pass through non-tile requests
|
||||||
return _inner.send(request);
|
return _inner.send(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<http.StreamedResponse> _handleTileRequest(http.BaseRequest request) async {
|
/// Parse URL to extract tile coordinates if it looks like a tile request
|
||||||
final pathSegments = request.url.pathSegments;
|
Map<String, dynamic>? _parseTileRequest(Uri url) {
|
||||||
|
final pathSegments = url.pathSegments;
|
||||||
|
|
||||||
// Parse z/x/y from URL like: /15/5242/12666.png
|
// Common patterns for tile URLs:
|
||||||
if (pathSegments.length == 3) {
|
// OSM: /z/x/y.png
|
||||||
final z = int.tryParse(pathSegments[0]);
|
// Google: /vt/lyrs=y&x=x&y=y&z=z (query params)
|
||||||
final x = int.tryParse(pathSegments[1]);
|
// Mapbox: /styles/v1/mapbox/streets-v12/tiles/z/x/y
|
||||||
final yPng = pathSegments[2];
|
// ArcGIS: /tile/z/y/x.png
|
||||||
final y = int.tryParse(yPng.replaceAll('.png', ''));
|
|
||||||
|
// Try query parameters first (Google style)
|
||||||
if (z != null && x != null && y != null) {
|
final query = url.queryParameters;
|
||||||
return _getTile(z, x, y);
|
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
|
// Try path-based patterns
|
||||||
return _inner.send(request);
|
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<http.StreamedResponse> _getTile(int z, int x, int y) async {
|
Future<http.StreamedResponse> _handleTileRequest(http.BaseRequest request, Map<String, dynamic> 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<http.StreamedResponse> _getTile(int z, int x, int y, String originalUrl, String providerHost) async {
|
||||||
try {
|
try {
|
||||||
// First try to get tile from offline storage
|
// First try to get tile from offline storage
|
||||||
final localTileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.local);
|
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
|
// Check if we're in offline mode
|
||||||
if (AppState.instance.offlineMode) {
|
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
|
// Report that we couldn't serve this tile offline
|
||||||
NetworkStatus.instance.reportOfflineMiss();
|
NetworkStatus.instance.reportOfflineMiss();
|
||||||
return http.StreamedResponse(
|
return http.StreamedResponse(
|
||||||
@@ -79,17 +109,17 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're online - try OSM with proper error handling
|
// We're online - try the original provider with proper error handling
|
||||||
debugPrint('[SimpleTileService] Online mode - trying OSM for $z/$x/$y');
|
debugPrint('[SimpleTileService] Online mode - trying $providerHost for $z/$x/$y');
|
||||||
try {
|
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
|
// Clear waiting status on successful network tile
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
NetworkStatus.instance.clearWaiting();
|
NetworkStatus.instance.clearWaiting();
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (networkError) {
|
} 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 404 instead of throwing - let flutter_map handle gracefully
|
||||||
return http.StreamedResponse(
|
return http.StreamedResponse(
|
||||||
Stream.value(<int>[]),
|
Stream.value(<int>[]),
|
||||||
|
|||||||
Reference in New Issue
Block a user