Track what was actually built, and flag when it's stale
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 16s

The .pmtiles files themselves carry no record of what upstream
snapshot they came from — only a local mtime. Each successful build
now writes a small sidecar (offline.pmtiles.meta.json: the Protomaps
build date used; terrain.pmtiles.meta.json: the Mapterhorn source
names/md5sums) recording that, surfaced in the panel next to the
existing size/updated line.

New GET .../check-update routes compare the recorded build against
the live upstream index/manifest — true/false/null, null meaning
"don't know" (no local build yet, or the check itself failed) rather
than a false "no". Kept separate from the plain status endpoints
(which map.js's own topbar dot hits on every page load) since this
one reaches an external service; only called once when the Offline
data panel is actually opened.

Also: flag emoji next to each country in the picker (derived from the
ISO code via Unicode regional indicators, no image assets needed).
This commit is contained in:
randy committed 2026-09-11 13:18:32 +02:00
1 parent 53c9a82af2
commit 4a3862189f
7 files changed
+261 -15

No files matched your search

+76
View File
@@ -189,6 +189,7 @@ module Maptool
run_pmtiles!("extract", source, tmp_path, "--region=#{region_path}", "--maxzoom=15")
File.rename(tmp_path, File.join(data_dir, "offline.pmtiles"))
File.write(offline_meta_path(data_dir), JSON.generate(source: File.basename(URI(source).path)))
ensure
FileUtils.remove_entry(scratch_dir) if scratch_dir && Dir.exist?(scratch_dir)
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
@@ -208,6 +209,33 @@ module Maptool
end
private_class_method :latest_protomaps_build_url
# Sidecar recording which Protomaps build (a date-stamped filename, e.g.
# "20260910.pmtiles") the on-disk offline.pmtiles was actually extracted
# from — the file itself carries no such record. nil if never built (or
# built before this existed).
def self.offline_meta(data_dir)
read_json(offline_meta_path(data_dir))
end
# Compares the persisted build against the current latest without
# rebuilding anything — nil (not true/false) if there's nothing to
# compare yet (no local build) or the upstream index couldn't be
# reached, so the caller can tell "don't know" apart from "no".
def self.offline_update_available?(data_dir, fetch_build_url: method(:latest_protomaps_build_url))
meta = offline_meta(data_dir)
return nil unless meta
latest = File.basename(URI(fetch_build_url.call).path)
latest != meta["source"]
rescue BuildError
nil
end
def self.offline_meta_path(data_dir)
File.join(data_dir, "offline.pmtiles.meta.json")
end
private_class_method :offline_meta_path
# --- terrain.pmtiles ---
# planet.pmtiles (z0-12, always) plus whichever regional chunks
@@ -248,6 +276,7 @@ module Maptool
run_pmtiles!("merge", *extracts, tmp_path)
end
File.rename(tmp_path, File.join(data_dir, "terrain.pmtiles"))
File.write(terrain_meta_path(data_dir), JSON.generate(sources: [planet, *chunks].map { |i| { name: i["name"], md5sum: i["md5sum"] } }))
ensure
FileUtils.remove_entry(scratch_dir) if scratch_dir && Dir.exist?(scratch_dir)
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
@@ -269,8 +298,55 @@ module Maptool
end
private_class_method :fetch_mapterhorn_manifest
# Sidecar recording which Mapterhorn sources (name + md5sum) the on-disk
# terrain.pmtiles was actually extracted+merged from. nil if never built.
def self.terrain_meta(data_dir)
read_json(terrain_meta_path(data_dir))
end
# Same "don't know" vs. "no" distinction as offline_update_available?
# above. Recomputes which sources *would* be selected for the current
# selection (same manifest + bbox_intersects? this build itself uses)
# and compares their md5sums against what's recorded — so this also
# correctly says "yes" if the selection changed since the last build
# (a chunk now relevant that wasn't recorded), not just if Mapterhorn's
# own data moved on.
def self.terrain_update_available?(data_dir, fetch_manifest: method(:fetch_mapterhorn_manifest))
meta = terrain_meta(data_dir)
return nil unless meta
codes = selected_regions(data_dir)
return nil if codes.empty?
items = fetch_manifest.call
planet = items.find { |item| item["name"] == MAPTERHORN_PLANET_NAME }
return nil unless planet
Dir.mktmpdir do |dir|
_region_path, bbox = region_geojson_and_bbox(codes, dir)
south, west, north, east = bbox
chunks = items.select { |item| item["name"] != MAPTERHORN_PLANET_NAME && bbox_intersects?(item, south, west, north, east) }
current = [planet, *chunks].map { |i| { "name" => i["name"], "md5sum" => i["md5sum"] } }.sort_by { |s| s["name"] }
current != (meta["sources"] || []).sort_by { |s| s["name"] }
end
rescue BuildError
nil
end
def self.terrain_meta_path(data_dir)
File.join(data_dir, "terrain.pmtiles.meta.json")
end
private_class_method :terrain_meta_path
# --- shared ---
def self.read_json(path)
File.exist?(path) ? JSON.parse(File.read(path)) : nil
rescue JSON::ParserError
nil
end
private_class_method :read_json
# ENV read lazily (not a load-time constant) so tests can point this at
# a fake script without a require-order dance.
def self.pmtiles_bin