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.
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
- Add
<type>to the<trk>element, from the trip's activity - Map the internal enum to GPX conventions; document the mapping in the code
- Export a real multi-segment ride and import it into all three consumers
- 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).