Files
samplez/rippr-src/docs/v2/15-export-format.md
uhryniuk 280fd7f988 Add Rippr source snapshot and full-history bundle
Two copies for two jobs. rippr-src/ is a browsable git archive export of
the tracked tree at 2f76983 - no build outputs, no local.properties, no
nested .git - which is convenient to read in gitea but carries no history
and will drift.

rippr-full-history.bundle is the real backup: all 18 commits, verified as
"records a complete history" and test-cloned before committing. This
matters because ~/dojo/rippr has no git remote and otherwise exists only
on one machine.

rippr-src/SNAPSHOT.md explains the difference and how to restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:30:25 -05:00

119 lines
4.3 KiB
Markdown

# T15 — GPX + GeoJSON writers
**Phase** 6 · **Depends on** T04 · **Status** Done
## Goal
Serialise a trip to GPX 1.1 and GeoJSON as pure strings, with pause structure preserved.
Done when a generated GPX opens correctly in Strava, Garmin Connect, or Google Earth, and
both formats are covered by unit tests running without a device.
## Context
Export is the hedge against Rippr's own map ever being limiting — it makes the data
portable into tooling that already exists. It is also cheap: pure string generation with
no Android dependency, fully unit-testable, and parallelisable with the entire UI track.
Everything needed is already recorded: `latitude`, `longitude`, `altitudeM`, `timestamp`,
`speedKmh` per point, and segments delimiting pauses.
## Design
### GPX 1.1
```xml
<gpx version="1.1" creator="Rippr" xmlns="http://www.topografix.com/GPX/1/1">
<metadata><name>…</name><time>…</time></metadata>
<trk>
<name>…</name>
<trkseg> <!-- one per Segment -->
<trkpt lat="51.04470" lon="-114.07190">
<ele>1045.0</ele>
<time>2026-08-10T13:48:50Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
```
**One `<trkseg>` per Segment** is the whole reason the Segment model exists — it is
exactly how GPX represents a recording gap, so pauses survive the round-trip into Strava
or Garmin rather than becoming a straight line across town.
Speed goes in a `<extensions>` block. Speed is not part of core GPX 1.1; consumers that
do not understand the extension ignore it, which is the correct degradation.
Formatting rules:
- Timestamps ISO 8601 **UTC with a `Z` suffix** — `Location.time` is already UTC epoch
millis, so no zone conversion, and local time here would be silently wrong
- Coordinates at 7 decimal places (~1 cm — beyond GPS precision but standard practice)
- Elevation at 1 decimal
- **XML-escape** any user-supplied trip name; a name containing `&` or `<` otherwise
produces a malformed file
### GeoJSON
`FeatureCollection`, one `LineString` `Feature` per segment, with trip metadata in
`properties`. Coordinates are `[lon, lat, ele]` — **longitude first**, which is the
opposite order to GPX and the most common mistake in GeoJSON output.
### API
```kotlin
object GpxWriter {
fun write(trip: Trip, segments: List<Segment>, points: List<TrackPoint>): String
}
object GeoJsonWriter {
fun write(trip: Trip, segments: List<Segment>, points: List<TrackPoint>): String
}
```
Pure functions over already-loaded data — no repository, no context, no I/O.
## Implementation
1. Create `export/GpxWriter.kt` and `export/GeoJsonWriter.kt`.
2. Group points by `segmentId`, preserving `segmentId, id` order.
3. ISO 8601 UTC formatting via `java.time.Instant`.
4. XML escaping for all interpolated text.
5. Build with `StringBuilder` — a full XML DOM is unnecessary for this shape.
6. Unit tests.
## Acceptance criteria
- [x] GPX is well-formed XML and validates against the GPX 1.1 schema
- [x] `<trkseg>` count equals segment count
- [x] Every raw point is present — **no decimation** (the T14 guard)
- [x] Timestamps are UTC with `Z`
- [x] A trip name containing `&`, `<`, `"` produces valid XML
- [x] GeoJSON coordinates are `[lon, lat, ele]`
- [x] Empty trip produces a valid document with no track points, not a crash
## Tests
JVM unit tests:
- Parse generated GPX with a real XML parser and assert structure
- Segment count matches; point count matches input exactly
- Timestamp format assertion against a known epoch
- XML-escaping test with a hostile trip name
- GeoJSON coordinate order — explicitly assert lon-first
- Empty and single-point trips
Manual: export a real ride and open it in Google Earth or Strava. This is the acceptance
test that actually matters — schema validity does not guarantee a consumer accepts it.
## Risks / gotchas
- **Coordinate order differs between the two formats.** GPX is lat/lon attributes;
GeoJSON is lon-first arrays. Easy to get backwards, and the result silently plots in
the wrong hemisphere.
- **Decimation must not appear here.** Assert full point counts in tests.
- **Unescaped names produce malformed XML** — the failure is invisible until an import
fails.
- **Memory**: a 21,600-point ride as one `String` is a few MB. Acceptable, but if trips
grow much larger, stream to the output instead.
## Out of scope
File writing, sharing, SAF (all T16).