summaryrefslogtreecommitdiff
path: root/android-app/app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt190
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt83
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt79
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt95
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt14
-rw-r--r--android-app/app/src/main/res/layout/activity_main.xml4
-rw-r--r--android-app/app/src/main/res/layout/layout_instruments_sheet.xml12
-rw-r--r--android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml352
-rw-r--r--android-app/app/src/main/res/layout/layout_nav_hud.xml3
9 files changed, 643 insertions, 189 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 21aa55b..86dd531 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
@@ -37,6 +37,7 @@ import org.terst.nav.ui.map.ParticleWindView
import org.terst.nav.ui.safety.SafetyFragment
import org.terst.nav.ui.voicelog.VoiceLogFragment
import java.util.*
+import org.terst.nav.data.model.MarineConditions
import org.terst.nav.safety.AnchorWatchState
class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
@@ -60,12 +61,35 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private lateinit var bottomNav: BottomNavigationView
private lateinit var mapCrosshair: View
- // HUD TextViews — wired to layout_nav_hud.xml
+ // HUD value TextViews
private lateinit var hudSog: TextView
private lateinit var hudCog: TextView
private lateinit var hudBsp: TextView
private lateinit var hudDepth: TextView
+ // HUD unit label TextViews
+ private lateinit var unitHudSog: TextView
+ private lateinit var unitHudBsp: TextView
+ private lateinit var unitHudDepth: TextView
+
+ // Instrument sheet unit label TextViews
+ private lateinit var unitTws: TextView
+ private lateinit var unitTemp: TextView
+ private lateinit var unitBaro: TextView
+ private lateinit var unitCurrSpd: TextView
+ private lateinit var unitWaveHt: TextView
+ private lateinit var unitSwellHt: TextView
+
+ // Unit preferences
+ private lateinit var unitPrefs: UnitPrefs
+
+ // Cached raw values for unit-change refresh
+ private var lastConditions: MarineConditions? = null
+ private var lastBaroHpa: Float? = null
+ private var lastSogKnots: Double? = null
+ private var lastBspKnots: Double? = null
+ private var lastDepthMeters: Double? = null
+
private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) }
private val viewModel: MainViewModel by viewModels()
@@ -85,6 +109,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
MapLibre.getInstance(this)
setContentView(R.layout.activity_main)
+ unitPrefs = UnitPrefs(this)
checkForegroundPermissions()
initializeUI()
}
@@ -99,24 +124,37 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
bottomNav = findViewById(R.id.bottom_navigation)
mapCrosshair = findViewById(R.id.map_crosshair)
- // HUD views (inside the included layout_nav_hud)
+ // HUD value views
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
+ // HUD unit label views
+ unitHudSog = findViewById(R.id.unit_hud_sog)
+ unitHudBsp = findViewById(R.id.unit_hud_bsp)
+ unitHudDepth = findViewById(R.id.unit_hud_depth)
+
+ // Instrument sheet unit label views
+ unitTws = findViewById(R.id.unit_tws)
+ unitTemp = findViewById(R.id.unit_temp)
+ unitBaro = findViewById(R.id.unit_baro)
+ unitCurrSpd = findViewById(R.id.unit_curr_spd)
+ unitWaveHt = findViewById(R.id.unit_wave_ht)
+ unitSwellHt = findViewById(R.id.unit_swell_ht)
+
+ // Init HUD with dashes; apply correct unit labels from stored prefs
hudSog.text = "—"
hudCog.text = "—"
hudBsp.text = "—"
hudDepth.text = "—"
+ applyUnitLabels()
setupBottomSheet()
setupBottomNavigation()
setupHandlers()
setupMap()
- // Single tap starts; long press stops (requires deliberate intent mid-sail)
fabRecordTrack.setOnClickListener {
if (!viewModel.isRecording.value) viewModel.startTrack()
}
@@ -129,12 +167,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
fabMob.setOnClickListener { onActivateMob() }
-
- fabRecenter.setOnClickListener {
- mapHandler?.recenter()
- }
-
+ fabRecenter.setOnClickListener { mapHandler?.recenter() }
btnQuit.setOnClickListener { onQuitRequested() }
+
lifecycleScope.launch {
viewModel.isRecording.collect { recording ->
val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record
@@ -151,11 +186,73 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
}
+ /** Updates all static unit label TextViews to match current [unitPrefs]. */
+ private fun applyUnitLabels() {
+ unitHudSog.text = unitPrefs.speedUnitLabel()
+ unitHudBsp.text = unitPrefs.speedUnitLabel()
+ unitHudDepth.text = unitPrefs.depthUnitLabel()
+ unitTws.text = unitPrefs.speedUnitLabel()
+ unitTemp.text = unitPrefs.tempUnitLabel()
+ unitBaro.text = unitPrefs.pressureUnitLabel()
+ unitCurrSpd.text = unitPrefs.speedUnitLabel()
+ unitWaveHt.text = unitPrefs.depthUnitLabel()
+ unitSwellHt.text = unitPrefs.depthUnitLabel()
+ }
+
+ /**
+ * Re-formats every cached raw value with the current unit prefs.
+ * Called after the user changes a unit in the settings sheet.
+ */
+ private fun refreshUnits() {
+ applyUnitLabels()
+ lastSogKnots?.let { hudSog.text = unitPrefs.formatSpeed(it) }
+ lastBspKnots?.let { hudBsp.text = unitPrefs.formatSpeed(it) }
+ lastDepthMeters?.let { hudDepth.text = unitPrefs.formatDepth(it) }
+ lastConditions?.let { applyConditions(it) }
+ lastBaroHpa?.let { instrumentHandler?.updateConditions(baroHpa = it) }
+ }
+
+ private fun applyConditions(c: MarineConditions) {
+ instrumentHandler?.updateConditions(
+ twsKt = c.windSpeedKt,
+ twsBearingDeg = c.windDirDeg?.toFloat(),
+ tempC = c.tempC,
+ currSpdKt = c.currentSpeedKt,
+ currDirDeg = c.currentDirDeg?.toFloat(),
+ waveHeightM = c.waveHeightM,
+ waveDirDeg = c.waveDirDeg?.toFloat(),
+ swellHeightM = c.swellHeightM,
+ swellDirDeg = c.swellDirDeg?.toFloat(),
+ swellPeriodS = c.swellPeriodS
+ )
+ instrumentHandler?.updateWaveState(
+ swellHeightM = c.swellHeightM?.toFloat() ?: 0.9f,
+ swellPeriodSec = c.swellPeriodS?.toFloat() ?: 10f,
+ windWaveHeightM = c.waveHeightM?.toFloat() ?: 0.45f,
+ windSpeedKt = c.windSpeedKt?.toFloat() ?: 0f
+ )
+ }
+
private fun setupBottomSheet() {
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
+ private fun buildLayerPickerSheet(): LayerPickerSheet {
+ val currentStyle = loadedStyleFlow.value
+ return LayerPickerSheet(
+ manager = layerManager,
+ onBaseChanged = { preset -> currentStyle?.let { layerManager.setBasePreset(it, preset) } },
+ onWindChanged = { enabled -> currentStyle?.let { layerManager.setWindEnabled(it, enabled) } },
+ onParticlesChanged = { enabled ->
+ layerManager.setParticlesEnabled(enabled)
+ particleWindView?.visibility = if (enabled) View.VISIBLE else View.GONE
+ },
+ unitPrefs = unitPrefs,
+ onUnitsChanged = { refreshUnits() }
+ )
+ }
+
private fun setupBottomNavigation() {
bottomNav.setOnItemSelectedListener { item ->
when (item.itemId) {
@@ -167,14 +264,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
true
}
R.id.nav_layers -> {
- val currentStyle = loadedStyleFlow.value
- if (currentStyle != null) {
- LayerPickerSheet(
- manager = layerManager,
- onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) },
- onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) }
- ).show(supportFragmentManager, "layer_picker")
- }
+ buildLayerPickerSheet().show(supportFragmentManager, "layer_picker")
bottomNav.post { bottomNav.selectedItemId = R.id.nav_map }
true
}
@@ -253,20 +343,18 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
bearingCurr = findViewById(R.id.bearing_curr),
bearingWaves = findViewById(R.id.bearing_waves),
bearingSwell = findViewById(R.id.bearing_swell),
- waveView = findViewById(R.id.wave_divider)
- )
- instrumentHandler?.updateConditions(
- tws = "—", baro = "—", currSpd = "—"
+ waveView = findViewById(R.id.wave_divider),
+ unitPrefs = unitPrefs
)
}
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
private fun checkForegroundPermissions() {
- val fineLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
- val coarseLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ val fineLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ val coarseLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
- if (fineLocationPermission == PackageManager.PERMISSION_GRANTED || coarseLocationPermission == PackageManager.PERMISSION_GRANTED) {
+ if (fineLocation == PackageManager.PERMISSION_GRANTED || coarseLocation == PackageManager.PERMISSION_GRANTED) {
startServices()
} else {
requestPermissionLauncher.launch(arrayOf(
@@ -299,6 +387,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
layerManager = MapLayerManager(this)
mapView = findViewById(R.id.mapView)
particleWindView = findViewById(R.id.particle_wind_view)
+
+ // Apply persisted particles preference immediately
+ if (!layerManager.particlesEnabled) {
+ particleWindView?.visibility = View.GONE
+ }
+
if (NavApplication.isTesting) return
mapView?.onCreate(null)
@@ -325,9 +419,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
maplibreMap.setStyle(styleBuilder) { style ->
loadedStyleFlow.value = style
- val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor)
- val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow)
- val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow)
+ val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor)
+ val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow)
+ val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow)
val windArrowBitmap = rasterizeDrawable(R.drawable.ic_wind_arrow)
mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap)
mapHandler?.setupWindLayer(style, windArrowBitmap)
@@ -346,19 +440,13 @@ 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 { _ ->
- val currentStyle = loadedStyleFlow.value ?: return@addOnMapLongClickListener true
- LayerPickerSheet(
- manager = layerManager,
- onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) },
- onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) }
- ).show(supportFragmentManager, "layer_picker")
+ buildLayerPickerSheet().show(supportFragmentManager, "layer_picker")
true
}
}
@@ -371,8 +459,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)
- // HUD — SOG and COG come from GPS
- hudSog.text = "%.1f".format(Locale.getDefault(), gpsData.sog)
+ // HUD — SOG and COG from GPS
+ lastSogKnots = gpsData.sog
+ hudSog.text = unitPrefs.formatSpeed(gpsData.sog)
hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog)
if (!conditionsLoaded) {
conditionsLoaded = true
@@ -400,41 +489,28 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
lifecycleScope.launch {
LocationService.barometerStatus.collect { status ->
if (status.history.isNotEmpty()) {
- instrumentHandler?.updateConditions(baro = status.formatPressure())
+ lastBaroHpa = status.currentPressureHpa
+ instrumentHandler?.updateConditions(baroHpa = status.currentPressureHpa)
}
}
}
lifecycleScope.launch {
viewModel.marineConditions.collect { c ->
if (c == null) return@collect
- 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,
- 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
- val windKt = c.windSpeedKt?.toFloat() ?: 0f
- instrumentHandler?.updateWaveState(swellHtFt, swellPeriod, windHtFt, windKt)
+ lastConditions = c
+ applyConditions(c)
}
}
lifecycleScope.launch {
LocationService.nmeaDepthDataFlow.collect { depthData ->
- // Update HUD depth (converted to feet)
- hudDepth.text = "%.1f".format(Locale.getDefault(), depthData.depthMeters * 3.28084)
+ lastDepthMeters = depthData.depthMeters
+ hudDepth.text = unitPrefs.formatDepth(depthData.depthMeters)
}
}
lifecycleScope.launch {
LocationService.nmeaBoatSpeedData.collect { bspData ->
- hudBsp.text = "%.1f".format(Locale.getDefault(), bspData.bspKnots)
+ lastBspKnots = bspData.bspKnots
+ hudBsp.text = unitPrefs.formatSpeed(bspData.bspKnots)
}
}
lifecycleScope.launch {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt b/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt
new file mode 100644
index 0000000..8d60ae0
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt
@@ -0,0 +1,83 @@
+package org.terst.nav
+
+import android.content.Context
+import java.util.Locale
+
+enum class TempUnit { CELSIUS, FAHRENHEIT }
+enum class DepthUnit { FEET, METERS }
+enum class PressureUnit { HPA, INHG }
+enum class SpeedUnit { KNOTS, MPH, KPH }
+
+/**
+ * Persists and applies the user's preferred display units.
+ * Defaults: Fahrenheit, feet, hPa, knots.
+ */
+class UnitPrefs(context: Context) {
+
+ private val prefs = context.getSharedPreferences("unit_prefs", Context.MODE_PRIVATE)
+
+ var tempUnit: TempUnit
+ get() = TempUnit.valueOf(prefs.getString(KEY_TEMP, TempUnit.FAHRENHEIT.name)!!)
+ set(v) { prefs.edit().putString(KEY_TEMP, v.name).apply() }
+
+ var depthUnit: DepthUnit
+ get() = DepthUnit.valueOf(prefs.getString(KEY_DEPTH, DepthUnit.FEET.name)!!)
+ set(v) { prefs.edit().putString(KEY_DEPTH, v.name).apply() }
+
+ var pressureUnit: PressureUnit
+ get() = PressureUnit.valueOf(prefs.getString(KEY_PRESSURE, PressureUnit.HPA.name)!!)
+ set(v) { prefs.edit().putString(KEY_PRESSURE, v.name).apply() }
+
+ var speedUnit: SpeedUnit
+ get() = SpeedUnit.valueOf(prefs.getString(KEY_SPEED, SpeedUnit.KNOTS.name)!!)
+ set(v) { prefs.edit().putString(KEY_SPEED, v.name).apply() }
+
+ fun formatTemp(tempC: Double): String = when (tempUnit) {
+ TempUnit.CELSIUS -> "%.0f".format(Locale.getDefault(), tempC)
+ TempUnit.FAHRENHEIT -> "%.0f".format(Locale.getDefault(), tempC * 9.0 / 5.0 + 32.0)
+ }
+
+ fun tempUnitLabel(): String = when (tempUnit) {
+ TempUnit.CELSIUS -> "°C"
+ TempUnit.FAHRENHEIT -> "°F"
+ }
+
+ fun formatDepth(metres: Double): String = when (depthUnit) {
+ DepthUnit.FEET -> "%.1f".format(Locale.getDefault(), metres * 3.28084)
+ DepthUnit.METERS -> "%.1f".format(Locale.getDefault(), metres)
+ }
+
+ fun depthUnitLabel(): String = when (depthUnit) {
+ DepthUnit.FEET -> "ft"
+ DepthUnit.METERS -> "m"
+ }
+
+ fun formatPressure(hpa: Float): String = when (pressureUnit) {
+ PressureUnit.HPA -> "%.1f".format(Locale.getDefault(), hpa)
+ PressureUnit.INHG -> "%.2f".format(Locale.getDefault(), hpa * 0.02953f)
+ }
+
+ fun pressureUnitLabel(): String = when (pressureUnit) {
+ PressureUnit.HPA -> "hPa"
+ PressureUnit.INHG -> "inHg"
+ }
+
+ fun formatSpeed(knots: Double): String = when (speedUnit) {
+ SpeedUnit.KNOTS -> "%.1f".format(Locale.getDefault(), knots)
+ SpeedUnit.MPH -> "%.1f".format(Locale.getDefault(), knots * 1.15078)
+ SpeedUnit.KPH -> "%.1f".format(Locale.getDefault(), knots * 1.852)
+ }
+
+ fun speedUnitLabel(): String = when (speedUnit) {
+ SpeedUnit.KNOTS -> "kt"
+ SpeedUnit.MPH -> "mph"
+ SpeedUnit.KPH -> "kph"
+ }
+
+ companion object {
+ private const val KEY_TEMP = "temp_unit"
+ private const val KEY_DEPTH = "depth_unit"
+ private const val KEY_PRESSURE = "pressure_unit"
+ private const val KEY_SPEED = "speed_unit"
+ }
+}
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 48ebb3b..ccb3a91 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,19 +1,11 @@
package org.terst.nav.ui
import android.widget.TextView
+import org.terst.nav.UnitPrefs
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)
@@ -31,11 +23,9 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String =
* 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)
- * - 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
+ * All numeric values are accepted in SI / nautical base units:
+ * speeds in knots, heights in metres, pressure in hPa, temperature in °C.
+ * [unitPrefs] is consulted on each update to produce the correct formatted string.
*/
class InstrumentHandler(
// ── Conditions header ────────────────────────────────────────────────
@@ -58,47 +48,60 @@ class InstrumentHandler(
private val bearingWaves: TextView,
private val bearingSwell: TextView,
// ── Wave view ────────────────────────────────────────────────────────
- private val waveView: WaveView
+ private val waveView: WaveView,
+ // ── Unit preferences ─────────────────────────────────────────────────
+ private val unitPrefs: UnitPrefs
) {
init {
arrowCurr.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
arrowWaves.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
arrowSwell.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN
+ valueTws.text = "—"
+ valueTemp.text = "—"
+ valueBaro.text = "—"
+ valueCurrSpd.text = "—"
+ valueWaveHt.text = "—"
+ valueSwellHt.text = "—"
}
/**
- * Updates all area-conditions fields.
- * Null arguments leave the current value unchanged.
- * [waveHeightM] and [swellHeightM] are raw metres — converted to feet internally.
+ * Updates all area-conditions fields. Null arguments leave the current value unchanged.
+ *
+ * @param twsKt True wind speed in knots
+ * @param tempC Air temperature in degrees Celsius
+ * @param baroHpa Barometric pressure in hPa
+ * @param currSpdKt Ocean current speed in knots
+ * @param waveHeightM Wind-wave height in metres
+ * @param swellHeightM Swell height in metres
*/
fun updateConditions(
- tws: String? = null, twsBearingDeg: Float? = null,
+ twsKt: Double? = null, twsBearingDeg: Float? = null,
tempC: Double? = null,
- baro: String? = null,
- currSpd: String? = null, currDirDeg: Float? = null,
+ baroHpa: Float? = null,
+ currSpdKt: Double? = 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) }
+ twsKt?.let { valueTws.text = unitPrefs.formatSpeed(it) }
+ tempC?.let { valueTemp.text = unitPrefs.formatTemp(it) }
+ baroHpa?.let { valueBaro.text = unitPrefs.formatPressure(it) }
twsBearingDeg?.let {
arrowTws.bearing = it
bearingTws.text = formatBearing(it.toDouble())
}
- currSpd?.let { valueCurrSpd.text = it }
+ currSpdKt?.let { valueCurrSpd.text = unitPrefs.formatSpeed(it) }
currDirDeg?.let {
arrowCurr.bearing = it
bearingCurr.text = formatBearing(it.toDouble())
}
- waveHeightM?.let { valueWaveHt.text = formatFt(metresToFeet(it)) }
+ waveHeightM?.let { valueWaveHt.text = unitPrefs.formatDepth(it) }
waveDirDeg?.let {
arrowWaves.bearing = it
bearingWaves.text = formatBearing(it.toDouble())
}
- swellHeightM?.let { valueSwellHt.text = formatFt(metresToFeet(it)) }
+ swellHeightM?.let { valueSwellHt.text = unitPrefs.formatDepth(it) }
swellDirDeg?.let {
arrowSwell.bearing = it
bearingSwell.text = formatBearing(it.toDouble())
@@ -108,26 +111,28 @@ class InstrumentHandler(
/**
* Updates the WaveView with current sea state. Call once when conditions load.
- *
- * View height is set here to reflect swell scale: 1ft → 56dp, 8ft → 160dp.
+ * Heights are passed in metres; the WaveView scale is computed internally.
* [windSpeedKt] gates whitecap rendering (Beaufort 4 threshold = 12 kt).
*/
fun updateWaveState(
- swellHeightFt: Float,
+ swellHeightM: Float,
swellPeriodSec: Float,
- windWaveHeightFt: Float,
+ windWaveHeightM: Float,
windSpeedKt: Float = 0f
) {
- waveView.swellHeightFt = swellHeightFt
+ val swellHtFt = swellHeightM * 3.28084f
+ val windWaveHtFt = windWaveHeightM * 3.28084f
+
+ waveView.swellHeightFt = swellHtFt
waveView.swellPeriodSec = swellPeriodSec
- waveView.windWaveHeightFt = windWaveHeightFt
+ waveView.windWaveHeightFt = windWaveHtFt
waveView.windSpeedKt = windSpeedKt
// Size the view to the swell — bigger swell = taller window
- val density = waveView.resources.displayMetrics.density
- val heightDp = (32f + swellHeightFt * 16f).coerceIn(56f, 160f)
- val lp = waveView.layoutParams
- lp.height = (heightDp * density).toInt()
+ val density = waveView.resources.displayMetrics.density
+ val heightDp = (32f + swellHtFt * 16f).coerceIn(56f, 160f)
+ val lp = waveView.layoutParams
+ lp.height = (heightDp * density).toInt()
waveView.layoutParams = lp
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
index 48dc808..78fd70f 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt
@@ -7,30 +7,33 @@ import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.ChipGroup
import com.google.android.material.switchmaterial.SwitchMaterial
+import org.terst.nav.DepthUnit
+import org.terst.nav.PressureUnit
import org.terst.nav.R
+import org.terst.nav.SpeedUnit
+import org.terst.nav.TempUnit
+import org.terst.nav.UnitPrefs
class LayerPickerSheet(
private val manager: MapLayerManager,
private val onBaseChanged: (MapBasePreset) -> Unit,
- private val onWindChanged: (Boolean) -> Unit
+ private val onWindChanged: (Boolean) -> Unit,
+ private val onParticlesChanged: (Boolean) -> Unit,
+ private val unitPrefs: UnitPrefs,
+ private val onUnitsChanged: () -> Unit
) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.layout_layer_picker_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val chipGroup = view.findViewById<ChipGroup>(R.id.chip_group_base)
- val windSwitch = view.findViewById<SwitchMaterial>(R.id.switch_wind)
-
- // Set initial state from manager
- val chipId = when (manager.basePreset) {
+ // ── Base layer chips ──────────────────────────────────────────────────
+ val chipGroup = view.findViewById<ChipGroup>(R.id.chip_group_base)
+ chipGroup.check(when (manager.basePreset) {
MapBasePreset.SATELLITE -> R.id.chip_satellite
MapBasePreset.CHARTS -> R.id.chip_charts
MapBasePreset.HYBRID -> R.id.chip_hybrid
- }
- chipGroup.check(chipId)
- windSwitch.isChecked = manager.windEnabled
-
+ })
chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
val preset = when (checkedIds.firstOrNull()) {
R.id.chip_satellite -> MapBasePreset.SATELLITE
@@ -41,8 +44,76 @@ class LayerPickerSheet(
onBaseChanged(preset)
}
- windSwitch.setOnCheckedChangeListener { _, checked ->
- onWindChanged(checked)
+ // ── Wind overlay switch ───────────────────────────────────────────────
+ val windSwitch = view.findViewById<SwitchMaterial>(R.id.switch_wind)
+ windSwitch.isChecked = manager.windEnabled
+ windSwitch.setOnCheckedChangeListener { _, checked -> onWindChanged(checked) }
+
+ // ── Particles switch ──────────────────────────────────────────────────
+ val particlesSwitch = view.findViewById<SwitchMaterial>(R.id.switch_particles)
+ particlesSwitch.isChecked = manager.particlesEnabled
+ particlesSwitch.setOnCheckedChangeListener { _, checked -> onParticlesChanged(checked) }
+
+ // ── Temperature chips ─────────────────────────────────────────────────
+ val chipTemp = view.findViewById<ChipGroup>(R.id.chip_group_temp)
+ chipTemp.check(when (unitPrefs.tempUnit) {
+ TempUnit.FAHRENHEIT -> R.id.chip_fahrenheit
+ TempUnit.CELSIUS -> R.id.chip_celsius
+ })
+ chipTemp.setOnCheckedStateChangeListener { _, ids ->
+ unitPrefs.tempUnit = when (ids.firstOrNull()) {
+ R.id.chip_fahrenheit -> TempUnit.FAHRENHEIT
+ R.id.chip_celsius -> TempUnit.CELSIUS
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onUnitsChanged()
+ }
+
+ // ── Depth chips ───────────────────────────────────────────────────────
+ val chipDepth = view.findViewById<ChipGroup>(R.id.chip_group_depth)
+ chipDepth.check(when (unitPrefs.depthUnit) {
+ DepthUnit.FEET -> R.id.chip_feet
+ DepthUnit.METERS -> R.id.chip_meters
+ })
+ chipDepth.setOnCheckedStateChangeListener { _, ids ->
+ unitPrefs.depthUnit = when (ids.firstOrNull()) {
+ R.id.chip_feet -> DepthUnit.FEET
+ R.id.chip_meters -> DepthUnit.METERS
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onUnitsChanged()
+ }
+
+ // ── Pressure chips ────────────────────────────────────────────────────
+ val chipPressure = view.findViewById<ChipGroup>(R.id.chip_group_pressure)
+ chipPressure.check(when (unitPrefs.pressureUnit) {
+ PressureUnit.HPA -> R.id.chip_hpa
+ PressureUnit.INHG -> R.id.chip_inhg
+ })
+ chipPressure.setOnCheckedStateChangeListener { _, ids ->
+ unitPrefs.pressureUnit = when (ids.firstOrNull()) {
+ R.id.chip_hpa -> PressureUnit.HPA
+ R.id.chip_inhg -> PressureUnit.INHG
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onUnitsChanged()
+ }
+
+ // ── Speed chips ───────────────────────────────────────────────────────
+ val chipSpeed = view.findViewById<ChipGroup>(R.id.chip_group_speed)
+ chipSpeed.check(when (unitPrefs.speedUnit) {
+ SpeedUnit.KNOTS -> R.id.chip_knots
+ SpeedUnit.MPH -> R.id.chip_mph
+ SpeedUnit.KPH -> R.id.chip_kph
+ })
+ chipSpeed.setOnCheckedStateChangeListener { _, ids ->
+ unitPrefs.speedUnit = when (ids.firstOrNull()) {
+ R.id.chip_knots -> SpeedUnit.KNOTS
+ R.id.chip_mph -> SpeedUnit.MPH
+ R.id.chip_kph -> SpeedUnit.KPH
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onUnitsChanged()
}
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
index 83cfa70..855233f 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt
@@ -26,6 +26,9 @@ class MapLayerManager(context: Context) {
var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true)
private set
+ var particlesEnabled: Boolean = prefs.getBoolean(KEY_PARTICLES, true)
+ private set
+
// ── Source / Layer IDs ────────────────────────────────────────────────────
companion object {
@@ -39,8 +42,9 @@ class MapLayerManager(context: Context) {
const val LAYER_WIND = "wind-layer"
const val LAYER_SEAMARKS = "openseamap-layer"
- private const val KEY_BASE = "base_preset"
- private const val KEY_WIND = "wind_enabled"
+ private const val KEY_BASE = "base_preset"
+ private const val KEY_WIND = "wind_enabled"
+ private const val KEY_PARTICLES = "particles_enabled"
// Tile URLs
private const val URL_SATELLITE =
@@ -106,6 +110,12 @@ class MapLayerManager(context: Context) {
}
}
+ /** Toggle wind particles (Canvas overlay — no style interaction needed). */
+ fun setParticlesEnabled(enabled: Boolean) {
+ particlesEnabled = enabled
+ prefs.edit().putBoolean(KEY_PARTICLES, enabled).apply()
+ }
+
/** Toggle wind overlay on a live style. */
fun setWindEnabled(style: Style, enabled: Boolean) {
windEnabled = enabled
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 5147506..5943949 100644
--- a/android-app/app/src/main/res/layout/activity_main.xml
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -90,10 +90,10 @@
android:visibility="gone"
app:cornerRadius="20dp"
app:elevation="20dp"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/nav_hud"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- android:layout_marginBottom="24dp" />
+ android:layout_marginTop="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
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 faec826..03cefb7 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
@@ -56,7 +56,7 @@
android:id="@+id/value_tws"
style="@style/InstrumentPrimaryValue"
tools:text="15.5" />
- <TextView style="@style/InstrumentUnit" android:text="kts" />
+ <TextView android:id="@+id/unit_tws" style="@style/InstrumentUnit" android:text="kt" />
<org.terst.nav.ui.DirectionArrowView
android:id="@+id/arrow_tws"
android:layout_width="14dp"
@@ -95,7 +95,7 @@
android:id="@+id/value_temp"
style="@style/InstrumentPrimaryValue"
tools:text="18" />
- <TextView style="@style/InstrumentUnit" android:text="°C" />
+ <TextView android:id="@+id/unit_temp" style="@style/InstrumentUnit" android:text="°F" />
</LinearLayout>
</LinearLayout>
@@ -123,7 +123,7 @@
android:id="@+id/value_baro"
style="@style/InstrumentPrimaryValue"
tools:text="1013" />
- <TextView style="@style/InstrumentUnit" android:text="hPa" />
+ <TextView android:id="@+id/unit_baro" style="@style/InstrumentUnit" android:text="hPa" />
</LinearLayout>
</LinearLayout>
@@ -175,7 +175,7 @@
android:id="@+id/value_curr_spd"
style="@style/ForecastValue"
tools:text="0.8" />
- <TextView style="@style/ForecastUnit" android:text="kts" />
+ <TextView android:id="@+id/unit_curr_spd" style="@style/ForecastUnit" android:text="kt" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
@@ -214,7 +214,7 @@
android:id="@+id/value_wave_ht"
style="@style/ForecastValue"
tools:text="3.5" />
- <TextView style="@style/ForecastUnit" android:text="ft" />
+ <TextView android:id="@+id/unit_wave_ht" style="@style/ForecastUnit" android:text="ft" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
@@ -253,7 +253,7 @@
android:id="@+id/value_swell_ht"
style="@style/ForecastValue"
tools:text="5.2" />
- <TextView style="@style/ForecastUnit" android:text="ft" />
+ <TextView android:id="@+id/unit_swell_ht" style="@style/ForecastUnit" android:text="ft" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
diff --git a/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
index c424606..fdfbb7a 100644
--- a/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml
@@ -1,104 +1,310 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
+<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
- android:paddingStart="24dp"
- android:paddingEnd="24dp"
- android:paddingBottom="32dp"
android:background="?attr/colorSurface">
- <View
- android:layout_width="36dp"
- android:layout_height="4dp"
- android:layout_gravity="center_horizontal"
- android:layout_marginTop="12dp"
- android:layout_marginBottom="20dp"
- android:background="@color/md_theme_outline" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
- android:text="MAP LAYERS"
- android:textSize="11sp"
- android:textAllCaps="true"
- android:letterSpacing="0.12"
- android:fontFamily="sans-serif-light"
- android:textColor="@color/instrument_text_secondary" />
-
- <!-- Base map selection -->
- <com.google.android.material.chip.ChipGroup
- android:id="@+id/chip_group_base"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="24dp"
- app:singleSelection="true"
- app:selectionRequired="true"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_satellite"
- style="@style/Widget.Material3.Chip.Filter"
+ android:orientation="vertical"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingBottom="40dp">
+
+ <View
+ android:layout_width="36dp"
+ android:layout_height="4dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="20dp"
+ android:background="@color/md_theme_outline" />
+
+ <!-- ── MAP LAYERS ──────────────────────────────────────────── -->
+
+ <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Satellite" />
+ android:layout_marginBottom="12dp"
+ android:text="MAP LAYERS"
+ android:textSize="11sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/instrument_text_secondary" />
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_charts"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_width="wrap_content"
+ <!-- Base map selection -->
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_base"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Charts" />
+ android:layout_marginBottom="20dp"
+ app:singleSelection="true"
+ app:selectionRequired="true">
- <com.google.android.material.chip.Chip
- android:id="@+id/chip_hybrid"
- style="@style/Widget.Material3.Chip.Filter"
- android:layout_width="wrap_content"
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_satellite"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Satellite" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_charts"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Charts" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_hybrid"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Hybrid" />
+
+ </com.google.android.material.chip.ChipGroup>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginBottom="16dp"
+ android:background="@color/md_theme_surfaceVariant" />
+
+ <!-- Wind overlay toggle -->
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Hybrid" />
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="12dp">
- </com.google.android.material.chip.ChipGroup>
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Wind overlay"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Wind speed colour map"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
- <!-- Wind divider -->
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginBottom="16dp"
- android:background="@color/md_theme_surfaceVariant" />
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_wind"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
- <!-- Wind toggle -->
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:gravity="center_vertical">
+ </LinearLayout>
+ <!-- Wind particles toggle -->
<LinearLayout
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical">
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="8dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Wind particles"
+ android:textSize="15sp"
+ android:textColor="@color/instrument_text_normal" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Animated particle overlay"
+ android:textSize="12sp"
+ android:textColor="@color/instrument_text_secondary" />
+ </LinearLayout>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_particles"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+ <!-- ── UNITS ───────────────────────────────────────────────── -->
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="16dp"
+ android:background="@color/md_theme_surfaceVariant" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="12dp"
+ android:text="UNITS"
+ android:textSize="11sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/instrument_text_secondary" />
+
+ <!-- Temperature -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="10dp">
<TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Temperature"
+ android:textSize="14sp"
+ android:textColor="@color/instrument_text_normal" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Wind overlay"
- android:textSize="15sp"
+ app:singleSelection="true"
+ app:selectionRequired="true">
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_fahrenheit"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="°F" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_celsius"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="°C" />
+ </com.google.android.material.chip.ChipGroup>
+ </LinearLayout>
+
+ <!-- Depth / height -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="10dp">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Depth / height"
+ android:textSize="14sp"
android:textColor="@color/instrument_text_normal" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_depth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:singleSelection="true"
+ app:selectionRequired="true">
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_feet"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="ft" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_meters"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="m" />
+ </com.google.android.material.chip.ChipGroup>
+ </LinearLayout>
+
+ <!-- Pressure -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_marginBottom="10dp">
<TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Pressure"
+ android:textSize="14sp"
+ android:textColor="@color/instrument_text_normal" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_pressure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="OpenWeatherMap wind speed"
- android:textSize="12sp"
- android:textColor="@color/instrument_text_secondary" />
+ app:singleSelection="true"
+ app:selectionRequired="true">
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_hpa"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="hPa" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_inhg"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="inHg" />
+ </com.google.android.material.chip.ChipGroup>
</LinearLayout>
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/switch_wind"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
+ <!-- Speed -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Speed"
+ android:textSize="14sp"
+ android:textColor="@color/instrument_text_normal" />
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/chip_group_speed"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:singleSelection="true"
+ app:selectionRequired="true">
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_knots"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="kt" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_mph"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="mph" />
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_kph"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="kph" />
+ </com.google.android.material.chip.ChipGroup>
+ </LinearLayout>
</LinearLayout>
-
-</LinearLayout>
+</ScrollView>
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
index 4623bb1..90119fa 100644
--- a/android-app/app/src/main/res/layout/layout_nav_hud.xml
+++ b/android-app/app/src/main/res/layout/layout_nav_hud.xml
@@ -39,6 +39,7 @@
android:fontFamily="sans-serif-medium"
tools:text="7.1" />
<TextView
+ android:id="@+id/unit_hud_sog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="kt"
@@ -110,6 +111,7 @@
android:fontFamily="sans-serif-medium"
tools:text="6.8" />
<TextView
+ android:id="@+id/unit_hud_bsp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="kt"
@@ -149,6 +151,7 @@
android:fontFamily="sans-serif-medium"
tools:text="42.0" />
<TextView
+ android:id="@+id/unit_hud_depth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ft"