diff options
| author | Claude <noreply@anthropic.com> | 2026-04-10 16:27:42 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-04-10 16:27:42 +0000 |
| commit | bf57713ef0e378ebedd518f4fb243328de08179d (patch) | |
| tree | 63e130972b2306cf578b51052219ed290f994e34 /android-app | |
| parent | 4daf9dfcd00844075768e5b0d1dd7a17002a26d0 (diff) | |
Add unit settings, wind particles toggle, and recenter-to-top
- New UnitPrefs: persisted C/F, ft/m, hPa/inHg, kt/mph/kph — defaults to °F
- InstrumentHandler now accepts raw values (knots, metres, hPa, °C) and formats
via UnitPrefs on every update
- LayerPickerSheet expanded with wind particles toggle and unit chip selectors;
wrapped in ScrollView to handle the additional height
- MapLayerManager tracks particlesEnabled (persisted); setParticlesEnabled() added
- MainActivity: caches last raw values so refreshUnits() can reformat everything
instantly when the user changes a unit without waiting for new sensor data
- Recenter button moved to top (below HUD strip) so it's never obscured by the
bottom sheet or bottom nav
- Unit label TextViews given IDs in HUD and instrument sheet for live updates
https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
Diffstat (limited to 'android-app')
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" |
