import 'package:latlong2/latlong.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter/foundation.dart'; import '../models/camera_profile.dart'; import '../models/osm_camera_node.dart'; import '../app_state.dart'; import 'map_data_submodules/cameras_from_overpass.dart'; import 'map_data_submodules/tiles_from_remote.dart'; import 'map_data_submodules/cameras_from_local.dart'; import 'map_data_submodules/tiles_from_local.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._(); // REMOVED: AppState get _appState => AppState(); bool get isOfflineMode => AppState.instance.offlineMode; void setOfflineMode(bool enabled) { AppState.instance.setOfflineMode(enabled); } /// Fetch cameras from OSM/Overpass or local storage. /// Remote is default. If source is MapSource.auto, remote is tried first unless offline. Future> getCameras({ required LatLngBounds bounds, required List profiles, UploadMode uploadMode = UploadMode.production, MapSource source = MapSource.auto, }) async { final offline = AppState.instance.offlineMode; // Explicit remote request: error if offline, else always remote if (source == MapSource.remote) { if (offline) { throw OfflineModeException("Cannot fetch remote cameras in offline mode."); } return camerasFromOverpass( bounds: bounds, profiles: profiles, uploadMode: uploadMode, pageSize: AppState.instance.maxCameras, fetchAllPages: false, ); } // Explicit local request: always use local if (source == MapSource.local) { return fetchLocalCameras( bounds: bounds, profiles: profiles, ); } // AUTO: default = remote first, fallback to local only if offline if (offline) { return fetchLocalCameras( bounds: bounds, profiles: profiles, ); } else { // Try remote, fallback to local ONLY if remote throws (optional, could be removed for stricter behavior) try { return await camerasFromOverpass( bounds: bounds, profiles: profiles, uploadMode: uploadMode, pageSize: AppState.instance.maxCameras, ); } catch (e) { debugPrint('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.'); return fetchLocalCameras( bounds: bounds, profiles: profiles, maxCameras: AppState.instance.maxCameras, ); } } } /// Bulk/paged camera fetch for offline downloads (handling paging, dedup, and Overpass retries) /// Only use for offline area download, not for map browsing! Ignores maxCameras config. Future> getAllCamerasForDownload({ required LatLngBounds bounds, required List profiles, UploadMode uploadMode = UploadMode.production, int pageSize = 500, int maxTries = 3, }) async { final offline = AppState.instance.offlineMode; if (offline) { throw OfflineModeException("Cannot fetch remote cameras for offline area download in offline mode."); } return camerasFromOverpass( bounds: bounds, profiles: profiles, uploadMode: uploadMode, fetchAllPages: true, pageSize: pageSize, maxTries: maxTries, ); } /// Fetch tile image bytes. Default is to try local first, then remote if not offline. Honors explicit source. Future> 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 Future> _fetchRemoteTileFromCurrentProvider(int z, int x, int y) async { final appState = AppState.instance; final selectedTileType = appState.selectedTileType; final selectedProvider = appState.selectedTileProvider; // We guarantee that a provider and tile type are always selected 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); return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl); } /// Clear any queued tile requests (call when map view changes significantly) void clearTileQueue() { clearRemoteTileQueue(); } }