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:
riricdev 2026-09-06 13:03:25 +02:00
commit 8e7ec41f18
104 changed files with 19256 additions and 0 deletions

188
public/zmap/web/app.js Normal file
View 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(){});
}
})();

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>ZMap — Web</title>
<link rel="stylesheet" href="leaflet/leaflet.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="map"></div>
<div class="topbar">
<div class="search-wrap">
<svg class="icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
<input id="search-input" type="text" placeholder="Rechercher un lieu..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
<button id="btn-gps" class="icon-btn" title="Ma position">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>
</button>
<button id="btn-sat" class="icon-btn" title="Vue satellite">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>
</button>
</div>
<div id="panel" class="panel open">
<div class="panel-head">
<h2>Itinéraire</h2>
<button class="panel-close" id="panel-close">&times;</button>
</div>
<div class="panel-body">
<div class="point-row">
<div class="point-dot from"></div>
<input id="from" placeholder="Départ (clic ou recherche)" readonly>
</div>
<div class="point-row">
<div class="point-dot to"></div>
<input id="to" placeholder="Arrivée (clic ou recherche)" readonly>
</div>
<div class="actions">
<button class="btn" id="btn-route" disabled>Calculer</button>
<button class="btn btn-outline btn-danger" id="btn-clear">Effacer</button>
</div>
<div id="route-info" class="route-info hidden"></div>
<div id="speed-legend" class="speed-legend"></div>
<p class="hint">Cliquez sur la carte ou utilisez la barre de recherche</p>
</div>
</div>
<div id="loading"><div class="spinner"></div> Calcul de l'itinéraire...</div>
<script src="leaflet/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

99
public/zmap/web/style.css Normal file
View file

