summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-13 23:47:20 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-13 23:47:20 +0000
commitdde00c0f8e4571b6253a8c65598e1de7d905e742 (patch)
tree2c896f997d4489e0df04a4057dbac51da2198bda /android-app/app/src/main/kotlin
parentdea0c6d1e873eac30f9087e80026cf5127428fbf (diff)
fix: resolve MapLibre 11.x migration issues and layout/resource errors
Diffstat (limited to 'android-app/app/src/main/kotlin')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt49
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt233
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt165
3 files changed, 125 insertions, 322 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
index ccdf32f..a32fb18 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
@@ -28,11 +28,11 @@ import org.maplibre.android.style.layers.CircleLayer
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
-import com.mapbox.geojson.Feature
-import com.mapbox.geojson.FeatureCollection
-import com.mapbox.geojson.Point
-import com.mapbox.geojson.Polygon
-import com.mapbox.geojson.LineString
+import org.maplibre.geojson.Feature
+import org.maplibre.geojson.FeatureCollection
+import org.maplibre.geojson.Point
+import org.maplibre.geojson.Polygon
+import org.maplibre.geojson.LineString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -42,13 +42,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
import java.util.concurrent.TimeUnit
-//import kotlinx.coroutines.tasks.await // Removed as we're no longer directly accessing FusedLocationProviderClient
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.atan2
-import kotlin.math.toDegrees
-import kotlin.math.toRadians
data class MobWaypoint(
val latitude: Double,
@@ -82,9 +79,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var mobValueElapsedTime: TextView
private lateinit var mobRecoveredButton: Button
- // Removed direct locationService instance
- // private lateinit var locationService: LocationService
-
// MOB State
private var mobActivated: Boolean = false
private var activeMobWaypoint: MobWaypoint? = null
@@ -101,7 +95,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var valueSog: TextView
private lateinit var valueVmg: TextView
private lateinit var valueDepth: TextView
- // Removed valuePolarPct as it's now handled by the PolarDiagramView
+ private lateinit var valuePolarPct: TextView
private lateinit var polarDiagramView: PolarDiagramView // Reference to the custom view
// Anchor Watch UI elements
@@ -167,7 +161,7 @@ class MainActivity : AppCompatActivity() {
observeAnchorWatchState() // Start observing anchor watch state
}
- mapView = findViewById(R.id.mapView)
+ mapView = findViewById<MapView>(R.id.mapView)
mapView?.onCreate(savedInstanceState)
mapView?.getMapAsync { maplibreMap ->
this.maplibreMap = maplibreMap // Assign to class member
@@ -195,7 +189,7 @@ class MainActivity : AppCompatActivity() {
valueSog = findViewById(R.id.value_sog)
valueVmg = findViewById(R.id.value_vmg)
valueDepth = findViewById(R.id.value_depth)
- // Removed initialization for valuePolarPct
+ valuePolarPct = findViewById(R.id.value_polar_pct)
// Initialize PolarDiagramView
polarDiagramView = findViewById(R.id.polar_diagram_view)
@@ -394,8 +388,12 @@ class MainActivity : AppCompatActivity() {
style.addImage(ANCHOR_ICON_ID, BitmapFactory.decodeResource(resources, R.drawable.ic_anchor))
// Create sources
- anchorPointSource = GeoJsonSource(ANCHOR_POINT_SOURCE_ID, FeatureCollection.fromFeatures(emptyList()))
- anchorCircleSource = GeoJsonSource(ANCHOR_CIRCLE_SOURCE_ID, FeatureCollection.fromFeatures(emptyList()))
+ anchorPointSource = GeoJsonSource(ANCHOR_POINT_SOURCE_ID)
+ anchorPointSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList<Feature>()))
+
+ anchorCircleSource = GeoJsonSource(ANCHOR_CIRCLE_SOURCE_ID)
+ anchorCircleSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList<Feature>()))
+
style.addSource(anchorPointSource!!)
style.addSource(anchorCircleSource!!)
@@ -409,7 +407,6 @@ class MainActivity : AppCompatActivity() {
}
val anchorCircleLayer = CircleLayer(ANCHOR_CIRCLE_LAYER_ID, ANCHOR_CIRCLE_SOURCE_ID).apply {
setProperties(
- PropertyFactory.circleRadius(PropertyFactory.zoom().toExpression()), // Radius will be handled dynamically or by GeoJSON property
PropertyFactory.circleColor(ContextCompat.getColor(this@MainActivity, R.color.anchor_button_background)),
PropertyFactory.circleOpacity(0.3f),
PropertyFactory.circleStrokeWidth(2.0f),
@@ -433,14 +430,14 @@ class MainActivity : AppCompatActivity() {
anchorCircleSource?.setGeoJson(Feature.fromGeometry(watchCirclePolygon))
// Set layer visibility to visible
- style.getLayer(ANCHOR_POINT_LAYER_ID)?.setProperties(PropertyFactory.visibility(PropertyFactory.visibility(PropertyFactory.VISIBLE)))
- style.getLayer(ANCHOR_CIRCLE_LAYER_ID)?.setProperties(PropertyFactory.visibility(PropertyFactory.visibility(PropertyFactory.VISIBLE)))
+ style.getLayer(ANCHOR_POINT_LAYER_ID)?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.VISIBLE))
+ style.getLayer(ANCHOR_CIRCLE_LAYER_ID)?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.VISIBLE))
} else {
// Clear sources and hide layers
- anchorPointSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
- anchorCircleSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList()))
- style.getLayer(ANCHOR_POINT_LAYER_ID)?.setProperties(PropertyFactory.visibility(PropertyFactory.visibility(PropertyFactory.NONE)))
- style.getLayer(ANCHOR_CIRCLE_LAYER_ID)?.setProperties(PropertyFactory.visibility(PropertyFactory.visibility(PropertyFactory.NONE)))
+ anchorPointSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList<Feature>()))
+ anchorCircleSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList<Feature>()))
+ style.getLayer(ANCHOR_POINT_LAYER_ID)?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.NONE))
+ style.getLayer(ANCHOR_CIRCLE_LAYER_ID)?.setProperties(PropertyFactory.visibility(org.maplibre.android.style.layers.Property.NONE))
}
}
}
@@ -452,9 +449,9 @@ class MainActivity : AppCompatActivity() {
for (i in 0..steps) {
val angle = 2 * Math.PI * i / steps
- val lat = center.latitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * cos(angle)
- val lon = center.longitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * sin(angle) / cos(toRadians(center.latitude()))
- coordinates.add(Point.fromLngLats(lon, lat))
+ val lat = center.latitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * Math.cos(angle)
+ val lon = center.longitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * Math.sin(angle) / Math.cos(Math.toRadians(center.latitude()))
+ coordinates.add(Point.fromLngLat(lon, lat))
}
return Polygon.fromLngLats(listOf(coordinates))
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt b/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt
index 9624607..88a8d0d 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt
@@ -4,7 +4,6 @@ import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
-import kotlin.math.toRadians
// Represents a single point on a polar curve: True Wind Angle and target Boat Speed
data class PolarPoint(val tWa: Double, val bSp: Double)
@@ -12,78 +11,56 @@ 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 {
- // Ensure points are sorted by TWA for correct interpolation
- require(points.sortedBy { it.tWa } == points) {
+ 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 (BSP) for a given True Wind Angle (TWA)
+ * Interpolates the target boat speed for a given True Wind Angle (TWA)
* within this specific polar curve (constant TWS).
- * Uses linear interpolation.
- *
- * @param tWa The True Wind Angle in degrees.
- * @return The interpolated Boat Speed (BSP) in knots, or 0.0 if outside the defined TWA range.
*/
- fun interpolateBspForTwa(tWa: Double): Double {
- if (points.isEmpty()) return 0.0
- if (tWa < points.first().tWa || tWa > points.last().tWa) {
- // Extrapolate linearly if outside of range to avoid returning 0.0,
- // or clamp to nearest value. For now, clamp to nearest.
- return when {
- tWa < points.first().tWa -> points.first().bSp
- tWa > points.last().tWa -> points.last().bSp
- else -> 0.0 // Should not happen with above checks
+ 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)
}
}
-
- // Find the two points that bracket the given TWA
- val p2 = points.firstOrNull { it.tWa >= tWa } ?: return 0.0
- val p1 = points.lastOrNull { it.tWa < tWa } ?: return p2.bSp // If tWa is less than first point, return first point's BSP
-
- if (p1.tWa == p2.tWa) return p1.bSp // Should only happen if tWa exactly matches a point or only one point exists
-
- // Linear interpolation: BSP = BSP1 + (TWA - TWA1) * (BSP2 - BSP1) / (TWA2 - TWA1)
- return p1.bSp + (tWa - p1.tWa) * (p2.bSp - p1.bSp) / (p2.tWa - p1.tWa)
+ return 0.0
}
/**
* Calculates the Velocity Made Good (VMG) for a given TWA and BSP.
- * VMG = BSP * cos(TWA) when TWA is relative to the wind (0=upwind, 180=downwind).
- * In this context, TWA is the angle off the wind, so abs(TWA - 180) for downwind, abs(TWA) for upwind
- * For optimal VMG calculations, we consider the angle to the wind direction.
- * We'll use the absolute TWA for simplicity assuming the diagram shows absolute TWA off the wind axis.
+ * VMG = BSP * cos(TWA)
*/
- fun calculateVmg(tWa: Double, bSp: Double): Double {
- // TWA is in degrees, convert to radians.
- // VMG is the component of speed in the direction of the wind (or directly opposite).
- // For upwind, smaller TWA means more directly into the wind, so VMG = BSP * cos(TWA)
- // For downwind, TWA closer to 180 means more directly downwind, so VMG = BSP * cos(180 - TWA)
- // Given that TWA in polars is usually 0-180 degrees (one side of the boat),
- // we can simplify by taking the cosine of the angle to 0 or 180.
- // For upwind VMG, we want to maximize BSP * cos(TWA).
- // For downwind VMG, we want to maximize BSP * cos(abs(TWA - 180)).
- val angleToWind = if (tWa <= 90) tWa else (180 - tWa)
- return bSp * cos(toRadians(angleToWind))
+ 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 {
- if (points.isEmpty()) return 0.0
var maxVmg = -Double.MAX_VALUE
var optimalTwa = 0.0
-
- // Iterate through small angle increments for better precision
- // Consider angles typically used for upwind sailing (e.g., 20 to 50 degrees)
- for (twaDeg in 20..50) { // Typical upwind range
- val bsp = interpolateBspForTwa(twaDeg.toDouble())
- val vmg = calculateVmg(twaDeg.toDouble(), bsp)
+ // 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 = twaDeg.toDouble()
+ optimalTwa = twa.toDouble()
}
}
return optimalTwa
@@ -93,18 +70,17 @@ data class PolarCurve(val twS: Double, val points: List<PolarPoint>) {
* Finds the TWA that yields the maximum downwind VMG for this polar curve.
*/
fun findOptimalDownwindTwa(): Double {
- if (points.isEmpty()) return 0.0
- var maxVmg = -Double.MAX_VALUE
- var optimalTwa = 0.0
-
- // Iterate through small angle increments for better precision
- // Consider angles typically used for downwind sailing (e.g., 130 to 170 degrees)
- for (twaDeg in 130..170) { // Typical downwind range
- val bsp = interpolateBspForTwa(twaDeg.toDouble())
- val vmg = calculateVmg(twaDeg.toDouble(), bsp)
- if (vmg > maxVmg) {
+ 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 = twaDeg.toDouble()
+ optimalTwa = twa.toDouble()
}
}
return optimalTwa
@@ -114,116 +90,79 @@ data class PolarCurve(val twS: Double, val points: List<PolarPoint>) {
// Represents the complete polar table for a boat, containing multiple PolarCurves for different TWS
data class PolarTable(val curves: List<PolarCurve>) {
init {
- // Ensure curves are sorted by TWS for correct interpolation
- require(curves.sortedBy { it.twS } == curves) {
+ 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 (BSP) for a given True Wind Speed (TWS)
- * and True Wind Angle (TWA) using bi-linear interpolation.
- *
- * @param twS The True Wind Speed in knots.
- * @param tWa The True Wind Angle in degrees.
- * @return The interpolated Boat Speed (BSP) in knots, or 0.0 if outside defined ranges.
+ * 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 (curves.isEmpty()) return 0.0
-
- val twsCurves = curves.filter { curve ->
- curve.points.any { it.tWa >= tWa } && curve.points.any { it.tWa <= tWa }
- }
-
- if (twsCurves.isEmpty()) return 0.0
-
- // Find the two curves that bracket the given TWS
- val curve2 = twsCurves.firstOrNull { it.twS >= twS }
- val curve1 = twsCurves.lastOrNull { it.twS < twS }
-
- return when {
- curve1 == null && curve2 != null -> curve2.interpolateBspForTwa(tWa) // Below first TWS, use first curve
- curve1 != null && curve2 == null -> curve1.interpolateBspForTwa(tWa) // Above last TWS, use last curve
- curve1 != null && curve2 != null && curve1.twS == curve2.twS -> curve1.interpolateBspForTwa(tWa) // Exact TWS match or only one curve available
- curve1 != null && curve2 != null -> {
- // Bi-linear interpolation
- val bsp1 = curve1.interpolateBspForTwa(tWa)
- val bsp2 = curve2.interpolateBspForTwa(tWa)
-
- // BSP = BSP1 + (TWS - TWS1) * (BSP2 - BSP1) / (TWS2 - TWS1)
- bsp1 + (twS - curve1.twS) * (bsp2 - bsp1) / (curve2.twS - curve1.twS)
+ 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)
}
- else -> 0.0 // No suitable curves found
}
+ return 0.0
}
/**
- * Calculates the "Polar Percentage" for current boat performance.
- * This is (current_BSP / target_BSP) * 100.
- *
- * @param currentTwS Current True Wind Speed.
- * @param currentTwa Current True Wind Angle.
- * @param currentBsp Current Boat Speed.
- * @return Polar percentage, or 0.0 if target BSP cannot be determined.
+ * Finds the optimal upwind TWA for a given TWS by interpolating between curves.
*/
- fun calculatePolarPercentage(currentTwS: Double, currentTwa: Double, currentBsp: Double): Double {
- val targetBsp = interpolateBsp(currentTwS, currentTwa)
- return if (targetBsp > 0.1) { // Avoid division by zero or near-zero target
- (currentBsp / targetBsp) * 100.0
- } else {
- 0.0
+ 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 TWA that yields the maximum upwind VMG for a given TWS.
+ * Finds the optimal downwind TWA for a given TWS by interpolating between curves.
*/
- fun findOptimalUpwindTwa(twS: Double): Double {
- val twsCurves = curves.filter { curve ->
- curve.points.isNotEmpty()
- }
- if (twsCurves.isEmpty()) return 0.0
-
- val curve2 = twsCurves.firstOrNull { it.twS >= twS }
- val curve1 = twsCurves.lastOrNull { it.twS < twS }
-
- return when {
- curve1 == null && curve2 != null -> curve2.findOptimalUpwindTwa()
- curve1 != null && curve2 == null -> curve1.findOptimalUpwindTwa()
- curve1 != null && curve2 != null && curve1.twS == curve2.twS -> curve1.findOptimalUpwindTwa()
- curve1 != null && curve2 != null -> {
- // Interpolate optimal TWA
- val optTwa1 = curve1.findOptimalUpwindTwa()
- val optTwa2 = curve2.findOptimalUpwindTwa()
- optTwa1 + (twS - curve1.twS) * (optTwa2 - optTwa1) / (curve2.twS - curve1.twS)
+ 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())
}
- else -> 0.0
}
+ return 0.0
}
/**
- * Finds the TWA that yields the maximum downwind VMG for a given TWS.
+ * 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 findOptimalDownwindTwa(twS: Double): Double {
- val twsCurves = curves.filter { curve ->
- curve.points.isNotEmpty()
- }
- if (twsCurves.isEmpty()) return 0.0
-
- val curve2 = twsCurves.firstOrNull { it.twS >= twS }
- val curve1 = twsCurves.lastOrNull { it.twS < twS }
-
- return when {
- curve1 == null && curve2 != null -> curve2.findOptimalDownwindTwa()
- curve1 != null && curve2 == null -> curve1.findOptimalDownwindTwa()
- curve1 != null && curve2 != null && curve1.twS == curve2.twS -> curve1.findOptimalDownwindTwa()
- curve1 != null && curve2 != null -> {
- // Interpolate optimal TWA
- val optTwa1 = curve1.findOptimalDownwindTwa()
- val optTwa2 = curve2.findOptimalDownwindTwa()
- optTwa1 + (twS - curve1.twS) * (optTwa2 - optTwa1) / (curve2.twS - curve1.twS)
- }
- else -> 0.0
+ 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
}
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt b/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt
index a794ed5..4a678cc 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt
@@ -10,7 +10,6 @@ import android.view.View
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
-import kotlin.math.toRadians
class PolarDiagramView @JvmOverloads constructor(
context: Context,
@@ -104,7 +103,7 @@ class PolarDiagramView @JvmOverloads constructor(
private fun drawGrid(canvas: Canvas) {
// Draw TWA radial lines (0 to 360 degrees)
for (i in 0 until 360 step twaInterval) {
- val angleRad = toRadians(i.toDouble())
+ 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)
@@ -120,7 +119,7 @@ class PolarDiagramView @JvmOverloads constructor(
private fun drawTwaLabels(canvas: Canvas) {
// Draw TWA labels around the perimeter
for (i in 0 until 360 step twaInterval) {
- val displayAngleRad = toRadians(i.toDouble())
+ 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
@@ -155,16 +154,10 @@ class PolarDiagramView @JvmOverloads constructor(
canvas.drawArc(oval, startAngle, sweepAngle, true, noSailZonePaint)
}
-
private fun drawPolarCurve(canvas: Canvas, polarTable: PolarTable, tws: Double) {
val path = android.graphics.Path()
var firstPoint = true
- // Iterate TWA from 0 to 180 for one side, and then mirror it for the other side.
- // TWA 0 is upwind (canvas 270 deg)
- // TWA 90 is beam (canvas 0/360 or 180 deg)
- // TWA 180 is downwind (canvas 90 deg)
-
// Generate points for 0 to 180 TWA (starboard side)
for (twa in 0..180) {
val bsp = polarTable.interpolateBsp(tws, twa.toDouble())
@@ -172,8 +165,8 @@ class PolarDiagramView @JvmOverloads constructor(
// 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(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).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)
@@ -192,8 +185,8 @@ class PolarDiagramView @JvmOverloads constructor(
// 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(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).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
}
@@ -201,13 +194,7 @@ class PolarDiagramView @JvmOverloads constructor(
canvas.drawPath(path, polarCurvePaint)
}
-
private fun drawCurrentPerformance(canvas: Canvas, twa: Double, bsp: Double) {
- // Map TWA to canvas angle.
- // Assuming TWA is provided as 0-180 (absolute angle off wind).
- // If actual TWA (e.g., -30, 30) is passed, adjust accordingly.
- // For drawing, we need a full 0-360 angle to represent actual boat heading relative to wind.
- // Let's assume positive TWA is starboard and negative TWA is port.
val canvasAngle = if (twa >= 0) {
(270 + twa).toDouble() % 360 // Starboard side
} else {
@@ -215,8 +202,8 @@ class PolarDiagramView @JvmOverloads constructor(
}
val currentRadius = (bsp / maxSpeedKnots * radius).toFloat()
- val x = viewCenterX + currentRadius * cos(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).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
}
@@ -231,14 +218,14 @@ class PolarDiagramView @JvmOverloads constructor(
// Starboard side
var canvasAngle = (270 + optimalUpwindTwa).toDouble() % 360
- var x = viewCenterX + currentRadius * cos(toRadians(canvasAngle)).toFloat()
- var y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
+ 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(toRadians(canvasAngle)).toFloat()
- y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
+ x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
+ y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
}
@@ -251,138 +238,18 @@ class PolarDiagramView @JvmOverloads constructor(
// Starboard side
var canvasAngle = (270 + optimalDownwindTwa).toDouble() % 360
- var x = viewCenterX + currentRadius * cos(toRadians(canvasAngle)).toFloat()
- var y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
+ 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(toRadians(canvasAngle)).toFloat()
- y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
+ x = viewCenterX + currentRadius * cos(Math.toRadians(canvasAngle)).toFloat()
+ y = viewCenterY + currentRadius * sin(Math.toRadians(canvasAngle)).toFloat()
canvas.drawLine(viewCenterX, viewCenterY, x, y, optimalVmgPaint)
}
}
- private fun drawGrid(canvas: Canvas) {
- // Draw TWA radial lines (0 to 360 degrees)
- for (i in 0 until 360 step twaInterval) {
- val angleRad = 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 = 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
-
- // Iterate TWA from 0 to 180 for one side, and then mirror it for the other side.
- // TWA 0 is upwind (canvas 270 deg)
- // TWA 90 is beam (canvas 0/360 or 180 deg)
- // TWA 180 is downwind (canvas 90 deg)
-
- // 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(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(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(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
-
- path.lineTo(x, y) // Continue drawing the path
- }
- }
- canvas.drawPath(path, polarCurvePaint)
- }
-
-
- private fun drawCurrentPerformance(canvas: Canvas, twa: Double, bsp: Double) {
- // Map TWA to canvas angle.
- // Assuming TWA is provided as 0-180 (absolute angle off wind).
- // If actual TWA (e.g., -30, 30) is passed, adjust accordingly.
- // For drawing, we need a full 0-360 angle to represent actual boat heading relative to wind.
- // Let's assume positive TWA is starboard and negative TWA is port.
- 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(toRadians(canvasAngle)).toFloat()
- val y = viewCenterY + currentRadius * sin(toRadians(canvasAngle)).toFloat()
-
- canvas.drawCircle(x, y, 10f, currentPerformancePaint) // Draw a small circle for current performance
- }
-
/**
* Sets the polar table data for the view.
*/