Get rid of double cache, filesystem checking for every tile fetch, swap out http interception for a fluttermap tileprovider that calls map_data, fix node rendering limit

This commit is contained in:
stopflock
2025-11-28 21:48:17 -06:00
parent 153377e9e6
commit df0377b41f
19 changed files with 228 additions and 45 deletions

View File

@@ -8,6 +8,7 @@ import 'dart:ui';
import '../app_state.dart';
import '../models/tile_provider.dart' as models;
import 'map_data_provider.dart';
import 'offline_area_service.dart';
/// Custom tile provider that integrates with DeFlock's offline/online architecture.
///
@@ -83,13 +84,16 @@ class DeflockTileImageProvider extends ImageProvider<DeflockTileImageProvider> {
throw Exception('No tile provider configured');
}
// Fetch tile through our existing MapDataProvider system
// This automatically handles offline/online routing, caching, etc.
// Smart cache routing: only check offline cache when needed
final MapSource source = _shouldCheckOfflineCache(appState)
? MapSource.auto // Check offline first, then network
: MapSource.remote; // Skip offline cache, go directly to network
final tileBytes = await mapDataProvider.getTile(
z: coordinates.z,
x: coordinates.x,
y: coordinates.y,
source: MapSource.auto, // Use auto routing (offline first, then online)
source: source,
);
// Decode the image bytes
@@ -119,4 +123,36 @@ class DeflockTileImageProvider extends ImageProvider<DeflockTileImageProvider> {
@override
int get hashCode => Object.hash(coordinates, providerId, tileTypeId);
/// Determine if we should check offline cache for this tile request.
/// Only check offline cache if:
/// 1. We're in offline mode (forced), OR
/// 2. We have offline areas for the current provider/type
///
/// This avoids expensive filesystem searches when browsing online
/// with providers that have no offline areas.
bool _shouldCheckOfflineCache(AppState appState) {
// Always check offline cache in offline mode
if (appState.offlineMode) {
return true;
}
// For online mode, only check if we might actually have relevant offline data
final currentProvider = appState.selectedTileProvider;
final currentTileType = appState.selectedTileType;
if (currentProvider == null || currentTileType == null) {
return false;
}
// Quick check: do we have any offline areas for this provider/type?
// This avoids the expensive per-tile filesystem search in fetchLocalTile
final offlineService = OfflineAreaService();
final hasRelevantAreas = offlineService.hasOfflineAreasForProvider(
currentProvider.id,
currentTileType.id,
);
return hasRelevantAreas;
}
}

View File

