diff options
| author | Claudomator Agent <agent@claudomator> | 2026-03-15 05:55:47 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-25 04:54:49 +0000 |
| commit | 984f915525184a9aaff87f3d5687ef46ebb00702 (patch) | |
| tree | e22e374260e40eced792cd155829359d500df502 /android-app/app/src/main/kotlin/org/terst | |
| parent | 826d56ede2c59cad19748f61d8b5d75d08a702d9 (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.kt | 69 | ||||
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/data/model/WindForecast.kt | 18 |
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 +) |
