SamDNX Claude Opus 4.8 commited on
Commit
3cc22e3
·
1 Parent(s): 57ffa0b

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>

Files changed (2) hide show
  1. static/app.js +56 -10
  2. 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) for the route.
 
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 = ""; return; }
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
- el.innerHTML = `⚡ <b>${stops} recharge${stops > 1 ? "s" : ""}</b> en route · autonomie ${ev.range} km` +
685
- `<br><span class="hint">${eur(kwh, 0)} kWh · ~${eur(kwh * 0.40, 0)} € (≈ 0,40 €/kWh)</span>`;
 
 
 
686
  return;
687
  }
 
688
  const car = thermCar;
689
- if (!car) { el.innerHTML = ""; return; }
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(liters * price, 2)} €</b> de ${esc(car.fuel)}` +
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 = "v8";
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;