first attempts to pull local data from offline areas

This commit is contained in:
stopflock
2025-08-10 16:42:31 -05:00
parent 167f3cc0a0
commit 20bd04e338
4 changed files with 181 additions and 23 deletions
@@ -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});
}