diff options
Diffstat (limited to 'android-app/app/src/main/kotlin/org')
4 files changed, 98 insertions, 26 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") |
