From 3fc3a72cdea9c97e26870ab3aef1a4bdf6bbcef5 Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 28 Jan 2026 20:21:25 -0600 Subject: [PATCH] Fixes for 360-deg FOVs --- README.md | 3 +- assets/changelog.json | 6 ++ lib/models/osm_node.dart | 5 ++ lib/state/upload_queue_state.dart | 5 ++ lib/widgets/map/direction_cones.dart | 33 +++++----- pubspec.yaml | 2 +- test/models/osm_node_test.dart | 91 ++++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 test/models/osm_node_test.dart diff --git a/README.md b/README.md index ec405ce..ae35f55 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/changelog.json b/assets/changelog.json index d36235c..5e2148c 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -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", diff --git a/lib/models/osm_node.dart b/lib/models/osm_node.dart index 7995327..efceedd 100644 --- a/lib/models/osm_node.dart +++ b/lib/models/osm_node.dart @@ -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) { diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart index a981623..f16879d 100644 --- a/lib/state/upload_queue_state.dart +++ b/lib/state/upload_queue_state.dart @@ -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; diff --git a/lib/widgets/map/direction_cones.dart b/lib/widgets/map/direction_cones.dart index 8668e25..88a227e 100644 --- a/lib/widgets/map/direction_cones.dart +++ b/lib/widgets/map/direction_cones.dart @@ -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 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; diff --git a/pubspec.yaml b/pubspec.yaml index e39aa83..5f7a69c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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+ diff --git a/test/models/osm_node_test.dart b/test/models/osm_node_test.dart new file mode 100644 index 0000000..3d9a58f --- /dev/null +++ b/test/models/osm_node_test.dart @@ -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) + }); + }); +} \ No newline at end of file