mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-18 14:45:07 +02:00
more cameras -> nodes
This commit is contained in:
+9
-9
@@ -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 {
|
||||
});
|
||||
|
||||
/// Built‑in 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 {
|
||||
);
|
||||
|
||||
/// Built‑in: 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 {
|
||||
);
|
||||
|
||||
/// Built‑in: 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 {
|
||||
);
|
||||
|
||||
/// Built‑in: Genetec ALPR camera
|
||||
factory CameraProfile.genetec() => CameraProfile(
|
||||
factory NodeProfile.genetec() => NodeProfile(
|
||||
id: 'builtin-genetec',
|
||||
name: 'Genetec',
|
||||
tags: const {
|
||||
@@ -92,7 +92,7 @@ class CameraProfile {
|
||||
);
|
||||
|
||||
/// Built‑in: 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 {
|
||||
);
|
||||
|
||||
/// Built‑in: Neology ALPR camera
|
||||
factory CameraProfile.neology() => CameraProfile(
|
||||
factory NodeProfile.neology() => NodeProfile(
|
||||
id: 'builtin-neology',
|
||||
name: 'Neology',
|
||||
tags: const {
|
||||
@@ -129,7 +129,7 @@ class CameraProfile {
|
||||
);
|
||||
|
||||
/// Built‑in: 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 {
|
||||
);
|
||||
|
||||
/// Built‑in: ShotSpotter gunshot detector
|
||||
factory CameraProfile.shotspotter() => CameraProfile(
|
||||
factory NodeProfile.shotspotter() => NodeProfile(
|
||||
id: 'builtin-shotspotter',
|
||||
name: 'ShotSpotter',
|
||||
tags: const {
|
||||
@@ -160,7 +160,7 @@ class CameraProfile {
|
||||
);
|
||||
|
||||
/// Built‑in: 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;
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+18
-18
@@ -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;
|
||||
}
|
||||
+9
-9
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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)))
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user