zektyc/public/zmap/web/app.js
riricdev d7ec32a458 chore: checkpoint initial — Zektyc dataset, avant humanisation
- site vitrine + fingerprint test + zmap + z-panel
- remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs)
- suppression du token DuckDNS (plus utilisé)
2026-09-06 13:03:25 +02:00

188 lines
8.6 KiB
JavaScript

(function(){
"use strict";
var API = location.origin;
var map = L.map("map",{zoomControl:false,attributionControl:false}).setView([48.85,2.35],13);
var osm = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",{maxZoom:19,attribution:"OSM"});
var esri = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{maxZoom:19,attribution:"Esri"});
osm.addTo(map);
L.control.attribution({position:"bottomleft",prefix:false}).addTo(map);
var fromPt=null, toPt=null, routeLayer=null, stationLayer=null, gpsWatchId=null, gpsMarker=null;
var selectingFrom=true;
var $=function(id){return document.getElementById(id)};
var $from=$("from"), $to=$("to"), $btnRoute=$("btn-route"), $btnClear=$("btn-clear");
var $btnSat=$("btn-sat"), $btnGps=$("btn-gps"), $panel=$("panel");
var $routeInfo=$("route-info"), $legend=$("speed-legend"), $loading=$("loading");
var $searchIn=$("search-input"), $searchRes=$("search-results");
function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}
$btnSat.onclick=function(){
if(map.hasLayer(esri)){map.removeLayer(esri);osm.addTo(map);$btnSat.className="icon-btn"}
else{map.removeLayer(osm);esri.addTo(map);$btnSat.className="icon-btn active"}
};
$btnGps.onclick=function(){
if(gpsWatchId!=null){stopGps();return}
if(!navigator.geolocation){alert("Géolocalisation non supportée");return}
gpsWatchId=navigator.geolocation.watchPosition(function(pos){
var lat=pos.coords.latitude,lng=pos.coords.longitude;
if(!gpsMarker){
var icon=L.divIcon({className:"",html:"<div class='gps-dot'></div>",iconSize:[14,14],iconAnchor:[7,7]});
gpsMarker=L.marker([lat,lng],{icon:icon,zIndexOffset:1000}).addTo(map);
} else gpsMarker.setLatLng([lat,lng]);
},function(e){alert("Erreur GPS: "+e.message)},{enableHighAccuracy:true,timeout:10000});
$btnGps.className="icon-btn active gps-active";
};
function stopGps(){
if(gpsWatchId!=null){navigator.geolocation.clearWatch(gpsWatchId);gpsWatchId=null}
if(gpsMarker){map.removeLayer(gpsMarker);gpsMarker=null}
$btnGps.className="icon-btn";
}
var searchTimer=null;
$searchIn.oninput=function(){
clearTimeout(searchTimer);
var q=$searchIn.value.trim();
if(q.length<2){$searchRes.className="search-results";return}
searchTimer=setTimeout(function(){doSearch(q)},300);
};
$searchIn.onkeydown=function(e){if(e.key==="Escape"){$searchRes.className="search-results";$searchIn.blur()}};
document.addEventListener("click",function(e){if(!e.target.closest(".search-wrap"))$searchRes.className="search-results"});
function doSearch(q){
fetch(API+"/api/zmap/search?q="+encodeURIComponent(q))
.then(function(r){return r.json()})
.then(function(d){
if(!d.results||!d.results.length){$searchRes.innerHTML='<div class="sr-item" style="color:var(--dim)">Aucun résultat</div>';return}
$searchRes.innerHTML="";
for(var i=0;i<d.results.length;i++){
var r=d.results[i];
var el=document.createElement("div");
el.className="sr-item";
el.innerHTML="<div>"+esc(r.display_name)+"</div><div class='sr-type'>"+esc(r.type)+" · "+esc(r.category)+"</div>";
el.dataset.lat=r.lat;el.dataset.lng=r.lon;
el.onclick=function(){
var lat=parseFloat(this.dataset.lat),lng=parseFloat(this.dataset.lng);
map.setView([lat,lng],16);
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
else{setTo(lat,lng);selectingFrom=true}
$searchRes.className="search-results";$searchIn.value="";
};
$searchRes.appendChild(el);
}
$searchRes.className="search-results open";
}).catch(function(){});
}
map.on("click",function(e){
var lat=+e.latlng.lat.toFixed(5),lng=+e.latlng.lng.toFixed(5);
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
else{setTo(lat,lng);selectingFrom=true}
});
function setFrom(lat,lng){fromPt={lat:lat,lng:lng};$from.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
function setTo(lat,lng){toPt={lat:lat,lng:lng};$to.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
$btnClear.onclick=function(){
fromPt=toPt=null;$from.value="";$to.value="";$btnRoute.disabled=true;selectingFrom=true;
$routeInfo.className="route-info hidden";$legend.innerHTML="";
if(routeLayer){map.removeLayer(routeLayer);routeLayer=null}
if(stationLayer){map.removeLayer(stationLayer);stationLayer=null}
};
$btnRoute.onclick=function(){
if(!fromPt||!toPt)return;
$loading.className="loading show";
fetch(API+"/api/zmap/route?from="+fromPt.lat+","+fromPt.lng+"&to="+toPt.lat+","+toPt.lng)
.then(function(r){return r.json()})
.then(function(d){if(d.error){alert(d.error);return}renderRoute(d)})
.catch(function(e){alert("Erreur: "+e.message)})
.finally(function(){$loading.className="loading"});
};
function speedColor(k){
if(k==null)return"#555";
if(k<=20)return"#ef4444";if(k<=40)return"#f59e0b";if(k<=60)return"#eab308";
if(k<=80)return"#22c55e";if(k<=100)return"#06b6d4";if(k<=120)return"#3b82f6";return"#8b5cf6";
}
function speedCls(k){
if(k==null)return"sl-unk";
if(k<=20)return"sl-20";if(k<=40)return"sl-40";if(k<=60)return"sl-60";
if(k<=80)return"sl-80";if(k<=100)return"sl-100";if(k<=120)return"sl-120";return"sl-max";
}
function renderRoute(d){
if(routeLayer)map.removeLayer(routeLayer);
var coords=d.geometry&&d.geometry.coordinates||[];
var speeds=d.speeds||[];
var segs=[];
for(var i=0;i<coords.length-1;i++){
var kmh=speeds[i]!=null?speeds[i]:null;
var line=L.polyline([[coords[i][1],coords[i][0]],[coords[i+1][1],coords[i+1][0]]],
{color:speedColor(kmh),weight:5,opacity:.9,lineCap:"round",lineJoin:"round"});
if(kmh!=null)line.bindPopup("<b>"+kmh+" km/h</b>");
segs.push(line);
}
routeLayer=L.layerGroup(segs).addTo(map);
map.fitBounds(L.polyline(coords.map(function(c){return[c[1],c[0]]})).getBounds().pad(.1));
var covered=speeds.filter(function(s){return s!=null}).length;
var total=speeds.length;
var pct=total?Math.round(covered/total*100):0;
$routeInfo.className="route-info";
var dist={};
for(var i=0;i<speeds.length;i++){
var k=speeds[i]==null?"?":speeds[i]<=20?"0-20":speeds[i]<=40?"21-40":speeds[i]<=60?"41-60":speeds[i]<=80?"61-80":speeds[i]<=100?"81-100":speeds[i]<=120?"101-120":">120";
dist[k]=(dist[k]||0)+1;
}
var colors={"?":"#555","0-20":"#ef4444","21-40":"#f59e0b","41-60":"#eab308","61-80":"#22c55e","81-100":"#06b6d4","101-120":"#3b82f6",">120":"#8b5cf6"};
var order=["?","0-20","21-40","41-60","61-80","81-100","101-120",">120"];
var barHtml="<div class='speed-bar'>";
var distribHtml="<div class='speed-distrib'>";
for(var j=0;j<order.length;j++){
var cnt=dist[order[j]]||0;
if(cnt===0)continue;
var bpct=Math.round(cnt/speeds.length*100);
barHtml+="<div style='width:"+bpct+"%;background:"+colors[order[j]]+"'></div>";
distribHtml+="<span><span class='sd-dot' style='background:"+colors[order[j]]+"'></span>"+order[j]+" km/h: "+cnt+"</span>";
}
barHtml+="</div>";distribHtml+="</div>";
$routeInfo.innerHTML='<div class="ri-main">'+d.distance_km+" km</div>"+
'<div class="ri-sub">'+d.duration_min+" min · "+covered+"/"+total+" segments · <b>"+pct+"%</b> couverts</div>"+barHtml+distribHtml;
var leg=[["≤20","sl-20"],["21-40","sl-40"],["41-60","sl-60"],
["61-80","sl-80"],["81-100","sl-100"],["101-120","sl-120"],[">120","sl-max"],["?","sl-unk"]];
$legend.innerHTML=leg.map(function(l){return'<span class="'+l[1]+'">'+l[0]+" km/h</span>"}).join("");
loadStations(coords);
}
function loadStations(coords){
if(stationLayer)map.removeLayer(stationLayer);
if(!coords.length)return;
var mnLa=90,mxLa=-90,mnLo=180,mxLo=-180;
for(var i=0;i<coords.length;i++){
if(coords[i][1]<mnLa)mnLa=coords[i][1];if(coords[i][1]>mxLa)mxLa=coords[i][1];
if(coords[i][0]<mnLo)mnLo=coords[i][0];if(coords[i][0]>mxLo)mxLo=coords[i][0];
}
var p=.02;
fetch(API+"/api/zmap/stations?south="+(mnLa-p)+"&west="+(mnLo-p)+"&north="+(mxLa+p)+"&east="+(mxLo+p))
.then(function(r){return r.json()})
.then(function(d){
if(!d.stations||!d.stations.length)return;
stationLayer=L.layerGroup().addTo(map);
for(var i=0;i<d.stations.length;i++){
var s=d.stations[i];
var icon=L.divIcon({className:"",html:"<div class='station-dot'></div>",iconSize:[10,10],iconAnchor:[5,5]});
var pw=0;
if(s.connectors)for(var j=0;j<s.connectors.length;j++){var v=s.connectors[j].powerKw||0;if(v>pw)pw=v}
L.marker([s.lat,s.lon],{icon:icon}).bindPopup(
"<b>"+esc((s.name||"").slice(0,40)||"Borne")+"</b><br>"+
(pw?pw+" kW":"")+" "+esc(s.network||"")+"<br>"+esc(s.status||"")
).addTo(stationLayer);
}
}).catch(function(){});
}
})();