Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions lib/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'models/search_result.dart';
import 'services/offline_area_service.dart';
import 'services/map_data_provider.dart';
import 'services/node_data_manager.dart';
import 'services/node_spatial_cache.dart';
import 'services/tile_preview_service.dart';
import 'services/changelog_service.dart';
import 'services/operator_profile_service.dart';
Expand Down Expand Up @@ -160,6 +161,7 @@ class AppState extends ChangeNotifier {
bool get proximityAlertsEnabled => _settingsState.proximityAlertsEnabled;
int get proximityAlertDistance => _settingsState.proximityAlertDistance;
bool get networkStatusIndicatorEnabled => _settingsState.networkStatusIndicatorEnabled;
bool get showCoverageOverlay => _settingsState.showCoverageOverlay;
int get suspectedLocationMinDistance => _settingsState.suspectedLocationMinDistance;

// Messages state
Expand Down Expand Up @@ -239,10 +241,13 @@ class AppState extends ChangeNotifier {

// Note: Re-auth check will be triggered from home screen after init

// Initialize OfflineAreaService to ensure offline areas are loaded
await OfflineAreaService().ensureInitialized();

// Preload offline nodes into cache for immediate display
// Initialize offline areas and node cache in parallel (independent)
await Future.wait([
OfflineAreaService().ensureInitialized(),
NodeSpatialCache().initPersistence(),
]);

// Overlay offline area nodes (depends on both above)
await NodeDataManager().preloadOfflineNodes();

// Start uploader if conditions are met
Expand Down Expand Up @@ -737,6 +742,11 @@ class AppState extends ChangeNotifier {
await _settingsState.setNetworkStatusIndicatorEnabled(enabled);
}

/// Set coverage overlay visibility
Future<void> setShowCoverageOverlay(bool enabled) async {
await _settingsState.setShowCoverageOverlay(enabled);
}



/// Set suspected location minimum distance from real nodes
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,9 @@
"success": "Überwachungsdaten geladen",
"nodeDataSlow": "Überwachungsdaten langsam",
"rateLimited": "Server-Limitierung",
"networkError": "Netzwerkfehler"
"networkError": "Netzwerkfehler",
"showCoverageOverlay": "Datenabdeckung anzeigen",
"showCoverageOverlaySubtitle": "Zeigt an, welche Kartenbereiche Daten geladen haben"
},
"nodeLimitIndicator": {
"message": "Zeige {rendered} von {total} Geräten",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Surveillance data loaded",
"nodeDataSlow": "Surveillance data slow",
"rateLimited": "Rate limited by server",
"networkError": "Network error"
"networkError": "Network error",
"showCoverageOverlay": "Data coverage overlay",
"showCoverageOverlaySubtitle": "Show which map areas have loaded data"
},
"nodeLimitIndicator": {
"message": "Showing {rendered} of {total} devices",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Datos de vigilancia cargados",
"nodeDataSlow": "Datos de vigilancia lentos",
"rateLimited": "Limitado por el servidor",
"networkError": "Error de red"
"networkError": "Error de red",
"showCoverageOverlay": "Superposición de cobertura de datos",
"showCoverageOverlaySubtitle": "Mostrar qué áreas del mapa tienen datos cargados"
},
"nodeLimitIndicator": {
"message": "Mostrando {rendered} de {total} dispositivos",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Données de surveillance chargées",
"nodeDataSlow": "Données de surveillance lentes",
"rateLimited": "Limité par le serveur",
"networkError": "Erreur réseau"
"networkError": "Erreur réseau",
"showCoverageOverlay": "Couverture des données",
"showCoverageOverlaySubtitle": "Afficher les zones de la carte dont les données sont chargées"
},
"nodeLimitIndicator": {
"message": "Affichage de {rendered} sur {total} appareils",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Dati di sorveglianza caricati",
"nodeDataSlow": "Dati di sorveglianza lenti",
"rateLimited": "Limitato dal server",
"networkError": "Errore di rete"
"networkError": "Errore di rete",
"showCoverageOverlay": "Sovrapposizione copertura dati",
"showCoverageOverlaySubtitle": "Mostra quali aree della mappa hanno dati caricati"
},
"nodeLimitIndicator": {
"message": "Mostra {rendered} di {total} dispositivi",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Surveillance data geladen",
"nodeDataSlow": "Surveillance data traag",
"rateLimited": "Snelheid beperkt door server",
"networkError": "Netwerk fout"
"networkError": "Netwerk fout",
"showCoverageOverlay": "Datadekking overlay",
"showCoverageOverlaySubtitle": "Toon welke kaartgebieden geladen data hebben"
},
"nodeLimitIndicator": {
"message": "{rendered} van {total} apparaten getoond",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Dane nadzoru załadowane",
"nodeDataSlow": "Dane nadzoru powolne",
"rateLimited": "Ograniczone przez serwer",
"networkError": "Błąd sieci"
"networkError": "Błąd sieci",
"showCoverageOverlay": "Nakładka pokrycia danymi",
"showCoverageOverlaySubtitle": "Pokaż które obszary mapy mają załadowane dane"
},
"nodeLimitIndicator": {
"message": "Pokazuje {rendered} z {total} urządzeń",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Dados de vigilância carregados",
"nodeDataSlow": "Dados de vigilância lentos",
"rateLimited": "Limitado pelo servidor",
"networkError": "Erro de rede"
"networkError": "Erro de rede",
"showCoverageOverlay": "Sobreposição de cobertura de dados",
"showCoverageOverlaySubtitle": "Mostrar quais áreas do mapa têm dados carregados"
},
"nodeLimitIndicator": {
"message": "Mostrando {rendered} de {total} dispositivos",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Gözetleme verisi yüklendi",
"nodeDataSlow": "Gözetleme verisi yavaş",
"rateLimited": "Sunucu tarafından hız sınırlandı",
"networkError": "Ağ hatası"
"networkError": "Ağ hatası",
"showCoverageOverlay": "Veri kapsama katmanı",
"showCoverageOverlaySubtitle": "Hangi harita alanlarının yüklenmiş veriye sahip olduğunu göster"
},
"nodeLimitIndicator": {
"message": "{total} cihazdan {rendered} tanesi gösteriliyor",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "Дані спостереження завантажено",
"nodeDataSlow": "Повільні дані спостереження",
"rateLimited": "Обмежено швидкість сервером",
"networkError": "Помилка мережі"
"networkError": "Помилка мережі",
"showCoverageOverlay": "Накладення покриття даними",
"showCoverageOverlaySubtitle": "Показати, які області карти мають завантажені дані"
},
"nodeLimitIndicator": {
"message": "Показано {rendered} з {total} пристроїв",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@
"success": "监控数据已加载",
"nodeDataSlow": "监控数据缓慢",
"rateLimited": "服务器限流",
"networkError": "网络错误"
"networkError": "网络错误",
"showCoverageOverlay": "数据覆盖范围",
"showCoverageOverlaySubtitle": "显示哪些地图区域已加载数据"
},
"nodeLimitIndicator": {
"message": "显示 {rendered} / {total} 设备",
Expand Down
5 changes: 3 additions & 2 deletions lib/screens/advanced_settings_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'settings/sections/max_nodes_section.dart';
import 'settings/sections/network_status_section.dart';
import 'settings/sections/proximity_alerts_section.dart';
import 'settings/sections/suspected_locations_section.dart';
import 'settings/sections/tile_provider_section.dart';
Expand Down Expand Up @@ -32,8 +33,8 @@ class AdvancedSettingsScreen extends StatelessWidget {
Divider(),
SuspectedLocationsSection(),
Divider(),
// NetworkStatusSection(), // Commented out - network status indicator now defaults to enabled
// Divider(),
NetworkStatusSection(),
Divider(),
TileProviderSection(),
],
),
Expand Down
11 changes: 11 additions & 0 deletions lib/screens/settings/sections/network_status_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ class NetworkStatusSection extends StatelessWidget {
},
contentPadding: EdgeInsets.zero,
),

