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
8 changes: 8 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,18 @@
"provider": "",
"token": ""
},
"_comment_opentopomap": "OpenTopoMap topographical tile provider. Enabling this makes USERS' BROWSERS fetch tiles from tile.opentopomap.org, exposing their IP and approximate viewing area to that third party. Tiles are CC-BY-SA: the credit and licence link must stay on the map. Limited to zoom level 17, and it is a small volunteer-run service with informal fair-use expectations.",
"opentopomap": {
"enabled": false
},
"_comment_stamen": "Stamen (hosted by Stadia). WARNING: Tokens are sent to the browser. Apply origin/referrer restrictions.",
"stamen": {
"enabled": false,
"token": ""
},
"_comment_usgs": "USGS topographical tile providers (USGS Topo, USGS Imagery). Enabling these makes USERS' BROWSERS fetch tiles from basemap.nationalmap.gov, exposing their IP and approximate viewing area to that third party. Limited to zoom level 16, and The National Map only serves the United States and its territories — elsewhere these layers render blank with no on-screen explanation.",
"usgs": {
"enabled": false
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions public/map-tile-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@
'positron-dark': { provider: 'carto', label: 'Carto Positron', url: function() { return _getCartoBase() + '/light_all/{z}/{x}/{y}{r}.png'; }, invertFilter: INVERT_CSS, type: 'dark', attribution: '© OpenStreetMap © CartoDB', maxZoom: 19 },
'osm-standard': { provider: 'osm', label: 'OSM Standard', url: _getOsmUrl, invertFilter: null, type: 'light', attribution: '© OpenStreetMap contributors, Maps © Mapbox/Thunderforest/MapTiler', maxZoom: 18 },
'osm-dark': { provider: 'osm', label: 'OSM Standard', url: _getOsmUrl, invertFilter: INVERT_CSS, type: 'dark', attribution: '© OpenStreetMap contributors, Maps © Mapbox/Thunderforest/MapTiler', maxZoom: 18 },
'opentopomap': { provider: 'opentopomap', label: 'OpenTopoMap', url: function() { return 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; }, invertFilter: null, type: 'light', attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="https://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)', maxZoom: 17 },
'stamen-toner-lite': { provider: 'stamen', label: 'Stamen Toner Lite', url: _getStamenUrl, invertFilter: null, type: 'light', attribution: '© Stadia Maps © Stamen Design © OpenStreetMap', maxZoom: 20 },
'stamen-toner-dark': { provider: 'stamen', label: 'Stamen Toner Lite', url: _getStamenUrl, invertFilter: INVERT_CSS, type: 'dark', attribution: '© Stadia Maps © Stamen Design © OpenStreetMap', maxZoom: 20 },
'usgs-topo': { provider: 'usgs', label: 'USGS Topographic', url: function() { return 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}'; }, invertFilter: null, type: 'light', attribution: 'U.S. Geological Survey, National Geospatial Program', maxZoom: 16 },
'usgs-imagery': { provider: 'usgs', label: 'USGS Imagery', url: function() { return 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}'; }, invertFilter: null, type: 'light', attribution: 'U.S. Geological Survey, National Geospatial Program', maxZoom: 16 },
'esri-darkgray-labels': { provider: 'esri', label: 'Esri Dark Gray Canvas', url: function() { return 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}'; }, invertFilter: null, type: 'dark', attribution: 'Tiles © Esri', maxZoom: 19 }
};

Expand All @@ -65,6 +68,8 @@

var HAS_CARTO = !_cfg || !_cfg.providers || !_cfg.providers.carto || _cfg.providers.carto.enabled !== false;
var HAS_OSM = _cfg && _cfg.providers && _cfg.providers.osm && _cfg.providers.osm.enabled;
var HAS_OPENTOPOMAP = _cfg && _cfg.providers && _cfg.providers.opentopomap && _cfg.providers.opentopomap.enabled;
var HAS_USGS = _cfg && _cfg.providers && _cfg.providers.usgs && _cfg.providers.usgs.enabled;
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

Expand All @@ -73,7 +78,9 @@
var style = BASE_STYLES[key];
if (style.provider === 'carto' && HAS_CARTO) REGISTRY[key] = style;
if (style.provider === 'osm' && HAS_OSM) REGISTRY[key] = style;
if (style.provider === 'opentopomap' && HAS_OPENTOPOMAP) REGISTRY[key] = style;
if (style.provider === 'stamen' && HAS_STAMEN) REGISTRY[key] = style;
if (style.provider === 'usgs' && HAS_USGS) REGISTRY[key] = style;
if (style.provider === 'esri' && HAS_ESRI) REGISTRY[key] = style;
}

Expand Down
86 changes: 86 additions & 0 deletions test-issue-1420-tile-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,92 @@ test('OSM providers appear when osm.enabled=true in MC_MAP_CFG', () => {
for (const id of ALL_OSM_IDS) assert.ok(reg[id], 'should have ' + id + ' when osm enabled');
});

// --- #1891: opt-in topographic layers (OpenTopoMap, USGS) ---
// These reach third-party tile servers the operator has to opt into, so the
// default-off gating is the security-relevant part, not a nicety.
const TOPO_IDS = { opentopomap: ['opentopomap'], usgs: ['usgs-topo', 'usgs-imagery'] };

test('#1891 topo providers are absent by default (no MC_MAP_CFG)', () => {
const ctx = makeSandbox();
loadProviders(ctx);
ctx.window.MC_initTileRegistry(false);
const reg = ctx.window.MC_TILE_PROVIDERS;
for (const ids of Object.values(TOPO_IDS)) {
for (const id of ids) assert.ok(!reg[id], id + ' must not appear without explicit opt-in');
}
});

test('#1891 topo providers stay absent under the shipped config.example.json', () => {
const providers = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.example.json'), 'utf8')).map.tiles.providers;
const ctx = makeSandbox();
loadProviders(ctx);
ctx.window.MC_MAP_CFG = { tiles: { providers } };
ctx.window.MC_initTileRegistry(false);
const reg = ctx.window.MC_TILE_PROVIDERS;
for (const ids of Object.values(TOPO_IDS)) {
for (const id of ids) assert.ok(!reg[id], id + ' must ship disabled in config.example.json');
}
});

for (const [provider, ids] of Object.entries(TOPO_IDS)) {
test('#1891 ' + provider + ' appears only when ' + provider + '.enabled=true', () => {
const on = makeSandbox();
loadProviders(on);
on.window.MC_MAP_CFG = { tiles: { providers: { [provider]: { enabled: true } } } };
on.window.MC_initTileRegistry(false);
for (const id of ids) assert.ok(on.window.MC_TILE_PROVIDERS[id], id + ' should appear when enabled');
// Enabling one topo provider must not pull in the other.
for (const [other, otherIds] of Object.entries(TOPO_IDS)) {
if (other === provider) continue;
for (const id of otherIds) assert.ok(!on.window.MC_TILE_PROVIDERS[id], id + ' must stay absent');
}

const off = makeSandbox();
loadProviders(off);
off.window.MC_MAP_CFG = { tiles: { providers: { [provider]: { enabled: false } } } };
off.window.MC_initTileRegistry(false);
for (const id of ids) assert.ok(!off.window.MC_TILE_PROVIDERS[id], id + ' should be absent when disabled');
});
}

test('#1891 topo layers carry their required attribution and zoom caps', () => {
const ctx = makeSandbox();
loadProviders(ctx);
ctx.window.MC_MAP_CFG = { tiles: { providers: { opentopomap: { enabled: true }, usgs: { enabled: true } } } };
ctx.window.MC_initTileRegistry(false);
const reg = ctx.window.MC_TILE_PROVIDERS;
// OpenTopoMap is CC-BY-SA: dropping the credit is a licence violation.
assert.match(reg['opentopomap'].attribution, /OpenTopoMap/, 'OpenTopoMap credit required');
assert.match(reg['opentopomap'].attribution, /CC-BY-SA/, 'OpenTopoMap licence required');
// CC BY-SA 3.0 4(a) asks for the licence URI where the medium allows it,
// and Leaflet renders HTML here, so the credit and licence must be links.
assert.match(reg['opentopomap'].attribution, /creativecommons\.org\/licenses\/by-sa/, 'CC-BY-SA licence URI required');
assert.match(reg['opentopomap'].attribution, /href="https:\/\/opentopomap\.org"/, 'OpenTopoMap credit must link to the project');
assert.strictEqual(reg['opentopomap'].maxZoom, 17, 'OpenTopoMap serves no tiles above z17');
for (const id of TOPO_IDS.usgs) {
assert.match(reg[id].attribution, /U\.S\. Geological Survey/, id + ' must credit USGS');
assert.strictEqual(reg[id].maxZoom, 16, id + ' serves no tiles above z16');
}
});

test('#1891 topo tile URLs use the template each service expects', () => {
const ctx = makeSandbox();
loadProviders(ctx);
ctx.window.MC_MAP_CFG = { tiles: { providers: { opentopomap: { enabled: true }, usgs: { enabled: true } } } };
ctx.window.MC_initTileRegistry(false);
const reg = ctx.window.MC_TILE_PROVIDERS;
const urlOf = (id) => typeof reg[id].url === 'function' ? reg[id].url() : reg[id].url;
// OpenTopoMap is {z}/{x}/{y}; the ArcGIS services are {z}/{y}/{x}. Getting
// the order wrong still yields 200s in some places, so pin it here.
assert.ok(urlOf('opentopomap').endsWith('/{z}/{x}/{y}.png'), 'OpenTopoMap uses z/x/y');
assert.ok(urlOf('opentopomap').startsWith('https://'), 'tiles must be fetched over TLS');
for (const id of TOPO_IDS.usgs) {
assert.ok(urlOf(id).endsWith('/tile/{z}/{y}/{x}'), id + ' uses the ArcGIS z/y/x order');
assert.ok(urlOf(id).startsWith('https://'), id + ' must be fetched over TLS');
assert.ok(!/\?|api_key|token/i.test(urlOf(id)), id + ' needs no credentials');
}
});

test('OSM providers absent when osm.enabled=false', () => {
const ctx = makeSandbox();
loadProviders(ctx);
Expand Down
Loading