diff options
4 files changed, 84 insertions, 3 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt index 015e334..9440d49 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt @@ -376,8 +376,8 @@ class LocationService : Service() { return EnvironmentalSnapshot( windSpeedKt = trueWind?.speedKt, windDirectionDeg = trueWind?.directionDeg, - currentSpeedKt = null, // TODO: Pull from latest forecast - currentDirectionDeg = null + currentSpeedKt = _currentSpeedKt.value, + currentDirectionDeg = _currentDirectionDeg.value ) } @@ -431,6 +431,14 @@ class LocationService : Service() { private val _nmeaBoatSpeedDataFlow = MutableSharedFlow<BoatSpeedData>(replay = 1) val nmeaBoatSpeedData: SharedFlow<BoatSpeedData> get() = _nmeaBoatSpeedDataFlow + private val _currentSpeedKt = MutableStateFlow<Double?>(null) + private val _currentDirectionDeg = MutableStateFlow<Double?>(null) + + fun setCurrentConditions(speedKt: Double?, directionDeg: Double?) { + _currentSpeedKt.value = speedKt + _currentDirectionDeg.value = directionDeg + } + private val _currentPowerMode = MutableStateFlow(PowerMode.FULL) val currentPowerMode: StateFlow<PowerMode> get() = _currentPowerMode 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 ddb9c56..e4bc9b5 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 @@ -333,6 +333,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun applyConditions(c: MarineConditions) { + LocationService.setCurrentConditions(c.currentSpeedKt, c.currentDirDeg) instrumentHandler?.updateConditions( twsKt = c.windSpeedKt, twsBearingDeg = c.windDirDeg?.toFloat(), 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 cedf4e1..25e7b15 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 @@ -1,9 +1,11 @@ package org.terst.nav.track +import android.graphics.BitmapFactory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -24,8 +26,10 @@ import org.maplibre.geojson.Feature import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.LineString import org.maplibre.geojson.Point +import org.terst.nav.NavApplication import org.terst.nav.R import org.terst.nav.ui.MainViewModel +import java.io.File import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -39,7 +43,8 @@ data class LogEvent( val timestampMs: Long, val icon: String, val label: String, - val detail: String + val detail: String, + val photoPath: String? = null ) class TrackDetailSheet : Fragment() { @@ -94,6 +99,8 @@ class TrackDetailSheet : Fragment() { map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/bright")) { style -> drawTrack(style, track.points) drawTacks(style, track.tacks) + val notePoints = events.filter { it.photoPath != null || it.icon == "đ" } + drawNotes(style, notePoints) zoomToTrack(map, track.points) } } @@ -129,6 +136,21 @@ class TrackDetailSheet : Fragment() { }) } + private fun drawNotes(style: Style, notes: List<LogEvent>) { + if (notes.isEmpty()) return + val features = notes.map { Feature.fromGeometry(Point.fromLngLat(it.lon, it.lat)) } + val source = GeoJsonSource("notes-detail-source", FeatureCollection.fromFeatures(features)) + style.addSource(source) + style.addLayer(CircleLayer("notes-detail-layer", "notes-detail-source").apply { + setProperties( + PropertyFactory.circleColor("#4CAF50"), + PropertyFactory.circleRadius(7f), + PropertyFactory.circleStrokeColor("#1B5E20"), + PropertyFactory.circleStrokeWidth(2f) + ) + }) + } + private fun zoomToTrack(map: MapLibreMap, points: List<TrackPoint>) { if (points.size < 2) return val bounds = LatLngBounds.Builder().apply { @@ -173,6 +195,17 @@ class TrackDetailSheet : Fragment() { "%.0f° â %.0f° (Î%.0f°)".format(t.cogBefore, t.cogAfter, abs(delta))) } + // Log entries saved during this track's time window + val logEntries = NavApplication.logbookRepository.getAll() + .filter { it.timestampMs in track.startMs..track.endMs } + for (entry in logEntries) { + val lat = entry.lat ?: nearestTrackPoint(points, entry.timestampMs)?.lat ?: continue + val lon = entry.lon ?: nearestTrackPoint(points, entry.timestampMs)?.lon ?: continue + val icon = if (entry.photoPath != null) "đˇ" else "đ" + val detail = entry.text.take(80) + events += LogEvent(lat, lon, entry.timestampMs, icon, "Note", detail, entry.photoPath) + } + points.lastOrNull()?.let { p -> events += LogEvent(p.lat, p.lon, p.timestampMs, "đ", "End", formatPointDetail(p)) } @@ -180,6 +213,9 @@ class TrackDetailSheet : Fragment() { return events.sortedBy { it.timestampMs } } + private fun nearestTrackPoint(points: List<TrackPoint>, timestampMs: Long): TrackPoint? = + points.minByOrNull { abs(it.timestampMs - timestampMs) } + private fun formatPointDetail(p: TrackPoint): String { val parts = mutableListOf("%.1f kt %.0f°".format(p.sogKnots, p.cogDeg)) p.windSpeedKnots?.let { parts += "W %.0f kt".format(it) } @@ -202,6 +238,7 @@ private class LogEventAdapter( val tvTime = view.findViewById<TextView>(R.id.tv_log_time) val tvLabel = view.findViewById<TextView>(R.id.tv_log_label) val tvDetail = view.findViewById<TextView>(R.id.tv_log_detail) + val ivThumb = view.findViewById<ImageView>(R.id.iv_log_thumb) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = @@ -216,5 +253,28 @@ private class LogEventAdapter( holder.tvLabel.text = e.label holder.tvDetail.text = e.detail holder.itemView.setOnClickListener { onTap(e) } + + val path = e.photoPath + if (path != null && File(path).exists()) { + val bm = decodeThumbnail(path, 160, 120) + if (bm != null) { + holder.ivThumb.setImageBitmap(bm) + holder.ivThumb.visibility = View.VISIBLE + } else { + holder.ivThumb.setImageDrawable(null) + holder.ivThumb.visibility = View.GONE + } + } else { + holder.ivThumb.setImageDrawable(null) + holder.ivThumb.visibility = View.GONE + } } } + +private fun decodeThumbnail(path: String, targetW: Int, targetH: Int): android.graphics.Bitmap? { + val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(path, opts) + if (opts.outWidth <= 0) return null + val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1) + return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply { inSampleSize = scale }) +} diff --git a/android-app/app/src/main/res/layout/item_log_entry.xml b/android-app/app/src/main/res/layout/item_log_entry.xml index fc53ef1..e23c7c2 100644 --- a/android-app/app/src/main/res/layout/item_log_entry.xml +++ b/android-app/app/src/main/res/layout/item_log_entry.xml @@ -49,6 +49,18 @@ app:layout_constraintTop_toBottomOf="@id/tv_log_label" app:layout_constraintStart_toEndOf="@id/tv_log_icon" app:layout_constraintEnd_toEndOf="parent" + android:layout_marginStart="8dp" /> + + <ImageView + android:id="@+id/iv_log_thumb" + android:layout_width="80dp" + android:layout_height="60dp" + android:scaleType="centerCrop" + android:layout_marginTop="6dp" + android:contentDescription="Log photo" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/tv_log_detail" + app:layout_constraintStart_toEndOf="@id/tv_log_icon" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginStart="8dp" /> |
