summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-04 07:10:41 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-04-04 07:10:41 +0000
commit9f01ddfba17dda7fb386e83f007c671fec6d5b8e (patch)
tree4ed5daadf94c168785595d18b70ba4a7d3d96ce3
parenta8d851e5bfb78b065f10d457bf3ce8f2c771bb4c (diff)
feat(ui): surface trip planning and reports in instrument sheet
-rw-r--r--.agent/worklog.md174
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt14
-rw-r--r--android-app/app/src/main/res/layout/layout_instruments_sheet.xml54
-rw-r--r--test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt14
4 files changed, 118 insertions, 138 deletions
diff --git a/.agent/worklog.md b/.agent/worklog.md
index 7a4467f..95bd266 100644
--- a/.agent/worklog.md
+++ b/.agent/worklog.md
@@ -1,154 +1,64 @@
# SESSION_STATE.md
## Current Task Goal
-Section 7.3 AIS display — COMPLETE (2026-03-15): AIS integrated into ViewModel, MapFragment, and MainActivity
+Section 7.4 Trip Reporting & Enhanced Tracks — COMPLETE (2026-04-04)
-## Verified (2026-03-15)
-- All 41 tests GREEN: 22 GPS/NMEA + 16 AIS + 3 ViewModel AIS (via /tmp/ais-vm-test-runner/)
-- NmeaParser extended with MWV (wind), DBT (depth), HDG/HDM (heading) parsers
-- Sensor data classes added: WindData, DepthData, HeadingData
-- NmeaStreamManager added for TCP stream management
+## Verified (2026-04-04)
+- Build Successful: `android-app/gradlew assembleDebug` passed.
+- UI Layout: `activity_main.xml` refactored to LinearLayout to prevent BottomNav overlap.
+- Track Differentiation: Active (solid) vs Past (dotted) tracks verified in `MapHandler.kt`.
+- Pre-Trip Logic: Sail suggestions reefing logic verified in `PreTripReportGenerator.kt`.
## Completed Items
-### [APPROVED] GpsPosition data class
+### [APPROVED] Trip Narrative Generator + Multi-Style AI (2026-04-04)
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt`: Logic for distance, speed, and stylized narratives (Professional, Adventurous, Journal, Pirate).
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt`: State management for narrative generation.
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt`: UI for viewing and selecting narrative styles.
+
+### [APPROVED] Pre-Trip Planning & Sail Suggestions (2026-04-04)
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt`: Routing and reefing suggestions based on wind/waves.
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt`: Domain models for boat profiles and suggestions.
+- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt`: UI for generating and viewing pre-trip plans.
+
+### [APPROVED] Enhanced Map & Track Visualization (2026-04-04)
+- `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`: `updateTrackLayer` now supports active and past tracks with distinct styles.
+- `android-app/app/src/main/res/layout/activity_main.xml`: Refactored to vertical LinearLayout root to stabilize BottomNav and FAB placement.
+- `android-app/app/src/main/res/layout/layout_instruments_sheet.xml`: Added "TRIP REPORTS & PLANNING" section for direct access from main view.
+- `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`: Wired Pre-Trip and Trip Report buttons from instrument sheet; added `showReport` navigation helper.
+- `android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`: Added `pastTracks` storage.
+- `test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`: Synchronized with main app implementation.
+
+### [APPROVED] GpsPosition data class (2026-03-15)
- File: `app/src/main/kotlin/org/terst/nav/gps/GpsPosition.kt`
-- Package: `org.terst.nav.gps`
- Fields: latitude, longitude, sog (knots), cog (degrees true), timestampMs
-### [APPROVED] GpsProvider / GpsListener interfaces
-- File: `app/src/main/kotlin/org/terst/nav/gps/GpsProvider.kt`
-- `GpsProvider`: start/stop, position property, addListener/removeListener
-- `GpsListener`: onPositionUpdate(GpsPosition), onFixLost()
-
-### [APPROVED] DeviceGpsProvider
-- File: `app/src/main/kotlin/org/terst/nav/gps/DeviceGpsProvider.kt`
+### [APPROVED] DeviceGpsProvider (2026-03-15)
- Wraps `LocationManager` with `GPS_PROVIDER`
-- Default interval: 1000ms (1 Hz); configurable via constructor
-- SOG: Location.speed (m/s) × 1.94384 → knots
-- COG: Location.bearing (degrees true, no conversion)
- Fix-lost timer: fires `onFixLost()` after 10s with no update
-- Thread-safe listener list (synchronized)
-- Android dependency: Context, LocationManager, Handler — device only
-
-### [APPROVED] FakeGpsProvider + GpsProviderTest (9 tests — all GREEN)
-- File: `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt`
-- No Android dependencies — pure JVM
-- Tests:
- - `start sets started to true`
- - `stop sets started to false`
- - `listener receives position update`
- - `listener notified of fix lost`
- - `multiple listeners all receive position update`
- - `multiple listeners all notified of fix lost`
- - `removing listener stops notifications`
- - `position property reflects last simulated position`
- - `SOG conversion sanity check - 1 mps is approximately 1_94384 knots`
-## Build Notes
-- `app/build` and `app/.kotlin/sessions` are root-owned from a prior run.
- Tests were verified via direct `kotlinc` + `JUnitCore` invocation (all 9 pass).
- Full Gradle build requires fixing directory permissions: `chown -R www-data:www-data app/build app/.kotlin`
-- Pre-existing XML layout error in `activity_main.xml:300` (unrelated to GPS work)
-
-### [APPROVED] NmeaParser — RMC parser
-- File: `app/src/main/kotlin/org/terst/nav/nmea/NmeaParser.kt`
+### [APPROVED] NmeaParser — RMC parser (2026-03-15)
- Parses any `*RMC` sentence (GP, GN, etc.)
-- Returns `null` for void status (V), malformed input, wrong sentence type
-- SOG/COG default to 0.0 when fields are empty
-- Latitude: positive = North, negative = South
-- Longitude: positive = East, negative = West
-- Timestamp: HHMMSS + DDMMYY → Unix epoch millis UTC (YY < 70 → 2000+YY)
-- No Android dependencies — pure JVM
-
-### [APPROVED] GpsPositionTest + NmeaParserTest (22 tests — all GREEN)
-- `app/src/test/kotlin/org/terst/nav/gps/GpsPositionTest.kt` (2 tests)
-- `app/src/test/kotlin/org/terst/nav/nmea/NmeaParserTest.kt` (11 tests)
-- `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt` (9 tests, pre-existing)
-- All verified via direct `kotlinc` (1.9.22) + `JUnitCore` invocation
-
-### [APPROVED] AisVessel data class
-- File: `app/src/main/kotlin/org/terst/nav/ais/AisVessel.kt`
-- Package: `org.terst.nav.ais`
-- Fields: mmsi, name, callsign, lat, lon, sog, cog, heading, vesselType, timestampMs
-- Note: `com/example/androidapp` tree is root-owned; files placed under `org/terst/nav/` (actual project package)
-
-### [APPROVED] CpaCalculator object
-- File: `app/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt`
-- Flat-earth CPA/TCPA algorithm; returns (cpa_nm, tcpa_min)
-- Zero-relative-velocity guard: returns (currentDist, 0.0)
-
-### [APPROVED] AisVdmParser class
-- File: `app/src/main/kotlin/org/terst/nav/nmea/AisVdmParser.kt`
-- Parses !AIVDM/!AIVDO sentences; multi-part reassembly by seqId
-- Type 1/2/3: MMSI, SOG, lon, lat, COG, heading decoded
-- Type 5: MMSI, callsign (7 chars), name (20 chars), vesselType decoded; trailing '@'/' ' trimmed
-- Not-available sentinel handling: SOG=1023→0.0, COG=3600→0.0, lon=0x6791AC0→0.0, lat=0x3412140→0.0
-
-### [APPROVED] AIS Tests (16 tests — all GREEN)
-- `test-runner/src/test/kotlin/org/terst/nav/ais/AisVesselTest.kt` (4 tests)
-- `test-runner/src/test/kotlin/org/terst/nav/ais/CpaCalculatorTest.kt` (4 tests)
-- `test-runner/src/test/kotlin/org/terst/nav/nmea/AisVdmParserTest.kt` (8 tests)
-- Verification harness: `/tmp/ais-test-runner/` (JUnit5, com.example.androidapp package)
-- Production test files also in `android-app/app/src/test/kotlin/org/terst/nav/ais/`
-
-### [APPROVED] AisRepository class
-- File: `app/src/main/kotlin/org/terst/nav/ais/AisRepository.kt`
-- Upserts position targets by MMSI; merges type-5 static data (name/callsign/vesselType)
-- Pending static map: holds type-5 data until position arrives for same MMSI
-- `evictStale(nowMs)`: removes vessels older than `staleTimeoutMs` (default 10 min)
-- `AisDataSource` interface (with Flow<String>) defined in same file
-
-### [APPROVED] AisHubApiService + AisHubVessel
-- File: `app/src/main/kotlin/org/terst/nav/ais/AisHubApiService.kt`
-- Note: placed in `ais/` directory (writable) with package `org.terst.nav.data.api`
-- `AisHubVessel` data class with Moshi `@Json` and `@JsonClass(generateAdapter=true)` annotations
-- `AisHubApiService` Retrofit interface: GET /0/down with lat/lon bounding box params
-
-### [APPROVED] AisHubSource object
-- File: `app/src/main/kotlin/org/terst/nav/ais/AisHubSource.kt`
-- Converts `AisHubVessel` REST response to `AisVessel` domain objects
-- Returns null for non-numeric MMSI, lat, or lon; defaults sog/cog=0.0, heading=511, vesselType=0
-
-### [APPROVED] AIS Repository Tests (11 tests — all GREEN)
-- `app/src/test/kotlin/org/terst/nav/ais/AisRepositoryTest.kt` (7 tests)
-- `app/src/test/kotlin/org/terst/nav/ais/AisHubSourceTest.kt` (4 tests)
-- Harness: `/tmp/ais-repo-test-runner/` (JUnit5, org.terst.nav package, stub annotations for offline build)
-
-### [APPROVED] Section 7.3 AIS display — ViewModel + MapFragment integration (2026-03-15)
-- `MainViewModel.processAisSentence(sentence)` — delegates to AisRepository, updates `_aisTargets` StateFlow
-- `MainViewModel.refreshAisFromInternet(latMin, latMax, lonMin, lonMax, username, password)` — AISHub polling via inline Retrofit; skips if username empty
-- `MainViewModel.aisTargets: StateFlow<List<AisVessel>>` — exposed to observers
-- `AisRepository.ingestVessel(vessel)` — direct insert for internet-sourced vessels
-- `MapFragment.updateAisLayer(style, vessels)` — GeoJSON FeatureCollection, SymbolLayer "ais-vessels"; heading-based iconRotate; zoom-step text (visible at zoom ≥ 12)
-- `MapFragment.addShipArrowImage(style)` — rasterises ic_ship_arrow.xml into style
-- `ic_ship_arrow.xml` — pink arrow vector drawable for AIS icons
-- `MainActivity.startAisHardwareFeed(host, port)` — TCP stub reads !AIVDM lines, forwards to viewModel
-- `MainViewModelTest` — 3 new tests: valid type-1 adds vessel, same MMSI deduped, non-AIS stays empty
-- JVM test harness: `/tmp/ais-vm-test-runner/` (3 tests — all GREEN)
+- HHMMSS + DDMMYY → Unix epoch millis UTC
-### [APPROVED] TrackRepository (2026-03-25)
-- `android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`
-- `test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`
-- `isRecording` flag; `startTrack()` clears + starts; `stopTrack()`; `addPoint()` guards on isRecording; `getPoints()` returns snapshot
-- 8 tests — all GREEN (`TrackRepositoryTest`)
+### [APPROVED] AIS Integration (2026-03-15)
+- `AisVdmParser.kt`: Parses !AIVDM/!AIVDO (Types 1, 2, 3, 5).
+- `AisRepository.kt`: Upserts vessels and merges static data.
+- `AisHubSource.kt`: Integration with AISHub REST API.
+- `MapFragment`: GeoJSON layer for AIS vessels with rotation and labels.
-### [APPROVED] Track ViewModel + Map overlay + Record FAB (2026-03-25)
-- `MainViewModel`: TrackRepository wired in; exposes `isRecording: StateFlow<Boolean>`, `trackPoints: StateFlow<List<TrackPoint>>`; `startTrack()`, `stopTrack()`, `addGpsPoint(lat, lon, sogKnots, cogDeg)`
-- `MapHandler.updateTrackLayer(style, points)`: lazy LineLayer init; red (#E53935) 3dp polyline from List<TrackPoint>
-- `MainActivity`: stores `loadedStyle`; GPS flow feeds `viewModel.addGpsPoint()` (m/s→knots); observes `trackPoints` → `mapHandler.updateTrackLayer()`; observes `isRecording` → FAB icon toggle (ic_track_record / ic_close)
-- `activity_main.xml`: `fab_record_track` FAB anchored top|end of bottom nav
-- `drawable/ic_track_record.xml`: red dot record icon
+### [APPROVED] Track Recording (2026-03-25)
+- `MainViewModel`: TrackRepository wired; `addGpsPoint` enriches with environmental data.
+- `MainActivity`: FAB for toggling recording; MapHandler for rendering.
## Next 3 Specific Steps
-1. **Persist track to GPX/Room** — export recorded track as GPX file or store in Room DB
-2. **Track stats in Log tab** — show elapsed time, distance, avg SOG while recording
-3. **AnchorWatchHandler UI** — wire `AnchorWatchHandler` fully into SafetyFragment (currently stub)
+1. **Unit Tests for Trip Generators** — Add JVM tests in `test-runner` for `TripReportGenerator` and `PreTripReportGenerator`.
+2. **Persist track to Room** — Replace in-memory `pastTracks` with a Room database for persistence across sessions.
+3. **AnchorWatchHandler UI** — Re-integrate `AnchorWatchHandler` UI into `activity_main.xml` and wire it to the Safety Dashboard.
## Scripts Added
-- `test-runner/` — standalone Kotlin/JVM Gradle project; runs all 22 GPS/NMEA tests without Android SDK
- - Command: `cd /workspace/nav/test-runner && GRADLE_USER_HOME=/tmp/gradle-home ./gradlew test`
+- `test-runner/` — standalone Kotlin/JVM Gradle project for fast test cycles.
## Process Improvements
-- Gradle builds blocked by Android SDK requirement; added `test-runner/` JVM-only subproject as reliable test runner
-- All 22 tests verified GREEN via `test-runner/` JVM project (2026-03-14) \ No newline at end of file
+- Stabilized UI layout by moving navigation out of the `CoordinatorLayout`.
+- Duplicated core logic into `test-runner` to bypass Android SDK requirements for logic testing.
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 0f2eb91..3f09309 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
@@ -93,6 +93,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
setupBottomNavigation()
setupHandlers()
setupMap()
+
+ findViewById<MaterialButton>(R.id.btn_nav_pretrip).setOnClickListener {
+ showReport(org.terst.nav.tripreport.PreTripReportFragment())
+ }
+
+ findViewById<MaterialButton>(R.id.btn_nav_tripreport).setOnClickListener {
+ showReport(org.terst.nav.tripreport.TripReportFragment())
+ }
fabRecordTrack.setOnClickListener {
if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack()
@@ -162,6 +170,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
fragmentContainer.visibility = View.GONE
}
+ private fun showReport(fragment: androidx.fragment.app.Fragment) {
+ bottomSheetBehavior.isHideable = true
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ showOverlay(fragment)
+ }
+
override fun onActivateMob() {
lifecycleScope.launch {
LocationService.locationFlow.firstOrNull()?.let { gpsData ->
diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
index a6b74b0..16410c0 100644
--- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml
@@ -159,8 +159,7 @@
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/label_conditions"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent">
+ app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="0dp"
@@ -197,4 +196,55 @@
</LinearLayout>
+ <!-- Reports Section -->
+ <View
+ android:id="@+id/reports_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/md_theme_outline"
+ android:layout_marginTop="20dp"
+ app:layout_constraintTop_toBottomOf="@id/conditions_row"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextView
+ android:id="@+id/label_reports"
+ style="@style/InstrumentLabel"
+ android:text="TRIP REPORTS &amp; PLANNING"
+ android:layout_marginTop="12dp"
+ app:layout_constraintTop_toBottomOf="@id/reports_divider"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <LinearLayout
+ android:id="@+id/reports_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="12dp"
+ app:layout_constraintTop_toBottomOf="@id/label_reports"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_nav_pretrip"
+ style="@style/Widget.Material3.Button.TonalButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="PRE-TRIP PLAN"
+ android:layout_marginEnd="8dp"
+ android:textSize="12sp" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_nav_tripreport"
+ style="@style/Widget.Material3.Button.TonalButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="GENERATE REPORT"
+ android:textSize="12sp" />
+
+ </LinearLayout>
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
index 7953822..85dd2dd 100644
--- a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
+++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
@@ -5,22 +5,28 @@ class TrackRepository {
var isRecording: Boolean = false
private set
- private val points = mutableListOf<TrackPoint>()
+ private val activePoints = mutableListOf<TrackPoint>()
+ private val pastTracks = mutableListOf<List<TrackPoint>>()
fun startTrack() {
- points.clear()
+ activePoints.clear()
isRecording = true
}
fun stopTrack() {
+ if (isRecording && activePoints.isNotEmpty()) {
+ pastTracks.add(activePoints.toList())
+ }
isRecording = false
}
fun addPoint(point: TrackPoint): Boolean {
if (!isRecording) return false
- points.add(point)
+ activePoints.add(point)
return true
}
- fun getPoints(): List<TrackPoint> = points.toList()
+ fun getPoints(): List<TrackPoint> = activePoints.toList()
+
+ fun getPastTracks(): List<List<TrackPoint>> = pastTracks.toList()
}