diff options
| author | Claude <noreply@anthropic.com> | 2026-05-26 09:13:38 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-26 09:13:38 +0000 |
| commit | 9f298ac64183bcf54d2531e0363b205a04e3366a (patch) | |
| tree | 818e63d9fee6b628bf34afc69969e3ffcec1cd2c /android-app | |
| parent | f564e06338f846930300c366774efb97c042a5fb (diff) | |
Fix gallery photo timestamp and map pin not updating after note save
Gallery photos were always anchored to track.endMs with the final GPS
position. Now reads EXIF DateTimeOriginal (parsed in device local time)
after copying the file; if the timestamp falls within the track window
the note is positioned at the correct time and nearest track point.
Notes taken outside the window fall back to the previous behaviour.
Map note layer was never refreshed after saving â drawNotes() ran once
at style-load time and the GeoJsonSource was never updated. Added
refreshNoteLayer() which updates the existing source or creates it if
no notes existed before.
https://claude.ai/code/session_01KXYBuAHZkvv63DeUG6XaZD
Diffstat (limited to 'android-app')
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt | 48 |
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) } |
