diff --git a/README.adoc b/README.adoc index abcb053..545bb11 100644 --- a/README.adoc +++ b/README.adoc @@ -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. diff --git a/docs/architecture.adoc b/docs/architecture.adoc index 8952c88..79cb7a0 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -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 diff --git a/docs/third-party-notices.adoc b/docs/third-party-notices.adoc index fb99c28..de29d82 100644 --- a/docs/third-party-notices.adoc +++ b/docs/third-party-notices.adoc @@ -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"], diff --git a/public/js/map.js b/public/js/map.js index 0efb06e..ade5571 100644 --- a/public/js/map.js +++ b/public/js/map.js @@ -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 diff --git a/public/js/osmand-style.js b/public/js/osmand-style.js deleted file mode 100644 index d18385f..0000000 --- a/public/js/osmand-style.js +++ /dev/null @@ -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; -} diff --git a/public/vendor/fonts/Noto Sans Italic/0-255.pbf b/public/vendor/fonts/Noto Sans Italic/0-255.pbf new file mode 100644 index 0000000..d80cf45 Binary files /dev/null and b/public/vendor/fonts/Noto Sans Italic/0-255.pbf differ diff --git a/public/vendor/fonts/Noto Sans Italic/256-511.pbf b/public/vendor/fonts/Noto Sans Italic/256-511.pbf new file mode 100644 index 0000000..d8496c2 Binary files /dev/null and b/public/vendor/fonts/Noto Sans Italic/256-511.pbf differ diff --git a/public/vendor/fonts/Noto Sans Medium/0-255.pbf b/public/vendor/fonts/Noto Sans Medium/0-255.pbf new file mode 100644 index 0000000..4ef427b Binary files /dev/null and b/public/vendor/fonts/Noto Sans Medium/0-255.pbf differ diff --git a/public/vendor/fonts/Noto Sans Medium/256-511.pbf b/public/vendor/fonts/Noto Sans Medium/256-511.pbf new file mode 100644 index 0000000..96e9bb3 Binary files /dev/null and b/public/vendor/fonts/Noto Sans Medium/256-511.pbf differ diff --git a/public/vendor/fonts/Noto Sans Regular/0-255.pbf b/public/vendor/fonts/Noto Sans Regular/0-255.pbf new file mode 100644 index 0000000..7f65901 Binary files /dev/null and b/public/vendor/fonts/Noto Sans Regular/0-255.pbf differ diff --git a/public/vendor/fonts/Noto Sans Regular/256-511.pbf b/public/vendor/fonts/Noto Sans Regular/256-511.pbf new file mode 100644 index 0000000..f030288 Binary files /dev/null and b/public/vendor/fonts/Noto Sans Regular/256-511.pbf differ diff --git a/public/vendor/fonts/OFL.txt b/public/vendor/fonts/OFL.txt new file mode 100644 index 0000000..0c037d1 --- /dev/null +++ b/public/vendor/fonts/OFL.txt @@ -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. \ No newline at end of file