require_relative "test_helper" require "maptool/gpx" class GpxTest < Minitest::Test def test_kinds_detects_waypoints_and_tracks assert_equal %w[waypoint track], Maptool::Gpx.kinds(fixture_path("sample.gpx")) end def test_to_geojson_produces_a_waypoint_point_feature geojson = Maptool::Gpx.to_geojson(fixture_path("sample.gpx")) waypoint = geojson["features"].find { |f| f["properties"]["kind"] == "waypoint" } assert_equal "Point", waypoint["geometry"]["type"] assert_equal [17.2, 48.1, 200.0], waypoint["geometry"]["coordinates"] assert_equal "Start", waypoint["properties"]["name"] assert_includes waypoint["properties"]["description"], "example.com" end def test_to_geojson_produces_a_track_multilinestring_with_stats geojson = Maptool::Gpx.to_geojson(fixture_path("sample.gpx")) track = geojson["features"].find { |f| f["properties"]["kind"] == "track" } assert_equal "MultiLineString", track["geometry"]["type"] assert_equal 3, track["geometry"]["coordinates"][0].size assert_equal "Morning Loop", track["properties"]["name"] assert_equal "running", track["properties"]["activity_type"] assert_operator track["properties"]["distance_meters"], :>, 0 assert_equal 50.0, track["properties"]["elevation_gain_meters"], "200->250 is the only gain, 250->230 descent not subtracted" assert_equal 1200, track["properties"]["duration_secs"] end def test_track_profile_carries_extension_fields_and_tolerates_points_without_them geojson = Maptool::Gpx.to_geojson(fixture_path("sample.gpx")) profile = geojson["features"].find { |f| f["properties"]["kind"] == "track" }["properties"]["profile"] assert_equal 3, profile.size assert_equal 140, profile[0]["heart_rate_bpm"] assert_equal 84, profile[1]["cadence_rpm"] assert_nil profile[2]["heart_rate_bpm"], "third point has no " assert_equal 0.0, profile[0]["distance_meters"] assert_equal [17.2, 48.1], [profile[0]["lon"], profile[0]["lat"]] end def test_waypoint_captures_category_and_color_from_poi_extensions geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx")) free_spot = geojson["features"].find { |f| f["properties"]["name"] == "Free spot" } assert_equal "Free motorhome area", free_spot["properties"]["category"] assert_equal "#2E7D32", free_spot["properties"]["color"] assert_equal "tourism_camp_site", free_spot["properties"]["icon"] end # Regression test for a real bug: a p4n2gpx export's carries # both an (a category key this app can render) and a # (a Locus-app drawable path) side by side. REXML strips # namespace prefixes from Element#name, so both elements report the same # local name "icon" — matching on that alone picked whichever happened to # come last in the file (locus, since it followed osmand), leaving every # waypoint's icon set to a meaningless drawable path that never matched # any category. Must match by namespace URI instead. def test_waypoint_icon_ignores_non_osmand_extensions_with_the_same_local_name geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx")) export = geojson["features"].find { |f| f["properties"]["name"] == "p4n2gpx export" } assert_equal "amenity_parking", export["properties"]["icon"] end def test_waypoint_without_category_or_color_leaves_them_nil geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx")) plain = geojson["features"].find { |f| f["properties"]["name"] == "Plain waypoint" } assert_nil plain["properties"]["category"] assert_nil plain["properties"]["color"] assert_nil plain["properties"]["icon"] 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"], " has no GPX equivalent of " 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 200 140 TRKPT end.join path = File.join(Dir.mktmpdir, "big.gpx") File.write(path, "#{points}") 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, "") assert_raises(Maptool::Gpx::ParseError) { Maptool::Gpx.to_geojson(path) } end def test_non_gpx_root_raises_parse_error path = File.join(Dir.mktmpdir, "notgpx.gpx") File.write(path, "") assert_raises(Maptool::Gpx::ParseError) { Maptool::Gpx.to_geojson(path) } end end