This commit is contained in:
pporcheret
2026-10-30 22:10:03 +01:00
parent 7f26f9b015
commit 90ffa635a0
161 changed files with 19042 additions and 111 deletions
@@ -0,0 +1,121 @@
/* DONA_NAV_V83 — navigation interactive et commandes vocales, sans clé API. */
(()=>{
'use strict';
if(window.DonaNavigationV83)return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!==undefined)n.textContent=String(text);return n;};
const button=(label,fn,cls='dn-btn')=>{const b=el('button',cls,label);b.type='button';b.addEventListener('click',fn);return b;};
const safeUrl=(raw,host)=>{try{const u=new URL(String(raw));return u.protocol==='https:'&&(u.hostname===host||u.hostname==='www.'+host)?u.href:'';}catch(_){return '';}};
let active=false,loading=false,selected=null,map=null,leaflet=null,readyPromise=null,headButton=null;
const status=msg=>{if($('dnStatus'))$('dnStatus').textContent=String(msg||'');};
function initialize(){
const root=$('alfredMainV1');if(!root){setTimeout(initialize,200);return;}
const head=root.querySelector('.av-head-actions');
if(head){headButton=button('⌖',open,'av-round dn-open-button');headButton.title='Navigation';headButton.setAttribute('aria-label','Ouvrir la navigation');head.prepend(headButton);}
const overlay=el('div','dn-overlay');overlay.id='donaNavigationV83';overlay.hidden=true;
const panel=el('section','dn-panel');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Navigation Dona');
const header=el('header','dn-header');const tit=el('div');tit.append(el('span','dn-eyebrow','DONA · NAVIGATION'),el('h2','','Carte et itinéraires'));
header.append(tit,button('×',close,'dn-close'));panel.append(header);
const form=el('form','dn-form');form.id='dnSearchForm';
const dst=el('input','dn-input');dst.id='dnDestination';dst.name='destination';dst.placeholder='Destination : Metz, Luxembourg…';dst.required=true;dst.maxLength=180;dst.autocomplete='off';dst.setAttribute('aria-label','Destination');
const org=el('input','dn-input');org.id='dnOrigin';org.name='origin';org.placeholder='Départ (facultatif)';org.maxLength=180;org.autocomplete='off';org.setAttribute('aria-label','Point de départ');
const submit=button('Afficher',()=>{},'dn-btn dn-primary');submit.type='submit';submit.id='dnSubmit';form.append(dst,org,submit);form.addEventListener('submit',event=>{event.preventDefault();search(dst.value,org.value);});panel.append(form);
const message=el('p','dn-status');message.id='dnStatus';message.setAttribute('role','status');message.setAttribute('aria-live','polite');panel.append(message);
const layout=el('div','dn-layout');const main=el('div','dn-map-area');const mapbox=el('div','dn-map');mapbox.id='dnMap';main.append(mapbox);
main.append(el('p','dn-credit','© OpenStreetMap contributors · Temps de trajet indicatifs, hors trafic temps réel'));
const aside=el('aside','dn-aside');aside.append(el('h3','','Destination'));
const address=el('p','dn-destination-title','Choisis une destination.');address.id='dnPlace';aside.append(address);
const details=el('div','dn-route-details');details.id='dnDetails';aside.append(details);
const actions=el('div','dn-actions');
const wa=el('a','dn-btn dn-waze','Ouvrir Waze');wa.target='_blank';wa.rel='noopener noreferrer';wa.id='dnWaze';wa.hidden=true;
const gm=el('a','dn-btn','Google Maps');gm.target='_blank';gm.rel='noopener noreferrer';gm.id='dnMaps';gm.hidden=true;
const phone=button('Envoyer à mon téléphone',sendPhone,'dn-btn dn-primary');phone.id='dnSend';phone.disabled=true;
actions.append(wa,gm,phone);aside.append(actions);
aside.append(el('p','dn-help','Dona ne connaît pas ta position. Le départ est facultatif ; pour obtenir un temps de trajet, renseigne-le. Pour l’envoi au téléphone, configure d’abord Home Assistant.'));
layout.append(main,aside);panel.append(layout);overlay.append(panel);document.body.append(overlay);
overlay.addEventListener('click',event=>{if(event.target===overlay)close();});
window.addEventListener('keydown',event=>{if(event.key==='Escape'&&active)close();});
fetch('/api/dona/navigation/config',{credentials:'same-origin'}).then(r=>r.ok?r.json():null).then(data=>{if(data&&data.origin&&!org.value)org.value=data.origin;}).catch(()=>{});
connectSocket();
}
function open(){if(!$( 'donaNavigationV83'))return;active=true;$('donaNavigationV83').hidden=false;document.body.classList.add('dn-active');if(map)setTimeout(()=>map.invalidateSize(),80);}
function close(){active=false;$('donaNavigationV83').hidden=true;document.body.classList.remove('dn-active');}
function loadLeaflet(){
if(window.L)return Promise.resolve(window.L);
if(readyPromise)return readyPromise;
readyPromise=new Promise((resolve,reject)=>{
const style=document.createElement('link');style.rel='stylesheet';style.href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';document.head.append(style);
const script=document.createElement('script');script.src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';script.async=true;
script.onload=()=>window.L?resolve(window.L):reject(new Error('Bibliothèque de cartes indisponible.'));
script.onerror=()=>reject(new Error('Carte interactive indisponible.'));
document.head.append(script);
setTimeout(()=>{if(!window.L)reject(new Error('Carte interactive non chargée.'));},12000);
});return readyPromise;
}
function drawFallback(place){
const box=$('dnMap');if(map){try{map.remove();}catch(_){}map=null;}
box.replaceChildren();
if(!place){box.append(el('p','dn-map-fallback','Destination à rechercher.'));return;}
const lat=Number(place.lat),lon=Number(place.lon);if(!Number.isFinite(lat)||!Number.isFinite(lon))return;
const iframe=el('iframe','dn-osm-frame');iframe.title='Carte OpenStreetMap de la destination';iframe.loading='lazy';iframe.referrerPolicy='strict-origin-when-cross-origin';
const delta=.035;
iframe.src='https://www.openstreetmap.org/export/embed.html?bbox='+encodeURIComponent([lon-delta,lat-delta,lon+delta,lat+delta].join(','))+'&layer=mapnik&marker='+encodeURIComponent(lat+','+lon);
box.append(iframe);
}
async function drawMap(data){
const place=data.place;const route=data.route;
if(!place){drawFallback(null);return;}
try{
const L=await loadLeaflet();if(!active)return;
if(map){map.remove();map=null;}
$('dnMap').replaceChildren();map=L.map('dnMap',{scrollWheelZoom:false});
const tileUrl='https://tile.openstreetmap.org/{z}/{x}/{y}.png';
L.tileLayer(tileUrl,{attribution:'© OpenStreetMap contributors',maxZoom:19}).addTo(map);
const loc=L.latLng(Number(place.lat),Number(place.lon));L.marker(loc).addTo(map).bindPopup(String(data.destination||'Destination').slice(0,120));
if(route&&Array.isArray(route.line)&&route.line.length>1){
const pts=route.line.filter(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite));
if(pts.length>1){const line=L.polyline(pts,{color:'#64d8ff',weight:5,opacity:.85}).addTo(map);map.fitBounds(line.getBounds(),{padding:[28,28]});}
else map.setView(loc,12);
if(data.origin_place){const start=L.latLng(Number(data.origin_place.lat),Number(data.origin_place.lon));L.circleMarker(start,{radius:8,color:'#93ffc9',fillColor:'#93ffc9',fillOpacity:.9}).addTo(map).bindPopup('Départ');}
}else map.setView(loc,12);
setTimeout(()=>map?.invalidateSize(),100);
}catch(_){drawFallback(place);status((data.note?data.note+' ':'')+'Carte simplifiée affichée : le module interactif n’a pas pu être chargé.');}
}
function show(data){
if(!data||data.type!=='dona_navigation')return;
selected=data.destination||null;open();
$('dnDestination').value=String(data.destination||'').slice(0,180);
if(data.origin)$('dnOrigin').value=String(data.origin).slice(0,180);
$('dnPlace').textContent=data.place?.name||String(data.destination||'Destination indisponible');
const info=$('dnDetails');info.replaceChildren();
if(data.route){info.append(el('strong','',data.route.distance_km+' km · '+data.route.duration_min+' min'),
el('p','','Durée estimée, sans trafic temps réel.'));}
else info.append(el('p','','Itinéraire non calculé — ouvre Waze pour le guidage.'));
const waze=safeUrl(data.links?.waze,'waze.com'),maps=safeUrl(data.links?.google_maps,'google.com');
for(const [id,url] of [['dnWaze',waze],['dnMaps',maps]]){const a=$(id);a.hidden=!url;if(url)a.href=url;else a.removeAttribute('href');}
$('dnSend').disabled=!selected;
status(data.error||data.send_error||(data.sent?'Itinéraire envoyé sur ton téléphone.':data.note||'Destination trouvée.'));
drawMap(data);
}
async function search(destination,origin=''){
if(loading)return;destination=String(destination||'').trim();origin=String(origin||'').trim();
if(destination.length<2||destination.length>180){status('Indique une destination valide.');return;}
loading=true;$('dnSubmit').disabled=true;open();status('Recherche de la destination et de l’itinéraire…');
try{
const res=await fetch('/api/dona/navigation/search',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({destination,origin})});
const data=await res.json();if(!res.ok||!data.ok)throw new Error(data.error||'Recherche indisponible.');
show({...data,type:'dona_navigation'});
}catch(e){status(e.message||'Erreur de navigation.');}
finally{loading=false;$('dnSubmit').disabled=false;}
}
async function sendPhone(){
if(!selected){status('Choisis d’abord une destination.');return;}
const b=$('dnSend');b.disabled=true;status('Envoi vers Home Assistant…');
try{
const resp=await fetch('/api/dona/navigation/send',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({destination:selected})});
const data=await resp.json();if(!resp.ok||!data.ok)throw new Error(data.error||'Envoi impossible.');status(data.message);
}catch(e){status(e.message||'Impossible d’envoyer le trajet.');}finally{b.disabled=false;}
}
function connectSocket(){const sk=window.alfredSocket;if(!sk){setTimeout(connectSocket,250);return;}sk.on('alfred_render',p=>{if(p?.type==='dona_navigation')show(p);});}
window.DonaNavigationV83={open,close,show,search,sendPhone};initialize();
})();