more cameras -> nodes

This commit is contained in:
stopflock
2025-08-29 18:20:42 -05:00
parent eeedbd7da7
commit a8ac237317
17 changed files with 127 additions and 126 deletions
+18 -17
View File
@@ -2,12 +2,12 @@ import 'package:latlong2/latlong.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter/foundation.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/osm_camera_node.dart';
import '../app_state.dart';
import 'map_data_submodules/cameras_from_overpass.dart';
import 'map_data_submodules/nodes_from_overpass.dart';
import 'map_data_submodules/tiles_from_remote.dart';
import 'map_data_submodules/cameras_from_local.dart';
import 'map_data_submodules/nodes_from_local.dart';
import 'map_data_submodules/tiles_from_local.dart';
enum MapSource { local, remote, auto } // For future use
@@ -33,9 +33,9 @@ class MapDataProvider {
/// Fetch surveillance nodes from OSM/Overpass or local storage.
/// Remote is default. If source is MapSource.auto, remote is tried first unless offline.
Future<List<OsmCameraNode>> getCameras({
Future<List<OsmCameraNode>> getNodes({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
required List<NodeProfile> profiles,
UploadMode uploadMode = UploadMode.production,
MapSource source = MapSource.auto,
}) async {
@@ -46,7 +46,7 @@ class MapDataProvider {
if (offline) {
throw OfflineModeException("Cannot fetch remote nodes in offline mode.");
}
return camerasFromOverpass(
return fetchOverpassNodes(
bounds: bounds,
profiles: profiles,
uploadMode: uploadMode,
@@ -56,7 +56,7 @@ class MapDataProvider {
// Explicit local request: always use local
if (source == MapSource.local) {
return fetchLocalCameras(
return fetchLocalNodes(
bounds: bounds,
profiles: profiles,
);
@@ -64,14 +64,15 @@ class MapDataProvider {
// AUTO: default = remote first, fallback to local only if offline
if (offline) {
return fetchLocalCameras(
return fetchLocalNodes(
bounds: bounds,
profiles: profiles,
maxNodes: AppState.instance.maxCameras,
);
} else {
// Try remote, fallback to local ONLY if remote throws (optional, could be removed for stricter behavior)
try {
return await camerasFromOverpass(
return await fetchOverpassNodes(
bounds: bounds,
profiles: profiles,
uploadMode: uploadMode,
@@ -79,20 +80,20 @@ class MapDataProvider {
);
} catch (e) {
debugPrint('[MapDataProvider] Remote node fetch failed, error: $e. Falling back to local.');
return fetchLocalCameras(
bounds: bounds,
profiles: profiles,
maxCameras: AppState.instance.maxCameras,
);
return fetchLocalNodes(
bounds: bounds,
profiles: profiles,
maxNodes: AppState.instance.maxCameras,
);
}
}
}
/// Bulk/paged node fetch for offline downloads (handling paging, dedup, and Overpass retries)
/// Only use for offline area download, not for map browsing! Ignores maxCameras config.
Future<List<OsmCameraNode>> getAllCamerasForDownload({
Future<List<OsmCameraNode>> getAllNodesForDownload({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
required List<NodeProfile> profiles,
UploadMode uploadMode = UploadMode.production,
int pageSize = 500,
int maxTries = 3,
@@ -101,7 +102,7 @@ class MapDataProvider {
if (offline) {
throw OfflineModeException("Cannot fetch remote nodes for offline area download in offline mode.");
}
return camerasFromOverpass(
return fetchOverpassNodes(
bounds: bounds,
profiles: profiles,
uploadMode: uploadMode,
@@ -3,15 +3,15 @@ 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 '../../models/node_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({
/// Fetch surveillance nodes from all offline areas intersecting the bounds/profile list.
Future<List<OsmCameraNode>> fetchLocalNodes({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
int? maxCameras,
required List<NodeProfile> profiles,
int? maxNodes,
}) async {
final areas = OfflineAreaService().offlineAreas;
final Map<int, OsmCameraNode> deduped = {};
@@ -20,24 +20,24 @@ Future<List<OsmCameraNode>> fetchLocalCameras({
if (area.status != OfflineAreaStatus.complete) continue;
if (!area.bounds.isOverlapping(bounds)) continue;
final nodes = await _loadAreaCameras(area);
for (final cam in nodes) {
// Deduplicate by camera ID, preferring the first occurrence
if (deduped.containsKey(cam.id)) continue;
final nodes = await _loadAreaNodes(area);
for (final node in nodes) {
// Deduplicate by node ID, preferring the first occurrence
if (deduped.containsKey(node.id)) continue;
// Within view bounds?
if (!_pointInBounds(cam.coord, bounds)) continue;
if (!_pointInBounds(node.coord, bounds)) continue;
// Profile filter if used
if (profiles.isNotEmpty && !_matchesAnyProfile(cam, profiles)) continue;
deduped[cam.id] = cam;
if (profiles.isNotEmpty && !_matchesAnyProfile(node, profiles)) continue;
deduped[node.id] = node;
}
}
final out = deduped.values.take(maxCameras ?? deduped.length).toList();
final out = deduped.values.take(maxNodes ?? deduped.length).toList();
return out;
}
// Try in-memory first, else load from disk
Future<List<OsmCameraNode>> _loadAreaCameras(OfflineArea area) async {
Future<List<OsmCameraNode>> _loadAreaNodes(OfflineArea area) async {
if (area.cameras.isNotEmpty) {
return area.cameras;
}
@@ -57,16 +57,16 @@ bool _pointInBounds(LatLng pt, LatLngBounds bounds) {
pt.longitude <= bounds.northEast.longitude;
}
bool _matchesAnyProfile(OsmCameraNode cam, List<CameraProfile> profiles) {
bool _matchesAnyProfile(OsmCameraNode node, List<NodeProfile> profiles) {
for (final prof in profiles) {
if (_cameraMatchesProfile(cam, prof)) return true;
if (_nodeMatchesProfile(node, prof)) return true;
}
return false;
}
bool _cameraMatchesProfile(OsmCameraNode cam, CameraProfile profile) {
bool _nodeMatchesProfile(OsmCameraNode node, NodeProfile profile) {
for (final e in profile.tags.entries) {
if (cam.tags[e.key] != e.value) return false; // All profile tags must match
if (node.tags[e.key] != e.value) return false; // All profile tags must match
}
return true;
}
@@ -4,15 +4,15 @@ import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/flutter_map.dart';
import '../../models/camera_profile.dart';
import '../../models/node_profile.dart';
import '../../models/osm_camera_node.dart';
import '../../app_state.dart';
import '../network_status.dart';
/// Fetches surveillance nodes from the Overpass OSM API for the given bounds and profiles.
Future<List<OsmCameraNode>> camerasFromOverpass({
Future<List<OsmCameraNode>> fetchOverpassNodes({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
required List<NodeProfile> profiles,
UploadMode uploadMode = UploadMode.production,
required int maxResults,
}) async {
@@ -24,8 +24,8 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
final query = _buildOverpassQuery(bounds, profiles, maxResults);
try {
debugPrint('[camerasFromOverpass] Querying Overpass for surveillance nodes...');
debugPrint('[camerasFromOverpass] Query:\n$query');
debugPrint('[fetchOverpassNodes] Querying Overpass for surveillance nodes...');
debugPrint('[fetchOverpassNodes] Query:\n$query');
final response = await http.post(
Uri.parse(overpassEndpoint),
@@ -33,7 +33,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
);
if (response.statusCode != 200) {
debugPrint('[camerasFromOverpass] Overpass API error: ${response.body}');
debugPrint('[fetchOverpassNodes] Overpass API error: ${response.body}');
NetworkStatus.instance.reportOverpassIssue();
return [];
}
@@ -42,7 +42,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
final elements = data['elements'] as List<dynamic>;
if (elements.length > 20) {
debugPrint('[camerasFromOverpass] Retrieved ${elements.length} surveillance nodes');
debugPrint('[fetchOverpassNodes] Retrieved ${elements.length} surveillance nodes');
}
NetworkStatus.instance.reportOverpassSuccess();
@@ -56,7 +56,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
}).toList();
} catch (e) {
debugPrint('[camerasFromOverpass] Exception: $e');
debugPrint('[fetchOverpassNodes] Exception: $e');
// Report network issues for connection errors
if (e.toString().contains('Connection refused') ||
@@ -70,7 +70,7 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
}
/// Builds an Overpass API query for surveillance nodes matching the given profiles within bounds.
String _buildOverpassQuery(LatLngBounds bounds, List<CameraProfile> profiles, int maxResults) {
String _buildOverpassQuery(LatLngBounds bounds, List<NodeProfile> profiles, int maxResults) {
// Build node clauses for each profile
final nodeClauses = profiles.map((profile) {
// Convert profile tags to Overpass filter format
@@ -147,7 +147,7 @@ class OfflineAreaDownloader {
}) async {
// Calculate expanded camera bounds that cover the entire tile area at minimum zoom
final cameraBounds = _calculateCameraBounds(bounds, minZoom);
final cameras = await MapDataProvider().getAllCamerasForDownload(
final cameras = await MapDataProvider().getAllNodesForDownload(
bounds: cameraBounds,
profiles: AppState.instance.profiles, // Use ALL profiles, not just enabled ones
);
+4 -4
View File
@@ -1,20 +1,20 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
class ProfileService {
static const _key = 'custom_profiles';
Future<List<CameraProfile>> load() async {
Future<List<NodeProfile>> load() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
final list = jsonDecode(jsonStr) as List<dynamic>;
return list.map((e) => CameraProfile.fromJson(e)).toList();
return list.map((e) => NodeProfile.fromJson(e)).toList();
}
Future<void> save(List<CameraProfile> profiles) async {
Future<void> save(List<NodeProfile> profiles) async {
final prefs = await SharedPreferences.getInstance();
// MUST convert to List before jsonEncode; the previous MappedIterable