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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
package org.terst.nav
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
class PolarDiagramView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val gridPaint = Paint().apply {
color = Color.parseColor("#404040") // Dark gray for grid lines
style = Paint.Style.STROKE
strokeWidth = 1f
isAntiAlias = true
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 24f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val polarCurvePaint = Paint().apply {
color = Color.CYAN // Bright color for the polar curve
style = Paint.Style.STROKE
strokeWidth = 3f
isAntiAlias = true
}
private val currentPerformancePaint = Paint().apply {
color = Color.RED // Red dot for current performance
style = Paint.Style.FILL
isAntiAlias = true
}
private val noSailZonePaint = Paint().apply {
color = Color.parseColor("#80FF0000") // Semi-transparent red for no-sail zone
style = Paint.Style.FILL
isAntiAlias = true
}
private val optimalVmgPaint = Paint().apply {
color = Color.GREEN // Green for optimal VMG angles
style = Paint.Style.STROKE
strokeWidth = 4f
isAntiAlias = true
}
private var viewCenterX: Float = 0f
private var viewCenterY: Float = 0f
private var radius: Float = 0f
// Data for rendering
private var polarTable: PolarTable? = null
private var currentTws: Double = 0.0
private var currentTwa: Double = 0.0
private var currentBsp: Double = 0.0
// Configuration for the diagram
private val maxSpeedKnots = 10.0 // Max speed for the outermost circle in knots
private val speedCircleInterval = 2.0 // Interval between speed circles in knots
private val twaInterval = 30 // Interval between TWA radial lines in degrees
private val noSailZoneAngle = 20.0 // Angle +/- from 0 degrees for no-sail zone
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewCenterX = w / 2f
viewCenterY = h / 2f
radius = min(w, h) / 2f * 0.9f // Use 90% of the minimum dimension for radius
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Draw basic diagram elements
drawGrid(canvas)
drawTwaLabels(canvas)
drawNoSailZone(canvas)
// Draw polar curve if data is available
polarTable?.let {
drawPolarCurve(canvas, it, currentTws)
drawOptimalVmgAngles(canvas, it, currentTws) // Draw optimal VMG angles
}
// Draw current performance if data is available and not zero
if (currentTws > 0 && currentTwa > 0 && currentBsp > 0) {
drawCurrentPerformance(canvas, currentTwa, currentBsp)
}
}
private fun drawGrid(canvas: Canvas) {
// Draw TWA radial lines (0 to 360 degrees)
for (i in 0 until 360 step twaInterval) {
val angleRad = Math.toRadians(i.toDouble())
val x = viewCenterX + radius * cos(angleRad).toFloat()
val y = viewCenterY + radius * sin(angleRad).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, gridPaint)
}
// Draw speed circles
for (i in 0..maxSpeedKnots.toInt() step speedCircleInterval.toInt()) {
val currentRadius = (i / maxSpeedKnots * radius).toFloat()
canvas.drawCircle(viewCenterX, viewCenterY, currentRadius, gridPaint)
}
}
private fun drawTwaLabels(canvas: Canvas) {
// Draw TWA labels around the perimeter
for (i in 0 until 360 step twaInterval) {
val displayAngleRad = Math.toRadians(i.toDouble())
// Position the text slightly outside the outermost circle
val textX = viewCenterX + (radius + 40) * cos(displayAngleRad).toFloat()
// Adjust textY to account for text height, so it's centered vertically on the arc
val textY = viewCenterY + (radius + 40) * sin(displayAngleRad).toFloat() + (textPaint.textSize / 3)
// Map canvas angle (0=right, 90=down) to polar diagram angle (0=up, 90=right)
// Example: canvas 270 is polar 0, canvas 0 is polar 90, canvas 90 is polar 180, canvas 180 is polar 270
val polarAngle = ( (i + 90) % 360 )
canvas.drawText(polarAngle.toString(), textX, textY, textPaint)
}
// Draw speed labels on the horizontal axis
for (i in 0..maxSpeedKnots.toInt() step speedCircleInterval.toInt()) {
if (i > 0) {
val currentRadius = (i / maxSpeedKnots * radius).toFloat()
// Left side
canvas.drawText(i.toString(), viewCenterX - currentRadius - 10, viewCenterY + (textPaint.textSize / 3), textPaint)
// Right side
canvas.drawText(i.toString(), viewCenterX + currentRadius + 10, viewCenterY + (textPaint.textSize / 3), textPaint)
}
}
}
private fun drawNoSailZone(canvas: Canvas) {
// The no-sail zone is typically symmetric around the wind direction (0 TWA, which is 'up' on our diagram)
// In canvas coordinates, 'up' is -90 degrees or 270 degrees.
// So the arc will be centered around 270 degrees.
val startAngle = (270 - noSailZoneAngle).toFloat()
val sweepAngle = (2 * noSailZoneAngle).toFloat()
val oval = RectF(viewCenterX - radius, viewCenterY - radius, viewCenterX + radius, viewCenterY + radius)
canvas.drawArc(oval, startAngle, sweepAngle, true, noSailZonePaint)
}
private fun drawPolarCurve(canvas: Canvas, polarTable: PolarTable, tws: Double) {
val path = android.graphics.Path()
var firstPoint = true
// Generate points for 0 to 180 TWA (starboard side)
for (twa in 0..180) {
val bsp = polarTable.interpolateBsp(tws, twa.toDouble())
if (bsp > 0) {
// Map TWA to canvas angle for the starboard side (0 TWA at 270, 90 TWA at 0, 180 TWA at 90)
val canvasAngle = (270 + twa).toDouble() % 360
val currentRadius = (bsp / maxSpeedKnots * radius).toFloat()
val x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
val y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
if (firstPoint) {
path.moveTo(x, y)
firstPoint = false
} else {
path.lineTo(x, y)
}
}
}
// Generate points for 0 to -180 TWA (port side) by mirroring
// Start from 180 back to 0 to connect the curve
for (twa in 180 downTo 0) {
val bsp = polarTable.interpolateBsp(tws, twa.toDouble())
if (bsp > 0) {
// Map negative TWA to canvas angle for the port side (0 TWA at 270, -90 TWA at 180, -180 TWA at 90)
val canvasAngle = (270 - twa).toDouble() // This maps TWA 0 to 270, TWA 90 to 180, TWA 180 to 90
val currentRadius = (bsp / maxSpeedKnots * radius).toFloat()
val x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
val y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
path.lineTo(x, y) // Continue drawing the path
}
}
canvas.drawPath(path, polarCurvePaint)
}
private fun drawCurrentPerformance(canvas: Canvas, twa: Double, bsp: Double) {
val canvasAngle = if (twa >= 0) {
(270 + twa).toDouble() % 360 // Starboard side
} else {
(270 + twa).toDouble() // Port side (e.g., -30 TWA is 240 canvas angle)
}
val currentRadius = (bsp / maxSpeedKnots * radius).toFloat()
val x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
val y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawCircle(x, y, 10f, currentPerformancePaint) // Draw a small circle for current performance
}
private fun drawOptimalVmgAngles(canvas: Canvas, polarTable: PolarTable, tws: Double) {
// Find optimal upwind TWA
val optimalUpwindTwa = polarTable.findOptimalUpwindTwa(tws)
if (optimalUpwindTwa > 0) {
// Draw a line indicating the optimal upwind TWA (both port and starboard)
val upwindBsp = polarTable.interpolateBsp(tws, optimalUpwindTwa)
val currentRadius = (upwindBsp / maxSpeedKnots * radius).toFloat() * 1.05f // Slightly longer
// Starboard side
var canvasAngle = (270 + optimalUpwindTwa).toDouble() % 360
var x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
var y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
// Port side
canvasAngle = (270 - optimalUpwindTwa).toDouble() // Use negative TWA for port side
x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
}
// Find optimal downwind TWA
val optimalDownwindTwa = polarTable.findOptimalDownwindTwa(tws)
if (optimalDownwindTwa > 0) {
// Draw a line indicating the optimal downwind TWA (both port and starboard)
val downwindBsp = polarTable.interpolateBsp(tws, optimalDownwindTwa)
val currentRadius = (downwindBsp / maxSpeedKnots * radius).toFloat() * 1.05f // Slightly longer
// Starboard side
var canvasAngle = (270 + optimalDownwindTwa).toDouble() % 360
var x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
var y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
// Port side
canvasAngle = (270 - optimalDownwindTwa).toDouble() // Use negative TWA for port side
x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
}
}
/**
* Sets the polar table data for the view.
*/
fun setPolarTable(table: PolarTable) {
this.polarTable = table
invalidate() // Redraw the view
}
/**
* Sets the current true wind speed, true wind angle, and boat speed.
*/
fun setCurrentPerformance(tws: Double, twa: Double, bsp: Double) {
this.currentTws = tws
this.currentTwa = twa
this.currentBsp = bsp
invalidate() // Redraw the view
}
}
|