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 d7ec32a458
92 changed files with 17476 additions and 0 deletions

View file

@ -0,0 +1,108 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Archives et changelog de ZMap : toutes les versions de l'application Android de cartographie libre OpenStreetMap avec les bornes de recharge électrique." />
<meta name="robots" content="index, follow" />
<title>Archives &amp; changelog · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/archives" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Archives &amp; changelog · ZMap" />
<meta property="og:description" content="Toutes les versions de ZMap avec leur changelog et leurs liens de téléchargement." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/archives" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/">← Retour à la page ZMap</a></p>
<h1>ZMap — Archives &amp; changelog</h1>
<p class="legal-meta">Toutes les versions publiées, avec leurs changelogs et leurs APK.</p>
<div class="dev-banner">
⚠️ Toutes les versions sont des <strong>versions de développement</strong>
publiques, destinées aux tests. Les APK archivées restent téléchargeables,
mais la dernière version est toujours recommandée.
</div>
<section class="projects-page">
<article class="card">
<h3>v0.5.0 <span class="tag-latest">dernière</span> <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">20 août 2026 · 6 011 ko · <a href="/zmap/zmap-0.5.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Vitesses par route précises : base de données serveur des limites de vitesse (maxspeed OSM) de toutes les routes.</li>
<li>Chaque point du trajet est rattaché à sa route réelle (snapping), sa vitesse est fournie avec l'itinéraire.</li>
<li>Le téléphone garde le trajet complet en cache local (hors-ligne pendant la conduite).</li>
</ul>
</article>
<article class="card">
<h3>v0.4.1 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">20 août 2026 · 6 012 ko · <a href="/zmap/zmap-0.4.1.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Correction : le démarrage de navigation ne bloque plus si le serveur de vitesses est lent — le trajet démarre toujours.</li>
<li>Bornes à accès restreint (private, customers, employés…) masquées par défaut et signalées en jaune.</li>
</ul>
</article>
<article class="card">
<h3>v0.4.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 6 012 ko · <a href="/zmap/zmap-0.4.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Nouvelle interface : thème clair/sombre, toasts en haut de l'écran, écran de chargement au démarrage.</li>
<li>Bornes colorées : vert = ouverte, rouge = fermée, jaune = accès privé, avec la puissance kW affichée sous chaque icône.</li>
<li>Réglages vie privée : WebSocket on/off, effacer le cache local, masquer les bornes privées.</li>
<li>Fluidité : fermeture automatique du clavier, animations, barre de recherche masquée pendant la conduite.</li>
<li>Calcul d'itinéraire accéléré : les vitesses maximales sont récupérées en une seule requête au lieu d'une par point.</li>
</ul>
</article>
<article class="card">
<h3>v0.3.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 6 001 ko · <a href="/zmap/zmap-0.3.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Navigation conduite façon GPS : caméra derrière la flèche, étapes « tournez à gauche », heure d'arrivée, vitesse et limite de vitesse.</li>
<li>Choix de la voiture (Tesla Model Y, Zoé, e-208, ID.3, EV6…) avec filtrage des bornes compatibles.</li>
<li>Connecteurs détaillés : Type 2, CCS, CHAdeMO… et calcul de la consommation / autonomie sur le trajet.</li>
<li>Cache offline et canal WebSocket sécurisé avec reconnexion automatique.</li>
</ul>
</article>
<article class="card">
<h3>v0.2.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 5 963 ko · <a href="/zmap/zmap-0.2.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Calcul d'itinéraire (OSRM) avec distance, durée et étapes.</li>
<li>Bornes de recharge le long du trajet.</li>
<li>Recherche de lieux (ville, rue, monument).</li>
<li>Estimation de la consommation en kWh et du pourcentage de batterie.</li>
</ul>
</article>
<article class="card">
<h3>v0.1.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">19 août 2026 · 5 932 ko · <a href="/zmap/zmap-0.1.0.apk" download>⬇ Télécharger</a></p>
<ul>
<li>Première version : carte OpenStreetMap libre.</li>
<li>Bornes de recharge électrique en Europe (Suisse incluse).</li>
<li>Passerelle Zektyc pour éviter les limitations de débit d'Overpass.</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,108 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap archives and changelog: all versions of the free OpenStreetMap Android mapping app with EV charging stations." />
<meta name="robots" content="index, follow" />
<title>Archives &amp; changelog · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/archives" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Archives &amp; changelog · ZMap" />
<meta property="og:description" content="Every ZMap version with its changelog and download links." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/archives" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/en/">← Back to ZMap page</a></p>
<h1>ZMap — Archives &amp; changelog</h1>
<p class="legal-meta">Every published version, with its changelog and APK.</p>
<div class="dev-banner">
⚠️ All versions are public <strong>development builds</strong> meant for
testing. Archived APKs remain downloadable, but the latest version is
always recommended.
</div>
<section class="projects-page">
<article class="card">
<h3>v0.5.0 <span class="tag-latest">latest</span> <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 20, 2026 · 6,011 KB · <a href="/zmap/zmap-0.5.0.apk" download>⬇ Download</a></p>
<ul>
<li>Precise per-road speeds: server database of speed limits (OSM maxspeed) for all roads.</li>
<li>Each trip point is snapped to its actual road; its speed is returned with the route.</li>
<li>The phone keeps the full trip in local cache (offline while driving).</li>
</ul>
</article>
<article class="card">
<h3>v0.4.1 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 20, 2026 · 6,012 KB · <a href="/zmap/zmap-0.4.1.apk" download>⬇ Download</a></p>
<ul>
<li>Fix: starting navigation no longer blocks if the speed server is slow — the trip always starts.</li>
<li>Restricted-access stations (private, customers, employees…) hidden by default and shown in yellow.</li>
</ul>
</article>
<article class="card">
<h3>v0.4.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 6,012 KB · <a href="/zmap/zmap-0.4.0.apk" download>⬇ Download</a></p>
<ul>
<li>New UI: light/dark theme, toasts at the top of the screen, loading screen on startup.</li>
<li>Color-coded stations: green = open, red = closed, yellow = private access, with the kW power shown under each icon.</li>
<li>Privacy settings: WebSocket on/off, clear local cache, hide private stations.</li>
<li>Smoothness: keyboard auto-hide, animations, search bar hidden while driving.</li>
<li>Faster route calculation: max speeds fetched in a single request instead of one per point.</li>
</ul>
</article>
<article class="card">
<h3>v0.3.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 6,001 KB · <a href="/zmap/zmap-0.3.0.apk" download>⬇ Download</a></p>
<ul>
<li>GPS-style driving navigation: camera behind the arrow, "turn left" steps, arrival time, speed and speed limit.</li>
<li>Car selection (Tesla Model Y, Zoé, e-208, ID.3, EV6…) with compatible station filtering.</li>
<li>Detailed connectors: Type 2, CCS, CHAdeMO… and consumption / range estimates.</li>
<li>Offline cache and secure WebSocket channel with auto-reconnect.</li>
</ul>
</article>
<article class="card">
<h3>v0.2.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 5,963 KB · <a href="/zmap/zmap-0.2.0.apk" download>⬇ Download</a></p>
<ul>
<li>Route calculation (OSRM) with distance, duration and steps.</li>
<li>Charging stations along the route.</li>
<li>Place search (city, street, landmark).</li>
<li>Energy consumption and battery percentage estimates.</li>
</ul>
</article>
<article class="card">
<h3>v0.1.0 <span class="tag tag-dev">dev</span></h3>
<p class="legal-note">Aug 19, 2026 · 5,932 KB · <a href="/zmap/zmap-0.1.0.apk" download>⬇ Download</a></p>
<ul>
<li>First release: free OpenStreetMap map.</li>
<li>EV charging stations across Europe (Switzerland included).</li>
<li>Zektyc gateway to avoid Overpass rate limits.</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

