Legends: clip tall ones and link to the full image; add the coastlines legend
The coastlines layer showed no legend because it was added without a legendUrl, so _registerLegend was never called. The provider does serve one: GeoServer renders it on demand through GetLegendGraphic. It is 230 x 2110 px with roughly 105 entries — one shoreline per year plus the rates-of-change classes. Nothing sensible happens if that is scaled into a 220 px panel. Two columns halves the label size, four columns without labels is not a legend, and one column scaled to fit is a 2,000 px scroll. So the image is now shown at natural size inside a frame capped at 200 px with overflow hidden: the first entries stay legible at full size, a gradient fades the cut edge, and a caption invites opening the full legend, which the frame links to in a new tab where neither the panel geometry nor the cross-origin policy applies. A legend that fits is left exactly as it was — no fade, no caption. Verified in a browser against both: the 2110 px one clips with fade and caption, the 125 px slope legend does not. That host sends neither Access-Control-Allow-Origin nor Cross-Origin-Resource-Policy, so under require-corp both the direct and the CORS path fail and it has to go through legend.php. It is allow-listed there with a note on which header each DEAfrica host is missing, since that is what decides whether the proxy is needed. Verified live: 35,802 bytes of image/png. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b7e79dc778
commit
8c859197ad
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/index-CPKm3M4d.js.map
vendored
Normal file
1
dist/assets/index-CPKm3M4d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-DY549L0n.js.map
vendored
1
dist/assets/index-DY549L0n.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
import{d as f,z as d}from"./index-DY549L0n.js";import"./vite-helpers-BlTxHScW.js";import"./bootstrap-D1-uvFxm.js";import"./openlayers-D5-VSKzq.js";import"./ol-ext-DV70ljNY.js";function p(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function h(e){let l="",o=e;for(;o>=0;)l=String.fromCharCode(o%26+65)+l,o=Math.floor(o/26)-1;return l}function x(e){return e&&!Array.isArray(e)&&Array.isArray(e.cells)?{cells:e.cells,bold:!!e.bold}:{cells:Array.isArray(e)?e:[e],bold:!1}}function u(e){const l=[];return e.forEach((o,n)=>{const{cells:t,bold:a}=x(o),r=a?' s="1"':"",i=t.map((s,c)=>{const m=`${h(c)}${n+1}`;return s==null||s===""?`<c r="${m}"${r}/>`:typeof s=="number"&&Number.isFinite(s)?`<c r="${m}"${r}><v>${s}</v></c>`:`<c r="${m}"${r} t="inlineStr"><is><t xml:space="preserve">${p(s)}</t></is></c>`}).join("");l.push(`<row r="${n+1}">${i}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
import{d as f,z as d}from"./index-CPKm3M4d.js";import"./vite-helpers-BlTxHScW.js";import"./bootstrap-D1-uvFxm.js";import"./openlayers-D5-VSKzq.js";import"./ol-ext-DV70ljNY.js";function p(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function h(e){let l="",o=e;for(;o>=0;)l=String.fromCharCode(o%26+65)+l,o=Math.floor(o/26)-1;return l}function x(e){return e&&!Array.isArray(e)&&Array.isArray(e.cells)?{cells:e.cells,bold:!!e.bold}:{cells:Array.isArray(e)?e:[e],bold:!1}}function u(e){const l=[];return e.forEach((o,n)=>{const{cells:t,bold:a}=x(o),r=a?' s="1"':"",i=t.map((s,c)=>{const m=`${h(c)}${n+1}`;return s==null||s===""?`<c r="${m}"${r}/>`:typeof s=="number"&&Number.isFinite(s)?`<c r="${m}"${r}><v>${s}</v></c>`:`<c r="${m}"${r} t="inlineStr"><is><t xml:space="preserve">${p(s)}</t></is></c>`}).join("");l.push(`<row r="${n+1}">${i}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${l.join("")}</sheetData></worksheet>`}const y=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${l.join("")}</sheetData></worksheet>`}const y=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||||
<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>
|
<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>
|
||||||
@ -27,4 +27,4 @@ ${n.map(t=>`<Relationship Id="rId${t.id}" Type="http://schemas.openxmlformats.or
|
|||||||
`)}
|
`)}
|
||||||
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||||
</Relationships>`,o["xl/styles.xml"]=y,d(o,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}function S(e,l){f(g(e),l)}export{g as buildXlsxBlob,S as exportXlsx};
|
</Relationships>`,o["xl/styles.xml"]=y,d(o,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}function S(e,l){f(g(e),l)}export{g as buildXlsxBlob,S as exportXlsx};
|
||||||
//# sourceMappingURL=xlsx-Xgu_2_hC.js.map
|
//# sourceMappingURL=xlsx-CE8N38FZ.js.map
|
||||||
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
@ -2004,7 +2004,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-DY549L0n.js"></script>
|
<script type="module" crossorigin src="/assets/index-CPKm3M4d.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vite-helpers-BlTxHScW.js">
|
<link rel="modulepreload" crossorigin href="/assets/vite-helpers-BlTxHScW.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/bootstrap-D1-uvFxm.js">
|
<link rel="modulepreload" crossorigin href="/assets/bootstrap-D1-uvFxm.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
|
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
|
||||||
|
|||||||
3
dist/legend.php
vendored
3
dist/legend.php
vendored
@ -31,7 +31,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/** Hosts whose legend images may be proxied. */
|
/** Hosts whose legend images may be proxied. */
|
||||||
const ALLOWED_HOSTS = [
|
const ALLOWED_HOSTS = [
|
||||||
'ows.digitalearth.africa',
|
'ows.digitalearth.africa', // datacube-ows — sends ACAO but no CORP
|
||||||
|
'geoserver.digitalearth.africa', // GeoServer — sends neither, so needs this proxy
|
||||||
];
|
];
|
||||||
|
|
||||||
const MAX_BYTES = 2097152; // 2 MB — legends are a few KB; this is a sanity cap
|
const MAX_BYTES = 2097152; // 2 MB — legends are a few KB; this is a sanity cap
|
||||||
|
|||||||
13
main.js
13
main.js
@ -2339,7 +2339,18 @@ function loadExternalWMSLayers() {
|
|||||||
'DEAfrica Coastlines v0.4',
|
'DEAfrica Coastlines v0.4',
|
||||||
'https://geoserver.digitalearth.africa/geoserver/wms',
|
'https://geoserver.digitalearth.africa/geoserver/wms',
|
||||||
'coastlines:DEAfrica_Coastlines',
|
'coastlines:DEAfrica_Coastlines',
|
||||||
{ serverType: 'geoserver', visible: false, onlineOnly: true }
|
{
|
||||||
|
serverType: 'geoserver',
|
||||||
|
visible: false,
|
||||||
|
onlineOnly: true,
|
||||||
|
// GeoServer renders this on demand. It is a tall legend — roughly 105
|
||||||
|
// entries, one shoreline per year plus the rates-of-change classes — so
|
||||||
|
// the panel clips it and links to the full image. This host sends neither
|
||||||
|
// CORS nor CORP headers, so it is served through /legend.php.
|
||||||
|
legendUrl:
|
||||||
|
'https://geoserver.digitalearth.africa/geoserver/wms?service=WMS&version=1.1.0' +
|
||||||
|
'&request=GetLegendGraphic&format=image/png&layer=coastlines:DEAfrica_Coastlines',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note: OpenTopoMap is available as the "Topographic" base map —
|
// Note: OpenTopoMap is available as the "Topographic" base map —
|
||||||
|
|||||||
@ -31,7 +31,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/** Hosts whose legend images may be proxied. */
|
/** Hosts whose legend images may be proxied. */
|
||||||
const ALLOWED_HOSTS = [
|
const ALLOWED_HOSTS = [
|
||||||
'ows.digitalearth.africa',
|
'ows.digitalearth.africa', // datacube-ows — sends ACAO but no CORP
|
||||||
|
'geoserver.digitalearth.africa', // GeoServer — sends neither, so needs this proxy
|
||||||
];
|
];
|
||||||
|
|
||||||
const MAX_BYTES = 2097152; // 2 MB — legends are a few KB; this is a sanity cap
|
const MAX_BYTES = 2097152; // 2 MB — legends are a few KB; this is a sanity cap
|
||||||
|
|||||||
@ -5209,30 +5209,64 @@ export class MapView {
|
|||||||
heading.textContent = title;
|
heading.textContent = title;
|
||||||
wrapper.appendChild(heading);
|
wrapper.appendChild(heading);
|
||||||
|
|
||||||
// Loading a legend here is not as simple as setting src. The app is
|
// A legend can be any size the provider feels like: the DEAfrica coastlines
|
||||||
// cross-origin isolated (Cross-Origin-Embedder-Policy: require-corp), so a
|
// legend is 230 x 2110 px with about 105 entries. Scaling that down to fit
|
||||||
// cross-origin image is blocked unless the provider sends
|
// makes the labels unreadable, so it is shown at natural size inside a
|
||||||
// Cross-Origin-Resource-Policy — which most OGC servers do not. A CORS
|
// capped, clipped frame — the first entries stay legible — and the frame
|
||||||
// request satisfies COEP on its own, so try the provider directly with
|
// links to the full image, which opens in a tab where neither the panel
|
||||||
// crossorigin first: that is how the base-map tiles already work. If it
|
// geometry nor the cross-origin policy applies.
|
||||||
// still fails, fall back once to /legend.php, which fetches the image
|
const frame = document.createElement('a');
|
||||||
// server-side and serves it from our own origin, where no cross-origin
|
frame.href = legendUrl;
|
||||||
// policy applies. Only after both fail is the entry hidden.
|
frame.target = '_blank';
|
||||||
|
frame.rel = 'noopener noreferrer';
|
||||||
|
frame.title = `Open the full ${title} legend`;
|
||||||
|
frame.style.cssText =
|
||||||
|
'display:block;position:relative;max-height:200px;overflow:hidden;' +
|
||||||
|
'border-radius:3px;text-decoration:none;color:inherit;';
|
||||||
|
|
||||||
|
// Loading the image is not as simple as setting src. The app is cross-origin
|
||||||
|
// isolated (Cross-Origin-Embedder-Policy: require-corp), so a cross-origin
|
||||||
|
// image is blocked unless the provider sends Cross-Origin-Resource-Policy —
|
||||||
|
// which most OGC servers do not. A CORS request satisfies COEP on its own, so
|
||||||
|
// try the provider directly with crossorigin first: that is how the base-map
|
||||||
|
// tiles already work. If it still fails, fall back once to /legend.php, which
|
||||||
|
// fetches the image server-side and serves it from our own origin. Only after
|
||||||
|
// both fail is the entry hidden.
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.alt = `${title} legend`;
|
img.alt = `${title} legend`;
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
img.style.cssText = 'display:block;max-width:100%;height:auto;border-radius:3px;';
|
img.style.cssText = 'display:block;max-width:100%;height:auto;';
|
||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
if (img.dataset.viaProxy === '1') {
|
if (img.dataset.viaProxy === '1') {
|
||||||
img.style.display = 'none';
|
wrapper.style.display = 'none'; // hide the entry, not just the image
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
img.dataset.viaProxy = '1';
|
img.dataset.viaProxy = '1';
|
||||||
img.removeAttribute('crossorigin'); // same-origin from here, so CORS is moot
|
img.removeAttribute('crossorigin'); // same-origin from here, so CORS is moot
|
||||||
img.src = `/legend.php?url=${encodeURIComponent(legendUrl)}`;
|
img.src = `/legend.php?url=${encodeURIComponent(legendUrl)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only once it has loaded can we tell whether it overflows the frame.
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
wrapper.style.display = '';
|
||||||
|
if (frame.scrollHeight <= frame.clientHeight + 1) return;
|
||||||
|
|
||||||
|
const fade = document.createElement('div');
|
||||||
|
fade.style.cssText =
|
||||||
|
'position:absolute;left:0;right:0;bottom:0;height:34px;pointer-events:none;' +
|
||||||
|
'background:linear-gradient(to bottom, transparent, var(--card, #fff));';
|
||||||
|
frame.appendChild(fade);
|
||||||
|
|
||||||
|
const more = document.createElement('div');
|
||||||
|
more.style.cssText =
|
||||||
|
'font-size:10px;margin-top:3px;color:var(--muted-foreground, #7a7a7a);';
|
||||||
|
more.textContent = 'Open the full legend \u2197';
|
||||||
|
wrapper.appendChild(more);
|
||||||
|
});
|
||||||
|
|
||||||
img.src = legendUrl;
|
img.src = legendUrl;
|
||||||
wrapper.appendChild(img);
|
frame.appendChild(img);
|
||||||
|
wrapper.appendChild(frame);
|
||||||
|
|
||||||
this._legendEntries.set(layer, wrapper);
|
this._legendEntries.set(layer, wrapper);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user