mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-03-21 18:33:51 +00:00
Replace our custom tile pipeline (fetchRemoteTile / _SimpleSemaphore / exponential backoff) with flutter_map's built-in NetworkTileProvider, gaining persistent disk cache, ETag revalidation, RetryClient, and obsolete request aborting for free. DeflockTileProvider now extends NetworkTileProvider and overrides getTileUrl() to route through TileType.getTileUrl() (quadkey, subdomains, API keys). getImageWithCancelLoadingSupport() routes between two paths at runtime: the common network path (super) when no offline areas exist, and a DeflockOfflineTileImageProvider for offline-first when they do. - Delete tiles_from_remote.dart (semaphore, retry loop, spatial helpers) - Simplify MapDataProvider._fetchRemoteTileFromCurrentProvider to plain http.get (only used by offline area downloader now) - Remove dead clearTileQueue/clearTileQueueSelective from MapDataProvider - Remove 7 tile fetch constants from dev_config.dart - TileLayerManager now disposes provider on cache clear and uses actual urlTemplate for cache key generation - 9 new tests covering URL delegation, routing, and equality Closes #87 Phase 2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
165 lines
5.8 KiB
Dart
165 lines
5.8 KiB
Dart
import 'package:latlong2/latlong.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
import '../models/node_profile.dart';
|
|
import '../models/osm_node.dart';
|
|
import '../app_state.dart';
|
|
import 'http_client.dart';
|
|
import 'map_data_submodules/tiles_from_local.dart';
|
|
import 'node_data_manager.dart';
|
|
import 'node_spatial_cache.dart';
|
|
|
|
enum MapSource { local, remote, auto } // For future use
|
|
|
|
class OfflineModeException implements Exception {
|
|
final String message;
|
|
OfflineModeException(this.message);
|
|
@override
|
|
String toString() => 'OfflineModeException: $message';
|
|
}
|
|
|
|
class MapDataProvider {
|
|
static final MapDataProvider _instance = MapDataProvider._();
|
|
factory MapDataProvider() => _instance;
|
|
MapDataProvider._();
|
|
|
|
final NodeDataManager _nodeDataManager = NodeDataManager();
|
|
final UserAgentClient _httpClient = UserAgentClient();
|
|
|
|
bool get isOfflineMode => AppState.instance.offlineMode;
|
|
void setOfflineMode(bool enabled) {
|
|
AppState.instance.setOfflineMode(enabled);
|
|
}
|
|
|
|
/// Fetch surveillance nodes using the new simplified system.
|
|
/// Returns cached data immediately if available, otherwise fetches from appropriate source.
|
|
Future<List<OsmNode>> getNodes({
|
|
required LatLngBounds bounds,
|
|
required List<NodeProfile> profiles,
|
|
UploadMode uploadMode = UploadMode.production,
|
|
MapSource source = MapSource.auto,
|
|
bool isUserInitiated = false,
|
|
}) async {
|
|
return _nodeDataManager.getNodesFor(
|
|
bounds: bounds,
|
|
profiles: profiles,
|
|
uploadMode: uploadMode,
|
|
isUserInitiated: isUserInitiated,
|
|
);
|
|
}
|
|
|
|
/// Bulk node fetch for offline downloads using new system
|
|
Future<List<OsmNode>> getAllNodesForDownload({
|
|
required LatLngBounds bounds,
|
|
required List<NodeProfile> profiles,
|
|
UploadMode uploadMode = UploadMode.production,
|
|
int maxResults = 0, // 0 = no limit for offline downloads
|
|
int maxTries = 3,
|
|
}) async {
|
|
if (AppState.instance.offlineMode) {
|
|
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);
|
|
}
|
|
|
|
/// Fetch tile image bytes. Default is to try local first, then remote if not offline. Honors explicit source.
|
|
Future<List<int>> getTile({
|
|
required int z,
|
|
required int x,
|
|
required int y,
|
|
MapSource source = MapSource.auto,
|
|
}) async {
|
|
final offline = AppState.instance.offlineMode;
|
|
|
|
// Explicitly remote
|
|
if (source == MapSource.remote) {
|
|
if (offline) {
|
|
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
|
|
}
|
|
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
|
}
|
|
|
|
// Explicitly local
|
|
if (source == MapSource.local) {
|
|
return fetchLocalTile(z: z, x: x, y: y);
|
|
}
|
|
|
|
// AUTO (default): try local first, then remote if not offline
|
|
try {
|
|
return await fetchLocalTile(z: z, x: x, y: y);
|
|
} catch (_) {
|
|
if (!offline) {
|
|
return _fetchRemoteTileFromCurrentProvider(z, x, y);
|
|
} else {
|
|
throw OfflineModeException("Tile $z/$x/$y not found in offline areas and offline mode is enabled.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fetch remote tile using current provider from AppState.
|
|
/// Only used by offline area downloader — the main tile pipeline now goes
|
|
/// through NetworkTileProvider (see DeflockTileProvider).
|
|
Future<List<int>> _fetchRemoteTileFromCurrentProvider(int z, int x, int y) async {
|
|
final appState = AppState.instance;
|
|
final selectedTileType = appState.selectedTileType;
|
|
final selectedProvider = appState.selectedTileProvider;
|
|
|
|
if (selectedTileType == null || selectedProvider == null) {
|
|
throw Exception('No tile provider selected - this should never happen');
|
|
}
|
|
|
|
final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey);
|
|
final resp = await _httpClient.get(Uri.parse(tileUrl));
|
|
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
|
return resp.bodyBytes;
|
|
}
|
|
throw Exception('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
|
|
}
|
|
|
|
/// Add or update nodes in cache (for upload queue integration)
|
|
void addOrUpdateNodes(List<OsmNode> nodes) {
|
|
_nodeDataManager.addOrUpdateNodes(nodes);
|
|
}
|
|
|
|
/// NodeCache compatibility - alias for addOrUpdateNodes
|
|
void addOrUpdate(List<OsmNode> nodes) {
|
|
addOrUpdateNodes(nodes);
|
|
}
|
|
|
|
/// Remove node from cache (for deletions)
|
|
void removeNodeById(int nodeId) {
|
|
_nodeDataManager.removeNodeById(nodeId);
|
|
}
|
|
|
|
/// Clear cache (when profiles change)
|
|
void clearCache() {
|
|
_nodeDataManager.clearCache();
|
|
}
|
|
|
|
/// Force refresh current area (manual retry)
|
|
Future<void> refreshArea({
|
|
required LatLngBounds bounds,
|
|
required List<NodeProfile> profiles,
|
|
UploadMode uploadMode = UploadMode.production,
|
|
}) async {
|
|
return _nodeDataManager.refreshArea(
|
|
bounds: bounds,
|
|
profiles: profiles,
|
|
uploadMode: uploadMode,
|
|
);
|
|
}
|
|
|
|
/// NodeCache compatibility methods for upload queue
|
|
/// These all delegate to the singleton cache to ensure consistency
|
|
OsmNode? getNodeById(int nodeId) => NodeSpatialCache().getNodeById(nodeId);
|
|
void removePendingEditMarker(int nodeId) => NodeSpatialCache().removePendingEditMarker(nodeId);
|
|
void removePendingDeletionMarker(int nodeId) => NodeSpatialCache().removePendingDeletionMarker(nodeId);
|
|
void removeTempNodeById(int tempNodeId) => NodeSpatialCache().removeTempNodeById(tempNodeId);
|
|
List<OsmNode> findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) =>
|
|
NodeSpatialCache().findNodesWithinDistance(coord, distanceMeters, excludeNodeId: excludeNodeId);
|
|
|
|
/// Check if we have good cache coverage for the given area (prevents submission in uncovered areas)
|
|
bool hasGoodCoverageFor(LatLngBounds bounds) => NodeSpatialCache().hasDataFor(bounds);
|
|
} |