summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt48
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt36
-rw-r--r--android-app/app/src/main/res/layout/activity_main.xml16
3 files changed, 96 insertions, 4 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 f9d4dbd..252761e 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
@@ -15,9 +15,11 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.button.MaterialButton
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers
@@ -53,6 +55,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
private lateinit var fragmentContainer: FrameLayout
private lateinit var fabRecordTrack: FloatingActionButton
+ private lateinit var fabMob: FloatingActionButton
+ private lateinit var fabRecenter: MaterialButton
+ private lateinit var bottomSheet: CardView
+ private lateinit var bottomNav: BottomNavigationView
private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) }
private val viewModel: MainViewModel by viewModels()
@@ -83,14 +89,20 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
setupBottomNavigation()
setupHandlers()
- findViewById<FloatingActionButton>(R.id.fab_mob).setOnClickListener {
- onActivateMob()
- }
-
fabRecordTrack = findViewById(R.id.fab_record_track)
fabRecordTrack.setOnClickListener {
if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack()
}
+
+ fabMob = findViewById(R.id.fab_mob)
+ fabMob.setOnClickListener { onActivateMob() }
+ fabRecenter = findViewById(R.id.fab_recenter)
+ bottomSheet = findViewById(R.id.instrument_bottom_sheet)
+ bottomNav = findViewById(R.id.bottom_navigation)
+
+ fabRecenter.setOnClickListener {
+ mapHandler?.recenter()
+ }
// Observe immediately — pure UI state, not gated on GPS permission
lifecycleScope.launch {
viewModel.isRecording.collect { recording ->
@@ -232,6 +244,17 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
mapView?.onCreate(null)
mapView?.getMapAsync { maplibreMap ->
mapHandler = MapHandler(maplibreMap)
+ lifecycleScope.launch {
+ mapHandler!!.isFollowing.collect { following ->
+ if (following) {
+ fadeOut(fabRecenter, gone = true)
+ fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack)
+ } else {
+ fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true)
+ fadeIn(fabRecenter)
+ }
+ }
+ }
val style = Style.Builder()
.fromUri("https://tiles.openfreemap.org/styles/liberty")
.withSource(RasterSource("openseamap-source",
@@ -309,6 +332,23 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
return PolarTable(curves)
}
+ private fun fadeOut(vararg views: View, gone: Boolean = false) {
+ views.forEach { v ->
+ v.animate().alpha(0f).setDuration(150).withEndAction {
+ v.visibility = if (gone) View.GONE else View.INVISIBLE
+ }.start()
+ }
+ }
+
+ private fun fadeIn(vararg views: View) {
+ views.forEach { v ->
+ if (v.visibility == View.VISIBLE && v.alpha == 1f) return@forEach
+ v.alpha = 0f
+ v.visibility = View.VISIBLE
+ v.animate().alpha(1f).setDuration(150).start()
+ }
+ }
+
override fun onStart() { super.onStart(); mapView?.onStart() }
override fun onPause() { super.onPause(); mapView?.onPause() }
override fun onStop() { super.onStop(); mapView?.onStop() }
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
index cbc2e90..7c82808 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt
@@ -24,12 +24,35 @@ import org.terst.nav.TidalCurrentState
import org.terst.nav.track.TrackPoint
import kotlin.math.cos
import kotlin.math.sin
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
/**
* Handles MapLibre initialization, layers, and updates.
*/
class MapHandler(private val maplibreMap: MapLibreMap) {
+ init {
+ maplibreMap.addOnCameraMoveStartedListener { reason ->
+ if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) {
+ _isFollowing.value = false
+ }
+ }
+ maplibreMap.addOnCameraIdleListener {
+ if (!_isFollowing.value) {
+ lastZoom = maplibreMap.cameraPosition.zoom
+ }
+ }
+ }
+
+ private val _isFollowing = MutableStateFlow(true)
+ val isFollowing: StateFlow<Boolean> = _isFollowing.asStateFlow()
+
+ private var lastLat: Double = 0.0
+ private var lastLon: Double = 0.0
+ private var lastZoom: Double = 14.0
+
private val ANCHOR_POINT_SOURCE_ID = "anchor-point-source"
private val ANCHOR_CIRCLE_SOURCE_ID = "anchor-circle-source"
private val ANCHOR_POINT_LAYER_ID = "anchor-point-layer"
@@ -95,6 +118,9 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
* Centers the map on the specified location.
*/
fun centerOnLocation(lat: Double, lon: Double, zoom: Double = 14.0) {
+ lastLat = lat
+ lastLon = lon
+ if (!_isFollowing.value) return
val position = CameraPosition.Builder()
.target(LatLng(lat, lon))
.zoom(zoom)
@@ -103,6 +129,16 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
}
/**
+ * Restores auto-follow mode and animates the camera back to the last known GPS position.
+ * No-op if no GPS fix has been received yet.
+ */
+ fun recenter() {
+ if (lastLat == 0.0 && lastLon == 0.0) return
+ _isFollowing.value = true
+ centerOnLocation(lastLat, lastLon, lastZoom)
+ }
+
+ /**
* Updates the anchor watch visualization on the map.
*/
fun updateAnchorWatch(state: AnchorWatchState) {
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 b4221ed..a3d347f 100644
--- a/android-app/app/src/main/res/layout/activity_main.xml
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -25,6 +25,22 @@
android:visibility="gone"
android:background="?attr/colorSurface" />
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/fab_recenter"
+ android:layout_width="wrap_content"
+ android:layout_height="40dp"
+ android:text="⊙ Recenter"
+ android:textSize="13sp"
+ android:paddingStart="20dp"
+ android:paddingEnd="20dp"
+ android:visibility="gone"
+ app:cornerRadius="20dp"
+ app:elevation="20dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginBottom="24dp" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Collapsible Instrument Bottom Sheet -->