fix: radar track rendering bugs

- Elevation ring labels were inverted (90/60/30 from outer to inner);
  now 30/60/90 correctly from outer ring to center
- Split track paths at the 0/360° azimuth wrap in both MutualRadarView
  and main RadarView, so passes crossing due north no longer draw a
  line straight across the plot
This commit is contained in:
atsunatsu committed 2026-08-03 01:53:37 +08:00
1 parent 998c98267b
commit 1a3f1bb34c
2 files changed
+37 -10

No files matched your search

@@ -103,29 +103,35 @@ fun MutualRadarView(
// Grid: elevation rings + azimuth spokes + cardinal labels // Grid: elevation rings + azimuth spokes + cardinal labels
drawRadarGrid(center, radius, gridColor, textColor, measurer) drawRadarGrid(center, radius, gridColor, textColor, measurer)
// Track paths (only the above-horizon arc) // Track paths (only the above-horizon arc, split at the 0/360° azimuth wrap)
val pathA = Path() val pathA = Path()
val pathB = Path() val pathB = Path()
var firstVisibleA = true var firstVisibleA = true
var firstVisibleB = true var firstVisibleB = true
var lastAzimA: Double? = null
var lastAzimB: Double? = null
trackSamples.forEach { sample -> trackSamples.forEach { sample ->
if (sample.elevationA > 0f) { if (sample.elevationA > 0f) {
val p = sph2Cart(center, sample.azimuthA, sample.elevationA, radius) val p = sph2Cart(center, sample.azimuthA, sample.elevationA, radius)
if (firstVisibleA) { val wrapA = lastAzimA != null && kotlin.math.abs(azimuthDelta(sample.azimuthA - lastAzimA!!)) > 180.0
if (firstVisibleA || wrapA) {
pathA.moveTo(p.x, p.y) pathA.moveTo(p.x, p.y)
firstVisibleA = false firstVisibleA = false
} else { } else {
pathA.lineTo(p.x, p.y) pathA.lineTo(p.x, p.y)
} }
lastAzimA = sample.azimuthA
} }
if (sample.elevationB > 0f) { if (sample.elevationB > 0f) {
val p = sph2Cart(center, sample.azimuthB, sample.elevationB, radius) val p = sph2Cart(center, sample.azimuthB, sample.elevationB, radius)
if (firstVisibleB) { val wrapB = lastAzimB != null && kotlin.math.abs(azimuthDelta(sample.azimuthB - lastAzimB!!)) > 180.0
if (firstVisibleB || wrapB) {
pathB.moveTo(p.x, p.y) pathB.moveTo(p.x, p.y)
firstVisibleB = false firstVisibleB = false
} else { } else {
pathB.lineTo(p.x, p.y) pathB.lineTo(p.x, p.y)
} }
lastAzimB = sample.azimuthB
} }
} }
@@ -198,13 +204,12 @@ private fun DrawScope.drawRadarGrid(
Offset(center.x - diag, center.y + diag), Offset(center.x + diag, center.y - diag), 1f Offset(center.x - diag, center.y + diag), Offset(center.x + diag, center.y - diag), 1f
) )
// Elevation ring labels // Elevation ring labels: 30° on the outer ring, 60° on the middle ring, 90° at center
// (outer edge is the 0° horizon). Labels sit just above their ring.
val style = TextStyle(color = textColor, fontSize = 11.sp) val style = TextStyle(color = textColor, fontSize = 11.sp)
for (i in 0 until CIRCLES) { drawText(measurer, "30°", Offset(center.x + 6f, (center.y - (radius - step)) - 24f), style = style)
val deg = 30 * (CIRCLES - i) drawText(measurer, "60°", Offset(center.x + 6f, (center.y - (radius - 2 * step)) - 24f), style = style)
val y = (center.y - (radius - step * i)) - 24f drawText(measurer, "90°", Offset(center.x + 6f, center.y - 18f), style = style)
drawText(measurer, "$deg°", Offset(center.x + 6f, y), style = style)
}
// Cardinal labels // Cardinal labels
drawText(measurer, "N", Offset(center.x - 8f, center.y - radius - 20f), style = style) drawText(measurer, "N", Offset(center.x - 8f, center.y - radius - 20f), style = style)
drawText(measurer, "E", Offset(center.x + radius + 4f, center.y - 10f), style = style) drawText(measurer, "E", Offset(center.x + radius + 4f, center.y - 10f), style = style)
@@ -212,6 +217,14 @@ private fun DrawScope.drawRadarGrid(
drawText(measurer, "W", Offset(center.x - radius - 24f, center.y - 10f), style = style) drawText(measurer, "W", Offset(center.x - radius - 24f, center.y - 10f), style = style)
} }
/** Normalize an azimuth delta (degrees) into the [-180, 180] range. */
private fun azimuthDelta(deltaDeg: Double): Double {
var d = deltaDeg % 360.0
if (d > 180.0) d -= 360.0
if (d < -180.0) d += 360.0
return d
}
/** Convert azimuth (deg, 0=N, clockwise) and elevation (deg) to canvas offset. */ /** Convert azimuth (deg, 0=N, clockwise) and elevation (deg) to canvas offset. */
private fun sph2Cart(center: Offset, azimDeg: Double, elevDeg: Double, r: Float): Offset { private fun sph2Cart(center: Offset, azimDeg: Double, elevDeg: Double, r: Float): Offset {
val azimRad = azimDeg * PI / 180.0 val azimRad = azimDeg * PI / 180.0
@@ -60,6 +60,8 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.predict.PI_2 import com.rtbishop.look4sat.core.domain.predict.PI_2
import com.rtbishop.look4sat.core.domain.utility.toRadians import com.rtbishop.look4sat.core.domain.utility.toRadians
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@@ -226,13 +228,25 @@ private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, b
private fun createTrackPath(positions: List<OrbitalPos>, radius: Float): Path { private fun createTrackPath(positions: List<OrbitalPos>, radius: Float): Path {
val trackPath = Path() val trackPath = Path()
var lastAzim: Double? = null
positions.forEachIndexed { index, pos -> positions.forEachIndexed { index, pos ->
val offset = sph2Cart(pos.azimuth, pos.elevation, radius.toDouble()) val offset = sph2Cart(pos.azimuth, pos.elevation, radius.toDouble())
if (index == 0) trackPath.moveTo(offset.x, offset.y) else trackPath.lineTo(offset.x, offset.y) // Split the path when azimuth wraps 0°/360° to avoid a line across the plot
val wrap = lastAzim != null && abs(azimuthDeltaRad(pos.azimuth - lastAzim!!)) > PI
if (index == 0 || wrap) trackPath.moveTo(offset.x, offset.y) else trackPath.lineTo(offset.x, offset.y)
lastAzim = pos.azimuth
} }
return trackPath return trackPath
} }
/** Normalize an azimuth delta (radians) into the [-PI, PI] range. */
private fun azimuthDeltaRad(deltaRad: Double): Double {
var d = deltaRad % (2 * PI)
if (d > PI) d -= 2 * PI
if (d < -PI) d += 2 * PI
return d
}
private fun createTrackEffect(trackPath: Path): PathEffect { private fun createTrackEffect(trackPath: Path): PathEffect {
val shapeRadius = 24f val shapeRadius = 24f
val angle = 120.0.toRadians() val angle = 120.0.toRadians()