Drop the OsmAnd-recolor map theme, add offline labels and a lighter palette nudge
Remove osmand-style.js (the per-layer color-patching approach) and its docs entirely — back to the plain OpenFreeMap "liberty" style online, and protomaps-themes-base's stock theme offline as the base. Offline maps previously had no text labels at all: the protomaps-themes-base layers() call never passed a lang option, which is required to include its label layers. Fixed, backed by vendored Noto Sans Regular/Italic/Medium glyph PBFs (ranges 0-255 and 256-511 only — Basic Latin + Latin-1 Supplement + Latin Extended-A, enough for accented Central European place names; other scripts won't render offline). Verified against the actual offline.pmtiles vector layer schema that the name/name:en/script fields this expects are present. Also nudge a handful of OFFLINE_THEME color roles (background, water, wood, buildings, the two road-importance tiers) towards the online style's actual palette (sampled from its style JSON) so switching Online/Offline doesn't look like two different apps — deliberately partial, not the removed full recolor.
This commit is contained in:
1 parent
06ea8ea141
commit
0887ccbfe7
12 files changed
+163
-217
No files matched your search
+28
-8
@@ -7,24 +7,41 @@ import {
|
||||
import { RadarControl } from "/js/radar.js";
|
||||
import { GpxPanelControl } from "/js/gpx.js";
|
||||
import { GnssControl } from "/js/gnss.js";
|
||||
import { onlineStyle, offlineTheme } from "/js/osmand-style.js";
|
||||
import { LAYERS_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
|
||||
const CENTER = [17.1077, 48.1486];
|
||||
const ZOOM = 13;
|
||||
const ONLINE_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
const protocol = new pmtiles.Protocol();
|
||||
addProtocol("pmtiles", protocol.tile);
|
||||
|
||||
// No label layers here (see docs/architecture.adoc): rendering text needs a
|
||||
// glyphs server, which the offline style deliberately does not depend on.
|
||||
// A few color roles nudged from protomaps-themes-base's stock "light" theme
|
||||
// towards the online OpenFreeMap "liberty" style's actual palette (sampled
|
||||
// from its style JSON — see docs/architecture.adoc), so switching between
|
||||
// Online/Offline doesn't look like two different apps. Not a full recolor:
|
||||
// most roles (labels, casings, minor terrain fills, ...) are left at their
|
||||
// theme defaults.
|
||||
const OFFLINE_THEME = {
|
||||
...protomaps_themes_base.namedTheme("light"),
|
||||
background: "#f8f4f0",
|
||||
earth: "#f8f4f0",
|
||||
water: "#9ebdff",
|
||||
wood_a: "#ace38c",
|
||||
wood_b: "#ace38c",
|
||||
buildings: "#dcd9d6",
|
||||
highway: "#f2934a",
|
||||
major: "#ffeeaa",
|
||||
link: "#ffeeaa",
|
||||
};
|
||||
|
||||
function offlineStyle() {
|
||||
const theme = offlineTheme(protomaps_themes_base.namedTheme("light"));
|
||||
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
|
||||
|
||||
return {
|
||||
version: 8,
|
||||
glyphs: `${window.location.origin}/vendor/fonts/{fontstack}/{range}.pbf`,
|
||||
sources: {
|
||||
offline: {
|
||||
type: "vector",
|
||||
@@ -32,7 +49,10 @@ function offlineStyle() {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
},
|
||||
},
|
||||
layers: protomaps_themes_base.layers("offline", theme),
|
||||
// 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" }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +103,7 @@ class LayerToggleControl {
|
||||
|
||||
const map = new MaplibreMap({
|
||||
container: "map",
|
||||
style: await onlineStyle(),
|
||||
style: ONLINE_STYLE,
|
||||
center: CENTER,
|
||||
zoom: ZOOM,
|
||||
});
|
||||
@@ -108,9 +128,9 @@ new GpxPanelControl(map).mount(sidebar);
|
||||
new RadarControl(map).mount(sidebar);
|
||||
new GnssControl(map).mount(sidebar);
|
||||
|
||||
async function applyStyle(name) {
|
||||
function applyStyle(name) {
|
||||
layerToggle.setActive(name);
|
||||
map.setStyle(name === "offline" ? offlineStyle() : await onlineStyle());
|
||||
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
|
||||
}
|
||||
|
||||
// If a local PMTiles file is present, prefer it as the default style — its
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
// Approximates the look of OsmAnd's own map render (osmand.net/map — muted
|
||||
// earth tones, cream background, orange roads) without depending on their
|
||||
// tile server: OsmAnd's terms restrict tile use to "personal, non-commercial
|
||||
// use within OsmAnd applications and platforms", which this app isn't.
|
||||
//
|
||||
// One shared palette drives two independent recolorings so Online and
|
||||
// Offline look consistent: OpenFreeMap's "liberty" vector style (paint
|
||||
// colors patched in place, see recolorLayer) for Online, and a custom
|
||||
// protomaps-themes-base Theme (see offlineTheme) for Offline.
|
||||
|
||||
const PALETTE = {
|
||||
background: "#f1ecd7",
|
||||
water: "#aad3df",
|
||||
wood: "#c8dfb0",
|
||||
park: "#d3e8b8",
|
||||
landuse: "#ece4c6",
|
||||
building: "#d9cead",
|
||||
buildingOutline: "#c7bb92",
|
||||
roadMajor: "#eb9d43",
|
||||
roadMajorCasing: "#c9822c",
|
||||
roadMinor: "#fdf8ee",
|
||||
roadMinorCasing: "#cfc09a",
|
||||
boundary: "#a98fc2",
|
||||
};
|
||||
|
||||
const LIBERTY_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
|
||||
let cachedOnlineStyle = null;
|
||||
|
||||
// Fetches OpenFreeMap's "liberty" style and recolors it in place. Cached —
|
||||
// the recoloring is pure, and re-fetching on every Online/Offline toggle
|
||||
// would be wasteful.
|
||||
export async function onlineStyle() {
|
||||
if (cachedOnlineStyle) return cachedOnlineStyle;
|
||||
|
||||
const response = await fetch(LIBERTY_STYLE_URL);
|
||||
const style = await response.json();
|
||||
style.layers = style.layers.map(recolorLayer);
|
||||
cachedOnlineStyle = style;
|
||||
return style;
|
||||
}
|
||||
|
||||
// Overrides the color roles that define the "look" on top of a
|
||||
// protomaps-themes-base Theme (e.g. namedTheme("light")), leaving roles it
|
||||
// doesn't set (labels, rail, tunnels, POI colors) at their defaults.
|
||||
export function offlineTheme(baseTheme) {
|
||||
return {
|
||||
...baseTheme,
|
||||
background: PALETTE.background,
|
||||
earth: PALETTE.background,
|
||||
water: PALETTE.water,
|
||||
wood_a: PALETTE.wood,
|
||||
wood_b: PALETTE.wood,
|
||||
park_a: PALETTE.park,
|
||||
park_b: PALETTE.park,
|
||||
scrub_a: PALETTE.park,
|
||||
scrub_b: PALETTE.park,
|
||||
pedestrian: PALETTE.landuse,
|
||||
industrial: PALETTE.landuse,
|
||||
school: PALETTE.landuse,
|
||||
hospital: PALETTE.landuse,
|
||||
buildings: PALETTE.building,
|
||||
highway_casing_early: PALETTE.roadMajorCasing,
|
||||
highway_casing_late: PALETTE.roadMajorCasing,
|
||||
highway: PALETTE.roadMajor,
|
||||
major_casing_early: PALETTE.roadMajorCasing,
|
||||
major_casing_late: PALETTE.roadMajorCasing,
|
||||
major: PALETTE.roadMajor,
|
||||
link_casing: PALETTE.roadMajorCasing,
|
||||
link: PALETTE.roadMajor,
|
||||
minor_casing: PALETTE.roadMinorCasing,
|
||||
minor_service_casing: PALETTE.roadMinorCasing,
|
||||
minor_a: PALETTE.roadMinor,
|
||||
minor_b: PALETTE.roadMinor,
|
||||
other: PALETTE.roadMinor,
|
||||
minor_service: PALETTE.roadMinor,
|
||||
boundaries: PALETTE.boundary,
|
||||
};
|
||||
}
|
||||
|
||||
const FILL_PROP = /^(background-color|fill-color|line-color|fill-extrusion-color)$/;
|
||||
const OUTLINE_PROP = /outline-color$/;
|
||||
|
||||
const FILL_COLOR = {
|
||||
background: PALETTE.background,
|
||||
water: PALETTE.water,
|
||||
wood: PALETTE.wood,
|
||||
park: PALETTE.park,
|
||||
landuse: PALETTE.landuse,
|
||||
building: PALETTE.building,
|
||||
roadMajor: PALETTE.roadMajor,
|
||||
roadMinor: PALETTE.roadMinor,
|
||||
boundary: PALETTE.boundary,
|
||||
};
|
||||
|
||||
const OUTLINE_COLOR = {
|
||||
building: PALETTE.buildingOutline,
|
||||
roadMajor: PALETTE.roadMajorCasing,
|
||||
roadMinor: PALETTE.roadMinorCasing,
|
||||
park: "#bcdb9a",
|
||||
};
|
||||
|
||||
// Maps a "liberty" style layer id to one of the categories above, based on
|
||||
// the id naming conventions OpenFreeMap's style uses (verified against its
|
||||
// actual layer list — see docs/architecture.adoc). Returns null for
|
||||
// anything left at its original color: labels/icons (all "symbol" layers,
|
||||
// filtered by type below before this even runs), railways, and minor
|
||||
// terrain fills (ice/wetland/sand) that aren't part of OsmAnd's signature
|
||||
// look.
|
||||
function categoryFor(layerId) {
|
||||
if (layerId === "background") return "background";
|
||||
if (/rail/.test(layerId)) return null;
|
||||
if (/^water/.test(layerId)) return "water";
|
||||
if (/^landcover_wood/.test(layerId)) return "wood";
|
||||
if (/^(park|landcover_grass)/.test(layerId)) return "park";
|
||||
if (/^landuse/.test(layerId)) return "landuse";
|
||||
if (/^building/.test(layerId)) return "building";
|
||||
if (/^(road|bridge|tunnel)_(motorway|trunk_primary)/.test(layerId)) return "roadMajor";
|
||||
if (/^(road|bridge|tunnel)_/.test(layerId)) return "roadMinor";
|
||||
if (/^boundary/.test(layerId)) return "boundary";
|
||||
return null;
|
||||
}
|
||||
|
||||
const RECOLORABLE_TYPES = new Set(["background", "fill", "line", "fill-extrusion"]);
|
||||
|
||||
function recolorLayer(layer) {
|
||||
if (!RECOLORABLE_TYPES.has(layer.type) || !layer.paint) return layer;
|
||||
|
||||
const category = categoryFor(layer.id);
|
||||
if (!category) return layer;
|
||||
|
||||
const paint = { ...layer.paint };
|
||||
for (const [prop, value] of Object.entries(paint)) {
|
||||
if (OUTLINE_PROP.test(prop) && OUTLINE_COLOR[category]) {
|
||||
paint[prop] = replaceColors(value, OUTLINE_COLOR[category]);
|
||||
} else if (FILL_PROP.test(prop)) {
|
||||
paint[prop] = replaceColors(value, FILL_COLOR[category]);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...layer, paint };
|
||||
}
|
||||
|
||||
const COLOR_PATTERN = /^#[0-9a-f]{3,8}$/i;
|
||||
const COLOR_FN_PATTERN = /^(rgb|hsl)a?\(/i;
|
||||
|
||||
// Walks a paint value — a flat color string, or a MapLibre expression like
|
||||
// ["interpolate", ["linear"], ["zoom"], 5, "hsl(26,87%,62%)", 6, "#fc8"] —
|
||||
// and swaps every color-shaped string for `newColor`, leaving operators,
|
||||
// zoom/get references and numbers untouched. Generic across expression
|
||||
// types (interpolate/match/case/step/...) since it never inspects the
|
||||
// expression's operator, only whether each leaf looks like a color.
|
||||
function replaceColors(value, newColor) {
|
||||
if (typeof value === "string") {
|
||||
return COLOR_PATTERN.test(value) || COLOR_FN_PATTERN.test(value) ? newColor : value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => replaceColors(item, newColor));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in new issue
Block a user