mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-15 21:48:18 +02:00
first attempts to pull local data from offline areas
This commit is contained in:
@@ -6,6 +6,8 @@ import '../models/osm_camera_node.dart';
|
||||
import '../app_state.dart';
|
||||
import 'map_data_submodules/cameras_from_overpass.dart';
|
||||
import 'map_data_submodules/tiles_from_osm.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
|
||||
|
||||
@@ -28,7 +30,8 @@ class MapDataProvider {
|
||||
AppState.instance.setOfflineMode(enabled);
|
||||
}
|
||||
|
||||
/// Fetch cameras from OSM/Overpass or local storage, depending on source/offline mode.
|
||||
/// Fetch cameras from OSM/Overpass or local storage.
|
||||
/// Remote is default. If source is MapSource.auto, remote is tried first unless offline.
|
||||
Future<List<OsmCameraNode>> getCameras({
|
||||
required LatLngBounds bounds,
|
||||
required List<CameraProfile> profiles,
|
||||
@@ -37,16 +40,13 @@ class MapDataProvider {
|
||||
}) async {
|
||||
final offline = AppState.instance.offlineMode;
|
||||
print('[MapDataProvider] getCameras called, source=$source, offlineMode=$offline');
|
||||
// Resolve source:
|
||||
if (offline && source != MapSource.local) {
|
||||
print('[MapDataProvider] BLOCKED by offlineMode for getCameras');
|
||||
throw OfflineModeException("Cannot fetch remote cameras in offline mode.");
|
||||
}
|
||||
if (source == MapSource.local) {
|
||||
// TODO: implement local camera loading
|
||||
throw UnimplementedError('Local camera loading not yet implemented.');
|
||||
} else {
|
||||
// Use Overpass remote fetch, from submodule:
|
||||
|
||||
// Explicit remote request: error if offline, else always remote
|
||||
if (source == MapSource.remote) {
|
||||
if (offline) {
|
||||
print('[MapDataProvider] BLOCKED by offlineMode for remote camera fetch');
|
||||
throw OfflineModeException("Cannot fetch remote cameras in offline mode.");
|
||||
}
|
||||
return camerasFromOverpass(
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
@@ -54,21 +54,72 @@ class MapDataProvider {
|
||||
maxCameras: AppState.instance.maxCameras,
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
maxCameras: AppState.instance.maxCameras,
|
||||
);
|
||||
} catch (e) {
|
||||
print('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.');
|
||||
return fetchLocalCameras(
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Fetch tile image bytes from OSM or local (future). Only fetches, does not save!
|
||||
/// 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 {
|
||||
print('[MapDataProvider] getTile called for $z/$x/$y, source=$source');
|
||||
if (source == MapSource.local) {
|
||||
// TODO: implement local tile loading
|
||||
throw UnimplementedError('Local tile loading not yet implemented.');
|
||||
} else {
|
||||
// Use OSM remote fetch from submodule:
|
||||
final offline = AppState.instance.offlineMode;
|
||||
print('[MapDataProvider] getTile called for $z/$x/$y, source=$source, offlineMode=$offline');
|
||||
|
||||
// Explicitly remote
|
||||
if (source == MapSource.remote) {
|
||||
if (offline) {
|
||||
print('[MapDataProvider] BLOCKED by offlineMode for remote tile fetch');
|
||||
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
|
||||
}
|
||||
return fetchOSMTile(z: z, x: x, y: 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 fetchOSMTile(z: z, x: x, y: y);
|
||||
} else {
|
||||
throw OfflineModeException("Tile $z/$x/$y not found in offline areas and offline mode is enabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
||||
import '../../models/osm_camera_node.dart';
|
||||
import '../../models/camera_profile.dart';
|
||||
import '../offline_area_service.dart';
|
||||
import '../offline_areas/offline_area_models.dart';
|
||||
|
||||
/// Fetch camera nodes from all offline areas intersecting the bounds/profile list.
|
||||
Future<List<OsmCameraNode>> fetchLocalCameras({
|
||||
required LatLngBounds bounds,
|
||||
required List<CameraProfile> profiles,
|
||||
}) async {
|
||||
final areas = OfflineAreaService().offlineAreas;
|
||||
final List<OsmCameraNode> result = [];
|
||||
final seenIds = <int>{};
|
||||
|
||||
for (final area in areas) {
|
||||
if (area.status != OfflineAreaStatus.complete) continue;
|
||||
if (!area.bounds.isOverlapping(bounds)) continue;
|
||||
|
||||
final nodes = await _loadAreaCameras(area);
|
||||
for (final cam in nodes) {
|
||||
if (seenIds.contains(cam.id)) continue;
|
||||
// Check geo bounds
|
||||
if (!_pointInBounds(cam.coord, bounds)) continue;
|
||||
// Check profiles
|
||||
if (profiles.isNotEmpty && !_matchesAnyProfile(cam, profiles)) continue;
|
||||
result.add(cam);
|
||||
seenIds.add(cam.id);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try in-memory first, else load from disk
|
||||
Future<List<OsmCameraNode>> _loadAreaCameras(OfflineArea area) async {
|
||||
if (area.cameras.isNotEmpty) {
|
||||
return area.cameras;
|
||||
}
|
||||
final file = File('${area.directory}/cameras.json');
|
||||
if (await file.exists()) {
|
||||
final str = await file.readAsString();
|
||||
final jsonList = jsonDecode(str) as List;
|
||||
return jsonList.map((e) => OsmCameraNode.fromJson(e)).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
bool _pointInBounds(LatLng pt, LatLngBounds bounds) {
|
||||
return pt.latitude >= bounds.southWest.latitude &&
|
||||
pt.latitude <= bounds.northEast.latitude &&
|
||||
pt.longitude >= bounds.southWest.longitude &&
|
||||
pt.longitude <= bounds.northEast.longitude;
|
||||
}
|
||||
|
||||
bool _matchesAnyProfile(OsmCameraNode cam, List<CameraProfile> profiles) {
|
||||
for (final prof in profiles) {
|
||||
if (_cameraMatchesProfile(cam, prof)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _cameraMatchesProfile(OsmCameraNode cam, CameraProfile profile) {
|
||||
for (final e in profile.tags.entries) {
|
||||
if (cam.tags[e.key] != e.value) return false; // All profile tags must match
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:io';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../offline_area_service.dart';
|
||||
import '../offline_areas/offline_area_models.dart';
|
||||
import '../offline_areas/offline_tile_utils.dart';
|
||||
|
||||
/// Fetch a tile from the newest offline area that plausibly contains it, or throw if not found.
|
||||
Future<List<int>> fetchLocalTile({required int z, required int x, required int y}) async {
|
||||
final areas = OfflineAreaService().offlineAreas;
|
||||
final List<_AreaTileMatch> candidates = [];
|
||||
|
||||
for (final area in areas) {
|
||||
if (area.status != OfflineAreaStatus.complete) continue;
|
||||
if (z < area.minZoom || z > area.maxZoom) continue;
|
||||
|
||||
// Get tile coverage for area at this zoom only
|
||||
final coveredTiles = computeTileList(area.bounds, z, z);
|
||||
if (coveredTiles.contains([z, x, y])) {
|
||||
final tilePath = _tilePath(area.directory, z, x, y);
|
||||
final file = File(tilePath);
|
||||
if (await file.exists()) {
|
||||
final stat = await file.stat();
|
||||
candidates.add(_AreaTileMatch(area: area, file: file, modified: stat.modified));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty) {
|
||||
throw Exception('Tile $z/$x/$y not found in any offline area');
|
||||
}
|
||||
candidates.sort((a, b) => b.modified.compareTo(a.modified)); // newest first
|
||||
return await candidates.first.file.readAsBytes();
|
||||
}
|
||||
|
||||
String _tilePath(String areaDir, int z, int x, int y) =>
|
||||
'$areaDir/tiles/$z/$x/$y.png';
|
||||
|
||||
class _AreaTileMatch {
|
||||
final OfflineArea area;
|
||||
final File file;
|
||||
final DateTime modified;
|
||||
_AreaTileMatch({required this.area, required this.file, required this.modified});
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ class TileProviderWithCache extends TileProvider {
|
||||
void _fetchAndCacheTile(TileCoordinates coords, String key, {MapSource source = MapSource.auto}) async {
|
||||
// Don't fire multiple fetches for the same tile simultaneously
|
||||
if (_tileCache.containsKey(key)) return;
|
||||
// Only block REMOTE fetch in offline mode, but allow local/offline sources in the future.
|
||||
if (AppState.instance.offlineMode && source != MapSource.local) {
|
||||
print('[TileProviderWithCache] BLOCKED tile $key due to offline mode');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final bytes = await MapDataProvider().getTile(
|
||||
z: coords.z, x: coords.x, y: coords.y, source: source,
|
||||
|
||||
Reference in New Issue
Block a user