Fixes for 360-deg FOVs

This commit is contained in:
stopflock
2026-01-28 20:21:25 -06:00
parent 1d65d5ecca
commit 3fc3a72cde
7 changed files with 128 additions and 17 deletions

View File

@@ -104,7 +104,8 @@ cp lib/keys.dart.example lib/keys.dart
## Roadmap
### Needed Bugfixes
- Fix rendering of 0-360 FOV ring
- Make submission guide scarier
- "More..." button in profiles dropdown -> identify page
- Node data fetching super slow; retries not working?
- Clean up tile cache; implement some max size or otherwise trim unused / old tiles to prevent infinite memory growth
- Filter NSI suggestions based on what has already been typed in

View File

@@ -1,4 +1,10 @@
{
"2.4.3": {
"content": [
"• Fixed 360° FOV rendering - devices with full circle coverage now render as complete rings instead of having a wedge cut out or being a line",
"• Fixed 360° FOV submission - now correctly submits '0-360' to OpenStreetMap instead of incorrect '180-180' values, disables direction slider"
]
},
"2.4.1": {
"content": [
"• Save button moved to top-right corner of profile editor screens",

View File

@@ -107,6 +107,11 @@ class OsmNode {
start = ((start % 360) + 360) % 360;
end = ((end % 360) + 360) % 360;
// Special case: if start equals end, this represents 360° FOV
if (start == end) {
return DirectionFov(start, 360.0);
}
double width, center;
if (start > end) {

View File

@@ -743,6 +743,11 @@ class UploadQueueState extends ChangeNotifier {
// Convert a center direction and FOV to range notation (e.g., 180° center with 90° FOV -> "135-225")
String _formatDirectionWithFov(double center, double fov) {
// Handle 360-degree FOV as special case
if (fov >= 360) {
return '0-360';
}
final halfFov = fov / 2;
final start = (center - halfFov + 360) % 360;
final end = (center + halfFov) % 360;

View File

@@ -170,7 +170,10 @@ class DirectionConesBuilder {
bool isActiveDirection = true,
}) {
// Handle full circle case (360-degree FOV)
if (halfAngleDeg >= 180) {
// Use 179.5 threshold to account for floating point precision
print("DEBUG: halfAngleDeg = $halfAngleDeg, bearing = $bearingDeg");
if (halfAngleDeg >= 179.5) {
print("DEBUG: Using full circle for 360° FOV");
return _buildFullCircle(
origin: origin,
zoom: zoom,
@@ -179,6 +182,7 @@ class DirectionConesBuilder {
isActiveDirection: isActiveDirection,
);
}
print("DEBUG: Using normal cone for FOV = ${halfAngleDeg * 2}°");
// Calculate pixel-based radii
final outerRadiusPx = kNodeIconDiameter + (kNodeIconDiameter * kDirectionConeBaseLength);
@@ -232,6 +236,7 @@ class DirectionConesBuilder {
}
/// Build a full circle for 360-degree FOV cases
/// Returns just the outer circle - we'll handle the donut effect differently
static Polygon _buildFullCircle({
required LatLng origin,
required double zoom,
@@ -239,17 +244,19 @@ class DirectionConesBuilder {
bool isSession = false,
bool isActiveDirection = true,
}) {
// Calculate pixel-based radii
print("DEBUG: Building full circle - isSession: $isSession, isActiveDirection: $isActiveDirection");
// Calculate pixel-based radii
final outerRadiusPx = kNodeIconDiameter + (kNodeIconDiameter * kDirectionConeBaseLength);
final innerRadiusPx = kNodeIconDiameter + (2 * getNodeRingThickness(context));
// Convert pixels to coordinate distances with zoom scaling
final pixelToCoordinate = 0.00001 * math.pow(2, 15 - zoom);
final outerRadius = outerRadiusPx * pixelToCoordinate;
final innerRadius = innerRadiusPx * pixelToCoordinate;
// Create full circle with many points for smooth rendering
const int circlePoints = 36;
print("DEBUG: Outer radius: $outerRadius, zoom: $zoom");
// Create simple filled circle - no donut complexity
const int circlePoints = 60;
final points = <LatLng>[];
LatLng project(double deg, double distance) {
@@ -260,17 +267,13 @@ class DirectionConesBuilder {
return LatLng(origin.latitude + dLat, origin.longitude + dLon);
}
// Add outer circle points
for (int i = 0; i < circlePoints; i++) {
final angle = i * 360.0 / circlePoints;
// Add outer circle points - simple complete circle
for (int i = 0; i <= circlePoints; i++) { // Note: <= to ensure closure
final angle = (i * 360.0 / circlePoints) % 360.0;
points.add(project(angle, outerRadius));
}
// Add inner circle points in reverse order to create donut
for (int i = circlePoints - 1; i >= 0; i--) {
final angle = i * 360.0 / circlePoints;
points.add(project(angle, innerRadius));
}
print("DEBUG: Created ${points.length} points for full circle");
// Adjust opacity based on direction state
double opacity = kDirectionConeOpacity;

View File

@@ -1,7 +1,7 @@
name: deflockapp
description: Map public surveillance infrastructure with OpenStreetMap
publish_to: "none"
version: 2.4.1+39 # The thing after the + is the version code, incremented with each release
version: 2.4.3+41 # The thing after the + is the version code, incremented with each release
environment:
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+

View File

@@ -0,0 +1,91 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:deflockapp/models/osm_node.dart';
void main() {
group('OsmNode Direction Parsing', () {
test('should parse 360-degree FOV from X-X notation', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '180-180'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(180.0));
expect(directionFovPairs[0].fovDegrees, equals(360.0));
});
test('should parse 360-degree FOV from 0-0 notation', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '0-0'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(0.0));
expect(directionFovPairs[0].fovDegrees, equals(360.0));
});
test('should parse 360-degree FOV from 270-270 notation', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '270-270'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(270.0));
expect(directionFovPairs[0].fovDegrees, equals(360.0));
});
test('should parse normal range notation correctly', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '90-270'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(180.0));
expect(directionFovPairs[0].fovDegrees, equals(180.0));
});
test('should parse wrapping range notation correctly', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '270-90'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(0.0));
expect(directionFovPairs[0].fovDegrees, equals(180.0));
});
test('should parse single direction correctly', () {
final node = OsmNode(
id: 1,
coord: const LatLng(0, 0),
tags: {'direction': '90'},
);
final directionFovPairs = node.directionFovPairs;
expect(directionFovPairs, hasLength(1));
expect(directionFovPairs[0].centerDegrees, equals(90.0));
// Default FOV from dev_config (kDirectionConeHalfAngle * 2)
});
});
}