diff options
Diffstat (limited to 'android-app/app/src/main/kotlin/org')
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) |