// Coverage overlay toggle
SwitchListTile(
title: Text(locService.t('networkStatus.showCoverageOverlay')),
subtitle: Text(locService.t('networkStatus.showCoverageOverlaySubtitle')),
value: appState.showCoverageOverlay,
onChanged: (enabled) {
appState.setShowCoverageOverlay(enabled);
},
contentPadding: EdgeInsets.zero,
),
],
);
},
Expand Down
12 changes: 10 additions & 2 deletions lib/services/map_data_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,16 @@ class MapDataProvider {
throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode.");
}

// For downloads, always fetch fresh data (don't use cache)
return _nodeDataManager.fetchWithSplitting(bounds, profiles);
// For downloads, always fetch fresh data (don't use cache).
// Note: passes null generation, so downloads are never cancelled by stale-fetch
// detection and will hold semaphore slots until complete. This is intentional —
// offline downloads should run to completion — but means concurrent downloads
// can block foreground map fetches via the shared semaphore.
return _nodeDataManager.fetchWithSplitting(
bounds,
profiles,
isUserInitiated: true,
);
}

/// Fetch tile image bytes. Default is to try local first, then remote if not offline. Honors explicit source.
Expand Down
27 changes: 10 additions & 17 deletions lib/services/map_data_submodules/tiles_from_local.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dart:io';
import 'dart:math';

