Skip to content
Closed
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
4 changes: 3 additions & 1 deletion lib/localizations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@
"offlineModeWarning": "Downloads im Offline-Modus deaktiviert. Deaktivieren Sie den Offline-Modus, um neue Bereiche herunterzuladen.",
"areaTooBigMessage": "Zoomen Sie auf mindestens Stufe {} heran, um Offline-Bereiche herunterzuladen. Downloads großer Gebiete können die App zum Absturz bringen.",
"downloadStarted": "Download gestartet! Lade Kacheln und Knoten...",
"downloadFailed": "Download konnte nicht gestartet werden: {}"
"downloadFailed": "Download konnte nicht gestartet werden: {}",
"offlineNotPermitted": "Der {}-Server erlaubt keine Offline-Downloads. Wechseln Sie zu einem Kachelanbieter, der Offline-Nutzung unterstützt (z. B. Bing Maps, Mapbox oder ein selbst gehosteter Kachelserver).",
"currentTileProvider": "aktuelle Kachel"
},
"downloadStarted": {
"title": "Download gestartet",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Downloads disabled while in offline mode. Disable offline mode to download new areas.",
"areaTooBigMessage": "Zoom in to at least level {} to download offline areas. Large area downloads can cause the app to become unresponsive.",
"downloadStarted": "Download started! Fetching tiles and nodes...",
"downloadFailed": "Failed to start download: {}"
"downloadFailed": "Failed to start download: {}",
"offlineNotPermitted": "The {} server does not permit offline downloads. Switch to a tile provider that allows offline use (e.g., Bing Maps, Mapbox, or a self-hosted tile server).",
"currentTileProvider": "current tile"
},
"downloadStarted": {
"title": "Download Started",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Descargas deshabilitadas en modo sin conexión. Deshabilite el modo sin conexión para descargar nuevas áreas.",
"areaTooBigMessage": "Amplíe al menos al nivel {} para descargar áreas sin conexión. Las descargas de áreas grandes pueden hacer que la aplicación deje de responder.",
"downloadStarted": "¡Descarga iniciada! Obteniendo mosaicos y nodos...",
"downloadFailed": "Error al iniciar la descarga: {}"
"downloadFailed": "Error al iniciar la descarga: {}",
"offlineNotPermitted": "El servidor {} no permite descargas sin conexión. Cambie a un proveedor de mosaicos que permita el uso sin conexión (p. ej., Bing Maps, Mapbox o un servidor de mosaicos propio).",
"currentTileProvider": "mosaico actual"
},
"downloadStarted": {
"title": "Descarga Iniciada",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Téléchargements désactivés en mode hors ligne. Désactivez le mode hors ligne pour télécharger de nouvelles zones.",
"areaTooBigMessage": "Zoomez au moins au niveau {} pour télécharger des zones hors ligne. Les téléchargements de grandes zones peuvent rendre l'application non réactive.",
"downloadStarted": "Téléchargement démarré ! Récupération des tuiles et nœuds...",
"downloadFailed": "Échec du démarrage du téléchargement: {}"
"downloadFailed": "Échec du démarrage du téléchargement: {}",
"offlineNotPermitted": "Le serveur {} ne permet pas les téléchargements hors ligne. Passez à un fournisseur de tuiles qui autorise l'utilisation hors ligne (par ex., Bing Maps, Mapbox ou un serveur de tuiles auto-hébergé).",
"currentTileProvider": "tuile actuelle"
},
"downloadStarted": {
"title": "Téléchargement Démarré",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Download disabilitati in modalità offline. Disabilita la modalità offline per scaricare nuove aree.",
"areaTooBigMessage": "Ingrandisci almeno al livello {} per scaricare aree offline. I download di aree grandi possono rendere l'app non reattiva.",
"downloadStarted": "Download avviato! Recupero tile e nodi...",
"downloadFailed": "Impossibile avviare il download: {}"
"downloadFailed": "Impossibile avviare il download: {}",
"offlineNotPermitted": "Il server {} non consente i download offline. Passa a un fornitore di tile che consenta l'uso offline (ad es., Bing Maps, Mapbox o un server di tile auto-ospitato).",
"currentTileProvider": "tile attuale"
},
"downloadStarted": {
"title": "Download Avviato",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Downloads uitgeschakeld in offline modus. Schakel offline modus uit om nieuwe gebieden te downloaden.",
"areaTooBigMessage": "Zoom in tot ten minste niveau {} om offline gebieden te downloaden. Grote gebied downloads kunnen ervoor zorgen dat de app niet meer reageert.",
"downloadStarted": "Download gestart! Tiles en nodes ophalen...",
"downloadFailed": "Download starten mislukt: {}"
"downloadFailed": "Download starten mislukt: {}",
"offlineNotPermitted": "De {}-server staat geen offline downloads toe. Schakel over naar een tegelserver die offline gebruik toestaat (bijv. Bing Maps, Mapbox of een zelf gehoste tegelserver).",
"currentTileProvider": "huidige tegel"
},
"downloadStarted": {
"title": "Download Gestart",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Pobieranie wyłączone w trybie offline. Wyłącz tryb offline, aby pobierać nowe obszary.",
"areaTooBigMessage": "Przybliż do co najmniej poziomu {}, aby pobierać obszary offline. Duże pobieranie obszarów może sprawić, że aplikacja przestanie odpowiadać.",
"downloadStarted": "Pobieranie rozpoczęte! Pobieranie kafelków i węzłów...",
"downloadFailed": "Nie udało się rozpocząć pobierania: {}"
"downloadFailed": "Nie udało się rozpocząć pobierania: {}",
"offlineNotPermitted": "Serwer {} nie zezwala na pobieranie offline. Przełącz się na dostawcę kafelków, który obsługuje tryb offline (np. Bing Maps, Mapbox lub samodzielnie hostowany serwer kafelków).",
"currentTileProvider": "bieżący kafelek"
},
"downloadStarted": {
"title": "Pobieranie Rozpoczęte",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Downloads desabilitados no modo offline. Desative o modo offline para baixar novas áreas.",
"areaTooBigMessage": "Amplie para pelo menos o nível {} para baixar áreas offline. Downloads de áreas grandes podem tornar o aplicativo não responsivo.",
"downloadStarted": "Download iniciado! Buscando tiles e nós...",
"downloadFailed": "Falha ao iniciar o download: {}"
"downloadFailed": "Falha ao iniciar o download: {}",
"offlineNotPermitted": "O servidor {} não permite downloads offline. Mude para um provedor de tiles que permita uso offline (por ex., Bing Maps, Mapbox ou um servidor de tiles próprio).",
"currentTileProvider": "tile atual"
},
"downloadStarted": {
"title": "Download Iniciado",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Çevrimdışı moddayken indirmeler devre dışı. Yeni alanları indirmek için çevrimdışı modu devre dışı bırakın.",
"areaTooBigMessage": "Çevrimdışı alanları indirmek için en az {} seviyesine yakınlaştırın. Büyük alan indirmeleri uygulamanın yanıt vermemesine neden olabilir.",
"downloadStarted": "İndirme başladı! Döşemeler ve düğümler getiriliyor...",
"downloadFailed": "İndirme başlatılamadı: {}"
"downloadFailed": "İndirme başlatılamadı: {}",
"offlineNotPermitted": "{} sunucusu çevrimdışı indirmelere izin vermiyor. Çevrimdışı kullanıma izin veren bir döşeme sağlayıcısına geçin (ör. Bing Maps, Mapbox veya kendi barındırdığınız bir döşeme sunucusu).",
"currentTileProvider": "mevcut döşeme"
},
"downloadStarted": {
"title": "İndirme Başladı",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "Завантаження вимкнено в офлайн режимі. Вимкніть офлайн режим для завантаження нових областей.",
"areaTooBigMessage": "Збільште масштаб до принаймні рівня {} для завантаження офлайн областей. Великі завантаження областей можуть призвести до того, що додаток перестане відповідати.",
"downloadStarted": "Завантаження почалося! Отримання плиток та вузлів...",
"downloadFailed": "Не вдалося почати завантаження: {}"
"downloadFailed": "Не вдалося почати завантаження: {}",
"offlineNotPermitted": "Сервер {} не дозволяє офлайн-завантаження. Перейдіть на постачальника плиток, який дозволяє офлайн-використання (наприклад, Bing Maps, Mapbox або власний сервер плиток).",
"currentTileProvider": "поточна плитка"
},
"downloadStarted": {
"title": "Завантаження Почалося",
Expand Down
4 changes: 3 additions & 1 deletion lib/localizations/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@
"offlineModeWarning": "离线模式下禁用下载。禁用离线模式以下载新区域。",
"areaTooBigMessage": "请放大至至少第{}级来下载离线区域。下载大区域可能导致应用程序无响应。",
"downloadStarted": "下载已开始!正在获取瓦片和节点...",
"downloadFailed": "启动下载失败:{}"
"downloadFailed": "启动下载失败:{}",
"offlineNotPermitted": "{}服务器不允许离线下载。请切换到允许离线使用的瓦片提供商(例如 Bing Maps、Mapbox 或自托管的瓦片服务器)。",
"currentTileProvider": "当前瓦片"
},
"downloadStarted": {
"title": "下载已开始",
Expand Down
8 changes: 6 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ import 'screens/release_notes_screen.dart';
import 'screens/osm_account_screen.dart';
import 'screens/upload_queue_screen.dart';
import 'services/localization_service.dart';
import 'services/provider_tile_cache_manager.dart';
import 'services/version_service.dart';
import 'services/deep_link_service.dart';



Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();

// Initialize version service
await VersionService().init();

// Initialize localization service
await LocalizationService.instance.init();

// Resolve platform cache directory for per-provider tile caching
await ProviderTileCacheManager.init();

// Initialize deep link service
await DeepLinkService().init();
DeepLinkService().setNavigatorKey(_navigatorKey);
Expand Down
13 changes: 12 additions & 1 deletion lib/models/tile_provider.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';

import '../services/service_policy.dart';

/// A specific tile type within a provider
class TileType {
final String id;
Expand All @@ -10,7 +12,7 @@ class TileType {
final Uint8List? previewTile; // Single tile image data for preview
final int maxZoom; // Maximum zoom level for this tile type

const TileType({
TileType({
required this.id,
required this.name,
required this.urlTemplate,
Expand Down Expand Up @@ -76,6 +78,15 @@ class TileType {
/// Check if this tile type needs an API key
bool get requiresApiKey => urlTemplate.contains('{api_key}');

/// The service policy that applies to this tile type's server.
/// Cached because [urlTemplate] is immutable.
late final ServicePolicy servicePolicy =
ServicePolicyResolver.resolve(urlTemplate);

/// Whether this tile server's usage policy permits offline/bulk downloading.
/// Resolved via [ServicePolicyResolver] from the URL template.
bool get allowsOfflineDownload => servicePolicy.allowsOfflineDownload;

Map<String, dynamic> toJson() => {
'id': id,
'name': name,
Expand Down
76 changes: 41 additions & 35 deletions lib/services/deflock_tile_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:http/http.dart';
import 'package:http/retry.dart';

import '../app_state.dart';
import '../models/tile_provider.dart' as models;
import 'http_client.dart';
import 'map_data_submodules/tiles_from_local.dart';
import 'offline_area_service.dart';
Expand All @@ -16,6 +17,10 @@ import 'offline_area_service.dart';
/// built-in disk cache, RetryClient, ETag revalidation, and abort support,
/// while routing URLs through our TileType logic and supporting offline tiles.
///
/// Each instance is configured for a specific tile provider/type combination
/// with frozen config — no AppState lookups at request time (except for the
/// global offlineMode toggle).
///
/// Two runtime paths:
/// 1. **Common path** (no offline areas for current provider): delegates to
/// super.getImageWithCancelLoadingSupport() — full NetworkTileImageProvider
Expand All @@ -29,34 +34,46 @@ class DeflockTileProvider extends NetworkTileProvider {
/// will be false (we passed it in), so super.dispose() won't close it.
final Client _sharedHttpClient;

DeflockTileProvider._({required Client httpClient})
: _sharedHttpClient = httpClient,
/// Frozen config for this provider instance.
final String providerId;
final models.TileType tileType;
final String? apiKey;

DeflockTileProvider._({
required Client httpClient,
required this.providerId,
required this.tileType,
this.apiKey,
super.cachingProvider,
}) : _sharedHttpClient = httpClient,
super(
httpClient: httpClient,
silenceExceptions: true,
);

factory DeflockTileProvider() {
factory DeflockTileProvider({
required String providerId,
required models.TileType tileType,
String? apiKey,
MapCachingProvider? cachingProvider,
}) {
final client = UserAgentClient(RetryClient(Client()));
return DeflockTileProvider._(httpClient: client);
return DeflockTileProvider._(
httpClient: client,
providerId: providerId,
tileType: tileType,
apiKey: apiKey,
cachingProvider: cachingProvider,
);
}

@override
String getTileUrl(TileCoordinates coordinates, TileLayer options) {
final appState = AppState.instance;
final selectedTileType = appState.selectedTileType;
final selectedProvider = appState.selectedTileProvider;

if (selectedTileType == null || selectedProvider == null) {
// Fallback to base implementation if no provider configured
return super.getTileUrl(coordinates, options);
}

return selectedTileType.getTileUrl(
return tileType.getTileUrl(
coordinates.z,
coordinates.x,
coordinates.y,
apiKey: selectedProvider.apiKey,
apiKey: apiKey,
);
}

Expand All @@ -77,19 +94,15 @@ class DeflockTileProvider extends NetworkTileProvider {
}

// Offline-first path: check local tiles first, fall back to network.
final appState = AppState.instance;
final providerId = appState.selectedTileProvider?.id ?? 'unknown';
final tileTypeId = appState.selectedTileType?.id ?? 'unknown';

return DeflockOfflineTileImageProvider(
coordinates: coordinates,
options: options,
httpClient: _sharedHttpClient,
headers: headers,
cancelLoading: cancelLoading,
isOfflineOnly: appState.offlineMode,
isOfflineOnly: AppState.instance.offlineMode,
providerId: providerId,
tileTypeId: tileTypeId,
tileTypeId: tileType.id,
tileUrl: getTileUrl(coordinates, options),
);
}
Expand All @@ -102,25 +115,16 @@ class DeflockTileProvider extends NetworkTileProvider {
/// This avoids the offline-first path (and its filesystem searches) when
/// browsing online with providers that have no offline areas.
bool _shouldCheckOfflineCache() {
final appState = AppState.instance;

// Always use offline path in offline mode
if (appState.offlineMode) {
if (AppState.instance.offlineMode) {
return true;
}

// For online mode, only use offline path if we have relevant offline data
final currentProvider = appState.selectedTileProvider;
final currentTileType = appState.selectedTileType;

if (currentProvider == null || currentTileType == null) {
return false;
}

final offlineService = OfflineAreaService();
return offlineService.hasOfflineAreasForProvider(
currentProvider.id,
currentTileType.id,
providerId,
tileType.id,
);
}

Expand Down Expand Up @@ -263,9 +267,11 @@ class DeflockOfflineTileImageProvider
return other is DeflockOfflineTileImageProvider &&
other.coordinates == coordinates &&
other.providerId == providerId &&
other.tileTypeId == tileTypeId;
other.tileTypeId == tileTypeId &&
other.isOfflineOnly == isOfflineOnly;
}

@override
int get hashCode => Object.hash(coordinates, providerId, tileTypeId);
int get hashCode =>
Object.hash(coordinates, providerId, tileTypeId, isOfflineOnly);
}
26 changes: 18 additions & 8 deletions lib/services/map_data_submodules/nodes_from_osm_api.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:xml/xml.dart';
Expand All @@ -7,6 +8,7 @@ import '../../models/node_profile.dart';
import '../../models/osm_node.dart';
import '../../app_state.dart';
import '../http_client.dart';
import '../service_policy.dart';

/// Fetches surveillance nodes from the direct OSM API using bbox query.
/// This is a fallback for when Overpass is not available (e.g., sandbox mode).
Expand Down Expand Up @@ -58,28 +60,36 @@ Future<List<OsmNode>> _fetchFromOsmApi({
try {
debugPrint('[fetchOsmApiNodes] Querying OSM API for nodes in bbox...');
debugPrint('[fetchOsmApiNodes] URL: $url');

final response = await _client.get(Uri.parse(url));


// Enforce max 2 concurrent download threads per OSM API usage policy
await ServiceRateLimiter.acquire(ServiceType.osmEditingApi);

final http.Response response;
try {
response = await _client.get(Uri.parse(url));
} finally {
ServiceRateLimiter.release(ServiceType.osmEditingApi);
}

if (response.statusCode != 200) {
debugPrint('[fetchOsmApiNodes] OSM API error: ${response.statusCode} - ${response.body}');
throw Exception('OSM API error: ${response.statusCode} - ${response.body}');
}

// Parse XML response
final document = XmlDocument.parse(response.body);
final nodes = _parseOsmApiResponseWithConstraints(document, profiles, maxResults);

if (nodes.isNotEmpty) {
debugPrint('[fetchOsmApiNodes] Retrieved ${nodes.length} matching surveillance nodes');
}

// Don't report success here - let the top level handle it
return nodes;

} catch (e) {
debugPrint('[fetchOsmApiNodes] Exception: $e');

// Don't report status here - let the top level handle it
rethrow; // Re-throw to let caller handle
}
Expand Down
Loading
Loading