summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt12
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt1
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt62
-rw-r--r--android-app/app/src/main/res/layout/item_log_entry.xml12
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" />