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é)
This commit is contained in:
commit
8e7ec41f18
104 changed files with 19256 additions and 0 deletions
188
public/zmap/web/app.js
Normal file
188
public/zmap/web/app.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
(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(){});
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in a new issue