v3
This commit is contained in:
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user