first time loading tiles from local storage!

This commit is contained in:
stopflock
2025-08-10 23:07:06 -05:00
parent 20bd04e338
commit 312f71af4b
6 changed files with 115 additions and 48 deletions
@@ -11,10 +11,10 @@ import '../offline_areas/offline_area_models.dart';
Future<List<OsmCameraNode>> fetchLocalCameras({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
int? maxCameras,
}) async {
final areas = OfflineAreaService().offlineAreas;
final List<OsmCameraNode> result = [];
final seenIds = <int>{};
final Map<int, OsmCameraNode> deduped = {};
for (final area in areas) {
if (area.status != OfflineAreaStatus.complete) continue;
@@ -22,16 +22,18 @@ Future<List<OsmCameraNode>> fetchLocalCameras({
final nodes = await _loadAreaCameras(area);
for (final cam in nodes) {
if (seenIds.contains(cam.id)) continue;
// Check geo bounds
// Deduplicate by camera ID, preferring the first occurrence
if (deduped.containsKey(cam.id)) continue;
// Within view bounds?
if (!_pointInBounds(cam.coord, bounds)) continue;
// Check profiles
// Profile filter if used
if (profiles.isNotEmpty && !_matchesAnyProfile(cam, profiles)) continue;
result.add(cam);
seenIds.add(cam.id);
deduped[cam.id] = cam;
}
}
return result;
final out = deduped.values.take(maxCameras ?? deduped.length).toList();
return out;
}
// Try in-memory first, else load from disk
@@ -8,13 +8,18 @@ import '../../models/osm_camera_node.dart';
import '../../app_state.dart';
/// Fetches cameras from the Overpass OSM API for the given bounds and profiles.
/// If fetchAllPages is true, returns all possible cameras using multiple API calls (paging with pageSize).
/// If false (the default), returns only the first page of up to pageSize results.
Future<List<OsmCameraNode>> camerasFromOverpass({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
UploadMode uploadMode = UploadMode.production,
int? maxCameras,
int pageSize = 500, // Used for both default limit and paging chunk
bool fetchAllPages = false, // True for offline area download, else just grabs first chunk
int maxTries = 3,
}) async {
if (profiles.isEmpty) return [];
const String prodEndpoint = 'https://overpass-api.de/api/interpreter';
final nodeClauses = profiles.map((profile) {
final tagFilters = profile.tags.entries
@@ -23,38 +28,71 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
return '''node\n $tagFilters\n (${bounds.southWest.latitude},${bounds.southWest.longitude},\n ${bounds.northEast.latitude},${bounds.northEast.longitude});''';
}).join('\n ');
const String prodEndpoint = 'https://overpass-api.de/api/interpreter';
final limit = maxCameras ?? AppState.instance.maxCameras;
final query = '''
[out:json][timeout:25];
(
$nodeClauses
);
out body $limit;
''';
try {
print('[camerasFromOverpass] Querying Overpass...');
print('[camerasFromOverpass] Query:\n$query');
final resp = await http.post(Uri.parse(prodEndpoint), body: {'data': query.trim()});
print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}');
if (resp.statusCode != 200) {
print('[camerasFromOverpass] Overpass failed: ${resp.body}');
// Helper for one Overpass chunk fetch
Future<List<OsmCameraNode>> fetchChunk() async {
final query = '''
[out:json][timeout:25];
(
$nodeClauses
);
out body $pageSize;
''';
try {
print('[camerasFromOverpass] Querying Overpass...');
print('[camerasFromOverpass] Query:\n$query');
final resp = await http.post(Uri.parse(prodEndpoint), body: {'data': query.trim()});
print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}');
if (resp.statusCode != 200) {
print('[camerasFromOverpass] Overpass failed: ${resp.body}');
return [];
}
final data = jsonDecode(resp.body) as Map<String, dynamic>;
final elements = data['elements'] as List<dynamic>;
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
return elements.whereType<Map<String, dynamic>>().map((e) {
return OsmCameraNode(
id: e['id'],
coord: LatLng(e['lat'], e['lon']),
tags: Map<String, String>.from(e['tags'] ?? {}),
);
}).toList();
} catch (e) {
print('[camerasFromOverpass] Overpass exception: $e');
return [];
}
final data = jsonDecode(resp.body) as Map<String, dynamic>;
final elements = data['elements'] as List<dynamic>;
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
return elements.whereType<Map<String, dynamic>>().map((e) {
return OsmCameraNode(
id: e['id'],
coord: LatLng(e['lat'], e['lon']),
tags: Map<String, String>.from(e['tags'] ?? {}),
);
}).toList();
} catch (e) {
print('[camerasFromOverpass] Overpass exception: $e');
return [];
}
if (!fetchAllPages) {
// Just one page
return await fetchChunk();
} else {
// Fetch all possible data, paging with deduplication and backoff
final seenIds = <int>{};
final allCameras = <OsmCameraNode>[];
int page = 0;
while (true) {
page++;
List<OsmCameraNode> pageCameras = [];
int tries = 0;
while (tries < maxTries) {
try {
final cams = await fetchChunk();
pageCameras = cams.where((c) => !seenIds.contains(c.id)).toList();
break;
} catch (e) {
tries++;
final delayMs = 400 * (1 << tries);
print('[camerasFromOverpass][paged] Error on page $page try $tries: $e. Retrying in ${delayMs}ms.');
await Future.delayed(Duration(milliseconds: delayMs));
}
}
if (pageCameras.isEmpty) break;
print('[camerasFromOverpass][paged] Page $page: got ${pageCameras.length} new cameras.');
allCameras.addAll(pageCameras);
seenIds.addAll(pageCameras.map((c) => c.id));
if (pageCameras.length < pageSize) break;
}
print('[camerasFromOverpass][paged] DONE. Found ${allCameras.length} cameras for download.');
return allCameras;
}
}
@@ -15,7 +15,8 @@ Future<List<int>> fetchLocalTile({required int z, required int x, required int y
// Get tile coverage for area at this zoom only
final coveredTiles = computeTileList(area.bounds, z, z);
if (coveredTiles.contains([z, x, y])) {
final hasTile = coveredTiles.any((tile) => tile[0] == z && tile[1] == x && tile[2] == y);
if (hasTile) {
final tilePath = _tilePath(area.directory, z, x, y);
final file = File(tilePath);
if (await file.exists()) {