Wire offline hiking trails into the real build, API, and renderer
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 12s

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.
This commit is contained in:
randy committed 2026-09-13 10:31:59 +02:00
1 parent b35ace9734
commit 079ac37787
11 files changed
+370 -25

No files matched your search

+9
View File
@@ -39,6 +39,15 @@ class AppTest < Minitest::Test
assert_equal({ "available" => false }, json_body(response))
end
def test_hiking_trails_reports_unavailable_when_no_geojson_present
response = request.get("/api/hiking-trails")
assert_equal({ "available" => false }, json_body(response))
end
def test_hiking_trails_file_404s_when_no_geojson_present
assert_equal 404, request.get("/tiles/hiking_trails.geojson").status
end
def test_kiwix_reports_unavailable_when_no_url_configured
response = request.get("/api/kiwix")
assert_equal({ "available" => false, "url" => "" }, json_body(response))
+37
View File
@@ -89,6 +89,43 @@ class HikingTrailsTest < Minitest::Test
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
+59 -2
View File
@@ -76,6 +76,27 @@ class PmtilesBuilderTest < Minitest::Test
SH
File.chmod(0o755, @osmium_ok)
# Same shape as osmium-ok, but fails on tags-filter specifically (the
# first subcommand HikingTrails.extract runs) while still succeeding
# on merge — lets a single-country build (which never calls merge)
# exercise "hiking-trails extraction failed" without also breaking
# offline.pmtiles' own osmium use in a multi-country test.
@osmium_fail_tags_filter = File.join(@bin_dir, "osmium-fail-tags-filter")
File.write(@osmium_fail_tags_filter, <<~SH)
#!/bin/sh
echo "osmium $1" >> "$PMTILES_LOG"
if [ "$1" = "tags-filter" ]; then
echo "boom" >&2
exit 1
fi
prev=""
for arg; do
if [ "$prev" = "-o" ]; then echo fake > "$arg"; fi
prev="$arg"
done
SH
File.chmod(0o755, @osmium_fail_tags_filter)
# Stands in for `java -jar planetiler.jar --osm-path=... --output=OUT`
# — PLANETILER_JAR itself is irrelevant here (this fake ignores -jar's
# argument entirely), only JAVA_BIN needs pointing at it.
@@ -188,13 +209,42 @@ class PmtilesBuilderTest < Minitest::Test
assert_path_exists File.join(@data_dir, "offline.pmtiles")
refute_path_exists File.join(@data_dir, "offline.tmp.pmtiles")
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
assert_equal ["planetiler --osm-path"], log_lines
assert_equal ["planetiler --osm-path", "osmium tags-filter", "osmium add-locations-to-ways", "osmium cat"], log_lines
assert_equal(
{ "sources" => [{ "code" => "SK", "slug" => "europe/slovakia", "last_modified" => "Wed, 10 Sep 2026 08:00:01 GMT" }] },
Maptool::PmtilesBuilder.offline_meta(@data_dir),
)
end
def test_build_offline_also_writes_a_hiking_trails_geojson
select_regions("SK")
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
thread.join
# The fake osmium binary doesn't produce real OPL output, just
# confirms the file lands and is valid (empty) GeoJSON — the real
# parsing logic is covered by test_hiking_trails.rb against real OPL
# fixtures.
path = File.join(@data_dir, "hiking_trails.geojson")
assert_path_exists path
assert_equal({ "type" => "FeatureCollection", "features" => [] }, JSON.parse(File.read(path)))
end
# The actual reasoning: hiking trails are a bonus overlay bolted onto the
# existing offline.pmtiles build, not the core map — an osmium hiccup on
# this one step shouldn't take down an otherwise-successful build.
def test_build_offline_still_succeeds_when_hiking_trails_extraction_fails
select_regions("SK")
ENV["OSMIUM_BIN"] = @osmium_fail_tags_filter
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
thread.join
assert_path_exists File.join(@data_dir, "offline.pmtiles")
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
refute_path_exists File.join(@data_dir, "hiking_trails.geojson")
refute_path_exists "#{File.join(@data_dir, "hiking_trails.geojson")}.tmp"
end
# Two countries selected — osmium merge should run before planetiler,
# each downloaded source recorded in the sidecar meta.
def test_build_offline_merges_multiple_selected_countries
@@ -203,7 +253,14 @@ class PmtilesBuilderTest < Minitest::Test
thread.join
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
assert_equal ["osmium merge", "planetiler --osm-path"], log_lines
assert_equal(
[
"osmium merge", "planetiler --osm-path",
"osmium tags-filter", "osmium add-locations-to-ways", "osmium cat", # SK's own hiking-trails extraction
"osmium tags-filter", "osmium add-locations-to-ways", "osmium cat", # AT's own
],
log_lines,
)
codes = Maptool::PmtilesBuilder.offline_meta(@data_dir)["sources"].map { |s| s["code"] }
assert_equal %w[SK AT], codes
end