@@ -34,7 +34,7 @@ void clearRemoteTileQueueSelective(LatLngBounds currentBounds) {
/// Calculate retry delay using configurable backoff strategy.
/// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay
int _calculateRetryDelay(int attempt, Random random) {
// Calculate exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
// Calculate exponential backoff
final baseDelay = (kTileFetchInitialDelayMs *
pow(kTileFetchBackoffMultiplier, attempt - 1)).round();
@@ -136,7 +136,7 @@ Future<List<int>> fetchRemoteTile({
}
await Future.delayed(Duration(milliseconds: delay));
} finally {
_tileFetchSemaphore.release();
_tileFetchSemaphore.release(z: z, x: x, y: y);
}
}
}
@@ -164,18 +164,40 @@ class _TileRequest {
_TileRequest({required this.z, required this.x, required this.y, required this.callback});
}
/// Spatially-aware counting semaphore for tile requests
/// Spatially-aware counting semaphore for tile requests with deduplication
class _SimpleSemaphore {
final int _max;
int _current = 0;
final List<_TileRequest> _queue = [];
final Set<String> _inFlightTiles = {}; // Track in-flight requests for deduplication
_SimpleSemaphore(this._max);
Future<void> acquire({int? z, int? x, int? y}) async {
// Create tile key for deduplication
final tileKey = '${z ?? -1}/${x ?? -1}/${y ?? -1}';
// If this tile is already in flight, skip the request
if (_inFlightTiles.contains(tileKey)) {
debugPrint('[SimpleSemaphore] Skipping duplicate request for $tileKey');
return;
}
// Add to in-flight tracking
_inFlightTiles.add(tileKey);
if (_current < _max) {
_current++;
return;
} else {
// Check queue size limit to prevent memory bloat
if (_queue.length >= kTileFetchMaxQueueSize) {
// Remove oldest request to make room
final oldRequest = _queue.removeAt(0);
final oldKey = '${oldRequest.z}/${oldRequest.x}/${oldRequest.y}';
_inFlightTiles.remove(oldKey);
debugPrint('[SimpleSemaphore] Queue full, dropped oldest request: $oldKey');
}
final c = Completer<void>();
final request = _TileRequest(
z: z ?? -1,
@@ -188,7 +210,11 @@ class _SimpleSemaphore {
}
}
void release() {
void release({int? z, int? x, int? y}) {
// Remove from in-flight tracking
final tileKey = '${z ?? -1}/${x ?? -1}/${y ?? -1}';
_inFlightTiles.remove(tileKey);
if (_queue.isNotEmpty) {
final request = _queue.removeAt(0);
request.callback();
@@ -201,19 +227,37 @@ class _SimpleSemaphore {
int clearQueue() {
final clearedCount = _queue.length;
_queue.clear();
_inFlightTiles.clear(); // Also clear deduplication tracking
return clearedCount;
}
/// Clear only tiles that don't pass the visibility filter
int clearStaleRequests(bool Function(int z, int x, int y) isStale) {
final initialCount = _queue.length;
_queue.removeWhere((request) => isStale(request.z, request.x, request.y));
final clearedCount = initialCount - _queue.length;
final initialInFlightCount = _inFlightTiles.length;
if (clearedCount > 0) {
debugPrint('[SimpleSemaphore] Cleared $clearedCount stale tile requests, kept ${_queue.length}');
// Remove stale requests from queue
_queue.removeWhere((request) => isStale(request.z, request.x, request.y));
// Remove stale tiles from in-flight tracking
_inFlightTiles.removeWhere((tileKey) {
final parts = tileKey.split('/');
if (parts.length == 3) {
final z = int.tryParse(parts[0]) ?? -1;
final x = int.tryParse(parts[1]) ?? -1;
final y = int.tryParse(parts[2]) ?? -1;
return isStale(z, x, y);
}
return false;
});
final queueClearedCount = initialCount - _queue.length;
final inFlightClearedCount = initialInFlightCount - _inFlightTiles.length;
if (queueClearedCount > 0 || inFlightClearedCount > 0) {
debugPrint('[SimpleSemaphore] Cleared $queueClearedCount stale queue + $inFlightClearedCount stale in-flight, kept ${_queue.length}');
}
return clearedCount;
return queueClearedCount + inFlightClearedCount;
}
}

View File

@@ -29,6 +29,21 @@ class OfflineAreaService {
/// Check if any areas are currently downloading
bool get hasActiveDownloads => _areas.any((area) => area.status == OfflineAreaStatus.downloading);
/// Fast check: do we have any completed offline areas for a specific provider/type?
/// This allows smart cache routing without expensive filesystem searches.
/// Safe to call before initialization - returns false if not yet initialized.
bool hasOfflineAreasForProvider(String providerId, String tileTypeId) {
if (!_initialized) {
return false; // No offline areas loaded yet
}
return _areas.any((area) =>
area.status == OfflineAreaStatus.complete &&
area.tileProviderId == providerId &&
area.tileTypeId == tileTypeId
);
}
/// Cancel all active downloads (used when enabling offline mode)
Future<void> cancelActiveDownloads() async {
final activeAreas = _areas.where((area) => area.status == OfflineAreaStatus.downloading).toList();