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

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 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

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

  • 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 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).