diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-04-06 09:41:32 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-04-06 09:41:32 +0000 |
| commit | f9b8801eb52c48986eb0123e8758f7ab78736dec (patch) | |
| tree | 7fbc4d06eaaf92223e1be0cc1d71a4b90f505948 /android-app/app/src/test/kotlin/org/terst | |
| parent | 36af31c9bda660706c3271380b13cba8486c0604 (diff) | |
feat(tracks): persist tracks as GPX in Documents/Nav/ — survives uninstall
GpxSerializer/GpxParser: full round-trip of all TrackPoint fields via
GPX 1.1 + nav: extensions namespace. 13 unit tests.
TrackStorage: MediaStore on API 29+ (no permission needed), direct file
I/O on API 24-28 (WRITE_EXTERNAL_STORAGE maxSdkVersion=28).
TrackRepository: stopTrack() is now suspend, writes GPX and returns
TrackSummary (distance nm, duration, max/avg SOG, avg wind, avg wave).
getPastTracks() lazy-loads from Documents/Nav/ on first call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app/src/test/kotlin/org/terst')
| -rw-r--r-- | android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt | 83 | ||||
| -rw-r--r-- | android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt | 56 |
2 files changed, 139 insertions, 0 deletions
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt new file mode 100644 index 0000000..7ed7ec7 --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt @@ -0,0 +1,83 @@ +package org.terst.nav.track + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class GpxRoundTripTest { + + private fun roundTrip(points: List<TrackPoint>): List<TrackPoint> { + val gpx = GpxSerializer.serialize(points, "Test Track") + return GpxParser.parse(gpx.byteInputStream()) + } + + @Test + fun `round-trip preserves lat and lon`() { + val pt = TrackPoint(lat = 37.8044, lon = -122.2712, sogKnots = 6.1, cogDeg = 247.0) + val result = roundTrip(listOf(pt)).first() + assertEquals(37.8044, result.lat, 0.00001) + assertEquals(-122.2712, result.lon, 0.00001) + } + + @Test + fun `round-trip preserves sog and cog`() { + val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 5.3, cogDeg = 183.0) + val result = roundTrip(listOf(pt)).first() + assertEquals(5.3, result.sogKnots, 0.001) + assertEquals(183.0, result.cogDeg, 0.001) + } + + @Test + fun `round-trip preserves optional nav fields`() { + val pt = TrackPoint( + lat = 1.0, lon = 2.0, sogKnots = 4.0, cogDeg = 90.0, + depthMeters = 12.5, baroHpa = 1013.2, windSpeedKnots = 14.0, + windAngleDeg = 45.0, isTrueWind = true, waveHeightM = 1.2 + ) + val result = roundTrip(listOf(pt)).first() + assertEquals(12.5, result.depthMeters!!, 0.001) + assertEquals(1013.2, result.baroHpa!!, 0.001) + assertEquals(14.0, result.windSpeedKnots!!, 0.001) + assertEquals(45.0, result.windAngleDeg!!, 0.001) + assertTrue(result.isTrueWind) + assertEquals(1.2, result.waveHeightM!!, 0.001) + } + + @Test + fun `round-trip with null optional fields leaves them null`() { + val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 0.0, cogDeg = 0.0) + val result = roundTrip(listOf(pt)).first() + assertNull(result.depthMeters) + assertNull(result.baroHpa) + assertNull(result.windSpeedKnots) + } + + @Test + fun `round-trip preserves multiple points in order`() { + val points = listOf( + TrackPoint(lat = 1.0, lon = 1.0, sogKnots = 1.0, cogDeg = 0.0, timestampMs = 1000L), + TrackPoint(lat = 2.0, lon = 2.0, sogKnots = 2.0, cogDeg = 90.0, timestampMs = 2000L), + TrackPoint(lat = 3.0, lon = 3.0, sogKnots = 3.0, cogDeg = 180.0, timestampMs = 3000L), + ) + val result = roundTrip(points) + assertEquals(3, result.size) + assertEquals(1.0, result[0].lat, 0.00001) + assertEquals(2.0, result[1].lat, 0.00001) + assertEquals(3.0, result[2].lat, 0.00001) + } + + @Test + fun `round-trip preserves timestamp`() { + val ts = 1712345678000L + val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 0.0, cogDeg = 0.0, timestampMs = ts) + val result = roundTrip(listOf(pt)).first() + assertEquals(ts, result.timestampMs) + } + + @Test + fun `track name with special chars is escaped`() { + val gpx = GpxSerializer.serialize(emptyList(), "Track & \"Fun\" <test>") + assertTrue(gpx.contains("Track & "Fun" <test>")) + } +} diff --git a/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt new file mode 100644 index 0000000..2daaf45 --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt @@ -0,0 +1,56 @@ +package org.terst.nav.track + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class TrackSummaryTest { + + private fun pt(lat: Double, lon: Double, sog: Double = 5.0, ts: Long = 0L) = + TrackPoint(lat = lat, lon = lon, sogKnots = sog, cogDeg = 0.0, timestampMs = ts) + + @Test + fun `distance between two points one nm apart`() { + // 1 nautical mile north along the prime meridian ≈ 0.01667° latitude + val a = pt(0.0, 0.0, ts = 0L) + val b = pt(0.016667, 0.0, ts = 60_000L) + val s = summarise(listOf(a, b)) + assertEquals(1.0, s.distanceNm, 0.01) + } + + @Test + fun `duration is last minus first timestamp`() { + val points = listOf(pt(0.0, 0.0, ts = 1_000L), pt(0.0, 0.0, ts = 61_000L)) + assertEquals(60_000L, summarise(points).durationMs) + } + + @Test + fun `max sog picks highest value`() { + val points = listOf(pt(0.0, 0.0, sog = 4.0), pt(0.0, 0.0, sog = 9.2), pt(0.0, 0.0, sog = 6.0)) + assertEquals(9.2, summarise(points).maxSogKt, 0.001) + } + + @Test + fun `avg wind is null when no wind data`() { + val s = summarise(listOf(pt(0.0, 0.0))) + assertNull(s.avgWindKt) + } + + @Test + fun `avg wind averages available readings`() { + val points = listOf( + TrackPoint(0.0, 0.0, 5.0, 0.0, windSpeedKnots = 10.0), + TrackPoint(0.0, 0.0, 5.0, 0.0, windSpeedKnots = 20.0), + ) + assertEquals(15.0, summarise(points).avgWindKt!!, 0.001) + } + + @Test + fun `avg wave height averages available readings`() { + val points = listOf( + TrackPoint(0.0, 0.0, 5.0, 0.0, waveHeightM = 1.0), + TrackPoint(0.0, 0.0, 5.0, 0.0, waveHeightM = 3.0), + ) + assertEquals(2.0, summarise(points).avgWaveHeightM!!, 0.001) + } +} |
