Track what was actually built, and flag when it's stale
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:
1 parent
53c9a82af2
commit
4a3862189f
7 files changed
+261
-15
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user