From e34df09b96f4ded3c25c5a14a0332a874205b64a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 05:13:48 +0000 Subject: Add notes to finished tracks, single Nav voice, fix speed coloring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrackDetailSheet: + Add note button opens a dialog with text, camera, and gallery. Entry is saved with timestampMs=track.endMs so it lands inside the track's time window and appears automatically in the event log. logEventAdapter.update() refreshes the list after save. - TripReportGenerator: removed NarrativeStyle enum and 4-style branch. generateNarrative() now emits a concise PASSAGE header (date, time range, duration), stats line (nm / avg kt / max kt), optional conditions (seas, temp), and a timestamped deck log. Fragment and layout updated to remove the ChipGroup style picker. - TrackColors: replaced Expression.get("color") — which silently fails for lineColor on LineLayer in MapLibre Android 11.x — with Expression.step(Expression.get("speed"), ...) that maps the numeric "speed" property directly to color literals. "color" string property removed from speedSegments() features. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../main/kotlin/org/terst/nav/track/TrackColors.kt | 26 ++-- .../kotlin/org/terst/nav/track/TrackDetailSheet.kt | 143 ++++++++++++++++++++- .../org/terst/nav/tripreport/TripReportFragment.kt | 20 +-- .../terst/nav/tripreport/TripReportGenerator.kt | 114 ++++++++-------- .../terst/nav/tripreport/TripReportViewModel.kt | 12 +- .../src/main/res/layout/dialog_add_track_note.xml | 69 ++++++++++ .../src/main/res/layout/fragment_trip_report.xml | 62 +-------- .../main/res/layout/layout_track_detail_sheet.xml | 35 +++-- .../nav/tripreport/TripReportGeneratorTest.kt | 32 +++-- 9 files changed, 340 insertions(+), 173 deletions(-) create mode 100644 android-app/app/src/main/res/layout/dialog_add_track_note.xml (limited to 'android-app') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt index 292240a..5e5e85d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt @@ -5,17 +5,9 @@ import org.maplibre.geojson.Feature import org.maplibre.geojson.LineString import org.maplibre.geojson.Point -private fun speedToHex(speedKt: Double): String = when { - speedKt >= 13 -> "#F44336" // red - speedKt >= 10 -> "#FFC107" // amber - speedKt >= 7 -> "#4CAF50" // green - speedKt >= 4 -> "#00BCD4" // cyan - else -> "#2196F3" // blue -} - /** * Breaks [points] into consecutive 2-point LineString Features. - * Each feature carries "speed" (knots) and "color" (CSS hex) properties. + * Each feature carries a numeric "speed" property (knots) for data-driven coloring. */ fun speedSegments(points: List): List = (0 until points.size - 1).map { i -> @@ -27,12 +19,20 @@ fun speedSegments(points: List): List = )) ).apply { addNumberProperty("speed", sog) - addStringProperty("color", speedToHex(sog)) } } /** - * Returns the lineColor expression for speed-colored segments. - * Reads the pre-computed "color" string property from each feature. + * Returns a step expression mapping the "speed" numeric property to a color. + * Uses Expression.step() which reads the numeric property directly, avoiding + * the silent failure of Expression.get("color") for string properties on LineLayer + * in MapLibre Android 11.x. */ -fun speedColorExpression(): Expression = Expression.get("color") +fun speedColorExpression(): Expression = Expression.step( + Expression.get("speed"), + Expression.literal("#2196F3"), // < 4 kt: blue + Expression.stop(4, Expression.literal("#00BCD4")), // 4–7 kt: cyan + Expression.stop(7, Expression.literal("#4CAF50")), // 7–10 kt: green + Expression.stop(10, Expression.literal("#FFC107")), // 10–13 kt: amber + Expression.stop(13, Expression.literal("#F44336")) // 13+ kt: red +) 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 25e7b15..1d92e18 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,17 +1,31 @@ package org.terst.nav.track +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent import android.graphics.BitmapFactory +import android.net.Uri import android.os.Bundle +import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.geometry.LatLngBounds @@ -28,8 +42,11 @@ import org.maplibre.geojson.LineString import org.maplibre.geojson.Point import org.terst.nav.NavApplication import org.terst.nav.R +import org.terst.nav.logbook.EntryType +import org.terst.nav.logbook.LogEntry import org.terst.nav.ui.MainViewModel import java.io.File +import java.io.FileOutputStream import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -53,6 +70,52 @@ class TrackDetailSheet : Fragment() { private lateinit var trackMapView: MapView private var trackMap: MapLibreMap? = null + // Note-adding state + private var pendingNotePhotoPath: String? = null + private var noteDialogPhotoFrame: FrameLayout? = null + private var noteDialogPhotoView: ImageView? = null + private var logEventAdapter: LogEventAdapter? = null + private var currentTrack: SavedTrack? = null + + private val noteCameraLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val path = pendingNotePhotoPath ?: return@registerForActivityResult + val bm = BitmapFactory.decodeFile(path) + if (bm != null) noteDialogPhotoView?.setImageBitmap(bm) + else noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path))) + noteDialogPhotoFrame?.visibility = View.VISIBLE + } else { + pendingNotePhotoPath?.let { File(it).delete() } + pendingNotePhotoPath = null + } + } + + private val noteGalleryLauncher = registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + if (uri != null) { + lifecycleScope.launch { + val path = 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 + }.getOrNull() + } + if (path != null) { + pendingNotePhotoPath = path + noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path))) + noteDialogPhotoFrame?.visibility = View.VISIBLE + } + } + } + } + companion object { fun newInstance() = TrackDetailSheet() } @@ -65,6 +128,7 @@ class TrackDetailSheet : Fragment() { parentFragmentManager.popBackStack() return } + currentTrack = track view.findViewById(R.id.btn_back).setOnClickListener { parentFragmentManager.popBackStack() @@ -88,9 +152,14 @@ class TrackDetailSheet : Fragment() { val rv = view.findViewById(R.id.rv_log_entries) rv.layoutManager = LinearLayoutManager(requireContext()) rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - rv.adapter = LogEventAdapter(events) { event -> + logEventAdapter = LogEventAdapter(events) { event -> trackMap?.easeCamera(CameraUpdateFactory.newLatLng(LatLng(event.lat, event.lon)), 300) } + rv.adapter = logEventAdapter + + view.findViewById(R.id.btn_add_note).setOnClickListener { + showAddNoteDialog(track) + } trackMapView = view.findViewById(R.id.track_map_view) trackMapView.onCreate(savedInstanceState) @@ -106,6 +175,67 @@ class TrackDetailSheet : Fragment() { } } + private fun showAddNoteDialog(track: SavedTrack) { + val dialogView = LayoutInflater.from(requireContext()) + .inflate(R.layout.dialog_add_track_note, null) + val etNote = dialogView.findViewById(R.id.et_note_text) + noteDialogPhotoFrame = dialogView.findViewById(R.id.frame_note_photo) + noteDialogPhotoView = dialogView.findViewById(R.id.iv_note_photo) + pendingNotePhotoPath = null + noteDialogPhotoFrame?.visibility = View.GONE + + dialogView.findViewById