// ==UserScript== // @name BRGM — Légende dynamique cartes.gouv.fr (V8.8) // @namespace lollien.brgm // @version 0.8.8 // @description Emprises WMS intégrées, activées par défaut et mémorisées ; conserve le moteur de légende V8.7. // @match https://cartes.gouv.fr/* // @match https://www.cartes.gouv.fr/* // @run-at document-idle // @grant none // ==/UserScript== (() => { 'use strict'; const VERSION = '8.8', RUN = '__brgmLegende88', ID = 'brgm-legende-siges-88'; const LAYER = 'GEO50_IMPRIME_IDF', R = 6378137, ORIGIN_KEY = 'brgm-legend-origin'; const EMPRISES_KEY = 'brgm-legend-show-emprises'; // Affichage par défaut. Le choix de l'utilisateur est conservé entre deux exécutions. const readOverlayPreference = () => { try {return localStorage.getItem(EMPRISES_KEY) !== 'false';} catch (_) {return true;} }; const saveOverlayPreference = visible => { try {localStorage.setItem(EMPRISES_KEY, String(visible));}catch (_) {} }; for (const old of ['__brgmLegende8','__brgmLegende81','__brgmLegende82','__brgmLegende83','__brgmLegende84','__brgmLegende85','__brgmLegende86','__brgmLegende87',RUN]) { try { window[old]?.stop?.(); } catch (_) {} } for (const old of ['brgm-legende-siges-8','brgm-legende-siges-81', 'brgm-legende-siges-82','brgm-legende-siges-83','brgm-legende-siges-84','brgm-legende-siges-85','brgm-legende-siges-86','brgm-legende-siges-87',ID]) { document.getElementById(old)?.remove(); } const abort = new AbortController(); const state = { active: true, origin: '', view: null, olMap: null, olSearchAt: 0, attachedViewport: null, drag: null, moving: false, waitingForZoomTiles: false, zoomSince: 0, zoomWaitReason: '', latestSamples: [], lastSampleId: '', lastSampleAt: 0, pendingKey: '', pendingSince: 0, lastMove: 0, lastKey: '', lastSuccessfulBbox: '', responseSheets: [], loading: false, controller: null, imageLoading: false, imageKey: '', retryAfter: 0, failureCount: 0, failedKey: '', requests: 0, lastResponseStatus: 0, error: '', lastKind: '', debugOverlay: readOverlayPreference(), debugRoot: null }; const clamp = (v,a,b) => Math.max(a, Math.min(b,v)); const norm = s => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase(); const getParam = (p,k) => { for (const [a,v] of p) if (a.toUpperCase() === k.toUpperCase()) return v; return ''; }; const parseBbox = s => { const b=String(s||'').split(',').map(Number); return b.length===4 && b.every(Number.isFinite) && b[0] !!a && !!b && a.every((n,i)=>Math.abs(n-b[i])<=tol); const mapViewport = () => document.querySelector('.ol-viewport'); const N=.725607765053267,C=11754255.426096,XS=700000,YS=12655612.049876; const E=.0818191910428158,LON0=3*Math.PI/180; function lambertToMercator(x,y) { const dx=x-XS,dy=YS-y,rho=Math.hypot(dx,dy),lambda=LON0+Math.atan2(dx,dy)/N; const L=-Math.log(rho/C)/N; let phi=2*Math.atan(Math.exp(L))-Math.PI/2; for(let i=0;i<8;i++) { const s=Math.sin(phi); phi=2*Math.atan(Math.exp(L)*Math.pow((1+E*s)/(1-E*s),E/2))-Math.PI/2; } return [R*lambda,R*Math.log(Math.tan(Math.PI/4+phi/2))]; } function mercatorToLambert(x,y) { const lambda=x/R, phi=2*Math.atan(Math.exp(y/R))-Math.PI/2, s=Math.sin(phi); const L=Math.log(Math.tan(Math.PI/4+phi/2)*Math.pow((1-E*s)/(1+E*s),E/2)); const rho=C*Math.exp(-N*L); return [XS+rho*Math.sin(N*(lambda-LON0)),YS-rho*Math.cos(N*(lambda-LON0))]; } const lonLatToMerc=(lon,lat)=>[ R*lon*Math.PI/180, R*Math.log(Math.tan(Math.PI/4+clamp(lat,-85,85)*Math.PI/360)) ]; function transformBbox(b,crs,version='1.3.0') { const point=(x,y)=>{ switch(crs) { case 'EPSG:3857': case 'EPSG:900913': return [x,y]; case 'EPSG:2154': return lambertToMercator(x,y); case 'EPSG:4326': return version.startsWith('1.3')?lonLatToMerc(y,x):lonLatToMerc(x,y); case 'CRS:84': return lonLatToMerc(x,y); default: return null; } }; const p=[[b[0],b[1]],[b[0],b[3]],[b[2],b[1]],[b[2],b[3]]].map(v=>point(...v)); if (p.some(v=>!v||v.some(x=>!Number.isFinite(x))))return null; return [Math.min(...p.map(v=>v[0])),Math.min(...p.map(v=>v[1])), Math.max(...p.map(v=>v[0])),Math.max(...p.map(v=>v[1]))]; } function parseGetMap(src) { try { const u=new URL(src,location.href),p=u.searchParams; // IMPORTANT : les GetLegendGraphic ont aussi des BBOX ; ils ne sont pas des tuiles. if(getParam(p,'REQUEST').toUpperCase()!=='GETMAP')return null; if(!getParam(p,'LAYERS').toUpperCase().split(',').map(s=>s.trim()).includes(LAYER))return null; const b=parseBbox(getParam(p,'BBOX'));if(!b)return null; const crs=(getParam(p,'CRS')||getParam(p,'SRS')||'').toUpperCase(); if(!crs)return null; const box=transformBbox(b,crs,getParam(p,'VERSION')||'1.3.0');if(!box)return null; const width=Number(getParam(p,'WIDTH')),height=Number(getParam(p,'HEIGHT')); if(width<16||height<16)return null; const rx=(box[2]-box[0])/width,ry=(box[3]-box[1])/height; if(!Number.isFinite(rx)||!Number.isFinite(ry)||rx<=0||ry<=0)return null; return {box,host:u.origin,width,height,resolution:(rx+ry)/2,crs,src:u.href}; }catch(_){return null;} } // Essaie d'abord la carte OpenLayers déjà utilisée par le site, sans la modifier. // Vue 3 ne l'expose pas toujours ; dans ce cas on utilise les requêtes de tuiles. const isOlMap=x=>x && typeof x.getView==='function' && typeof x.getSize==='function' && typeof x.getTargetElement==='function'; function discoverMap() { if (isOlMap(state.olMap))return state.olMap; if (Date.now()-state.olSearchAt<15000)return null; state.olSearchAt=Date.now(); const app=document.querySelector('#app')?.__vue_app__ || [...document.querySelectorAll('[data-v-app]')].map(e=>e.__vue_app__).find(Boolean); let root=app?._instance; if(!root)return null; const queue=[root],visited=new Set();let processed=0; while(queue.length && processed++<2600) { const comp=queue.shift(); if(!comp || typeof comp!=='object' || visited.has(comp))continue; visited.add(comp); // Accès ciblé aux refs de composants ; évite de parcourir les gros objets applicatifs. for(const source of [comp.setupState,comp.exposed,comp.ctx,comp.refs]) { if(!source || typeof source!=='object')continue; let keys=[];try{keys=Object.keys(source);}catch(_){} for(const k of keys) { if(!/(^map$|olmap|mapinstance|mapref|mapobject)/i.test(k))continue; let obj;try{obj=source[k];}catch(_){continue;} if(isOlMap(obj)){state.olMap=obj;return obj;} if(isOlMap(obj?.value)){state.olMap=obj.value;return obj.value;} } } const q=[comp.subTree]; while(q.length) { const n=q.shift();if(!n || typeof n!=='object')continue; if(Array.isArray(n)){q.push(...n);continue;} if(n.component)queue.push(n.component); if(n.suspense?.activeBranch)q.push(n.suspense.activeBranch); if(n.children && Array.isArray(n.children))q.push(...n.children); } } return null; } function olExtent() { const map=discoverMap();if(!map)return null; try { const view=map.getView(),size=map.getSize(),code=view.getProjection().getCode().toUpperCase(); if(!size || size[0]<150||size[1]<150)return null; const b=transformBbox(view.calculateExtent(size),code,'1.1.1');if(!b)return null; return {bbox:b,source:'OpenLayers',precision:'emprise exacte (vue OpenLayers)'}; }catch(_){state.olMap=null;return null;} } function observeRequest(entry) { if (!state.active)return; // Une entrée Performance ancienne peut venir de Versailles après un saut vers Noisiel. if (performance.now()-entry.startTime>12000)return; const parsed=parseGetMap(entry.name);if(!parsed)return; const s={...parsed,time:performance.timeOrigin+entry.startTime,id:entry.name+'|'+entry.startTime}; if(s.id===state.lastSampleId)return; state.lastSampleId=s.id; state.lastSampleAt=Date.now(); state.latestSamples.push(s); state.latestSamples=state.latestSamples.filter(x=>s.time-x.time<17000).slice(-160); } const observer=new PerformanceObserver(list=>{ for(const entry of list.getEntries())observeRequest(entry); }); try{observer.observe({type:'resource',buffered:true});}catch(_){ try{observer.observe({entryTypes:['resource']});}catch(__){} } for (const entry of performance.getEntriesByType('resource').slice(-200))observeRequest(entry); // Ensemble exact des requêtes pris en compte pour estimer la vue. // Les enveloppes de tuiles ne garantissent PAS une couverture continue. function selectedWmsBatch() { const r=mapViewport()?.getBoundingClientRect(); if(!r||r.width<150||r.height<150)return null; const samples=state.latestSamples; if(!samples.length)return null; let recent=samples; if(state.waitingForZoomTiles) { recent=samples.filter(s=>s.time>=state.zoomSince-120); if(!recent.length)return null; } const last=recent[recent.length-1]; let same=recent.filter(s=>last.time-s.time<6000 && Math.abs(Math.log(s.resolution/last.resolution))<0.025 && s.host===last.host); if(!same.length)same=[last]; const lc=[(last.box[0]+last.box[2])/2,(last.box[1]+last.box[3])/2]; const range=Math.max(r.width,r.height)*last.resolution*1.5; same=same.filter(s=>Math.hypot((s.box[0]+s.box[2])/2-lc[0], (s.box[1]+s.box[3])/2-lc[1])s.box[0])),Math.min(...same.map(s=>s.box[1])), Math.max(...same.map(s=>s.box[2])),Math.max(...same.map(s=>s.box[3]))]; const res=last.resolution; const w=r.width*res,h=r.height*res; const coverageX=(union[2]-union[0])/w,coverageY=(union[3]-union[1])/h; return {samples:same,union,last,res,viewportW:w,viewportH:h,coverageX,coverageY}; } function requestsExtent() { const batch=selectedWmsBatch();if(!batch)return null; const {samples:same,union,last,res,viewportW:w,viewportH:h,coverageX,coverageY}=batch; if(coverageX<0.76||coverageY<0.76)return null; const cx=(union[0]+union[2])/2,cy=(union[1]+union[3])/2; return { bbox:[cx-w/2,cy-h/2,cx+w/2,cy+h/2], host:last.host,source:'tuiles WMS', precision:'recalée depuis '+same.length+' requêtes WMS (approximation)', sampleCount:same.length,coverageX,coverageY,resolution:res, lastTileTime:last.time }; } function setView(next) { if(!next || !next.bbox || next.bbox.some(n=>!Number.isFinite(n)))return false; if(!state.view || !sameExtent(state.view.bbox,next.bbox)) { state.view=next; state.lastMove=Date.now(); invalidateImage(); return true; } // Une meilleure source de coordonnées remplace l'estimation sans déclencher de requête. if(next.source==='OpenLayers')state.view=next; return false; } function syncView() { const exact=olExtent(); if(exact){state.waitingForZoomTiles=false;setView(exact);return;} if(state.latestSamples.length && state.lastSampleAt>0 && state.lastSampleAt!==state._usedSampleAt && !state.drag) { const next=requestsExtent(); // Ne pas consommer les nouvelles tuiles tant que la couverture est insuffisante. if(next) { state._usedSampleAt=state.lastSampleAt; state.waitingForZoomTiles=false; setView(next); } } } function moveView(dx,dy) { const b=state.view?.bbox,v=mapViewport()?.getBoundingClientRect(); if(!b||!v||v.width<80||v.height<80)return; const mx=-dx*(b[2]-b[0])/v.width,my=dy*(b[3]-b[1])/v.height; setView({...state.view,bbox:[b[0]+mx,b[1]+my,b[2]+mx,b[3]+my], source:'suivi du déplacement',precision:'estimée (déplacement suivi)'}); } function beginZoom(reason) { if(state.olMap)return; state.zoomSince=Date.now(); state.waitingForZoomTiles=true; state.zoomWaitReason=reason; state.view=null; state._usedSampleAt=state.lastSampleAt; state.lastMove=Date.now(); invalidateImage(); setStatus('Zoom détecté : attente des nouvelles tuiles WMS pour recaler la légende…'); } function installGestureListeners() { const v=mapViewport();if(!v||state.attachedViewport===v)return; state.attachedViewport=v; v.addEventListener('pointerdown',e=>{ if(!e.isPrimary||e.button!==0)return; syncView();state.drag={id:e.pointerId,x:e.clientX,y:e.clientY}; },{capture:true,signal:abort.signal}); document.addEventListener('pointermove',e=>{ if(!state.drag||e.pointerId!==state.drag.id)return; const dx=e.clientX-state.drag.x,dy=e.clientY-state.drag.y; if(!dx&&!dy)return; state.moving=true;moveView(dx,dy); state.drag.x=e.clientX;state.drag.y=e.clientY; },{capture:true,signal:abort.signal}); document.addEventListener('pointerup',e=>{ if(state.drag?.id===e.pointerId){state.drag=null;state.moving=false;state.lastMove=Date.now();} },{capture:true,signal:abort.signal}); document.addEventListener('pointercancel',e=>{ if(state.drag?.id===e.pointerId){state.drag=null;state.moving=false;} },{capture:true,signal:abort.signal}); v.addEventListener('wheel',e=>{ beginZoom('molette'); },{capture:true,passive:true,signal:abort.signal}); document.addEventListener('click',e=>{ const btn=e.target.closest?.('.ol-zoom-in,.ol-zoom-out');if(!btn)return; beginZoom('bouton zoom'); },{capture:true,signal:abort.signal}); window.addEventListener('resize',()=>{ state.olSearchAt=0; const r=v.getBoundingClientRect(),b=state.view?.bbox; if(b && state.view?.source!=='OpenLayers' && !state.waitingForZoomTiles) { const res=(b[2]-b[0])/Math.max(1,state._lastViewportW||r.width), cx=(b[0]+b[2])/2,cy=(b[1]+b[3])/2; setView({...state.view,bbox:[cx-r.width*res/2,cy-r.height*res/2, cx+r.width*res/2,cy+r.height*res/2]}); } state._lastViewportW=r.width; },{signal:abort.signal}); state._lastViewportW=v.getBoundingClientRect().width; } function boundsLambert() { syncView();const b=state.view?.bbox; if(!b)return null; const corners=[[b[0],b[1]],[b[0],b[3]],[b[2],b[1]],[b[2],b[3]]] .map(p=>mercatorToLambert(...p)); if(corners.some(p=>p.some(x=>!Number.isFinite(x))))return null; return { bbox:[Math.min(...corners.map(p=>p[0])),Math.min(...corners.map(p=>p[1])), Math.max(...corners.map(p=>p[0])),Math.max(...corners.map(p=>p[1]))], host:state.view.host,precision:state.view.precision }; } // Mesure l'union des rectangles de requêtes DANS la vue estimée. // Elle ne mesure ni le rendu des tuiles, ni les formations réellement sélectionnées par SIGES. function unionCoverage(boxes,view) { if(!view||!boxes.length)return 0; const clipped=boxes.map(b=>[Math.max(view[0],b[0]),Math.max(view[1],b[1]), Math.min(view[2],b[2]),Math.min(view[3],b[3])]) .filter(b=>b[2]>b[0]&&b[3]>b[1]); if(!clipped.length)return 0; const xs=[...new Set([view[0],view[2],...clipped.flatMap(b=>[b[0],b[2]])])].sort((a,b)=>a-b); let area=0; for(let i=0;ib[0]<=x&&b[2]>=x).map(b=>[b[1],b[3]]).sort((a,b)=>a[0]-b[0]); let length=0,low=null,high=null; for(const [y1,y2] of intervals) { if(low===null){low=y1;high=y2;} else if(y1<=high){high=Math.max(high,y2);} else{length+=high-low;low=y1;high=y2;} } if(low!==null)length+=high-low; area+=(xs[i+1]-xs[i])*length; } return Math.max(0,Math.min(1,area/((view[2]-view[0])*(view[3]-view[1])))); } function removeDebugOverlay() { state.debugRoot?.remove();state.debugRoot=null; } function drawOverlay() { if(!state.debugOverlay){removeDebugOverlay();return;} const viewport=mapViewport();if(!viewport)return; let root=state.debugRoot; if(!root || root.parentElement!==viewport) { removeDebugOverlay(); root=document.createElement('div');root.id=ID+'-overlay'; root.style.cssText='position:absolute;inset:0;pointer-events:none;z-index:9000;overflow:hidden;'; const c=document.createElement('canvas');c.style.cssText='width:100%;height:100%;display:block;'; const info=document.createElement('div');info.style.cssText='position:absolute;bottom:42px;right:78px;padding:8px 10px;background:rgba(15,24,34,.88);color:#fff;border-radius:5px;font:11px/1.5 Arial,sans-serif;max-width:260px;white-space:pre-line;'; info.id=ID+'-overlay-info';root.append(c,info);viewport.appendChild(root);state.debugRoot=root; } const c=root.querySelector('canvas'),label=root.querySelector('#'+ID+'-overlay-info'); const rect=viewport.getBoundingClientRect(),w=rect.width,h=rect.height; if(w<150||h<150)return; const dpr=Math.min(window.devicePixelRatio||1,2); const cw=Math.round(w*dpr),ch=Math.round(h*dpr); if(c.width!==cw)c.width=cw; if(c.height!==ch)c.height=ch; const ctx=c.getContext('2d');if(!ctx)return; ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h); const v=state.view?.bbox,batch=selectedWmsBatch(); if(!v){label.textContent='Attente d’une emprise géographique valide…';return;} const toXY=([x,y])=>[(x-v[0])/(v[2]-v[0])*w,(v[3]-y)/(v[3]-v[1])*h]; const boxRect=(b,color,fill,dash=[],width=2)=>{ const [x1,y1]=toXY([b[0],b[3]]),[x2,y2]=toXY([b[2],b[1]]); if(x2<0||x1>w||y2<0||y1>h)return; ctx.save();ctx.beginPath();ctx.rect(x1,y1,x2-x1,y2-y1); if(fill){ctx.fillStyle=fill;ctx.fill();} ctx.strokeStyle=color;ctx.lineWidth=width;ctx.setLineDash(dash);ctx.stroke();ctx.restore(); }; let count=0,coverage=null; if(batch){ count=batch.samples.length; const boxes=batch.samples.map(s=>s.box); coverage=unionCoverage(boxes,v); for(const b of boxes)boxRect(b,'rgba(255,170,0,.95)','rgba(255,170,0,.09)',[],1.2); boxRect(batch.union,'#ffb000','', [8,5],2.5); } // L'emprise bleu cyan représente la vraie BBOX envoyée à GetLegendGraphic, // reprojetée en EPSG:3857 (polygone à 4 coins, et non juste rectangle projeté). let legendBox=null; try{ const url=state.pendingKey ? new URL(state.pendingKey) : null; legendBox=parseBbox(url?.searchParams.get('BBOX')||state.lastSuccessfulBbox); }catch(_){} if(legendBox){ const vertices=[[legendBox[0],legendBox[1]],[legendBox[2],legendBox[1]], [legendBox[2],legendBox[3]],[legendBox[0],legendBox[3]]] .map(pt=>toXY(lambertToMercator(...pt))); ctx.save();ctx.beginPath();vertices.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p)); ctx.closePath();ctx.fillStyle='rgba(0,215,255,.10)';ctx.fill(); ctx.strokeStyle='#00d7ff';ctx.lineWidth=3;ctx.setLineDash([12,5]);ctx.stroke();ctx.restore(); } // Un cadre blanc indique les limites du viewport, pas une mesure de localisation indépendante. ctx.save();ctx.strokeStyle='rgba(255,255,255,.88)';ctx.lineWidth=2; ctx.setLineDash([4,6]);ctx.strokeRect(3,3,w-6,h-6);ctx.restore(); const parts=[ '🟠 Rectangles : requêtes GetMap', '🟠 Pointillés : enveloppe des requêtes', '🔵 Cyan : emprise demandée à SIGES', '⬜ Bordure : écran visible', 'Requêtes retenues : '+count, 'Couverture théorique : '+(coverage===null?'indisponible':(coverage*100).toFixed(1)+' %'), 'Position : '+(state.view?.source||'indisponible')+' (estimée)' ]; label.textContent=parts.join('\n'); } function findPanel() { const els=[...document.querySelectorAll('h1,h2,h3,h4,button,span,div,[role="heading"]')] .filter(e=>e.children.length===0 && /^l[eé]gendes?$/i.test((e.textContent||'').trim())); for(const e of els) { const poss=[]; for(let p=e.parentElement,n=0;p&&n<11;p=p.parentElement,n++){ const a=p.getBoundingClientRect(); if(a.width<205||a.width>720||a.height<100||a.height>innerHeight+450||a.bottom<0||a.top>innerHeight)continue; const tx=norm(p.textContent),cls=norm((typeof p.className==='string'?p.className:'')+' '+(p.getAttribute('aria-label')||'')); const score=(/legend/.test(cls)?6:0)+(/carte geologique|plan ign|photographies aeriennes|ouvrir la legende|aucune legende|geologie imprimee/.test(tx)?4:0)+(a.height>175?2:0)+n/15; poss.push({p,score}); } poss.sort((a,b)=>b.score-a.score); if(poss.length && poss[0].score>1)return {panel:poss[0].p,title:e}; } return null; } function ui() { let section=document.getElementById(ID);if(section&&document.contains(section))return section; const f=findPanel();if(!f)return null; section=document.createElement('section');section.id=ID; section.style.cssText='padding:10px;margin:8px 6px 12px;border:1px solid #d6d9df;border-radius:4px;background:#fff;color:#1c2430;font:12px/1.4 Arial,sans-serif;max-height:65vh;overflow-y:auto'; const heading=document.createElement('div');heading.textContent='Géologie imprimée — légende du secteur';heading.style.cssText='font-weight:700;font-size:13px;margin:0 0 7px'; const status=document.createElement('div');status.id=ID+'-status';status.style.cssText='margin:6px 0;color:#485568;font-size:11px';status.textContent='Détection de l’emprise…'; const image=document.createElement('img');image.id=ID+'-image';image.alt='Légende géologique du secteur';image.style.cssText='display:none;width:100%;max-width:330px;height:auto;background:white'; const buttons=document.createElement('div');buttons.style.cssText='display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:7px 0'; const refresh=document.createElement('button');refresh.type='button';refresh.textContent='Actualiser la légende';refresh.style.cssText='border:1px solid #adb6c4;border-radius:4px;background:#f4f5f7;color:#1c2430;padding:5px 8px;cursor:pointer;font-size:11px'; refresh.addEventListener('click',()=>{state.lastKey='';state.failedKey='';state.retryAfter=0;update(true);}); const test=document.createElement('a');test.id=ID+'-test';test.textContent='Tester le serveur';test.target='_blank';test.rel='noopener noreferrer';test.style.cssText='font-size:11px;text-decoration:underline;display:none'; const debug=document.createElement('button');debug.type='button';debug.id=ID+'-debug'; debug.style.cssText='border:1px solid #adb6c4;border-radius:4px;background:#f4f5f7;color:#1c2430;padding:5px 8px;cursor:pointer;font-size:11px'; debug.textContent=state.debugOverlay?'Masquer les emprises':'Afficher les emprises'; debug.addEventListener('click',()=>{ state.debugOverlay=!state.debugOverlay; saveOverlayPreference(state.debugOverlay); debug.textContent=state.debugOverlay?'Masquer les emprises':'Afficher les emprises'; drawOverlay(); }); buttons.append(refresh,test,debug); const details=document.createElement('details');details.style.cssText='margin-top:7px;font-size:11px'; const summary=document.createElement('summary');summary.textContent='Réglages et diagnostic'; const input=document.createElement('input');input.type='url';input.placeholder='https://votre-projet.pages.dev';input.value=localStorage.getItem(ORIGIN_KEY)||'';input.style.cssText='width:100%;box-sizing:border-box;padding:6px;margin:6px 0'; const save=document.createElement('button');save.type='button';save.textContent='Enregistrer l’adresse Cloudflare';save.style.cssText='padding:5px 8px;cursor:pointer'; save.addEventListener('click',()=>{ try{const u=new URL(input.value);if(u.protocol!=='https:')throw Error('HTTPS requis'); state.origin=u.origin;localStorage.setItem(ORIGIN_KEY,state.origin); state.lastKey='';state.pendingKey='';state.retryAfter=0;update(true); }catch(_){setStatus('Adresse Cloudflare invalide.');} }); const diag=document.createElement('pre');diag.id=ID+'-diag';diag.style.cssText='white-space:pre-wrap;font-size:10px;max-height:130px;overflow:auto'; details.addEventListener('toggle',()=>{if(details.open)diagnostic();}); details.append(summary,input,save,diag);section.append(heading,status,buttons,image,details); const header=f.title.closest('header')||f.title.parentElement; if(header&&header!==f.panel&&f.panel.contains(header))header.insertAdjacentElement('afterend',section); else f.panel.insertBefore(section,f.panel.firstElementChild||null); console.info('[BRGM V8.8] Légende insérée.'); return section; } function setStatus(s){const e=document.getElementById(ID+'-status');if(e)e.textContent=s;} function invalidateImage() { const image=document.getElementById(ID+'-image'); if(image){image.onload=null;image.onerror=null;image.style.display='none';image.removeAttribute('src');} state.imageLoading=false;state.imageKey=''; state.responseSheets=[];state.lastSuccessfulBbox=''; state.lastKey=''; if(state.controller)state.controller.abort(); state.pendingKey=''; } function diagnostic() { const i=boundsLambert(); const batch=selectedWmsBatch(); const theoreticalCoverage=state.view?.bbox&&batch ? unionCoverage(batch.samples.map(x=>x.box),state.view.bbox) : null; const obj={version:VERSION,debugOverlay:state.debugOverlay,selectedMapRequests:batch?.samples.length||0,theoreticalCoverage,viewportDetected:!!mapViewport(),panelDetected:!!findPanel(), mapObjectFound:!!state.olMap,source:state.view?.source, precision:i?.precision,viewWebMercator:state.view?.bbox,extentLambert93:i?.bbox, mapRequestCount:state.latestSamples.length, latestMapRequestAt:state.lastSampleAt ? new Date(state.lastSampleAt).toLocaleTimeString() : null, lastMapRequest:state.latestSamples.at(-1)?.box, lastMapRequestResolution:state.latestSamples.at(-1)?.resolution, waitingForZoomTiles:state.waitingForZoomTiles,zoomSince:state.zoomSince||null, sampleCount:state.view?.sampleCount||null, coverageX:state.view?.coverageX||null,coverageY:state.view?.coverageY||null, origin:state.origin||localStorage.getItem(ORIGIN_KEY)||i?.host, currentLegendBbox:state.lastSuccessfulBbox, currentLegendSheets:state.responseSheets, pendingKey:state.pendingKey,loading:state.loading,imageLoading:state.imageLoading,imageKey:state.imageKey,error:state.error, status:state.lastResponseStatus,lastKind:state.lastKind}; const el=document.getElementById(ID+'-diag');if(el)el.textContent=JSON.stringify(obj,null,2); return obj; } function update(force=false) { if(!state.active)return; installGestureListeners();const section=ui();if(!section)return; const image=section.querySelector('#'+ID+'-image'),test=section.querySelector('#'+ID+'-test'); const i=boundsLambert(); if(!i){setStatus(state.waitingForZoomTiles ? 'Attente du recalage : les nouvelles tuiles WMS ne couvrent pas encore assez l’écran. Si besoin, déplace légèrement la carte.' : 'Emprise introuvable. Déplace légèrement la carte pour charger les tuiles BRGM.');invalidateImage();return;} if(state.drag||state.moving){setStatus('Déplacement de la carte…');return;} const b=i.bbox; if(b[2]-b[0]>50000||b[3]-b[1]>50000){ setStatus('Zoome davantage (limite du serveur : 50 km par côté).');invalidateImage();return; } const origin=state.origin||localStorage.getItem(ORIGIN_KEY)||i.host; if(!origin){setStatus('Indique l’adresse Cloudflare dans Réglages.');invalidateImage();return;} let u;try{u=new URL('/',origin);}catch(_){setStatus('Adresse Cloudflare incorrecte.');invalidateImage();return;} const BB=b.map(x=>Math.round(x/25)*25).join(','); Object.entries({SERVICE:'WMS',REQUEST:'GetLegendGraphic',LAYER, FORMAT:'image/svg+xml',CRS:'EPSG:2154',BBOX:BB}).forEach(([k,v])=>u.searchParams.set(k,v)); test.href=u.toString();test.style.display='inline-block'; const key=u.toString(); if(key!==state.pendingKey){ state.pendingKey=key;state.pendingSince=Date.now(); if(state.controller)state.controller.abort(); image.onload=null;image.onerror=null;image.style.display='none';image.removeAttribute('src'); state.imageLoading=false;state.imageKey=''; state.lastSuccessfulBbox='';state.responseSheets=[]; } if(!force && Date.now()-Math.max(state.pendingSince,state.lastMove)<1200){ setStatus('Mise à jour de l’emprise…');return; } if(!force && key===state.lastKey)return; // Une fois l'image demandée, ne pas répéter les fetch toutes les 700 ms. if(state.imageLoading && state.imageKey===key)return; if(!force && state.failedKey===key && Date.now()ctrl.abort(),18000),requested=key; const markFailure=e=>{ if(requested!==state.pendingKey || !state.active)return; state.error=String(e?.message||e);state.lastKind='service'; image.onload=null;image.onerror=null; image.style.display='none';image.removeAttribute('src');state.lastKey=''; state.imageLoading=false;state.imageKey=''; state.responseSheets=[];state.lastSuccessfulBbox=''; if(state.failedKey!==requested)state.failureCount=0; state.failedKey=requested;state.failureCount++; const seconds=[5,10,20,30][Math.min(state.failureCount-1,3)]; state.retryAfter=Date.now()+seconds*1000; setStatus('Légende indisponible : '+state.error+'. Nouvel essai dans '+seconds+' s.'); console.warn('[BRGM V8.6]',state.error); }; const showImage=fallback=>{ if(requested!==state.pendingKey || !state.active)return; state.imageLoading=true;state.imageKey=requested; image.onload=()=>{ if(requested!==state.pendingKey || !state.active)return; state.imageLoading=false; state.lastKey=requested;state.lastSuccessfulBbox=BB; state.failedKey='';state.failureCount=0;state.retryAfter=0; state.error='';state.lastKind=fallback?'image-seule':'ok'; image.style.display='block'; setStatus(fallback ? 'Légende affichée en mode image (contenu non contrôlé par JavaScript).' : 'Légende actualisée — '+i.precision+'.'); }; image.onerror=()=>{ if(requested!==state.pendingKey || !state.active)return; markFailure(Error('Impossible de charger le SVG dans la page')); }; image.src=requested; setStatus(fallback ? 'Requête JavaScript bloquée : essai par image directe…' : 'Légende SVG validée : chargement de l’image…'); }; fetch(requested,{method:'GET',mode:'cors',cache:'no-store',signal:ctrl.signal}) .then(async response=>{ state.lastResponseStatus=response.status; if(!response.ok)throw Error('Service géologique HTTP '+response.status); const type=(response.headers.get('content-type')||'').toLowerCase(); if(!type.includes('svg'))throw Error('Le serveur ne renvoie pas une image SVG'); const svg=await response.text(); if(!/]/i.test(svg))throw Error('Réponse SVG invalide'); if(/SIGES indisponible|SIGES\s*:\s*impossible|n.a renvoy[eé] aucune feuille|Aucune formation r[eé]pertori[eé]e|Emprise > 50 km/i.test(svg)) throw Error('SIGES n’a renvoyé aucune légende exploitable'); const sheets=[...svg.matchAll(/Feuille\s+(\d+)\s*[-—:]?\s*([^<\r\n]*)/gi)] .map(x=>x[1]+' '+x[2].replace(/&[^;]+;/g,' ').trim().slice(0,55)); if(!sheets.length)throw Error('Réponse sans numéro de feuille géologique'); return sheets; }) .then(sheets=>{ if(!state.active || requested!==state.pendingKey)return; state.responseSheets=sheets; showImage(false); }) .catch(e=>{ if(requested!==state.pendingKey || !state.active)return; if(ctrl.signal.aborted && state.controller!==ctrl)return; // Le fetch CORS peut être refusé alors qu'un HTTPS est autorisé. // La V8.5 avait supprimé ce secours, présent en V8.3. if(e instanceof TypeError && !ctrl.signal.aborted){ state.responseSheets=[]; showImage(true); return; } markFailure(e); }) .finally(()=>{ clearTimeout(timeout); if(state.controller===ctrl){state.controller=null;state.loading=false;} }); } const interval=setInterval(()=>{ try {update();drawOverlay();}catch(e){state.error=String(e.message||e);setStatus('Erreur script : '+state.error);console.warn('[BRGM V8.8]',e);} },700); window[RUN]={ stop(){state.active=false;clearInterval(interval);abort.abort();observer.disconnect(); state.controller?.abort();removeDebugOverlay();document.getElementById(ID)?.remove();delete window[RUN];}, diagnostic, showEmprises(on=true){state.debugOverlay=!!on;saveOverlayPreference(state.debugOverlay);const btn=document.getElementById(ID+'-debug');if(btn)btn.textContent=state.debugOverlay?'Masquer les emprises':'Afficher les emprises';drawOverlay();}, refresh(){state.lastKey='';state.failedKey='';state.retryAfter=0;update(true);} }; setTimeout(()=>{update();drawOverlay();},400); console.info('[BRGM V8.8] Activé. Diagnostic : window.__brgmLegende88.diagnostic()'); })();