Skip to content

Mapping — TCX

Source: a TCX file (Garmin Training Center XML, TrainingCenterDatabase/v2 — Garmin Connect, Polar legacy exports, MapMyRun, and any other TCX v2 exporter). Pillars: A + B. Mapper: mapTcxmapTcx(xml) → { records, warnings }, one Session per <Activity>. Where GPX is pure telemetry, TCX also carries structure: laps.

Structural correspondence

TCXOpenBody
<Activity Sport="…">Session (Runningrunning, Bikingcycling; TCX’s only other value, Other, round-trips as tcx:other)
<Activity><Id>clientRecordId (§7.1 — the activity’s key in every TCX producer)
<Lap>a continuous WorkUnit in Session.workUnits — time (TotalTimeSeconds), distance (DistanceMeters), energy (Calories), own startTime from the StartTime attribute
<Lap><Intensity>Resting</Intensity>setRole: "tcx:resting" (namespaced — the core setRole vocab has no rest token); Active is the TCX default and is not emitted
AverageHeartRateBpm / MaximumHeartRateBpm per lapinterval quantity aggregates (heart_rate_mean/_max) with derivedFrom → the HR stream when one exists
Trackpoint Position/AltitudeMetersone multi-channel location sampleArray (lat/lon/alt); a GPS dropout becomes a [null, null, alt] row
Trackpoint HeartRateBpm / Cadence / TPX <Watts>single-channel sampleArray Measurements (heart_rate, cadence, power)
<Creator><Name>provenance.device.model (no manufacturer is stated separately, so none is fabricated)

Input (excerpt of examples/tcx/tcx-sample.tcx)

<Activity Sport="Running">
<Id>2010-06-26T10:06:11Z</Id>
<Lap StartTime="2010-06-26T10:06:11Z">
<TotalTimeSeconds>15.0</TotalTimeSeconds>
<DistanceMeters>50.0</DistanceMeters>
<Calories>4</Calories>
<AverageHeartRateBpm><Value>133</Value></AverageHeartRateBpm>
<MaximumHeartRateBpm><Value>138</Value></MaximumHeartRateBpm>
<Intensity>Active</Intensity>
<Track>
<Trackpoint>
<Time>2010-06-26T10:06:11Z</Time>
<Position>
<LatitudeDegrees>52.148746</LatitudeDegrees>
<LongitudeDegrees>4.494590</LongitudeDegrees>
</Position>
<AltitudeMeters>3.4</AltitudeMeters>
<HeartRateBpm><Value>128</Value></HeartRateBpm>
<Cadence>83</Cadence>
<Extensions><ns3:TPX><ns3:Watts>245</ns3:Watts></ns3:TPX></Extensions>
</Trackpoint>
<!-- … -->
</Track>
</Lap>
<!-- second Lap, Intensity Resting … -->
<Creator xsi:type="Device_t"><Name>Garmin Forerunner 305</Name></Creator>
</Activity>

Output (selected records)

A lap’s average HR → an aggregate with lineage to the stream:

{
"id": "tcx-1-lap-1-hr-mean",
"recordType": "Measurement",
"subject": "subj-001",
"type": "heart_rate_mean",
"quantity": 133,
"unit": "/min",
"startTime": "2010-06-26T10:06:11Z",
"endTime": "2010-06-26T10:06:26Z",
"provenance": { "method": "algorithm", "sourceApp": "tcx", "device": { "model": "Garmin Forerunner 305" }, "algorithm": { "name": "tcx-lap-summary", "version": "TrainingCenterDatabase/v2" } },
"links": [{ "type": "derivedFrom", "ref": "tcx-1-hr" }]
}

The activity → a Session whose laps are workUnits:

{
"id": "tcx-1",
"recordType": "Session",
"subject": "subj-001",
"clientRecordId": "2010-06-26T10:06:11Z",
"disciplines": ["running"],
"intent": "train",
"startTime": "2010-06-26T10:06:11Z",
"endTime": "2010-06-26T10:06:41Z",
"provenance": { "method": "sensor", "sourceApp": "tcx", "device": { "model": "Garmin Forerunner 305" } },
"links": [
{ "type": "measuredBy", "ref": "tcx-1-route" },
{ "type": "measuredBy", "ref": "tcx-1-hr" },
{ "type": "measuredBy", "ref": "tcx-1-cadence" },
{ "type": "measuredBy", "ref": "tcx-1-power" }
],
"workUnits": [
{
"id": "tcx-1-lap-1",
"recordType": "WorkUnit",
"scoring": "continuous",
"performance": {
"time": { "absolute": { "value": 15, "unit": "s" } },
"distance": { "absolute": { "value": 50, "unit": "m" } },
"energy": { "absolute": { "value": 4, "unit": "kcal" } }
},
"startTime": "2010-06-26T10:06:11Z"
},
{
"id": "tcx-1-lap-2",
"recordType": "WorkUnit",
"scoring": "continuous",
"performance": {
"time": { "absolute": { "value": 20, "unit": "s" } },
"distance": { "absolute": { "value": 65, "unit": "m" } },
"energy": { "absolute": { "value": 6, "unit": "kcal" } }
},
"startTime": "2010-06-26T10:06:26Z",
"setRole": "tcx:resting"
}
]
}

Notes

  • Multiple <Activity> elements → multiple Sessions (tcx-1, tcx-2, …); the legs of a <MultiSportSession> are plain <Activity> elements and map the same way.
  • <Id> is an identifier, not a timestamp — most producers happen to set it to the start time, so it serves as a start-time fallback only when it actually parses as a date.
  • A Courses- or Workouts-only file (no <Activity>) maps to [] — reported gracefully as an empty document rather than throwing.
  • Parsing is the same namespace-prefix-tolerant, zero-dependency technique as GPX (<ns3:Watts> and <Watts> both match) — browser- and node-safe.