Replace offline basemap pipeline with Planetiler, unify online/offline styling
Offline tiles were built from Protomaps' own basemap schema, needing a completely separate hand-tuned theme (protomaps-themes-base + OFFLINE_THEME) from the online Liberty style's ONLINE_STYLE_PATCH — the two could never be made to match exactly no matter how much either side got tuned (today's session found and fixed several mismatches: building opacity, park boundaries, road casings, road widths). Offline.pmtiles is now built with Planetiler's default OpenMapTiles profile against Geofabrik per-country OSM extracts (osmium-merged when more than one country is selected), the same vector schema Liberty already expects. offlineStyle() now clones the same Liberty JSON (vendored as a local snapshot for reliability with no internet) and applies the identical ONLINE_STYLE_PATCH, just repointed at local PMTiles/glyphs/sprite — one palette instead of two, correct on both sides by construction. Also vendors Liberty's icon sprite for the first time (offline had none before), adds Java/osmium-tool/planetiler.jar to the Docker image, and removes the now-unused protomaps-themes-base dependency and the theme-comparison debug page that found today's bugs.
This commit is contained in:
1 parent
0874e8f8ed
commit
d08ed41185
21 files changed
+4286
-512
No files matched your search
+2
-2
@@ -402,13 +402,13 @@ class AppTest < Minitest::Test
|
||||
assert_equal 409, response.status
|
||||
end
|
||||
|
||||
# fetch_build_url blocks on a Queue, called directly (not through the
|
||||
# download_extract blocks on a Queue, called directly (not through the
|
||||
# route, which has no way to inject a fake) so the build is
|
||||
# deterministically still "running" when the route call below happens.
|
||||
def test_offline_tiles_build_is_409_when_one_is_already_running
|
||||
Maptool::PmtilesBuilder.set_selected_regions(Maptool::App::DATA_DIR, ["SK"])
|
||||
gate = Queue.new
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: Maptool::App::DATA_DIR, fetch_build_url: -> { gate.pop })
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: Maptool::App::DATA_DIR, download_extract: ->(_url, _dest) { gate.pop })
|
||||
|
||||
response = request.post("/api/offline-tiles/build", **logged_in_env)
|
||||
assert_equal 409, response.status
|
||||
|
||||
+106
-35
@@ -3,14 +3,15 @@ require "maptool/pmtiles_builder"
|
||||
require "tmpdir"
|
||||
require "fileutils"
|
||||
|
||||
# Network calls (the protomaps build index, the mapterhorn manifest) are
|
||||
# Network calls (Geofabrik downloads/HEADs, the mapterhorn manifest) are
|
||||
# never exercised here — same "keep this offline/deterministic" reasoning
|
||||
# test_helper.rb already gives for /api/plugins staying off the real
|
||||
# Overpass API — build_offline/build_terrain take fetch_build_url/
|
||||
# fetch_manifest precisely so tests can swap those out. A fake `pmtiles`
|
||||
# CLI (PMTILES_BIN) stands in for the real go-pmtiles binary, logging each
|
||||
# invocation's subcommand + first arg to PMTILES_LOG so tests can assert on
|
||||
# what actually got called (e.g. "was merge invoked, and with what").
|
||||
# Overpass API — build_offline/build_terrain take download_extract/
|
||||
# fetch_manifest precisely so tests can swap those out. Fake `pmtiles`,
|
||||
# `osmium`, and `java` binaries (PMTILES_BIN/OSMIUM_BIN/JAVA_BIN) stand in
|
||||
# for the real tools, logging each invocation's subcommand/first arg to
|
||||
# PMTILES_LOG so tests can assert on what actually got called (e.g. "was
|
||||
# merge invoked, and with what").
|
||||
#
|
||||
# Country codes below are real ones from the vendored
|
||||
# lib/maptool/data/europe_countries.geojson (Slovakia, Iceland, Spain) —
|
||||
@@ -46,8 +47,45 @@ class PmtilesBuilderTest < Minitest::Test
|
||||
File.write(@pmtiles_fail, "#!/bin/sh\necho 'boom' >&2\nexit 1\n")
|
||||
File.chmod(0o755, @pmtiles_fail)
|
||||
|
||||
# osmium merge PBF1 PBF2 ... -o OUTPUT — output is always "-o"'s next
|
||||
# arg, unlike pmtiles' varying-position output above.
|
||||
@osmium_ok = File.join(@bin_dir, "osmium-ok")
|
||||
File.write(@osmium_ok, <<~SH)
|
||||
#!/bin/sh
|
||||
echo "osmium $1" >> "$PMTILES_LOG"
|
||||
prev=""
|
||||
for arg; do
|
||||
if [ "$prev" = "-o" ]; then echo fake > "$arg"; fi
|
||||
prev="$arg"
|
||||
done
|
||||
SH
|
||||
File.chmod(0o755, @osmium_ok)
|
||||
|
||||
# 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.
|
||||
@java_ok = File.join(@bin_dir, "java-ok")
|
||||
File.write(@java_ok, <<~SH)
|
||||
#!/bin/sh
|
||||
echo "planetiler ${3%%=*}" >> "$PMTILES_LOG"
|
||||
for arg; do
|
||||
case "$arg" in
|
||||
--output=*) echo fake > "${arg#--output=}" ;;
|
||||
esac
|
||||
done
|
||||
SH
|
||||
File.chmod(0o755, @java_ok)
|
||||
|
||||
@java_fail = File.join(@bin_dir, "java-fail")
|
||||
File.write(@java_fail, "#!/bin/sh\necho 'boom' >&2\nexit 1\n")
|
||||
File.chmod(0o755, @java_fail)
|
||||
|
||||
@original_pmtiles_bin = ENV["PMTILES_BIN"]
|
||||
@original_osmium_bin = ENV["OSMIUM_BIN"]
|
||||
@original_java_bin = ENV["JAVA_BIN"]
|
||||
ENV["PMTILES_LOG"] = @log
|
||||
ENV["OSMIUM_BIN"] = @osmium_ok
|
||||
ENV["JAVA_BIN"] = @java_ok
|
||||
Maptool::PmtilesBuilder.reset!
|
||||
end
|
||||
|
||||
@@ -55,10 +93,22 @@ class PmtilesBuilderTest < Minitest::Test
|
||||
FileUtils.remove_entry(@data_dir)
|
||||
FileUtils.remove_entry(@bin_dir)
|
||||
ENV["PMTILES_BIN"] = @original_pmtiles_bin
|
||||
ENV["OSMIUM_BIN"] = @original_osmium_bin
|
||||
ENV["JAVA_BIN"] = @original_java_bin
|
||||
ENV.delete("PMTILES_LOG")
|
||||
Maptool::PmtilesBuilder.reset!
|
||||
end
|
||||
|
||||
# download_extract stub: writes a marker file (same "fake" content the
|
||||
# pmtiles/osmium/java fixtures use) instead of hitting Geofabrik, and
|
||||
# returns a fixed Last-Modified so sidecar-meta assertions are stable.
|
||||
def fake_download(last_modified: "Wed, 10 Sep 2026 08:00:01 GMT")
|
||||
->(_url, dest) {
|
||||
File.write(dest, "fake")
|
||||
last_modified
|
||||
}
|
||||
end
|
||||
|
||||
def log_lines
|
||||
File.exist?(@log) ? File.readlines(@log, chomp: true) : []
|
||||
end
|
||||
@@ -107,21 +157,36 @@ class PmtilesBuilderTest < Minitest::Test
|
||||
|
||||
def test_build_offline_raises_with_no_selection
|
||||
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "unused" })
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
end
|
||||
end
|
||||
|
||||
def test_build_offline_writes_the_file_and_reports_done
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_ok
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
thread.join
|
||||
|
||||
assert_path_exists File.join(@data_dir, "offline.pmtiles")
|
||||
refute_path_exists File.join(@data_dir, "offline.pmtiles.tmp")
|
||||
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
||||
assert_equal ["extract https://example.com/fake.pmtiles"], log_lines
|
||||
assert_equal({ "source" => "fake.pmtiles" }, Maptool::PmtilesBuilder.offline_meta(@data_dir))
|
||||
assert_equal ["planetiler --osm-path"], 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
|
||||
|
||||
# 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
|
||||
select_regions("SK", "AT")
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
thread.join
|
||||
|
||||
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
||||
assert_equal ["osmium merge", "planetiler --osm-path"], log_lines
|
||||
codes = Maptool::PmtilesBuilder.offline_meta(@data_dir)["sources"].map { |s| s["code"] }
|
||||
assert_equal %w[SK AT], codes
|
||||
end
|
||||
|
||||
def test_offline_meta_is_nil_before_any_build
|
||||
@@ -129,38 +194,45 @@ class PmtilesBuilderTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_offline_update_available_is_nil_before_any_build
|
||||
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
|
||||
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "unused" })
|
||||
end
|
||||
|
||||
def test_offline_update_available_is_false_when_unchanged
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_ok
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }).join
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
||||
|
||||
refute Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
|
||||
refute Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" })
|
||||
end
|
||||
|
||||
def test_offline_update_available_is_true_when_a_newer_build_exists
|
||||
def test_offline_update_available_is_true_when_a_newer_extract_exists
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_ok
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/old.pmtiles" }).join
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
||||
|
||||
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: -> { "https://example.com/new.pmtiles" })
|
||||
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Thu, 11 Sep 2026 08:00:01 GMT" })
|
||||
end
|
||||
|
||||
def test_offline_update_available_is_nil_when_the_index_is_unreachable
|
||||
# Same recorded source, but the selection has since grown — recorded
|
||||
# sources no longer match what the current selection would need.
|
||||
def test_offline_update_available_is_true_when_the_selection_grew
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_ok
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }).join
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
||||
|
||||
fetch_build_url = -> { raise Maptool::PmtilesBuilder::BuildError, "could not reach index" }
|
||||
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_build_url: fetch_build_url)
|
||||
select_regions("SK", "AT")
|
||||
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" })
|
||||
end
|
||||
|
||||
def test_offline_update_available_is_nil_when_geofabrik_is_unreachable
|
||||
select_regions("SK")
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
||||
|
||||
fetch_last_modified = ->(_url) { raise Maptool::PmtilesBuilder::BuildError, "could not reach geofabrik" }
|
||||
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: fetch_last_modified)
|
||||
end
|
||||
|
||||
def test_build_offline_reports_an_error_status_on_non_zero_exit
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_fail
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
|
||||
ENV["JAVA_BIN"] = @java_fail
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
thread.join
|
||||
|
||||
refute_path_exists File.join(@data_dir, "offline.pmtiles")
|
||||
@@ -169,31 +241,30 @@ class PmtilesBuilderTest < Minitest::Test
|
||||
assert_match(/exited with status/, status[:message])
|
||||
end
|
||||
|
||||
def test_build_offline_reports_a_clear_error_when_the_binary_is_missing
|
||||
def test_build_offline_reports_a_clear_error_when_planetiler_is_missing
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = File.join(@bin_dir, "does-not-exist")
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" })
|
||||
ENV["JAVA_BIN"] = File.join(@bin_dir, "does-not-exist")
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
thread.join
|
||||
|
||||
status = Maptool::PmtilesBuilder.offline_status
|
||||
assert_equal "error", status[:state]
|
||||
assert_match(/go-pmtiles installed/, status[:message])
|
||||
assert_match(/Java and planetiler\.jar installed/, status[:message])
|
||||
end
|
||||
|
||||
# fetch_build_url blocks on a Queue so the build is deterministically
|
||||
# download_extract blocks on a Queue so the build is deterministically
|
||||
# still "running" when the second call happens, rather than racing a real
|
||||
# one to see which state wins.
|
||||
def test_build_offline_raises_when_one_is_already_running
|
||||
select_regions("SK")
|
||||
ENV["PMTILES_BIN"] = @pmtiles_ok
|
||||
gate = Queue.new
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { gate.pop })
|
||||
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: ->(_url, _dest) { gate.pop })
|
||||
|
||||
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, fetch_build_url: -> { "unused" })
|
||||
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
||||
end
|
||||
ensure
|
||||
gate.push("https://example.com/fake.pmtiles")
|
||||
gate.push("Wed, 10 Sep 2026 08:00:01 GMT")
|
||||
thread&.join
|
||||
end
|
||||
|
||||
|
||||
Reference in new issue
Block a user