import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
import 'package:flutter/foundation.dart' show visibleForTesting;

import '../offline_area_service.dart';
import '../offline_areas/offline_area_models.dart';
import '../offline_areas/offline_tile_utils.dart' show latLonToTileRaw;
import '../../app_state.dart';

/// Fetch a tile from the newest offline area that matches the given provider, or throw if not found.
Expand Down Expand Up @@ -58,28 +58,21 @@ Future<List<int>> fetchLocalTile({

/// O(1) check whether tile (z, x, y) falls within the given lat/lng bounds.
///
/// Uses the same Mercator projection math as [latLonToTile] in
/// offline_tile_utils.dart, but only computes the bounding tile range
/// instead of enumerating every tile at that zoom level.
/// Reuses [latLonToTileRaw] from offline_tile_utils.dart for the Mercator
/// projection, computing only the bounding tile range instead of enumerating
/// every tile at that zoom level.
///
/// Note: Y axis is inverted in tile coordinates — north = lower Y.
@visibleForTesting
bool tileInBounds(LatLngBounds bounds, int z, int x, int y) {
final n = pow(2.0, z);
final west = bounds.west;
final east = bounds.east;
final north = bounds.north;
final south = bounds.south;
final swTile = latLonToTileRaw(bounds.south, bounds.west, z);
final neTile = latLonToTileRaw(bounds.north, bounds.east, z);

final minX = ((west + 180.0) / 360.0 * n).floor();
final maxX = ((east + 180.0) / 360.0 * n).floor();
final minX = swTile[0].floor();
final maxX = neTile[0].floor();
// North → lower Y (Mercator projection inverts latitude)
final minY = ((1.0 - log(tan(north * pi / 180.0) +
1.0 / cos(north * pi / 180.0)) /
pi) / 2.0 * n).floor();
final maxY = ((1.0 - log(tan(south * pi / 180.0) +
1.0 / cos(south * pi / 180.0)) /
pi) / 2.0 * n).floor();
final minY = neTile[1].floor();
final maxY = swTile[1].floor();

return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
Expand Down
23 changes: 18 additions & 5 deletions lib/services/network_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ class NetworkStatus extends ChangeNotifier {

NetworkRequestStatus _status = NetworkRequestStatus.idle;
Timer? _autoResetTimer;


/// Rate limit countdown: seconds until slot should be free (from Overpass /api/status)
int _rateLimitWaitSeconds = 0;

int get rateLimitWaitSeconds => _rateLimitWaitSeconds;

/// Current network status
NetworkRequestStatus get status => _status;

Expand Down Expand Up @@ -50,7 +55,8 @@ class NetworkStatus extends ChangeNotifier {
});
break;
case NetworkRequestStatus.rateLimited:
_autoResetTimer = Timer(const Duration(minutes: 2), () {
// Use actual wait time + 1s buffer so countdown finishes before reset
_autoResetTimer = Timer(Duration(seconds: _rateLimitWaitSeconds + 1), () {
_setStatus(NetworkRequestStatus.idle);
});
break;
Expand Down Expand Up @@ -86,9 +92,16 @@ class NetworkStatus extends ChangeNotifier {
_setStatus(NetworkRequestStatus.timeout);
}

/// Rate limited by API
void setRateLimited() {
debugPrint('[NetworkStatus] Rate limited by API');
/// Rate limited by API, with countdown duration from Overpass /api/status
void setRateLimited({int waitSeconds = 5}) {
debugPrint('[NetworkStatus] Rate limited by API (${waitSeconds}s)');
final waitChanged = _rateLimitWaitSeconds != waitSeconds;
_rateLimitWaitSeconds = waitSeconds;
Comment thread
dougborg marked this conversation as resolved.
// If already rate-limited but wait time changed, force a status transition
// so the auto-reset timer and countdown UI are updated.
if (_status == NetworkRequestStatus.rateLimited && waitChanged) {
_status = NetworkRequestStatus.idle; // allow _setStatus to re-enter
}
_setStatus(NetworkRequestStatus.rateLimited);
}

Expand Down
Loading
Loading