123
public/zmap/en/index.html Normal file
View file

@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap: a free Android mapping app using OpenStreetMap, showing EV charging stations across Europe (Switzerland included)." />
<meta name="robots" content="index, follow" />
<title>ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="ZMap · Zektyc" />
<meta property="og:description" content="Free OpenStreetMap map with EV charging stations across Europe (Switzerland included). Android app with automatic updates." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Back to home</a></p>
<h1>ZMap</h1>
<p class="legal-meta">Free Android mapping app · v0.5.0</p>
<a class="btn btn-ghost" href="/zmap/en/archives">🗂️ Archives &amp; changelog</a>
<a class="btn btn-ghost" href="/zmap/en/road-speeds">🧭 Per-road speeds</a>
<div class="dev-banner">
⚠️ <strong>Development build.</strong> All versions published here are
development builds meant for testing. They are public but may contain
bugs or UI changes.
</div>
<section class="projects-page">
<article class="card">
<h3>OpenStreetMap map</h3>
<p>
ZMap displays a free map based on OpenStreetMap tiles, with no account,
no trackers and no data collection. Move around freely with multi-touch
(pinch to zoom), the map and stations load based on your position.
</p>
</article>
<article class="card">
<h3>EV charging stations</h3>
<p>
ZMap queries the OpenStreetMap database (via the Zektyc proxy) and
displays EV charging stations across Europe, Switzerland included.
Tap a station to see its details: operator, power, connectors
(Type 2, CCS, CHAdeMO…), whether it is free and its address.
</p>
</article>
<article class="card">
<h3>Search &amp; navigation</h3>
<p>
Search for a place (city, street, landmark…) from the search bar.
ZMap then computes your route, shows distance and duration, and
highlights the charging stations along the way. Tap the directions
icon then the map to choose a destination.
</p>
</article>
<article class="card">
<h3>Turn-by-turn driving</h3>
<p>
A GPS-style navigation mode: the camera follows the car and stays
oriented behind the arrow (the road direction, not the phone's),
with "turn left" steps, arrival time, speed and road speed limit.
The map is dimmed so the blue route stands out.
</p>
</article>
<article class="card">
<h3>Built for your car</h3>
<p>
Pick your model (Tesla Model Y, Zoé, e-208, ID.3, EV6…): ZMap
computes consumption and range on your trip, and only shows stations
compatible with your connectors (Type 2, CCS, CHAdeMO…).
</p>
</article>
<article class="card">
<h3>Connected &amp; offline-ready</h3>
<p>
ZMap connects to the Zektyc server over secure WebSocket
(auto-reconnect, offline mode banner on disconnect). Before you
leave, the map, route and stations are cached on the phone: during
the drive, data usage is nearly zero.
</p>
</article>
<article class="card">
<h3>Automatic updates</h3>
<p>
ZMap automatically checks for new versions and updates itself, without
going through the Play Store.
</p>
</article>
<article class="card">
<h3>Download</h3>
<p>Latest version: <strong>0.5.0</strong></p>
<a class="btn btn-ghost" href="/zmap/zmap-0.5.0.apk" download>⬇ Download APK (6 MB)</a>
<p class="legal-note">
Compatible with Android 7.0 (API 24) and above. The app is signed
with a release key.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

View file

@ -0,0 +1,204 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap's per-road speed database: 90.5 million sampled points, 3.6 million km of roads mapped with their speed limit (OpenStreetMap), across all of Europe." />
<meta name="robots" content="index, follow" />
<title>Per-road speeds · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/en/road-speeds/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Per-road speeds · ZMap" />
<meta property="og:description" content="ZMap knows the speed limit of every road in Europe: 90.5 million points, 3.6 million km, built from OpenStreetMap data." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/en/road-speeds/" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="fr_FR" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin: 24px 0;
}
.stat {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
text-align: center;
}
.stat b {
display: block;
font-size: 1.7em;
line-height: 1.2;
color: var(--accent, #4dd6a8);
}
.stat span {
font-size: 0.85em;
opacity: 0.7;
}
table.speeds {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
table.speeds th,
table.speeds td {
text-align: right;
padding: 6px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
table.speeds th:first-child,
table.speeds td:first-child {
text-align: left;
}
.bar {
display: inline-block;
height: 10px;
border-radius: 3px;
background: var(--accent, #4dd6a8);
vertical-align: middle;
}
code {
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.9em;
}
</style>
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/en/">← Back to ZMap</a></p>
<h1>Per-road speeds</h1>
<p class="legal-meta">
ZMap's internal database · how it is built and what it contains
</p>
<div class="dev-banner">
🧭 <strong>Navigation feature.</strong> ZMap snaps every point of your
route to its actual road and reads its speed limit from a local database
covering all of Europe — then keeps it cached on your phone for offline
driving.
</div>
<section class="projects-page">
<article class="card">
<h3>The idea</h3>
<p>
Most navigation apps need a connection to know the speed limit. ZMap
does it differently: a per-road speed-limit database is built from
OpenStreetMap data and lives on the server. When a route is computed,
each trip point is snapped to the nearest road and given its speed.
The full route — speeds included — is then cached on the phone: while
driving, no network request is needed.
</p>
</article>
</section>
<div class="stats-grid">
<div class="stat"><b>90.5M</b><span>sampled points</span></div>
<div class="stat"><b>3.6M km</b><span>of roads with a known speed</span></div>
<div class="stat"><b>5,104</b><span>cells of 0.5° × 0.5°</span></div>
<div class="stat"><b>370 MB</b><span>compressed grid (gzip)</span></div>
<div class="stat"><b>40 m</b><span>sampling step along roads</span></div>
<div class="stat"><b>5 B</b><span>per point (lon, lat, speed)</span></div>
</div>
<section class="projects-page">
<article class="card">
<h3>How it is built</h3>
<ol>
<li>
<strong>Source:</strong> the OpenStreetMap Europe extract
(Geofabrik, 34.8 GB) contains 98.2 million roads
(<code>highway</code>).
</li>
<li>
<strong>Filtering:</strong> <code>osmium tags-filter</code> keeps
only the roads (8.5 GB), then only those carrying a speed
(<code>maxspeed</code>) are kept — about 13 million.
</li>
<li>
<strong>Sampling:</strong> a point is placed every 40 metres along
each road, with its speed in km/h.
</li>
<li>
<strong>Indexing:</strong> Europe is split into 0.5° cells
(≈ 55 km), each subdivided into 32 × 32. Every point is quantized
to ≈ 0.76 m and stored in 5 bytes, then the cell is gzip-compressed:
90.5 million points in 370 MB total.
</li>
</ol>
<p>
It is fully reproducible: the build script
(<code>build_maxspeed.py</code>) runs end to end on a small server.
</p>
</article>
<article class="card">
<h3>What is inside</h3>
<p>
Share of points by speed limit (most common values) :
</p>
<table class="speeds">
<tr><th>Limit</th><th>Share</th><th></th></tr>
<tr><td>50 km/h</td><td>26.2%</td><td><span class="bar" style="width:100%"></span></td></tr>
<tr><td>30 km/h (30 zones)</td><td>18.5%</td><td><span class="bar" style="width:71%"></span></td></tr>
<tr><td>90 km/h</td><td>9.5%</td><td><span class="bar" style="width:36%"></span></td></tr>
<tr><td>80 km/h</td><td>9.2%</td><td><span class="bar" style="width:35%"></span></td></tr>
<tr><td>70 km/h</td><td>6.0%</td><td><span class="bar" style="width:23%"></span></td></tr>
<tr><td>60 km/h</td><td>5.7%</td><td><span class="bar" style="width:22%"></span></td></tr>
<tr><td>100 km/h</td><td>4.6%</td><td><span class="bar" style="width:18%"></span></td></tr>
<tr><td>40 km/h</td><td>3.3%</td><td><span class="bar" style="width:13%"></span></td></tr>
<tr><td>48 km/h (30 mph, UK)</td><td>2.8%</td><td><span class="bar" style="width:11%"></span></td></tr>
<tr><td>97 km/h (60 mph, UK)</td><td>2.4%</td><td><span class="bar" style="width:9%"></span></td></tr>
<tr><td>32 km/h (20 mph, UK)</td><td>2.1%</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>20 km/h (pedestrian zones)</td><td>2.1%</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>120 km/h</td><td>1.8%</td><td><span class="bar" style="width:7%"></span></td></tr>
<tr><td>130 km/h (motorways)</td><td>1.7%</td><td><span class="bar" style="width:6%"></span></td></tr>
<tr><td>110 km/h</td><td>1.4%</td><td><span class="bar" style="width:5%"></span></td></tr>
</table>
<p class="legal-note">
<em>Numeric</em> values found in OpenStreetMap. Actual limits may
differ (roadworks, temporary signs, weather). Roads with no limit
(e.g. some German sections) have no <code>maxspeed</code>: ZMap then
shows no speed.
</p>
</article>
<article class="card">
<h3>In the app</h3>
<p>
The server returns, with every route, an array of speeds aligned with
the geometry points (one value per point). The phone stores
everything in its local cache: while driving, the current section's
limit is read without network, and offline mode keeps the whole trip.
Snapping a 4,500-point route takes about 170 ms on the server.
</p>
</article>
<article class="card">
<h3>Accuracy and coverage</h3>
<ul>
<li>Coverage: all of Europe, longitude −12° to 46°, latitude 27° to 72°.</li>
<li>Snapping: the nearest road within 40 m of the point.</li>
<li>Point location accuracy: ≈ 0.76 m.</li>
<li>Data: OpenStreetMap, free (ODbL licence).</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

129
public/zmap/index.html Normal file
View file

@ -0,0 +1,129 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="ZMap : application Android de cartographie libre utilisant OpenStreetMap, affichant les bornes de recharge électrique en Europe (Suisse incluse)." />
<meta name="robots" content="index, follow" />
<title>ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="ZMap · Zektyc" />
<meta property="og:description" content="Carte libre OpenStreetMap avec les bornes de recharge électrique en Europe (Suisse incluse). Application Android, mise à jour automatique." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/" />
<meta property="og:image" content="https://riricdev.tail5ea5cd.ts.net/og-image.png" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/">← Retour à l'accueil</a></p>
<h1>ZMap</h1>
<p class="legal-meta">Application Android de cartographie libre · v0.5.0</p>
<a class="btn btn-ghost" href="/zmap/archives">🗂️ Archives &amp; changelog</a>
<a class="btn btn-ghost" href="/zmap/vitesses-par-route">🧭 Vitesses par route</a>
<div class="dev-banner">
⚠️ <strong>Version de développement.</strong> Toutes les versions publiées
ici sont des versions de développement (dev builds) destinées aux tests.
Elles sont publiques mais peuvent contenir des bugs ou des changements
d'interface.
</div>
<section class="projects-page">
<article class="card">
<h3>Carte OpenStreetMap</h3>
<p>
ZMap affiche une carte libre basée sur les tuiles OpenStreetMap,
sans compte, sans tracker et sans collecte de données. Déplacez-vous
librement avec le multi-touch (pincer pour zoomer), la carte et les
bornes se chargent en fonction de votre position.
</p>
</article>
<article class="card">
<h3>Bornes de recharge électrique</h3>
<p>
ZMap interroge la base OpenStreetMap (via le proxy Zektyc) et affiche
les bornes de recharge électrique en Europe, Suisse incluse. Un simple
appui sur une borne affiche ses caractéristiques : opérateur,
puissance, connecteurs (Type 2, CCS, CHAdeMO…), gratuité et adresse.
</p>
</article>
<article class="card">
<h3>Recherche et navigation</h3>
<p>
Recherchez un lieu (ville, rue, monument…) directement depuis la barre
de recherche. ZMap calcule ensuite votre itinéraire, affiche la
distance et la durée, et met en évidence les bornes de recharge
situées sur le chemin. Touchez l'icône itinéraire puis la carte pour
choisir une destination.
</p>
</article>
<article class="card">
<h3>Conduite assistée</h3>
<p>
Un mode navigation façon GPS : la caméra suit la voiture et reste
orientée derrière la flèche (la direction de la route, pas du
téléphone), avec étapes « tournez à gauche », heure d'arrivée,
vitesse et limite de vitesse du tronçon. La carte est assombrie pour
bien distinguer l'itinéraire en bleu.
</p>
</article>
<article class="card">
<h3>Adaptée à votre voiture</h3>
<p>
Choisissez votre modèle (Tesla Model Y, Zoé, e-208, ID.3, EV6…) :
ZMap calcule la consommation et l'autonomie sur votre trajet, et
n'affiche que les bornes compatibles avec vos connecteurs
(Type 2, CCS, CHAdeMO…).
</p>
</article>
<article class="card">
<h3>Connexion et hors-ligne</h3>
<p>
ZMap se connecte au serveur Zektyc en WebSocket sécurisé (reconnexion
automatique, mode hors-ligne affiché en cas de coupure). Avant un
départ, la carte, l'itinéraire et les bornes sont mis en cache sur le
téléphone : pendant le trajet, la consommation de données est quasi
nulle.
</p>
</article>
<article class="card">
<h3>Mises à jour automatiques</h3>
<p>
ZMap vérifie automatiquement les nouvelles versions et se met à jour
en toute simplicité, sans passer par le Play Store.
</p>
</article>
<article class="card">
<h3>Téléchargement</h3>
<p>Dernière version : <strong>0.5.0</strong></p>
<a class="btn btn-ghost" href="/zmap/zmap-0.5.0.apk" download>⬇ Télécharger l'APK (6 Mo)</a>
<p class="legal-note">
Compatible Android 7.0 (API 24) et supérieur. L'application est
signée avec une clé de release.
</p>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

7
public/zmap/update.json Normal file
View file

@ -0,0 +1,7 @@
{
"versionCode": 6,
"versionName": "0.5.0",
"apkUrl": "https://riricdev.tail5ea5cd.ts.net/zmap/zmap-0.5.0.apk",
"notes": "Vitesses par route précises : le serveur dispose désormais d'une base de données des limites de vitesse (maxspeed OSM) de toutes les routes. Chaque point du trajet est rattaché à sa route et sa vitesse est fournie avec l'itinéraire (plus de requête séparée). Le téléphone garde tout en cache local pour fonctionner hors-ligne.",
"mandatory": false
}

View file

@ -0,0 +1,212 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="La base de données des vitesses par route de ZMap : 90,5 millions de points, 3,6 millions de km de routes cartographiées avec leur limite de vitesse (OpenStreetMap), sur toute l'Europe." />
<meta name="robots" content="index, follow" />
<title>Vitesses par route · ZMap · Zektyc</title>
<link rel="canonical" href="https://riricdev.tail5ea5cd.ts.net/zmap/vitesses-par-route/" />
<meta name="theme-color" content="#070a0f" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Zektyc" />
<meta property="og:title" content="Vitesses par route · ZMap" />
<meta property="og:description" content="ZMap connaît la limite de vitesse de chaque route d'Europe : 90,5 millions de points, 3,6 millions de km, à partir des données OpenStreetMap." />
<meta property="og:url" content="https://riricdev.tail5ea5cd.ts.net/zmap/vitesses-par-route/" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_US" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css?v=7" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin: 24px 0;
}
.stat {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 16px;
text-align: center;
}
.stat b {
display: block;
font-size: 1.7em;
line-height: 1.2;
color: var(--accent, #4dd6a8);
}
.stat span {
font-size: 0.85em;
opacity: 0.7;
}
table.speeds {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
table.speeds th,
table.speeds td {
text-align: right;
padding: 6px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
table.speeds th:first-child,
table.speeds td:first-child {
text-align: left;
}
.bar {
display: inline-block;
height: 10px;
border-radius: 3px;
background: var(--accent, #4dd6a8);
vertical-align: middle;
}
code {
background: rgba(255, 255, 255, 0.06);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.9em;
}
</style>
</head>
<body>
<header id="h"></header>
<main class="legal" id="main">
<p class="legal-note"><a href="/zmap/">← Retour à ZMap</a></p>
<h1>Vitesses par route</h1>
<p class="legal-meta">
La base de données interne de ZMap · comment elle est construite et
ce qu'elle contient
</p>
<div class="dev-banner">
🧭 <strong>Fonctionnalité de navigation.</strong> ZMap rattache chaque
point de votre itinéraire à sa route réelle et lit sa limite de vitesse
dans une base de données locale de toute l'Europe — puis la garde en
cache sur votre téléphone pour la conduite hors-ligne.
</div>
<section class="projects-page">
<article class="card">
<h3>Le principe</h3>
<p>
La plupart des applications de navigation ont besoin d'une connexion
pour connaître la vitesse autorisée. ZMap fait différemment : une base
de données des limites de vitesse est construite <em>par route</em> à
partir des données OpenStreetMap, puis embarquée côté serveur. Quand
un itinéraire est calculé, chaque point du trajet est rattaché à la
route la plus proche et reçoit sa vitesse. L'itinéraire complet — y
compris ses vitesses — est ensuite mis en cache sur le téléphone :
pendant la conduite, plus aucune requête réseau n'est nécessaire.
</p>
</article>
</section>
<div class="stats-grid">
<div class="stat"><b>90,5 M</b><span>points échantillonnés</span></div>
<div class="stat"><b>3,6 M km</b><span>de routes avec vitesse connue</span></div>
<div class="stat"><b>5 104</b><span>cellules de 0,5° × 0,5°</span></div>
<div class="stat"><b>370 Mo</b><span>grille compressée (gzip)</span></div>
<div class="stat"><b>40 m</b><span>échantillonnage le long des routes</span></div>
<div class="stat"><b>5 o</b><span>par point (lon, lat, vitesse)</span></div>
</div>
<section class="projects-page">
<article class="card">
<h3>Comment c'est construit</h3>
<ol>
<li>
<strong>Source :</strong> l'extrait Europe d'OpenStreetMap
(Geofabrik, 34,8 Go) contient 98,2 millions de routes
(<code>highway</code>).
</li>
<li>
<strong>Filtrage :</strong> <code>osmium tags-filter</code> ne garde
que les routes (8,5 Go), puis seules celles portant une vitesse
(<code>maxspeed</code>) sont conservées — environ 13 millions.
</li>
<li>
<strong>Échantillonnage :</strong> un point est posé tous les
40 mètres le long de chaque route, avec sa vitesse en km/h.
</li>
<li>
<strong>Indexation :</strong> l'Europe est découpée en cellules de
0,5° de côté (≈ 55 km), elles-mêmes subdivisées en 32 × 32. Chaque
point est quantifié sur ~0,76 m et stocké sur 5 octets, puis la
cellule est compressée en gzip : 90,5 millions de points en 370 Mo
au total.
</li>
</ol>
<p>
Le tout est reproductible : le script de construction
(<code>build_maxspeed.py</code>) tourne de bout en bout sur un petit
serveur.
</p>
</article>
<article class="card">
<h3>Ce qu'il y a dedans</h3>
<p>
Répartition des points par limite de vitesse (extrait des valeurs les
plus courantes) :
</p>
<table class="speeds">
<tr><th>Limite</th><th>Part des points</th><th></th></tr>
<tr><td>50 km/h</td><td>26,2 %</td><td><span class="bar" style="width:100%"></span></td></tr>
<tr><td>30 km/h (zones 30)</td><td>18,5 %</td><td><span class="bar" style="width:71%"></span></td></tr>
<tr><td>90 km/h</td><td>9,5 %</td><td><span class="bar" style="width:36%"></span></td></tr>
<tr><td>80 km/h</td><td>9,2 %</td><td><span class="bar" style="width:35%"></span></td></tr>
<tr><td>70 km/h</td><td>6,0 %</td><td><span class="bar" style="width:23%"></span></td></tr>
<tr><td>60 km/h</td><td>5,7 %</td><td><span class="bar" style="width:22%"></span></td></tr>
<tr><td>100 km/h</td><td>4,6 %</td><td><span class="bar" style="width:18%"></span></td></tr>
<tr><td>40 km/h</td><td>3,3 %</td><td><span class="bar" style="width:13%"></span></td></tr>
<tr><td>48 km/h (30 mph, RU)</td><td>2,8 %</td><td><span class="bar" style="width:11%"></span></td></tr>
<tr><td>97 km/h (60 mph, RU)</td><td>2,4 %</td><td><span class="bar" style="width:9%"></span></td></tr>
<tr><td>32 km/h (20 mph, RU)</td><td>2,1 %</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>20 km/h (zones piétonnes)</td><td>2,1 %</td><td><span class="bar" style="width:8%"></span></td></tr>
<tr><td>120 km/h</td><td>1,8 %</td><td><span class="bar" style="width:7%"></span></td></tr>
<tr><td>130 km/h (autoroutes)</td><td>1,7 %</td><td><span class="bar" style="width:6%"></span></td></tr>
<tr><td>110 km/h</td><td>1,4 %</td><td><span class="bar" style="width:5%"></span></td></tr>
</table>
<p class="legal-note">
Valeurs <em>numériques</em> présentes dans OpenStreetMap. Les vitesses
réelles peuvent différer (travaux, panneaux temporaires, conditions
météo). Les autoroutes sans limite (ex. certaines portions allemandes)
n'ont pas de <code>maxspeed</code> : ZMap n'affiche alors aucune
vitesse.
</p>
</article>
<article class="card">
<h3>Côté application</h3>
<p>
Le serveur renvoie, avec chaque itinéraire, un tableau de vitesses
aligné sur les points du tracé (une valeur par point). Le téléphone
enregistre tout dans son cache local : pendant la conduite, la limite
du tronçon courant est lue sans réseau, et le mode hors-ligne garde
tout le trajet. Le rattachement d'un trajet de 4 500 points prend
environ 170 ms sur le serveur.
</p>
</article>
<article class="card">
<h3>Précision et couverture</h3>
<ul>
<li>Couverture : Europe entière, longitude −12° à 46°, latitude 27° à 72°.</li>
<li>Rattachement : la route la plus proche à moins de 40 m du point.</li>
<li>Localisation d'un point : précision ≈ 0,76 m.</li>
<li>Données : OpenStreetMap, libre de droits (licence ODbL).</li>
</ul>
</article>
</section>
</main>
<footer id="f"></footer>
<script src="/js/theme.js" defer></script>
<script src="/js/components.js?v=6" defer></script>
</body>
</html>

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}
}

BIN
public/zmap/zmap-0.1.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.2.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.3.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.4.0.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.4.1.apk Normal file

Binary file not shown.

BIN
public/zmap/zmap-0.5.0.apk Normal file

Binary file not shown.