summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt25
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt18
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt28
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt17
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt45
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt19
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt7
7 files changed, 127 insertions, 32 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 ead98ae..6c091ff 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
@@ -484,6 +484,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
fabFishing.visibility = View.GONE
fabLayers.visibility = View.GONE
fabRecenter.visibility = View.GONE
+ if (::bottomSheetBehavior.isInitialized) {
+ bottomSheetBehavior.isHideable = true
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ }
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
@@ -600,14 +604,25 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private fun checkForegroundPermissions() {
val fineLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
val coarseLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ val locationGranted = fineLocation == PackageManager.PERMISSION_GRANTED ||
+ coarseLocation == PackageManager.PERMISSION_GRANTED
+
+ val toRequest = mutableListOf<String>()
+ if (!locationGranted) {
+ toRequest += Manifest.permission.ACCESS_FINE_LOCATION
+ toRequest += Manifest.permission.ACCESS_COARSE_LOCATION
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED) {
+ toRequest += Manifest.permission.POST_NOTIFICATIONS
+ }
- if (fineLocation == PackageManager.PERMISSION_GRANTED || coarseLocation == PackageManager.PERMISSION_GRANTED) {
+ if (locationGranted) {
startServices()
+ if (toRequest.isNotEmpty()) requestPermissionLauncher.launch(toRequest.toTypedArray())
} else {
- requestPermissionLauncher.launch(arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ))
+ requestPermissionLauncher.launch(toRequest.toTypedArray())
}
}
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
index c676390..20de3e4 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt
@@ -7,6 +7,7 @@ enum class TempUnit { CELSIUS, FAHRENHEIT }
enum class DepthUnit { FEET, METERS }
enum class PressureUnit { HPA, INHG }
enum class SpeedUnit { KNOTS, MPH, KPH }
+enum class DistanceUnit { NM, KM, MI }
/**
* Persists and applies the user's preferred display units.
@@ -32,6 +33,10 @@ class UnitPrefs(context: Context) {
get() = runCatching { SpeedUnit.valueOf(prefs.getString(KEY_SPEED, null) ?: "") }.getOrDefault(SpeedUnit.KNOTS)
set(v) { prefs.edit().putString(KEY_SPEED, v.name).apply() }
+ var distanceUnit: DistanceUnit
+ get() = runCatching { DistanceUnit.valueOf(prefs.getString(KEY_DISTANCE, null) ?: "") }.getOrDefault(DistanceUnit.NM)
+ set(v) { prefs.edit().putString(KEY_DISTANCE, 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)
@@ -74,10 +79,23 @@ class UnitPrefs(context: Context) {
SpeedUnit.KPH -> "kph"
}
+ fun formatDistance(nm: Double): String = when (distanceUnit) {
+ DistanceUnit.NM -> "%.1f".format(Locale.getDefault(), nm)
+ DistanceUnit.KM -> "%.1f".format(Locale.getDefault(), nm * 1.852)
+ DistanceUnit.MI -> "%.1f".format(Locale.getDefault(), nm * 1.15078)
+ }
+
+ fun distanceUnitLabel(): String = when (distanceUnit) {
+ DistanceUnit.NM -> "nm"
+ DistanceUnit.KM -> "km"
+ DistanceUnit.MI -> "mi"
+ }
+
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"
+ private const val KEY_DISTANCE = "distance_unit"
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
index 7dd0e07..e194efc 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
@@ -352,9 +352,11 @@ class TrackDetailSheet : Fragment() {
"%.0f° → %.0f° (Δ%.0f°)".format(t.cogBefore, t.cogAfter, abs(delta)))
}
- // Log entries saved during or associated with this track's time window
+ // Log entries saved during or associated with this track's time window.
+ // Photo-only entries (no real text) are excluded — photos belong to log entries, not as standalone events.
val logEntries = NavApplication.logbookRepository.getAll()
.filter { it.timestampMs in track.startMs..track.endMs }
+ .filter { it.text.isNotBlank() && it.text != "(photo only)" }
for (entry in logEntries) {
val lat = entry.lat ?: nearestTrackPoint(points, entry.timestampMs)?.lat ?: continue
val lon = entry.lon ?: nearestTrackPoint(points, entry.timestampMs)?.lon ?: continue
@@ -447,17 +449,41 @@ private class LogEventAdapter(
if (bm != null) {
holder.ivThumb.setImageBitmap(bm)
holder.ivThumb.visibility = View.VISIBLE
+ holder.ivThumb.setOnClickListener { showFullscreenPhoto(holder.itemView.context, path) }
} else {
holder.ivThumb.setImageDrawable(null)
holder.ivThumb.visibility = View.GONE
+ holder.ivThumb.setOnClickListener(null)
}
} else {
holder.ivThumb.setImageDrawable(null)
holder.ivThumb.visibility = View.GONE
+ holder.ivThumb.setOnClickListener(null)
}
}
}
+private fun showFullscreenPhoto(context: android.content.Context, pathOrUri: String) {
+ val uri = if (pathOrUri.startsWith("content://")) Uri.parse(pathOrUri) else Uri.fromFile(File(pathOrUri))
+ val imageView = android.widget.ImageView(context).apply {
+ setImageURI(uri)
+ scaleType = android.widget.ImageView.ScaleType.FIT_CENTER
+ setBackgroundColor(android.graphics.Color.BLACK)
+ layoutParams = android.view.ViewGroup.LayoutParams(
+ android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+ android.view.ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+ android.app.AlertDialog.Builder(context)
+ .setView(imageView)
+ .setPositiveButton("Close", null)
+ .show()
+ .window?.setLayout(
+ android.view.ViewGroup.LayoutParams.MATCH_PARENT,
+ android.view.ViewGroup.LayoutParams.MATCH_PARENT
+ )
+}
+
private fun decodeThumbnail(context: android.content.Context, pathOrUri: String, targetW: Int, targetH: Int): android.graphics.Bitmap? {
if (pathOrUri.startsWith("content://")) {
return runCatching {
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 b60af50..47d29c4 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
@@ -13,6 +13,7 @@ import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.ChipGroup
import com.google.android.material.switchmaterial.SwitchMaterial
import org.terst.nav.DepthUnit
+import org.terst.nav.DistanceUnit
import org.terst.nav.PressureUnit
import org.terst.nav.R
import org.terst.nav.SpeedUnit
@@ -178,6 +179,22 @@ class LayerPickerSheet(
}
onUnitsChanged()
}
+
+ val chipDistance = view.findViewById<ChipGroup>(R.id.chip_group_distance)
+ chipDistance.check(when (unitPrefs.distanceUnit) {
+ DistanceUnit.NM -> R.id.chip_nm
+ DistanceUnit.KM -> R.id.chip_km
+ DistanceUnit.MI -> R.id.chip_mi
+ })
+ chipDistance.setOnCheckedStateChangeListener { _, ids ->
+ unitPrefs.distanceUnit = when (ids.firstOrNull()) {
+ R.id.chip_nm -> DistanceUnit.NM
+ R.id.chip_km -> DistanceUnit.KM
+ R.id.chip_mi -> DistanceUnit.MI
+ else -> return@setOnCheckedStateChangeListener
+ }
+ onUnitsChanged()
+ }
}
private fun dpToPx(dp: Int): Int =
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 f6ee4fb..8cee462 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
@@ -2,6 +2,8 @@ package org.terst.nav.ui
import android.content.Context
import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.BackgroundLayer
+import org.maplibre.android.style.layers.FillLayer
import org.maplibre.android.style.layers.PropertyFactory
import org.maplibre.android.style.layers.RasterLayer
import org.maplibre.android.style.sources.RasterSource
@@ -129,25 +131,38 @@ class MapLayerManager(context: Context) {
}
/**
- * Inserts base-map rasters at indices 0/1/2 so OFM vector layers sit on top.
- * Call this immediately after the style finishes loading.
+ * Inserts base-map rasters above the last OFM fill/background layer so they appear
+ * above OFM water/land fills but below OFM road lines and label symbols.
+ * addLayerAt(0) would put rasters below everything, where OFM fills cover them.
+ * addLayer() (appending) would put rasters on top of labels, hiding them.
+ * addLayerAbove(lastFill) is the sweet spot: imagery shows through, labels overlay.
*/
fun addBaseLayersToStyle(style: Style) {
- android.util.Log.d("MapLayers", "addBaseLayersToStyle preset=$basePreset ofmLayers=${style.layers.size}")
- // Insert in ascending index order so final order from bottom is: ocean(0) → satellite(1) → charts(2) → OFM…
- style.addLayerAt(RasterLayer(LAYER_OCEAN, SOURCE_OCEAN).apply {
- setProperties(PropertyFactory.rasterOpacity(1f))
- setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_OCEAN)))
- }, 0)
- style.addLayerAt(RasterLayer(LAYER_SATELLITE, SOURCE_SATELLITE).apply {
- setProperties(PropertyFactory.rasterOpacity(1f))
- setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_SATELLITE)))
- }, 1)
- style.addLayerAt(RasterLayer(LAYER_CHARTS, SOURCE_CHARTS).apply {
+ val lastFillId = style.layers
+ .lastOrNull { it is BackgroundLayer || it is FillLayer }?.id
+ android.util.Log.d("MapLayers", "addBaseLayersToStyle preset=$basePreset ofmLayers=${style.layers.size} insertAbove=$lastFillId")
+
+ fun addBase(layer: RasterLayer) {
+ if (lastFillId != null) style.addLayerAbove(layer, lastFillId)
+ else style.addLayerAt(layer, 0)
+ }
+
+ // Add in reverse visual order (charts first = lowest in our stack)
+ // because each addLayerAbove(x, lastFillId) inserts x just above lastFillId,
+ // pushing previous insertions higher. Final order: ocean → satellite → charts → OFM lines/labels.
+ addBase(RasterLayer(LAYER_CHARTS, SOURCE_CHARTS).apply {
setProperties(PropertyFactory.rasterOpacity(opacityFor(basePreset)))
setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_CHARTS)))
- }, 2)
- android.util.Log.d("MapLayers", "base layers inserted, ocean=${visibilityFor(basePreset, LAYER_OCEAN)} satellite=${visibilityFor(basePreset, LAYER_SATELLITE)}")
+ })
+ addBase(RasterLayer(LAYER_SATELLITE, SOURCE_SATELLITE).apply {
+ setProperties(PropertyFactory.rasterOpacity(1f))
+ setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_SATELLITE)))
+ })
+ addBase(RasterLayer(LAYER_OCEAN, SOURCE_OCEAN).apply {
+ setProperties(PropertyFactory.rasterOpacity(1f))
+ setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_OCEAN)))
+ })
+ android.util.Log.d("MapLayers", "base layers inserted above $lastFillId, ocean=${visibilityFor(basePreset, LAYER_OCEAN)}")
}
/**
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
index c3bef8f..91c3489 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt
@@ -13,8 +13,10 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.materialswitch.MaterialSwitch
+import org.terst.nav.DepthUnit
import org.terst.nav.NavApplication
import org.terst.nav.R
+import org.terst.nav.UnitPrefs
import org.terst.nav.ais.CpaAlert
import org.terst.nav.ais.CpaSeverity
import org.terst.nav.ais.label
@@ -63,6 +65,11 @@ class SafetyFragment : Fragment() {
chevronAnchor.text = if (expanding) "▼" else "▶"
}
+ val unitPrefs = UnitPrefs(requireContext())
+ val unitLabel = unitPrefs.depthUnitLabel()
+ view.findViewById<TextView>(R.id.tv_anchor_depth_label).text = "Depth ($unitLabel)"
+ view.findViewById<TextView>(R.id.tv_anchor_rode_label).text = "Rode Out ($unitLabel)"
+
val etDepth = view.findViewById<EditText>(R.id.et_anchor_depth)
val etRode = view.findViewById<EditText>(R.id.et_anchor_rode)
val tvRadius = view.findViewById<TextView>(R.id.tv_anchor_suggested_radius)
@@ -70,11 +77,13 @@ class SafetyFragment : Fragment() {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
- val depth = etDepth.text.toString().toDoubleOrNull()
- val rode = etRode.text.toString().toDoubleOrNull()
- tvRadius.text = if (depth != null && rode != null && depth >= 0.0 && rode > 0.0) {
- val r = AnchorWatchState.calculateRecommendedWatchCircleRadius(depth, 2.0, rode)
- getString(R.string.anchor_suggested_radius_fmt, r)
+ val inputDepth = etDepth.text.toString().toDoubleOrNull()
+ val inputRode = etRode.text.toString().toDoubleOrNull()
+ tvRadius.text = if (inputDepth != null && inputRode != null && inputDepth >= 0.0 && inputRode > 0.0) {
+ val depthM = if (unitPrefs.depthUnit == DepthUnit.FEET) inputDepth * 0.3048 else inputDepth
+ val rodeM = if (unitPrefs.depthUnit == DepthUnit.FEET) inputRode * 0.3048 else inputRode
+ val r = AnchorWatchState.calculateRecommendedWatchCircleRadius(depthM, 2.0, rodeM)
+ "${unitPrefs.formatDepth(r)} $unitLabel"
} else {
getString(R.string.anchor_suggested_radius_empty)
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
index 51a0685..83e8c07 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
@@ -164,12 +164,7 @@ class VoiceLogFragment : Fragment() {
}
btnClear.setOnClickListener { clearEntry() }
- // Show log-entry section only while recording
- viewLifecycleOwner.lifecycleScope.launch {
- mainViewModel.isRecording.collect { recording ->
- layoutLogEntry.visibility = if (recording) View.VISIBLE else View.GONE
- }
- }
+ layoutLogEntry.visibility = View.VISIBLE
// Log entries list
val tvNoEntries = view.findViewById<TextView>(R.id.tv_no_log_entries)