Files
samplez/rippr-flutter-src/docs/v3/V3-14-gpx-interop.md
uhryniuk 4c634354bd Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221.

The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
2026-08-19 13:36:04 -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).