From 395ef77fe3dd21323a779e7c80e1be1642645430 Mon Sep 17 00:00:00 2001 From: stopflock Date: Fri, 29 Aug 2025 13:25:25 -0500 Subject: [PATCH] gunshot detection - direction optional as defined by profile --- lib/models/camera_profile.dart | 62 +++++++++++++++++++++++++++++- lib/screens/profile_editor.dart | 14 ++++++- lib/services/uploader.dart | 6 ++- lib/state/profile_state.dart | 3 ++ lib/state/upload_queue_state.dart | 8 +++- lib/widgets/add_camera_sheet.dart | 20 +++++++++- lib/widgets/edit_camera_sheet.dart | 20 +++++++++- 7 files changed, 124 insertions(+), 9 deletions(-) diff --git a/lib/models/camera_profile.dart b/lib/models/camera_profile.dart index d1ae17f..e0dfb57 100644 --- a/lib/models/camera_profile.dart +++ b/lib/models/camera_profile.dart @@ -6,12 +6,14 @@ class CameraProfile { final String name; final Map tags; final bool builtin; + final bool requiresDirection; CameraProfile({ required this.id, required this.name, required this.tags, this.builtin = false, + this.requiresDirection = true, }); /// Built‑in default: Generic ALPR camera (view-only) @@ -23,6 +25,7 @@ class CameraProfile { 'surveillance:type': 'ALPR', }, builtin: true, + requiresDirection: true, ); /// Built‑in: Flock Safety ALPR camera @@ -39,6 +42,7 @@ class CameraProfile { 'manufacturer:wikidata': 'Q108485435', }, builtin: true, + requiresDirection: true, ); /// Built‑in: Motorola Solutions/Vigilant ALPR camera @@ -55,6 +59,7 @@ class CameraProfile { 'manufacturer:wikidata': 'Q634815', }, builtin: true, + requiresDirection: true, ); /// Built‑in: Genetec ALPR camera @@ -71,6 +76,7 @@ class CameraProfile { 'manufacturer:wikidata': 'Q30295174', }, builtin: true, + requiresDirection: true, ); /// Built‑in: Leonardo/ELSAG ALPR camera @@ -87,6 +93,7 @@ class CameraProfile { 'manufacturer:wikidata': 'Q910379', }, builtin: true, + requiresDirection: true, ); /// Built‑in: Neology ALPR camera @@ -102,6 +109,49 @@ class CameraProfile { 'manufacturer': 'Neology, Inc.', }, builtin: true, + requiresDirection: true, + ); + + /// Built‑in: Generic gunshot detector + factory CameraProfile.genericGunshotDetector() => CameraProfile( + id: 'builtin-generic-gunshot', + name: 'Generic Gunshot Detector', + tags: const { + 'man_made': 'surveillance', + 'surveillance:type': 'gunshot_detector', + }, + builtin: true, + requiresDirection: false, + ); + + /// Built‑in: ShotSpotter gunshot detector + factory CameraProfile.shotspotter() => CameraProfile( + id: 'builtin-shotspotter', + name: 'ShotSpotter', + tags: const { + 'man_made': 'surveillance', + 'surveillance': 'public', + 'surveillance:type': 'gunshot_detector', + 'surveillance:brand': 'ShotSpotter', + 'surveillance:brand:wikidata': 'Q107740188', + }, + builtin: true, + requiresDirection: false, + ); + + /// Built‑in: Flock Raven gunshot detector + factory CameraProfile.flockRaven() => CameraProfile( + id: 'builtin-flock-raven', + name: 'Flock Raven', + tags: const { + 'man_made': 'surveillance', + 'surveillance': 'public', + 'surveillance:type': 'gunshot_detector', + 'brand': 'Flock Safety', + 'brand:wikidata': 'Q108485435', + }, + builtin: true, + requiresDirection: false, ); /// Returns true if this profile can be used for submissions @@ -116,22 +166,30 @@ class CameraProfile { String? name, Map? tags, bool? builtin, + bool? requiresDirection, }) => CameraProfile( id: id ?? this.id, name: name ?? this.name, tags: tags ?? this.tags, builtin: builtin ?? this.builtin, + requiresDirection: requiresDirection ?? this.requiresDirection, ); - Map toJson() => - {'id': id, 'name': name, 'tags': tags, 'builtin': builtin}; + Map toJson() => { + 'id': id, + 'name': name, + 'tags': tags, + 'builtin': builtin, + 'requiresDirection': requiresDirection, + }; factory CameraProfile.fromJson(Map j) => CameraProfile( id: j['id'], name: j['name'], tags: Map.from(j['tags']), builtin: j['builtin'] ?? false, + requiresDirection: j['requiresDirection'] ?? true, // Default to true for backward compatibility ); @override diff --git a/lib/screens/profile_editor.dart b/lib/screens/profile_editor.dart index 9ef2055..0459319 100644 --- a/lib/screens/profile_editor.dart +++ b/lib/screens/profile_editor.dart @@ -17,6 +17,7 @@ class ProfileEditor extends StatefulWidget { class _ProfileEditorState extends State { late TextEditingController _nameCtrl; late List> _tags; + late bool _requiresDirection; static const _defaultTags = [ MapEntry('man_made', 'surveillance'), @@ -33,6 +34,7 @@ class _ProfileEditorState extends State { void initState() { super.initState(); _nameCtrl = TextEditingController(text: widget.profile.name); + _requiresDirection = widget.profile.requiresDirection; if (widget.profile.tags.isEmpty) { // New profile → start with sensible defaults @@ -67,7 +69,16 @@ class _ProfileEditorState extends State { hintText: 'e.g., Custom ALPR Camera', ), ), - const SizedBox(height: 24), + const SizedBox(height: 16), + if (!widget.profile.builtin) + CheckboxListTile( + title: const Text('Requires Direction'), + subtitle: const Text('Whether cameras of this type need a direction tag'), + value: _requiresDirection, + onChanged: (value) => setState(() => _requiresDirection = value ?? true), + controlAffinity: ListTileControlAffinity.leading, + ), + const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -170,6 +181,7 @@ class _ProfileEditorState extends State { name: name, tags: tagMap, builtin: false, + requiresDirection: _requiresDirection, ); context.read().addOrUpdateProfile(newProfile); diff --git a/lib/services/uploader.dart b/lib/services/uploader.dart index 547fb14..bced41e 100644 --- a/lib/services/uploader.dart +++ b/lib/services/uploader.dart @@ -36,8 +36,10 @@ class Uploader { print('Uploader: Created changeset ID: $csId'); // 2. create or update node - final mergedTags = Map.from(p.profile.tags) - ..['direction'] = p.direction.round().toString(); + final mergedTags = Map.from(p.profile.tags); + if (p.profile.requiresDirection) { + mergedTags['direction'] = p.direction.round().toString(); + } final tagsXml = mergedTags.entries.map((e) => '').join('\n '); diff --git a/lib/state/profile_state.dart b/lib/state/profile_state.dart index 5031868..d2a16fb 100644 --- a/lib/state/profile_state.dart +++ b/lib/state/profile_state.dart @@ -25,6 +25,9 @@ class ProfileState extends ChangeNotifier { _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.addAll(await ProfileService().load()); // Load enabled profile IDs from prefs diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart index 107a4f5..b643ee2 100644 --- a/lib/state/upload_queue_state.dart +++ b/lib/state/upload_queue_state.dart @@ -41,7 +41,9 @@ class UploadQueueState extends ChangeNotifier { // Using timestamp as negative ID to ensure uniqueness final tempId = -DateTime.now().millisecondsSinceEpoch; final tags = Map.from(upload.profile.tags); - tags['direction'] = upload.direction.toStringAsFixed(0); + if (upload.profile.requiresDirection) { + tags['direction'] = upload.direction.toStringAsFixed(0); + } tags['_pending_upload'] = 'true'; // Mark as pending for potential UI distinction final tempNode = OsmCameraNode( @@ -85,7 +87,9 @@ class UploadQueueState extends ChangeNotifier { // 2. Create new temp node for the edited camera (purple ring) at new location final tempId = -DateTime.now().millisecondsSinceEpoch; final editedTags = Map.from(upload.profile.tags); - editedTags['direction'] = upload.direction.toStringAsFixed(0); + if (upload.profile.requiresDirection) { + editedTags['direction'] = upload.direction.toStringAsFixed(0); + } editedTags['_pending_upload'] = 'true'; // Mark as pending upload editedTags['_original_node_id'] = session.originalNode.id.toString(); // Track original for line drawing diff --git a/lib/widgets/add_camera_sheet.dart b/lib/widgets/add_camera_sheet.dart index 81c3eb9..f532261 100644 --- a/lib/widgets/add_camera_sheet.dart +++ b/lib/widgets/add_camera_sheet.dart @@ -64,9 +64,27 @@ class AddCameraSheet extends StatelessWidget { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: (v) => appState.updateSession(directionDeg: v), + onChanged: session.profile.requiresDirection + ? (v) => appState.updateSession(directionDeg: v) + : null, // Disables slider when requiresDirection is false ), ), + if (!session.profile.requiresDirection) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: const [ + Icon(Icons.info_outline, color: Colors.grey, size: 16), + SizedBox(width: 6), + Expanded( + child: Text( + 'This profile does not require a direction.', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ], + ), + ), if (submittableProfiles.isEmpty) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), diff --git a/lib/widgets/edit_camera_sheet.dart b/lib/widgets/edit_camera_sheet.dart index 5cc9105..5f0cfc1 100644 --- a/lib/widgets/edit_camera_sheet.dart +++ b/lib/widgets/edit_camera_sheet.dart @@ -71,9 +71,27 @@ class EditCameraSheet extends StatelessWidget { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: (v) => appState.updateEditSession(directionDeg: v), + onChanged: session.profile.requiresDirection + ? (v) => appState.updateEditSession(directionDeg: v) + : null, // Disables slider when requiresDirection is false ), ), + if (!session.profile.requiresDirection) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: const [ + Icon(Icons.info_outline, color: Colors.grey, size: 16), + SizedBox(width: 6), + Expanded( + child: Text( + 'This profile does not require a direction.', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ], + ), + ), if (isSandboxMode) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),