summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin/org/terst
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin/org/terst')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt78
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt172
2 files changed, 178 insertions, 72 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 023cb94..e5d3080 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
@@ -181,31 +181,42 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private fun setupHandlers() {
instrumentHandler = InstrumentHandler(
- valueAws = findViewById(R.id.value_aws),
- valueTws = findViewById(R.id.value_tws),
- valueHdg = findViewById(R.id.value_hdg),
- valueCog = findViewById(R.id.value_cog),
- valueBsp = findViewById(R.id.value_bsp),
- valueSog = findViewById(R.id.value_sog),
+ // Instrument TextViews
+ valueAws = findViewById(R.id.value_aws),
+ valueTws = findViewById(R.id.value_tws),
+ valueHdg = findViewById(R.id.value_hdg),
+ valueCog = findViewById(R.id.value_cog),
+ valueBsp = findViewById(R.id.value_bsp),
+ valueSog = findViewById(R.id.value_sog),
valueDepth = findViewById(R.id.value_depth),
- valueBaro = findViewById(R.id.value_baro),
- valueCurrSpd = findViewById(R.id.value_curr_spd),
- valueCurrDir = null,
- valueWaveHt = findViewById(R.id.value_wave_ht),
- valueWaveDir = null,
- valueSwellHt = findViewById(R.id.value_swell_ht),
- valueSwellPer = findViewById(R.id.value_swell_per)
+ valueBaro = findViewById(R.id.value_baro),
+ // Instrument arrows
+ arrowAws = findViewById(R.id.arrow_aws),
+ arrowTws = findViewById(R.id.arrow_tws),
+ arrowHdg = findViewById(R.id.arrow_hdg),
+ arrowCog = findViewById(R.id.arrow_cog),
+ // Forecast TextViews
+ valueCurrSpd = findViewById(R.id.value_curr_spd),
+ valueWaveHt = findViewById(R.id.value_wave_ht),
+ valueSwellHt = findViewById(R.id.value_swell_ht),
+ valueSwellPer = findViewById(R.id.value_swell_per),
+ // Forecast arrows
+ arrowCurr = findViewById(R.id.arrow_curr),
+ arrowWaves = findViewById(R.id.arrow_waves),
+ arrowSwell = findViewById(R.id.arrow_swell),
+ // Forecast bearing labels
+ bearingCurr = findViewById(R.id.bearing_curr),
+ bearingWaves = findViewById(R.id.bearing_waves),
+ bearingSwell = findViewById(R.id.bearing_swell),
+ // Wave view
+ waveView = findViewById(R.id.wave_divider)
)
instrumentHandler?.updateDisplay(
aws = "—", tws = "—", hdg = "—",
cog = "—", bsp = "—", sog = "—",
- depth = "—", baro = "—"
- )
- instrumentHandler?.updateConditions(
- currSpd = "—", currDir = "—",
- waveHt = "—", waveDir = "—",
- swellHt = "—", swellPer = "—"
+ baro = "—"
)
+ instrumentHandler?.updateConditions(currSpd = "—")
}
// Helper to convert dp to px
@@ -296,11 +307,11 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
LocationService.locationFlow.collect { gpsData ->
mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude)
mapHandler?.updateUserPosition(gpsData.latitude, gpsData.longitude, gpsData.cog.toFloat())
-
viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, gpsData.sog, gpsData.cog)
instrumentHandler?.updateDisplay(
sog = "%.1f".format(Locale.getDefault(), gpsData.sog),
- cog = "%.0f°".format(Locale.getDefault(), gpsData.cog)
+ cog = "%.0f°".format(Locale.getDefault(), gpsData.cog),
+ cogBearingDeg = gpsData.cog.toFloat()
)
if (!conditionsLoaded) {
conditionsLoaded = true
@@ -319,16 +330,27 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
viewModel.marineConditions.collect { c ->
if (c == null) return@collect
instrumentHandler?.updateDisplay(
- tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—"
+ tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) },
+ twsBearingDeg = c.windDirDeg?.toFloat()
)
instrumentHandler?.updateConditions(
- currSpd = c.currentSpeedKt?.let { "%.1f kn".format(Locale.getDefault(), it) } ?: "—",
- currDir = c.currentDirDeg?.let { "%.0f°".format(Locale.getDefault(), it) } ?: "—",
- waveHt = c.waveHeightM?.let { "%.1f m".format(Locale.getDefault(), it) } ?: "—",
- waveDir = c.waveDirDeg?.let { "%.0f°".format(Locale.getDefault(), it) } ?: "—",
- swellHt = c.swellHeightM?.let { "%.1f m".format(Locale.getDefault(), it) } ?: "—",
- swellPer = c.swellPeriodS?.let { "%.0f s".format(Locale.getDefault(), it) } ?: "—"
+ currSpd = c.currentSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—",
+ currDirDeg = c.currentDirDeg?.toFloat(),
+ waveHeightM = c.waveHeightM,
+ waveDirDeg = c.waveDirDeg?.toFloat(),
+ swellHeightM = c.swellHeightM,
+ swellDirDeg = c.swellDirDeg?.toFloat(),
+ swellPeriodS = c.swellPeriodS
)
+ val swellHtFt = c.swellHeightM?.let { (it * 3.28084f).toFloat() } ?: 3f
+ val windHtFt = c.waveHeightM?.let { (it * 3.28084f).toFloat() } ?: 1.5f
+ val swellPeriod = c.swellPeriodS?.toFloat() ?: 10f
+ instrumentHandler?.updateWaveState(swellHtFt, swellPeriod, windHtFt)
+ }
+ }
+ lifecycleScope.launch {
+ LocationService.nmeaDepthDataFlow.collect { depthData ->
+ instrumentHandler?.updateDisplay(depthM = depthData.depthMeters)
}
}
lifecycleScope.launch {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
index 370d8cf..84815ce 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
@@ -1,66 +1,150 @@
package org.terst.nav.ui
import android.widget.TextView
+import java.util.Locale
+
+// ── Pure formatting helpers (top-level for testability) ──────────────────────
+
+private const val M_TO_FT = 3.28084
+
+/** Converts metres to feet. */
+fun metresToFeet(metres: Double): Double = metres * M_TO_FT
+
+/** Formats a feet value to one decimal place. */
+fun formatFt(ft: Double, locale: Locale = Locale.getDefault()): String =
+ "%.1f".format(locale, ft)
+
+/** Formats a bearing to zero decimal places with a degree symbol. */
+fun formatBearing(deg: Double, locale: Locale = Locale.getDefault()): String =
+ "%.0f°".format(locale, deg)
+
+/** Formats a swell period with leading dot separator. */
+fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String =
+ "· %.0fs".format(locale, sec)
+
+// ── InstrumentHandler ────────────────────────────────────────────────────────
/**
- * Handles the display of instrument data in the UI.
+ * Drives all text fields, direction arrows, and the wave view in the
+ * instrument bottom sheet.
+ *
+ * Forecast [DirectionArrowView] instances are initialised with OCEAN style.
+ *
+ * Units contract:
+ * - Speed values: pre-formatted strings in knots (caller's responsibility)
+ * - Height values: caller passes raw metres; this class converts to feet
+ * - Bearing values: raw degrees as Float, rotated into arrows and formatted
+ * into bearing TextViews by this class
*/
class InstrumentHandler(
- private val valueAws: TextView,
- private val valueTws: TextView,
- private val valueHdg: TextView,
- private val valueCog: TextView,
- private val valueBsp: TextView,
- private val valueSog: TextView,
+ // ── Instrument section TextViews ─────────────────────────────────
+ private val valueAws: TextView,
+ private val valueTws: TextView,
+ private val valueHdg: TextView,
+ private val valueCog: TextView,
+ private val valueBsp: TextView,
+ private val valueSog: TextView,
private val valueDepth: TextView,
- private val valueBaro: TextView,
- private val valueCurrSpd: TextView,
- private val valueCurrDir: TextView?,
- private val valueWaveHt: TextView,
- private val valueWaveDir: TextView?,
- private val valueSwellHt: TextView,
- private val valueSwellPer: TextView
+ private val valueBaro: TextView,
+ // ── Instrument section DirectionArrowViews ───────────────────────
+ private val arrowAws: DirectionArrowView,
+ private val arrowTws: DirectionArrowView,
+ private val arrowHdg: DirectionArrowView,
+ private val arrowCog: DirectionArrowView,
+ // ── Forecast section TextViews ───────────────────────────────────
+ private val valueCurrSpd: TextView,
+ private val valueWaveHt: TextView,
+ private val valueSwellHt: TextView,
+ private val valueSwellPer: TextView,
+ // ── Forecast section DirectionArrowViews ─────────────────────────
+ private val arrowCurr: DirectionArrowView,
+ private val arrowWaves: DirectionArrowView,
+ private val arrowSwell: DirectionArrowView,
+ // ── Forecast section bearing TextViews ───────────────────────────
+ private val bearingCurr: TextView,
+ private val bearingWaves: TextView,
+ private val bearingSwell: TextView,
+ // ── Wave view ────────────────────────────────────────────────────
+ private val waveView: WaveView
) {
+ init {
+ arrowCurr.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
+ arrowWaves.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
+ arrowSwell.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
+ }
+
/**
- * Updates the text displays for the given instruments. Null arguments leave the current value unchanged.
+ * Updates instrument-section text and arrows.
+ * Null arguments leave the current value unchanged.
+ * [depthM] is raw metres — converted to feet internally.
*/
fun updateDisplay(
- aws: String? = null,
- tws: String? = null,
- hdg: String? = null,
- cog: String? = null,
- bsp: String? = null,
- sog: String? = null,
- depth: String? = null,
- baro: String? = null
+ aws: String? = null, awsBearingDeg: Float? = null,
+ tws: String? = null, twsBearingDeg: Float? = null,
+ hdg: String? = null, hdgBearingDeg: Float? = null,
+ cog: String? = null, cogBearingDeg: Float? = null,
+ bsp: String? = null,
+ sog: String? = null,
+ depthM: Double? = null,
+ baro: String? = null
) {
- aws?.let { valueAws.text = it }
- tws?.let { valueTws.text = it }
- hdg?.let { valueHdg.text = it }
- cog?.let { valueCog.text = it }
- bsp?.let { valueBsp.text = it }
- sog?.let { valueSog.text = it }
- depth?.let { valueDepth.text = it }
- baro?.let { valueBaro.text = it }
+ aws?.let { valueAws.text = it }
+ tws?.let { valueTws.text = it }
+ hdg?.let { valueHdg.text = it }
+ cog?.let { valueCog.text = it }
+ bsp?.let { valueBsp.text = it }
+ sog?.let { valueSog.text = it }
+ baro?.let { valueBaro.text = it }
+ depthM?.let { valueDepth.text = formatFt(metresToFeet(it)) }
+
+ awsBearingDeg?.let { arrowAws.bearing = it }
+ twsBearingDeg?.let { arrowTws.bearing = it }
+ hdgBearingDeg?.let { arrowHdg.bearing = it }
+ cogBearingDeg?.let { arrowCog.bearing = it }
}
/**
- * Updates the forecast conditions section (Curr, Wave, Swell).
- * Null arguments leave the current value unchanged.
+ * Updates the forecast section.
+ * [waveHeightM] and [swellHeightM] are raw metres — converted to feet internally.
*/
fun updateConditions(
- currSpd: String? = null,
- currDir: String? = null,
- waveHt: String? = null,
- waveDir: String? = null,
- swellHt: String? = null,
- swellPer: String? = null
+ currSpd: String? = null,
+ currDirDeg: Float? = null,
+ waveHeightM: Double? = null,
+ waveDirDeg: Float? = null,
+ swellHeightM: Double? = null,
+ swellDirDeg: Float? = null,
+ swellPeriodS: Double? = null
) {
currSpd?.let { valueCurrSpd.text = it }
- currDir?.let { valueCurrDir?.text = it }
- waveHt?.let { valueWaveHt.text = it }
- waveDir?.let { valueWaveDir?.text = it }
- swellHt?.let { valueSwellHt.text = it }
- swellPer?.let { valueSwellPer.text = it }
+ currDirDeg?.let {
+ arrowCurr.bearing = it
+ bearingCurr.text = formatBearing(it.toDouble())
+ }
+ waveHeightM?.let { valueWaveHt.text = formatFt(metresToFeet(it)) }
+ waveDirDeg?.let {
+ arrowWaves.bearing = it
+ bearingWaves.text = formatBearing(it.toDouble())
+ }
+ swellHeightM?.let { valueSwellHt.text = formatFt(metresToFeet(it)) }
+ swellDirDeg?.let {
+ arrowSwell.bearing = it
+ bearingSwell.text = formatBearing(it.toDouble())
+ }
+ swellPeriodS?.let { valueSwellPer.text = formatPeriod(it) }
+ }
+
+ /**
+ * Updates the WaveView with current sea state. Call whenever new forecast data arrives.
+ * All inputs in feet.
+ */
+ fun updateWaveState(
+ swellHeightFt: Float,
+ swellPeriodSec: Float,
+ windWaveHeightFt: Float
+ ) {
+ waveView.swellHeightFt = swellHeightFt
+ waveView.swellPeriodSec = swellPeriodSec
+ waveView.windWaveHeightFt = windWaveHeightFt
}
}