mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
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:
17
DEVELOPER.md
17
DEVELOPER.md
@@ -402,9 +402,22 @@ bool get showUploadModeSelector => kDebugMode;
|
|||||||
**Why the architectural change:**
|
**Why the architectural change:**
|
||||||
The previous HTTP interception approach (`SimpleTileHttpClient` with fake URLs) fought against Flutter Map's architecture and created unnecessary complexity. The new `TileProvider` approach:
|
The previous HTTP interception approach (`SimpleTileHttpClient` with fake URLs) fought against Flutter Map's architecture and created unnecessary complexity. The new `TileProvider` approach:
|
||||||
- **Cleaner integration**: Works with Flutter Map's design instead of against it
|
- **Cleaner integration**: Works with Flutter Map's design instead of against it
|
||||||
- **Simpler caching**: One cache layer instead of multiple conflicting systems
|
- **Smart cache routing**: Only checks offline cache when needed, eliminating expensive filesystem searches
|
||||||
- **Better error handling**: Graceful fallbacks for missing tiles
|
- **Better error handling**: Graceful fallbacks for missing tiles
|
||||||
- **Reduced complexity**: Eliminated semaphores, queue management, and tile completion tracking
|
- **Cross-platform performance**: Optimizations that work well on both iOS and Android
|
||||||
|
|
||||||
|
**Tile Loading Performance Fix (v1.5.2):**
|
||||||
|
The major performance issue was discovered to be double caching with expensive operations:
|
||||||
|
1. **Problem**: Every tile request checked offline areas via filesystem I/O, even when no offline data existed
|
||||||
|
2. **Solution**: Smart cache detection - only check offline cache when in offline mode OR when offline areas actually exist for the current provider
|
||||||
|
3. **Result**: Dramatically improved tile loading from 0.5-5 tiles/sec back to ~70 tiles/sec for normal browsing
|
||||||
|
|
||||||
|
**Cross-Platform Optimizations:**
|
||||||
|
- **Request deduplication**: Prevents multiple simultaneous requests for identical tile coordinates
|
||||||
|
- **Optimized retry timing**: Faster initial retry (150ms vs 200ms) with shorter backoff for quicker recovery
|
||||||
|
- **Queue size limits**: Maximum 100 queued requests to prevent memory bloat
|
||||||
|
- **Smart queue management**: Drops oldest requests when queue fills up
|
||||||
|
- **Reduced concurrent connections**: 8 threads instead of 10 for better stability across platforms
|
||||||
|
|
||||||
### 14. Navigation & Routing (Implemented, Awaiting Integration)
|
### 14. Navigation & Routing (Implemented, Awaiting Integration)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
{
|
{
|
||||||
"1.5.2": {
|
"1.5.2": {
|
||||||
"content": [
|
"content": [
|
||||||
|
"• MAJOR: Fixed severe tile loading performance issue - eliminated expensive filesystem searches on every tile request",
|
||||||
|
"• IMPROVED: Smart cache routing - only checks offline cache when actually needed, dramatically improving browsing speed",
|
||||||
"• IMPROVED: Simplified tile loading architecture - replaced HTTP interception with clean TileProvider implementation",
|
"• IMPROVED: Simplified tile loading architecture - replaced HTTP interception with clean TileProvider implementation",
|
||||||
|
"• IMPROVED: Cross-platform tile performance optimizations - better retry timing, request deduplication, queue management",
|
||||||
"• IMPROVED: Network status indicator now focuses only on surveillance data loading, not tile loading (tiles show their own progress)",
|
"• IMPROVED: Network status indicator now focuses only on surveillance data loading, not tile loading (tiles show their own progress)",
|
||||||
|
"• IMPROVED: Node limit behavior - buttons now show helpful messages instead of being disabled, encouraging users to zoom in for safe editing",
|
||||||
|
"• IMPROVED: Indicator positioning - network status and node limit indicators automatically move down when search bar is visible",
|
||||||
"• IMPROVED: Reduced complexity in cache management and state tracking",
|
"• IMPROVED: Reduced complexity in cache management and state tracking",
|
||||||
"• FIXED: Max nodes setting now correctly limits rendering only (not data fetching) to prevent UI lag",
|
"• FIXED: Max nodes setting now correctly limits rendering only (not data fetching) to prevent UI lag",
|
||||||
"• FIXED: New node limit indicator shows when not all devices are displayed due to rendering limit",
|
"• FIXED: New node limit indicator shows when not all devices are displayed due to rendering limit",
|
||||||
"• FIXED: Tile cache properly clears when switching between tile providers/types - no more mixed tiles",
|
"• FIXED: Tile cache properly clears when switching between tile providers/types - no more mixed tiles",
|
||||||
"• FIXED: Network status indicator no longer shows false timeouts during surveillance data splitting operations",
|
"• FIXED: Network status indicator no longer shows false timeouts during surveillance data splitting operations"
|
||||||
"• FIXED: Eliminated potential conflicts between multiple cache layers"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"1.5.1": {
|
"1.5.1": {
|
||||||
|
|||||||
@@ -125,11 +125,12 @@ const double kPinchMoveThreshold = 30.0; // How much drag required for two-finge
|
|||||||
const double kRotationThreshold = 6.0; // Degrees of rotation required before map actually rotates (Google Maps style)
|
const double kRotationThreshold = 6.0; // Degrees of rotation required before map actually rotates (Google Maps style)
|
||||||
|
|
||||||
// Tile fetch configuration (brutalist approach: simple, configurable, unlimited retries)
|
// Tile fetch configuration (brutalist approach: simple, configurable, unlimited retries)
|
||||||
const int kTileFetchConcurrentThreads = 10; // Number of simultaneous tile downloads
|
const int kTileFetchConcurrentThreads = 8; // Reduced from 10 to 8 for better cross-platform performance
|
||||||
const int kTileFetchInitialDelayMs = 200; // Base delay for first retry (500ms)
|
const int kTileFetchInitialDelayMs = 150; // Reduced from 200ms for faster retries
|
||||||
const double kTileFetchBackoffMultiplier = 1.5; // Multiply delay by this each attempt
|
const double kTileFetchBackoffMultiplier = 1.4; // Slightly reduced for faster recovery
|
||||||
const int kTileFetchMaxDelayMs = 5000; // Cap delays at this value (10 seconds max)
|
const int kTileFetchMaxDelayMs = 4000; // Reduced from 5000ms for faster max retry
|
||||||
const int kTileFetchRandomJitterMs = 100; // Random fuzz to add (0 to 250ms)
|
const int kTileFetchRandomJitterMs = 50; // Reduced jitter for more predictable timing
|
||||||
|
const int kTileFetchMaxQueueSize = 100; // Reasonable queue size to prevent memory bloat
|
||||||
// Note: Removed max attempts - tiles retry indefinitely until they succeed or are canceled
|
// Note: Removed max attempts - tiles retry indefinitely until they succeed or are canceled
|
||||||
|
|
||||||
// User download max zoom span (user can download up to kMaxUserDownloadZoomSpan zooms above min)
|
// User download max zoom span (user can download up to kMaxUserDownloadZoomSpan zooms above min)
|
||||||
@@ -165,3 +166,4 @@ double getNodeRingThickness(BuildContext context) {
|
|||||||
// return _kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio;
|
// return _kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio;
|
||||||
return _kNodeRingThicknessBase;
|
return _kNodeRingThicknessBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,8 @@
|
|||||||
"nodeDataSlow": "Überwachungsdaten langsam"
|
"nodeDataSlow": "Überwachungsdaten langsam"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Zeige {rendered} von {total} Geräten"
|
"message": "Zeige {rendered} von {total} Geräten",
|
||||||
|
"editingDisabledMessage": "Zu viele Geräte sichtbar für sicheres Bearbeiten. Vergrößern Sie die Ansicht, um die Anzahl sichtbarer Geräte zu reduzieren, und versuchen Sie es erneut."
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "DeFlock - Überwachungs-Transparenz",
|
"title": "DeFlock - Überwachungs-Transparenz",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "Surveillance data slow"
|
"nodeDataSlow": "Surveillance data slow"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Showing {rendered} of {total} devices"
|
"message": "Showing {rendered} of {total} devices",
|
||||||
|
"editingDisabledMessage": "Too many devices shown to safely edit. Zoom in further to reduce the number of visible devices, then try again."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Search Location",
|
"searchLocation": "Search Location",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "Datos de vigilancia lentos"
|
"nodeDataSlow": "Datos de vigilancia lentos"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Mostrando {rendered} de {total} dispositivos"
|
"message": "Mostrando {rendered} de {total} dispositivos",
|
||||||
|
"editingDisabledMessage": "Demasiados dispositivos visibles para editar con seguridad. Acerque más para reducir el número de dispositivos visibles, luego inténtelo de nuevo."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Buscar ubicación",
|
"searchLocation": "Buscar ubicación",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "Données de surveillance lentes"
|
"nodeDataSlow": "Données de surveillance lentes"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Affichage de {rendered} sur {total} appareils"
|
"message": "Affichage de {rendered} sur {total} appareils",
|
||||||
|
"editingDisabledMessage": "Trop d'appareils visibles pour éditer en toute sécurité. Zoomez davantage pour réduire le nombre d'appareils visibles, puis réessayez."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Rechercher lieu",
|
"searchLocation": "Rechercher lieu",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "Dati di sorveglianza lenti"
|
"nodeDataSlow": "Dati di sorveglianza lenti"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Mostra {rendered} di {total} dispositivi"
|
"message": "Mostra {rendered} di {total} dispositivi",
|
||||||
|
"editingDisabledMessage": "Troppi dispositivi visibili per modificare in sicurezza. Ingrandisci per ridurre il numero di dispositivi visibili, poi riprova."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Cerca posizione",
|
"searchLocation": "Cerca posizione",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "Dados de vigilância lentos"
|
"nodeDataSlow": "Dados de vigilância lentos"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "Mostrando {rendered} de {total} dispositivos"
|
"message": "Mostrando {rendered} de {total} dispositivos",
|
||||||
|
"editingDisabledMessage": "Muitos dispositivos visíveis para editar com segurança. Aproxime mais para reduzir o número de dispositivos visíveis, e tente novamente."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "Buscar localização",
|
"searchLocation": "Buscar localização",
|
||||||
|
|||||||
@@ -417,7 +417,8 @@
|
|||||||
"nodeDataSlow": "监控数据缓慢"
|
"nodeDataSlow": "监控数据缓慢"
|
||||||
},
|
},
|
||||||
"nodeLimitIndicator": {
|
"nodeLimitIndicator": {
|
||||||
"message": "显示 {rendered} / {total} 设备"
|
"message": "显示 {rendered} / {total} 设备",
|
||||||
|
"editingDisabledMessage": "可见设备过多,无法安全编辑。请放大地图以减少可见设备数量,然后重试。"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"searchLocation": "搜索位置",
|
"searchLocation": "搜索位置",
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
LocalizationService.instance.t('editNode.zoomInRequiredMessage',
|
LocalizationService.instance.t('editNode.zoomInRequiredMessage',
|
||||||
params: [kMinZoomForNodeEditingSheets.toString()])
|
params: [kMinZoomForNodeEditingSheets.toString()])
|
||||||
),
|
),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if node limit is active and warn user
|
||||||
|
if (_isNodeLimitActive) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
LocalizationService.instance.t('nodeLimitIndicator.editingDisabledMessage')
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -775,7 +791,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
builder: (context, child) => ElevatedButton.icon(
|
builder: (context, child) => ElevatedButton.icon(
|
||||||
icon: Icon(Icons.add_location_alt),
|
icon: Icon(Icons.add_location_alt),
|
||||||
label: Text(LocalizationService.instance.tagNode),
|
label: Text(LocalizationService.instance.tagNode),
|
||||||
onPressed: _isNodeLimitActive ? null : _openAddNodeSheet,
|
onPressed: _openAddNodeSheet,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: Size(0, 48),
|
minimumSize: Size(0, 48),
|
||||||
textStyle: TextStyle(fontSize: 16),
|
textStyle: TextStyle(fontSize: 16),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'dart:ui';
|
|||||||
import '../app_state.dart';
|
import '../app_state.dart';
|
||||||
import '../models/tile_provider.dart' as models;
|
import '../models/tile_provider.dart' as models;
|
||||||
import 'map_data_provider.dart';
|
import 'map_data_provider.dart';
|
||||||
|
import 'offline_area_service.dart';
|
||||||
|
|
||||||
/// Custom tile provider that integrates with DeFlock's offline/online architecture.
|
/// 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');
|
throw Exception('No tile provider configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch tile through our existing MapDataProvider system
|
// Smart cache routing: only check offline cache when needed
|
||||||
// This automatically handles offline/online routing, caching, etc.
|
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(
|
final tileBytes = await mapDataProvider.getTile(
|
||||||
z: coordinates.z,
|
z: coordinates.z,
|
||||||
x: coordinates.x,
|
x: coordinates.x,
|
||||||
y: coordinates.y,
|
y: coordinates.y,
|
||||||
source: MapSource.auto, // Use auto routing (offline first, then online)
|
source: source,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decode the image bytes
|
// Decode the image bytes
|
||||||
@@ -119,4 +123,36 @@ class DeflockTileImageProvider extends ImageProvider<DeflockTileImageProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(coordinates, providerId, tileTypeId);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ void clearRemoteTileQueueSelective(LatLngBounds currentBounds) {
|
|||||||
/// Calculate retry delay using configurable backoff strategy.
|
/// Calculate retry delay using configurable backoff strategy.
|
||||||
/// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay
|
/// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay
|
||||||
int _calculateRetryDelay(int attempt, Random random) {
|
int _calculateRetryDelay(int attempt, Random random) {
|
||||||
// Calculate exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
|
// Calculate exponential backoff
|
||||||
final baseDelay = (kTileFetchInitialDelayMs *
|
final baseDelay = (kTileFetchInitialDelayMs *
|
||||||
pow(kTileFetchBackoffMultiplier, attempt - 1)).round();
|
pow(kTileFetchBackoffMultiplier, attempt - 1)).round();
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ Future<List<int>> fetchRemoteTile({
|
|||||||
}
|
}
|
||||||
await Future.delayed(Duration(milliseconds: delay));
|
await Future.delayed(Duration(milliseconds: delay));
|
||||||
} finally {
|
} 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});
|
_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 {
|
class _SimpleSemaphore {
|
||||||
final int _max;
|
final int _max;
|
||||||
int _current = 0;
|
int _current = 0;
|
||||||
final List<_TileRequest> _queue = [];
|
final List<_TileRequest> _queue = [];
|
||||||
|
final Set<String> _inFlightTiles = {}; // Track in-flight requests for deduplication
|
||||||
_SimpleSemaphore(this._max);
|
_SimpleSemaphore(this._max);
|
||||||
|
|
||||||
Future<void> acquire({int? z, int? x, int? y}) async {
|
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) {
|
if (_current < _max) {
|
||||||
_current++;
|
_current++;
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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 c = Completer<void>();
|
||||||
final request = _TileRequest(
|
final request = _TileRequest(
|
||||||
z: z ?? -1,
|
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) {
|
if (_queue.isNotEmpty) {
|
||||||
final request = _queue.removeAt(0);
|
final request = _queue.removeAt(0);
|
||||||
request.callback();
|
request.callback();
|
||||||
@@ -201,19 +227,37 @@ class _SimpleSemaphore {
|
|||||||
int clearQueue() {
|
int clearQueue() {
|
||||||
final clearedCount = _queue.length;
|
final clearedCount = _queue.length;
|
||||||
_queue.clear();
|
_queue.clear();
|
||||||
|
_inFlightTiles.clear(); // Also clear deduplication tracking
|
||||||
return clearedCount;
|
return clearedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear only tiles that don't pass the visibility filter
|
/// Clear only tiles that don't pass the visibility filter
|
||||||
int clearStaleRequests(bool Function(int z, int x, int y) isStale) {
|
int clearStaleRequests(bool Function(int z, int x, int y) isStale) {
|
||||||
final initialCount = _queue.length;
|
final initialCount = _queue.length;
|
||||||
_queue.removeWhere((request) => isStale(request.z, request.x, request.y));
|
final initialInFlightCount = _inFlightTiles.length;
|
||||||
final clearedCount = initialCount - _queue.length;
|
|
||||||
|
|
||||||
if (clearedCount > 0) {
|
// Remove stale requests from queue
|
||||||
debugPrint('[SimpleSemaphore] Cleared $clearedCount stale tile requests, kept ${_queue.length}');
|
_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,21 @@ class OfflineAreaService {
|
|||||||
/// Check if any areas are currently downloading
|
/// Check if any areas are currently downloading
|
||||||
bool get hasActiveDownloads => _areas.any((area) => area.status == OfflineAreaStatus.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)
|
/// Cancel all active downloads (used when enabling offline mode)
|
||||||
Future<void> cancelActiveDownloads() async {
|
Future<void> cancelActiveDownloads() async {
|
||||||
final activeAreas = _areas.where((area) => area.status == OfflineAreaStatus.downloading).toList();
|
final activeAreas = _areas.where((area) => area.status == OfflineAreaStatus.downloading).toList();
|
||||||
|
|||||||
@@ -610,14 +610,24 @@ class MapViewState extends State<MapView> {
|
|||||||
MarkerLayer(markers: [...suspectedLocationMarkers, ...markers, ...centerMarkers]),
|
MarkerLayer(markers: [...suspectedLocationMarkers, ...markers, ...centerMarkers]),
|
||||||
|
|
||||||
// Node limit indicator (top-left) - shown when limit is active
|
// Node limit indicator (top-left) - shown when limit is active
|
||||||
NodeLimitIndicator(
|
Builder(
|
||||||
isActive: isLimitActive,
|
builder: (context) {
|
||||||
renderedCount: nodesToRender.length,
|
final appState = context.read<AppState>();
|
||||||
totalCount: isLimitActive ? allNodes.where((node) {
|
// Add search bar offset when search bar is visible
|
||||||
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
|
final searchBarOffset = (!appState.offlineMode && appState.isInSearchMode) ? 60.0 : 0.0;
|
||||||
node.coord.latitude.abs() <= 90 &&
|
|
||||||
node.coord.longitude.abs() <= 180;
|
return NodeLimitIndicator(
|
||||||
}).length : 0,
|
isActive: isLimitActive,
|
||||||
|
renderedCount: nodesToRender.length,
|
||||||
|
totalCount: isLimitActive ? allNodes.where((node) {
|
||||||
|
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
|
||||||
|
node.coord.latitude.abs() <= 90 &&
|
||||||
|
node.coord.longitude.abs() <= 180;
|
||||||
|
}).length : 0,
|
||||||
|
top: 8.0 + searchBarOffset,
|
||||||
|
left: 8.0,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -774,7 +784,18 @@ class MapViewState extends State<MapView> {
|
|||||||
|
|
||||||
// Network status indicator (top-left) - conditionally shown
|
// Network status indicator (top-left) - conditionally shown
|
||||||
if (appState.networkStatusIndicatorEnabled)
|
if (appState.networkStatusIndicatorEnabled)
|
||||||
const NetworkStatusIndicator(),
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
// Calculate position based on node limit indicator presence and search bar
|
||||||
|
final searchBarOffset = (!appState.offlineMode && appState.isInSearchMode) ? 60.0 : 0.0;
|
||||||
|
final nodeLimitOffset = isLimitActive ? 48.0 : 0.0; // Height of node limit indicator + spacing
|
||||||
|
|
||||||
|
return NetworkStatusIndicator(
|
||||||
|
top: 8.0 + searchBarOffset + nodeLimitOffset,
|
||||||
|
left: 8.0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Proximity alert banner (top)
|
// Proximity alert banner (top)
|
||||||
ProximityAlertBanner(
|
ProximityAlertBanner(
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import '../services/network_status.dart';
|
|||||||
import '../services/localization_service.dart';
|
import '../services/localization_service.dart';
|
||||||
|
|
||||||
class NetworkStatusIndicator extends StatelessWidget {
|
class NetworkStatusIndicator extends StatelessWidget {
|
||||||
const NetworkStatusIndicator({super.key});
|
final double top;
|
||||||
|
final double left;
|
||||||
|
|
||||||
|
const NetworkStatusIndicator({
|
||||||
|
super.key,
|
||||||
|
this.top = 56.0,
|
||||||
|
this.left = 8.0,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -61,8 +68,8 @@ class NetworkStatusIndicator extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 56, // Position below node limit indicator when present
|
top: top, // Position dynamically based on other indicators
|
||||||
left: 8,
|
left: left,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ class NodeLimitIndicator extends StatelessWidget {
|
|||||||
final bool isActive;
|
final bool isActive;
|
||||||
final int renderedCount;
|
final int renderedCount;
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
|
final double top;
|
||||||
|
final double left;
|
||||||
|
|
||||||
const NodeLimitIndicator({
|
const NodeLimitIndicator({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.renderedCount,
|
required this.renderedCount,
|
||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
|
this.top = 8.0,
|
||||||
|
this.left = 8.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,8 +29,8 @@ class NodeLimitIndicator extends StatelessWidget {
|
|||||||
.replaceAll('{total}', totalCount.toString());
|
.replaceAll('{total}', totalCount.toString());
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 8, // Position at top-left of map area
|
top: top, // Position at top-left of map area
|
||||||
left: 8,
|
left: left,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -37,6 +37,20 @@ class NodeTagSheet extends StatelessWidget {
|
|||||||
node.tags['_pending_deletion'] != 'true');
|
node.tags['_pending_deletion'] != 'true');
|
||||||
|
|
||||||
void _openEditSheet() {
|
void _openEditSheet() {
|
||||||
|
// Check if node limit is active and warn user
|
||||||
|
if (isNodeLimitActive) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
locService.t('nodeLimitIndicator.editingDisabledMessage')
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (onEditPressed != null) {
|
if (onEditPressed != null) {
|
||||||
onEditPressed!(); // Use callback if provided
|
onEditPressed!(); // Use callback if provided
|
||||||
} else {
|
} else {
|
||||||
@@ -206,7 +220,7 @@ class NodeTagSheet extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (isEditable) ...[
|
if (isEditable) ...[
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: isNodeLimitActive ? null : _openEditSheet,
|
onPressed: _openEditSheet,
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
label: Text(locService.edit),
|
label: Text(locService.edit),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: deflockapp
|
name: deflockapp
|
||||||
description: Map public surveillance infrastructure with OpenStreetMap
|
description: Map public surveillance infrastructure with OpenStreetMap
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.5.2+20 # The thing after the + is the version code, incremented with each release
|
version: 1.5.2+23 # The thing after the + is the version code, incremented with each release
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||||
|
|||||||
Reference in New Issue
Block a user