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>
4.3 KiB
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
<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
Zsuffix —Location.timeis 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
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
- Create
export/GpxWriter.ktandexport/GeoJsonWriter.kt. - Group points by
segmentId, preservingsegmentId, idorder. - ISO 8601 UTC formatting via
java.time.Instant. - XML escaping for all interpolated text.
- Build with
StringBuilder— a full XML DOM is unnecessary for this shape. - Unit tests.
Acceptance criteria
- GPX is well-formed XML and validates against the GPX 1.1 schema
<trkseg>count equals segment count- Every raw point is present — no decimation (the T14 guard)
- Timestamps are UTC with
Z - A trip name containing
&,<,"produces valid XML - GeoJSON coordinates are
[lon, lat, ele] - 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
Stringis a few MB. Acceptable, but if trips grow much larger, stream to the output instead.
Out of scope
File writing, sharing, SAF (all T16).