https://gpx.pub/ns/1The GPX with every version (master GPX)
Rally Route Builder can export a route as one GPX file that carries every version of it. Garmin, Wahoo, Komoot, Ride with GPS and any other app read it as an ordinary route. Route Builder, Rally and other club apps can find the rest inside it, and offer riders the same choices the route's author made.
This page is for anyone writing software that reads these files.
What's in the file
The file is a valid GPX 1.1 document. It validates against the GPX 1.1 schema, because GPX allows other namespaces inside <extensions>, and readers skip what they don't know.
<metadata>holds the route's device name and a description. The description ends with a sentence saying how many more versions are inside, plus "Route data © OpenStreetMap contributors".<wpt>elements are the main route's named stops and lap starts, as in any Route Builder GPX.<trk>is the main route: one track and one segment, without timestamps. A device that knows nothing else rides this. There's only one track because Garmin Connect has been reported to import only the first track of a file.<extensions>sits at the end of<gpx>and holds one<rb:route>. That puts everything else after the track, so a streaming reader reaches the track first.
The namespace is https://gpx.pub/ns/1, and that address shows this page. Files written before October 10, 2026 use https://rosser-routes.web.app/ns/gpx/1; readers should accept both. Rally Route Builder writes the prefix rb, but readers should look up the prefix the file declares for that namespace and use it, since another tool may rename it.
<gpx version="1.1" creator="Rally Route Builder" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:rb="https://gpx.pub/ns/1">
<metadata>…</metadata>
<wpt …/>
<trk><name>Sat Café A</name><trkseg>…</trkseg></trk>
<extensions>
<rb:route version="1" id="k3J8…" name="Sat Café">
<rb:choices>
<rb:option id="o-loop" name="Café loop" kind="loop"/>
<rb:laps id="laps" name="Climb" min="1" max="5" default="3"/>
</rb:choices>
<rb:version letter="A" name="Sat Café A" label="Main" options="" laps="laps:3"
reversed="false" distance="42118" ascent="612" descent="609" loop="true">
<rb:line>1…</rb:line>
<rb:mark index="0" name="Start" kind="start"/>
<rb:segment name="Cypress climb" start="812" end="1290"/>
</rb:version>
<rb:version letter="B" …>…</rb:version>
<rb:plan format="routebuilder" version="1">{…}</rb:plan>
</rb:route>
</extensions>
</gpx>
rb:route
| Attribute | Meaning |
|---|---|
version | The format version, 1. A reader should refuse versions it doesn't know. |
id | The route's id in Route Builder. |
name | The route's name. |
rb:choices
These are the choices a rider can make:
rb:option: one for each option that has been drawn, withid,nameandkind. The kind isloop,out-and-back,alternateorbailout.rb:laps: one for each lap loop, withid,name,min,maxand the usual lap count,default.
An app can show these as toggles and steppers, as Route Builder's Export panel does.
rb:version
There is one rb:version for the main route (letter A) and one for each saved version (B, C, …), in that order. Saved versions that have problems are left out, such as one that bails out on a lap the route no longer reaches.
| Attribute | Meaning |
|---|---|
letter | A for the main route, then the saved versions' letters. |
name | The short device name, at most 15 characters, such as Sat Café B. |
label | What the version is, such as Main or Café loop · Climb ×4. |
options | The option ids turned on, separated by spaces. |
laps | Each lap loop's count, as id:count pairs separated by spaces. Every lap loop is listed. |
reversed | true when the version is ridden backwards. |
distance, ascent, descent | Metres, rounded. |
loop | true when the version finishes within 50 m of its start. |
Inside each rb:version:
rb:line: the version's whole line, packed as described below. The line isn't trimmed. Route Builder's own exports end a loop 30 m short of its start, so devices don't stop navigating on the first pass. An app that sends a version to a device can do the same withtrimLoopEndinsrc/model/exporters.ts.rb:mark: the waypoints.indexis a point of the line,nameis shown on devices, andkindisstart,finish,cafe,summit,water,regroup,turnaround,otherorlap.rb:segment: each named segment on this version, withstartandendas points of the line. A segment ridden on every lap appears once for each lap.
rb:plan
The whole route as a Route Builder file: the same JSON as a .routebuilder file (src/model/routefile.ts), written as XML text. It has every junction, section, option, lap loop and saved version. Route Builder opens it as the full route when the GPX is imported. An app can compile any other combination from it with compile in src/model/compile.ts, which needs no network.
The packed line
Lines are packed the way the cloud library stores them (encodeLine and decodeLine in src/model/codec.ts):
- The first character is the format,
1. - Then, for each point, three numbers follow: the change in latitude × 10⁶, the change in longitude × 10⁶, and the elevation field. Each change is from the previous point, and the first point's change is from zero.
- The elevation field is twice the change in elevation in decimetres since the last known elevation. The value
1means the point has no elevation. - Each number is written as in Google's encoded polyline format: zig-zag to unsigned (
n < 0 ? -2n − 1 : 2n), then 5-bit groups from the lowest, each with 32 added if more follow, plus 63, as a character.
That is about 6–8 bytes a point, against about 60 for a <trkpt>.
A decoder, in TypeScript:
export function decodeLine(s: string): [lon: number, lat: number, ele: number][] {
if (!s) return [];
if (s[0] !== '1') throw new Error(`Unknown line format ${s[0]}`);
let at = 1, lat = 0, lon = 0, ele = 0;
const next = () => {
let n = 0, mult = 1, c: number;
do {
c = s.charCodeAt(at++) - 63;
n += (c % 32) * mult;
mult *= 32;
} while (c >= 32);
return n % 2 ? -(n + 1) / 2 : n / 2;
};
const out: [number, number, number][] = [];
while (at < s.length) {
lat += next();
lon += next();
const e = next();
let value = NaN;
if (e !== 1 && e % 2 === 0) {
ele += e / 2;
value = ele / 10;
}
out.push([lon / 1e6, lat / 1e6, value]);
}
return out;
}
Reading one
readMasterGpx(text) in src/model/master.ts returns the plan, the choices and the decoded versions, or null for any other GPX. It works on the text alone, with no XML parser, so it runs anywhere the model does: in Cloud Functions, React Native and JavaScriptCore. isMasterGpx(text) is a quick check.
To offer the route to riders:
- Show the versions by
letterandlabel, withdistanceandascent. - Use the chosen version's points, waypoints and segments as they are, or trim a loop's end before sending it to a device.
- For a combination nobody saved, build a selection from
rb:choicesand compile it from the plan.
When another app changes the track
Other apps that edit a GPX and save it again usually drop extensions they don't know, so the file becomes an ordinary GPX. A few keep them. Then the track and the plan disagree, and the track is what the person meant. readMasterGpx sets trackEdited when the track strays more than 15 m from the main version, or its length differs by more than 80 m or 1%. Route Builder then imports the file as an ordinary GPX. Other readers should do the same.
For Rally
The Rally integration (integrations/rally/) doesn't read these files yet. When it does, a club route made from a master GPX can keep the plan, list the versions in the activity screen, and send the version a rider picks to their device. That work belongs in the Rally patch; the Rally repo isn't edited from here.