summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin/org/terst
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin/org/terst')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt48
1 files changed, 42 insertions, 6 deletions
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 7c26a01..f101779 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,5 +1,6 @@
package org.terst.nav.track
+import android.media.ExifInterface
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
@@ -50,7 +51,9 @@ import java.io.FileOutputStream
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
+import java.text.SimpleDateFormat
import java.util.Locale
+import java.util.TimeZone
import kotlin.math.abs
/** A notable event extracted from a track for display in the log step-through. */
@@ -72,6 +75,7 @@ class TrackDetailSheet : Fragment() {
// Note-adding state
private var pendingNotePhotoPath: String? = null
+ private var pendingNotePhotoTimestampMs: Long? = null
private var noteDialogPhotoFrame: FrameLayout? = null
private var noteDialogPhotoView: ImageView? = null
private var logEventAdapter: LogEventAdapter? = null
@@ -97,18 +101,20 @@ class TrackDetailSheet : Fragment() {
) { uri: Uri? ->
if (uri != null) {
lifecycleScope.launch {
- val path = withContext(Dispatchers.IO) {
+ val result = withContext(Dispatchers.IO) {
runCatching {
val dir = NavApplication.logbookRepository.photoDir
val dest = File(dir, "log_${System.currentTimeMillis()}.jpg")
requireContext().contentResolver.openInputStream(uri)?.use { inp ->
FileOutputStream(dest).use { out -> inp.copyTo(out) }
}
- dest.absolutePath
+ dest.absolutePath to readExifTimestamp(dest.absolutePath)
}.getOrNull()
}
- if (path != null) {
+ if (result != null) {
+ val (path, ts) = result
pendingNotePhotoPath = path
+ pendingNotePhotoTimestampMs = ts
noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path)))
noteDialogPhotoFrame?.visibility = View.VISIBLE
}
@@ -182,6 +188,7 @@ class TrackDetailSheet : Fragment() {
noteDialogPhotoFrame = dialogView.findViewById(R.id.frame_note_photo)
noteDialogPhotoView = dialogView.findViewById(R.id.iv_note_photo)
pendingNotePhotoPath = null
+ pendingNotePhotoTimestampMs = null
noteDialogPhotoFrame?.visibility = View.GONE
dialogView.findViewById<Button>(R.id.btn_note_camera).setOnClickListener {
@@ -204,6 +211,7 @@ class TrackDetailSheet : Fragment() {
dialogView.findViewById<Button>(R.id.btn_note_remove_photo).setOnClickListener {
pendingNotePhotoPath?.let { File(it).delete() }
pendingNotePhotoPath = null
+ pendingNotePhotoTimestampMs = null
noteDialogPhotoView?.setImageDrawable(null)
noteDialogPhotoFrame?.visibility = View.GONE
}
@@ -215,10 +223,13 @@ class TrackDetailSheet : Fragment() {
val text = etNote.text?.toString()?.trim() ?: ""
val path = pendingNotePhotoPath
if (text.isBlank() && path == null) return@setPositiveButton
- val anchor = track.points.lastOrNull()
+ // Use EXIF timestamp if the photo was taken during this track; otherwise anchor to end.
+ val photoTs = pendingNotePhotoTimestampMs
+ val entryTs = if (photoTs != null && photoTs in track.startMs..track.endMs) photoTs else track.endMs
+ val anchor = if (entryTs < track.endMs) nearestTrackPoint(track.points, entryTs) else track.points.lastOrNull()
NavApplication.logbookRepository.save(
LogEntry(
- timestampMs = track.endMs,
+ timestampMs = entryTs,
text = text.ifBlank { "(photo only)" },
entryType = EntryType.GENERAL,
lat = anchor?.lat,
@@ -227,11 +238,15 @@ class TrackDetailSheet : Fragment() {
)
)
pendingNotePhotoPath = null
- logEventAdapter?.update(buildLogEvents(track))
+ pendingNotePhotoTimestampMs = null
+ val newEvents = buildLogEvents(track)
+ logEventAdapter?.update(newEvents)
+ refreshNoteLayer(newEvents)
}
.setNegativeButton("Cancel") { _, _ ->
pendingNotePhotoPath?.let { File(it).delete() }
pendingNotePhotoPath = null
+ pendingNotePhotoTimestampMs = null
}
.show()
}
@@ -354,6 +369,27 @@ class TrackDetailSheet : Fragment() {
private fun nearestTrackPoint(points: List<TrackPoint>, timestampMs: Long): TrackPoint? =
points.minByOrNull { abs(it.timestampMs - timestampMs) }
+ /** Updates the note circle layer after a new note is saved, creating the source if needed. */
+ private fun refreshNoteLayer(events: List<LogEvent>) {
+ val notes = events.filter { it.photoPath != null || it.icon == "📝" }
+ val fc = FeatureCollection.fromFeatures(notes.map { Feature.fromGeometry(Point.fromLngLat(it.lon, it.lat)) })
+ trackMap?.style?.let { style ->
+ (style.getSource("notes-detail-source") as? GeoJsonSource)?.setGeoJson(fc)
+ ?: drawNotes(style, notes)
+ }
+ }
+
+ /** Reads EXIF DateTimeOriginal from a JPEG, parsed as device-local time. Returns null on failure. */
+ private fun readExifTimestamp(filePath: String): Long? = runCatching {
+ val exif = ExifInterface(filePath)
+ val dt = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
+ ?: exif.getAttribute(ExifInterface.TAG_DATETIME)
+ ?: return@runCatching null
+ SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US)
+ .apply { timeZone = TimeZone.getDefault() }
+ .parse(dt)?.time
+ }.getOrNull()
+
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) }