diff options
Diffstat (limited to 'android-app/app/src/main')
| -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) } |
