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
+9 -9
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import 'models/camera_profile.dart';
import 'models/node_profile.dart';
import 'models/operator_profile.dart';
import 'models/osm_camera_node.dart';
import 'models/pending_upload.dart';
@@ -61,9 +61,9 @@ class AppState extends ChangeNotifier {
String get username => _authState.username;
// Profile state
List<CameraProfile> get profiles => _profileState.profiles;
List<CameraProfile> get enabledProfiles => _profileState.enabledProfiles;
bool isEnabled(CameraProfile p) => _profileState.isEnabled(p);
List<NodeProfile> get profiles => _profileState.profiles;
List<NodeProfile> get enabledProfiles => _profileState.enabledProfiles;
bool isEnabled(NodeProfile p) => _profileState.isEnabled(p);
// Operator profile state
List<OperatorProfile> get operatorProfiles => _operatorProfileState.profiles;
@@ -134,15 +134,15 @@ class AppState extends ChangeNotifier {
}
// ---------- Profile Methods ----------
void toggleProfile(CameraProfile p, bool e) {
void toggleProfile(NodeProfile p, bool e) {
_profileState.toggleProfile(p, e);
}
void addOrUpdateProfile(CameraProfile p) {
void addOrUpdateProfile(NodeProfile p) {
_profileState.addOrUpdateProfile(p);
}
void deleteProfile(CameraProfile p) {
void deleteProfile(NodeProfile p) {
_profileState.deleteProfile(p);
}
@@ -166,7 +166,7 @@ class AppState extends ChangeNotifier {
void updateSession({
double? directionDeg,
CameraProfile? profile,
NodeProfile? profile,
OperatorProfile? operatorProfile,
LatLng? target,
}) {
@@ -180,7 +180,7 @@ class AppState extends ChangeNotifier {
void updateEditSession({
double? directionDeg,
CameraProfile? profile,
NodeProfile? profile,
OperatorProfile? operatorProfile,
LatLng? target,
}) {
@@ -1,7 +1,7 @@
import 'package:uuid/uuid.dart';
/// A bundle of preset OSM tags that describe a particular camera model/type.
class CameraProfile {
/// A bundle of preset OSM tags that describe a particular surveillance node model/type.
class NodeProfile {
final String id;
final String name;
final Map<String, String> tags;
@@ -10,7 +10,7 @@ class CameraProfile {
final bool submittable;
final bool editable;
CameraProfile({
NodeProfile({
required this.id,
required this.name,
required this.tags,
@@ -21,7 +21,7 @@ class CameraProfile {
});
/// Builtin default: Generic ALPR camera (customizable template, not submittable)
factory CameraProfile.genericAlpr() => CameraProfile(
factory NodeProfile.genericAlpr() => NodeProfile(
id: 'builtin-generic-alpr',
name: 'Generic ALPR',
tags: const {
@@ -35,7 +35,7 @@ class CameraProfile {
);
/// Builtin: Flock Safety ALPR camera
factory CameraProfile.flock() => CameraProfile(
factory NodeProfile.flock() => NodeProfile(
id: 'builtin-flock',
name: 'Flock',
tags: const {
@@ -54,7 +54,7 @@ class CameraProfile {
);
/// Builtin: Motorola Solutions/Vigilant ALPR camera
factory CameraProfile.motorola() => CameraProfile(
factory NodeProfile.motorola() => NodeProfile(
id: 'builtin-motorola',
name: 'Motorola/Vigilant',
tags: const {
@@ -73,7 +73,7 @@ class CameraProfile {
);
/// Builtin: Genetec ALPR camera
factory CameraProfile.genetec() => CameraProfile(
factory NodeProfile.genetec() => NodeProfile(
id: 'builtin-genetec',
name: 'Genetec',
tags: const {
@@ -92,7 +92,7 @@ class CameraProfile {
);
/// Builtin: Leonardo/ELSAG ALPR camera
factory CameraProfile.leonardo() => CameraProfile(
factory NodeProfile.leonardo() => NodeProfile(
id: 'builtin-leonardo',
name: 'Leonardo/ELSAG',
tags: const {
@@ -111,7 +111,7 @@ class CameraProfile {
);
/// Builtin: Neology ALPR camera
factory CameraProfile.neology() => CameraProfile(
factory NodeProfile.neology() => NodeProfile(
id: 'builtin-neology',
name: 'Neology',
tags: const {
@@ -129,7 +129,7 @@ class CameraProfile {
);
/// Builtin: Generic gunshot detector (customizable template, not submittable)
factory CameraProfile.genericGunshotDetector() => CameraProfile(
factory NodeProfile.genericGunshotDetector() => NodeProfile(
id: 'builtin-generic-gunshot',
name: 'Generic Gunshot Detector',
tags: const {
@@ -143,7 +143,7 @@ class CameraProfile {
);
/// Builtin: ShotSpotter gunshot detector
factory CameraProfile.shotspotter() => CameraProfile(
factory NodeProfile.shotspotter() => NodeProfile(
id: 'builtin-shotspotter',
name: 'ShotSpotter',
tags: const {
@@ -160,7 +160,7 @@ class CameraProfile {
);
/// Builtin: Flock Raven gunshot detector
factory CameraProfile.flockRaven() => CameraProfile(
factory NodeProfile.flockRaven() => NodeProfile(
id: 'builtin-flock-raven',
name: 'Flock Raven',
tags: const {
@@ -179,7 +179,7 @@ class CameraProfile {
/// Returns true if this profile can be used for submissions
bool get isSubmittable => submittable;
CameraProfile copyWith({
NodeProfile copyWith({
String? id,
String? name,
Map<String, String>? tags,
@@ -188,7 +188,7 @@ class CameraProfile {
bool? submittable,
bool? editable,
}) =>
CameraProfile(
NodeProfile(
id: id ?? this.id,
name: name ?? this.name,
tags: tags ?? this.tags,
@@ -208,7 +208,7 @@ class CameraProfile {
'editable': editable,
};
factory CameraProfile.fromJson(Map<String, dynamic> j) => CameraProfile(
factory NodeProfile.fromJson(Map<String, dynamic> j) => NodeProfile(
id: j['id'],
name: j['name'],
tags: Map<String, String>.from(j['tags']),
@@ -221,7 +221,7 @@ class CameraProfile {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CameraProfile &&
other is NodeProfile &&
runtimeType == other.runtimeType &&
id == other.id;
+4 -4
View File
@@ -1,12 +1,12 @@
import 'package:latlong2/latlong.dart';
import 'camera_profile.dart';
import 'node_profile.dart';
import 'operator_profile.dart';
import '../state/settings_state.dart';
class PendingUpload {
final LatLng coord;
final double direction;
final CameraProfile profile;
final NodeProfile profile;
final OperatorProfile? operatorProfile;
final UploadMode uploadMode; // Capture upload destination when queued
final int? originalNodeId; // If this is an edit, the ID of the original OSM node
@@ -75,8 +75,8 @@ class PendingUpload {
coord: LatLng(j['lat'], j['lon']),
direction: j['dir'],
profile: j['profile'] is Map<String, dynamic>
? CameraProfile.fromJson(j['profile'])
: CameraProfile.genericAlpr(),
? NodeProfile.fromJson(j['profile'])
: NodeProfile.genericAlpr(),
operatorProfile: j['operatorProfile'] != null
? OperatorProfile.fromJson(j['operatorProfile'])
: null,
+2 -2
View File
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../app_state.dart';
class ProfileEditor extends StatefulWidget {
const ProfileEditor({super.key, required this.profile});
final CameraProfile profile;
final NodeProfile profile;
@override
State<ProfileEditor> createState() => _ProfileEditorState();
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:provider/provider.dart';
import '../../app_state.dart';
import '../../models/camera_profile.dart';
import '../../models/node_profile.dart';
import '../profile_editor.dart';
class ProfileListSection extends StatelessWidget {
@@ -17,13 +17,13 @@ class ProfileListSection extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Camera Profiles', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const Text('Node Profiles', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
TextButton.icon(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileEditor(
profile: CameraProfile(
profile: NodeProfile(
id: const Uuid().v4(),
name: '',
tags: const {},
@@ -111,7 +111,7 @@ class ProfileListSection extends StatelessWidget {
);
}
void _showDeleteProfileDialog(BuildContext context, CameraProfile profile) {
void _showDeleteProfileDialog(BuildContext context, NodeProfile profile) {
final appState = context.read<AppState>();
showDialog(
context: context,
+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
+18 -18
View File
@@ -1,33 +1,33 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../services/profile_service.dart';
class ProfileState extends ChangeNotifier {
static const String _enabledPrefsKey = 'enabled_profiles';
final List<CameraProfile> _profiles = [];
final Set<CameraProfile> _enabled = {};
final List<NodeProfile> _profiles = [];
final Set<NodeProfile> _enabled = {};
// Getters
List<CameraProfile> get profiles => List.unmodifiable(_profiles);
bool isEnabled(CameraProfile p) => _enabled.contains(p);
List<CameraProfile> get enabledProfiles =>
List<NodeProfile> get profiles => List.unmodifiable(_profiles);
bool isEnabled(NodeProfile p) => _enabled.contains(p);
List<NodeProfile> get enabledProfiles =>
_profiles.where(isEnabled).toList(growable: false);
// Initialize profiles from built-in and custom sources
Future<void> init() async {
// Initialize profiles: built-in + custom
_profiles.add(CameraProfile.genericAlpr());
_profiles.add(CameraProfile.flock());
_profiles.add(CameraProfile.motorola());
_profiles.add(CameraProfile.genetec());
_profiles.add(CameraProfile.leonardo());
_profiles.add(CameraProfile.neology());
_profiles.add(CameraProfile.genericGunshotDetector());
_profiles.add(CameraProfile.shotspotter());
_profiles.add(CameraProfile.flockRaven());
_profiles.add(NodeProfile.genericAlpr());
_profiles.add(NodeProfile.flock());
_profiles.add(NodeProfile.motorola());
_profiles.add(NodeProfile.genetec());
_profiles.add(NodeProfile.leonardo());
_profiles.add(NodeProfile.neology());
_profiles.add(NodeProfile.genericGunshotDetector());
_profiles.add(NodeProfile.shotspotter());
_profiles.add(NodeProfile.flockRaven());
_profiles.addAll(await ProfileService().load());
// Load enabled profile IDs from prefs
@@ -42,7 +42,7 @@ class ProfileState extends ChangeNotifier {
}
}
void toggleProfile(CameraProfile p, bool e) {
void toggleProfile(NodeProfile p, bool e) {
if (e) {
_enabled.add(p);
} else {
@@ -57,7 +57,7 @@ class ProfileState extends ChangeNotifier {
notifyListeners();
}
void addOrUpdateProfile(CameraProfile p) {
void addOrUpdateProfile(NodeProfile p) {
final idx = _profiles.indexWhere((x) => x.id == p.id);
if (idx >= 0) {
_profiles[idx] = p;
@@ -70,7 +70,7 @@ class ProfileState extends ChangeNotifier {
notifyListeners();
}
void deleteProfile(CameraProfile p) {
void deleteProfile(NodeProfile p) {
if (!p.editable) return;
_enabled.remove(p);
_profiles.removeWhere((x) => x.id == p.id);
+9 -9
View File
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../models/osm_camera_node.dart';
// ------------------ AddNodeSession ------------------
class AddNodeSession {
AddNodeSession({required this.profile, this.directionDegrees = 0});
CameraProfile profile;
NodeProfile profile;
OperatorProfile? operatorProfile;
double directionDegrees;
LatLng? target;
@@ -24,7 +24,7 @@ class EditNodeSession {
});
final OsmCameraNode originalNode; // The original node being edited
CameraProfile profile;
NodeProfile profile;
OperatorProfile? operatorProfile;
double directionDegrees;
LatLng target; // Current position (can be dragged)
@@ -38,7 +38,7 @@ class SessionState extends ChangeNotifier {
AddNodeSession? get session => _session;
EditNodeSession? get editSession => _editSession;
void startAddSession(List<CameraProfile> enabledProfiles) {
void startAddSession(List<NodeProfile> enabledProfiles) {
final submittableProfiles = enabledProfiles.where((p) => p.isSubmittable).toList();
final defaultProfile = submittableProfiles.isNotEmpty
? submittableProfiles.first
@@ -48,11 +48,11 @@ class SessionState extends ChangeNotifier {
notifyListeners();
}
void startEditSession(OsmCameraNode node, List<CameraProfile> enabledProfiles) {
void startEditSession(OsmCameraNode node, List<NodeProfile> enabledProfiles) {
final submittableProfiles = enabledProfiles.where((p) => p.isSubmittable).toList();
// Try to find a matching profile based on the node's tags
CameraProfile matchingProfile = submittableProfiles.isNotEmpty
NodeProfile matchingProfile = submittableProfiles.isNotEmpty
? submittableProfiles.first
: enabledProfiles.first;
@@ -74,7 +74,7 @@ class SessionState extends ChangeNotifier {
notifyListeners();
}
bool _profileMatchesTags(CameraProfile profile, Map<String, String> tags) {
bool _profileMatchesTags(NodeProfile profile, Map<String, String> tags) {
// Simple matching: check if all profile tags are present in node tags
for (final entry in profile.tags.entries) {
if (tags[entry.key] != entry.value) {
@@ -86,7 +86,7 @@ class SessionState extends ChangeNotifier {
void updateSession({
double? directionDeg,
CameraProfile? profile,
NodeProfile? profile,
OperatorProfile? operatorProfile,
LatLng? target,
}) {
@@ -114,7 +114,7 @@ class SessionState extends ChangeNotifier {
void updateEditSession({
double? directionDeg,
CameraProfile? profile,
NodeProfile? profile,
OperatorProfile? operatorProfile,
LatLng? target,
}) {
+2 -2
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app_state.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import 'refine_tags_sheet.dart';
@@ -64,7 +64,7 @@ class AddNodeSheet extends StatelessWidget {
const SizedBox(height: 16),
ListTile(
title: const Text('Profile'),
trailing: DropdownButton<CameraProfile>(
trailing: DropdownButton<NodeProfile>(
value: session.profile,
items: submittableProfiles
.map((p) => DropdownMenuItem(value: p, child: Text(p.name)))
+5 -5
View File
@@ -6,7 +6,7 @@ import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
import '../services/map_data_provider.dart';
import '../services/camera_cache.dart';
import '../services/network_status.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/osm_camera_node.dart';
import '../app_state.dart';
@@ -38,7 +38,7 @@ class CameraProviderWithCache extends ChangeNotifier {
/// and notifies listeners/UI when new data is available.
void fetchAndUpdate({
required LatLngBounds bounds,
required List<CameraProfile> profiles,
required List<NodeProfile> profiles,
UploadMode uploadMode = UploadMode.production,
}) {
// Fast: serve cached immediately
@@ -48,7 +48,7 @@ class CameraProviderWithCache extends ChangeNotifier {
_debounceTimer = Timer(const Duration(milliseconds: 400), () async {
try {
// Use MapSource.auto to handle both offline and online modes appropriately
final fresh = await MapDataProvider().getCameras(
final fresh = await MapDataProvider().getNodes(
bounds: bounds,
profiles: profiles,
uploadMode: uploadMode,
@@ -79,7 +79,7 @@ class CameraProviderWithCache extends ChangeNotifier {
}
/// Check if a camera matches any of the provided profiles
bool _matchesAnyProfile(OsmCameraNode camera, List<CameraProfile> profiles) {
bool _matchesAnyProfile(OsmCameraNode camera, List<NodeProfile> profiles) {
for (final profile in profiles) {
if (_cameraMatchesProfile(camera, profile)) return true;
}
@@ -87,7 +87,7 @@ class CameraProviderWithCache extends ChangeNotifier {
}
/// Check if a camera matches a specific profile (all profile tags must match)
bool _cameraMatchesProfile(OsmCameraNode camera, CameraProfile profile) {
bool _cameraMatchesProfile(OsmCameraNode camera, NodeProfile profile) {
for (final entry in profile.tags.entries) {
if (camera.tags[entry.key] != entry.value) return false;
}
+2 -2
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app_state.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
@@ -71,7 +71,7 @@ class EditNodeSheet extends StatelessWidget {
const SizedBox(height: 16),
ListTile(
title: const Text('Profile'),
trailing: DropdownButton<CameraProfile>(
trailing: DropdownButton<NodeProfile>(
value: session.profile,
items: submittableProfiles
.map((p) => DropdownMenuItem(value: p, child: Text(p.name)))
@@ -3,7 +3,7 @@ import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../models/camera_profile.dart';
import '../../models/node_profile.dart';
import '../../app_state.dart' show UploadMode;
import '../camera_provider_with_cache.dart';
import '../../dev_config.dart';
@@ -12,7 +12,7 @@ import '../../dev_config.dart';
/// Handles debounced camera fetching and profile-based cache invalidation.
class CameraRefreshController {
late final CameraProviderWithCache _cameraProvider;
List<CameraProfile>? _lastEnabledProfiles;
List<NodeProfile>? _lastEnabledProfiles;
VoidCallback? _onCamerasUpdated;
/// Initialize the camera refresh controller
@@ -32,7 +32,7 @@ class CameraRefreshController {
/// Check if camera profiles changed and handle cache clearing if needed.
/// Returns true if profiles changed (triggering a refresh).
bool checkAndHandleProfileChanges({
required List<CameraProfile> currentEnabledProfiles,
required List<NodeProfile> currentEnabledProfiles,
required VoidCallback onProfilesChanged,
}) {
if (_lastEnabledProfiles == null ||
@@ -57,7 +57,7 @@ class CameraRefreshController {
/// Refresh cameras from provider for the current map view
void refreshCamerasFromProvider({
required AnimatedMapController controller,
required List<CameraProfile> enabledProfiles,
required List<NodeProfile> enabledProfiles,
required UploadMode uploadMode,
required BuildContext context,
}) {
@@ -93,7 +93,7 @@ class CameraRefreshController {
CameraProviderWithCache get cameraProvider => _cameraProvider;
/// Helper to check if two profile lists are equal by comparing IDs
bool _profileListsEqual(List<CameraProfile> list1, List<CameraProfile> list2) {
bool _profileListsEqual(List<NodeProfile> list1, List<NodeProfile> list2) {
if (list1.length != list2.length) return false;
// Compare by profile IDs since profiles are value objects
final ids1 = list1.map((p) => p.id).toSet();
+1 -1
View File
@@ -8,7 +8,7 @@ import '../app_state.dart';
import '../services/offline_area_service.dart';
import '../services/network_status.dart';
import '../models/osm_camera_node.dart';
import '../models/camera_profile.dart';
import '../models/node_profile.dart';
import '../models/tile_provider.dart';
import 'debouncer.dart';
import 'camera_provider_with_cache.dart';