您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Détecttion des virages dangereux
当前为
// ==UserScript== // @name WME Curve Safety // @namespace csa.ultralite.locked.uifold // @version 2025.01.00 // @description Détecttion des virages dangereux // @author CSA // @match https://www.waze.com/*/editor* // @match https://www.waze.com/editor* // @match https://beta.waze.com/*/editor* // @match https://beta.waze.com/editor* // @grant none // @license MIT // @run-at document-end // ==/UserScript== (function () { 'use strict'; /* ===== Utils ===== */ const g = 9.81; const fmt = (x, n = 0) => (x == null || Number.isNaN(x) ? '—' : x.toFixed(n).replace('.', ',')); const toNum = (s) => (typeof s === 'number' ? s : Number(String(s ?? '').replace(',', '.'))); const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); const lsGet = (k, d) => { try { return localStorage.getItem(k) ?? d; } catch { return d; } }; const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch {} }; const until = (test, every=200, tries=80) => new Promise((res, rej)=>{ let n=0, t=setInterval(()=>{ try{ if(test()){ clearInterval(t); res(); } else if(++n>=tries){ clearInterval(t); rej(new Error('WME non prêt')); } }catch(e){ clearInterval(t); rej(e); } }, every); }); /* ===== Tooltip (simple) ===== */ const TOOLTIP_CSS = ` .csa-tip{ position:fixed; max-width:320px; background:#111; color:#f5f5f5; padding:8px 10px; border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.35); z-index:999999; font:12px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; opacity:0; transform:translateY(-4px); transition:opacity .12s ease, transform .12s ease; pointer-events:none; } .csa-tip.show{ opacity:1; transform:translateY(0); pointer-events:auto; } .csa-tip .csa-tip-title{ font-weight:700; margin-bottom:4px; color:#eaeaea; } .csa-tip .csa-tip-muted{ color:#cbd5e1; } .csa-row-label{ display:flex; align-items:center; gap:6px; min-width:150px; color:#111; cursor:help; } `; function injectCSS(css){ const s=document.createElement('style'); s.textContent=css; document.head.appendChild(s); } injectCSS(TOOLTIP_CSS); let tipEl=null; let tipHideTO=null; function ensureTipEl(){ if(tipEl) return tipEl; tipEl=document.createElement('div'); tipEl.className='csa-tip'; document.body.appendChild(tipEl); return tipEl; } function showTipNear(target, html){ const tip=ensureTipEl(); tip.innerHTML=html; const r=target.getBoundingClientRect(); const pad=8; const vw=innerWidth, vh=innerHeight; tip.style.left=Math.min(Math.max(8,r.left+6),vw-8-320)+'px'; tip.style.top=Math.min(Math.max(8,r.bottom+pad),vh-8-200)+'px'; tip.classList.add('show'); clearTimeout(tipHideTO); tipHideTO=setTimeout(()=>{},9e6); } function hideTipSoon(d=120){ clearTimeout(tipHideTO); tipHideTO=setTimeout(()=> tipEl && tipEl.classList.remove('show'), d); } function bindTip(node, html){ node.addEventListener('mouseenter', ()=>showTipNear(node, typeof html==='function'? html(): html)); node.addEventListener('mouseleave', ()=>hideTipSoon(150)); node.addEventListener('click', (e)=>{ e.stopPropagation(); showTipNear(node, typeof html==='function'? html(): html); }); } /* ===== Drag helper ===== */ function enableDrag(panel, handle, storageKey='csa.panelPos'){ let down=false, ox=0, oy=0; const readPos = () => { try { return JSON.parse(localStorage.getItem(storageKey)||'{}'); } catch { return {}; } }; const savePos = (x,y) => { try { localStorage.setItem(storageKey, JSON.stringify({x, y})); } catch {} }; (function restore(){ const p = readPos(); if (Number.isFinite(p.x) && Number.isFinite(p.y)){ panel.style.left=p.x+'px'; panel.style.top=p.y+'px'; panel.style.right='unset'; panel.style.bottom='unset'; } })(); const start=(cx,cy)=>{ const r=panel.getBoundingClientRect(); ox=cx-r.left; oy=cy-r.top; down=true; document.body.style.userSelect='none'; }; const move=(cx,cy)=>{ if(!down) return; const x=Math.max(4,cx-ox), y=Math.max(4,cy-oy); panel.style.left=x+'px'; panel.style.top=y+'px'; panel.style.right='unset'; panel.style.bottom='unset'; }; const end=()=>{ if(!down) return; down=false; document.body.style.userSelect=''; const r=panel.getBoundingClientRect(); savePos(r.left,r.top); }; handle.addEventListener('mousedown',e=>{ e.preventDefault(); start(e.clientX,e.clientY); }); addEventListener('mousemove',e=>move(e.clientX,e.clientY)); addEventListener('mouseup',end); handle.addEventListener('touchstart',e=>{ const t=e.touches[0]; if(!t) return; start(t.clientX,t.clientY); },{passive:false}); addEventListener('touchmove',e=>{ const t=e.touches[0]; if(!t) return; move(t.clientX,t.clientY); },{passive:false}); addEventListener('touchend',end); } /* ===== Profils ===== */ const PROFILES = { urbain: { label: 'Urbain (strict)', f: 0.14, iDeg: 2, step: 1.5, halfWin: 20, marginKmh: 12, vTargetKmh: 50, percentile: 30 }, campagne: { label: 'Campagne (équilibré)', f: 0.16, iDeg: 2, step: 2.0, halfWin: 30, marginKmh: 18, vTargetKmh: 50, percentile: 40 }, montagne: { label: 'Montagne (épingles)', f: 0.13, iDeg: 2, step: 1.5, halfWin: 18, marginKmh: 10, vTargetKmh: 40, percentile: 25 } }; const defaultProfileKey = lsGet('csa.profile', 'campagne'); /* ===== Etat UI ===== */ let ui={}; let scheduled=null; let vectorLayer=null; let headerBadge=null; let collapseBtn=null; /* ===== Boot ===== */ until(()=>window.W && W.map && W.model && W.selectionManager && window.OpenLayers).then(init).catch(()=>console.warn('[CSA] init timeout')); /* ===== Sélection ===== */ function getSelSegs(){ const sm=W?.selectionManager; const segById=id=>W?.model?.segments?.getObjectById?.(id); const resolve=(x)=>{ if(!x) return null; if(x.type==='segment'||x.CLASS_NAME==='Waze.Model.Segment') return x; const m=x.model||x.dataModel||x._dataModel||x.attributes?.model; if(m && (m.type==='segment'||m.CLASS_NAME==='Waze.Model.Segment')) return m; const id=x.id||x.attributes?.id||m?.attributes?.id; if(id!=null){ const s=segById(id); if(s) return s; } if(x.geometry && x.attributes?.id!=null){ const s=segById(x.attributes.id); if(s) return s; } return null; }; try{ const raw=sm.getSelectedDataModelObjects?.()||[]; const list=Array.isArray(raw)?raw:[raw]; const segs=list.map(resolve).filter(Boolean); if(segs.length) return segs; }catch{} try{ const feats=sm.getSelectedWMEFeatures?.()||sm.getSelectedFeatures?.()||[]; const segs=feats.map(resolve).filter(Boolean); if(segs.length) return segs; }catch{} return []; } function hookSelection(){ const sm=W?.selectionManager; try{ sm.events.register('selectionchanged',null,()=>{ if(ui.live?.checked) scheduleRun(40); else clearOverlay(); }); }catch{} try{ sm.events.register('selected',null,()=>{ if(ui.live?.checked) scheduleRun(40); else clearOverlay(); }); }catch{} let last=''; setInterval(()=>{ const sel=getSelSegs(); const now=sel.map(s=>s?.attributes?.id).join(','); if(now!==last){ last=now; if(ui.live?.checked) scheduleRun(60); } }, 300); document.addEventListener('keydown',(e)=>{ if(e.key==='Escape') clearOverlay(); }); } /* ===== UI ===== */ function el(tag, attrs={}){ const d=document.createElement(tag); for(const [k,v] of Object.entries(attrs)){ if(k==='text') d.textContent=v; else if(k==='style') d.setAttribute('style',v); else d[k]=v; } return d; } function setCollapsed(coll){ lsSet('csa.collapsed', coll?'1':'0'); if(!ui.panel) return; ui.panel.classList.toggle('csa-collapsed', !!coll); collapseBtn.textContent = coll ? '+' : '–'; if (coll){ const t=document.querySelector('.csa-tip'); t && t.classList.remove('show'); } } function buildPanel(){ const panel = el('div', { id:'csa-panel', style: ` position:fixed; right:16px; top:16px; width:360px; max-width:42vw; background:#fff; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.18); padding:12px; font:13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; z-index:9999; `}); // CSS pour repli total const extraCSS = ` #csa-panel.csa-collapsed{ padding:8px 10px; width:auto; } #csa-panel.csa-collapsed .csa-content{ display:none; } #csa-panel .csa-head-btn{ border:0; background:#eef2f6; color:#333; width:28px; height:28px; border-radius:6px; cursor:pointer; font-weight:700; } #csa-panel .csa-group-right{ display:flex; align-items:center; gap:6px; } `; injectCSS(extraCSS); const head = el('div', {style:'display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; cursor:move;'}); const left = el('div', {style:'display:flex; align-items:center; gap:8px;'}); const title = el('div', {text:'WME CSA', style:'font-weight:700;'}); headerBadge = el('span', {text:'—', style:'font-weight:700; padding:2px 6px; border-radius:6px; background:#eee; color:#333;'}); left.appendChild(title); left.appendChild(headerBadge); // Droite: ? ⚙︎ ± const right = el('div', {className:'csa-group-right'}); const helpBtn = el('button', {text:'?', title:'Aide', className:'csa-head-btn'}); const gearBtn = el('button', {text:'⚙︎', title:'Afficher/Masquer les réglages', className:'csa-head-btn'}); collapseBtn = el('button', {text:'–', title:'Replier/Déplier', className:'csa-head-btn'}); right.appendChild(helpBtn); right.appendChild(gearBtn); right.appendChild(collapseBtn); head.appendChild(left); head.appendChild(right); panel.appendChild(head); const content = el('div', {className:'csa-content'}); const row = (labelTxt, inputNode, tipHTML) => { const r = el('div', {style:'display:flex; align-items:center; margin:6px 0; gap:8px;'}); const lblWrap = el('div', {className:'csa-row-label'}); const l = el('div', {}); l.textContent = labelTxt; lblWrap.appendChild(l); if (tipHTML) bindTip(lblWrap, tipHTML); r.appendChild(lblWrap); r.appendChild(inputNode); content.appendChild(r); return r; }; // Profil const selProfile = el('select', {id:'csa-profile', style:'width:200px;'}); for (const [k,p] of Object.entries(PROFILES)) selProfile.appendChild(el('option', {value:k, text:p.label})); selProfile.value = defaultProfileKey; row('Profil', selProfile, () => ` <div class="csa-tip-title">Profil</div> <div class="csa-tip-muted">Préréglages (f, i°, pas, fenêtre, marge). Changer de profil met à jour tous les paramètres.</div> `); // Réglages (repliés par défaut) const settingsWrap = el('div', {id:'csa-settings', style:'display:none; border-top:1px dashed #e2e8f0; margin-top:6px; padding-top:8px;'}); const mkInput = (id,w=90,title='')=>el('input',{id, type:'text', title, style:`width:${w}px; box-sizing:border-box; padding:4px 6px; border:1px solid #ccc; border-radius:8px;`}); const f=mkInput('csa-f',90,'f (adhérence)'), i=mkInput('csa-i',90,'i (dévers°)'), step=mkInput('csa-step',90,'Pas (m)'), hw=mkInput('csa-halfwin',90,'½ Fenêtre (m)'); const margin=mkInput('csa-margin',90,'Marge (km/h)'), vt=mkInput('csa-vtarget',90,'V cible (km/h)'), perc=mkInput('csa-percentile',90,'Percentile (R) %'); settingsWrap.appendChild(row('f (adhérence)', f, `<div class="csa-tip-title">f – Adhérence</div><div class="csa-tip-muted">~0.10–0.20. Plus f ↑, plus V sûre ↑.</div>`)); settingsWrap.appendChild(row('i (dévers°)', i, `<div class="csa-tip-title">i – Dévers (°)</div><div class="csa-tip-muted">i>0 (vers l’intérieur) ↑ V sûre, i<0 ↓ V sûre.</div>`)); settingsWrap.appendChild(row('Pas (m)', step, `<div class="csa-tip-title">Pas (m)</div><div class="csa-tip-muted">1–3 m conseillé. Plus petit = plus précis, plus coûteux.</div>`)); settingsWrap.appendChild(row('½ Fenêtre (m)', hw, `<div class="csa-tip-title">½ Fenêtre</div><div class="csa-tip-muted">Plus grand = courbe lissée, apex élargi.</div>`)); settingsWrap.appendChild(row('Marge (km/h)', margin, `<div class="csa-tip-title">Marge (km/h)</div><div class="csa-tip-muted">Tolérance ajoutée à V sûre pour la comparaison.</div>`)); settingsWrap.appendChild(row('V cible (km/h)', vt, `<div class="csa-tip-title">V cible</div><div class="csa-tip-muted">Référence si limite absente.</div>`)); settingsWrap.appendChild(row('Percentile (R) %', perc, `<div class="csa-tip-title">Percentile (R)</div><div class="csa-tip-muted">Rmin robuste = P% ; plus bas = plus sévère.</div>`)); content.appendChild(settingsWrap); // Options const optsRow = el('div', {style:'display:flex; align-items:center; gap:16px; margin:8px 0 6px 0; flex-wrap:wrap;'}); const live = el('input', {id:'csa-live', type:'checkbox'}); live.checked = true; const liveLbl = el('label', {text:'Live (sélection)'}); liveLbl.htmlFor='csa-live'; const draw = el('input', {id:'csa-draw', type:'checkbox'}); draw.checked = true; const drawLbl = el('label', {text:'Surligner dangereux (vue)'}); drawLbl.htmlFor='csa-draw'; bindTip(liveLbl, `<div class="csa-tip-title">Live</div><div class="csa-tip-muted">Relance à chaque changement de sélection.</div>`); bindTip(drawLbl, `<div class="csa-tip-title">Surligner dangereux</div><div class="csa-tip-muted">Overlay sur les tronçons dangereux.</div>`); optsRow.appendChild(live); optsRow.appendChild(liveLbl); optsRow.appendChild(draw); optsRow.appendChild(drawLbl); content.appendChild(optsRow); // Bouton + astuce + sortie const btn = el('button', {id:'csa-run', text:'Analyser la sélection', style:'padding:8px 10px; border:0; border-radius:8px; background:#2a77f4; color:#fff; cursor:pointer; font-weight:600;'}); btn.addEventListener('click', ()=>scheduleRun(20)); content.appendChild(btn); content.appendChild(el('div', {text:'Astuce: pas 1–3 m, ½ fenêtre 20–30 m.', style:'color:#666; margin:6px 0;'})); const out = el('div', {id:'csa-out', style:'margin-top:4px; white-space:pre-line; color:#111;'}); content.appendChild(out); panel.appendChild(content); (document.body||document.documentElement).appendChild(panel); // Help + gear + collapse bindTip(helpBtn, () => ` <div class="csa-tip-title">Méthode & critères</div> <div class="csa-tip-muted">Rmin robuste = percentile des rayons (fenêtre glissante). V sûre = 3.6×√((f+tan(i°))×g×R). Statut: Dangereux si V sûre + Marge < V ref.</div> `); gearBtn.addEventListener('click', ()=>toggleSettings()); collapseBtn.addEventListener('click', ()=> setCollapsed(!ui.panel.classList.contains('csa-collapsed'))); // Drag enableDrag(panel, head, 'csa.panelPos'); ui = { panel, selProfile, settingsWrap, f, i, step, halfWin: hw, margin, vtarget: vt, percentile: perc, live, draw, out, btn }; // Etats init applyProfileToUI(defaultProfileKey); setCollapsed(lsGet('csa.collapsed','0')==='1'); } function toggleSettings(force){ const shown = force!==undefined ? !!force : (ui.settingsWrap.style.display==='none'); ui.settingsWrap.style.display = shown ? 'block' : 'none'; lsSet('csa.showSettings', shown?'1':'0'); } function applyProfileToUI(key){ const p = PROFILES[key]; if(!p) return; ui.f.value=String(p.f).replace('.',','); ui.i.value=p.iDeg; ui.step.value=p.step; ui.halfWin.value=p.halfWin; ui.margin.value=p.marginKmh; ui.vtarget.value=p.vTargetKmh; ui.percentile.value=p.percentile; lsSet('csa.profile', key); } function hookInputs(){ ui.selProfile.addEventListener('change', e=>applyProfileToUI(e.target.value)); [ui.f, ui.i, ui.step, ui.halfWin, ui.margin, ui.vtarget, ui.percentile].forEach(elm => elm.addEventListener('input', ()=>{ if (ui.live.checked) scheduleRun(80); })); } function scheduleRun(delay=120){ if (scheduled) clearTimeout(scheduled); scheduled=setTimeout(run, delay); } /* ===== Géométrie & calcul ===== */ function segToPts(seg){ const geom=seg.getOLGeometry? seg.getOLGeometry(): seg.geometry; if(!geom||!geom.components) return []; return geom.components.map(c=>[c.x,c.y]); } function densifyPath(pts, stepM=2){ if(!pts||pts.length<2) return []; const out=[pts[0]]; let prev=pts[0]; for(let i=1;i<pts.length;i++){ const cur=pts[i], dx=cur[0]-prev[0], dy=cur[1]-prev[1]; const L=Math.hypot(dx,dy); if(L<=0) continue; const n=Math.max(1,Math.floor(L/stepM)); for(let k=1;k<=n;k++){ const t=k/n; out.push([prev[0]+dx*t, prev[1]+dy*t]); } prev=cur; } return out; } function circleRadius(a,b,c){ const ax=a[0],ay=a[1],bx=b[0],by=b[1],cx=c[0],cy=c[1]; const A=bx-ax,B=by-ay,C=cx-ax,D=cy-ay,E=A*(ax+bx)+B*(ay+by),F=C*(ax+cx)+D*(ay+cy),G=2*(A*(cy-by)-B*(cx-bx)); if(Math.abs(G)<1e-9) return Infinity; const ux=(D*E-B*F)/G, uy=(A*F-C*E)/G; return Math.hypot(ax-ux,ay-uy); } function slidingRadii(pts, halfWinMeters, stepM){ const hw=Math.max(2,Math.round(halfWinMeters/(stepM||2))); const out=[]; for(let i=hw;i<pts.length-hw;i++){ out.push(circleRadius(pts[i-hw],pts[i],pts[i+hw])); } return out; } function percentile(arr,p){ if(!arr.length) return Infinity; const k=clamp(Math.floor((p/100)*arr.length),0,arr.length-1); const s=arr.slice().sort((a,b)=>a-b); return s[k]; } function radiusToSpeed(R,f,iDeg){ if(!Number.isFinite(R)||R<=0) return 0; const i=Math.tan((iDeg||0)*Math.PI/180); const a=(f+i)*g; return Math.sqrt(a*R)*3.6; } function speedRef(segs, fallback){ let lim=null; for(const s of segs){ const a=s.attributes||{}; const v=a.fwdMaxSpeed ?? a.revMaxSpeed ?? a.speedLimit ?? a.speed ?? null; if(v && v>0){ lim=v; break; } } return (lim && lim>0)? lim: fallback; } function classify(vSure,vRef,margin){ if(!vRef) return '—'; return (vSure + margin >= vRef) ? 'OK' : 'Dangereux'; } /* ===== Overlay minimal (danger) ===== */ function buildVectorLayer(){ if(!W||!W.map) return; const styleBad=new OpenLayers.Style({ strokeColor:'#d33', strokeWidth:3, strokeOpacity:0.95, strokeDashstyle:'dash' }); vectorLayer=new OpenLayers.Layer.Vector('CSA overlay',{ styleMap:new OpenLayers.StyleMap(styleBad) }); W.map.addLayer(vectorLayer); } function clearOverlay(){ vectorLayer && vectorLayer.removeAllFeatures(); } function drawDangerLine(pts, halfWinMeters, stepM){ if(!vectorLayer || !pts || !pts.length || !ui.draw.checked) return; clearOverlay(); const hw=Math.max(2,Math.round(halfWinMeters/(stepM||2))); let minR=Infinity, idx=-1; for(let i=hw;i<pts.length-hw;i++){ const r=circleRadius(pts[i-hw],pts[i],pts[i+hw]); if(r<minR){ minR=r; idx=i; } } if(idx<0) return; const A=new OpenLayers.Geometry.Point(pts[idx-hw][0],pts[idx-hw][1]); const C=new OpenLayers.Geometry.Point(pts[idx+hw][0],pts[idx+hw][1]); const line=new OpenLayers.Geometry.LineString([A,C]); vectorLayer.addFeatures([new OpenLayers.Feature.Vector(line)]); } /* ===== Run ===== */ function run(){ if(!ui||!ui.out) return; const segs=getSelSegs(); clearOverlay(); if(!segs.length){ ui.out.textContent=''; headerBadge.textContent='—'; headerBadge.style.background='#eee'; headerBadge.style.color='#333'; return; } const p={ f:toNum(ui.f.value), iDeg:toNum(ui.i.value), step:clamp(toNum(ui.step.value)||2,.5,10), halfWin:clamp(toNum(ui.halfWin.value)||20,5,80), marginKmh:clamp(toNum(ui.margin.value)||10,0,50), vTargetKmh:clamp(toNum(ui.vtarget.value)||50,10,130), percentile:clamp(toNum(ui.percentile.value)||40,1,99) }; const all=[]; segs.forEach(s=>all.push(...segToPts(s))); if(all.length<3){ ui.out.textContent='Quasi-ligne droite (pas assez de points)'; headerBadge.textContent='—'; return; } const dens=densifyPath(all,p.step); const radii=slidingRadii(dens,p.halfWin,p.step); const finite=radii.filter(r=>Number.isFinite(r)&&r>0); const Rrob=percentile(finite,p.percentile); const vSure=radiusToSpeed(Rrob,p.f,p.iDeg); const vRef=speedRef(segs,p.vTargetKmh); const statut=classify(vSure,vRef,p.marginKmh); if(statut==='Dangereux') drawDangerLine(dens,p.halfWin,p.step); if(statut==='OK'){ headerBadge.textContent='OK'; headerBadge.style.background='#e6f7ea'; headerBadge.style.color='#0b7a2a'; } else if(statut==='Dangereux'){ headerBadge.textContent='Dangereux'; headerBadge.style.background='#fde8e8'; headerBadge.style.color='#c00'; } else { headerBadge.textContent='—'; headerBadge.style.background='#eee'; headerBadge.style.color='#333'; } ui.out.textContent = `Rmin robuste (P${fmt(toNum(ui.percentile.value),0)}): ${Number.isFinite(Rrob)? fmt(Rrob,0)+' m':'—'}\n`+ `V sûre (f,i) : ${fmt(vSure,1)} km/h\n`+ `V. de référence : ${vRef? fmt(vRef,0):'—'} km/h\n`+ `Statut : ${statut}`; } function init(){ buildPanel(); buildVectorLayer(); hookSelection(); hookInputs(); console.log('[CSA] locked-uifold prêt.'); } })();