mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-08 18:02:46 +02:00
web: blend basemap providers to fix HOT/CARTO tile checkerboard (#846)
This commit is contained in:
+78
-4
@@ -2678,6 +2678,14 @@ wiring in `web/public/assets/js/app/main.js` and federation wiring in
|
||||
checks from `web/`, shell checks from the repo root.
|
||||
|
||||
### HT-A1 — HOT is the primary basemap on both maps; CARTO retained as fallback — HT1
|
||||
|
||||
> **⚠️ CARTO-URL half superseded by BL-A2** (§ *Bugfix: Basemap provider blend
|
||||
> (chess-pattern fix)*). The CARTO fallback source is intentionally migrated from
|
||||
> the natively-dark Dark Matter (`dark_all`) to the *colored* Voyager
|
||||
> (`rastertiles/voyager`), so the `dark_all` grep below no longer matches by
|
||||
> design. The **HOT-primary half stands** (HOT is still the primary on both maps);
|
||||
> **BL-A2 is the authoritative check** for the fallback source.
|
||||
|
||||
```bash
|
||||
git grep -nE "tile\.openstreetmap\.fr/hot" -- web/public/assets/js
|
||||
git grep -nE "basemaps\.cartocdn\.com/dark_all" -- web/public/assets/js
|
||||
@@ -2693,6 +2701,17 @@ the per-tile fallback source, not the primary. HOT options (`subdomains:'abc'`,
|
||||
**Supersedes DM-A1** (which required the HOT reference to be absent).
|
||||
|
||||
### HT-A2 — Dark filter reintroduced for HOT only; static, dark-only, off the contract — HT2
|
||||
|
||||
> **⚠️ Filter-scope half superseded by BL-A1** (§ *Bugfix: Basemap provider blend
|
||||
> (chess-pattern fix)*). The dark filter is intentionally **no longer HOT-only**:
|
||||
> `.map-tiles-fallback` now carries the *same* filter as `.map-tiles-hot` (BL3), so
|
||||
> the two providers blend. The greps below still pass unchanged (the filter is
|
||||
> still one static `base.css` rule, the removed Ruby/contract machinery stays
|
||||
> removed, `resolve_initial_theme` is still `"dark"`); only the *scope* prose
|
||||
> ("HOT-only", "`.map-tiles-fallback { filter: none }`") is amended. **BL-A1 is the
|
||||
> authoritative check** for the shared filter. Offline placeholder tiles still stay
|
||||
> unfiltered.
|
||||
|
||||
```bash
|
||||
git grep -nE "grayscale\(1\) invert\(1\)" -- web/public/assets/styles/base.css
|
||||
git grep -niE "tile_filters|DEFAULT_TILE_FILTER|map_tile_filter|tileFilters|resolveTileFilter|applyFiltersToAllTiles|--map-tile" -- web/lib web/public/assets/js web/public/assets/styles web/views
|
||||
@@ -2706,10 +2725,11 @@ per-tile class). The second prints **nothing** — none of the removed
|
||||
per-theme machinery returns: no Ruby `tile_filters`/`DEFAULT_TILE_FILTER_*`, no
|
||||
`data-app-config` `tileFilters`, no JS `resolveTileFilter`/`applyFiltersToAllTiles`,
|
||||
and no `--map-tile*-filter` custom property. The filter is one static CSS rule
|
||||
(HOT-only; CARTO-fallback tiles carry `.map-tiles-fallback { filter: none }`); the
|
||||
third shows `resolve_initial_theme` still returns `"dark"` (app stays dark-only, so
|
||||
no light filter exists). **Supersedes the CSS/JS half of DM-A2**; the Ruby/contract
|
||||
half of DM-A2 still holds.
|
||||
(shared by `.map-tiles-hot` and `.map-tiles-fallback` per BL3; offline placeholder
|
||||
tiles carry neither class and stay unfiltered); the third shows
|
||||
`resolve_initial_theme` still returns `"dark"` (app stays dark-only, so no light
|
||||
filter exists). **Supersedes the CSS/JS half of DM-A2**; the Ruby/contract half of
|
||||
DM-A2 still holds.
|
||||
|
||||
### HT-A3 — Per-tile 1000 ms timeout swaps HOT→CARTO — HT3
|
||||
```bash
|
||||
@@ -2783,3 +2803,57 @@ new `main/fallback-tile-layer.js` and its test), and **D1 / BF1** (the `/version
|
||||
config block is unchanged). The DM-era JS tests are **updated** to the HOT-primary
|
||||
+ CARTO-fallback wiring, not removed: `__tests__/config.test.js`,
|
||||
`__tests__/federation-page.test.js`, and the leaflet-stub map-init harness.
|
||||
|
||||
---
|
||||
|
||||
## Bugfix: Basemap provider blend (chess-pattern fix)
|
||||
|
||||
Maps to SPEC decisions **BL1–BL4**. The graceful timeout and colored CARTO source
|
||||
live in `web/public/assets/js/app/basemap-config.js`; the shared dark filter in
|
||||
`web/public/assets/styles/base.css`; both are locked by
|
||||
`web/public/assets/js/app/__tests__/basemap-blend.test.js`. The per-tile HOT vs
|
||||
CARTO looks (dark-filtered HOT tiles beside unfiltered CARTO tiles, on a routine
|
||||
1000 ms fallback) rendered the basemap as a **light/dark checkerboard**; the fix
|
||||
makes fallback rare (2500 ms) **and** blends the two providers to one dark look
|
||||
(colored Voyager source + shared filter). Run JS checks from `web/`, shell checks
|
||||
from the repo root.
|
||||
|
||||
### BL-A1 — Graceful 2500 ms timeout + colored Voyager fallback + shared filter
|
||||
```bash
|
||||
( cd web && node --test public/assets/js/app/__tests__/basemap-blend.test.js )
|
||||
```
|
||||
**Expected:** pass. Asserts (1) `FALLBACK_TIMEOUT_MS === 2500` (raised from the
|
||||
aggressive 1000 ms, so a slow-but-arriving HOT tile beats the deadline and
|
||||
fallback stays rare); (2) `CARTO_TILE_URL` targets the *colored* CARTO **Voyager**
|
||||
style (`/rastertiles/voyager/`), not the natively-dark `dark_all`; and (3)
|
||||
`base.css` applies the **same** `grayscale(1) invert(1) …` dark filter to
|
||||
`.map-tiles-fallback` as to `.map-tiles-hot` (no longer `filter:none`). Together
|
||||
these make a viewport mixing HOT and CARTO tiles render as one coherent dark
|
||||
basemap instead of a checkerboard.
|
||||
|
||||
### BL-A2 — No Dark Matter reference remains; Voyager is the sole fallback source
|
||||
```bash
|
||||
git grep -n "dark_all" -- web/public
|
||||
git grep -nE "rastertiles/voyager" -- web/public/assets/js/app/basemap-config.js
|
||||
```
|
||||
**Expected:** the first prints **nothing** — the natively-dark Dark Matter source
|
||||
is fully replaced (production constant and test fixtures alike); the second prints
|
||||
the Voyager fallback URL from the one shared basemap module. **Supersedes the
|
||||
`dark_all` half of HT-A1**; the HOT-primary half of HT-A1 is unchanged (HOT is
|
||||
still the primary basemap on both maps).
|
||||
|
||||
### BL-R1 — Regression: prior acceptance still holds
|
||||
```bash
|
||||
( cd web && npm test ) && ( cd web && bundle exec rspec )
|
||||
```
|
||||
**Expected:** every prior check still passes. Explicitly amended and required to
|
||||
stay green: **HT-A1** (the CARTO fallback URL is now Voyager, not `dark_all` — the
|
||||
`basemap-config.test.js` / `fallback-tile-layer.test.js` fixtures are **updated**,
|
||||
not removed); **HT-A2** (the dark filter now also covers `.map-tiles-fallback` —
|
||||
still one static `base.css` rule; the removed Ruby/contract `tileFilters`
|
||||
machinery stays removed, offline tiles stay unfiltered); **HT-A3** (the per-tile
|
||||
swap mechanism is unchanged — only the timeout constant and the swapped-in URL
|
||||
differ). Still green unchanged: **HT-A4 / A5 / A6 / A7** (fallback ladder, one
|
||||
shared factory on both maps, no attribution, apex/contract untouched), **A1** (no
|
||||
broker — the basemap hosts are raster CDNs), **B1** (all suites), and **B4** (exact
|
||||
Apache header on the new `basemap-blend.test.js`).
|
||||
|
||||
@@ -598,3 +598,47 @@ URL/layer config, no new package or build step. *D8 (stable contract)* —
|
||||
| **HT7** | **Apex / privacy / stack / contract all untouched.** HOT and CARTO are raster tile CDNs, not MQTT/cloud brokers — the apex (I) holds and `guard-edits.py` is untriggered (no manifest/dependency change). Both are keyless and cookieless, and CARTO is requested only for a HOT tile that failed, so the common case egresses to HOT alone — no new phone-home or per-operator credential (II / D11). Native Leaflet only, no new package or build step (D7). The dark filter is a frontend CSS constant and never enters `/version`, `data-app-config`, or any `/api/*` shape, so there is no contract change or version bump (D8). | proposed |
|
||||
| **HT8** | **Engineering bar (D9).** The new/changed frontend units — the shared `createBasemapLayer` factory, the per-tile timeout/fallback tile layer, and its pure timeout/URL helpers — ship with **100% unit tests**, full JSDoc, the exact Apache header, and clean linters; all existing suites stay green. The DM-era tile tests are **updated**, not left dangling: `__tests__/config.test.js`, `__tests__/federation-page.test.js`, the leaflet-stub map-init harness, and `main/__tests__/tile-failure-policy.test.js` are retargeted to the HOT-primary + CARTO-fallback wiring. | D9 + proposed |
|
||||
|
||||
---
|
||||
|
||||
## Bugfix: Basemap provider blend (chess-pattern fix)
|
||||
|
||||
The HOT-primary / per-tile-CARTO-fallback basemap (**HT1–HT3**) rendered a
|
||||
**light/dark checkerboard** in normal use: HOT tiles that beat the per-tile
|
||||
deadline showed dark-filtered, while tiles that missed it fell back to the
|
||||
**unfiltered, natively-dark CARTO Dark Matter** (HT2's deliberate `filter:none`
|
||||
exemption) — two visibly different looks tiling the same viewport. The mix was
|
||||
*routine*, not rare, because HOT (`openstreetmap.fr`) is slow and HT3's **1000 ms**
|
||||
deadline was aggressive, so a healthy fraction of tiles fell back on every load
|
||||
(and re-raced, so the pattern shifted on each pan/zoom). This is a **spec-silent**
|
||||
consequence of HT2 (filtered-HOT vs unfiltered-CARTO look different *by
|
||||
construction*) meeting HT3 (frequent per-tile fallback) — no acceptance criterion
|
||||
was violated; the contract was silent on the visual seam. The fix attacks both
|
||||
halves: make fallback *rare* (graceful timeout) **and** make the two providers
|
||||
*look alike* (colored CARTO source + shared dark filter). Frontend-only
|
||||
(`basemap-config.js`, `base.css`); no API/DB/ingestor change and no contract move
|
||||
(HT7 posture preserved: still frontend constants, off `/version` and
|
||||
`data-app-config`).
|
||||
|
||||
**Conflict check.** *HT1 (CARTO source = Dark Matter)* — **amended** (BL2: CARTO
|
||||
Voyager). *HT2 (CARTO fallback exempt from the filter)* — **amended** (BL3: the
|
||||
fallback shares HOT's filter; HT2's "already dark → don't invert" rationale is void
|
||||
because the source is now light/colored). *HT3 (1000 ms)* — **amended** (BL1:
|
||||
2500 ms). *HT4 / HT5 (fallback ladder, shared factory, both maps)* —
|
||||
**consistent**: the per-tile swap mechanism, the offline last tier, and the single
|
||||
`createBasemapLayer` factory are unchanged; the federation map shares the `#map`
|
||||
filter selector, so the blend lands on both maps (parity, Invariant IV). *HT6 (no
|
||||
attribution)* — **reaffirmed**: Voyager is OSM+CARTO like Dark Matter and
|
||||
`attributionControl:false` is unchanged. *Apex I / privacy II / D7 / D8 / D11* —
|
||||
**untouched**: Voyager is a keyless, cookieless, CORS-enabled raster CDN like Dark
|
||||
Matter; no manifest/dependency/contract change, no version bump. The pattern may
|
||||
still not be eliminated 100% (a genuinely-failing HOT tile whose CARTO cover is
|
||||
also slow can briefly flash), but under normal latency both levers together make
|
||||
it rare and, when it occurs, near-invisible.
|
||||
|
||||
| # | Decision | Source |
|
||||
| --- | --- | --- |
|
||||
| **BL1** | **Graceful per-tile timeout: 1000 ms → 2500 ms (amends HT3).** `FALLBACK_TIMEOUT_MS` (the single source of truth in `basemap-config.js`) is raised to **2500 ms**, so a slow-but-arriving HOT tile beats the deadline instead of falling back — restoring CARTO to the *rare safety net* HT3 intended, given HOT's real-world latency. Fewer routine fallbacks is the first half of removing the checkerboard. Accepted cost: when HOT is genuinely dead, the blank→CARTO recovery for that tile is up to 2.5 s (was 1 s); the offline tier (HT4) is unaffected. | interview |
|
||||
| **BL2** | **Colored CARTO source: Dark Matter → Voyager (amends HT1).** The fallback URL becomes `https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png` (same `subdomains:'abcd'`, `detectRetina`, `crossOrigin:'anonymous'`; keyless CORS CDN). Voyager is CARTO's natively-colourful, light-background raster style — chosen precisely so the *same* dark filter that greys HOT greys it too. (A natively-dark source could not be filtered to match — HT2's original constraint — which is why HT2 exempted it; BL3 removes that exemption because BL2 removes its cause.) | interview |
|
||||
| **BL3** | **Shared dark filter on the fallback tile (amends HT2).** `.map-tiles-fallback` no longer renders `filter:none`; it carries the **same** `grayscale(1) invert(1) brightness(0.9) contrast(1.08)` filter as `.map-tiles-hot`, expressed as one comma-grouped `base.css` rule (the single source of truth for the value). Both providers therefore converge to one coherent dark look, so a viewport mixing HOT and CARTO tiles is no longer a checkerboard — the second half of the fix. The per-tile class swap in `fallback-tile-layer.js` is **unchanged**; only what `.map-tiles-fallback` *does* in CSS changes. Offline placeholder tiles still carry neither class and stay unfiltered. | interview |
|
||||
| **BL4** | **Both maps, one rule; posture preserved (reaffirms HT5 / HT7).** The federation map shares the dashboard's `#map` container, so the single `base.css` filter rule and the shared `createBasemapLayer` factory land the blend on **both** maps identically (parity, Invariant IV). The filter value and the timeout stay **frontend constants** — no Ruby `tile_filters`, no `/version` / `data-app-config` key, no `/api/*` change, no version bump (HT7 / D8 unchanged). | proposed |
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright © 2025-26 l5yth & contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Regression guard: basemap provider blend (chess-pattern fix).
|
||||
*
|
||||
* The dashboard/federation basemap renders primary HOT tiles and per-tile CARTO
|
||||
* fallback tiles in the same viewport. When the two providers looked different —
|
||||
* HOT dark-filtered vs a natively-dark, *unfiltered* CARTO Dark Matter tile — a
|
||||
* viewport mixing both (routine, because HOT is slow and the fallback timeout was
|
||||
* aggressive) rendered as a light/dark **checkerboard**. This suite locks the fix
|
||||
* so the defect cannot silently return:
|
||||
*
|
||||
* 1. **Graceful timeout** — the per-tile HOT deadline is 2500 ms (was 1000 ms),
|
||||
* so a slow-but-arriving HOT tile beats the deadline and fallback returns to
|
||||
* the rare safety net it was designed to be.
|
||||
* 2. **Colored fallback** — the CARTO source is the *colored* Voyager raster
|
||||
* basemap (not the natively-dark Dark Matter), so the same dark filter that
|
||||
* greys HOT applies meaningfully to it too.
|
||||
* 3. **Shared filter (the blend)** — ``.map-tiles-fallback`` carries the *same*
|
||||
* ``grayscale/invert`` dark filter as ``.map-tiles-hot`` (no longer
|
||||
* ``filter: none``), so HOT and CARTO tiles converge to one coherent dark look
|
||||
* instead of a checkerboard.
|
||||
*
|
||||
* @module __tests__/basemap-blend
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { CARTO_TILE_URL, FALLBACK_TIMEOUT_MS } from '../basemap-config.js';
|
||||
|
||||
/** Absolute path to the shared stylesheet carrying the tile filter rules. */
|
||||
const BASE_CSS_PATH = fileURLToPath(new URL('../../../styles/base.css', import.meta.url));
|
||||
|
||||
/**
|
||||
* Return the declaration body of the first CSS rule whose selector mentions
|
||||
* ``className``. Whitespace-tolerant so the assertions do not depend on the exact
|
||||
* selector formatting (single rule vs. comma-grouped selectors both work).
|
||||
* Comments are stripped first so a class name *mentioned in a comment* above an
|
||||
* unrelated rule cannot be mistaken for that rule's selector.
|
||||
*
|
||||
* @param {string} css Full stylesheet text.
|
||||
* @param {string} className Class name to locate in a selector (without the dot).
|
||||
* @returns {string|null} The rule's declaration body, or ``null`` when absent.
|
||||
*/
|
||||
function ruleBodyFor(css, className) {
|
||||
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
for (const block of withoutComments.split('}')) {
|
||||
const braceIdx = block.indexOf('{');
|
||||
if (braceIdx === -1) continue;
|
||||
const selector = block.slice(0, braceIdx);
|
||||
if (selector.includes(className)) return block.slice(braceIdx + 1).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the standalone ``filter:`` value from a CSS declaration body, ignoring
|
||||
* the ``-webkit-filter:`` vendor twin.
|
||||
*
|
||||
* @param {string|null} ruleBody Declaration body from {@link ruleBodyFor}.
|
||||
* @returns {string|null} The filter value (e.g. ``grayscale(1) …``), or ``null``.
|
||||
*/
|
||||
function filterValueOf(ruleBody) {
|
||||
if (!ruleBody) return null;
|
||||
for (const decl of ruleBody.split(';')) {
|
||||
const trimmed = decl.trim();
|
||||
if (trimmed.startsWith('filter:')) return trimmed.slice('filter:'.length).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
test('the per-tile HOT fallback timeout is graceful (2500 ms)', () => {
|
||||
assert.equal(FALLBACK_TIMEOUT_MS, 2500);
|
||||
});
|
||||
|
||||
test('the CARTO fallback uses a colored basemap (Voyager), not Dark Matter', () => {
|
||||
assert.match(CARTO_TILE_URL, /basemaps\.cartocdn\.com\/rastertiles\/voyager/);
|
||||
assert.doesNotMatch(CARTO_TILE_URL, /dark_all/);
|
||||
});
|
||||
|
||||
test('CARTO fallback tiles share the same dark filter as HOT tiles (blend)', () => {
|
||||
const css = readFileSync(BASE_CSS_PATH, 'utf8');
|
||||
const hotFilter = filterValueOf(ruleBodyFor(css, 'map-tiles-hot'));
|
||||
const fallbackFilter = filterValueOf(ruleBodyFor(css, 'map-tiles-fallback'));
|
||||
|
||||
assert.match(hotFilter, /grayscale\(1\) invert\(1\)/);
|
||||
// The fallback is no longer exempt (`filter: none`); it carries HOT's filter.
|
||||
assert.notEqual(fallbackFilter, 'none');
|
||||
assert.equal(fallbackFilter, hotFilter);
|
||||
});
|
||||
@@ -40,10 +40,10 @@ test('HOT is the primary tile source; CARTO is retained for the fallback', () =>
|
||||
assert.equal(HOT_TILE_OPTIONS.subdomains, 'abc');
|
||||
assert.equal(HOT_TILE_OPTIONS.crossOrigin, 'anonymous');
|
||||
assert.equal(HOT_TILE_OPTIONS.maxZoom, 19);
|
||||
assert.match(CARTO_TILE_URL, /basemaps\.cartocdn\.com\/dark_all/);
|
||||
assert.match(CARTO_TILE_URL, /basemaps\.cartocdn\.com\/rastertiles\/voyager/);
|
||||
assert.equal(CARTO_TILE_OPTIONS.subdomains, 'abcd');
|
||||
assert.equal(CARTO_TILE_OPTIONS.detectRetina, true);
|
||||
assert.equal(FALLBACK_TIMEOUT_MS, 1000);
|
||||
assert.equal(FALLBACK_TIMEOUT_MS, 2500);
|
||||
});
|
||||
|
||||
test('prefersRetinaTiles reflects the device pixel ratio', () => {
|
||||
@@ -80,7 +80,10 @@ test('createBasemapLayer builds the HOT-primary layer with CARTO fallback wiring
|
||||
assert.equal(tile.src, 'https://a.tile.openstreetmap.fr/hot/6/2/1.png');
|
||||
tile.dispatch('error'); // fall back
|
||||
// (2 + 1) % 4 = 3 -> 'd' CARTO, non-retina
|
||||
assert.equal(tile.src, 'https://d.basemaps.cartocdn.com/dark_all/6/2/1.png');
|
||||
assert.equal(
|
||||
tile.src,
|
||||
'https://d.basemaps.cartocdn.com/rastertiles/voyager/6/2/1.png'
|
||||
);
|
||||
assert.equal(tile.classList.contains(FALLBACK_TILE_CLASS), true);
|
||||
} finally {
|
||||
globalThis.devicePixelRatio = previous;
|
||||
|
||||
@@ -24,12 +24,14 @@
|
||||
*
|
||||
* The **primary** basemap is OpenStreetMap France **HOT** (Humanitarian OSM
|
||||
* Team) — a natively colourful raster basemap greyed to match the dark UI by the
|
||||
* static ``grayscale``/``invert`` CSS filter on ``.map-tiles-hot``. **CARTO Dark
|
||||
* Matter** (natively dark-grey) is retained as a **per-tile fallback**: any HOT
|
||||
* tile that errors or fails to load within {@link FALLBACK_TIMEOUT_MS} is
|
||||
* individually replaced by the CARTO tile at the same coordinate (see
|
||||
* ``main/fallback-tile-layer.js``). Both providers are keyless, CORS-enabled
|
||||
* public CDNs.
|
||||
* static ``grayscale``/``invert`` CSS filter on ``.map-tiles-hot``. **CARTO
|
||||
* Voyager** (also a natively colourful raster basemap) is retained as a
|
||||
* **per-tile fallback**: any HOT tile that errors or fails to load within
|
||||
* {@link FALLBACK_TIMEOUT_MS} is individually replaced by the CARTO tile at the
|
||||
* same coordinate (see ``main/fallback-tile-layer.js``) and is greyed by the
|
||||
* *same* dark filter (``.map-tiles-fallback``), so a viewport mixing both
|
||||
* providers renders as one coherent dark basemap rather than a light/dark
|
||||
* checkerboard. Both providers are keyless, CORS-enabled public CDNs.
|
||||
*
|
||||
* @module app/basemap-config
|
||||
*/
|
||||
@@ -66,15 +68,20 @@ export const HOT_TILE_OPTIONS = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Tile URL template for the CARTO Dark Matter fallback basemap.
|
||||
* Tile URL template for the CARTO Voyager fallback basemap.
|
||||
*
|
||||
* ``{s}`` rotates over the ``abcd`` subdomains, ``{r}`` expands to ``@2x`` on
|
||||
* HiDPI displays, and ``{z}/{x}/{y}`` is the standard slippy-map tile coordinate.
|
||||
* Voyager is CARTO's natively colourful raster style (unlike the previously used
|
||||
* Dark Matter, which was already dark). A colourful source is deliberate: the
|
||||
* per-tile CARTO fallback is greyed by the *same* ``grayscale``/``invert`` filter
|
||||
* as HOT (``.map-tiles-fallback``), so both providers converge to the same dark
|
||||
* look. ``{s}`` rotates over the ``abcd`` subdomains, ``{r}`` expands to ``@2x``
|
||||
* on HiDPI displays, and ``{z}/{x}/{y}`` is the standard slippy-map tile
|
||||
* coordinate.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const CARTO_TILE_URL =
|
||||
'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
|
||||
|
||||
/**
|
||||
* Leaflet ``tileLayer`` options describing the CARTO fallback source.
|
||||
@@ -97,11 +104,16 @@ export const CARTO_TILE_OPTIONS = {
|
||||
* Per-tile timeout, in milliseconds, before a slow HOT tile falls back to CARTO.
|
||||
*
|
||||
* The single source of truth for the fallback deadline; a HOT tile that has
|
||||
* neither loaded nor errored within this window is swapped to CARTO.
|
||||
* neither loaded nor errored within this window is swapped to CARTO. Set at
|
||||
* **2500 ms** (raised from an aggressive 1000 ms) so a slow-but-arriving HOT tile
|
||||
* beats the deadline rather than falling back — keeping fallback the rare safety
|
||||
* net it is meant to be, given HOT's real-world latency. Fewer routine fallbacks
|
||||
* (combined with the shared dark filter on ``.map-tiles-fallback``) is what keeps
|
||||
* a viewport from rendering as a HOT/CARTO checkerboard.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const FALLBACK_TIMEOUT_MS = 1000;
|
||||
export const FALLBACK_TIMEOUT_MS = 2500;
|
||||
|
||||
/**
|
||||
* Whether the current display should request ``@2x`` (HiDPI) fallback tiles.
|
||||
|
||||
@@ -1337,9 +1337,10 @@ export function initializeApp(config) {
|
||||
// The basemap is HOT (primary, dark-filtered via the ``.map-tiles-hot`` CSS
|
||||
// rule) with a per-tile CARTO fallback, built by the shared
|
||||
// ``createBasemapLayer`` factory (see ``./basemap-config.js`` and
|
||||
// ``./main/fallback-tile-layer.js``). A HOT tile that errors or is slow (>1s)
|
||||
// is individually swapped to CARTO; only a tile that fails on both providers
|
||||
// reaches the offline placeholder wired below.
|
||||
// ``./main/fallback-tile-layer.js``). A HOT tile that errors or is slow
|
||||
// (>2.5s) is individually swapped to CARTO Voyager (greyed by the same dark
|
||||
// filter as HOT, so the two providers blend); only a tile that fails on both
|
||||
// providers reaches the offline placeholder wired below.
|
||||
|
||||
if (hasLeaflet) {
|
||||
mapCenterLatLng = L.latLng(MAP_CENTER_COORDS.lat, MAP_CENTER_COORDS.lon);
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
withImgDocument,
|
||||
} from './tile-test-helpers.js';
|
||||
|
||||
const CARTO_TEMPLATE = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
const CARTO_TEMPLATE = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
|
||||
|
||||
/**
|
||||
* Build a recording ``done`` callback for tile-ready assertions.
|
||||
@@ -55,7 +55,7 @@ test('buildFallbackTileUrl substitutes subdomain and coords without a retina suf
|
||||
{ template: CARTO_TEMPLATE, subdomains: 'abcd', retina: false }
|
||||
);
|
||||
// (3 + 2) % 4 = 1 -> 'b'
|
||||
assert.equal(url, 'https://b.basemaps.cartocdn.com/dark_all/5/3/2.png');
|
||||
assert.equal(url, 'https://b.basemaps.cartocdn.com/rastertiles/voyager/5/3/2.png');
|
||||
});
|
||||
|
||||
test('buildFallbackTileUrl appends @2x when retina is requested', () => {
|
||||
@@ -64,7 +64,7 @@ test('buildFallbackTileUrl appends @2x when retina is requested', () => {
|
||||
{ template: CARTO_TEMPLATE, subdomains: 'abcd', retina: true }
|
||||
);
|
||||
// (1 + 0) % 4 = 1 -> 'b'
|
||||
assert.equal(url, 'https://b.basemaps.cartocdn.com/dark_all/4/1/0@2x.png');
|
||||
assert.equal(url, 'https://b.basemaps.cartocdn.com/rastertiles/voyager/4/1/0@2x.png');
|
||||
});
|
||||
|
||||
test('buildFallbackTileUrl defaults the subdomains when none are configured', () => {
|
||||
@@ -73,7 +73,7 @@ test('buildFallbackTileUrl defaults the subdomains when none are configured', ()
|
||||
{ template: CARTO_TEMPLATE, retina: false }
|
||||
);
|
||||
// subdomains default 'abc'; (0 + 0) % 3 = 0 -> 'a'
|
||||
assert.equal(url, 'https://a.basemaps.cartocdn.com/dark_all/2/0/0.png');
|
||||
assert.equal(url, 'https://a.basemaps.cartocdn.com/rastertiles/voyager/2/0/0.png');
|
||||
});
|
||||
|
||||
test('buildFallbackTileUrl uses the absolute coord sum for the subdomain index', () => {
|
||||
@@ -82,7 +82,7 @@ test('buildFallbackTileUrl uses the absolute coord sum for the subdomain index',
|
||||
{ template: CARTO_TEMPLATE, subdomains: 'abcd', retina: false }
|
||||
);
|
||||
// abs(-1 + -2) = 3 -> 3 % 4 = 3 -> 'd'
|
||||
assert.equal(url, 'https://d.basemaps.cartocdn.com/dark_all/3/-1/-2.png');
|
||||
assert.equal(url, 'https://d.basemaps.cartocdn.com/rastertiles/voyager/3/-1/-2.png');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -297,7 +297,7 @@ test('createFallbackTileLayer.createTile builds a filtered HOT tile that falls b
|
||||
assert.equal(tile.src, 'https://c.hot/3/1/1.png');
|
||||
tile.dispatch('error'); // force the fallback
|
||||
// (1 + 1) % 4 = 2 -> 'c'; retina -> @2x
|
||||
assert.equal(tile.src, 'https://c.basemaps.cartocdn.com/dark_all/3/1/1@2x.png');
|
||||
assert.equal(tile.src, 'https://c.basemaps.cartocdn.com/rastertiles/voyager/3/1/1@2x.png');
|
||||
assert.equal(tile.classList.contains(FALLBACK_TILE_CLASS), true);
|
||||
} finally {
|
||||
doc.restore();
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
* dark-styled by the CSS ``grayscale``/``invert`` filter on
|
||||
* {@link HOT_TILE_CLASS}. Any HOT tile that fires ``error`` or fails to load
|
||||
* within a short timeout is individually swapped to the corresponding **CARTO
|
||||
* Dark Matter** tile at the same ``z/x/y``; the swapped tile drops
|
||||
* {@link HOT_TILE_CLASS} for {@link FALLBACK_TILE_CLASS} and renders unfiltered
|
||||
* (CARTO is already dark, so an ``invert`` would render it light).
|
||||
* Voyager** tile at the same ``z/x/y``; the swapped tile drops
|
||||
* {@link HOT_TILE_CLASS} for {@link FALLBACK_TILE_CLASS}, which carries the
|
||||
* *same* dark filter (both providers are natively colourful), so a fallback tile
|
||||
* blends with its HOT neighbours instead of standing out as a checkerboard cell.
|
||||
*
|
||||
* The URL builder ({@link buildFallbackTileUrl}) and the per-tile state machine
|
||||
* ({@link wireTileFallback}) are Leaflet-free — they operate on a plain
|
||||
@@ -49,7 +50,11 @@
|
||||
export const HOT_TILE_CLASS = 'map-tiles-hot';
|
||||
|
||||
/**
|
||||
* CSS class marking a tile that has fallen back to CARTO (rendered unfiltered).
|
||||
* CSS class marking a tile that has fallen back to CARTO.
|
||||
*
|
||||
* Carries the same ``grayscale``/``invert`` dark filter as {@link HOT_TILE_CLASS}
|
||||
* (CARTO Voyager is natively colourful, like HOT), so a fallback tile blends with
|
||||
* its HOT neighbours rather than reading as a distinct checkerboard cell.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
|
||||
@@ -1900,24 +1900,22 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
/*
|
||||
* Primary HOT basemap tiles are natively colourful; this static filter greys
|
||||
* them to match the dark UI. Leaflet stamps the layer's `className` on the tile
|
||||
* container, so the per-tile class `.map-tiles-hot` (added by
|
||||
* `main/fallback-tile-layer.js`) is what carries the filter. A tile that has
|
||||
* fallen back to CARTO (already dark) drops `.map-tiles-hot` for
|
||||
* `.map-tiles-fallback` and renders unfiltered — inverting it would make it
|
||||
* light. Offline placeholder tiles carry neither class and stay unfiltered too.
|
||||
* Both basemap providers are natively colourful, light-background raster maps
|
||||
* (HOT primary, CARTO Voyager fallback). This one static filter greys and
|
||||
* inverts them to match the dark UI, applied identically to primary HOT tiles
|
||||
* (`.map-tiles-hot`) and to per-tile CARTO fallback tiles (`.map-tiles-fallback`)
|
||||
* so a viewport mixing both providers renders as one coherent dark basemap
|
||||
* instead of a light/dark checkerboard. Leaflet stamps the layer's `className`
|
||||
* on the tile container, so these per-tile classes (added by
|
||||
* `main/fallback-tile-layer.js`) are what carry the filter. Offline placeholder
|
||||
* tiles carry neither class and stay unfiltered.
|
||||
*/
|
||||
#map .leaflet-tile.map-tiles-hot {
|
||||
#map .leaflet-tile.map-tiles-hot,
|
||||
#map .leaflet-tile.map-tiles-fallback {
|
||||
filter: grayscale(1) invert(1) brightness(0.9) contrast(1.08);
|
||||
-webkit-filter: grayscale(1) invert(1) brightness(0.9) contrast(1.08);
|
||||
}
|
||||
|
||||
#map .leaflet-tile.map-tiles-fallback {
|
||||
filter: none;
|
||||
-webkit-filter: none;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: #fff;
|
||||
|
||||
Reference in New Issue
Block a user