fix(maps): Carto now stamps its key-free tiles, so Light and Topo move to Esri
Reported with a screenshot: the map came up with API KEY REQUIRED written twice across the middle of it, on the Light and Voyager views only. Those are exactly the two Carto basemaps. Nothing here could have caught it. The tiles still answer 200 with a real image — the watermark is IN the image — so the map looks like it is working and simply says otherwise in large grey letters. Both views move to Esri, which is already behind Street and Satellite. That is the point: a second key-free provider is a second chance to be cut off, and this is now the third time a tile source has changed its terms under us. Light is Esri's light canvas with its own label layer, and Voyager becomes the topographic map, labelled Topo — the stored key stays 'voyager' so nobody who chose it is quietly moved somewhere else. The canvas runs out of tiles at zoom 16 where the others go to 19, so maxNativeZoom is set per basemap: Leaflet upscales the last real tile instead of asking for a level that does not exist, which is the difference between a slightly soft map and a blank grey square.
This commit is contained in:
@@ -42,8 +42,18 @@ function unwrapLon(ring: [number, number][]): [number, number][] {
|
||||
return out;
|
||||
}
|
||||
|
||||
const CARTO_LIGHT = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
const CARTO_ATTR = '© OpenStreetMap © CARTO';
|
||||
// CARTO IS GONE, and it went the same way OpenStreetMap did.
|
||||
//
|
||||
// Their key-free basemaps now serve tiles stamped "API KEY REQUIRED" across
|
||||
// the middle of the map — reported by an operator whose Light and Voyager views
|
||||
// came up watermarked. They still answer 200 with a real image, which is why
|
||||
// nothing here could detect it: the map looks like it is working and simply
|
||||
// says otherwise in large grey letters.
|
||||
//
|
||||
// So both are replaced by Esri layers, the same provider already behind Street
|
||||
// and Satellite. That is deliberate: a second key-free provider is a second
|
||||
// chance to be cut off, and this one has already been serving the other two
|
||||
// views for as long as OpsLog has had a map.
|
||||
|
||||
// NOT tile.openstreetmap.org. Those are OpenStreetMap's VOLUNTEER-run servers,
|
||||
// and their usage policy does not cover a desktop application handed to an
|
||||
@@ -55,17 +65,33 @@ const CARTO_ATTR = '© OpenStreetMap © CARTO';
|
||||
// still credits OpenStreetMap) and Esri. Anything added here later must be
|
||||
// checked the same way — a free tile URL is not the same thing as a tile URL we
|
||||
// are allowed to ship.
|
||||
const ESRI_STREET = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}';
|
||||
const ESRI = 'https://server.arcgisonline.com/ArcGIS/rest/services';
|
||||
const ESRI_STREET = ESRI + '/World_Street_Map/MapServer/tile/{z}/{y}/{x}';
|
||||
const ESRI_STREET_ATTR = 'Tiles © Esri — Source: Esri, HERE, Garmin, © OpenStreetMap contributors';
|
||||
|
||||
// The plain background the old Carto "Light" was: a pale canvas that lets the
|
||||
// paths and the spots carry the colour. Its names come on a separate
|
||||
// transparent layer, the same arrangement the satellite view already uses.
|
||||
const ESRI_LIGHT = ESRI + '/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
|
||||
const ESRI_LIGHT_LABELS = ESRI + '/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}';
|
||||
const ESRI_LIGHT_ATTR = 'Tiles © Esri — Esri, DeLorme, NAVTEQ';
|
||||
|
||||
// Selectable basemaps for the world (great-circle) map. All key-free and all
|
||||
// LABELLED (country/continent names). `labelsUrl` adds a transparent place-name
|
||||
// overlay on top of an imagery basemap (so satellite keeps its names too).
|
||||
export type BasemapKey = 'light' | 'voyager' | 'street' | 'satellite';
|
||||
export const BASEMAPS: Record<BasemapKey, { label: string; url: string; attr: string; subdomains?: string; labelsUrl?: string }> = {
|
||||
light: { label: 'Light', url: CARTO_LIGHT, attr: CARTO_ATTR, subdomains: 'abcd' },
|
||||
voyager: { label: 'Voyager', url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
|
||||
attr: CARTO_ATTR, subdomains: 'abcd' },
|
||||
// maxNativeZoom is where a layer RUNS OUT of tiles. Leaflet then upscales the
|
||||
// last real one instead of asking for a level that does not exist, which is the
|
||||
// difference between a slightly soft map and a blank grey square: the light
|
||||
// canvas stops at 16 where the imagery and the topo go on to 19.
|
||||
export const BASEMAPS: Record<BasemapKey, { label: string; url: string; attr: string; subdomains?: string; labelsUrl?: string; maxNativeZoom?: number }> = {
|
||||
light: { label: 'Light', url: ESRI_LIGHT, attr: ESRI_LIGHT_ATTR, labelsUrl: ESRI_LIGHT_LABELS, maxNativeZoom: 16 },
|
||||
// The key is still "voyager" so that everyone who chose it keeps their view
|
||||
// rather than being silently moved to another one. What it draws is Esri's
|
||||
// topographic map: terrain under the labels, which is what made Voyager worth
|
||||
// choosing over the plain canvas.
|
||||
voyager: { label: 'Topo', url: ESRI + '/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
|
||||
attr: 'Tiles © Esri — Esri, HERE, Garmin, USGS, NGA, © OpenStreetMap contributors' },
|
||||
street: { label: 'Street', url: ESRI_STREET, attr: ESRI_STREET_ATTR },
|
||||
satellite: { label: 'Satellite', url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
attr: 'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics',
|
||||
@@ -100,7 +126,8 @@ export function addBasemap(
|
||||
if (labels.current) { m.removeLayer(labels.current); labels.current = null; }
|
||||
const bm = BASEMAPS[key];
|
||||
const tileOpts: L.TileLayerOptions = {
|
||||
maxZoom: 19, updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 1,
|
||||
maxZoom: 19, maxNativeZoom: bm.maxNativeZoom ?? 19,
|
||||
updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 1,
|
||||
noWrap: !!opts?.noWrap,
|
||||
...(opts?.bounds ? { bounds: opts.bounds } : {}),
|
||||
};
|
||||
@@ -448,8 +475,12 @@ export function LocatorMap({ toGrid, toLabel }: LocatorProps) {
|
||||
// single ring of buffer tiles. Requesting tiles as fast as the pointer
|
||||
// moves is what gets an app blocked, and the next provider is under no
|
||||
// more obligation to tolerate it than the last one was.
|
||||
L.tileLayer(CARTO_LIGHT, {
|
||||
attribution: CARTO_ATTR, subdomains: 'abcd', maxZoom: 19,
|
||||
L.tileLayer(ESRI_LIGHT, {
|
||||
attribution: ESRI_LIGHT_ATTR, maxZoom: 19, maxNativeZoom: 16,
|
||||
updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 1,
|
||||
}).addTo(m);
|
||||
L.tileLayer(ESRI_LIGHT_LABELS, {
|
||||
maxZoom: 19, maxNativeZoom: 16,
|
||||
updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 1,
|
||||
}).addTo(m);
|
||||
locatorOverlay.current = L.layerGroup().addTo(m);
|
||||
|
||||
Reference in New Issue
Block a user