Files
rippr/docs/v3/V3-14-gpx-interop.md
uhryniuk a1e99684e0 V3-14: GPX <type> from activity (code only)
gpxActivityType() maps Activity to Strava/Garmin's <trk><type> vocabulary; Activity.other omits the element rather than writing it empty. The ticket's real acceptance criteria are device/account verification (Strava, Garmin Connect, Google Earth import) which can't happen in this environment -- marked partially done and paired with V3-13's real-ride work in the Outcome section.
2026-08-17 19:17:49 -05:00

4.3 KiB

V3-14 — GPX interoperability

Phase Quality · Depends on V3-01 for <type> · Size S · Status Partially done

Goal

Confirm an exported ride actually imports into Strava, Garmin Connect and Google Earth — and add the activity type so it lands as the right kind of activity.

Context

Export is well tested: 15 tests, parsed with a real XML parser, and byte-identical to the Kotlin original under the parity harness. But every one of those tests proves structural validity, and structural validity does not mean a consumer accepts the file. That gap has been open since v2.

Design

Two parts.

Verification — export a real ride and import it into each of Strava, Garmin Connect and Google Earth. Record what each does with pauses, elevation and timestamps. Pauses are the interesting case: <trkseg> per segment is the correct GPX representation, but consumers vary in whether they honour it.

<type> on <trk> — Strava and Garmin read it to decide the activity. Without it a bicycle ride may import as a run. Needs V3-01's activity, mapped to each consumer's vocabulary (Strava uses ride, run, and so on).

Implementation

  1. Add <type> to the <trk> element, from the trip's activity
  2. Map the internal enum to GPX conventions; document the mapping in the code
  3. Export a real multi-segment ride and import it into all three consumers
  4. Write the findings into this file, including anything that surprises

Acceptance criteria

  • A real ride imports into Strava with the right activity type
  • It imports into Garmin Connect
  • It opens in Google Earth with the path in the right place
  • Pause behaviour in each consumer is documented, whatever it turns out to be
  • Existing export tests still pass, including the byte-identical parity check — this one will need updating, since <type> changes the output

Tests

  • <type> present and correct per activity
  • Absent, not empty, when the activity is other
  • The parity harness will now differ from Kotlin here. That is expected and correct — update its expectation and note why, rather than dropping the check.

Risks

Silently breaking the parity harness by changing export output. Update it deliberately.

Out of scope

GPX import into Rippr. FIT and TCX formats.

Outcome

The code half is done; the verification half is explicitly not, and can't be from here.

gpxActivityType(Activity) maps the internal enum to GPX <trk><type> values chosen to match Strava's and Garmin Connect's published import vocabularies (motorcycling, cycling, skateboarding, running, walking). Activity.other maps to null, and gpx() omits the element entirely rather than writing <type/> — an empty element would claim "this ride has a type, and it's nothing," which isn't the same fact as "no type was recorded." Scooter reuses motorcycling: GPX has no dedicated vocabulary entry for it and that is the closer of the two categories a consumer actually offers.

No existing test needed updating, and there is no byte-identical Kotlin-comparison harness for GPX in this repo to speak of — the parity harness described in the original port plan compares pure-logic modules (geo, telemetry, ride statistics) against fixtures, not a live GPX diff against a Kotlin process. <type> is a pure addition; the nine existing GPX tests assert specific element counts and positions that a new sibling element doesn't disturb, confirmed by running them unchanged. Three new tests added: <type> present and correct for a mapped activity, absent (not empty) for other, and every Activity value covered without throwing.

Not done, and cannot be done in this environment: the ticket's actual acceptance criteria are entirely device/account verification — export a real ride and import it into Strava, Garmin Connect, and Google Earth; confirm the activity type lands correctly; document how each consumer treats <trkseg> boundaries at a pause. None of that is reachable without real accounts on those services and a phone to generate a real multi- segment ride. This ticket should be reopened for that verification pass once V3-13's real-ride work happens — the two naturally pair, since V3-13 already requires an actual ride to exist. flutter analyze clean; full suite green (257 tests, up from 254).