mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-17 08:30:31 +02:00
finalize code paths for offline areas, caching, in light of multiple tile providers
This commit is contained in:
@@ -3,9 +3,14 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import '../offline_area_service.dart';
|
import '../offline_area_service.dart';
|
||||||
import '../offline_areas/offline_area_models.dart';
|
import '../offline_areas/offline_area_models.dart';
|
||||||
import '../offline_areas/offline_tile_utils.dart';
|
import '../offline_areas/offline_tile_utils.dart';
|
||||||
|
import '../../app_state.dart';
|
||||||
|
|
||||||
/// Fetch a tile from the newest offline area that plausibly contains it, or throw if not found.
|
/// Fetch a tile from the newest offline area that matches the current provider, or throw if not found.
|
||||||
Future<List<int>> fetchLocalTile({required int z, required int x, required int y}) async {
|
Future<List<int>> fetchLocalTile({required int z, required int x, required int y}) async {
|
||||||
|
final appState = AppState.instance;
|
||||||
|
final currentProvider = appState.selectedTileProvider;
|
||||||
|
final currentTileType = appState.selectedTileType;
|
||||||
|
|
||||||
final offlineService = OfflineAreaService();
|
final offlineService = OfflineAreaService();
|
||||||
await offlineService.ensureInitialized();
|
await offlineService.ensureInitialized();
|
||||||
final areas = offlineService.offlineAreas;
|
final areas = offlineService.offlineAreas;
|
||||||
@@ -14,6 +19,9 @@ Future<List<int>> fetchLocalTile({required int z, required int x, required int y
|
|||||||
for (final area in areas) {
|
for (final area in areas) {
|
||||||
if (area.status != OfflineAreaStatus.complete) continue;
|
if (area.status != OfflineAreaStatus.complete) continue;
|
||||||
if (z < area.minZoom || z > area.maxZoom) continue;
|
if (z < area.minZoom || z > area.maxZoom) continue;
|
||||||
|
|
||||||
|
// Only consider areas that match the current provider/type
|
||||||
|
if (area.tileProviderId != currentProvider?.id || area.tileTypeId != currentTileType?.id) continue;
|
||||||
|
|
||||||
// Get tile coverage for area at this zoom only
|
// Get tile coverage for area at this zoom only
|
||||||
final coveredTiles = computeTileList(area.bounds, z, z);
|
final coveredTiles = computeTileList(area.bounds, z, z);
|
||||||
@@ -28,7 +36,7 @@ Future<List<int>> fetchLocalTile({required int z, required int x, required int y
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (candidates.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
throw Exception('Tile $z/$x/$y not found in any offline area');
|
throw Exception('Tile $z/$x/$y from current provider ${currentProvider?.id}/${currentTileType?.id} not found in any offline area');
|
||||||
}
|
}
|
||||||
candidates.sort((a, b) => b.modified.compareTo(a.modified)); // newest first
|
candidates.sort((a, b) => b.modified.compareTo(a.modified)); // newest first
|
||||||
return await candidates.first.file.readAsBytes();
|
return await candidates.first.file.readAsBytes();
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class OfflineAreaService {
|
|||||||
|
|
||||||
await _loadAreasFromDisk();
|
await _loadAreasFromDisk();
|
||||||
await WorldAreaManager.ensureWorldArea(_areas, getOfflineAreaDir, downloadArea);
|
await WorldAreaManager.ensureWorldArea(_areas, getOfflineAreaDir, downloadArea);
|
||||||
|
await saveAreasToDisk(); // Save any world area updates
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class OfflineAreaDownloader {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
// Use the same unified path as live tiles: always go through MapDataProvider
|
// Use the same unified path as live tiles: always go through MapDataProvider
|
||||||
|
// MapDataProvider will use current AppState provider for downloads
|
||||||
final bytes = await MapDataProvider().getTile(
|
final bytes = await MapDataProvider().getTile(
|
||||||
z: tile[0],
|
z: tile[0],
|
||||||
x: tile[1],
|
x: tile[1],
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class WorldAreaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create world area if it doesn't exist
|
// Create world area if it doesn't exist, or update existing area without provider info
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
final appDocDir = await getOfflineAreaDir();
|
final appDocDir = await getOfflineAreaDir();
|
||||||
final dir = "${appDocDir.path}/$_worldAreaId";
|
final dir = "${appDocDir.path}/$_worldAreaId";
|
||||||
@@ -51,8 +51,38 @@ class WorldAreaManager {
|
|||||||
directory: dir,
|
directory: dir,
|
||||||
status: OfflineAreaStatus.downloading,
|
status: OfflineAreaStatus.downloading,
|
||||||
isPermanent: true,
|
isPermanent: true,
|
||||||
|
// World area always uses OpenStreetMap
|
||||||
|
tileProviderId: 'openstreetmap',
|
||||||
|
tileProviderName: 'OpenStreetMap',
|
||||||
|
tileTypeId: 'osm_street',
|
||||||
|
tileTypeName: 'Street Map',
|
||||||
);
|
);
|
||||||
areas.insert(0, world);
|
areas.insert(0, world);
|
||||||
|
} else if (world.tileProviderId == null || world.tileTypeId == null) {
|
||||||
|
// Update existing world area that lacks provider metadata
|
||||||
|
final updatedWorld = OfflineArea(
|
||||||
|
id: world.id,
|
||||||
|
name: world.name,
|
||||||
|
bounds: world.bounds,
|
||||||
|
minZoom: world.minZoom,
|
||||||
|
maxZoom: world.maxZoom,
|
||||||
|
directory: world.directory,
|
||||||
|
status: world.status,
|
||||||
|
progress: world.progress,
|
||||||
|
tilesDownloaded: world.tilesDownloaded,
|
||||||
|
tilesTotal: world.tilesTotal,
|
||||||
|
cameras: world.cameras,
|
||||||
|
sizeBytes: world.sizeBytes,
|
||||||
|
isPermanent: world.isPermanent,
|
||||||
|
// Add missing provider metadata
|
||||||
|
tileProviderId: 'openstreetmap',
|
||||||
|
tileProviderName: 'OpenStreetMap',
|
||||||
|
tileTypeId: 'osm_street',
|
||||||
|
tileTypeName: 'Street Map',
|
||||||
|
);
|
||||||
|
final index = areas.indexOf(world);
|
||||||
|
areas[index] = updatedWorld;
|
||||||
|
world = updatedWorld;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check world area status and start download if needed
|
// Check world area status and start download if needed
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||||
// Extract tile coordinates from the URL using our standard pattern
|
// Extract tile coordinates from our custom URL scheme
|
||||||
final tileCoords = _extractTileCoords(request.url);
|
final tileCoords = _extractTileCoords(request.url);
|
||||||
if (tileCoords != null) {
|
if (tileCoords != null) {
|
||||||
final z = tileCoords['z']!; // We know these are not null from _extractTileCoords
|
final z = tileCoords['z']!;
|
||||||
final x = tileCoords['x']!;
|
final x = tileCoords['x']!;
|
||||||
final y = tileCoords['y']!;
|
final y = tileCoords['y']!;
|
||||||
return _handleTileRequest(z, x, y);
|
return _handleTileRequest(z, x, y);
|
||||||
@@ -26,21 +26,22 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
return _inner.send(request);
|
return _inner.send(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract z/x/y coordinates from our standard tile URL pattern
|
/// Extract z/x/y coordinates from our fake domain: https://tiles.local/provider/type/z/x/y
|
||||||
|
/// We ignore the provider/type in the URL since we use current AppState for actual fetching
|
||||||
Map<String, int>? _extractTileCoords(Uri url) {
|
Map<String, int>? _extractTileCoords(Uri url) {
|
||||||
// We'll use a simple standard pattern: /{z}/{x}/{y}.png
|
if (url.host != 'tiles.local') return null;
|
||||||
// This will be the format we use in map_view.dart
|
|
||||||
final pathSegments = url.pathSegments;
|
|
||||||
|
|
||||||
if (pathSegments.length == 3) {
|
final pathSegments = url.pathSegments;
|
||||||
final z = int.tryParse(pathSegments[0]);
|
if (pathSegments.length != 5) return null;
|
||||||
final x = int.tryParse(pathSegments[1]);
|
|
||||||
final yWithExt = pathSegments[2];
|
// pathSegments[0] = providerId (for cache separation only)
|
||||||
final y = int.tryParse(yWithExt.replaceAll(RegExp(r'\.[^.]*$'), '')); // Remove .png
|
// pathSegments[1] = tileTypeId (for cache separation only)
|
||||||
|
final z = int.tryParse(pathSegments[2]);
|
||||||
if (z != null && x != null && y != null) {
|
final x = int.tryParse(pathSegments[3]);
|
||||||
return {'z': z, 'x': x, 'y': y};
|
final y = int.tryParse(pathSegments[4]);
|
||||||
}
|
|
||||||
|
if (z != null && x != null && y != null) {
|
||||||
|
return {'z': z, 'x': x, 'y': y};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -49,6 +50,7 @@ class SimpleTileHttpClient extends http.BaseClient {
|
|||||||
Future<http.StreamedResponse> _handleTileRequest(int z, int x, int y) async {
|
Future<http.StreamedResponse> _handleTileRequest(int z, int x, int y) async {
|
||||||
try {
|
try {
|
||||||
// Always go through MapDataProvider - it handles offline/online routing
|
// Always go through MapDataProvider - it handles offline/online routing
|
||||||
|
// MapDataProvider will get current provider from AppState
|
||||||
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.auto);
|
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y, source: MapSource.auto);
|
||||||
|
|
||||||
// Clear waiting status - we got data
|
// Clear waiting status - we got data
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ class MapViewState extends State<MapView> {
|
|||||||
String? _lastTileTypeId;
|
String? _lastTileTypeId;
|
||||||
bool? _lastOfflineMode;
|
bool? _lastOfflineMode;
|
||||||
int _mapRebuildKey = 0;
|
int _mapRebuildKey = 0;
|
||||||
bool _shouldClearCache = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -181,40 +180,21 @@ class MapViewState extends State<MapView> {
|
|||||||
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build tile layer - uses standard URL that SimpleTileHttpClient can parse
|
/// Build tile layer - uses fake domain that SimpleTileHttpClient can parse
|
||||||
Widget _buildTileLayer(AppState appState) {
|
Widget _buildTileLayer(AppState appState) {
|
||||||
final selectedTileType = appState.selectedTileType;
|
final selectedTileType = appState.selectedTileType;
|
||||||
final selectedProvider = appState.selectedTileProvider;
|
final selectedProvider = appState.selectedTileProvider;
|
||||||
final offlineMode = appState.offlineMode;
|
|
||||||
|
|
||||||
// Create a unique cache key that includes provider, tile type, and offline mode
|
// Use fake domain with standard HTTPS scheme: https://tiles.local/provider/type/z/x/y
|
||||||
// This ensures different providers/modes have separate cache entries
|
// This naturally separates cache entries by provider and type while being HTTP-compatible
|
||||||
String generateTileKey(String url) {
|
final urlTemplate = 'https://tiles.local/${selectedProvider?.id ?? 'unknown'}/${selectedTileType?.id ?? 'unknown'}/{z}/{x}/{y}';
|
||||||
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(
|
return TileLayer(
|
||||||
urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png?provider=${selectedProvider?.id}&type=${selectedTileType?.id}&mode=${offlineMode ? 'offline' : 'online'}',
|
urlTemplate: urlTemplate,
|
||||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||||
tileProvider: NetworkTileProvider(
|
tileProvider: NetworkTileProvider(
|
||||||
httpClient: httpClient,
|
httpClient: _tileHttpClient,
|
||||||
// Also disable flutter_map caching
|
// Enable flutter_map caching - cache busting handled by URL changes and FlutterMap key
|
||||||
cachingProvider: const DisabledMapCachingProvider(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,17 +226,15 @@ class MapViewState extends State<MapView> {
|
|||||||
|
|
||||||
if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) ||
|
if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) ||
|
||||||
(_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) {
|
(_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) {
|
||||||
// Force map rebuild with new key and destroy cache
|
// Force map rebuild with new key to bust flutter_map cache
|
||||||
_mapRebuildKey++;
|
_mapRebuildKey++;
|
||||||
_shouldClearCache = true;
|
|
||||||
final reason = _lastTileTypeId != currentTileTypeId
|
final reason = _lastTileTypeId != currentTileTypeId
|
||||||
? 'tile type ($currentTileTypeId)'
|
? 'tile type ($currentTileTypeId)'
|
||||||
: 'offline mode ($currentOfflineMode)';
|
: 'offline mode ($currentOfflineMode)';
|
||||||
debugPrint('[MapView] *** CACHE CLEAR *** $reason changed - destroying cache $_mapRebuildKey');
|
debugPrint('[MapView] *** CACHE CLEAR *** $reason changed - rebuilding map $_mapRebuildKey');
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
debugPrint('[MapView] Post-frame: Clearing tile request queue');
|
debugPrint('[MapView] Post-frame: Clearing tile request queue');
|
||||||
_tileHttpClient.clearTileQueue();
|
_tileHttpClient.clearTileQueue();
|
||||||
_shouldClearCache = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user