summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt11
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt86
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt1
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt1
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt86
5 files changed, 92 insertions, 93 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
index b18db8d..41fb2ec 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.asStateFlow
import org.terst.nav.nmea.NmeaParser
import org.terst.nav.nmea.NmeaStreamManager
+import org.terst.nav.sensors.BoatSpeedData
import org.terst.nav.sensors.DepthData
import org.terst.nav.sensors.HeadingData
import org.terst.nav.sensors.WindData
@@ -128,6 +129,13 @@ class LocationService : Service() {
}
}
+ // Collect NMEA Boat Speed Data
+ serviceScope.launch {
+ nmeaStreamManager.nmeaBoatSpeedData.collectLatest { bspData ->
+ _nmeaBoatSpeedDataFlow.emit(bspData)
+ }
+ }
+
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
@@ -393,6 +401,9 @@ class LocationService : Service() {
private val _nmeaHeadingDataFlow = MutableSharedFlow<HeadingData>(replay = 1)
val nmeaHeadingDataFlow: SharedFlow<HeadingData> get() = _nmeaHeadingDataFlow
+ private val _nmeaBoatSpeedDataFlow = MutableSharedFlow<BoatSpeedData>(replay = 1)
+ val nmeaBoatSpeedData: SharedFlow<BoatSpeedData> get() = _nmeaBoatSpeedDataFlow
+
private val _currentPowerMode = MutableStateFlow(PowerMode.FULL)
val currentPowerMode: StateFlow<PowerMode> get() = _currentPowerMode
}
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 6d02014..21aa55b 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
@@ -11,6 +11,7 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.View
import android.widget.FrameLayout
+import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -57,6 +58,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private lateinit var btnQuit: MaterialButton
private lateinit var bottomSheet: CardView
private lateinit var bottomNav: BottomNavigationView
+ private lateinit var mapCrosshair: View
+
+ // HUD TextViews — wired to layout_nav_hud.xml
+ private lateinit var hudSog: TextView
+ private lateinit var hudCog: TextView
+ private lateinit var hudBsp: TextView
+ private lateinit var hudDepth: TextView
+
private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) }
private val viewModel: MainViewModel by viewModels()
@@ -88,6 +97,19 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
btnQuit = findViewById(R.id.btn_quit)
bottomSheet = findViewById(R.id.instrument_bottom_sheet)
bottomNav = findViewById(R.id.bottom_navigation)
+ mapCrosshair = findViewById(R.id.map_crosshair)
+
+ // HUD views (inside the included layout_nav_hud)
+ hudSog = findViewById(R.id.hud_sog)
+ hudCog = findViewById(R.id.hud_cog)
+ hudBsp = findViewById(R.id.hud_bsp)
+ hudDepth = findViewById(R.id.hud_depth)
+
+ // Initialise HUD with dashes
+ hudSog.text = "—"
+ hudCog.text = "—"
+ hudBsp.text = "—"
+ hudDepth.text = "—"
setupBottomSheet()
setupBottomNavigation()
@@ -184,12 +206,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
fragmentContainer.visibility = View.GONE
}
- private fun showReport(fragment: androidx.fragment.app.Fragment) {
- bottomSheetBehavior.isHideable = true
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
- showOverlay(fragment)
- }
-
private fun onQuitRequested() {
if (viewModel.isRecording.value) {
androidx.appcompat.app.AlertDialog.Builder(this)
@@ -222,18 +238,11 @@ 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),
- valueDepth = findViewById(R.id.value_depth),
- valueBaro = findViewById(R.id.value_baro),
- arrowAws = findViewById(R.id.arrow_aws),
- arrowTws = findViewById(R.id.arrow_tws),
- arrowHdg = findViewById(R.id.arrow_hdg),
- arrowCog = findViewById(R.id.arrow_cog),
+ valueTws = findViewById(R.id.value_tws),
+ arrowTws = findViewById(R.id.arrow_tws),
+ bearingTws = findViewById(R.id.bearing_tws),
+ valueTemp = findViewById(R.id.value_temp),
+ valueBaro = findViewById(R.id.value_baro),
valueCurrSpd = findViewById(R.id.value_curr_spd),
valueWaveHt = findViewById(R.id.value_wave_ht),
valueSwellHt = findViewById(R.id.value_swell_ht),
@@ -246,12 +255,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
bearingSwell = findViewById(R.id.bearing_swell),
waveView = findViewById(R.id.wave_divider)
)
- instrumentHandler?.updateDisplay(
- aws = "—", tws = "—", hdg = "—",
- cog = "—", bsp = "—", sog = "—",
- baro = "—"
+ instrumentHandler?.updateConditions(
+ tws = "—", baro = "—", currSpd = "—"
)
- instrumentHandler?.updateConditions(currSpd = "—")
}
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
@@ -302,11 +308,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
lifecycleScope.launch {
mapHandler!!.isFollowing.collect { following ->
+ mapCrosshair.visibility = if (following) View.GONE else View.VISIBLE
if (following) {
fadeOut(fabRecenter, gone = true)
- fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack)
+ fadeIn(bottomNav, fabMob, fabRecordTrack)
} else {
- fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true)
+ fadeOut(bottomNav, fabMob, fabRecordTrack, gone = true)
fadeIn(fabRecenter)
}
}
@@ -339,6 +346,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
corners.minOf { it.longitude }, corners.maxOf { it.longitude }
)
}
+ // Refresh conditions for current map center (works in both follow + pan mode)
+ maplibreMap.cameraPosition.target?.let { center ->
+ viewModel.loadConditions(center.latitude, center.longitude)
+ }
}
maplibreMap.addOnMapLongClickListener { _ ->
@@ -360,11 +371,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
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),
- cogBearingDeg = gpsData.cog.toFloat()
- )
+ // HUD — SOG and COG come from GPS
+ hudSog.text = "%.1f".format(Locale.getDefault(), gpsData.sog)
+ hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog)
if (!conditionsLoaded) {
conditionsLoaded = true
viewModel.loadConditions(gpsData.latitude, gpsData.longitude)
@@ -391,18 +400,17 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
lifecycleScope.launch {
LocationService.barometerStatus.collect { status ->
if (status.history.isNotEmpty()) {
- instrumentHandler?.updateDisplay(baro = status.formatPressure())
+ instrumentHandler?.updateConditions(baro = status.formatPressure())
}
}
}
lifecycleScope.launch {
viewModel.marineConditions.collect { c ->
if (c == null) return@collect
- instrumentHandler?.updateDisplay(
- tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) },
- twsBearingDeg = c.windDirDeg?.toFloat()
- )
instrumentHandler?.updateConditions(
+ tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) },
+ twsBearingDeg = c.windDirDeg?.toFloat(),
+ tempC = c.tempC,
currSpd = c.currentSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—",
currDirDeg = c.currentDirDeg?.toFloat(),
waveHeightM = c.waveHeightM,
@@ -420,7 +428,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
lifecycleScope.launch {
LocationService.nmeaDepthDataFlow.collect { depthData ->
- instrumentHandler?.updateDisplay(depthM = depthData.depthMeters)
+ // Update HUD depth (converted to feet)
+ hudDepth.text = "%.1f".format(Locale.getDefault(), depthData.depthMeters * 3.28084)
+ }
+ }
+ lifecycleScope.launch {
+ LocationService.nmeaBoatSpeedData.collect { bspData ->
+ hudBsp.text = "%.1f".format(Locale.getDefault(), bspData.bspKnots)
}
}
lifecycleScope.launch {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt
index 3cde023..557d6a2 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt
@@ -7,6 +7,7 @@ package org.terst.nav.data.model
data class MarineConditions(
val windSpeedKt: Double?,
val windDirDeg: Double?,
+ val tempC: Double?,
val waveHeightM: Double?,
val waveDirDeg: Double?,
val swellHeightM: Double?,
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
index c79366d..dc47a20 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
@@ -59,6 +59,7 @@ class WeatherRepository(
MarineConditions(
windSpeedKt = w.windspeed10m.firstOrNull(),
windDirDeg = w.winddirection10m.firstOrNull(),
+ tempC = w.temperature2m.firstOrNull(),
waveHeightM = m.waveHeight.firstOrNull(),
waveDirDeg = m.waveDirection.firstOrNull(),
swellHeightM = m.swellWaveHeight.firstOrNull(),
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 cb59a3a..48ebb3b 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
@@ -25,10 +25,11 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String =
// ── InstrumentHandler ────────────────────────────────────────────────────────
/**
- * Drives all text fields, direction arrows, and the wave view in the
- * instrument bottom sheet.
+ * Drives the area-conditions bottom sheet: wind header row, wave view,
+ * and the forecast section (current / waves / swell).
*
- * Forecast [DirectionArrowView] instances are initialised with OCEAN style.
+ * All boat-instrument data (SOG, COG, BSP, Depth) is handled directly
+ * in MainActivity via the HUD strip.
*
* Units contract:
* - Speed values: pre-formatted strings in knots (caller's responsibility)
@@ -37,34 +38,26 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String =
* into bearing TextViews by this class
*/
class InstrumentHandler(
- // ── 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,
- // ── Instrument section DirectionArrowViews ───────────────────────
- private val arrowAws: DirectionArrowView,
- private val arrowTws: DirectionArrowView,
- private val arrowHdg: DirectionArrowView,
- private val arrowCog: DirectionArrowView,
- // ── Forecast section TextViews ───────────────────────────────────
+ // ── Conditions header ────────────────────────────────────────────────
+ private val valueTws: TextView,
+ private val arrowTws: DirectionArrowView,
+ private val bearingTws: TextView,
+ private val valueTemp: TextView,
+ private val valueBaro: TextView,
+ // ── Forecast section TextViews ───────────────────────────────────────
private val valueCurrSpd: TextView,
private val valueWaveHt: TextView,
private val valueSwellHt: TextView,
private val valueSwellPer: TextView,
- // ── Forecast section DirectionArrowViews ─────────────────────────
+ // ── Forecast section DirectionArrowViews ─────────────────────────────
private val arrowCurr: DirectionArrowView,
private val arrowWaves: DirectionArrowView,
private val arrowSwell: DirectionArrowView,
- // ── Forecast section bearing TextViews ───────────────────────────
+ // ── Forecast section bearing TextViews ───────────────────────────────
private val bearingCurr: TextView,
private val bearingWaves: TextView,
private val bearingSwell: TextView,
- // ── Wave view ────────────────────────────────────────────────────
+ // ── Wave view ────────────────────────────────────────────────────────
private val waveView: WaveView
) {
init {
@@ -74,48 +67,27 @@ class InstrumentHandler(
}
/**
- * Updates instrument-section text and arrows.
+ * Updates all area-conditions fields.
* Null arguments leave the current value unchanged.
- * [depthM] is raw metres — converted to feet internally.
- */
- fun updateDisplay(
- 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 }
- 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 section.
* [waveHeightM] and [swellHeightM] are raw metres — converted to feet internally.
*/
fun updateConditions(
- currSpd: String? = null,
- currDirDeg: Float? = null,
- waveHeightM: Double? = null,
- waveDirDeg: Float? = null,
- swellHeightM: Double? = null,
- swellDirDeg: Float? = null,
+ tws: String? = null, twsBearingDeg: Float? = null,
+ tempC: Double? = null,
+ baro: String? = null,
+ currSpd: String? = null, currDirDeg: Float? = null,
+ waveHeightM: Double? = null, waveDirDeg: Float? = null,
+ swellHeightM: Double? = null, swellDirDeg: Float? = null,
swellPeriodS: Double? = null
) {
+ tws?.let { valueTws.text = it }
+ baro?.let { valueBaro.text = it }
+ tempC?.let { valueTemp.text = "%.0f".format(Locale.getDefault(), it) }
+ twsBearingDeg?.let {
+ arrowTws.bearing = it
+ bearingTws.text = formatBearing(it.toDouble())
+ }
+
currSpd?.let { valueCurrSpd.text = it }
currDirDeg?.let {
arrowCurr.bearing = it