@ -0,0 +1,99 @@
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0c0c0f;--surface:#16161a;--surface2:#1e1e24;--border:#2a2a32;--text:#e8e8ed;--dim:#8888a0;--accent:#3b82f6;--accent2:#2563eb;--green:#22c55e;--red:#ef4444;--orange:#f59e0b;--radius:12px;--radius-sm:8px}
html,body{height:100%;overflow:hidden}
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
#map{position:fixed;top:0;left:0;right:0;bottom:0;z-index:0}
/* --- Top bar --- */
.topbar{position:fixed;top:12px;left:12px;right:12px;z-index:1000;display:flex;gap:8px;align-items:center}
.search-wrap{flex:1;position:relative}
.search-wrap input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px 12px 40px;color:var(--text);font-size:14px;outline:none;transition:border-color .2s}
.search-wrap input:focus{border-color:var(--accent)}
.search-wrap input::placeholder{color:var(--dim)}
.search-wrap .icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--dim);pointer-events:none}
.search-results{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;display:none;max-height:320px;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.5);z-index:2000}
.search-results.open{display:block}
.search-results .sr-item{padding:10px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);transition:background .15s}
.search-results .sr-item:last-child{border-bottom:none}
.search-results .sr-item:hover{background:var(--surface2)}
.search-results .sr-item .sr-type{font-size:11px;color:var(--dim)}
.icon-btn{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);width:44px;height:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text);transition:all .2s;flex-shrink:0}
.icon-btn:hover{background:var(--surface2);border-color:var(--accent)}
.icon-btn.active{background:var(--accent);border-color:var(--accent);color:#fff}
/* --- Side panel --- */
.panel{position:fixed;top:68px;left:12px;z-index:1000;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:0;width:340px;max-height:calc(100vh - 92px);overflow:hidden;display:none;box-shadow:0 8px 32px rgba(0,0,0,.4)}
.panel.open{display:block}
.panel-head{padding:16px 16px 0;display:flex;align-items:center;justify-content:space-between}
.panel-head h2{font-size:16px;font-weight:700}
.panel-close{background:none;border:none;color:var(--dim);cursor:pointer;padding:4px;font-size:20px}
.panel-close:hover{color:var(--text)}
.panel-body{padding:12px 16px 16px}
.point-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.point-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.point-dot.from{background:var(--green)}
.point-dot.to{background:var(--red)}
.point-row input{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 10px;color:var(--text);font-size:13px;outline:none}
.point-row input:focus{border-color:var(--accent)}
.actions{display:flex;gap:6px;margin-top:4px}
.btn{background:var(--accent);color:#fff;border:none;border-radius:var(--radius-sm);padding:10px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
.btn:hover{background:var(--accent2)}
.btn:disabled{opacity:.35;cursor:default}
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-outline:hover{background:var(--surface2);border-color:var(--accent)}
.btn-danger{background:var(--red)}
.btn-danger:hover{background:#dc2626}
/* Route info */
.route-info{background:var(--bg);border-radius:var(--radius-sm);padding:12px;margin-top:10px;font-size:13px;line-height:1.7}
.route-info .ri-main{font-size:18px;font-weight:700;margin-bottom:2px}
.route-info .ri-sub{color:var(--dim);font-size:12px}
.route-info .ri-stat{display:inline-block;margin-right:12px}
.route-info .ri-stat b{color:var(--accent)}
/* Speed legend */
.speed-legend{display:flex;gap:4px;margin-top:10px;flex-wrap:wrap}
.speed-legend span{padding:4px 8px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.3px}
.sl-20{background:#ef4444;color:#fff}.sl-40{background:#f59e0b;color:#000}.sl-60{background:#eab308;color:#000}
.sl-80{background:#22c55e;color:#000}.sl-100{background:#06b6d4;color:#000}.sl-120{background:#3b82f6;color:#fff}
.sl-max{background:#8b5cf6;color:#fff}.sl-unk{background:#555;color:#aaa}
.speed-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;margin-top:6px}
.speed-bar div{height:100%;transition:width .3s}
.speed-distrib{display:flex;gap:8px;margin-top:4px;flex-wrap:wrap;font-size:11px;color:var(--dim)}
.speed-distrib span{display:flex;align-items:center;gap:3px}
.speed-distrib .sd-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
.hint{margin-top:12px;font-size:11px;color:var(--dim);text-align:center}
/* --- GPS button --- */
.gps-active .gps-dot{animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(59,130,246,.5)}50%{box-shadow:0 0 0 8px rgba(59,130,246,0)}}
/* --- Loading --- */
/* --- Leaflet overrides --- */
.leaflet-control-zoom{border:none!important}
.leaflet-control-zoom a{background:var(--surface)!important;color:var(--text)!important;border:1px solid var(--border)!important;border-radius:var(--radius-sm)!important;width:36px!important;height:36px!important;line-height:36px!important;font-size:18px!important}
.leaflet-control-zoom a:hover{background:var(--surface2)!important}
.leaflet-control-zoom{gap:0;margin-right:54px!important}
/* --- Marker popups --- */
.leaflet-popup-content-wrapper{background:var(--surface)!important;color:var(--text)!important;border-radius:var(--radius-sm)!important;border:1px solid var(--border)!important;box-shadow:0 4px 16px rgba(0,0,0,.4)!important}
.leaflet-popup-tip{background:var(--surface)!important;border:1px solid var(--border)!important}
.leaflet-popup-content{font-size:13px;margin:8px 12px!important}
.gps-dot{width:14px;height:14px;background:var(--accent);border:3px solid #fff;border-radius:50%;box-shadow:0 0 8px rgba(59,130,246,.6)}
.station-dot{width:10px;height:10px;background:var(--green);border:2px solid #fff;border-radius:50%}
.loading{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2000;background:var(--surface);padding:14px 22px;border-radius:var(--radius);font-size:13px;border:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,.5);align-items:center;gap:8px}
.loading.show{display:flex}
.spinner{width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* --- Responsive --- */
@media(max-width:600px){
.topbar{top:8px;left:8px;right:8px}
.panel{left:8px;right:8px;width:auto;top:60px;max-height:55vh}
.map-ctrls.top{top:8px}
.map-ctrls.bottom{bottom:8px}
}