import 'osm_node.dart'; /// Sentinel value for copyWith methods to distinguish between null and not provided const Object _notProvided = Object(); /// A bundle of preset OSM tags that describe a particular surveillance node model/type. class NodeProfile { final String id; final String name; final Map tags; final bool builtin; final bool requiresDirection; final bool submittable; final bool editable; final double? fov; // Field-of-view in degrees (null means use dev_config default) NodeProfile({ required this.id, required this.name, required this.tags, this.builtin = false, this.requiresDirection = true, this.submittable = true, this.editable = true, this.fov, }); /// Get all built-in default node profiles static List getDefaults() => [ NodeProfile( id: 'builtin-generic-alpr', name: 'Generic ALPR', tags: const { 'man_made': 'surveillance', 'surveillance:type': 'ALPR', }, builtin: true, requiresDirection: true, submittable: false, editable: false, ), NodeProfile( id: 'builtin-flock', name: 'Flock', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Flock Safety', 'manufacturer:wikidata': 'Q108485435', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-motorola', name: 'Motorola/Vigilant', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Motorola Solutions', 'manufacturer:wikidata': 'Q634815', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-genetec', name: 'Genetec', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Genetec', 'manufacturer:wikidata': 'Q30295174', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-leonardo', name: 'Leonardo/ELSAG', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Leonardo', 'manufacturer:wikidata': 'Q910379', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-neology', name: 'Neology', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Neology, Inc.', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-rekor', name: 'Rekor', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Rekor', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-axis', name: 'Axis Communications', tags: const { 'man_made': 'surveillance', 'surveillance': 'public', 'surveillance:type': 'ALPR', 'surveillance:zone': 'traffic', 'camera:type': 'fixed', 'camera:mount': '', // Empty value for refinement 'manufacturer': 'Axis Communications', 'manufacturer:wikidata': 'Q2347731', }, builtin: true, requiresDirection: true, submittable: true, editable: true, ), NodeProfile( id: 'builtin-generic-gunshot', name: 'Generic Gunshot Detector', tags: const { 'man_made': 'surveillance', 'surveillance:type': 'gunshot_detector', }, builtin: true, requiresDirection: false, submittable: false, editable: false, ), NodeProfile( 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, submittable: true, editable: true, ), NodeProfile( 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, submittable: true, editable: true, ), ]; /// Returns true if this profile can be used for submissions bool get isSubmittable => submittable; NodeProfile copyWith({ String? id, String? name, Map? tags, bool? builtin, bool? requiresDirection, bool? submittable, bool? editable, Object? fov = _notProvided, }) => NodeProfile( id: id ?? this.id, name: name ?? this.name, tags: tags ?? this.tags, builtin: builtin ?? this.builtin, requiresDirection: requiresDirection ?? this.requiresDirection, submittable: submittable ?? this.submittable, editable: editable ?? this.editable, fov: fov == _notProvided ? this.fov : fov as double?, ); Map toJson() => { 'id': id, 'name': name, 'tags': tags, 'builtin': builtin, 'requiresDirection': requiresDirection, 'submittable': submittable, 'editable': editable, 'fov': fov, }; factory NodeProfile.fromJson(Map j) => NodeProfile( 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 submittable: j['submittable'] ?? true, // Default to true for backward compatibility editable: j['editable'] ?? true, // Default to true for backward compatibility fov: j['fov']?.toDouble(), // Can be null for backward compatibility ); @override bool operator ==(Object other) => identical(this, other) || other is NodeProfile && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; /// Create a temporary empty profile for editing existing nodes /// Used as the default "" option when editing nodes /// All existing tags will flow through as additionalExistingTags static NodeProfile createExistingTagsProfile(OsmNode node) { // Calculate FOV from existing direction ranges if applicable double? calculatedFov; // If node has direction/FOV pairs, check if they all have the same FOV if (node.directionFovPairs.isNotEmpty) { final firstFov = node.directionFovPairs.first.fovDegrees; // If all directions have the same FOV, use it for the profile if (node.directionFovPairs.every((df) => df.fovDegrees == firstFov)) { calculatedFov = firstFov; } } return NodeProfile( id: 'temp-empty-${node.id}', name: '', // Will be localized in UI tags: {}, // Completely empty - all existing tags become additional builtin: false, requiresDirection: true, submittable: true, editable: false, fov: calculatedFov, // Use calculated FOV from existing direction ranges ); } }