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
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ jobs:
BASE_URL=http://localhost:13581 node test-channels-client-state-152-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-154-155-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-map-modal-fluid-e2e.js 2>&1 | tee -a e2e-output.txt
# #332 — no map may request a tile from basemaps.cartocdn.com.
# No config.json here, so this exercises the built-in non-CARTO
# fallback (tile.openstreetmap.org) a default install resolves to.
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-332-no-carto-tiles-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-map-nodes-pagination-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-123-map-lifecycle-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-observer-iata-1188-e2e.js 2>&1 | tee -a e2e-output.txt
Expand Down
7 changes: 4 additions & 3 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,11 @@
},
"map": {
"tiles": {
"darkDefault": "carto-dark",
"lightDefault": "carto-light",
"_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.",
"darkDefault": "osm-dark",
"lightDefault": "osm-standard",
"providers": {
"_comment_carto": "Carto is the default free-tier provider. Optional: specify 'domain' for Carto enterprise (e.g. 'mycompany' for 'https://{s}.mycompany.cartocdn.com').",
"_comment_carto": "Carto styles stay available and selectable, but keyless raster tiles are watermarked 'API KEY REQUIRED' (see _comment_defaults). Optional: specify 'domain' for Carto enterprise (e.g. 'mycompany' for 'https://{s}.mycompany.cartocdn.com').",
"carto": {
"enabled": true,
"domain": ""
Expand Down
15 changes: 14 additions & 1 deletion public/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -2534,7 +2534,20 @@
// Render minimap
if (hasMap && typeof L !== 'undefined') {
const map = L.map('subpathMap', { zoomControl: false, attributionControl: false });
L.tileLayer(getTileUrl(), { maxZoom: 18 }).addTo(map);
// #332 — go through the one shared helper (nodes.js, also used by the
// node-detail inset, node-reach, packet-path, area-nodes and
// rx-coverage maps). It resolves the configured provider for the
// current theme AND applies that provider's invertFilter to THIS
// map's own tile pane. A bare L.tileLayer(getTileUrl()) skipped the
// filter, so the keyless-OSM dark default (the light OSM template
// plus an invert filter) rendered a light basemap in a dark page.
if (typeof window._applyTilesToNodeMap === 'function') {
window._applyTilesToNodeMap(map);
} else {
// Loud, not silent: the shared tile helper is missing.
console.warn('subpath minimap: _applyTilesToNodeMap unavailable — using OSM fallback');
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
}

const latlngs = [];
nodesWithLoc.forEach((n, i) => {
Expand Down
33 changes: 27 additions & 6 deletions public/customize-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -1637,8 +1637,12 @@
function _renderTileProviderSelector() {
var reg = (typeof window !== 'undefined') && window.MC_TILE_PROVIDERS;
if (!reg) return '';
var activeDark = (typeof window.MC_getDarkTileProvider === 'function') ? window.MC_getDarkTileProvider() : 'carto-dark';
var activeLight = (typeof window.MC_getLightTileProvider === 'function') ? window.MC_getLightTileProvider() : 'carto-light';
// #332 — the last-resort id comes from the registry's own exported
// defaults, never from a hard-coded 'carto-*'. CARTO is no longer the
// built-in default, so naming it here would have preselected a style
// the instance may not even have enabled.
var activeDark = (typeof window.MC_getDarkTileProvider === 'function') ? window.MC_getDarkTileProvider() : window.MC_DARK_TILE_DEFAULT;
var activeLight = (typeof window.MC_getLightTileProvider === 'function') ? window.MC_getLightTileProvider() : window.MC_LIGHT_TILE_DEFAULT;

var darkIds = Object.keys(reg).filter(function(id) { return reg[id].type === 'dark'; });
var lightIds = Object.keys(reg).filter(function(id) { return reg[id].type === 'light'; });
Expand Down Expand Up @@ -1766,6 +1770,21 @@
'</div>';
}

/* #332 — both geo-filter preview maps used to hard-code the CARTO
* light_all template, so they rendered "API KEY REQUIRED" tiles even on
* instances configured for Esri/OpenTopoMap/OSM. Resolve the operator's
* configured LIGHT provider instead (these previews are always
* light-styled regardless of the app theme), falling back to roles.js
* TILE_LIGHT, which is itself non-CARTO. */
function _cv2PreviewTile() {
var spec = (typeof window.MC_getTileSpec === 'function') ? window.MC_getTileSpec('light') : null;
return {
url: (spec && spec.url) || window.TILE_LIGHT,
attribution: (spec && spec.attribution) || '© OpenStreetMap contributors',
maxZoom: (spec && spec.maxZoom) || 19
};
}

function _gfOpenModal(container) {
var existing = document.getElementById('cv2-gf-modal-overlay');
if (existing) existing.remove();
Expand Down Expand Up @@ -1835,8 +1854,9 @@
var modalClosingLine = null;

_gfModalMap = L.map(mapDiv, { zoomControl: true });
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CartoDB', maxZoom: 19
var _cv2Tile = _cv2PreviewTile();
L.tileLayer(_cv2Tile.url, {
attribution: _cv2Tile.attribution, maxZoom: _cv2Tile.maxZoom
}).addTo(_gfModalMap);

function renderModal() {
Expand Down Expand Up @@ -2044,8 +2064,9 @@
if (!mapEl || typeof L === 'undefined') return;

_gfMap = L.map(mapEl, { zoomControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, touchZoom: false });
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CartoDB', maxZoom: 19
var _cv2Tile = _cv2PreviewTile();
L.tileLayer(_cv2Tile.url, {
attribution: _cv2Tile.attribution, maxZoom: _cv2Tile.maxZoom
}).addTo(_gfMap);

if (!_gfLoaded) {
Expand Down
15 changes: 13 additions & 2 deletions public/geofilter-builder.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
#btnClear:hover { background: #7a2020; }
.hint { font-size: 0.8rem; color: #888; margin-left: auto; }
#map { flex: 1; }
/* #332 — the keyless OSM basemap is a LIGHT style; invert the tile pane so
this builder keeps the dark map it had while it hard-coded CARTO
dark_all. Same filter string as INVERT_CSS in map-tile-providers.js.
Scoped to the tile pane, so markers and the polygon stay true-colour. */
#map .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(1.05); }
#output-panel { background: #0f0f23; border-top: 1px solid #333; padding: 12px 16px; display: flex; gap: 12px; align-items: flex-start; }
#output-panel label { font-size: 0.75rem; color: #888; white-space: nowrap; padding-top: 6px; }
#output { flex: 1; background: #111; border: 1px solid #333; border-radius: 6px; padding: 10px 12px; font-family: monospace; font-size: 0.78rem; color: #7ec8e3; white-space: pre; overflow-x: auto; min-height: 54px; max-height: 140px; overflow-y: auto; cursor: text; }
Expand Down Expand Up @@ -88,8 +93,14 @@ <h1>GeoFilter Builder</h1>
<script>
const map = L.map('map').setView([50.5, 4.4], 8);

L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CartoDB',
// #332 — this standalone builder loads neither roles.js nor
// map-tile-providers.js, so it has no provider registry to consult. It used
// to hard-code the CARTO dark template, which now returns tiles stamped
// "API KEY REQUIRED" on every keyless request. Use the same keyless OSM
// baseline the registry falls back to; the #map .leaflet-tile-pane invert
// filter in the stylesheet above restores the dark look.
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);

Expand Down
31 changes: 20 additions & 11 deletions public/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -1548,21 +1548,30 @@
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
// #1420 — multi-provider dark-tile picker. Light mode unchanged.
let _liveDarkRefLayer = null;
// #332 — every tile URL and attribution comes from the configured
// dark/light provider. The old body hard-coded CARTO in three places
// (the light-mode url fallback, the 'carto-dark' registry fallback and
// the '© OpenStreetMap © CartoDB' credit), so a map on an instance
// configured for Esri/OpenTopoMap still fetched keyless CARTO tiles —
// which have been stamped "API KEY REQUIRED" since August 2026.
function _liveResolveTile(dark) {
const reg = window.MC_TILE_PROVIDERS || {};
if (!dark) {
const lightId = (typeof window.MC_getLightTileProvider === 'function') ? window.MC_getLightTileProvider() : null;
const lp = lightId ? (reg[lightId] || null) : null;
if (lp && lp.url) {
return { url: (typeof lp.url === 'function' ? lp.url() : lp.url), attribution: lp.attribution || '© OpenStreetMap © CartoDB', refUrl: null };
// Preferred path: the registry's single resolver. It picks the
// provider for this theme and invokes lazy `url` functions, so url
// AND attribution always come from the configured provider.
if (typeof window.MC_getTileSpec === 'function') {
const s = window.MC_getTileSpec(dark ? 'dark' : 'light');
if (s && s.url) {
return { url: s.url, attribution: s.attribution || '© OpenStreetMap contributors', refUrl: s.refUrl || null };
}
return { url: TILE_LIGHT, attribution: '© OpenStreetMap © CartoDB', refUrl: null };
}
const id = (typeof window.MC_getDarkTileProvider === 'function') ? window.MC_getDarkTileProvider() : 'carto-dark';
const p = reg[id] || reg['carto-dark'] || {};
// Legacy/partial registry: accessors without getTileSpec.
const reg = window.MC_TILE_PROVIDERS || {};
const getId = dark ? window.MC_getDarkTileProvider : window.MC_getLightTileProvider;
const id = (typeof getId === 'function') ? getId() : null;
const p = (id && reg[id]) || {};
return {
url: (typeof p.url === 'function' ? p.url() : p.url) || (typeof p.baseUrl === 'function' ? p.baseUrl() : p.baseUrl) || TILE_DARK,
attribution: p.attribution || '© OpenStreetMap © CartoDB',
url: (typeof p.url === 'function' ? p.url() : p.url) || (typeof p.baseUrl === 'function' ? p.baseUrl() : p.baseUrl) || (dark ? TILE_DARK : TILE_LIGHT),
attribution: p.attribution || '© OpenStreetMap contributors',
refUrl: p.refUrl || null
};
}
Expand Down
77 changes: 75 additions & 2 deletions public/map-tile-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@
var STORAGE_KEY = 'mc-dark-tile-provider';
var STORAGE_KEY_LIGHT = 'mc-light-tile-provider';

var DEFAULT_ID = 'carto-dark';
var DEFAULT_ID_LIGHT = 'carto-light';
// #332 — the built-in fallback must NOT be CARTO. Since August 2026 CARTO
// stamps "API KEY REQUIRED" into every keyless raster tile, and this fork
// has no basemap-key plumbing, so an unconfigured instance that defaulted
// to 'carto-dark' / 'carto-light' rendered watermarked maps. OSM Standard
// is the keyless baseline: no token, tiles to zoom 18, and it is already
// the unconditional provider for the home mini-map, node-reach, packet-path
// and area maps — so this default introduces no new third party. Operators
// who want CARTO (or anything else) still set map.tiles.darkDefault /
// lightDefault, which keep priority over these.
var DEFAULT_ID = 'osm-dark';
var DEFAULT_ID_LIGHT = 'osm-standard';
var EVENT_NAME = 'mc-tile-provider-changed';

var _serverDefault = null;
Expand All @@ -40,6 +49,23 @@
}
return 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
};
/* #332 — credit the vendor that actually serves the tiles. osm-standard /
* osm-dark used to credit "Maps © Mapbox/Thunderforest/MapTiler"
* unconditionally; #332 makes them the default every unconfigured
* instance shows, and such an instance uses none of those three — its
* tiles come straight from the OSMF servers. Resolved the same way
* _getOsmUrl() picks the URL, and re-resolved on every registry init so
* config landing after parse time still applies. */
var _getOsmAttribution = function() {
var OSM = '© OpenStreetMap contributors';
if (_cfg && _cfg.providers && _cfg.providers.osm && _cfg.providers.osm.provider && _cfg.providers.osm.token) {
var prov = _cfg.providers.osm.provider.toLowerCase();
if (prov === 'thunderforest') return OSM + ', Tiles © Thunderforest';
if (prov === 'maptiler') return OSM + ', Tiles © MapTiler';
if (prov === 'mapbox') return OSM + ', Tiles © Mapbox';
}
return OSM;
};

var BASE_STYLES = {
'carto-dark': { provider: 'carto', label: 'Carto Dark', url: function() { return _getCartoBase() + '/dark_all/{z}/{x}/{y}{r}.png'; }, invertFilter: null, type: 'dark', attribution: '© OpenStreetMap © CartoDB', maxZoom: 19 },
Expand Down Expand Up @@ -73,6 +99,13 @@
var HAS_STAMEN = _cfg && _cfg.providers && _cfg.providers.stamen && _cfg.providers.stamen.enabled && !!_cfg.providers.stamen.token;
var HAS_ESRI = true; // Kept for backwards compatibility

// #332 — resolve the OSM credit against the configured vendor before
// the registry is handed out. Keeping `attribution` a plain string here
// means every reader (getTileSpec, the layer control, the inset-map
// helper, map.js/live.js legacy branches) stays unchanged.
BASE_STYLES['osm-standard'].attribution = _getOsmAttribution();
BASE_STYLES['osm-dark'].attribution = _getOsmAttribution();

REGISTRY = {};
for (var key in BASE_STYLES) {
var style = BASE_STYLES[key];
Expand All @@ -84,6 +117,16 @@
if (style.provider === 'esri' && HAS_ESRI) REGISTRY[key] = style;
}

// #332 — the fallback defaults must stay resolvable no matter how the
// optional providers are gated. Without this a config that disables
// everything leaves REGISTRY without the active id, every caller falls
// through to a URL of its own, and that is exactly how the hard-coded
// CARTO templates spread in the first place. It also keeps the default
// selectable in the layer picker, so a user who switches away can
// switch back. `enabled` still governs every other style of a provider.
if (!REGISTRY[DEFAULT_ID]) REGISTRY[DEFAULT_ID] = BASE_STYLES[DEFAULT_ID];
if (!REGISTRY[DEFAULT_ID_LIGHT]) REGISTRY[DEFAULT_ID_LIGHT] = BASE_STYLES[DEFAULT_ID_LIGHT];

// Keep the public reference in sync with the newly rebuilt REGISTRY
window.MC_TILE_PROVIDERS = REGISTRY;

Expand Down Expand Up @@ -179,9 +222,38 @@
}


/* #332 — getTileSpec(type): the ONE place a map resolves the active
* provider. `type` is 'dark' or 'light'; anything else is read as 'dark'.
* Returns a plain object so no caller needs to know that a style's `url`
* may be a lazy function, nor to carry its own fallback URL/attribution:
*
* { id, url, attribution, refUrl, maxZoom, invertFilter }
*
* `url` is always a string template (or null if a style is malformed) and
* `attribution` always comes from the resolved provider — never from a
* hard-coded CARTO credit. Re-resolved on every call so config landing
* after map init still applies.
*/
function getTileSpec(type) {
var isLight = (type === 'light');
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 {
id: id,
url: (typeof u === 'function' ? u() : u) || null,
attribution: (p && p.attribution) || BASE_STYLES[fallback].attribution,
refUrl: (p && p.refUrl) || null,
maxZoom: (p && p.maxZoom) || 19,
invertFilter: (p && p.invertFilter) || null
};
}

// ── Public surface ──────────────────────────────────────────────────────

window.MC_DARK_TILE_DEFAULT = DEFAULT_ID;
window.MC_LIGHT_TILE_DEFAULT = DEFAULT_ID_LIGHT;
window.MC_TILE_PROVIDERS = REGISTRY; // initial ref; MC_initTileRegistry keeps this in sync
window.MC_setDarkTileProvider = function(id) { return setActive(id, 'dark'); };
window.MC_setLightTileProvider = function(id) { return setActive(id, 'light'); };
Expand All @@ -190,6 +262,7 @@
window.MC_setServerDefaultTileProvider = setServerDefault;
window.MC_setServerDefaultLightTileProvider = setServerDefaultLight;
window.MC_applyTileFilter = applyTileFilter;
window.MC_getTileSpec = getTileSpec;


/**
Expand Down
31 changes: 20 additions & 11 deletions public/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,30 @@
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
// #1420 — multi-provider dark-tile picker. Light mode unchanged.
let _darkRefLayer = null; // Esri-only: labels overlay
// #332 — every tile URL and attribution comes from the configured
// dark/light provider. The old body hard-coded CARTO in three places
// (the light-mode url fallback, the 'carto-dark' registry fallback and
// the '© OpenStreetMap © CartoDB' credit), so a map on an instance
// configured for Esri/OpenTopoMap still fetched keyless CARTO tiles —
// which have been stamped "API KEY REQUIRED" since August 2026.
function _resolveTileUrl(dark) {
const reg = window.MC_TILE_PROVIDERS || {};
if (!dark) {
const lightId = (typeof window.MC_getLightTileProvider === 'function') ? window.MC_getLightTileProvider() : null;
const lp = lightId ? (reg[lightId] || null) : null;
if (lp && lp.url) {
return { url: (typeof lp.url === 'function' ? lp.url() : lp.url), attribution: lp.attribution || '© OpenStreetMap © CartoDB', refUrl: null };
// Preferred path: the registry's single resolver. It picks the
// provider for this theme and invokes lazy `url` functions, so url
// AND attribution always come from the configured provider.
if (typeof window.MC_getTileSpec === 'function') {
const s = window.MC_getTileSpec(dark ? 'dark' : 'light');
if (s && s.url) {
return { url: s.url, attribution: s.attribution || '© OpenStreetMap contributors', refUrl: s.refUrl || null };
}
return { url: TILE_LIGHT, attribution: '© OpenStreetMap © CartoDB', refUrl: null };
}
const id = (typeof window.MC_getDarkTileProvider === 'function') ? window.MC_getDarkTileProvider() : 'carto-dark';
const p = reg[id] || reg['carto-dark'] || {};
// Legacy/partial registry: accessors without getTileSpec.
const reg = window.MC_TILE_PROVIDERS || {};
const getId = dark ? window.MC_getDarkTileProvider : window.MC_getLightTileProvider;
const id = (typeof getId === 'function') ? getId() : null;
const p = (id && reg[id]) || {};
return {
url: (typeof p.url === 'function' ? p.url() : p.url) || (typeof p.baseUrl === 'function' ? p.baseUrl() : p.baseUrl) || TILE_DARK,
attribution: p.attribution || '© OpenStreetMap © CartoDB',
url: (typeof p.url === 'function' ? p.url() : p.url) || (typeof p.baseUrl === 'function' ? p.baseUrl() : p.baseUrl) || (dark ? TILE_DARK : TILE_LIGHT),
attribution: p.attribution || '© OpenStreetMap contributors',
refUrl: p.refUrl || null
};
}
Expand Down
Loading
Loading