- trajet.py : fonction couts() pure (calcul des coûts sans appel réseau) - app.py : mémorisation distance/durée/péage + adresses dans le CSV ; auto-calcul best-effort après ajout (scrape simple et en lot) ; api_distance recalcule en local si adresses inchangées, complet sinon - index.html : affichage auto du trajet en cache à l'ouverture du prospect
120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
"""Distance routière et coût carburant entre deux adresses (OpenStreetMap : Nominatim + OSRM).
|
||
|
||
Aucune dépendance ni clé d'API : on utilise les services publics gratuits d'OpenStreetMap
|
||
via la bibliothèque standard. Usage personnel et ponctuel (un prospect à la fois).
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import urllib.parse
|
||
import urllib.request
|
||
|
||
NOMINATIM = "https://nominatim.openstreetmap.org/search"
|
||
OSRM = "https://router.project-osrm.org/route/v1/driving"
|
||
# Nominatim impose un User-Agent identifiant l'application
|
||
ENTETES = {"User-Agent": "AutoMood/1.0 (outil local de prospection)"}
|
||
|
||
# Les coordonnées d'une adresse ne changent pas : on évite de re-géocoder (et on
|
||
# reste poli envers Nominatim, dont l'usage est limité à ~1 requête/seconde).
|
||
_cache_geo = {}
|
||
|
||
|
||
class ErreurTrajet(Exception):
|
||
pass
|
||
|
||
|
||
def _http_json(url):
|
||
requete = urllib.request.Request(url, headers=ENTETES)
|
||
try:
|
||
with urllib.request.urlopen(requete, timeout=20) as reponse:
|
||
return json.load(reponse)
|
||
except Exception as e:
|
||
raise ErreurTrajet(f"Service de cartographie injoignable : {e}")
|
||
|
||
|
||
def geocoder(adresse):
|
||
"""Adresse texte -> (longitude, latitude). Lève ErreurTrajet si introuvable."""
|
||
adresse = (adresse or "").strip()
|
||
if not adresse:
|
||
raise ErreurTrajet("Adresse vide.")
|
||
if adresse in _cache_geo:
|
||
return _cache_geo[adresse]
|
||
params = urllib.parse.urlencode({
|
||
"q": adresse, "format": "json", "limit": 1, "countrycodes": "fr",
|
||
})
|
||
donnees = _http_json(f"{NOMINATIM}?{params}")
|
||
if not donnees:
|
||
raise ErreurTrajet(f"Adresse introuvable : « {adresse} ».")
|
||
coord = (float(donnees[0]["lon"]), float(donnees[0]["lat"]))
|
||
_cache_geo[adresse] = coord
|
||
return coord
|
||
|
||
|
||
def _est_autoroute(ref):
|
||
"""Vrai si le numéro de route est une autoroute française (« A 11 », « A8;E60 »...).
|
||
|
||
Heuristique : la plupart des autoroutes « A » sont à péage. Certaines sont
|
||
gratuites (rocades, sections urbaines) : l'estimation reste donc approximative.
|
||
"""
|
||
return any(re.match(r"A\s?\d", t.strip(), re.I) for t in re.split(r"[;,]", ref or ""))
|
||
|
||
|
||
def itineraire(depart, arrivee):
|
||
"""(lon, lat) x2 -> (distance_km, duree_min, km_autoroute) du trajet le plus court."""
|
||
(lon1, lat1), (lon2, lat2) = depart, arrivee
|
||
donnees = _http_json(f"{OSRM}/{lon1},{lat1};{lon2},{lat2}?overview=false&steps=true")
|
||
if donnees.get("code") != "Ok" or not donnees.get("routes"):
|
||
raise ErreurTrajet("Aucun itinéraire routier trouvé entre ces deux adresses.")
|
||
route = donnees["routes"][0]
|
||
metres_autoroute = sum(
|
||
etape.get("distance", 0)
|
||
for jambe in route.get("legs", [])
|
||
for etape in jambe.get("steps", [])
|
||
if _est_autoroute(etape.get("ref"))
|
||
)
|
||
return route["distance"] / 1000.0, route["duration"] / 60.0, metres_autoroute / 1000.0
|
||
|
||
|
||
def couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km=0.0):
|
||
"""Données géographiques + tarifs -> dict complet (coûts aller simple et aller-retour).
|
||
|
||
Calcul purement local (aucun appel réseau) : permet de réappliquer de nouveaux prix
|
||
sur une distance déjà connue sans re-géocoder ni re-router.
|
||
|
||
Le péage est estimé : km d'autoroute du trajet × `cout_peage_km` (tarif moyen
|
||
paramétrable, en €/km). Mettre 0 pour ne pas compter de péage.
|
||
"""
|
||
carburant = distance_km / 100.0 * conso_l_100km * prix_carburant
|
||
peage = km_peage * cout_peage_km
|
||
cout_aller = carburant + peage
|
||
return {
|
||
"distance_km": round(distance_km, 1),
|
||
"distance_aller_retour_km": round(distance_km * 2, 1),
|
||
"duree_min": round(duree_min),
|
||
"km_peage": round(km_peage, 1),
|
||
"carburant_aller": round(carburant, 2),
|
||
"carburant_aller_retour": round(carburant * 2, 2),
|
||
"peage_aller": round(peage, 2),
|
||
"peage_aller_retour": round(peage * 2, 2),
|
||
"cout_aller": round(cout_aller, 2),
|
||
"cout_aller_retour": round(cout_aller * 2, 2),
|
||
}
|
||
|
||
|
||
def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cout_peage_km=0.0):
|
||
"""Estime distance, durée, coût carburant et péage (aller simple et aller-retour).
|
||
|
||
Fait les appels réseau (Nominatim + OSRM) puis délègue le calcul des coûts à `couts`.
|
||
"""
|
||
distance_km, duree_min, km_peage = itineraire(
|
||
geocoder(adresse_depart), geocoder(adresse_arrivee))
|
||
return couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
if len(sys.argv) != 3:
|
||
print("Usage : python trajet.py <adresse-depart> <adresse-arrivee>")
|
||
sys.exit(1)
|
||
print(json.dumps(calculer(sys.argv[1], sys.argv[2], 6.5, 1.90), ensure_ascii=False, indent=2))
|