import 'package:flutter/foundation.dart'; import 'state/vision_state.dart'; /// The four horizontal radar sides, clockwise from the nose. enum RadarSide { front('Front', 'NOSE', 'forward', 0, 60), right('Right', 'RIGHT', 'right', 90, 40), back('Rear', 'TAIL', 'backward', 180, 60), left('Left', 'LEFT', 'left', 270, 40); const RadarSide(this.label, this.position, this.healthKey, this.bearing, this.span); final String label; /// `VisionDetectionState` position name. final String position; /// `obstacleSensors` key. final String healthKey; /// Centre of the side, degrees clockwise from the nose. final double bearing; /// Arc the side covers on the radar, degrees. final double span; } /// How alarming a sector is, for its colour. enum RadarBand { none, notice, caution, danger } @immutable class RadarSector { const RadarSector({required this.start, required this.sweep, this.distance, this.level = 0}); /// Degrees clockwise from the nose. final double start; final double sweep; final double? distance; // m /// 0 (nothing / invalid) … 6 (very close), as MSDK's `ObstacleDetectionSectorWarning`. final int level; RadarBand get band => VisionRadar.bandFor(level); } @immutable class RadarSideView { const RadarSideView({required this.side, this.sectors = const [], this.closest, this.blocked = false, this.off = false}); final RadarSide side; final List sectors; final double? closest; // m /// Switched on but not seeing (covered, dirty, too dark). final bool blocked; /// Obstacle sensing for this side is switched off. final bool off; int get level => sectors.fold(0, (int w, RadarSector s) => s.level > w ? s.level : w); RadarBand get band => VisionRadar.bandFor(level); } /// What the vision radar overlay (plan P8, GO 4's `vision_radar_view`) draws: /// per side, sectors graded by distance, the nearest distance, and whether the /// side's sensors are blocked; plus the obstacle overhead. /// /// Pure: built from [VisionState] so the grading is unit-tested without a /// painter. Each side prefers the aircraft's own `VisionDetectionState` /// sectors (and their warning levels); a side that has none is filled from /// omni perception's distance ring, graded by [levelFor]. @immutable class VisionRadar { const VisionRadar({this.sides = const {}, this.up}); /// Obstacles further than this are not drawn. static const double range = 16; /// Above this the overhead distance is not shown. static const double upRange = 10; /// Sectors a perception-filled side is split into. static const int perceptionSectors = 4; final Map sides; /// Overhead obstacle, m, when within [upRange]. final double? up; bool get isEmpty => sides.values.every((RadarSideView s) => s.sectors.every((RadarSector x) => x.level == 0) && !s.blocked) && up == null; int get level => sides.values.fold(0, (int w, RadarSideView s) => s.level > w ? s.level : w); List get blocked => [for (final RadarSideView s in sides.values) if (s.blocked) s.side]; static RadarBand bandFor(int level) => level >= 5 ? RadarBand.danger : level >= 3 ? RadarBand.caution : level >= 1 ? RadarBand.notice : RadarBand.none; /// A warning level for a bare distance (perception reports no level). static int levelFor(double? d) { if (d == null || d > range) return 0; if (d <= 1.5) return 6; if (d <= 2.5) return 5; if (d <= 4) return 4; if (d <= 6) return 3; if (d <= 10) return 2; return 1; } static int _sdkLevel(VisionSector s) => s.distance != null && s.distance! > range ? 0 : s.level; static VisionRadar of(VisionState v) { final Map sides = {}; final Perception? p = v.perception; for (final RadarSide side in RadarSide.values) { final VisionReading? r = v.sensors[side.position]; final SensorHealth? h = v.health[side.healthKey]; List sectors = const []; if (r != null && r.sectors.isNotEmpty) { // Sectors run left to right as the sensor sees them: clockwise, whichever way it faces. final double sweep = side.span / r.sectors.length; final double start = side.bearing - side.span / 2; sectors = [ for (int i = 0; i < r.sectors.length; i++) RadarSector(start: start + i * sweep, sweep: sweep, distance: r.sectors[i].distance, level: _sdkLevel(r.sectors[i])), ]; } else if (p != null && p.angleInterval > 0 && p.distances.isNotEmpty) { sectors = _fromPerception(side, p); } final bool off = r?.isDisabled == true || (h != null && !h.enabled); final bool blocked = !off && (h?.blocked ?? false); if (sectors.isEmpty && !blocked && !off && r == null) continue; double? closest; for (final RadarSector s in sectors) { final double? d = s.distance; if (s.level > 0 && d != null && (closest == null || d < closest)) closest = d; } sides[side] = RadarSideView(side: side, sectors: sectors, closest: closest, blocked: blocked, off: off); } final double? up = p?.up; return VisionRadar(sides: sides, up: up != null && up <= upRange ? up : null); } /// Splits [side]'s arc into [perceptionSectors] and takes the nearest ring /// sample in each. static List _fromPerception(RadarSide side, Perception p) { final int n = p.distances.length; final double sweep = side.span / perceptionSectors; final double start = side.bearing - side.span / 2; return [ for (int i = 0; i < perceptionSectors; i++) () { double? nearest; final double a0 = start + i * sweep, a1 = a0 + sweep; for (int k = 0; k < n; k++) { final double? d = p.distances[k]; if (d == null || !_within(k * p.angleInterval, a0, a1)) continue; if (nearest == null || d < nearest) nearest = d; } return RadarSector(start: a0, sweep: sweep, distance: nearest, level: levelFor(nearest)); }(), ]; } /// Whether bearing [a] lies in [a0, a1), all in degrees, wrapping at 360. static bool _within(double a, double a0, double a1) { double norm(double x) => ((x % 360) + 360) % 360; final double x = norm(a - a0); return x < a1 - a0; } }