Support FOV range notation: 0-360, 90-270, 10-45;90-125

This commit is contained in:
stopflock
2025-11-21 19:25:34 -06:00
parent ee26576c5e
commit 9319bbda48
16 changed files with 333 additions and 27 deletions
+24
View File
@@ -0,0 +1,24 @@
/// Represents a direction with its associated field-of-view (FOV) cone.
class DirectionFov {
/// The center direction in degrees (0-359, where 0 is north)
final double centerDegrees;
/// The field-of-view width in degrees (e.g., 35, 90, 180, 360)
final double fovDegrees;
DirectionFov(this.centerDegrees, this.fovDegrees);
@override
String toString() => 'DirectionFov(center: ${centerDegrees}°, fov: ${fovDegrees}°)';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DirectionFov &&
runtimeType == other.runtimeType &&
centerDegrees == other.centerDegrees &&
fovDegrees == other.fovDegrees;
@override
int get hashCode => centerDegrees.hashCode ^ fovDegrees.hashCode;
}
+11
View File
@@ -9,6 +9,7 @@ class NodeProfile {
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,
@@ -18,6 +19,7 @@ class NodeProfile {
this.requiresDirection = true,
this.submittable = true,
this.editable = true,
this.fov,
});
/// Get all built-in default node profiles
@@ -50,6 +52,7 @@ class NodeProfile {
requiresDirection: true,
submittable: true,
editable: true,
fov: 45.0, // Flock cameras typically have narrow FOV
),
NodeProfile(
id: 'builtin-motorola',
@@ -67,6 +70,7 @@ class NodeProfile {
requiresDirection: true,
submittable: true,
editable: true,
fov: 60.0, // Motorola cameras typically have moderate FOV
),
NodeProfile(
id: 'builtin-genetec',
@@ -84,6 +88,7 @@ class NodeProfile {
requiresDirection: true,
submittable: true,
editable: true,
fov: 50.0, // Genetec cameras typically have moderate FOV
),
NodeProfile(
id: 'builtin-leonardo',
@@ -101,6 +106,7 @@ class NodeProfile {
requiresDirection: true,
submittable: true,
editable: true,
fov: 55.0, // Leonardo cameras typically have moderate FOV
),
NodeProfile(
id: 'builtin-neology',
@@ -150,6 +156,7 @@ class NodeProfile {
requiresDirection: true,
submittable: true,
editable: true,
fov: 90.0, // Axis cameras can have wider FOV
),
NodeProfile(
id: 'builtin-generic-gunshot',
@@ -208,6 +215,7 @@ class NodeProfile {
bool? requiresDirection,
bool? submittable,
bool? editable,
double? fov,
}) =>
NodeProfile(
id: id ?? this.id,
@@ -217,6 +225,7 @@ class NodeProfile {
requiresDirection: requiresDirection ?? this.requiresDirection,
submittable: submittable ?? this.submittable,
editable: editable ?? this.editable,
fov: fov ?? this.fov,
);
Map<String, dynamic> toJson() => {
@@ -227,6 +236,7 @@ class NodeProfile {
'requiresDirection': requiresDirection,
'submittable': submittable,
'editable': editable,
'fov': fov,
};
factory NodeProfile.fromJson(Map<String, dynamic> j) => NodeProfile(
@@ -237,6 +247,7 @@ class NodeProfile {
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
+56 -9
View File
@@ -1,4 +1,6 @@
import 'package:latlong2/latlong.dart';
import 'direction_fov.dart';
import '../dev_config.dart';
class OsmNode {
final int id;
@@ -36,9 +38,10 @@ class OsmNode {
);
}
bool get hasDirection => directionDeg.isNotEmpty;
bool get hasDirection => directionFovPairs.isNotEmpty;
List<double> get directionDeg {
/// Get direction and FOV pairs, supporting range notation like "90-270" or "10-45;90-125;290"
List<DirectionFov> get directionFovPairs {
final raw = tags['direction'] ?? tags['camera:direction'];
if (raw == null) return [];
@@ -50,17 +53,35 @@ class OsmNode {
'W': 270.0, 'WNW': 292.5, 'NW': 315.0, 'NNW': 337.5,
};
// Split on semicolons and parse each direction
final directions = <double>[];
final directionFovList = <DirectionFov>[];
final parts = raw.split(';');
for (final part in parts) {
final trimmed = part.trim().toUpperCase();
final trimmed = part.trim();
if (trimmed.isEmpty) continue;
// Check if this part contains a range (e.g., "90-270")
if (trimmed.contains('-') && RegExp(r'^\d+\.?\d*-\d+\.?\d*$').hasMatch(trimmed)) {
final rangeParts = trimmed.split('-');
if (rangeParts.length == 2) {
final start = double.tryParse(rangeParts[0]);
final end = double.tryParse(rangeParts[1]);
if (start != null && end != null) {
final normalized = _calculateRangeCenter(start, end);
directionFovList.add(normalized);
continue;
}
}
}
// Not a range, handle as single direction
final trimmedUpper = trimmed.toUpperCase();
// First try compass direction lookup
if (compassDirections.containsKey(trimmed)) {
directions.add(compassDirections[trimmed]!);
if (compassDirections.containsKey(trimmedUpper)) {
final degrees = compassDirections[trimmedUpper]!;
directionFovList.add(DirectionFov(degrees, kDirectionConeHalfAngle * 2));
continue;
}
@@ -74,9 +95,35 @@ class OsmNode {
// Normalize: wrap negative or >360 into 0359 range
final normalized = ((val % 360) + 360) % 360;
directions.add(normalized);
directionFovList.add(DirectionFov(normalized, kDirectionConeHalfAngle * 2));
}
return directions;
return directionFovList;
}
/// Calculate center and width for a range like "90-270" or "270-90"
DirectionFov _calculateRangeCenter(double start, double end) {
// Normalize start and end to 0-359 range
start = ((start % 360) + 360) % 360;
end = ((end % 360) + 360) % 360;
double width, center;
if (start > end) {
// Wrapping case: 270-90
width = (end + 360) - start;
center = ((start + end + 360) / 2) % 360;
} else {
// Normal case: 90-270
width = end - start;
center = (start + end) / 2;
}
return DirectionFov(center, width);
}
/// Legacy getter for backward compatibility - returns just center directions
List<double> get directionDeg {
return directionFovPairs.map((df) => df.centerDegrees).toList();
}
}