Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"map": {
"tiles": {
"_comment_defaults": "#332 — the built-in defaults are 'osm-dark' / 'osm-standard' (keyless OpenStreetMap, tiles to zoom 18). Set darkDefault/lightDefault to any enabled style id to override; they take priority over the built-in fallback. Carto is NOT the default any more: since August 2026 keyless Carto raster tiles come back HTTP 200 with 'API KEY REQUIRED' stamped into the PNG, and CoreScope has no Carto basemap-key plumbing.",
"_comment_legacy": "Legacy raw tile-URL overrides — \"tiles.dark\" / \"tiles.light\" at the top level, or \"darkUrl\" / \"lightUrl\" here — are still honoured: they apply when no provider was chosen in the browser and no darkDefault/lightDefault is set, i.e. precedence is browser choice > darkDefault/lightDefault > raw URL > built-in fallback. A raw URL carries no vendor metadata, so such a map shows the generic \"\u00a9 OpenStreetMap contributors\" credit (no vendor-specific or licence attribution), is never CSS-inverted in dark mode, and is capped at zoom 18 like the built-in OpenStreetMap default. Prefer darkDefault/lightDefault with a registered style id when you need the correct credit, licence link or a different zoom ceiling.",
"darkDefault": "osm-dark",
"lightDefault": "osm-standard",
"providers": {
Expand Down
45 changes: 31 additions & 14 deletions public/map-tile-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
var _serverDefaultLight = null;

var INVERT_CSS = 'invert(1) hue-rotate(180deg) brightness(0.9) contrast(1.05)';
/* #364/#390 — a legacy raw tile URL carries no vendor metadata, so it
* keeps the historical generic credit. Documented for operators in
* config.example.json `map.tiles._comment_legacy`. */
var LEGACY_ATTRIBUTION = '© OpenStreetMap contributors';

var _cfg = null;

Expand Down Expand Up @@ -157,14 +161,21 @@
}


function getActiveId() {
// #390 — mirror of getSelectedLightId(): a valid explicit choice can
// itself be the built-in fallback id, so an implicit fallback has to stay
// distinguishable from one the operator or user actually picked.
function getSelectedDarkId() {
try {
var stored = window.localStorage && window.localStorage.getItem(STORAGE_KEY);
if (_hasId(stored) && REGISTRY[stored].type === 'dark') return stored;
} catch (_) {}
if (_cfg && _cfg.darkDefault && _hasId(_cfg.darkDefault)) return _cfg.darkDefault;
if (_hasId(_serverDefault)) return _serverDefault;
return DEFAULT_ID;
return null;
}

function getActiveId() {
return getSelectedDarkId() || DEFAULT_ID;
}

// A valid explicit choice can itself be the built-in fallback id. Keep
Expand Down Expand Up @@ -221,10 +232,12 @@
return;
}

// #390 — resolve through getTileSpec so the legacy-URL branch is
// honoured here too. A legacy dark URL is an operator's own dark
// tileset; inverting it on top of that renders the map light.
var isDark = _isDarkEffective();
var id = isDark ? getActiveId() : getActiveLightId();
var p = REGISTRY[id];
pane.style.filter = (isDark && p && p.invertFilter) ? p.invertFilter : '';
var spec = getTileSpec(isDark ? 'dark' : 'light');
pane.style.filter = (isDark && spec && spec.invertFilter) ? spec.invertFilter : '';
}


Expand All @@ -242,17 +255,21 @@
*/
function getTileSpec(type) {
var isLight = (type === 'light');
// #364 precedence: browser selection > configured provider default >
// legacy light URL > built-in fallback. roles.js records the actual
// loaded URL, not TILE_LIGHT's always-present built-in value. Unknown
// custom vendors keep the historical generic credit and no inversion;
// id:null means this is not a registry provider with vendor metadata.
var legacyLightUrl = window.MC_LIGHT_TILE_URL_OVERRIDE;
if (isLight && !getSelectedLightId() && typeof legacyLightUrl === 'string' && legacyLightUrl) {
return { id: null, url: legacyLightUrl, attribution: '© OpenStreetMap contributors', refUrl: null, maxZoom: BASE_STYLES[DEFAULT_ID_LIGHT].maxZoom, invertFilter: null };
var fallback = isLight ? DEFAULT_ID_LIGHT : DEFAULT_ID;
// #364/#390 precedence, applied per theme: browser selection >
// configured provider default > legacy URL > built-in fallback.
// roles.js records the actual loaded URL, not TILE_DARK/TILE_LIGHT's
// always-present built-in value. Unknown custom vendors keep the
// historical generic credit and no inversion; id:null means this is
// not a registry provider with vendor metadata. #390 gives dark the
// branch light got in #366 — `tiles.dark` / `map.tiles.darkUrl` were
// masked by the registry default the same way.
var legacyUrl = isLight ? window.MC_LIGHT_TILE_URL_OVERRIDE : window.MC_DARK_TILE_URL_OVERRIDE;
var selected = isLight ? getSelectedLightId() : getSelectedDarkId();
if (!selected && typeof legacyUrl === 'string' && legacyUrl) {
return { id: null, url: legacyUrl, attribution: LEGACY_ATTRIBUTION, refUrl: null, maxZoom: BASE_STYLES[fallback].maxZoom, invertFilter: null };
}
var id = isLight ? getActiveLightId() : getActiveId();
var fallback = isLight ? DEFAULT_ID_LIGHT : DEFAULT_ID;
var p = REGISTRY[id] || BASE_STYLES[id] || BASE_STYLES[fallback];
var u = p ? (p.url || p.baseUrl) : null;
return {
Expand Down
6 changes: 6 additions & 0 deletions public/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,11 @@
window.TILE_LIGHT = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
// Runtime metadata for the registry, not a new config setting. An
// implicit provider fallback must not mask the operator's legacy URL.
// #390 — dark needs the same treatment as light got in #366: before it,
// `tiles.dark` / `map.tiles.darkUrl` were silently masked by whichever
// dark style the registry defaulted to.
window.MC_LIGHT_TILE_URL_OVERRIDE = null;
window.MC_DARK_TILE_URL_OVERRIDE = null;

function _tileThemeIsDark() {
return document.documentElement.getAttribute('data-theme') === 'dark' ||
Expand Down Expand Up @@ -787,6 +791,8 @@
}
window.MC_LIGHT_TILE_URL_OVERRIDE = cfg.tiles ? (cfg.tiles.light || null) :
(cfg.map && cfg.map.tiles && cfg.map.tiles.lightUrl) || null;
window.MC_DARK_TILE_URL_OVERRIDE = cfg.tiles ? (cfg.tiles.dark || null) :
(cfg.map && cfg.map.tiles && cfg.map.tiles.darkUrl) || null;
if (typeof window.MC_initTileRegistry === 'function') window.MC_initTileRegistry(true);
if (cfg.snrThresholds) Object.assign(SNR_THRESHOLDS, cfg.snrThresholds);
if (cfg.distThresholds) Object.assign(DIST_THRESHOLDS, cfg.distThresholds);
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ run test-issue-1418-polish-review.js
run test-issue-1420-tile-providers.js
run test-issue-1614-tile-url-function.js
run test-issue-332-tile-provider-fallback.js
run test-issue-390-legacy-tile-followups.js
run test-issue-1438-marker-css-vars.js
run test-issue-1438-customizer-mcrole.js
run test-issue-1446-cb-preset-cascade.js
Expand Down
62 changes: 62 additions & 0 deletions test-issue-332-no-carto-tiles-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
* template plus an invert filter, so a map that attaches tiles but
* skips the filter renders a light basemap inside a dark page.
*
* #364/#390 extend it: an operator's legacy raw tile URL (`tiles.light` /
* `tiles.dark`, or `map.tiles.lightUrl` / `darkUrl`) must reach the real
* network requests of the surfaces above, per theme, and must not borrow a
* registry provider's credit, reference overlay or invert filter — a raw
* dark tileset inverted on top of being dark renders light.
*
* Third-party tile IMAGE requests are stubbed with a 1x1 PNG at the route
* level: the point is which URL the page asks for, not what comes back,
* and CI has no business reaching a tile CDN. Everything else off-origin
Expand Down Expand Up @@ -363,6 +369,62 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
});
}

