data/hiking_trails.geojson now generates automatically as the last step of every offline.pmtiles build (PmtilesBuilder.extract_hiking_trails, best-effort — a failure here doesn't fail the whole build), merged across all selected countries with cross-border dedup by OSM relation id (HikingTrails.extract_merged). Served at GET /tiles/hiking_trails.geojson and GET /api/hiking-trails, fetched client-side only once the overlay is actually toggled on. hiking_trails.js now prefers this local file whenever it exists, regardless of which base map (online/offline) is active — same "prefer local when available" story as HillshadeControl already uses for terrain. Offline click hit-tests the real vector layer directly (queryRenderedFeatures) instead of querying Overpass — instant, no network. Coordinates are rounded to 5dp to keep the combined file smaller; colour tag values are normalized against a known-safe set before ever reaching a style paint property. Verified extract_merged (new: combines multiple countries' extracts, dedupes overlapping relations) against two real cached country PBFs on brick directly, not just fixtures.
141 lines
5.1 KiB
Ruby
141 lines
5.1 KiB
Ruby
require_relative "test_helper"
|
|
require "maptool/hiking_trails"
|
|
|
|
# No real osmium calls — extract() is a thin subprocess-plus-parse wrapper
|
|
# (same "not independently tested" reasoning OverpassPois.fetch gets), but
|
|
# parse_opl is pure text in, GeoJSON out, and that's the part actually worth
|
|
# protecting: the OPL escaping rules and the way-id -> relation-member
|
|
# stitching are easy to get subtly wrong and osmium isn't installed in every
|
|
# environment this test suite runs in.
|
|
class HikingTrailsTest < Minitest::Test
|
|
def test_parse_opl_builds_a_multilinestring_from_member_way_geometry
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
w2 Thighway=path Nn11x20.088y49.18,n12x20.089y49.181
|
|
r1 Troute=hiking,type=route,name=Rysy Mw1@,w2@
|
|
OPL
|
|
|
|
features = Maptool::HikingTrails.parse_opl(opl)["features"]
|
|
|
|
assert_equal 1, features.size
|
|
feature = features.first
|
|
assert_equal "MultiLineString", feature["geometry"]["type"]
|
|
assert_equal [[[20.087, 49.179], [20.088, 49.18]], [[20.088, 49.18], [20.089, 49.181]]], feature["geometry"]["coordinates"]
|
|
assert_equal "Rysy", feature["properties"]["name"]
|
|
end
|
|
|
|
def test_parse_opl_skips_relations_that_are_not_route_hiking
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1 Ttype=multipolygon Mw1@outer
|
|
OPL
|
|
|
|
assert_empty Maptool::HikingTrails.parse_opl(opl)["features"]
|
|
end
|
|
|
|
def test_parse_opl_falls_back_to_description_when_there_is_no_name
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1 Troute=hiking,type=route,description=Rázc.%20%pri%20%Žabom%20%potoku%20%-%20%Rysy Mw1@
|
|
OPL
|
|
|
|
feature = Maptool::HikingTrails.parse_opl(opl)["features"].first
|
|
|
|
assert_equal "Rázc. pri Žabom potoku - Rysy", feature["properties"]["name"]
|
|
end
|
|
|
|
def test_parse_opl_unescapes_commas_and_equals_in_tag_values
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1 Troute=hiking,type=route,name=Salt%2c%%20%Pepper%3d%Co Mw1@
|
|
OPL
|
|
|
|
feature = Maptool::HikingTrails.parse_opl(opl)["features"].first
|
|
|
|
assert_equal "Salt, Pepper=Co", feature["properties"]["name"]
|
|
end
|
|
|
|
def test_parse_opl_carries_over_colour_network_operator_ref
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1 Troute=hiking,type=route,name=Trail,colour=red,network=rwn,operator=KST,ref=0933 Mw1@
|
|
OPL
|
|
|
|
properties = Maptool::HikingTrails.parse_opl(opl)["features"].first["properties"]
|
|
|
|
assert_equal "red", properties["colour"]
|
|
assert_equal "rwn", properties["network"]
|
|
assert_equal "KST", properties["operator"]
|
|
assert_equal "0933", properties["ref"]
|
|
end
|
|
|
|
def test_parse_opl_computes_distance_from_member_way_geometry
|
|
# Roughly 111.2m per 0.001 degree of latitude at the equator.
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x0.0y0.0,n11x0.0y0.001
|
|
r1 Troute=hiking,type=route,name=Trail Mw1@
|
|
OPL
|
|
|
|
distance = Maptool::HikingTrails.parse_opl(opl)["features"].first["properties"]["distance_meters"]
|
|
|
|
assert_in_delta 111.2, distance, 1.0
|
|
end
|
|
|
|
def test_parse_opl_drops_a_relation_whose_member_ways_are_all_missing
|
|
opl = <<~OPL
|
|
r1 Troute=hiking,type=route,name=Ghost Mw999@
|
|
OPL
|
|
|
|
assert_empty Maptool::HikingTrails.parse_opl(opl)["features"]
|
|
end
|
|
|
|
def test_parse_opl_carries_the_osm_relation_id_for_cross_country_dedup
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1246205 Troute=hiking,type=route,name=Trail Mw1@
|
|
OPL
|
|
|
|
feature = Maptool::HikingTrails.parse_opl(opl)["features"].first
|
|
|
|
assert_equal 1246205, feature["properties"]["id"]
|
|
end
|
|
|
|
def test_parse_opl_rounds_coordinates_to_5_decimal_places
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.0876543210y49.1791234567,n11x20.0880000001y49.1800000009
|
|
r1 Troute=hiking,type=route,name=Trail Mw1@
|
|
OPL
|
|
|
|
coords = Maptool::HikingTrails.parse_opl(opl)["features"].first["geometry"]["coordinates"]
|
|
|
|
assert_equal [[[20.08765, 49.17912], [20.088, 49.18]]], coords
|
|
end
|
|
|
|
def test_extract_merged_deduplicates_by_relation_id_across_files
|
|
feature = ->(id) {
|
|
{ "type" => "Feature", "properties" => { "id" => id, "name" => "Trail #{id}" }, "geometry" => { "type" => "MultiLineString", "coordinates" => [] } }
|
|
}
|
|
# Relation 1 shows up in both "country" files (a cross-border trail);
|
|
# relation 2 only in the second.
|
|
per_file = { "at.osm.pbf" => [feature.call(1)], "cz.osm.pbf" => [feature.call(1), feature.call(2)] }
|
|
extract = ->(pbf_path) { { "type" => "FeatureCollection", "features" => per_file.fetch(pbf_path) } }
|
|
|
|
merged = Maptool::HikingTrails.extract_merged(%w[at.osm.pbf cz.osm.pbf], extract: extract)
|
|
ids = merged["features"].map { |f| f["properties"]["id"] }.sort
|
|
|
|
assert_equal [1, 2], ids
|
|
end
|
|
|
|
def test_parse_opl_ignores_a_dangling_member_reference_but_keeps_the_rest
|
|
opl = <<~OPL
|
|
w1 Thighway=path Nn10x20.087y49.179,n11x20.088y49.18
|
|
r1 Troute=hiking,type=route,name=Trail Mw1@,w999@
|
|
OPL
|
|
|
|
features = Maptool::HikingTrails.parse_opl(opl)["features"]
|
|
|
|
assert_equal 1, features.size
|
|
assert_equal 1, features.first["geometry"]["coordinates"].size
|
|
end
|
|
end
|