summaryrefslogtreecommitdiff
path: root/test-runner/src
diff options
context:
space:
mode:
Diffstat (limited to 'test-runner/src')
-rw-r--r--test-runner/src/main/kotlin/org/terst/nav/track/TackDetector.kt23
-rw-r--r--test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt8
2 files changed, 21 insertions, 10 deletions
diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TackDetector.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TackDetector.kt
index 763e693..5bf86c3 100644
--- a/test-runner/src/main/kotlin/org/terst/nav/track/TackDetector.kt
+++ b/test-runner/src/main/kotlin/org/terst/nav/track/TackDetector.kt
@@ -13,7 +13,7 @@ import kotlin.math.sin
*
* For each GPS fix as candidate apex: collect before/after settle windows (excluding the guard zone),
* require MIN_PTS fixes in each, compute circularMean + circularMAD, reject if MAD > STAB_MAX,
- * accept if heading delta in [MIN_DELTA, MAX_DELTA].
+ * accept if heading delta ≥ MIN_DELTA (no upper cap — 180° crash-tacks are valid).
*
* De-duplicate: group candidates within MIN_GAP_MS, keep max-delta per group.
* Refine position: within the maneuver zone, find the point with the greatest instantaneous
@@ -29,8 +29,7 @@ object TackDetector {
private const val T_SETTLE = 30_000L // ms — stable heading window required before/after
private const val T_MANEUVER = 30_000L // ms — guard zone around apex (±15 s each side)
private const val STAB_MAX = 20.0 // ° — max circularMAD in settle windows
- private const val MIN_DELTA = 60.0 // ° — minimum heading change to count as tack/jibe
- private const val MAX_DELTA = 160.0 // ° — maximum heading change (beyond = U-turn, not a tack)
+ private const val MIN_DELTA = 75.0 // ° — minimum heading change to count as a maneuver
private const val MIN_GAP_MS = 60_000L // ms — minimum time between accepted events
private const val START_SKIP_MS = 60_000L // ms — skip first 60 s (cold-start noise)
private const val MIN_PTS = 3 // minimum GPS fixes required in each settle window
@@ -55,12 +54,12 @@ object TackDetector {
val beforeEnd = t - T_MANEUVER / 2
val beforeStart = beforeEnd - T_SETTLE
- val before = points.filter { it.timestampMs in beforeStart until beforeEnd }
+ val before = points.subList(lowerBound(points, beforeStart), lowerBound(points, beforeEnd))
if (before.size < MIN_PTS) continue
val afterStart = t + T_MANEUVER / 2
val afterEnd = afterStart + T_SETTLE
- val after = points.filter { it.timestampMs > afterStart && it.timestampMs <= afterEnd }
+ val after = points.subList(upperBound(points, afterStart), upperBound(points, afterEnd))
if (after.size < MIN_PTS) continue
val cogBefore = circularMean(before.map { it.cogDeg })
@@ -72,7 +71,7 @@ object TackDetector {
if (spreadAfter > STAB_MAX) continue
val delta = abs(angleDiff(cogBefore, cogAfter))
- if (delta < MIN_DELTA || delta > MAX_DELTA) continue
+ if (delta < MIN_DELTA) continue
raw += Candidate(i, t, delta, cogBefore, cogAfter)
}
@@ -112,6 +111,18 @@ object TackDetector {
return TackEvent(bestIdx, points[bestIdx].lat, points[bestIdx].lon, c.cogBefore, c.cogAfter)
}
+ private fun lowerBound(points: List<TrackPoint>, ms: Long): Int {
+ var lo = 0; var hi = points.size
+ while (lo < hi) { val mid = (lo + hi) ushr 1; if (points[mid].timestampMs < ms) lo = mid + 1 else hi = mid }
+ return lo
+ }
+
+ private fun upperBound(points: List<TrackPoint>, ms: Long): Int {
+ var lo = 0; var hi = points.size
+ while (lo < hi) { val mid = (lo + hi) ushr 1; if (points[mid].timestampMs <= ms) lo = mid + 1 else hi = mid }
+ return lo
+ }
+
internal fun angleDiff(from: Double, to: Double): Double {
var diff = to - from
while (diff > 180) diff -= 360
diff --git a/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt b/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
index 6d946a0..265ba76 100644
--- a/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
+++ b/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
@@ -49,13 +49,13 @@ class TackDetectorTest {
}
@Test fun `heading change below MIN_DELTA is ignored`() {
- // 45° delta — below MIN_DELTA=60°
+ // 45° delta — below MIN_DELTA=75°
assertTrue(TackDetector.detectTacks(tackTrack(90.0, 135.0)).isEmpty())
}
- @Test fun `heading change above MAX_DELTA is ignored`() {
- // 170° delta — above MAX_DELTA=160° (U-turn, not a tack)
- assertTrue(TackDetector.detectTacks(tackTrack(90.0, 260.0)).isEmpty())
+ @Test fun `heading change of 170 degrees is detected`() {
+ // 170° delta — valid crash-tack; no upper cap
+ assertEquals(1, TackDetector.detectTacks(tackTrack(90.0, 260.0)).size)
}
@Test fun `detects two tacks in sequence with sufficient gap`() {