parking-land/map_simple.py

442 lines
No EOL
17 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Version simplifiée du script pour générer une carte visuelle d'une ville à partir de son ID OpenStreetMap.
Utilise l'API Overpass et Folium pour créer une carte interactive.
"""
import os
import sys
import argparse
import json
import requests
import folium
from folium.plugins import MarkerCluster
import logging
from datetime import datetime
# Configuration de la journalisation
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_city_data_from_osm(osm_id):
"""
Récupère les données d'une ville à partir de son ID OpenStreetMap via l'API Overpass.
Args:
osm_id (int): L'identifiant OpenStreetMap de la ville
Returns:
dict: Un dictionnaire contenant les données de la ville
"""
try:
logger.info(f"Récupération des données pour l'ID OSM: {osm_id}")
# Essayer d'abord en tant que relation
city_data = _try_get_osm_data(osm_id, "relation")
# Si ça ne fonctionne pas, essayer en tant que way (chemin)
if not city_data:
logger.info(f"Essai en tant que chemin (way) pour l'ID OSM: {osm_id}")
city_data = _try_get_osm_data(osm_id, "way")
# Si ça ne fonctionne toujours pas, essayer en tant que node (nœud)
if not city_data:
logger.info(f"Essai en tant que nœud (node) pour l'ID OSM: {osm_id}")
city_data = _try_get_osm_data(osm_id, "node")
# Si aucune méthode ne fonctionne
if not city_data:
logger.error(f"Aucune donnée trouvée pour l'ID OSM: {osm_id}")
return None
return city_data
except Exception as e:
logger.error(f"Erreur lors de la récupération des données: {str(e)}")
return None
def _try_get_osm_data(osm_id, element_type):
"""
Essaie de récupérer les données OSM pour un type d'élément spécifique.
Args:
osm_id (int): L'identifiant OpenStreetMap
element_type (str): Le type d'élément ('relation', 'way', 'node')
Returns:
dict: Un dictionnaire contenant les données ou None en cas d'échec
"""
try:
# Requête pour obtenir les informations de base
if element_type == "relation":
# Pour les relations (villes, quartiers, etc.)
query = f"""
[out:json];
relation({osm_id});
out body;
>;
out skel qt;
"""
elif element_type == "way":
# Pour les chemins (routes, contours, etc.)
query = f"""
[out:json];
way({osm_id});
out body;
>;
out skel qt;
"""
elif element_type == "node":
# Pour les nœuds (points d'intérêt, etc.)
query = f"""
[out:json];
node({osm_id});
out body;
"""
else:
logger.error(f"Type d'élément non reconnu: {element_type}")
return None
# Envoyer la requête à l'API Overpass
overpass_url = "https://overpass-api.de/api/interpreter"
response = requests.post(overpass_url, data={"data": query})
if response.status_code != 200:
logger.error(f"Erreur lors de la requête Overpass: {response.status_code}")
return None
data = response.json()
if not data.get('elements'):
logger.warning(f"Aucune donnée trouvée pour l'ID OSM {osm_id} en tant que {element_type}")
return None
# Extraire les informations de base
element_info = next((e for e in data['elements'] if e.get('id') == osm_id), None)
if not element_info:
logger.warning(f"Élément {element_type} non trouvé pour l'ID OSM: {osm_id}")
return None
# Récupérer le nom et les coordonnées
name = element_info.get('tags', {}).get('name', f"Lieu_{osm_id}")
logger.info(f"Lieu identifié: {name}")
# Déterminer les coordonnées centrales
if element_type == "node":
# Pour un nœud, utiliser directement ses coordonnées
center_lat = element_info.get('lat')
center_lon = element_info.get('lon')
center = (center_lat, center_lon)
# Pour un nœud, définir une zone autour
bbox = (center_lat - 0.01, center_lon - 0.01, center_lat + 0.01, center_lon + 0.01)
else:
# Pour les relations et chemins, calculer à partir des nœuds
nodes = [e for e in data['elements'] if e.get('type') == 'node']
if not nodes:
logger.error(f"Aucun nœud trouvé pour {element_type} {osm_id}")
return None
lats = [n.get('lat', 0) for n in nodes if 'lat' in n]
lons = [n.get('lon', 0) for n in nodes if 'lon' in n]
if not lats or not lons:
logger.error(f"Coordonnées manquantes pour {element_type} {osm_id}")
return None
center_lat = sum(lats) / len(lats)
center_lon = sum(lons) / len(lons)
center = (center_lat, center_lon)
# Calculer la boîte englobante
min_lat, max_lat = min(lats), max(lats)
min_lon, max_lon = min(lons), max(lons)
bbox = (min_lat, min_lon, max_lat, max_lon)
# Récupérer les éléments dans la zone (routes, bâtiments, parkings)
# Utiliser l'ID de la relation/way pour limiter aux éléments à l'intérieur de la ville
if element_type == "relation":
# Pour les relations (villes), il faut d'abord obtenir l'ID d'area correspondant
# L'ID d'area est calculé comme 3600000000 + ID de la relation
area_id = 3600000000 + osm_id
area_query = f"""
[out:json];
area({area_id})->.searchArea;
(
way[highway](area.searchArea);
way[building](area.searchArea);
way[amenity=parking](area.searchArea);
);
out body;
>;
out skel qt;
"""
elif element_type == "way":
# Pour les ways (contours), il faut d'abord obtenir l'ID d'area correspondant
# L'ID d'area est calculé comme 2400000000 + ID du way
area_id = 2400000000 + osm_id
area_query = f"""
[out:json];
area({area_id})->.searchArea;
(
way[highway](area.searchArea);
way[building](area.searchArea);
way[amenity=parking](area.searchArea);
);
out body;
>;
out skel qt;
"""
else:
# Pour les nodes ou si les méthodes précédentes échouent, utiliser la boîte englobante
# mais avec un rayon plus petit autour du point
area_query = f"""
[out:json];
(
way[highway]({bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]});
way[building]({bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]});
way[amenity=parking]({bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]});
);
out body;
>;
out skel qt;
"""
logger.warning(f"Utilisation d'une boîte englobante pour {element_type} {osm_id} (moins précis)")
logger.info(f"Récupération des routes, bâtiments et parkings pour {name}...")
response = requests.post(overpass_url, data={"data": area_query})
if response.status_code != 200:
logger.error(f"Erreur lors de la requête Overpass pour les éléments: {response.status_code}")
return None
elements_data = response.json()
return {
'city_name': name,
'center': center,
'bbox': bbox,
'city_data': data,
'elements_data': elements_data,
'element_type': element_type
}
except Exception as e:
logger.error(f"Erreur lors de la récupération des données pour {element_type} {osm_id}: {str(e)}")
return None
def create_folium_map(city_data, output_path=None):
"""
Crée une carte interactive avec Folium.
Args:
city_data (dict): Dictionnaire contenant les données de la ville
output_path (str, optional): Chemin où sauvegarder l'image HTML. Si None, utilise le nom de la ville.
Returns:
str: Chemin vers le fichier HTML généré
"""
if not city_data:
logger.error("Aucune donnée de ville fournie pour générer la carte")
return None
try:
city_name = city_data['city_name']
center = city_data['center']
elements_data = city_data['elements_data']
element_type = city_data.get('element_type', 'unknown')
# Créer une carte centrée sur la ville avec fond Stamen Terrain
m = folium.Map(
location=center,
zoom_start=14,
tiles='Stamen Terrain',
attr='Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
)
# Ajouter d'autres options de fonds de carte
folium.TileLayer(
'Stamen Toner',
attr='Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
).add_to(m)
folium.TileLayer(
'Stamen Watercolor',
attr='Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
).add_to(m)
folium.TileLayer(
'OpenStreetMap',
attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
).add_to(m)
folium.LayerControl().add_to(m)
# Ajouter un titre
title_html = f'''
<h3 align="center" style="font-size:16px"><b>Carte de {city_name}</b></h3>
<p align="center" style="font-size:12px">ID OpenStreetMap: {city_data.get('city_data', {}).get('elements', [{}])[0].get('id', 'N/A')} (Type: {element_type})</p>
'''
m.get_root().html.add_child(folium.Element(title_html))
# Ajouter un marqueur pour le centre
folium.Marker(
location=center,
popup=f"Centre de {city_name}",
icon=folium.Icon(color='red', icon='info-sign')
).add_to(m)
# Extraire les nœuds pour construire les géométries
nodes = {n['id']: (n['lat'], n['lon']) for n in elements_data['elements'] if n['type'] == 'node'}
# Compter les éléments pour les statistiques
highways_count = 0
buildings_count = 0
parkings_count = 0
# Traiter les routes, bâtiments et parkings
for element in elements_data['elements']:
if element['type'] == 'way' and 'tags' in element:
# Récupérer les coordonnées des nœuds
coords = []
for node_id in element['nodes']:
if node_id in nodes:
coords.append(nodes[node_id])
if not coords:
continue
# Déterminer le type d'élément
if 'highway' in element['tags']:
# C'est une route
highways_count += 1
folium.PolyLine(
coords,
color='#555555',
weight=2,
opacity=0.7,
tooltip=element['tags'].get('name', 'Route')
).add_to(m)
elif 'building' in element['tags']:
# C'est un bâtiment
buildings_count += 1
folium.Polygon(
coords,
color='#777777',
fill=True,
fill_color='#777777',
fill_opacity=0.7,
tooltip=element['tags'].get('name', 'Bâtiment')
).add_to(m)
elif element['tags'].get('amenity') == 'parking':
# C'est un parking
parkings_count += 1
folium.Polygon(
coords,
color='#999999',
fill=True,
fill_color='#999999',
fill_opacity=0.7,
tooltip=element['tags'].get('name', 'Parking')
).add_to(m)
# Ajouter une légende avec les statistiques
legend_html = f'''
<div style="position: fixed;
bottom: 50px; right: 50px; width: 200px; height: 130px;
border:2px solid grey; z-index:9999; font-size:12px;
background-color: white; padding: 10px;
border-radius: 5px;">
<p><b>Statistiques</b></p>
<p>Routes: {highways_count}</p>
<p>Bâtiments: {buildings_count}</p>
<p>Parkings: {parkings_count}</p>
</div>
'''
m.get_root().html.add_child(folium.Element(legend_html))
# Définir le chemin de sortie
if not output_path:
output_path = f"{city_name.replace(' ', '_')}_map.html"
# Sauvegarder la carte
logger.info(f"Sauvegarde de la carte dans: {output_path}")
m.save(output_path)
# Générer également une image statique si possible
try:
import selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
logger.info("Génération d'une image statique de la carte...")
# Configurer Selenium pour prendre une capture d'écran
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=chrome_options)
driver.set_window_size(1200, 800)
# Charger la carte HTML
driver.get(f"file://{os.path.abspath(output_path)}")
# Attendre que la carte se charge
driver.implicitly_wait(5)
# Prendre une capture d'écran
png_path = output_path.replace('.html', '.png')
driver.save_screenshot(png_path)
driver.quit()
logger.info(f"Image statique générée: {png_path}")
except Exception as e:
logger.warning(f"Impossible de générer une image statique: {str(e)}")
return output_path
except Exception as e:
logger.error(f"Erreur lors de la création de la carte: {str(e)}")
return None
def main():
"""
Fonction principale qui traite les arguments de ligne de commande
et génère la carte de la ville.
"""
parser = argparse.ArgumentParser(description='Génère une carte interactive d\'une ville à partir de son ID OpenStreetMap.')
parser.add_argument('osm_id', type=int, help='ID OpenStreetMap de la ville')
parser.add_argument('-o', '--output', type=str, help='Chemin où sauvegarder la carte (par défaut: nom_ville_map.html)')
parser.add_argument('-v', '--verbose', action='store_true', help='Afficher les messages de débogage')
args = parser.parse_args()
# Configurer le niveau de journalisation
if args.verbose:
logger.setLevel(logging.DEBUG)
# Récupérer les données de la ville
city_data = get_city_data_from_osm(args.osm_id)
if not city_data:
logger.error(f"Impossible de récupérer les données pour l'ID OSM: {args.osm_id}")
sys.exit(1)
# Créer la carte
output_path = create_folium_map(city_data, args.output)
if output_path:
logger.info(f"Carte générée avec succès: {output_path}")
else:
logger.error("Échec de la génération de la carte")
sys.exit(1)
if __name__ == "__main__":
main()