mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-14 21:28:08 +02:00
gunshot detection - direction optional as defined by profile
This commit is contained in:
@@ -6,12 +6,14 @@ class CameraProfile {
|
||||
final String name;
|
||||
final Map<String, String> 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<String, String>? 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<String, dynamic> toJson() =>
|
||||
{'id': id, 'name': name, 'tags': tags, 'builtin': builtin};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'tags': tags,
|
||||
'builtin': builtin,
|
||||
'requiresDirection': requiresDirection,
|
||||
};
|
||||
|
||||
factory CameraProfile.fromJson(Map<String, dynamic> j) => CameraProfile(
|
||||
id: j['id'],
|
||||
name: j['name'],
|
||||
tags: Map<String, String>.from(j['tags']),
|
||||
builtin: j['builtin'] ?? false,
|
||||
requiresDirection: j['requiresDirection'] ?? true, // Default to true for backward compatibility
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class ProfileEditor extends StatefulWidget {
|
||||
class _ProfileEditorState extends State<ProfileEditor> {
|
||||
late TextEditingController _nameCtrl;
|
||||
late List<MapEntry<String, String>> _tags;
|
||||
late bool _requiresDirection;
|
||||
|
||||
static const _defaultTags = [
|
||||
MapEntry('man_made', 'surveillance'),
|
||||
@@ -33,6 +34,7 @@ class _ProfileEditorState extends State<ProfileEditor> {
|
||||
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<ProfileEditor> {
|
||||
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<ProfileEditor> {
|
||||
name: name,
|
||||
tags: tagMap,
|
||||
builtin: false,
|
||||
requiresDirection: _requiresDirection,
|
||||
);
|
||||
|
||||
context.read<AppState>().addOrUpdateProfile(newProfile);
|
||||
|
||||
@@ -36,8 +36,10 @@ class Uploader {
|
||||
print('Uploader: Created changeset ID: $csId');
|
||||
|
||||
// 2. create or update node
|
||||
final mergedTags = Map<String, String>.from(p.profile.tags)
|
||||
..['direction'] = p.direction.round().toString();
|
||||
final mergedTags = Map<String, String>.from(p.profile.tags);
|
||||
if (p.profile.requiresDirection) {
|
||||
mergedTags['direction'] = p.direction.round().toString();
|
||||
}
|
||||
final tagsXml = mergedTags.entries.map((e) =>
|
||||
'<tag k="${e.key}" v="${e.value}"/>').join('\n ');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,9 @@ class UploadQueueState extends ChangeNotifier {
|
||||
// Using timestamp as negative ID to ensure uniqueness
|
||||
final tempId = -DateTime.now().millisecondsSinceEpoch;
|
||||
final tags = Map<String, String>.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<String, String>.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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user