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
+90
-112
@@ -28,29 +28,46 @@ const CENTER = [17.1077, 48.1486];
|
||||
const ZOOM = 13;
|
||||
const ONLINE_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
// Recolors OpenFreeMap Liberty to the same NPS-inspired palette as
|
||||
// OFFLINE_THEME below — warm cream ground, muted roads/boundaries — mapped
|
||||
// onto Liberty's OpenMapTiles layer ids instead of protomaps' theme roles
|
||||
// (the two schemas don't share layer names, so this is a separate
|
||||
// hand-built table, not a shared object). Only the layers that dominate the
|
||||
// map's overall color read are touched; labels, halos, rail, and minor
|
||||
// overlays (cemetery/hospital/school/pitch/track shading, building
|
||||
// outlines) are left at Liberty's own defaults — same "not a full
|
||||
// recolor" scope as OFFLINE_THEME. Road casings have no protomaps-role
|
||||
// equivalent to reuse (offline roads don't get a separate outline), so
|
||||
// #c9a66b there is reasoned to fit the family rather than sourced from
|
||||
// anywhere real.
|
||||
// Recolors OpenFreeMap Liberty to a warm, NPS-Park-Tiles-inspired palette —
|
||||
// cream ground, muted roads/boundaries — mapped onto Liberty's OpenMapTiles
|
||||
// layer ids. Applied identically regardless of which PMTiles data actually
|
||||
// backs the "openmaptiles" source (see offlineStyle() below): offline tiles
|
||||
// are built by Planetiler's own OpenMapTiles profile (docs/development.adoc),
|
||||
// the same schema Liberty already expects, so one patch serves both —
|
||||
// no separate offline theme/palette to keep in sync. Only the layers that
|
||||
// dominate the map's overall color read are touched; labels, halos, rail,
|
||||
// and minor overlays (cemetery/hospital/school/pitch/track shading,
|
||||
// building outlines) are left at Liberty's own defaults.
|
||||
//
|
||||
// wood/grass are deliberately left unpatched — Liberty's own green reads
|
||||
// better than this app's earlier brown/khaki recolor, and OFFLINE_THEME's
|
||||
// wood_a/scrub_a below were changed to match Liberty's green instead.
|
||||
// wood/grass are deliberately left unpatched — Liberty's own green (a
|
||||
// translucent landcover_wood/landcover_grass wash) already reads well over
|
||||
// the patched cream background.
|
||||
const ONLINE_STYLE_PATCH = {
|
||||
background: { "background-color": "#f9f6ef" },
|
||||
// Liberty washes every built-up area in a translucent gray-tan below
|
||||
// z12 (landuse_residential, maxzoom 12 — gone by this app's default
|
||||
// z13) that the offline schema has no equivalent of; protomaps just
|
||||
// leaves that ground as plain background/earth. Matched to background
|
||||
// here rather than left alone, so a zoomed-out view doesn't read as
|
||||
// visibly dingier online than offline.
|
||||
landuse_residential: { "fill-color": "#f9f6ef" },
|
||||
landcover_sand: { "fill-color": "#f0e4c4" },
|
||||
water: { "fill-color": "#a3cedf" },
|
||||
park: { "fill-color": "#b3e378" },
|
||||
park_outline: { "line-color": "#546619" },
|
||||
building: { "fill-color": "#ded7cc" },
|
||||
// fill-outline-color isn't touched by a plain fill-color patch — Liberty
|
||||
// draws it separately, defaulting to a bright green that reads as a
|
||||
// distinct border around named conservation areas (e.g. the Malé
|
||||
// Karpaty CHKO). Matched to the fill itself, and park_outline's own
|
||||
// dashed line hidden below, so a reserve blends in as ordinary forest —
|
||||
// offline has no equivalent stroke at all (see park_a/park_b below),
|
||||
// and the point was never to call reserves out as visibly different,
|
||||
// on either side.
|
||||
park: { "fill-color": "#b3e378", "fill-outline-color": "#b3e378" },
|
||||
park_outline: { "line-opacity": 0 },
|
||||
// fill-opacity: 0.5 matches the offline theme's own "buildings" layer —
|
||||
// without it, Liberty's dense individual building footprints (each with
|
||||
// its own outline) paint fully opaque and read much heavier/grayer than
|
||||
// offline's softer, half-transparent fill over the same cream ground.
|
||||
building: { "fill-color": "#ded7cc", "fill-opacity": 0.5 },
|
||||
boundary_2: { "line-color": "#546619" },
|
||||
boundary_3: { "line-color": "#546619" },
|
||||
|
||||
@@ -58,19 +75,30 @@ const ONLINE_STYLE_PATCH = {
|
||||
// minor (residential/service/track), pedestrian — each repeated for its
|
||||
// tunnel_/bridge_ counterpart so a road doesn't change color when it
|
||||
// dips into a tunnel or crosses a bridge. Rail is left alone.
|
||||
road_motorway: { "line-color": "#fdefbe" },
|
||||
road_trunk_primary: { "line-color": "#fdefbe" },
|
||||
road_secondary_tertiary: { "line-color": "#fef7dd" },
|
||||
road_link: { "line-color": "#fef7dd" },
|
||||
road_motorway_link: { "line-color": "#fef7dd" },
|
||||
//
|
||||
// line-width below is a flat number, not Liberty's own zoom-interpolated
|
||||
// expression — picked to match the offline theme's actual rendered width
|
||||
// at this app's default zoom (13; verified live via queryRenderedFeatures
|
||||
// on both sides), since without it Liberty's own curve draws surface
|
||||
// roads roughly twice as thick as offline at street level, reading as a
|
||||
// much bolder road network online.
|
||||
// ponytail: tuned for z13 only, not a matching curve across all zooms —
|
||||
// will drift back apart at very different zoom levels. Upgrade path:
|
||||
// sample offline's width at a few more zooms and replace with a matching
|
||||
// ["interpolate", ["linear"], ["zoom"], ...] expression.
|
||||
road_motorway: { "line-color": "#fdefbe", "line-width": 2.26 },
|
||||
road_trunk_primary: { "line-color": "#fdefbe", "line-width": 2.26 },
|
||||
road_secondary_tertiary: { "line-color": "#fef7dd", "line-width": 1.87 },
|
||||
road_link: { "line-color": "#fef7dd", "line-width": 1 },
|
||||
road_motorway_link: { "line-color": "#fef7dd", "line-width": 1 },
|
||||
road_minor: { "line-color": "#fffdf8" },
|
||||
road_service_track: { "line-color": "#fffdf8" },
|
||||
road_path_pedestrian: { "line-color": "#e9e2d8" },
|
||||
road_motorway_casing: { "line-color": "#c9a66b" },
|
||||
road_trunk_primary_casing: { "line-color": "#c9a66b" },
|
||||
road_secondary_tertiary_casing: { "line-color": "#c9a66b" },
|
||||
road_link_casing: { "line-color": "#c9a66b" },
|
||||
road_motorway_link_casing: { "line-color": "#c9a66b" },
|
||||
road_motorway_casing: { "line-color": "#c9a66b", "line-width": 1.48 },
|
||||
road_trunk_primary_casing: { "line-color": "#c9a66b", "line-width": 1.48 },
|
||||
road_secondary_tertiary_casing: { "line-color": "#c9a66b", "line-width": 1 },
|
||||
road_link_casing: { "line-color": "#c9a66b", "line-width": 1.5 },
|
||||
road_motorway_link_casing: { "line-color": "#c9a66b", "line-width": 1.5 },
|
||||
road_minor_casing: { "line-color": "#c9a66b" },
|
||||
road_service_track_casing: { "line-color": "#c9a66b" },
|
||||
|
||||
@@ -161,95 +189,45 @@ async function loadOnlineStyle() {
|
||||
|
||||
const ONLINE_STYLE = await loadOnlineStyle();
|
||||
|
||||
// A vendored copy of the same Liberty style ONLINE_STYLE fetches live
|
||||
// (public/vendor/liberty-style/liberty.json — a point-in-time snapshot, not
|
||||
// auto-updated) that offlineStyle() below patches+repoints the exact same
|
||||
// way. Offline mode can't depend on ONLINE_STYLE_URL actually being
|
||||
// reachable — that fetch failing/timing out is the normal case in the
|
||||
// field, not an edge case — so this is a local, same-origin fetch instead
|
||||
// (fast and always available, since it's served by this app itself).
|
||||
const OFFLINE_STYLE_TEMPLATE = await fetch("/vendor/liberty-style/liberty.json").then((response) => response.json());
|
||||
|
||||
const protocol = new pmtiles.Protocol();
|
||||
addProtocol("pmtiles", protocol.tile);
|
||||
|
||||
// Nudged from protomaps-themes-base's stock "light" theme towards the
|
||||
// National Park Service's own "Park Tiles" cartography — a warm,
|
||||
// Harpers-Ferry-Center-derived look. Not their actual map: a handful of
|
||||
// values below (marked) are the genuine paint colors pulled from a public
|
||||
// Park Tiles style JSON
|
||||
// (nps.gov/lib/npmap.js/style/ck72fwp2642dv07o7tbqinvz4.json, the "Imagery"
|
||||
// variant — the only one of their four styles served from a plain,
|
||||
// unauthenticated URL; "Standard"/"Slate"/"Light" resolve through Mapbox
|
||||
// Studio's private API with an access token, which isn't something to
|
||||
// piggyback on even though it's technically visible in their own JS bundle).
|
||||
// The rest are reasoned to match that same family (warm cream ground, olive
|
||||
// greens, muted blue) rather than sourced. Not a full recolor: labels,
|
||||
// casings, and roles with no clean real-color anchor are left at theme
|
||||
// defaults.
|
||||
// Builds the offline style from OFFLINE_STYLE_TEMPLATE — the identical
|
||||
// Liberty JSON + ONLINE_STYLE_PATCH recolor as the online map, just
|
||||
// repointed at a local PMTiles source instead of tiles.openfreemap.org.
|
||||
// This works because data/offline.pmtiles is now built by Planetiler's own
|
||||
// OpenMapTiles profile (see docs/development.adoc), the same vector schema
|
||||
// Liberty expects — before that, offline tiles used a different schema
|
||||
// (protomaps' own basemaps) needing a completely separate hand-tuned
|
||||
// theme (protomaps-themes-base + OFFLINE_THEME) that could never be made
|
||||
// to match ONLINE_STYLE_PATCH's look exactly, no matter how much either
|
||||
// side got tuned.
|
||||
//
|
||||
// Recreating the color choices is legitimate — that's not the kind of thing
|
||||
// a data license restricts (see docs/architecture.adoc's "Offline maps"
|
||||
// section) — what stays off-limits is their actual tile *service*: its
|
||||
// OSM/terrain layers are licensed through Mapbox Atlas, a commercial,
|
||||
// non-sublicensable data service scoped to NPS.
|
||||
const OFFLINE_THEME = {
|
||||
...protomaps_themes_base.namedTheme("light"),
|
||||
background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%)
|
||||
earth: "#f9f6ef",
|
||||
water: "#a3cedf",
|
||||
// Matches OpenFreeMap Liberty's own wood/grass green (its landcover_wood
|
||||
// hsla(98,61%,72%,0.7) and landcover_grass rgba(176,213,154,1) at 0.3
|
||||
// opacity, blended over the cream background) — the online map's actual
|
||||
// look whenever ONLINE_STYLE_PATCH's brown/khaki recolor fails to apply
|
||||
// (see loadOnlineStyle's fetch fallback) turned out to be the look this
|
||||
// app's users actually want, so offline was changed to match it instead
|
||||
// of chasing the patch's own reliability.
|
||||
wood_a: "#c3e8ab",
|
||||
wood_b: "#a8d888",
|
||||
scrub_a: "#dcecc9",
|
||||
scrub_b: "#cfe3b8",
|
||||
// Close to wood_b, not the more saturated Park Tiles green: protomaps'
|
||||
// theme routes "forest"/"nature_reserve"/"protected_area" (e.g. the Malé
|
||||
// Karpaty CHKO boundary) through this same park_a/park_b pair as actual
|
||||
// named parks — Liberty's online style doesn't make that reserve-boundary
|
||||
// distinction at all, so a visibly different green here reads as an
|
||||
// offline-only blotch rather than a deliberate distinction.
|
||||
park_a: "#c3e8ab",
|
||||
park_b: "#a8d888",
|
||||
sand: "#f0e4c4",
|
||||
beach: "#f0e4c4",
|
||||
buildings: "#ded7cc",
|
||||
pedestrian: "#e9e2d8",
|
||||
highway: "#fdefbe", // real: Park Tiles' primary/highway road color
|
||||
major: "#fef7dd", // real: Park Tiles' secondary road color
|
||||
// "minor" (singular) is never actually read by protomaps' own paint
|
||||
// expressions — only the zoom-interpolated minor_a/minor_b pair is, so a
|
||||
// plain "minor" key here was dead: ordinary streets were silently
|
||||
// falling back to the base theme's default pale yellow instead of this
|
||||
// app's cream.
|
||||
minor_a: "#fffdf8",
|
||||
minor_b: "#fffdf8",
|
||||
link: "#fef7dd",
|
||||
// Distinct brown, not cream: protomaps' "other"/"path" kind is what a
|
||||
// camper actually needs called out — unpaved tracks and footpaths,
|
||||
// separate from paved minor_road/service (minor_service, left at the
|
||||
// base theme's default since it's real driveways/parking aisles, not
|
||||
// unpaved). Already rendered dashed by the theme itself; this just picks
|
||||
// a real "dirt" tone instead of unstyled white.
|
||||
other: "#c9a66b",
|
||||
boundaries: "#546619", // real: Park Tiles' park-boundary line color
|
||||
};
|
||||
|
||||
// glyphs/sprite point at vendored local copies (public/vendor/fonts,
|
||||
// public/vendor/liberty-sprite) rather than tiles.openfreemap.org's —
|
||||
// glyphs are Latin-only (see docs/architecture.adoc: labels in other
|
||||
// scripts won't render offline), sprite is a one-time snapshot like
|
||||
// OFFLINE_STYLE_TEMPLATE itself. ne2_shaded (Liberty's low-zoom shaded-
|
||||
// relief backdrop, z0-6 only) is left pointing at its remote URL
|
||||
// unchanged — offline usage is regional, not whole-earth zoomed out, so
|
||||
// this only ever silently fails to load at a zoom level offline users
|
||||
// aren't in, the same graceful-degradation story as this app's other
|
||||
// online-only extras (Wikipedia, live radar).
|
||||
function offlineStyle() {
|
||||
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
|
||||
|
||||
return {
|
||||
version: 8,
|
||||
glyphs: `${window.location.origin}/vendor/fonts/{fontstack}/{range}.pbf`,
|
||||
sources: {
|
||||
offline: {
|
||||
type: "vector",
|
||||
url: `pmtiles://${pmtilesUrl}`,
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
},
|
||||
},
|
||||
// lang: "en" pulls in labels_layers (see docs/architecture.adoc) — only
|
||||
// the Latin-script glyph ranges are vendored, so labels using other
|
||||
// scripts won't render offline.
|
||||
layers: protomaps_themes_base.layers("offline", OFFLINE_THEME, { lang: "en" }),
|
||||
};
|
||||
const style = patchOnlineStyle(structuredClone(OFFLINE_STYLE_TEMPLATE));
|
||||
style.sources.openmaptiles.url = `pmtiles://${window.location.origin}/tiles/offline.pmtiles`;
|
||||
style.glyphs = `${window.location.origin}/vendor/fonts/{fontstack}/{range}.pbf`;
|
||||
style.sprite = `${window.location.origin}/vendor/liberty-sprite/ofm`;
|
||||
return style;
|
||||
}
|
||||
|
||||
// A sidebar icon that opens a full-height side panel (like the GPX one)
|
||||
|
||||
Reference in new issue
Block a user