Replace CARTO raster tiles with OpenStreetMap vector tiles - #53816
Open
bramkragten wants to merge 9 commits into
Open
Replace CARTO raster tiles with OpenStreetMap vector tiles#53816bramkragten wants to merge 9 commits into
bramkragten wants to merge 9 commits into
Conversation
The CARTO basemap started rendering an "API KEY REQUIRED" overlay into
its tiles, which breaks every map in the frontend.
Draw the base map with MapLibre GL through maplibre-gl-leaflet, on the
Shortbread vector tiles the OpenStreetMap Foundation serves. Everything
else about the map stays on Leaflet - markers, clustering, zone editing,
paths and the scale control are untouched - so this swaps the base layer
and nothing else.
Browsers without WebGL2 fall back to OSM raster tiles, which the OSMF
allows us to use. MapLibre draws raster sources through WebGL as well,
so that fallback has to stay a plain Leaflet tile layer.
The style, its SDF glyphs and its sprites are served from our own
/static/map/, because vector.openstreetmap.org sets CORS headers on the
tile endpoint only. A new gulp task assembles them from pinned and
checksummed VersaTiles releases. CJK, kana and hangul are rendered with
a font from the device, which keeps the shipped glyph set at 5.4 MB
instead of 74 MB.
Dark mode is now a real dark cartography rather than a CSS invert filter
over the raster tiles.
Two details that the base layer swap forced:
- The Leaflet map gets an explicit zoom range. It used to inherit one
from the raster tile layer, and marker clustering throws without it.
- The generated style is fetched and its sprite URL made absolute.
MapLibre rejects a relative one, while the glyph URL has to stay
relative so its {fontstack} and {range} placeholders survive.
Fixes #53800
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@maplibre/mlt, a dependency of maplibre-gl, is dual licensed. Both halves are already on the allowlist individually, so the expression is unambiguously permissive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Replaces the broken CARTO raster basemap with self-hosted MapLibre-styled OpenStreetMap vector tiles and a raster fallback.
Changes:
- Adds vector/raster base-layer selection and dark-mode switching.
- Builds verified map styles, glyphs, and sprites.
- Adds MapLibre dependencies and license support.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores downloaded map archives. |
build-scripts/gulp/gather-static.js |
Copies generated map assets. |
build-scripts/gulp/index.mjs |
Registers the map-assets task. |
build-scripts/gulp/map-assets.js |
Generates and verifies map assets. |
package.json |
Adds mapping dependencies. |
script/check-licenses |
Allows a new permissive license expression. |
src/common/dom/setup-leaflet-map.ts |
Configures zoom bounds and base layers. |
src/common/map/base-layer.ts |
Implements vector and raster basemaps. |
src/components/map/ha-map.ts |
Integrates base-layer lifecycle and themes. |
yarn.lock |
Locks new dependencies. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Bold is cut to U+0000-04FF because the styles we ship only use it for motorway shields, which carry road refs. That is half of the glyph set, and it silently breaks the moment bold is used for a name instead: the `neutrino` style, for one, sets country and state labels in bold, which would turn to tofu outside Latin, Greek and Cyrillic. Fail the build in that case rather than ship a map that loses its labels in half the world. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The OSMF asks consumers to point at https://vector.openstreetmap.org/shortbread_v1/tilejson.json rather than hardcode the tile URL, so they can move the tiles without every client needing a release. It also carries the attribution, zoom range and bounds, so those stop being baked into our style. The style builder can only write a tile URL, so the source is repointed afterwards, keyed on the URL it is replacing: if @versatiles/style ever stops emitting that, the build fails instead of quietly leaving the style on the builder's own default host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The build asserted that the generated style carried the tile URL we had handed the style builder, before replacing it with the TileJSON. That assertion held, but it left a hardcoded OSM tile URL in the source that looked like it mattered at runtime while it could never ship: the tile URL is resolved from the TileJSON, and the styles we write contain no tile template at all. Key the check on the shortbread styles carrying exactly one source instead. That still catches the failure that matters - the builder's own host ending up in a shipped style - without naming a URL we do not use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
balloob
previously approved these changes
Aug 26, 2026
Three problems the Copilot review found, all in the path that is meant to guarantee a working map: The vector layer was added to the map outside the guarded path. The Leaflet adapter only constructs the MapLibre map in `onAdd`, so that is where a refused WebGL context, a blocked worker or a rejected blob URL throws - after the WebGL2 probe has already succeeded. That left no fallback and a half initialized map. Adding the layer now happens inside `createBaseLayer`, which tears down the partial layer and falls back to raster tiles. Theme changes fetch a style, so a burst of them could resolve out of order and leave the map on the wrong theme, with the tracked mode saying otherwise so later toggles did nothing. Only the newest request may touch the map now. Setting up now awaits a style fetch, so `ha-map` could disconnect while that is pending: `disconnectedCallback` had no map to tear down, and the continuation installed a live map, and its WebGL context, on a detached host whose container was too initialized to set up again on reconnect. None of this is reachable from the `ha-map` tests, which run in jsdom and only ever take the raster branch, so the fallback contract now has its own tests with MapLibre mocked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twenty map cards on one page was worth measuring, and the Copilot review was right: browsers cap live WebGL contexts at around 16 and drop the oldest, so the first four cards lost theirs. MapLibre does call preventDefault, so it is asking for the context back, but nothing frees a slot and webglcontextrestored never arrives. Those cards stayed blank for good. Swap them to raster tiles instead, after a grace period so a transient loss - a GPU reset, where the browser does restore - keeps its vector layer. Measured again with the same twenty cards: sixteen vector, four raster, none blank. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more from the Copilot review, both real. The raster tiles are plain images, so they inherited the page's same-origin referrer policy and went out with no Referer at all. OSM's raster tile policy asks for one, and being blocked would take out the fallback on exactly the devices that depend on it. Leaflet takes a per-layer `referrerPolicy`, so the fallback now sends its origin like the vector tiles already did. Rolling a failed theme request back to the opposite of what it asked for is only right while a single request is in flight. Request dark, then light, and let light fail: the map is still light, but the tracked mode flipped to dark, so asking for dark again did nothing and the map stayed light for good. Track what is applied separately from what was requested, and roll back to the former. The test for that one passed against the old code until it waited for the rejections to land, so it now does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
build-scripts/gulp/map-assets.js:188
- The Noto Sans glyphs are converted derivatives covered by OFL-1.1, but this archive is generated with glyph files only and this filter ships only
.pbfentries. The resulting/static/map/fontsdistribution therefore contains neither the required copyright notice nor the OFL text; OFL condition 2 requires every bundled copy to include both. Add the Noto license notice to the shipped map assets (and the project's third-party license inventory) before redistribution.
extract({
file: fontArchive,
cwd: path.join(outputDir, "fonts"),
filter: keepGlyph,
}),
The Copilot review is right that the adapter ignores `options.attribution`: it overrides `getAttribution` to return either its own `customAttribution` option or the attributions of the style's sources. The assignment was dead. The conclusion that the map therefore shows no copyright link is not, though. The source attribution is displayed, and it comes from the TileJSON: Leaflet | © <a href="…/copyright">OpenStreetMap</a> contributors which is a better attribution than the one we were setting - it says "contributors", as OSM asks - and it follows whoever ends up serving the tiles instead of being frozen at build time. Setting `customAttribution` would replace it with ours, so this drops the dead line instead and says where the credit comes from. The raster fallback is a plain Leaflet tile layer, which does read the option, so the constant stays for that one - now with "contributors" as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed change
The CARTO basemap we have been using started rendering an "API KEY REQUIRED" overlay into its tiles, which breaks every map in the frontend (#53800). Rather than hunting for another free raster host, this moves the base map to vector tiles, which we wanted to do anyway.
The base map is now drawn by MapLibre GL through
maplibre-gl-leaflet, on the Shortbread vector tiles the OpenStreetMap Foundation serves. Everything else stays on Leaflet — markers, clustering, zone editing, paths, the scale control — so only the base layer changes and the diff stays small. Going fully native MapLibre would mean replacingleaflet.markerclusterandleaflet-drawas well, and we would still need Leaflet around for the fallback below.Fallback. MapLibre needs WebGL2, which rules out iOS below 15 (iPad 2/3/4 and Air 1, common as wall panels) and devices whose GPU driver the browser blocklists. Those get OSM raster tiles instead, which the OSMF gives permission to use. MapLibre draws raster sources through WebGL too, so the fallback has to stay a plain Leaflet tile layer. Detection is a runtime
canvas.getContext("webgl2")probe, deliberately not gated on the bundle: the legacy bundle is picked by user agent, and nearly everything in that range does have WebGL2.Self-hosted assets.
vector.openstreetmap.orgsets CORS headers on the tile endpoint only, so the style, its SDF glyphs and its sprites are served from our own/static/map/. A new gulp task (build-map-assets) assembles them from pinned, sha256-verified VersaTiles releases and generates a light (colorful) and dark (eclipse) style with@versatiles/style. Self-hosting is also the better end state: it is one less host that can disappear on us, and it makes the styles ours to theme.Shipped size is 5.4 MB (102 glyph files, 4 sprites, 2 styles). The full Noto Sans SDF set is 74 MB; CJK, kana and hangul are rendered with a font from the device via
localIdeographFontFamily, which is 90% of that, and bold is limited to U+0000–04FF because the style only uses it for motorway shields. Coverage was checked against what OSM serves: the sprites are byte-identical, and the glyph codepoint sets match per range.MapLibre is a lazily loaded chunk of ~276 KB gzip, fetched only when a map is shown on a WebGL2 device.
Referrer. The OSMF vector tile policy asks for a referrer, and we send none (
index.htmlsetssame-origin).transformRequestnow setsreferrerPolicy: "origin"for the tile requests only, so the OSMF sees the origin and never the page URL. What they do with it is covered by the OSMF privacy policy. Worth a conscious decision from reviewers: for Nabu Casa remote URLs the origin is a per-instance identifier.Dark mode is now a real dark cartography instead of a CSS
invert()filter. The filter is still applied, but scoped to the raster fallback.Two things the swap forced, both easy to miss:
markerclusterthrowsMap has no maxZoom specifiedwithout it.{fontstack}/{range}placeholders are not percent-encoded.maplibre-glis pinned to 5.24.0 on purpose. v6 is ESM-only and derives its worker URL fromimport.meta.url, which rspack replaces with a build-machinefile://path; it then falls back tonew Worker("")and nothing renders. v5 inlines its worker, and is also what the OSM demo runs.Screenshots
New vector tiles:


New raster tiles (fallback for older devices):


Type of change
Additional information
Worth checking before merge:
worker-src blob:, the map breaks.maplibre-gl-csp.jsis the escape hatch if so.colorful/eclipseis a design decision that has not been made yet; both are one constant inbuild-scripts/gulp/map-assets.js.shortbread_v1/tilejson.jsonrather than a hardcoded URL, as the OSMF asks. That means one extra request before tiles start, and no tiles at all if the TileJSON is unreachable, where a hardcoded URL would still have worked. Theirstale-if-error=86400covers most of that.Checklist
If user exposed functionality or configuration variables are added/changed:
To help with the load of incoming pull requests: