summaryrefslogtreecommitdiff
path: root/android-app
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-04-10 09:30:55 +0000
committerClaude <noreply@anthropic.com>2026-04-10 09:30:55 +0000
commit4daf9dfcd00844075768e5b0d1dd7a17002a26d0 (patch)
treecf1f01bfd64ddc8fc06a469e126df7b4ab1f9e39 /android-app
parent12c6193c6d4f666425962e0bd1804358570465f6 (diff)
Area conditions HUD redesign: map-center conditions refresh + boat HUD strip
- Persistent top-of-map HUD strip (SOG/COG/BSP/Depth) replaces instrument grid in bottom sheet - Bottom sheet now shows area conditions only (Wind/Temp/Baro + wave forecast) — always visible - loadConditions() fires on every camera idle event so panning the map refreshes conditions - Crosshair appears at map center while panning; hides when following GPS - Added tempC field to MarineConditions (already fetched from Open-Meteo hourly) - InstrumentHandler slimmed to area conditions only; updateDisplay() removed - LocationService pipes nmeaBoatSpeedData from NmeaStreamManager to companion flow https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
Diffstat (limited to 'android-app')
-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
-rw-r--r--android-app/app/src/main/res/drawable/ic_crosshair.xml28
-rw-r--r--android-app/app/src/main/res/layout/activity_main.xml24
-rw-r--r--android-app/app/src/main/res/layout/layout_instruments_sheet.xml187
-rw-r--r--android-app/app/src/main/res/layout/layout_nav_hud.xml161
9 files changed, 329 insertions, 256 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
diff --git a/android-app/app/src/main/res/drawable/ic_crosshair.xml b/android-app/app/src/main/res/drawable/ic_crosshair.xml
new file mode 100644
index 0000000..609538e
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_crosshair.xml
@@ -0,0 +1,28 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32dp"
+ android:height="32dp"
+ android:viewportWidth="32"
+ android:viewportHeight="32">
+
+ <!-- Horizontal line -->
+ <path
+ android:pathData="M2,16 L13,16 M19,16 L30,16"
+ android:strokeColor="#CCFFFFFF"
+ android:strokeWidth="1.5"
+ android:strokeLineCap="round" />
+
+ <!-- Vertical line -->
+ <path
+ android:pathData="M16,2 L16,13 M16,19 L16,30"
+ android:strokeColor="#CCFFFFFF"
+ android:strokeWidth="1.5"
+ android:strokeLineCap="round" />
+
+ <!-- Center circle -->
+ <path
+ android:pathData="M16,16 m-2.5,0 a2.5,2.5 0 1,0 5,0 a2.5,2.5 0 1,0 -5,0"
+ android:strokeColor="#CCFFFFFF"
+ android:strokeWidth="1.5"
+ android:fillColor="#00000000" />
+
+</vector>
diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml
index 1741c62..5147506 100644
--- a/android-app/app/src/main/res/layout/activity_main.xml
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -29,6 +29,30 @@
android:clickable="false"
android:focusable="false" />
+ <!-- Boat-instrument HUD strip — always on top of map -->
+ <include
+ android:id="@+id/nav_hud"
+ layout="@layout/layout_nav_hud"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <!-- Crosshair — visible when not following (panning) -->
+ <ImageView
+ android:id="@+id/map_crosshair"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:src="@drawable/ic_crosshair"
+ android:visibility="gone"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
<!-- Overlay Fragment Container (for Log, Safety, Help) -->
<FrameLayout
android:id="@+id/fragment_container"
diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
index 33a7bd9..faec826 100644
--- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
@@ -24,113 +24,28 @@
app:layout_constraintEnd_toEndOf="parent" />
<!--
- 3×2 grid: AWS/HDG/BSP top row, TWS/COG/SOG bottom row.
- Cells with direction: AWS, TWS (kts + unit + arrow inline)
- HDG, COG (°-in-value + arrow inline)
- Cells without: BSP, SOG (kts + unit, no arrow)
+ Area-conditions header row: Wind (kts + arrow) | Temp (°C) | Baro (hPa)
+ Always shows conditions for the current map center.
-->
- <androidx.gridlayout.widget.GridLayout
- android:id="@+id/instrument_grid"
+ <LinearLayout
+ android:id="@+id/conditions_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:orientation="horizontal"
android:layout_marginTop="12dp"
- app:columnCount="3"
- app:rowCount="2"
app:layout_constraintTop_toBottomOf="@id/drag_handle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
- <!-- AWS -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_columnWeight="1"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="AWS" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <TextView
- android:id="@+id/value_aws"
- style="@style/InstrumentPrimaryValue"
- tools:text="18.2" />
- <TextView style="@style/InstrumentUnit" android:text="kts" />
- <org.terst.nav.ui.DirectionArrowView
- android:id="@+id/arrow_aws"
- android:layout_width="14dp"
- android:layout_height="14dp"
- android:layout_marginStart="3dp"
- android:layout_marginBottom="3dp"
- android:layout_gravity="bottom" />
- </LinearLayout>
- </LinearLayout>
-
- <!-- HDG: ° is part of the value string -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_columnWeight="1"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="HDG" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <TextView
- android:id="@+id/value_hdg"
- style="@style/InstrumentPrimaryValue"
- tools:text="247°" />
- <org.terst.nav.ui.DirectionArrowView
- android:id="@+id/arrow_hdg"
- android:layout_width="14dp"
- android:layout_height="14dp"
- android:layout_marginStart="3dp"
- android:layout_marginBottom="3dp"
- android:layout_gravity="bottom" />
- </LinearLayout>
- </LinearLayout>
-
- <!-- BSP: no direction -->
+ <!-- Wind -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
- app:layout_columnWeight="1"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="BSP" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <TextView
- android:id="@+id/value_bsp"
- style="@style/InstrumentPrimaryValue"
- tools:text="6.8" />
- <TextView style="@style/InstrumentUnit" android:text="kts" />
- </LinearLayout>
- </LinearLayout>
-
- <!-- TWS -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_columnWeight="1"
+ android:layout_weight="1"
android:orientation="vertical"
android:gravity="center"
android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="TWS" />
+ <TextView style="@style/InstrumentLabel" android:text="Wind" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -150,73 +65,18 @@
android:layout_marginBottom="3dp"
android:layout_gravity="bottom" />
</LinearLayout>
+ <TextView
+ android:id="@+id/bearing_tws"
+ style="@style/InstrumentLabel"
+ tools:text="270°" />
</LinearLayout>
- <!-- COG: ° is part of the value string -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_columnWeight="1"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="COG" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <TextView
- android:id="@+id/value_cog"
- style="@style/InstrumentPrimaryValue"
- tools:text="253°" />
- <org.terst.nav.ui.DirectionArrowView
- android:id="@+id/arrow_cog"
- android:layout_width="14dp"
- android:layout_height="14dp"
- android:layout_marginStart="3dp"
- android:layout_marginBottom="3dp"
- android:layout_gravity="bottom" />
- </LinearLayout>
- </LinearLayout>
-
- <!-- SOG: no direction -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_columnWeight="1"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="SOG" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <TextView
- android:id="@+id/value_sog"
- style="@style/InstrumentPrimaryValue"
- tools:text="7.1" />
- <TextView style="@style/InstrumentUnit" android:text="kts" />
- </LinearLayout>
- </LinearLayout>
-
- </androidx.gridlayout.widget.GridLayout>
-
- <!-- Depth + Baro side by side -->
- <LinearLayout
- android:id="@+id/expanded_instruments"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layout_marginTop="4dp"
- app:layout_constraintTop_toBottomOf="@id/instrument_grid"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent">
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="@color/md_theme_outline" />
+ <!-- Temp -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -224,7 +84,7 @@
android:orientation="vertical"
android:gravity="center"
android:padding="8dp">
- <TextView style="@style/InstrumentLabel" android:text="Depth" />
+ <TextView style="@style/InstrumentLabel" android:text="Temp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -232,18 +92,19 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
- android:id="@+id/value_depth"
+ android:id="@+id/value_temp"
style="@style/InstrumentPrimaryValue"
- tools:text="42.0" />
- <TextView style="@style/InstrumentUnit" android:text="ft" />
+ tools:text="18" />
+ <TextView style="@style/InstrumentUnit" android:text="°C" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
- android:background="#2B2930" />
+ android:background="@color/md_theme_outline" />
+ <!-- Baro -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -275,7 +136,7 @@
android:layout_height="72dp"
android:layout_marginStart="-16dp"
android:layout_marginEnd="-16dp"
- app:layout_constraintTop_toBottomOf="@id/expanded_instruments"
+ app:layout_constraintTop_toBottomOf="@id/conditions_header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/android-app/app/src/main/res/layout/layout_nav_hud.xml b/android-app/app/src/main/res/layout/layout_nav_hud.xml
new file mode 100644
index 0000000..4623bb1
--- /dev/null
+++ b/android-app/app/src/main/res/layout/layout_nav_hud.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Persistent boat-instrument HUD strip.
+ Lives at the top of the map; always visible; shows dashes when NMEA unavailable.
+ Fields: SOG | COG | BSP | Depth
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:orientation="horizontal"
+ android:background="#CC000000">
+
+ <!-- SOG -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SOG"
+ android:textSize="9sp"
+ android:textColor="#99FFFFFF"
+ android:letterSpacing="0.08" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/hud_sog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="#FFFFFF"
+ android:fontFamily="sans-serif-medium"
+ tools:text="7.1" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="kt"
+ android:textSize="10sp"
+ android:textColor="#99FFFFFF"
+ android:layout_marginStart="2dp" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <View android:layout_width="1dp" android:layout_height="match_parent" android:background="#33FFFFFF" />
+
+ <!-- COG -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="COG"
+ android:textSize="9sp"
+ android:textColor="#99FFFFFF"
+ android:letterSpacing="0.08" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/hud_cog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="#FFFFFF"
+ android:fontFamily="sans-serif-medium"
+ tools:text="253°" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <View android:layout_width="1dp" android:layout_height="match_parent" android:background="#33FFFFFF" />
+
+ <!-- BSP -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="BSP"
+ android:textSize="9sp"
+ android:textColor="#99FFFFFF"
+ android:letterSpacing="0.08" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/hud_bsp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="#FFFFFF"
+ android:fontFamily="sans-serif-medium"
+ tools:text="6.8" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="kt"
+ android:textSize="10sp"
+ android:textColor="#99FFFFFF"
+ android:layout_marginStart="2dp" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <View android:layout_width="1dp" android:layout_height="match_parent" android:background="#33FFFFFF" />
+
+ <!-- Depth -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Depth"
+ android:textSize="9sp"
+ android:textColor="#99FFFFFF"
+ android:letterSpacing="0.08" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/hud_depth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textColor="#FFFFFF"
+ android:fontFamily="sans-serif-medium"
+ tools:text="42.0" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="ft"
+ android:textSize="10sp"
+ android:textColor="#99FFFFFF"
+ android:layout_marginStart="2dp" />
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout>