File size: 5,673 Bytes
01a6a9e
284e818
 
 
 
01a6a9e
284e818
01a6a9e
4ccb3e1
01a6a9e
 
284e818
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b02f256
 
71c08ae
 
 
 
 
 
 
 
 
 
284e818
 
b02f256
284e818
71c08ae
 
 
 
 
 
284e818
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71c08ae
 
284e818
 
71c08ae
 
 
284e818
 
71c08ae
284e818
71c08ae
 
 
 
 
 
 
 
 
 
 
284e818
 
 
 
 
 
 
4ccb3e1
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
---
title: Prix des Carburants France
emoji: 
colorFrom: green
colorTo: red
sdk: docker
app_port: 7860
pinned: false
license: apache-2.0
---

# ⛽ Petrol Map — live French fuel prices (PWA)

An installable, mobile-friendly map of every petrol station in France,
colour-coded by price and refreshed twice a day from the official open-data feed.

**Data source:** [`prix_des_carburants_j_7`](https://public.opendatasoft.com/explore/dataset/prix_des_carburants_j_7)
on opendatasoft (the last 7 days of prices reported by ~9 800 stations).

## Features

- **Installable PWA** — add it to your phone's home screen and launch it like a
  native app; works offline (shows the last cached prices) via a service worker.
- **Responsive** — full-screen map with touch-friendly controls and safe-area
  handling for notched phones.
- **One layer per fuel** — Gazole, SP95, SP98, E10, E85, GPLc — via the selector.
- **Price colour scale** — green = cheaper, red = pricier, scaled to each fuel's
  national 5th–95th percentile; the legend updates per fuel.
- **Search by postal code / city** — type e.g. `75011` or `Lyon` to filter the
  map to that area and zoom to it.
- **Itinerary maker** — enter a *ville de départ* and *ville d'arrivée* (with city
  autocomplete via the Base Adresse Nationale); the route is drawn (OSRM) and the
  map shows only the stations along the way, flagging the cheapest for the fuel.
- **Real vehicle picker + journey cost** — choose an actual model (≈550 from the
  ADEME database, with their exact WLTP L/100 km, hybrids included) to estimate the
  trip's fuel cost from the cheapest along-route price.
- **Electric vehicles** — pick an EV (WLTP range), see the number of recharge stops
  needed and energy used, with stops placed at real chargers along the route.
- **EV charging stations** — toggle the national IRVE layer (≈224 000 points, loaded
  live by viewport), colour-coded by power (fast ≥ 50 kW).
- **Marker clustering** keeps ~10 000 stations smooth; zoom in to split clusters.
- **Click a station** for all prices, address, last-reported date, and a
  one-click *Directions* link, plus hover tooltips.

Data sources: fuel prices [`prix_des_carburants_j_7`](https://public.opendatasoft.com/explore/dataset/prix_des_carburants_j_7),
vehicle consumption [ADEME Car Labelling](https://data.ademe.fr/datasets/ademe-car-labelling),
chargers [IRVE via ODRE](https://odre.opendatasoft.com/explore/dataset/bornes-irve),
geocoding [BAN](https://adresse.data.gouv.fr/), routing [OSRM](https://project-osrm.org/),
basemap [IGN Plan v2](https://geoservices.ign.fr/).

## Run locally

```bash
pip install -r requirements.txt
python app.py            # builds, then serves http://localhost:7860
```

Open <http://localhost:7860> — on a phone (same network or behind HTTPS) you can
then "Add to Home Screen" to install it.

Other entrypoints:

```bash
python build_map.py      # build output/index.html once, no server
python scheduler.py      # rebuild twice daily without serving (for static hosting)
python scheduler.py --once
```

> **PWA note:** install/offline features need a secure context — i.e. `https://`
> or `http://localhost`. Opening the file directly (`file://`) shows the map but
> not the installable app.

## Deploy to Hugging Face Spaces (Docker)

1. Create a new Space → **SDK: Docker**.
2. Push these files to the Space repo (the YAML header above configures it):

   ```bash
   git init && git add . && git commit -m "Petrol map PWA"
   git remote add origin https://huggingface.co/spaces/<user>/<space>
   git push origin main
   ```

3. The Space builds the `Dockerfile`, serves on port `7860`, and rebuilds the
   prices twice a day. The public Space URL is HTTPS, so the PWA is installable
   straight from your phone.

Run the same container anywhere else:

```bash
docker build -t petrol-map .
docker run -p 7860:7860 petrol-map      # http://localhost:7860
```

## Files

**Backend (Python, build/serve):**

| File | Purpose |
|------|---------|
| `fuel_data.py` | Fetch + normalise fuel prices (drops implausible values) |
| `car_data.py` | Fetch real vehicle models + WLTP L/100 km from ADEME |
| `build_map.py` | Inject the dataset into the HTML shell, copy static assets |
| `app.py` | Build + serve on `$PORT` + twice-daily refresh (container entrypoint) |
| `scheduler.py` | Twice-daily rebuild without serving |
| `generate_icons.py` | One-off PWA icon generator (needs Pillow; not used at runtime) |
| `Dockerfile` | Container image for Hugging Face Spaces / anywhere |

**Frontend (served from `static/`, separated for clean architecture):**

| File | Purpose |
|------|---------|
| `map_template.html` | Slim HTML shell; injects the build's data as `window.APP_DATA` |
| `static/app.js` | All map logic (one IIFE, sectioned by concern) |
| `static/styles.css` | All styles |
| `static/sw.js` | Service worker (offline + installable) |
| `static/manifest.webmanifest`, icons | PWA metadata |
| `output/` | Generated site (`index.html` + copied static assets + `stations.json`) |

## How the refresh works

The upstream dataset is updated continuously by the French administration.
`app.py` re-fetches the full feed and regenerates the page twice daily (07:00 and
19:00, container local time — set `TZ`), so the map always reflects a recent
snapshot. Change `REFRESH_TIMES` to refresh on a different cadence.

## License

Licensed under the [Apache License, Version 2.0](LICENSE) — see the `LICENSE`
file for the full text. © 2026 Samuel Adone.

Open data used by this project remains under its own licences (Licence Ouverte /
Etalab for the French government feeds).