chore: checkpoint initial — Zektyc dataset, avant humanisation
- site vitrine + fingerprint test + zmap + z-panel - remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs) - suppression du token DuckDNS (plus utilisé)
This commit is contained in:
commit
d7ec32a458
92 changed files with 17476 additions and 0 deletions
108
public/zmap/archives/index.html
Normal file
108
public/zmap/archives/index.html
Normal 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 & 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 & 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 & 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>
|
||||
108
public/zmap/en/archives/index.html
Normal file
108
public/zmap/en/archives/index.html
Normal 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 & 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 & 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 & 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
123
public/zmap/en/index.html
Normal 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 & 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 & 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 & 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>
|
||||
204
public/zmap/en/road-speeds/index.html
Normal file
204
public/zmap/en/road-speeds/index.html
Normal 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
129
public/zmap/index.html
Normal 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 & 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
7
public/zmap/update.json
Normal 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
|
||||
}
|
||||
212
public/zmap/vitesses-par-route/index.html
Normal file
212
public/zmap/vitesses-par-route/index.html
Normal 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
188
public/zmap/web/app.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
(function(){
|
||||
"use strict";
|
||||
var API = location.origin;
|
||||
|
||||
var map = L.map("map",{zoomControl:false,attributionControl:false}).setView([48.85,2.35],13);
|
||||
var osm = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",{maxZoom:19,attribution:"OSM"});
|
||||
var esri = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{maxZoom:19,attribution:"Esri"});
|
||||
osm.addTo(map);
|
||||
L.control.attribution({position:"bottomleft",prefix:false}).addTo(map);
|
||||
|
||||
var fromPt=null, toPt=null, routeLayer=null, stationLayer=null, gpsWatchId=null, gpsMarker=null;
|
||||
var selectingFrom=true;
|
||||
|
||||
var $=function(id){return document.getElementById(id)};
|
||||
var $from=$("from"), $to=$("to"), $btnRoute=$("btn-route"), $btnClear=$("btn-clear");
|
||||
var $btnSat=$("btn-sat"), $btnGps=$("btn-gps"), $panel=$("panel");
|
||||
var $routeInfo=$("route-info"), $legend=$("speed-legend"), $loading=$("loading");
|
||||
var $searchIn=$("search-input"), $searchRes=$("search-results");
|
||||
|
||||
function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}
|
||||
|
||||
$btnSat.onclick=function(){
|
||||
if(map.hasLayer(esri)){map.removeLayer(esri);osm.addTo(map);$btnSat.className="icon-btn"}
|
||||
else{map.removeLayer(osm);esri.addTo(map);$btnSat.className="icon-btn active"}
|
||||
};
|
||||
|
||||
$btnGps.onclick=function(){
|
||||
if(gpsWatchId!=null){stopGps();return}
|
||||
if(!navigator.geolocation){alert("Géolocalisation non supportée");return}
|
||||
gpsWatchId=navigator.geolocation.watchPosition(function(pos){
|
||||
var lat=pos.coords.latitude,lng=pos.coords.longitude;
|
||||
if(!gpsMarker){
|
||||
var icon=L.divIcon({className:"",html:"<div class='gps-dot'></div>",iconSize:[14,14],iconAnchor:[7,7]});
|
||||
gpsMarker=L.marker([lat,lng],{icon:icon,zIndexOffset:1000}).addTo(map);
|
||||
} else gpsMarker.setLatLng([lat,lng]);
|
||||
},function(e){alert("Erreur GPS: "+e.message)},{enableHighAccuracy:true,timeout:10000});
|
||||
$btnGps.className="icon-btn active gps-active";
|
||||
};
|
||||
function stopGps(){
|
||||
if(gpsWatchId!=null){navigator.geolocation.clearWatch(gpsWatchId);gpsWatchId=null}
|
||||
if(gpsMarker){map.removeLayer(gpsMarker);gpsMarker=null}
|
||||
$btnGps.className="icon-btn";
|
||||
}
|
||||
|
||||
var searchTimer=null;
|
||||
$searchIn.oninput=function(){
|
||||
clearTimeout(searchTimer);
|
||||
var q=$searchIn.value.trim();
|
||||
if(q.length<2){$searchRes.className="search-results";return}
|
||||
searchTimer=setTimeout(function(){doSearch(q)},300);
|
||||
};
|
||||
$searchIn.onkeydown=function(e){if(e.key==="Escape"){$searchRes.className="search-results";$searchIn.blur()}};
|
||||
document.addEventListener("click",function(e){if(!e.target.closest(".search-wrap"))$searchRes.className="search-results"});
|
||||
|
||||
function doSearch(q){
|
||||
fetch(API+"/api/zmap/search?q="+encodeURIComponent(q))
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if(!d.results||!d.results.length){$searchRes.innerHTML='<div class="sr-item" style="color:var(--dim)">Aucun résultat</div>';return}
|
||||
$searchRes.innerHTML="";
|
||||
for(var i=0;i<d.results.length;i++){
|
||||
var r=d.results[i];
|
||||
var el=document.createElement("div");
|
||||
el.className="sr-item";
|
||||
el.innerHTML="<div>"+esc(r.display_name)+"</div><div class='sr-type'>"+esc(r.type)+" · "+esc(r.category)+"</div>";
|
||||
el.dataset.lat=r.lat;el.dataset.lng=r.lon;
|
||||
el.onclick=function(){
|
||||
var lat=parseFloat(this.dataset.lat),lng=parseFloat(this.dataset.lng);
|
||||
map.setView([lat,lng],16);
|
||||
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
|
||||
else{setTo(lat,lng);selectingFrom=true}
|
||||
$searchRes.className="search-results";$searchIn.value="";
|
||||
};
|
||||
$searchRes.appendChild(el);
|
||||
}
|
||||
$searchRes.className="search-results open";
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
map.on("click",function(e){
|
||||
var lat=+e.latlng.lat.toFixed(5),lng=+e.latlng.lng.toFixed(5);
|
||||
if(selectingFrom){setFrom(lat,lng);selectingFrom=false}
|
||||
else{setTo(lat,lng);selectingFrom=true}
|
||||
});
|
||||
|
||||
function setFrom(lat,lng){fromPt={lat:lat,lng:lng};$from.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
|
||||
function setTo(lat,lng){toPt={lat:lat,lng:lng};$to.value=lat+","+lng;if(fromPt&&toPt)$btnRoute.disabled=false}
|
||||
|
||||
$btnClear.onclick=function(){
|
||||
fromPt=toPt=null;$from.value="";$to.value="";$btnRoute.disabled=true;selectingFrom=true;
|
||||
$routeInfo.className="route-info hidden";$legend.innerHTML="";
|
||||
if(routeLayer){map.removeLayer(routeLayer);routeLayer=null}
|
||||
if(stationLayer){map.removeLayer(stationLayer);stationLayer=null}
|
||||
};
|
||||
|
||||
$btnRoute.onclick=function(){
|
||||
if(!fromPt||!toPt)return;
|
||||
$loading.className="loading show";
|
||||
fetch(API+"/api/zmap/route?from="+fromPt.lat+","+fromPt.lng+"&to="+toPt.lat+","+toPt.lng)
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){if(d.error){alert(d.error);return}renderRoute(d)})
|
||||
.catch(function(e){alert("Erreur: "+e.message)})
|
||||
.finally(function(){$loading.className="loading"});
|
||||
};
|
||||
|
||||
function speedColor(k){
|
||||
if(k==null)return"#555";
|
||||
if(k<=20)return"#ef4444";if(k<=40)return"#f59e0b";if(k<=60)return"#eab308";
|
||||
if(k<=80)return"#22c55e";if(k<=100)return"#06b6d4";if(k<=120)return"#3b82f6";return"#8b5cf6";
|
||||
}
|
||||
function speedCls(k){
|
||||
if(k==null)return"sl-unk";
|
||||
if(k<=20)return"sl-20";if(k<=40)return"sl-40";if(k<=60)return"sl-60";
|
||||
if(k<=80)return"sl-80";if(k<=100)return"sl-100";if(k<=120)return"sl-120";return"sl-max";
|
||||
}
|
||||
|
||||
function renderRoute(d){
|
||||
if(routeLayer)map.removeLayer(routeLayer);
|
||||
var coords=d.geometry&&d.geometry.coordinates||[];
|
||||
var speeds=d.speeds||[];
|
||||
var segs=[];
|
||||
for(var i=0;i<coords.length-1;i++){
|
||||
var kmh=speeds[i]!=null?speeds[i]:null;
|
||||
var line=L.polyline([[coords[i][1],coords[i][0]],[coords[i+1][1],coords[i+1][0]]],
|
||||
{color:speedColor(kmh),weight:5,opacity:.9,lineCap:"round",lineJoin:"round"});
|
||||
if(kmh!=null)line.bindPopup("<b>"+kmh+" km/h</b>");
|
||||
segs.push(line);
|
||||
}
|
||||
routeLayer=L.layerGroup(segs).addTo(map);
|
||||
map.fitBounds(L.polyline(coords.map(function(c){return[c[1],c[0]]})).getBounds().pad(.1));
|
||||
|
||||
var covered=speeds.filter(function(s){return s!=null}).length;
|
||||
var total=speeds.length;
|
||||
var pct=total?Math.round(covered/total*100):0;
|
||||
$routeInfo.className="route-info";
|
||||
var dist={};
|
||||
for(var i=0;i<speeds.length;i++){
|
||||
var k=speeds[i]==null?"?":speeds[i]<=20?"0-20":speeds[i]<=40?"21-40":speeds[i]<=60?"41-60":speeds[i]<=80?"61-80":speeds[i]<=100?"81-100":speeds[i]<=120?"101-120":">120";
|
||||
dist[k]=(dist[k]||0)+1;
|
||||
}
|
||||
var colors={"?":"#555","0-20":"#ef4444","21-40":"#f59e0b","41-60":"#eab308","61-80":"#22c55e","81-100":"#06b6d4","101-120":"#3b82f6",">120":"#8b5cf6"};
|
||||
var order=["?","0-20","21-40","41-60","61-80","81-100","101-120",">120"];
|
||||
var barHtml="<div class='speed-bar'>";
|
||||
var distribHtml="<div class='speed-distrib'>";
|
||||
for(var j=0;j<order.length;j++){
|
||||
var cnt=dist[order[j]]||0;
|
||||
if(cnt===0)continue;
|
||||
var bpct=Math.round(cnt/speeds.length*100);
|
||||
barHtml+="<div style='width:"+bpct+"%;background:"+colors[order[j]]+"'></div>";
|
||||
distribHtml+="<span><span class='sd-dot' style='background:"+colors[order[j]]+"'></span>"+order[j]+" km/h: "+cnt+"</span>";
|
||||
}
|
||||
barHtml+="</div>";distribHtml+="</div>";
|
||||
$routeInfo.innerHTML='<div class="ri-main">'+d.distance_km+" km</div>"+
|
||||
'<div class="ri-sub">'+d.duration_min+" min · "+covered+"/"+total+" segments · <b>"+pct+"%</b> couverts</div>"+barHtml+distribHtml;
|
||||
|
||||
var leg=[["≤20","sl-20"],["21-40","sl-40"],["41-60","sl-60"],
|
||||
["61-80","sl-80"],["81-100","sl-100"],["101-120","sl-120"],[">120","sl-max"],["?","sl-unk"]];
|
||||
$legend.innerHTML=leg.map(function(l){return'<span class="'+l[1]+'">'+l[0]+" km/h</span>"}).join("");
|
||||
loadStations(coords);
|
||||
}
|
||||
|
||||
function loadStations(coords){
|
||||
if(stationLayer)map.removeLayer(stationLayer);
|
||||
if(!coords.length)return;
|
||||
var mnLa=90,mxLa=-90,mnLo=180,mxLo=-180;
|
||||
for(var i=0;i<coords.length;i++){
|
||||
if(coords[i][1]<mnLa)mnLa=coords[i][1];if(coords[i][1]>mxLa)mxLa=coords[i][1];
|
||||
if(coords[i][0]<mnLo)mnLo=coords[i][0];if(coords[i][0]>mxLo)mxLo=coords[i][0];
|
||||
}
|
||||
var p=.02;
|
||||
fetch(API+"/api/zmap/stations?south="+(mnLa-p)+"&west="+(mnLo-p)+"&north="+(mxLa+p)+"&east="+(mxLo+p))
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if(!d.stations||!d.stations.length)return;
|
||||
stationLayer=L.layerGroup().addTo(map);
|
||||
for(var i=0;i<d.stations.length;i++){
|
||||
var s=d.stations[i];
|
||||
var icon=L.divIcon({className:"",html:"<div class='station-dot'></div>",iconSize:[10,10],iconAnchor:[5,5]});
|
||||
var pw=0;
|
||||
if(s.connectors)for(var j=0;j<s.connectors.length;j++){var v=s.connectors[j].powerKw||0;if(v>pw)pw=v}
|
||||
L.marker([s.lat,s.lon],{icon:icon}).bindPopup(
|
||||
"<b>"+esc((s.name||"").slice(0,40)||"Borne")+"</b><br>"+
|
||||
(pw?pw+" kW":"")+" "+esc(s.network||"")+"<br>"+esc(s.status||"")
|
||||
).addTo(stationLayer);
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
})();
|
||||
56
public/zmap/web/index.html
Normal file
56
public/zmap/web/index.html
Normal 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">×</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>
|
||||
BIN
public/zmap/web/leaflet/images/layers-2x.png
Normal file
BIN
public/zmap/web/leaflet/images/layers-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/zmap/web/leaflet/images/layers.png
Normal file
BIN
public/zmap/web/leaflet/images/layers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
661
public/zmap/web/leaflet/leaflet.css
Normal file
661
public/zmap/web/leaflet/leaflet.css
Normal 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;
|
||||
}
|
||||
}
|
||||
6
public/zmap/web/leaflet/leaflet.js
Normal file
6
public/zmap/web/leaflet/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/zmap/web/leaflet/marker-icon-2x.png
Normal file
BIN
public/zmap/web/leaflet/marker-icon-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/zmap/web/leaflet/marker-icon.png
Normal file
BIN
public/zmap/web/leaflet/marker-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/zmap/web/leaflet/marker-shadow.png
Normal file
BIN
public/zmap/web/leaflet/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
99
public/zmap/web/style.css
Normal file
99
public/zmap/web/style.css
Normal 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
BIN
public/zmap/zmap-0.1.0.apk
Normal file
Binary file not shown.
BIN
public/zmap/zmap-0.2.0.apk
Normal file
BIN
public/zmap/zmap-0.2.0.apk
Normal file
Binary file not shown.
BIN
public/zmap/zmap-0.3.0.apk
Normal file
BIN
public/zmap/zmap-0.3.0.apk
Normal file
Binary file not shown.
BIN
public/zmap/zmap-0.4.0.apk
Normal file
BIN
public/zmap/zmap-0.4.0.apk
Normal file
Binary file not shown.
BIN
public/zmap/zmap-0.4.1.apk
Normal file
BIN
public/zmap/zmap-0.4.1.apk
Normal file
Binary file not shown.
BIN
public/zmap/zmap-0.5.0.apk
Normal file
BIN
public/zmap/zmap-0.5.0.apk
Normal file
Binary file not shown.
Loading…
Reference in a new issue