Fix a severe GPX parse slowdown and add route (<rte>) support
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 16s

GPX tracks were silently failing to render for large real-world files:
point_extensions used REXML's "extensions//*" XPath, and the // descendant
axis costs low-single-digit milliseconds per call regardless of subtree
size. Run once per point, that's negligible on small files but ~30s on a
real 8000-point track — long enough to hit a proxy timeout in production,
so the file just never appeared with no error. Replaced with plain
recursive child traversal (~40x faster on the same file, confirmed
29.9s -> 3.16s); test_parsing_many_points_with_extensions_stays_fast
guards against reintroducing it.

Also adds support for <rte> routes (not just <trk> tracks and <wpt>
waypoints), sharing the track/route conversion logic via a new
line_feature helper. Routes get their own "route" kind and a dashed
ROUTE_ICON in the file tree so the type is visible at a glance.
This commit is contained in:
randy committed 2026-09-07 14:36:12 +02:00
1 parent 0394befdec
commit 708c08259b
6 files changed
+149 -19

No files matched your search

+21 -3
View File
@@ -181,8 +181,16 @@ same GeoJSON `FeatureCollection` shape so the frontend doesn't need to
know which format it got. `Gpx` parses the GPX with REXML: `<wpt>`
waypoints become `Point` features (with `name`, `desc` and `time`
carried through as properties), each `<trk>` becomes one
`MultiLineString` feature (one line per `<trkseg>`); `<rte>` routes
are not handled. `Fit` reads Garmin's binary FIT format directly
`MultiLineString` feature (one line per `<trkseg>`), and each `<rte>`
becomes one too (a route is a single continuous line by definition, no
`<trkseg>`-style grouping) — the two are told apart only by a `kind`
property (`"track"` vs `"route"`; waypoints are `"waypoint"`), which
`Gpx.kinds` also exposes per-file so the file tree can show a dashed
route icon next to a route file and a solid one for a track
(`public/js/icons.js`'s `ROUTE_ICON`/`TRACK_ICON`,
`public/js/gpx.js`'s `_kindIcon`). `<rte>` has no GPX equivalent of
`<trk>`'s `<type>`, so a route's `activity_type` is always `nil`.
`Fit` reads Garmin's binary FIT format directly
(hand-rolled — see below) into a single `MultiLineString` `track`
feature; FIT activity files don't really have a GPX-waypoint
equivalent, so it never produces `Point` features. MapLibre's
@@ -280,7 +288,17 @@ below). For FIT, those extras are the `record` message's own
device-specific `<extensions>` block under each `<trkpt>` — usually
Garmin's `TrackPointExtension`, matched by local element name only
(`hr`/`cad`/`speed`) so it doesn't care which XML namespace prefix a
given file happens to declare.
given file happens to declare. That `<extensions>` block is walked by
hand (a plain recursive `elements.each`) rather than REXML's
`"extensions//*"` XPath, which this used originally: the `//`
descendant axis costs low-single-digit milliseconds *per call*
regardless of how small the subtree actually is, and this runs once
per point — negligible on a short file, but ~30s for a real
8000-point one (enough to hit a proxy timeout in production, so the
track just silently never rendered). Plain traversal is ~40x faster on
the same file; `test/test_gpx.rb`'s
`test_parsing_many_points_with_extensions_stays_fast` is a regression
test for it.
+
Each chart is its own single-series `<svg>` (elevation/speed/cadence/
heart rate are on wildly different scales, so this is small multiples