// #390: the same contract for the DARK legacy override, which stayed
// masked by the registry default after #366 fixed light. A raw dark
// tileset must also never be CSS-inverted on top of already being dark —
// checkFilter() reads the resolved spec, so it asserts exactly that.
const legacyDarkUrl = 'https://private.example/dark-tiles/{z}/{x}/{y}.png';
for (const [label, clientConfig] of [
['tiles.dark', { tiles: { dark: legacyDarkUrl } }],
['map.tiles.darkUrl', { map: { tiles: { darkUrl: legacyDarkUrl } } }],
]) {
for (const theme of ['dark', 'light']) {
await step(`#390 ${label}: node inset uses ${theme === 'dark' ? 'legacy URL' : 'unchanged light provider'}`, async () => {
await withPage(theme, async (page, tiles) => {
await page.goto(BASE + '/#/nodes/' + NODE_KEY, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#nodeFullMap .leaflet-tile-pane', { state: 'attached' });
await page.waitForTimeout(600);
check(label, tiles, { expectHost: theme === 'dark' ? 'private.example' : EXPECT.light });
await checkFilter(page, '#nodeFullMap .leaflet-tile-pane', theme, label);
if (theme === 'light') assert(!tiles.some((u) => u.includes('private.example')), 'the legacy DARK URL leaked into light tiles');
}, { clientConfig });
});
await step(`#390 ${label}: /#/map in ${theme} follows the per-theme resolution`, async () => {
await withPage(theme, async (page, tiles) => {
await page.goto(BASE + '/#/map', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#leaflet-map .leaflet-tile-pane', { state: 'attached', timeout: 15000 });
await page.waitForTimeout(1500);
check(label + ' map', tiles, { expectHost: theme === 'dark' ? 'private.example' : EXPECT.light });
await checkFilter(page, '#leaflet-map .leaflet-tile-pane', theme, label + ' map');
}, { clientConfig });
});
}
await step(`#390 ${label}: explicit dark selection recovers from failed legacy tiles`, async () => {
await withPage('dark', async (page, tiles) => {
// An explicit choice of the built-in dark id must not be mistaken
// for an implicit fallback; a subsequent mount stops using the URL.
let failedRequests = 0;
await page.route('https://private.example/**', (route) => {
failedRequests++;
return route.fulfill({ status: 503, body: 'synthetic tile failure' });
});
await page.goto(BASE + '/#/nodes/' + NODE_KEY, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#nodeFullMap .leaflet-tile-pane', { state: 'attached' });
await page.waitForTimeout(600);
assert(failedRequests > 0, 'the legacy dark tile URL never produced a failing network request');
assert(await page.evaluate(() => window.getTileUrl()) === legacyDarkUrl, 'the failing legacy provider was not active');
await page.evaluate(() => window.MC_setDarkTileProvider('osm-dark'));
tiles.length = 0;
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForSelector('#nodeFullMap .leaflet-tile-pane', { state: 'attached' });
await page.waitForTimeout(600);
check('explicit dark recovery', tiles, { expectHost: OSM_HOST });
assert(!tiles.some((u) => u.includes('private.example')), 'legacy URL still masked the explicit dark choice');
await checkFilter(page, '#nodeFullMap .leaflet-tile-pane', 'dark', label);
}, { clientConfig });
});
}

// The standalone geo-filter builder has no provider registry at all; it
// used to hard-code the CARTO dark template.
await step('standalone /geofilter-builder.html requests no CARTO tiles', async () => {
Expand Down
Loading
Loading