summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin/org/terst
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-15 05:55:47 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-25 04:54:49 +0000
commit984f915525184a9aaff87f3d5687ef46ebb00702 (patch)
treee22e374260e40eced792cd155829359d500df502 /android-app/app/src/main/kotlin/org/terst
parent826d56ede2c59cad19748f61d8b5d75d08a702d9 (diff)
feat: implement isochrone-based weather routing (Section 3.4)
Diffstat (limited to 'android-app/app/src/main/kotlin/org/terst')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/data/model/BoatPolars.kt69
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/data/model/WindForecast.kt18
2 files changed, 87 insertions, 0 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/BoatPolars.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/BoatPolars.kt
new file mode 100644
index 0000000..0286ea8
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/BoatPolars.kt
@@ -0,0 +1,69 @@
+package org.terst.nav.data.model
+
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+/**
+ * Boat polar speed table: maps (TWA, TWS) → BSP (boat speed through water, knots).
+ *
+ * Interpolation is bilinear — linear on TWA within a given TWS, then linear on TWS.
+ * Port-tack mirror: TWA > 180° is folded to 360° - TWA before lookup.
+ */
+data class BoatPolars(
+ /** Outer key: TWS in knots. Inner key: TWA in degrees [0, 180]. Value: BSP in knots. */
+ val table: Map<Double, Map<Double, Double>>
+) {
+ /**
+ * Returns boat speed (knots) for the given True Wind Angle and True Wind Speed.
+ * TWA outside [0, 360] is clamped; port/starboard symmetry is applied (>180° mirrored).
+ */
+ fun bsp(twaDeg: Double, twsKt: Double): Double {
+ val twa = if (twaDeg > 180.0) 360.0 - twaDeg else twaDeg.coerceIn(0.0, 180.0)
+
+ val twsKeys = table.keys.sorted()
+ if (twsKeys.isEmpty()) return 0.0
+
+ val twsClamped = twsKt.coerceIn(twsKeys.first(), twsKeys.last())
+ val twsLow = twsKeys.lastOrNull { it <= twsClamped } ?: twsKeys.first()
+ val twsHigh = twsKeys.firstOrNull { it >= twsClamped } ?: twsKeys.last()
+
+ val bspLow = bspAtTws(twa, table[twsLow] ?: return 0.0)
+ val bspHigh = bspAtTws(twa, table[twsHigh] ?: return 0.0)
+
+ return if (twsHigh == twsLow) bspLow
+ else {
+ val t = (twsClamped - twsLow) / (twsHigh - twsLow)
+ bspLow + t * (bspHigh - bspLow)
+ }
+ }
+
+ private fun bspAtTws(twaDeg: Double, twaMap: Map<Double, Double>): Double {
+ val twaKeys = twaMap.keys.sorted()
+ if (twaKeys.isEmpty()) return 0.0
+
+ val twaClamped = twaDeg.coerceIn(twaKeys.first(), twaKeys.last())
+ val twaLow = twaKeys.lastOrNull { it <= twaClamped } ?: twaKeys.first()
+ val twaHigh = twaKeys.firstOrNull { it >= twaClamped } ?: twaKeys.last()
+
+ val bspLow = twaMap[twaLow] ?: 0.0
+ val bspHigh = twaMap[twaHigh] ?: 0.0
+
+ return if (twaHigh == twaLow) bspLow
+ else {
+ val t = (twaClamped - twaLow) / (twaHigh - twaLow)
+ bspLow + t * (bspHigh - bspLow)
+ }
+ }
+
+ companion object {
+ /** Default polar for a typical 35-foot cruising sloop. */
+ val DEFAULT: BoatPolars = BoatPolars(
+ mapOf(
+ 5.0 to mapOf(45.0 to 3.5, 60.0 to 4.2, 90.0 to 4.8, 120.0 to 5.0, 150.0 to 4.5, 180.0 to 4.0),
+ 10.0 to mapOf(45.0 to 5.5, 60.0 to 6.5, 90.0 to 7.0, 120.0 to 7.2, 150.0 to 6.8, 180.0 to 6.0),
+ 15.0 to mapOf(45.0 to 6.5, 60.0 to 7.5, 90.0 to 8.0, 120.0 to 8.5, 150.0 to 8.0, 180.0 to 7.0),
+ 20.0 to mapOf(45.0 to 7.0, 60.0 to 8.0, 90.0 to 8.5, 120.0 to 9.0, 150.0 to 8.5, 180.0 to 7.5)
+ )
+ )
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/WindForecast.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/WindForecast.kt
new file mode 100644
index 0000000..f009da8
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/WindForecast.kt
@@ -0,0 +1,18 @@
+package org.terst.nav.data.model
+
+/**
+ * Wind conditions at a specific location and time.
+ *
+ * @param lat Latitude (decimal degrees).
+ * @param lon Longitude (decimal degrees).
+ * @param timestampMs UNIX time in milliseconds.
+ * @param twsKt True Wind Speed in knots.
+ * @param twdDeg True Wind Direction in degrees (the direction FROM which the wind blows, 0–360).
+ */
+data class WindForecast(
+ val lat: Double,
+ val lon: Double,
+ val timestampMs: Long,
+ val twsKt: Double,
+ val twdDeg: Double
+)