mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-13 01:03:03 +00:00
Consolidate / dedupe some code
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'models/camera_profile.dart';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/camera_profile.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
@@ -125,7 +126,7 @@ class MapDataProvider {
|
||||
if (offline) {
|
||||
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
|
||||
}
|
||||
return fetchOSMTile(z: z, x: x, y: y);
|
||||
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
||||
}
|
||||
|
||||
// Explicitly local
|
||||
@@ -138,13 +139,29 @@ class MapDataProvider {
|
||||
return await fetchLocalTile(z: z, x: x, y: y);
|
||||
} catch (_) {
|
||||
if (!offline) {
|
||||
return fetchOSMTile(z: z, x: x, y: y);
|
||||
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
||||
} else {
|
||||
throw OfflineModeException("Tile $z/$x/$y not found in offline areas and offline mode is enabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch remote tile using current provider from AppState
|
||||
Future<List<int>> _fetchRemoteTileFromCurrentProvider(int z, int x, int y) async {
|
||||
final appState = AppState.instance;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear any queued tile requests (call when map view changes significantly)
|
||||
void clearTileQueue() {
|
||||
clearRemoteTileQueue();
|
||||
|
||||
@@ -41,23 +41,23 @@ Future<List<int>> fetchRemoteTile({
|
||||
while (true) {
|
||||
await _tileFetchSemaphore.acquire();
|
||||
try {
|
||||
print('[fetchRemoteTile] FETCH $z/$x/$y from $hostInfo');
|
||||
// Only log on first attempt or errors
|
||||
if (attempt == 1) {
|
||||
debugPrint('[fetchRemoteTile] Fetching $z/$x/$y from $hostInfo');
|
||||
}
|
||||
attempt++;
|
||||
final resp = await http.get(Uri.parse(url));
|
||||
print('[fetchRemoteTile] HTTP ${resp.statusCode} for $z/$x/$y from $hostInfo, length=${resp.bodyBytes.length}');
|
||||
|
||||
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||
print('[fetchRemoteTile] SUCCESS $z/$x/$y from $hostInfo');
|
||||
// Success - no logging for normal operation
|
||||
NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now
|
||||
return resp.bodyBytes;
|
||||
} else {
|
||||
print('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
||||
debugPrint('[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('[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') ||
|
||||
@@ -66,12 +66,14 @@ Future<List<int>> fetchRemoteTile({
|
||||
}
|
||||
|
||||
if (attempt >= maxAttempts) {
|
||||
print("[fetchRemoteTile] Failed for $z/$x/$y from $hostInfo after $attempt attempts: $e");
|
||||
debugPrint("[fetchRemoteTile] Failed for $z/$x/$y from $hostInfo after $attempt attempts: $e");
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final delay = delays[attempt - 1].clamp(0, 60000);
|
||||
print("[fetchRemoteTile] Attempt $attempt for $z/$x/$y from $hostInfo failed: $e. Retrying in ${delay}ms.");
|
||||
if (attempt == 1) {
|
||||
debugPrint("[fetchRemoteTile] Attempt $attempt for $z/$x/$y from $hostInfo failed: $e. Retrying in ${delay}ms.");
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
} finally {
|
||||
_tileFetchSemaphore.release();
|
||||
|
||||
@@ -4,13 +4,10 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../app_state.dart';
|
||||
import '../../models/osm_camera_node.dart';
|
||||
import '../../models/tile_provider.dart';
|
||||
import '../map_data_provider.dart';
|
||||
import '../map_data_submodules/tiles_from_remote.dart';
|
||||
import 'offline_area_models.dart';
|
||||
import 'offline_tile_utils.dart';
|
||||
import 'package:flock_map_app/dev_config.dart';
|
||||
@@ -30,32 +27,6 @@ class OfflineAreaDownloader {
|
||||
required Future<void> Function() saveAreasToDisk,
|
||||
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
||||
}) async {
|
||||
// Get tile provider info from the area metadata or current AppState
|
||||
TileProvider? tileProvider;
|
||||
TileType? tileType;
|
||||
|
||||
final appState = AppState.instance;
|
||||
|
||||
if (area.tileProviderId != null && area.tileTypeId != null) {
|
||||
// Use the provider info stored with the area (for refreshing existing areas)
|
||||
try {
|
||||
tileProvider = appState.tileProviders.firstWhere(
|
||||
(p) => p.id == area.tileProviderId,
|
||||
);
|
||||
tileType = tileProvider.tileTypes.firstWhere(
|
||||
(t) => t.id == area.tileTypeId,
|
||||
);
|
||||
} catch (e) {
|
||||
// Fallback if stored provider/type not found
|
||||
tileProvider = appState.selectedTileProvider ?? appState.tileProviders.firstOrNull;
|
||||
tileType = appState.selectedTileType ?? tileProvider?.tileTypes.firstOrNull;
|
||||
}
|
||||
} else {
|
||||
// New area - use currently selected provider
|
||||
tileProvider = appState.selectedTileProvider ?? appState.tileProviders.firstOrNull;
|
||||
tileType = appState.selectedTileType ?? tileProvider?.tileTypes.firstOrNull;
|
||||
}
|
||||
// Calculate tiles to download
|
||||
Set<List<int>> allTiles;
|
||||
if (area.isPermanent) {
|
||||
allTiles = computeTileList(globalWorldBounds(), kWorldMinZoom, kWorldMaxZoom);
|
||||
@@ -72,8 +43,6 @@ class OfflineAreaDownloader {
|
||||
onProgress: onProgress,
|
||||
saveAreasToDisk: saveAreasToDisk,
|
||||
getAreaSizeBytes: getAreaSizeBytes,
|
||||
tileProvider: tileProvider,
|
||||
tileType: tileType,
|
||||
);
|
||||
|
||||
// Download cameras for non-permanent areas
|
||||
@@ -99,8 +68,6 @@ class OfflineAreaDownloader {
|
||||
void Function(double progress)? onProgress,
|
||||
required Future<void> Function() saveAreasToDisk,
|
||||
required Future<void> Function(OfflineArea) getAreaSizeBytes,
|
||||
TileProvider? tileProvider,
|
||||
TileType? tileType,
|
||||
}) async {
|
||||
int pass = 0;
|
||||
Set<List<int>> tilesToFetch = allTiles;
|
||||
@@ -113,7 +80,7 @@ class OfflineAreaDownloader {
|
||||
for (final tile in tilesToFetch) {
|
||||
if (area.status == OfflineAreaStatus.cancelled) break;
|
||||
|
||||
if (await _downloadSingleTile(tile, directory, area, tileProvider, tileType)) {
|
||||
if (await _downloadSingleTile(tile, directory, area)) {
|
||||
totalDone++;
|
||||
area.tilesDownloaded = totalDone;
|
||||
area.progress = area.tilesTotal == 0 ? 0.0 : (totalDone / area.tilesTotal);
|
||||
@@ -134,25 +101,20 @@ class OfflineAreaDownloader {
|
||||
return false; // Failed after max retries
|
||||
}
|
||||
|
||||
/// Download a single tile
|
||||
/// Download a single tile using the unified MapDataProvider path
|
||||
static Future<bool> _downloadSingleTile(
|
||||
List<int> tile,
|
||||
String directory,
|
||||
OfflineArea area,
|
||||
TileProvider? tileProvider,
|
||||
TileType? tileType,
|
||||
) async {
|
||||
try {
|
||||
List<int> bytes;
|
||||
|
||||
if (tileType != null && tileProvider != null) {
|
||||
// Use the same path as live tiles: build URL and fetch directly
|
||||
final tileUrl = tileType.getTileUrl(tile[0], tile[1], tile[2], apiKey: tileProvider.apiKey);
|
||||
bytes = await fetchRemoteTile(z: tile[0], x: tile[1], y: tile[2], url: tileUrl);
|
||||
} else {
|
||||
// Fallback to OSM for legacy areas or when no provider info
|
||||
bytes = await fetchOSMTile(z: tile[0], x: tile[1], y: tile[2]);
|
||||
}
|
||||
// Use the same unified path as live tiles: always go through MapDataProvider
|
||||
final bytes = await MapDataProvider().getTile(
|
||||
z: tile[0],
|
||||
x: tile[1],
|
||||
y: tile[2],
|
||||
source: MapSource.remote, // Force remote fetch for downloads
|
||||
);
|
||||
if (bytes.isNotEmpty) {
|
||||
await OfflineAreaDownloader.saveTileBytes(tile[0], tile[1], tile[2], directory, bytes);
|
||||
return true;
|
||||
|
||||
@@ -13,77 +13,50 @@ class SimpleTileHttpClient extends http.BaseClient {
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
// Try to parse as a tile request from any provider
|
||||
final tileInfo = _parseTileRequest(request.url);
|
||||
if (tileInfo != null) {
|
||||
return _handleTileRequest(request, tileInfo);
|
||||
// Extract tile coordinates from the URL using our standard pattern
|
||||
final tileCoords = _extractTileCoords(request.url);
|
||||
if (tileCoords != null) {
|
||||
final z = tileCoords['z']!; // We know these are not null from _extractTileCoords
|
||||
final x = tileCoords['x']!;
|
||||
final y = tileCoords['y']!;
|
||||
return _handleTileRequest(z, x, y);
|
||||
}
|
||||
|
||||
// Pass through non-tile requests
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
/// Parse URL to extract tile coordinates if it looks like a tile request
|
||||
Map<String, dynamic>? _parseTileRequest(Uri url) {
|
||||
/// Extract z/x/y coordinates from our standard tile URL pattern
|
||||
Map<String, int>? _extractTileCoords(Uri url) {
|
||||
// We'll use a simple standard pattern: /{z}/{x}/{y}.png
|
||||
// This will be the format we use in map_view.dart
|
||||
final pathSegments = url.pathSegments;
|
||||
|
||||
// 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()};
|
||||
}
|
||||
}
|
||||
|
||||
// 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<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 {
|
||||
// First try to get tile from offline storage
|
||||
final localTileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.local);
|
||||
if (pathSegments.length == 3) {
|
||||
final z = int.tryParse(pathSegments[0]);
|
||||
final x = int.tryParse(pathSegments[1]);
|
||||
final yWithExt = pathSegments[2];
|
||||
final y = int.tryParse(yWithExt.replaceAll(RegExp(r'\.[^.]*$'), '')); // Remove .png
|
||||
|
||||
debugPrint('[SimpleTileService] Serving tile $z/$x/$y from offline storage');
|
||||
if (z != null && x != null && y != null) {
|
||||
return {'z': z, 'x': x, 'y': y};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<http.StreamedResponse> _handleTileRequest(int z, int x, int y) async {
|
||||
try {
|
||||
// Always go through MapDataProvider - it handles offline/online routing
|
||||
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.auto);
|
||||
|
||||
// Clear waiting status - we got data
|
||||
NetworkStatus.instance.clearWaiting();
|
||||
|
||||
// Serve offline tile with proper cache headers
|
||||
// Serve tile with proper cache headers
|
||||
return http.StreamedResponse(
|
||||
Stream.value(localTileBytes),
|
||||
Stream.value(tileBytes),
|
||||
200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
@@ -94,39 +67,15 @@ class SimpleTileHttpClient extends http.BaseClient {
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
// No offline tile available
|
||||
debugPrint('[SimpleTileService] No offline tile for $z/$x/$y');
|
||||
debugPrint('[SimpleTileService] Could not get tile $z/$x/$y: $e');
|
||||
|
||||
// Check if we're in offline mode
|
||||
if (AppState.instance.offlineMode) {
|
||||
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(
|
||||
Stream.value(<int>[]),
|
||||
404,
|
||||
reasonPhrase: 'Tile not available offline',
|
||||
);
|
||||
}
|
||||
|
||||
// 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(originalUrl)));
|
||||
// Clear waiting status on successful network tile
|
||||
if (response.statusCode == 200) {
|
||||
NetworkStatus.instance.clearWaiting();
|
||||
}
|
||||
return response;
|
||||
} catch (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(<int>[]),
|
||||
404,
|
||||
reasonPhrase: 'Network tile unavailable: $networkError',
|
||||
);
|
||||
}
|
||||
// Let MapDataProvider handle offline mode logic
|
||||
// Just return 404 and let flutter_map handle it gracefully
|
||||
return http.StreamedResponse(
|
||||
Stream.value(<int>[]),
|
||||
404,
|
||||
reasonPhrase: 'Tile unavailable: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
// Track zoom to clear queue on zoom changes
|
||||
double? _lastZoom;
|
||||
|
||||
// Track tile type changes to clear cache
|
||||
String? _lastTileTypeId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -171,56 +174,16 @@ class MapViewState extends State<MapView> {
|
||||
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
||||
}
|
||||
|
||||
/// Build tile layer based on selected tile provider
|
||||
/// Build tile layer - uses standard URL that SimpleTileHttpClient can parse
|
||||
Widget _buildTileLayer(AppState appState) {
|
||||
final selectedTileType = appState.selectedTileType;
|
||||
final selectedProvider = appState.selectedTileProvider;
|
||||
|
||||
// Fallback to first available tile type if none selected
|
||||
if (selectedTileType == null || selectedProvider == null) {
|
||||
final allTypes = <TileType>[];
|
||||
for (final provider in appState.tileProviders) {
|
||||
allTypes.addAll(provider.availableTileTypes);
|
||||
}
|
||||
|
||||
final fallback = allTypes.firstOrNull;
|
||||
if (fallback != null) {
|
||||
return TileLayer(
|
||||
urlTemplate: fallback.urlTemplate,
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Ultimate fallback - hardcoded OSM
|
||||
return TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Get the URL template with API key if needed
|
||||
String urlTemplate = selectedTileType.urlTemplate;
|
||||
if (selectedTileType.requiresApiKey && selectedProvider.apiKey != null) {
|
||||
urlTemplate = urlTemplate.replaceAll('{api_key}', selectedProvider.apiKey!);
|
||||
}
|
||||
|
||||
// For now, use our custom HTTP client for all tile requests
|
||||
// This will enable offline support for all providers
|
||||
// Use a generic URL template that SimpleTileHttpClient recognizes
|
||||
// The actual provider URL will be built by MapDataProvider using current AppState
|
||||
return TileLayer(
|
||||
urlTemplate: urlTemplate,
|
||||
urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
additionalOptions: {
|
||||
'attribution': selectedTileType.attribution,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,6 +208,17 @@ class MapViewState extends State<MapView> {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if tile type changed and clear cache if needed
|
||||
final currentTileTypeId = appState.selectedTileType?.id;
|
||||
if (_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Clear our tile request queue
|
||||
_tileHttpClient.clearTileQueue();
|
||||
// Note: The ValueKey on FlutterMap will cause flutter_map to rebuild and clear its cache
|
||||
});
|
||||
}
|
||||
_lastTileTypeId = currentTileTypeId;
|
||||
|
||||
// Seed add‑mode target once, after first controller center is available.
|
||||
if (session != null && session.target == null) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user