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/track/SavedTracksFragment.kt14
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt12
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt96
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt2
-rw-r--r--android-app/app/src/main/res/layout/layout_track_detail_sheet.xml184
5 files changed, 195 insertions, 113 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
index 44055ce..67f32f6 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
@@ -34,18 +34,20 @@ class SavedTracksFragment : Fragment() {
(requireActivity() as MainActivity).hideOverlays()
}
- val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
- val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
- val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
- val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
+ val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
+ val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
+ val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
+ val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
val adapter = SavedTrackAdapter { track ->
viewModel.selectTrack(track)
- (requireActivity() as MainActivity).hideOverlays()
- TrackDetailSheet.newInstance().show(parentFragmentManager, "track_detail")
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, TrackDetailSheet.newInstance())
+ .addToBackStack("track_detail")
+ .commit()
}
rv.adapter = adapter
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
index 3c3385c..9668fd1 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
@@ -3,11 +3,12 @@ package org.terst.nav.track
import kotlin.math.abs
object TackDetector {
- private const val HALF_WIN = 2
- private const val MIN_DELTA = 60.0
- private const val MAX_DELTA = 140.0
- private const val STABILITY_MAX = 20.0
- private const val MIN_GAP_MS = 45_000L // 45 s minimum between tacks
+ private const val HALF_WIN = 2
+ private const val MIN_DELTA = 60.0
+ private const val MAX_DELTA = 140.0
+ private const val STABILITY_MAX = 30.0
+ private const val MIN_GAP_MS = 45_000L // 45 s minimum between tacks
+ private const val START_SKIP_MS = 120_000L // skip first 2 min (GPS cold-start noise)
fun detectTacks(points: List<TrackPoint>): List<TackEvent> {
if (points.size < 2 * HALF_WIN + 1) return emptyList()
@@ -16,6 +17,7 @@ object TackDetector {
var lastTackMs = Long.MIN_VALUE
for (i in HALF_WIN until points.size - HALF_WIN) {
+ if (points[i].timestampMs - points.first().timestampMs < START_SKIP_MS) continue
if (points[i].timestampMs - lastTackMs < MIN_GAP_MS) continue
val spreadBefore = abs(angleDiff(points[i - 2].cogDeg, points[i - 1].cogDeg))
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 98205fd..cedf4e1 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
@@ -5,13 +5,25 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
+import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.maplibre.android.camera.CameraUpdateFactory
+import org.maplibre.android.geometry.LatLng
+import org.maplibre.android.geometry.LatLngBounds
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.CircleLayer
+import org.maplibre.android.style.layers.LineLayer
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.sources.GeoJsonSource
+import org.maplibre.geojson.Feature
+import org.maplibre.geojson.FeatureCollection
+import org.maplibre.geojson.LineString
+import org.maplibre.geojson.Point
import org.terst.nav.R
import org.terst.nav.ui.MainViewModel
import java.time.Instant
@@ -30,9 +42,11 @@ data class LogEvent(
val detail: String
)
-class TrackDetailSheet : BottomSheetDialogFragment() {
+class TrackDetailSheet : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
+ private lateinit var trackMapView: MapView
+ private var trackMap: MapLibreMap? = null
companion object {
fun newInstance() = TrackDetailSheet()
@@ -43,11 +57,13 @@ class TrackDetailSheet : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val track = viewModel.selectedTrack.value ?: run {
- dismiss()
+ parentFragmentManager.popBackStack()
return
}
- view.findViewById<View>(R.id.btn_back).setOnClickListener { dismiss() }
+ view.findViewById<View>(R.id.btn_back).setOnClickListener {
+ parentFragmentManager.popBackStack()
+ }
val titleFmt = DateTimeFormatter.ofPattern("dd MMM yyyy · HH:mm", Locale.getDefault())
.withZone(ZoneId.systemDefault())
@@ -68,20 +84,72 @@ class TrackDetailSheet : BottomSheetDialogFragment() {
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
rv.adapter = LogEventAdapter(events) { event ->
- viewModel.panToTrackPoint(event.lat, event.lon)
+ trackMap?.easeCamera(CameraUpdateFactory.newLatLng(LatLng(event.lat, event.lon)), 300)
}
- (dialog as? BottomSheetDialog)?.behavior?.apply {
- isFitToContents = false
- halfExpandedRatio = 0.5f
- skipCollapsed = true
- isDraggable = false
- state = BottomSheetBehavior.STATE_HALF_EXPANDED
+ trackMapView = view.findViewById(R.id.track_map_view)
+ trackMapView.onCreate(savedInstanceState)
+ trackMapView.getMapAsync { map ->
+ trackMap = map
+ map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/bright")) { style ->
+ drawTrack(style, track.points)
+ drawTacks(style, track.tacks)
+ zoomToTrack(map, track.points)
+ }
}
- dialog?.window?.setDimAmount(0.2f)
+ }
+
+ private fun drawTrack(style: Style, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val source = GeoJsonSource("track-detail-source",
+ Feature.fromGeometry(LineString.fromLngLats(points.map { Point.fromLngLat(it.lon, it.lat) })))
+ style.addSource(source)
+ style.addLayer(LineLayer("track-detail-layer", "track-detail-source").apply {
+ setProperties(
+ PropertyFactory.lineColor("#2B8FC4"),
+ PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineCap("round"),
+ PropertyFactory.lineJoin("round")
+ )
+ })
+ }
+
+ private fun drawTacks(style: Style, tacks: List<TackEvent>) {
+ if (tacks.isEmpty()) return
+ val features = tacks.map { Feature.fromGeometry(Point.fromLngLat(it.lon, it.lat)) }
+ val source = GeoJsonSource("tack-detail-source", FeatureCollection.fromFeatures(features))
+ style.addSource(source)
+ style.addLayer(CircleLayer("tack-detail-layer", "tack-detail-source").apply {
+ setProperties(
+ PropertyFactory.circleColor("#FDD835"),
+ PropertyFactory.circleRadius(7f),
+ PropertyFactory.circleStrokeColor("#F57F17"),
+ PropertyFactory.circleStrokeWidth(2f)
+ )
+ })
+ }
+
+ private fun zoomToTrack(map: MapLibreMap, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val bounds = LatLngBounds.Builder().apply {
+ points.forEach { include(LatLng(it.lat, it.lon)) }
+ }.build()
+ map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 80), 600)
+ }
+
+ override fun onStart() { super.onStart(); if (::trackMapView.isInitialized) trackMapView.onStart() }
+ override fun onResume() { super.onResume(); if (::trackMapView.isInitialized) trackMapView.onResume() }
+ override fun onPause() { super.onPause(); if (::trackMapView.isInitialized) trackMapView.onPause() }
+ override fun onStop() { super.onStop(); if (::trackMapView.isInitialized) trackMapView.onStop() }
+ override fun onLowMemory() { super.onLowMemory(); if (::trackMapView.isInitialized) trackMapView.onLowMemory() }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (::trackMapView.isInitialized) trackMapView.onSaveInstanceState(outState)
}
override fun onDestroyView() {
+ if (::trackMapView.isInitialized) trackMapView.onDestroy()
super.onDestroyView()
viewModel.clearSelectedTrack()
}
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 0c31024..7614a49 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
@@ -315,7 +315,7 @@ class MapHandler(private val maplibreMap: MapLibreMap) {
style.addLayer(LineLayer(TRACK_PAST_DIM_LAYER_ID, TRACK_PAST_DIM_SOURCE_ID).apply {
setProperties(
PropertyFactory.lineColor("#0B3050"),
- PropertyFactory.lineOpacity(0.22f),
+ PropertyFactory.lineOpacity(0.12f),
PropertyFactory.lineWidth(2.5f),
PropertyFactory.lineCap("round"),
PropertyFactory.lineJoin("round")
diff --git a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
index 97b1da8..993edf8 100644
--- a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
@@ -6,107 +6,117 @@
android:orientation="vertical"
android:background="?attr/colorSurface">
- <!-- Drag handle -->
- <View
- android:layout_width="36dp"
- android:layout_height="40dp"
- android:layout_gravity="center_horizontal"
- android:background="@drawable/drag_handle_bg" />
-
- <!-- Header -->
+ <!-- Track map — top half -->
+ <org.maplibre.android.maps.MapView
+ android:id="@+id/track_map_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <!-- Detail panel — bottom half -->
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="56dp"
- android:orientation="horizontal"
- android:gravity="center_vertical"
- android:paddingStart="8dp"
- android:paddingEnd="16dp"
- android:elevation="4dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
android:background="?attr/colorSurface">
- <ImageButton
- android:id="@+id/btn_back"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:src="@drawable/ic_back"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="Back" />
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="16dp"
+ android:elevation="4dp"
+ android:background="?attr/colorSurface">
- <TextView
- android:id="@+id/detail_title"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:textSize="17sp"
- android:textStyle="bold"
- android:layout_marginStart="4dp" />
+ <ImageButton
+ android:id="@+id/btn_back"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:src="@drawable/ic_back"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="Back" />
- </LinearLayout>
+ <TextView
+ android:id="@+id/detail_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="17sp"
+ android:textStyle="bold"
+ android:layout_marginStart="4dp" />
- <!-- Stats row: distance · duration · max · avg -->
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingHorizontal="12dp"
- android:paddingTop="12dp"
- android:paddingBottom="12dp">
-
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </LinearLayout>
+ <!-- Stats row: distance · duration · max · avg -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingHorizontal="12dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </LinearLayout>
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- </LinearLayout>
+ <View android:layout_width="match_parent" android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
- <View android:layout_width="match_parent" android:layout_height="1dp"
- android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="20dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="4dp"
+ android:text="Event Log"
+ android:textSize="13sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.08"
+ android:textColor="?attr/colorOnSurfaceVariant" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="20dp"
- android:paddingTop="12dp"
- android:paddingBottom="4dp"
- android:text="Event Log"
- android:textSize="13sp"
- android:textAllCaps="true"
- android:letterSpacing="0.08"
- android:textColor="?attr/colorOnSurfaceVariant" />
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/rv_log_entries"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:clipToPadding="false"
- android:paddingBottom="24dp" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/rv_log_entries"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:paddingBottom="24dp" />
+
+ </LinearLayout>
</LinearLayout>