Wire offline hiking trails into the real build, API, and renderer
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:
1 parent
b35ace9734
commit
079ac37787
11 files changed
+370
-25
No files matched your search
@@ -63,6 +63,22 @@ module Maptool
|
||||
end
|
||||
end
|
||||
|
||||
# Runs extract() over one .osm.pbf per selected country and combines the
|
||||
# results into a single FeatureCollection — a route relation crossing a
|
||||
# border (an E-path, say) shows up once per country whose extract it
|
||||
# was pulled from, deduplicated here by OSM relation id (properties.id)
|
||||
# rather than left to render as overlapping duplicate lines.
|
||||
# extract: same injectable-seam reasoning as PmtilesBuilder's own
|
||||
# download_extract:/fetch_manifest: — lets a test swap out the real
|
||||
# (osmium-shelling-out) implementation for a canned one.
|
||||
def self.extract_merged(pbf_paths, extract: method(:extract))
|
||||
seen = {}
|
||||
pbf_paths.each do |pbf_path|
|
||||
extract.call(pbf_path)["features"].each { |feature| seen[feature["properties"]["id"]] ||= feature }
|
||||
end
|
||||
{ "type" => "FeatureCollection", "features" => seen.values }
|
||||
end
|
||||
|
||||
# Pure — no subprocess — so it's cheaply testable against a canned OPL
|
||||
# fixture, same "keep the real logic testable without the real tool
|
||||
# installed" reasoning OverpassPois.to_geojson already gives for staying
|
||||
@@ -81,7 +97,7 @@ module Maptool
|
||||
ways[head[1..].to_i] = way_coordinates(fields)
|
||||
when "r"
|
||||
tags = tags_field(fields)
|
||||
relations << { tags: tags, members: members_field(fields) } if tags["route"] == "hiking"
|
||||
relations << { id: head[1..].to_i, tags: tags, members: members_field(fields) } if tags["route"] == "hiking"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -99,6 +115,7 @@ module Maptool
|
||||
distance = lines.sum { |coords| TrackStats.compute(coords)["distance_meters"] }
|
||||
tags = relation[:tags]
|
||||
properties = {
|
||||
"id" => relation[:id],
|
||||
"name" => tags["name"] || tags["description"],
|
||||
"distance_meters" => distance.round(1),
|
||||
}
|
||||
@@ -107,11 +124,20 @@ module Maptool
|
||||
{
|
||||
"type" => "Feature",
|
||||
"properties" => properties.compact,
|
||||
"geometry" => { "type" => "MultiLineString", "coordinates" => lines },
|
||||
# Rounded to 5 decimal places (~1m) — full OSM float precision
|
||||
# (~1cm) is pointless for a rendered line and roughly doubles the
|
||||
# size of every coordinate pair once this many-country GeoJSON is
|
||||
# actually served to a browser.
|
||||
"geometry" => { "type" => "MultiLineString", "coordinates" => round_coordinates(lines) },
|
||||
}
|
||||
end
|
||||
private_class_method :relation_feature
|
||||
|
||||
def self.round_coordinates(lines)
|
||||
lines.map { |coords| coords.map { |lon, lat| [lon.round(5), lat.round(5)] } }
|
||||
end
|
||||
private_class_method :round_coordinates
|
||||
|
||||
def self.field(fields, letter)
|
||||
fields.find { |f| f.start_with?(letter) }
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ require "json"
|
||||
require "tmpdir"
|
||||
require "fileutils"
|
||||
require "etc"
|
||||
require_relative "hiking_trails"
|
||||
|
||||
module Maptool
|
||||
# Regenerates data/offline.pmtiles and data/terrain.pmtiles for the
|
||||
@@ -289,12 +290,37 @@ module Maptool
|
||||
run_planetiler!("--osm-path=#{osm_path}", "--output=#{tmp_path}", "--download", "--threads=#{Etc.nprocessors}")
|
||||
File.rename(tmp_path, File.join(data_dir, "offline.pmtiles"))
|
||||
File.write(offline_meta_path(data_dir), JSON.generate(sources: sources.map { |s| s.slice("code", "slug", "last_modified") }))
|
||||
|
||||
report_step.call("extracting hiking trails")
|
||||
extract_hiking_trails(sources, data_dir)
|
||||
ensure
|
||||
FileUtils.remove_entry(scratch_dir) if scratch_dir && Dir.exist?(scratch_dir)
|
||||
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
||||
end
|
||||
private_class_method :extract_offline
|
||||
|
||||
# Runs over the same per-country .osm.pbf files sources above already
|
||||
# downloaded/cached for offline.pmtiles itself — no separate download.
|
||||
# Best-effort: a hiking-trails extraction problem (a malformed PBF
|
||||
# region, osmium hiccup on one particular file) shouldn't fail the
|
||||
# whole offline.pmtiles build over what's a bonus overlay, not the
|
||||
# core map — logged into the sidecar meta's own "error" field instead,
|
||||
# so it's visible without taking build_offline itself down.
|
||||
def self.extract_hiking_trails(sources, data_dir)
|
||||
geojson = HikingTrails.extract_merged(sources.map { |s| s["path"] })
|
||||
tmp_path = "#{hiking_trails_path(data_dir)}.tmp"
|
||||
File.write(tmp_path, JSON.generate(geojson))
|
||||
File.rename(tmp_path, hiking_trails_path(data_dir))
|
||||
rescue HikingTrails::ExtractError => e
|
||||
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
||||
warn "hiking trails extraction failed, skipping: #{e.message}"
|
||||
end
|
||||
private_class_method :extract_hiking_trails
|
||||
|
||||
def self.hiking_trails_path(data_dir)
|
||||
File.join(data_dir, "hiking_trails.geojson")
|
||||
end
|
||||
|
||||
# Renders a report_step string like "fetching PL (3/4) — done: SK, CZ —
|
||||
# pending: HU" — so GET .../build-status shows not just "it's running"
|
||||
# but which of the current build's sources is in flight and which are
|
||||
@@ -442,6 +468,7 @@ module Maptool
|
||||
|
||||
File.delete(File.join(data_dir, "offline.pmtiles")) if File.exist?(File.join(data_dir, "offline.pmtiles"))
|
||||
File.delete(offline_meta_path(data_dir)) if File.exist?(offline_meta_path(data_dir))
|
||||
File.delete(hiking_trails_path(data_dir)) if File.exist?(hiking_trails_path(data_dir))
|
||||
cache_dir = geofabrik_cache_dir(data_dir)
|
||||
FileUtils.remove_entry(cache_dir) if Dir.exist?(cache_dir)
|
||||
end
|
||||
|
||||
Reference in new issue
Block a user