summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt
blob: 88a8d0d87ae1bf7adf5e7c0f0ffc9dd23964b5e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package org.terst.nav

import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min

// Represents a single point on a polar curve: True Wind Angle and target Boat Speed
data class PolarPoint(val tWa: Double, val bSp: Double)

// Represents a polar curve for a specific True Wind Speed
data class PolarCurve(val twS: Double, val points: List<PolarPoint>) {
    init {
        require(points.isNotEmpty()) { "PolarCurve must have at least one point." }
        require(points.all { it.tWa in 0.0..180.0 }) {
            "TWA in PolarCurve must be between 0 and 180 degrees."
        }
        require(points.zipWithNext().all { it.first.tWa < it.second.tWa }) {
            "PolarPoints in a PolarCurve must be sorted by TWA."
        }
    }

    /**
     * Interpolates the target boat speed for a given True Wind Angle (TWA)
     * within this specific polar curve (constant TWS).
     */
    fun interpolateBsp(twa: Double): Double {
        val absoluteTwa = abs(twa)
        if (absoluteTwa < points.first().tWa) return points.first().bSp
        if (absoluteTwa > points.last().tWa) return points.last().bSp

        for (i in 0 until points.size - 1) {
            val p1 = points[i]
            val p2 = points[i + 1]
            if (absoluteTwa >= p1.tWa && absoluteTwa <= p2.tWa) {
                val ratio = (absoluteTwa - p1.tWa) / (p2.tWa - p1.tWa)
                return p1.bSp + ratio * (p2.bSp - p1.bSp)
            }
        }
        return 0.0
    }

    /**
     * Calculates the Velocity Made Good (VMG) for a given TWA and BSP.
     * VMG = BSP * cos(TWA)
     */
    fun calculateVmg(twa: Double, bsp: Double): Double {
        return bsp * cos(Math.toRadians(twa))
    }

    /**
     * Finds the TWA that yields the maximum upwind VMG for this polar curve.
     */
    fun findOptimalUpwindTwa(): Double {
        var maxVmg = -Double.MAX_VALUE
        var optimalTwa = 0.0
        // Search through TWA 0 to 90
        for (twa in 0..90) {
            val bsp = interpolateBsp(twa.toDouble())
            val vmg = calculateVmg(twa.toDouble(), bsp)
            if (vmg > maxVmg) {
                maxVmg = vmg
                optimalTwa = twa.toDouble()
            }
        }
        return optimalTwa
    }

    /**
     * Finds the TWA that yields the maximum downwind VMG for this polar curve.
     */
    fun findOptimalDownwindTwa(): Double {
        var maxVmg = -Double.MAX_VALUE // We want the most negative VMG for downwind
        var optimalTwa = 180.0
        // Search through TWA 90 to 180
        // For downwind, VMG is negative (moving away from wind)
        // We look for the minimum value (largest absolute negative)
        for (twa in 90..180) {
            val bsp = interpolateBsp(twa.toDouble())
            val vmg = calculateVmg(twa.toDouble(), bsp)
            if (vmg < maxVmg) {
                maxVmg = vmg
                optimalTwa = twa.toDouble()
            }
        }
        return optimalTwa
    }
}

// Represents the complete polar table for a boat, containing multiple PolarCurves for different TWS
data class PolarTable(val curves: List<PolarCurve>) {
    init {
        require(curves.isNotEmpty()) { "PolarTable must have at least one curve." }
        require(curves.zipWithNext().all { it.first.twS < it.second.twS }) {
            "PolarCurves in a PolarTable must be sorted by TWS."
        }
    }

    /**
     * Interpolates the target boat speed for a given True Wind Speed (TWS) and True Wind Angle (TWA).
     */
    fun interpolateBsp(tws: Double, twa: Double): Double {
        if (tws <= curves.first().twS) return curves.first().interpolateBsp(twa)
        if (tws >= curves.last().twS) return curves.last().interpolateBsp(twa)

        for (i in 0 until curves.size - 1) {
            val c1 = curves[i]
            val c2 = curves[i + 1]
            if (tws >= c1.twS && tws <= c2.twS) {
                val ratio = (tws - c1.twS) / (c2.twS - c1.twS)
                val bsp1 = c1.interpolateBsp(twa)
                val bsp2 = c2.interpolateBsp(twa)
                return bsp1 + ratio * (bsp2 - bsp1)
            }
        }
        return 0.0
    }

    /**
     * Finds the optimal upwind TWA for a given TWS by interpolating between curves.
     */
    fun findOptimalUpwindTwa(tws: Double): Double {
        if (tws <= curves.first().twS) return curves.first().findOptimalUpwindTwa()
        if (tws >= curves.last().twS) return curves.last().findOptimalUpwindTwa()

        for (i in 0 until curves.size - 1) {
            val c1 = curves[i]
            val c2 = curves[i + 1]
            if (tws >= c1.twS && tws <= c2.twS) {
                val ratio = (tws - c1.twS) / (c2.twS - c1.twS)
                return c1.findOptimalUpwindTwa() + ratio * (c2.findOptimalUpwindTwa() - c1.findOptimalUpwindTwa())
            }
        }
        return 0.0
    }

    /**
     * Finds the optimal downwind TWA for a given TWS by interpolating between curves.
     */
    fun findOptimalDownwindTwa(tws: Double): Double {
        if (tws <= curves.first().twS) return curves.first().findOptimalDownwindTwa()
        if (tws >= curves.last().twS) return curves.last().findOptimalDownwindTwa()

        for (i in 0 until curves.size - 1) {
            val c1 = curves[i]
            val c2 = curves[i + 1]
            if (tws >= c1.twS && tws <= c2.twS) {
                val ratio = (tws - c1.twS) / (c2.twS - c1.twS)
                return c1.findOptimalDownwindTwa() + ratio * (c2.findOptimalDownwindTwa() - c1.findOptimalDownwindTwa())
            }
        }
        return 0.0
    }

    /**
     * Calculates the "Polar Percentage" for current boat performance.
     * Polar % = (Actual BSP / Target BSP) * 100
     * @return Polar percentage, or 0.0 if target BSP cannot be determined.
     */
    fun calculatePolarPercentage(currentTwS: Double, currentTwa: Double, currentBsp: Double): Double {
        val targetBsp = interpolateBsp(currentTwS, currentTwa)
        return if (targetBsp > 0) {
            (currentBsp / targetBsp) * 100.0
        } else {
            0.0
        }
    }
}