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>
This commit is contained in:
118
rippr-src/docs/v2/15-export-format.md
Normal file
118
rippr-src/docs/v2/15-export-format.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user