Spaces:
Sleeping
Sleeping
Add motorway toll estimate + fuel/recharge-plus-toll total to itinerary cost
Browse files- Estimate the tolled-motorway distance from OSRM step refs (steps=true; a step
counts as autoroute when its ref looks like "A 6"), times an average light-
vehicle rate (~0,09 €/km).
- Itinerary cost now shows "🅿️ Péage autoroute : ≈ X €" and a "💰 Total" line
combining fuel (or recharge) + toll, for both thermique and électrique modes.
- Toll shown on its own when a route exists but no vehicle is selected.
- Bump service worker cache to v9.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- static/app.js +56 -10
- static/sw.js +1 -1
static/app.js
CHANGED
|
@@ -18,6 +18,9 @@ const STATIONS = DATA.stations || [];
|
|
| 18 |
const CARS = DATA.cars || []; // [[label, fuel, L/100km], ...] real ADEME models
|
| 19 |
const PRICES = 6, CP = 7, CITY = 8, HW = 9; // station record indices (HW: 1 = autoroute)
|
| 20 |
const CORRIDOR_KM = 6; // show stations within this distance of a route
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
// Green -> yellow -> red gradient stops (cheap -> expensive).
|
| 23 |
const STOPS = [
|
|
@@ -63,6 +66,7 @@ let placeQuery = "";
|
|
| 63 |
let routeCoords = null; // L.LatLng[] of the active route, or null
|
| 64 |
let routeStations = null; // stations near the active route (any fuel)
|
| 65 |
let routeDistanceKm = null; // length of the active route, km
|
|
|
|
| 66 |
let vehicleMode = "thermique"; // "thermique" | "electrique"
|
| 67 |
let thermCar = null; // { label, fuel, cons } chosen combustion model
|
| 68 |
let stopMarkers = []; // recharge-stop markers on the map
|
|
@@ -478,10 +482,11 @@ function routeMsg(text) {
|
|
| 478 |
if (el) el.textContent = text || "";
|
| 479 |
}
|
| 480 |
|
| 481 |
-
// Driving route between two points via the public OSRM server.
|
|
|
|
| 482 |
async function routeBetween(a, b) {
|
| 483 |
const url = `https://router.project-osrm.org/route/v1/driving/` +
|
| 484 |
-
`${a.lon},${a.lat};${b.lon},${b.lat}?overview=full&geometries=geojson`;
|
| 485 |
const r = await fetch(url);
|
| 486 |
if (!r.ok) return null;
|
| 487 |
const j = await r.json();
|
|
@@ -489,6 +494,22 @@ async function routeBetween(a, b) {
|
|
| 489 |
return j.routes[0];
|
| 490 |
}
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
function drawRoute(coords, a, b) {
|
| 493 |
clearRouteLayers();
|
| 494 |
routeLine = L.polyline(coords, { color: "#1a73e8", weight: 5, opacity: 0.85 }).addTo(map);
|
|
@@ -526,6 +547,7 @@ async function computeRoute() {
|
|
| 526 |
routeCoords = coords;
|
| 527 |
routeStations = nearRoute(STATIONS, coords); // cache once for filtering + cost
|
| 528 |
routeDistanceKm = route.distance / 1000;
|
|
|
|
| 529 |
render(false);
|
| 530 |
map.fitBounds(L.latLngBounds(coords).pad(0.1));
|
| 531 |
const km = (route.distance / 1000).toFixed(0);
|
|
@@ -544,6 +566,7 @@ function clearRoute() {
|
|
| 544 |
routeCoords = null;
|
| 545 |
routeStations = null;
|
| 546 |
routeDistanceKm = null;
|
|
|
|
| 547 |
depCoord = arrCoord = null;
|
| 548 |
const d = document.getElementById("dep"), a = document.getElementById("arr");
|
| 549 |
if (d) d.value = "";
|
|
@@ -670,38 +693,61 @@ function attachCarAutocomplete(input) {
|
|
| 670 |
input.addEventListener("blur", () => setTimeout(hide, 150));
|
| 671 |
}
|
| 672 |
|
| 673 |
-
// Journey fuel cost (combustion) or charging summary (electric)
|
|
|
|
| 674 |
function updateCost() {
|
| 675 |
const el = document.getElementById("cost-line");
|
| 676 |
if (!el) return;
|
| 677 |
if (!routeDistanceKm) { el.innerHTML = ""; return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
if (vehicleMode === "electrique") {
|
| 679 |
const ev = currentEv();
|
| 680 |
-
if (!ev) { el.innerHTML =
|
| 681 |
const usable = ev.range * 0.8;
|
| 682 |
const stops = Math.max(0, Math.ceil(routeDistanceKm / usable) - 1);
|
| 683 |
const kwh = routeDistanceKm / 100 * ev.kwh;
|
| 684 |
-
|
| 685 |
-
|
|
|
|
|
|
|
|
|
|
| 686 |
return;
|
| 687 |
}
|
|
|
|
| 688 |
const car = thermCar;
|
| 689 |
-
if (!car) { el.innerHTML =
|
| 690 |
const liters = routeDistanceKm / 100 * car.cons;
|
| 691 |
const fi = FUELS.indexOf(car.fuel);
|
| 692 |
const price = cheapestAlongRoute(fi);
|
| 693 |
if (price == null) {
|
| 694 |
-
el.innerHTML = `~${eur(liters, 1)} L · aucune station ${esc(car.fuel)} sur le trajet.`;
|
| 695 |
return;
|
| 696 |
}
|
|
|
|
| 697 |
const hwPrice = cheapestAlongRoute(fi, true); // cheapest autoroute station on the route
|
| 698 |
-
let html = `⛽ <b>≈ ${eur(
|
| 699 |
`<br><span class="hint">${eur(liters, 1)} L · au moins cher du trajet (${eur(price, 3)} €/L)</span>`;
|
| 700 |
if (hwPrice != null) {
|
| 701 |
html += `<br><span class="hint">🛣️ sur autoroute : ≈ ${eur(liters * hwPrice, 2)} € ` +
|
| 702 |
`(${eur(hwPrice, 3)} €/L)</span>`;
|
| 703 |
}
|
| 704 |
-
el.innerHTML = html;
|
| 705 |
}
|
| 706 |
|
| 707 |
// ===================== EV chargers (IRVE) + recharge stops =====================
|
|
|
|
| 18 |
const CARS = DATA.cars || []; // [[label, fuel, L/100km], ...] real ADEME models
|
| 19 |
const PRICES = 6, CP = 7, CITY = 8, HW = 9; // station record indices (HW: 1 = autoroute)
|
| 20 |
const CORRIDOR_KM = 6; // show stations within this distance of a route
|
| 21 |
+
// Rough average French autoroute toll for a light vehicle (classe 1), €/km of
|
| 22 |
+
// motorway driven. Tolls vary by concessionaire, so this is an estimate only.
|
| 23 |
+
const TOLL_EUR_PER_KM = 0.09;
|
| 24 |
|
| 25 |
// Green -> yellow -> red gradient stops (cheap -> expensive).
|
| 26 |
const STOPS = [
|
|
|
|
| 66 |
let routeCoords = null; // L.LatLng[] of the active route, or null
|
| 67 |
let routeStations = null; // stations near the active route (any fuel)
|
| 68 |
let routeDistanceKm = null; // length of the active route, km
|
| 69 |
+
let routeTollKm = 0; // motorway distance on the route (for toll estimate), km
|
| 70 |
let vehicleMode = "thermique"; // "thermique" | "electrique"
|
| 71 |
let thermCar = null; // { label, fuel, cons } chosen combustion model
|
| 72 |
let stopMarkers = []; // recharge-stop markers on the map
|
|
|
|
| 482 |
if (el) el.textContent = text || "";
|
| 483 |
}
|
| 484 |
|
| 485 |
+
// Driving route between two points via the public OSRM server. steps=true so we
|
| 486 |
+
// can read each segment's road ref and estimate the tolled-motorway distance.
|
| 487 |
async function routeBetween(a, b) {
|
| 488 |
const url = `https://router.project-osrm.org/route/v1/driving/` +
|
| 489 |
+
`${a.lon},${a.lat};${b.lon},${b.lat}?overview=full&geometries=geojson&steps=true`;
|
| 490 |
const r = await fetch(url);
|
| 491 |
if (!r.ok) return null;
|
| 492 |
const j = await r.json();
|
|
|
|
| 494 |
return j.routes[0];
|
| 495 |
}
|
| 496 |
|
| 497 |
+
// A step is on a motorway if its road ref looks like "A 6", "A6", "A 7;E 15"…
|
| 498 |
+
function isAutorouteRef(ref) {
|
| 499 |
+
return typeof ref === "string" && /(^|\W)A\s?\d/.test(ref);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
// Total motorway distance (km) on the route, summed from autoroute steps.
|
| 503 |
+
function tollKmFromRoute(route) {
|
| 504 |
+
let meters = 0;
|
| 505 |
+
for (const leg of route.legs || []) {
|
| 506 |
+
for (const step of leg.steps || []) {
|
| 507 |
+
if (isAutorouteRef(step.ref)) meters += step.distance || 0;
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
return meters / 1000;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
function drawRoute(coords, a, b) {
|
| 514 |
clearRouteLayers();
|
| 515 |
routeLine = L.polyline(coords, { color: "#1a73e8", weight: 5, opacity: 0.85 }).addTo(map);
|
|
|
|
| 547 |
routeCoords = coords;
|
| 548 |
routeStations = nearRoute(STATIONS, coords); // cache once for filtering + cost
|
| 549 |
routeDistanceKm = route.distance / 1000;
|
| 550 |
+
routeTollKm = tollKmFromRoute(route);
|
| 551 |
render(false);
|
| 552 |
map.fitBounds(L.latLngBounds(coords).pad(0.1));
|
| 553 |
const km = (route.distance / 1000).toFixed(0);
|
|
|
|
| 566 |
routeCoords = null;
|
| 567 |
routeStations = null;
|
| 568 |
routeDistanceKm = null;
|
| 569 |
+
routeTollKm = 0;
|
| 570 |
depCoord = arrCoord = null;
|
| 571 |
const d = document.getElementById("dep"), a = document.getElementById("arr");
|
| 572 |
if (d) d.value = "";
|
|
|
|
| 693 |
input.addEventListener("blur", () => setTimeout(hide, 150));
|
| 694 |
}
|
| 695 |
|
| 696 |
+
// Journey fuel cost (combustion) or charging summary (electric), plus an
|
| 697 |
+
// estimated motorway toll and the grand total (energy + toll) for the route.
|
| 698 |
function updateCost() {
|
| 699 |
const el = document.getElementById("cost-line");
|
| 700 |
if (!el) return;
|
| 701 |
if (!routeDistanceKm) { el.innerHTML = ""; return; }
|
| 702 |
+
|
| 703 |
+
const toll = routeTollKm * TOLL_EUR_PER_KM;
|
| 704 |
+
const hasToll = routeTollKm > 0.5; // ignore a few hundred m of motorway slip roads
|
| 705 |
+
const tollLine = hasToll
|
| 706 |
+
? `<br><span class="hint">🅿️ Péage autoroute : ≈ ${eur(toll, 2)} € ` +
|
| 707 |
+
`(estimation, ${eur(routeTollKm, 0)} km)</span>`
|
| 708 |
+
: "";
|
| 709 |
+
const totalLine = (energy, label) => hasToll
|
| 710 |
+
? `<br>💰 <b>Total : ≈ ${eur(energy + toll, 2)} €</b>` +
|
| 711 |
+
`<span class="hint"> (${label} + péage)</span>`
|
| 712 |
+
: "";
|
| 713 |
+
// When a route exists but no vehicle is chosen, still show the toll on its own.
|
| 714 |
+
const tollOnly = hasToll
|
| 715 |
+
? `🅿️ <b>≈ ${eur(toll, 2)} €</b> de péage` +
|
| 716 |
+
`<br><span class="hint">estimation autoroute (${eur(routeTollKm, 0)} km)</span>`
|
| 717 |
+
: "";
|
| 718 |
+
|
| 719 |
if (vehicleMode === "electrique") {
|
| 720 |
const ev = currentEv();
|
| 721 |
+
if (!ev) { el.innerHTML = tollOnly; return; }
|
| 722 |
const usable = ev.range * 0.8;
|
| 723 |
const stops = Math.max(0, Math.ceil(routeDistanceKm / usable) - 1);
|
| 724 |
const kwh = routeDistanceKm / 100 * ev.kwh;
|
| 725 |
+
const energy = kwh * 0.40;
|
| 726 |
+
el.innerHTML =
|
| 727 |
+
`⚡ <b>${stops} recharge${stops > 1 ? "s" : ""}</b> en route · autonomie ${ev.range} km` +
|
| 728 |
+
`<br><span class="hint">${eur(kwh, 0)} kWh · ~${eur(energy, 0)} € (≈ 0,40 €/kWh)</span>` +
|
| 729 |
+
tollLine + totalLine(energy, "recharge");
|
| 730 |
return;
|
| 731 |
}
|
| 732 |
+
|
| 733 |
const car = thermCar;
|
| 734 |
+
if (!car) { el.innerHTML = tollOnly; return; }
|
| 735 |
const liters = routeDistanceKm / 100 * car.cons;
|
| 736 |
const fi = FUELS.indexOf(car.fuel);
|
| 737 |
const price = cheapestAlongRoute(fi);
|
| 738 |
if (price == null) {
|
| 739 |
+
el.innerHTML = `~${eur(liters, 1)} L · aucune station ${esc(car.fuel)} sur le trajet.` + tollLine;
|
| 740 |
return;
|
| 741 |
}
|
| 742 |
+
const fuelCost = liters * price;
|
| 743 |
const hwPrice = cheapestAlongRoute(fi, true); // cheapest autoroute station on the route
|
| 744 |
+
let html = `⛽ <b>≈ ${eur(fuelCost, 2)} €</b> de ${esc(car.fuel)}` +
|
| 745 |
`<br><span class="hint">${eur(liters, 1)} L · au moins cher du trajet (${eur(price, 3)} €/L)</span>`;
|
| 746 |
if (hwPrice != null) {
|
| 747 |
html += `<br><span class="hint">🛣️ sur autoroute : ≈ ${eur(liters * hwPrice, 2)} € ` +
|
| 748 |
`(${eur(hwPrice, 3)} €/L)</span>`;
|
| 749 |
}
|
| 750 |
+
el.innerHTML = html + tollLine + totalLine(fuelCost, "carburant");
|
| 751 |
}
|
| 752 |
|
| 753 |
// ===================== EV chargers (IRVE) + recharge stops =====================
|
static/sw.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
* - The app shell, icons and CDN libraries are cached so the app boots offline.
|
| 5 |
* - Map tiles are cached with a bounded LRU-ish cap for offline revisits.
|
| 6 |
*/
|
| 7 |
-
const VERSION = "
|
| 8 |
const SHELL = "shell-" + VERSION;
|
| 9 |
const RUNTIME = "runtime-" + VERSION;
|
| 10 |
const TILES = "tiles-" + VERSION;
|
|
|
|
| 4 |
* - The app shell, icons and CDN libraries are cached so the app boots offline.
|
| 5 |
* - Map tiles are cached with a bounded LRU-ish cap for offline revisits.
|
| 6 |
*/
|
| 7 |
+
const VERSION = "v9";
|
| 8 |
const SHELL = "shell-" + VERSION;
|
| 9 |
const RUNTIME = "runtime-" + VERSION;
|
| 10 |
const TILES = "tiles-" + VERSION;
|