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:
randy committed 2026-08-25 22:38:56 +02:00
1 parent 06ea8ea141
commit 0887ccbfe7
12 files changed
+163 -217

No files matched your search

+1 -2
View File
@@ -10,8 +10,7 @@ The project's shape is still open — this is the initial scaffold.
== Status
Early scaffold. The web application serves a vector map (MapLibre GL
JS), styled to resemble https://osmand.net/map[OsmAnd's own map], with
an online style and an optional offline style backed by a local
JS), with an online style and an optional offline style backed by a local
PMTiles file, an animated RainViewer radar overlay, GPX
upload/display (points and tracks, organized in folders), and a live
position marker from a local GNSS receiver via gpsd.
+27 -47
View File
@@ -70,59 +70,39 @@ support) just returns byte ranges of the file; pmtiles.js in the
browser reads the archive's own directory and requests only the bytes
it needs for the tiles on screen.
=== Known limitation: no offline text labels
=== Offline text labels and color palette
The offline style only renders `protomaps-themes-base`'s non-label
layers (roads, buildings, land use, water — no place names or street
labels). Label rendering in MapLibre needs a "glyphs" server serving
font PBF ranges, which would mean vendoring a font glyph set locally
too; that has not been done, to keep the initial scope manageable.
Revisit this if labelled offline maps turn out to matter in practice.
Label rendering in MapLibre needs a "glyphs" server serving font PBF
ranges (256 Unicode code points each); `data/offline.pmtiles`'s fields
(`name`, `name:en`, `script`, ...) match what
`protomaps-themes-base`'s `layers(source, theme, { lang: "en" })`
expects for its label layers (verified against the actual PMTiles
vector layer schema), so passing that option turns labels on. The
glyphs themselves are vendored under `public/vendor/fonts/` — Noto
Sans Regular/Italic/Medium, ranges `0-255` and `256-511`
(Basic Latin + Latin-1 Supplement + Latin Extended-A, SIL OFL
licensed, from
https://github.com/protomaps/basemaps-assets[protomaps/basemaps-assets]),
covering accented Latin place names (Slovak, Czech, Polish, German,
...). Labels in other scripts (Cyrillic, Greek, CJK, ...) won't render
offline, since those glyph ranges aren't vendored.
`public/js/map.js`'s `OFFLINE_THEME` also nudges a handful of color
roles (background, water, wood, buildings, the two road-importance
tiers) from `protomaps-themes-base`'s stock "light" theme towards the
online "liberty" style's actual palette (sampled from its style JSON),
so Online/Offline don't look like two different apps. This is
deliberately partial — most roles (labels, casings, minor terrain
fills, ...) are left at their theme defaults — not the kind of
full per-layer recolor a previous iteration of this app did (and
removed, along with any dependency on OsmAnd's own tile server, which
this app cannot use per OsmAnd's terms restricting tile use to "personal,
non-commercial use within OsmAnd applications and platforms").
No PMTiles file ships with the repository — it is expected to be
supplied locally (see `docs/development.adoc`) and stays gitignored
because such extracts are typically large binary files.
=== Visual style
Both styles are recolored, by `public/js/osmand-style.js`, to
approximate the look of https://osmand.net/map[OsmAnd's own web map]
(cream background, green forests, orange roads, purple admin
boundaries) — deliberately *not* by using OsmAnd's tile server
directly: its terms restrict tile use to "personal, non-commercial use
within OsmAnd applications and platforms", which this app isn't.
One shared color palette drives two independent recolorings:
* *Online*: `onlineStyle()` fetches OpenFreeMap's raw "liberty" style
JSON and patches `paint` colors on its layers in place — matching a
layer to a category (water, wood, park, landuse, building, major/
minor road, boundary) by its id, then walking that layer's color
properties (`background-color`, `fill-color`, `line-color`,
`fill-extrusion-color`, and any `*-outline-color`) and replacing
every color-shaped leaf — including inside `interpolate`/other
expressions — with the category's palette color, leaving non-color
leaves (operators, zoom stops, `["get", ...]`) untouched. Labels and
railways are left at their original colors — only categorized layer
types (`background`/`fill`/`line`/`fill-extrusion`) are touched at
all, so `symbol` layers (all labels/icons) are never in scope.
Result is cached after the first fetch.
* *Offline*: `offlineTheme()` overrides the relevant color roles on a
`protomaps-themes-base` `Theme` object (starting from
`namedTheme("light")`), leaving roles it doesn't set (labels — moot,
see above — rail, tunnels, POI colors) at their defaults.
=== Known limitation: coarser road colors offline
protomaps-themes-base's `Theme` only has two paint-relevant road tiers
(`major` / `minor_a`+`minor_b`, plus `link` for ramps) versus
OpenFreeMap's finer-grained classes (`road_motorway`,
`road_trunk_primary`, `road_secondary_tertiary`, `road_minor`, ...).
Mapping `major` to the palette's road-major orange colors more roads
orange offline than online (protomaps' "major" bucket is broader than
just motorway/trunk/primary) — a real difference in how coarse the two
underlying style systems are, not a bug in the recoloring itself.
== Weather radar overlay
`public/js/radar.js` adds a "Radar" map control that overlays recent
+14
View File
@@ -23,6 +23,20 @@ BSD-3-Clause licensed.
| https://github.com/protomaps/basemaps
|===
Offline map text labels need font glyphs too (see
`docs/architecture.adoc`), vendored under `public/vendor/fonts/` —
SIL Open Font License, not BSD-3-Clause like the above:
[cols="1,2"]
|===
| Asset | Source
| Noto Sans Regular/Italic/Medium glyph PBFs (ranges `0-255`,
`256-511` only)
| https://github.com/protomaps/basemaps-assets[protomaps/basemaps-assets]
(`fonts/`, `OFL.txt` included alongside)
|===
The online map style (not vendored, fetched at runtime when the
"Online" layer is selected) is
https://tiles.openfreemap.org/styles/liberty[OpenFreeMap "liberty"],
+28 -8
View File
@@ -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
-160
View File
@@ -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;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.