Apply the NPS-inspired palette to the online map too

ONLINE_STYLE_PATCH mirrors OFFLINE_THEME's color choices onto
OpenFreeMap Liberty's OpenMapTiles layer ids (a separate hand-built
table -- the two schemas share no layer names). loadOnlineStyle
fetches Liberty's style.json once, patches it in place, and the
result replaces the plain URL everywhere ONLINE_STYLE is used --
MapLibre accepts a style object anywhere it accepts a style URL, so
applyStyle needed no change.

Runs before the map (and the rest of the page) is constructed, so
there's no flash of Liberty's own colors first -- but with a 3s
timeout and a catch-all fallback to the plain URL, since this app is
meant to keep working offline in the field and blocking startup on a
remote style server would undermine that.

Verified in a real browser: online map now visually matches the
offline theme (cream ground, brown/khaki land cover, green parks,
muted roads), no console errors, Online/Offline toggle still works.
This commit is contained in:
randy committed 2026-09-07 23:12:20 +02:00
1 parent 24b6180c03
commit ceaaa0bb92
3 files changed
+134 -3

No files matched your search

+107 -1
View File
@@ -19,7 +19,113 @@ import * as maplibregl from "/vendor/maplibre-gl/maplibre-gl.mjs";
const CENTER = [17.1077, 48.1486];
const ZOOM = 13;
const ONLINE_STYLE = "https://tiles.openfreemap.org/styles/liberty";
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, brown/khaki land cover, green
// reserved for parks, 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.
const ONLINE_STYLE_PATCH = {
background: { "background-color": "#f9f6ef" },
landcover_wood: { "fill-color": "#c9bd9c" },
landcover_grass: { "fill-color": "#d9cfb0" },
landcover_sand: { "fill-color": "#f0e4c4" },
water: { "fill-color": "#a3cedf" },
park: { "fill-color": "#b3e378" },
park_outline: { "line-color": "#546619" },
building: { "fill-color": "#ded7cc" },
boundary_2: { "line-color": "#546619" },
boundary_3: { "line-color": "#546619" },
// Highway (motorway/trunk/primary), major (secondary/tertiary/link),
// 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" },
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_minor_casing: { "line-color": "#c9a66b" },
road_service_track_casing: { "line-color": "#c9a66b" },
tunnel_motorway: { "line-color": "#fdefbe" },
tunnel_trunk_primary: { "line-color": "#fdefbe" },
tunnel_secondary_tertiary: { "line-color": "#fef7dd" },
tunnel_link: { "line-color": "#fef7dd" },
tunnel_motorway_link: { "line-color": "#fef7dd" },
tunnel_minor: { "line-color": "#fffdf8" },
tunnel_service_track: { "line-color": "#fffdf8" },
tunnel_path_pedestrian: { "line-color": "#e9e2d8" },
tunnel_motorway_casing: { "line-color": "#c9a66b" },
tunnel_trunk_primary_casing: { "line-color": "#c9a66b" },
tunnel_secondary_tertiary_casing: { "line-color": "#c9a66b" },
tunnel_link_casing: { "line-color": "#c9a66b" },
tunnel_motorway_link_casing: { "line-color": "#c9a66b" },
tunnel_street_casing: { "line-color": "#c9a66b" },
bridge_motorway: { "line-color": "#fdefbe" },
bridge_trunk_primary: { "line-color": "#fdefbe" },
bridge_secondary_tertiary: { "line-color": "#fef7dd" },
bridge_link: { "line-color": "#fef7dd" },
bridge_motorway_link: { "line-color": "#fef7dd" },
bridge_street: { "line-color": "#fffdf8" },
bridge_service_track: { "line-color": "#fffdf8" },
bridge_path_pedestrian: { "line-color": "#e9e2d8" },
bridge_motorway_casing: { "line-color": "#c9a66b" },
bridge_trunk_primary_casing: { "line-color": "#c9a66b" },
bridge_secondary_tertiary_casing: { "line-color": "#c9a66b" },
bridge_link_casing: { "line-color": "#c9a66b" },
bridge_motorway_link_casing: { "line-color": "#c9a66b" },
bridge_street_casing: { "line-color": "#c9a66b" },
bridge_path_pedestrian_casing: { "line-color": "#c9a66b" },
};
function patchOnlineStyle(style) {
for (const layer of style.layers) {
const patch = ONLINE_STYLE_PATCH[layer.id];
if (patch) Object.assign((layer.paint ??= {}), patch);
}
return style;
}
// Fetched (not just referenced by URL) so its colors can be patched before
// the map ever sees it — MapLibre accepts a style object anywhere it'd
// accept a style URL, so every other use of ONLINE_STYLE (applyStyle's
// map.setStyle) needs no change. This runs before the map — and the rest
// of the page's setup — is constructed (see the top-level await below), so
// it falls back to the plain URL, letting MapLibre fetch and render it
// unpatched, both when the fetch fails outright and when it's just slow
// (a 3s timeout): this app is meant to work offline in the field, and
// blocking the entire page on a remote style server would undermine that
// even though this particular fetch is only for the online map.
async function loadOnlineStyle() {
try {
const response = await fetch(ONLINE_STYLE_URL, { signal: AbortSignal.timeout(3000) });
return patchOnlineStyle(await response.json());
} catch (err) {
return ONLINE_STYLE_URL;
}
}
const ONLINE_STYLE = await loadOnlineStyle();
const protocol = new pmtiles.Protocol();
addProtocol("pmtiles", protocol.tile);