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

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="test">
<rte>
<name>Planned Loop</name>
<rtept lat="48.1" lon="17.2">
<ele>200</ele>
</rtept>
<rtept lat="48.101" lon="17.201">
<ele>210</ele>
</rtept>
</rte>
</gpx>
+40
View File
@@ -41,6 +41,46 @@ class GpxTest < Minitest::Test
assert_equal [17.2, 48.1], [profile[0]["lon"], profile[0]["lat"]]
end
def test_kinds_detects_routes
assert_equal ["route"], Maptool::Gpx.kinds(fixture_path("sample_route.gpx"))
end
def test_to_geojson_produces_a_route_multilinestring
geojson = Maptool::Gpx.to_geojson(fixture_path("sample_route.gpx"))
route = geojson["features"].find { |f| f["properties"]["kind"] == "route" }
assert_equal "MultiLineString", route["geometry"]["type"]
assert_equal 2, route["geometry"]["coordinates"][0].size
assert_equal "Planned Loop", route["properties"]["name"]
assert_nil route["properties"]["activity_type"], "<rte> has no GPX equivalent of <trk><type>"
assert_operator route["properties"]["distance_meters"], :>, 0
end
# Regression test for a real bug: point_extensions originally used the
# "extensions//*" XPath (REXML's `//` descendant axis), which cost
# milliseconds *per call* regardless of subtree size — fine on this
# suite's small fixtures, ~30s on a real 8000-point file. This fixture is
# smaller than that (keeping the suite fast) but big enough that an
# O(n^2) reintroduction would make itself obvious well under the bound.
def test_parsing_many_points_with_extensions_stays_fast
points = Array.new(2000) do |i|
<<~TRKPT
<trkpt lat="48.1" lon="17.2"><ele>200</ele><time>2026-01-01T09:00:#{format("%02d", i % 60)}Z</time>
<extensions><gpxtpx:TrackPointExtension xmlns:gpxtpx="urn"><gpxtpx:hr>140</gpxtpx:hr></gpxtpx:TrackPointExtension></extensions>
</trkpt>
TRKPT
end.join
path = File.join(Dir.mktmpdir, "big.gpx")
File.write(path, "<gpx><trk><trkseg>#{points}</trkseg></trk></gpx>")
started = Time.now
geojson = Maptool::Gpx.to_geojson(path)
elapsed = Time.now - started
assert_equal 2000, geojson["features"][0]["geometry"]["coordinates"][0].size
assert_operator elapsed, :<, 5, "took #{elapsed}s for 2000 points — was O(n^2) via an XPath descendant search once, see comment above"
end
def test_invalid_xml_raises_parse_error
path = File.join(Dir.mktmpdir, "bad.gpx")
File.write(path, "<gpx><trk unclosed>")