tbh not sure, left for a bit. sorry. pretty sure closer than last commit..

This commit is contained in:
stopflock
2025-08-09 22:44:24 -05:00
parent 000bb6c567
commit 63675bd18a
6 changed files with 36 additions and 122 deletions

View File

@@ -131,6 +131,7 @@ class AppState extends ChangeNotifier {
}
// Ensure AuthService follows loaded mode
_auth.setUploadMode(_uploadMode);
print('AppState: AuthService mode now updated to $_uploadMode');
await _loadQueue();

View File

@@ -21,10 +21,11 @@ class MapDataProvider {
factory MapDataProvider() => _instance;
MapDataProvider._();
bool _offlineMode = false;
bool get isOfflineMode => _offlineMode;
AppState get _appState => AppState(); // Use singleton for now
bool get isOfflineMode => _appState.offlineMode;
void setOfflineMode(bool enabled) {
_offlineMode = enabled;
_appState.setOfflineMode(enabled);
}
/// Fetch cameras from OSM/Overpass or local storage, depending on source/offline mode.
@@ -34,8 +35,10 @@ class MapDataProvider {
UploadMode uploadMode = UploadMode.production,
MapSource source = MapSource.auto,
}) async {
print('[MapDataProvider] getCameras called, source=$source, offlineMode=$isOfflineMode');
// Resolve source:
if (_offlineMode && source != MapSource.local) {
if (isOfflineMode && source != MapSource.local) {
print('[MapDataProvider] BLOCKED by offlineMode for getCameras');
throw OfflineModeException("Cannot fetch remote cameras in offline mode.");
}
if (source == MapSource.local) {
@@ -53,8 +56,9 @@ class MapDataProvider {
required int y,
MapSource source = MapSource.auto,
}) async {
print('[MapDataProvider] getTile called for $z/$x/$y');
if (_offlineMode && source != MapSource.local) {
print('[MapDataProvider] getTile called for $z/$x/$y, source=$source, offlineMode=$isOfflineMode');
if (isOfflineMode && source != MapSource.local) {
print('[MapDataProvider] BLOCKED by offlineMode for $z/$x/$y');
throw OfflineModeException("Cannot fetch remote tiles in offline mode.");
}
if (source == MapSource.local) {

View File

@@ -9,11 +9,17 @@ final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
/// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit.
/// Returns tile image bytes, or throws on persistent failure.
import '../../app_state.dart';
Future<List<int>> fetchOSMTile({
required int z,
required int x,
required int y,
}) async {
if (AppState().offlineMode) {
print('[fetchOSMTile] BLOCKED by offline mode ($z/$x/$y)');
throw Exception('Offline mode enabled—cannot fetch OSM tile.');
}
final url = 'https://tile.openstreetmap.org/$z/$x/$y.png';
const int maxAttempts = 3;
int attempt = 0;

View File

@@ -6,8 +6,11 @@ import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
import 'package:path_provider/path_provider.dart';
import 'offline_areas/offline_area_models.dart';
import 'offline_areas/offline_tile_utils.dart';
import 'offline_areas/offline_area_service_tile_fetch.dart';
import 'offline_areas/offline_area_service_tile_fetch.dart'; // Only used for file IO during area downloads.
import '../models/osm_camera_node.dart';
import '../app_state.dart';
import 'map_data_provider.dart';
import 'map_data_submodules/cameras_from_overpass.dart';
/// Service for managing download, storage, and retrieval of offline map areas and cameras.
class OfflineAreaService {
@@ -213,7 +216,11 @@ class OfflineAreaService {
for (final tile in tilesToFetch) {
if (area.status == OfflineAreaStatus.cancelled) break;
try {
await downloadTile(tile[0], tile[1], tile[2], directory);
final bytes = await MapDataProvider().getTile(
z: tile[0], x: tile[1], y: tile[2], source: MapSource.remote);
if (bytes.isNotEmpty) {
await saveTileBytes(tile[0], tile[1], tile[2], directory, bytes);
}
totalDone++;
doneThisPass++;
area.tilesDownloaded = totalDone;
@@ -238,7 +245,10 @@ class OfflineAreaService {
}
if (!area.isPermanent) {
final cameras = await downloadAllCameras(bounds);
final cameras = await camerasFromOverpass(
bounds: bounds,
profiles: AppState().enabledProfiles,
);
area.cameras = cameras;
await saveCameras(cameras, directory);
} else {

View File

@@ -1,55 +1,18 @@
import 'dart:math';
import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
import '../../models/osm_camera_node.dart';
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
Future<void> downloadTile(int z, int x, int y, String baseDir) async {
final url = 'https://tile.openstreetmap.org/$z/$x/$y.png';
/// Disk IO utilities for offline area file management ONLY. No network requests should occur here.
/// Save-to-disk for a tile that has already been fetched elsewhere.
Future<void> saveTileBytes(int z, int x, int y, String baseDir, List<int> bytes) async {
final dir = Directory('$baseDir/tiles/$z/$x');
await dir.create(recursive: true);
final file = File('${dir.path}/$y.png');
if (await file.exists()) return; // already downloaded
const int maxAttempts = 3;
int attempt = 0;
final random = Random();
final delays = [0, 3000 + random.nextInt(1000) - 500, 10000 + random.nextInt(4000) - 2000];
while (true) {
try {
attempt++;
final resp = await http.get(Uri.parse(url));
if (resp.statusCode == 200) {
await file.writeAsBytes(resp.bodyBytes);
return;
} else {
throw Exception('Failed to download tile $z/$x/$y (status \\${resp.statusCode})');
}
} catch (e) {
if (attempt >= maxAttempts) {
throw Exception("Failed to download tile $z/$x/$y after $attempt attempts: $e");
}
final delay = delays[attempt-1].clamp(0, 60000);
await Future.delayed(Duration(milliseconds: delay));
}
}
}
Future<List<OsmCameraNode>> downloadAllCameras(LatLngBounds bounds) async {
final sw = bounds.southWest;
final ne = bounds.northEast;
final bbox = [sw.latitude, sw.longitude, ne.latitude, ne.longitude].join(',');
final query = '[out:json][timeout:60];node["man_made"="surveillance"]["camera:mount"="pole"]($bbox);out body;';
final url = 'https://overpass-api.de/api/interpreter';
final resp = await http.post(Uri.parse(url), body: { 'data': query });
if (resp.statusCode != 200) {
throw Exception('Failed to fetch cameras');
}
final data = jsonDecode(resp.body);
return (data['elements'] as List<dynamic>?)?.map((e) => OsmCameraNode.fromJson(e)).toList() ?? [];
await file.writeAsBytes(bytes);
}
/// Save-to-disk for cameras.json; called only by OfflineAreaService during area download
Future<void> saveCameras(List<OsmCameraNode> cams, String dir) async {
final file = File('$dir/cameras.json');
await file.writeAsString(jsonEncode(cams.map((c) => c.toJson()).toList()));

View File

@@ -1,70 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../models/camera_profile.dart';
import '../models/osm_camera_node.dart';
import '../app_state.dart';
class OverpassService {
static const _prodEndpoint = 'https://overpass-api.de/api/interpreter';
static const _sandboxEndpoint = 'https://overpass-api.dev.openstreetmap.org/api/interpreter';
// You can pass UploadMode, or use production by default
Future<List<OsmCameraNode>> fetchCameras(
LatLngBounds bbox,
List<CameraProfile> profiles,
{UploadMode uploadMode = UploadMode.production}
) async {
if (profiles.isEmpty) return [];
// Build one node query per enabled profile (each with all its tags required)
final nodeClauses = profiles.map((profile) {
final tagFilters = profile.tags.entries
.map((e) => '["${e.key}"="${e.value}"]')
.join('\n ');
return '''node\n $tagFilters\n (${bbox.southWest.latitude},${bbox.southWest.longitude},\n ${bbox.northEast.latitude},${bbox.northEast.longitude});''';
}).join('\n ');
final query = '''
[out:json][timeout:25];
(
$nodeClauses
);
out body 250;
''';
Future<List<OsmCameraNode>> fetchFromUri(String endpoint, String query) async {
try {
print('[Overpass] Querying $endpoint');
print('[Overpass] Query:\n$query');
final resp = await http.post(Uri.parse(endpoint), body: {'data': query.trim()});
print('[Overpass] Status: \\${resp.statusCode}, Length: \\${resp.body.length}');
if (resp.statusCode != 200) {
print('[Overpass] Failed: \\${resp.body}');
return [];
}
final data = jsonDecode(resp.body) as Map<String, dynamic>;
final elements = data['elements'] as List<dynamic>;
print('[Overpass] 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('[Overpass] Exception: \\${e}');
// Network error return empty list silently
return [];
}
}
// Fetch from production Overpass for all modes.
return await fetchFromUri(_prodEndpoint, query);
}
}