Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
Tykayn 2025-08-02 11:00:14 +02:00 committed by tykayn
commit b84f1e9237
17 changed files with 4262 additions and 0 deletions

10
counting_osm_objects/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
osm_data/*pbf
osm_data/*geojson
polygons/*.poly
test_data/*
test_temp/*
test_results/*
osm_config.txt
__pycache__
secrets.sh
cookie.txt

View file

@ -0,0 +1,151 @@
# Counting OSM Objects
Ce répertoire contient des scripts pour compter et analyser les objets OpenStreetMap dans différentes zones
administratives françaises.
Pour fonctionner vous aurez besoin du fichier historisé de la france, pour cela connectez vous à geofabrik avec votre
compte osm (oui c'est relou).
## Scripts disponibles
activez le venv python et les dépendances
```shell
python -m venv bin/venv activate
source bin/venv/bin/activate
pip install plotly pandas
```
```shell
# extraire un historique pour une ville à partir de l'historique de france et du polygone de la ville
# ici pour paris on peut utiliser l'historique de l'ile de france
wget https://osm-commerces.cipherbliss.com/admin/export_csv
py get_all_polys.py # ce qui utilise l'export csv des villes de osm mon commerce
osmium extract -p polygons/commune_75056.poly -H osm_data/ile-de-france-internal.osh.pbf -s complete_ways -O -o commune_75056.osh.pbf
```
```shell
# générer les historiques de la commune 76216 pour tous ses thèmes et tous ses graphes
py loop_thematics_history_in_zone_to_counts.py --input osm_data/commune_76216.osh.pbf --output-dir test_results --temp-dir test_temp --max-dates 100 --poly polygons/commune_76216.poly
```
```shell
# générer un graphe pour un thème dans une ville
py generate_graph.py --insee 91111 test_results/commune_75056_building.csv
Graphique sauvegardé: test_results/commune_75056_building_monthly_graph.png
Graphique de complétion sauvegardé: test_results/commune_75056_building_monthly_graph_completion.png
Graphiques générés avec succès!
```
### historize_zone.py
Ce script principal permet de lancer l'analyse historique d'une ville dans OpenStreetMap. Il:
1. Demande à l'utilisateur quelle ville il souhaite traiter
2. Trouve le code INSEE de la ville demandée
3. Vérifie si le polygone de la ville existe, sinon le récupère
4. Traite les données historiques OSM pour cette ville en utilisant loop_thematics_history_in_zone_to_counts.py
#### Utilisation
```bash
python historize_zone.py [--input fichier_historique.osh.pbf]
```
Si le fichier d'historique n'est pas spécifié, le script utilisera par défaut le fichier
`osm_data/france-internal.osh.pbf`.
#### Exemples
Lancer l'analyse avec le fichier d'historique par défaut:
```bash
python historize_zone.py
```
Lancer l'analyse avec un fichier d'historique spécifique:
```bash
python historize_zone.py --input osm_data/ile-de-france-internal.osh.pbf
```
### loop_thematics_history_in_zone_to_counts.py
Ce script compte les objets OSM par thématique sur une zone donnée à différentes dates. Il:
1. Filtre les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
2. Compte les objets correspondant à chaque thématique à chaque date
3. Calcule le pourcentage de complétion des attributs importants pour chaque thème
4. Sauvegarde les résultats dans des fichiers CSV
5. Génère des graphiques montrant l'évolution dans le temps
#### Utilisation
```bash
python loop_thematics_history_in_zone_to_counts.py --input fichier.osh.pbf --poly polygons/commune_XXXXX.poly
```
Ce script est généralement appelé par historize_zone.py et ne nécessite pas d'être exécuté directement.
### get_poly.py
Ce script permet de récupérer le polygone d'une commune française à partir de son code INSEE. Il interroge l'API
Overpass Turbo pour obtenir les limites administratives de la commune et sauvegarde le polygone dans un fichier au
format .poly (compatible avec Osmosis).
#### Utilisation
```bash
python get_poly.py [code_insee]
```
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
#### Exemples
Récupérer le polygone de la commune d'Étampes (code INSEE 91111) :
```bash
python get_poly.py 91111
```
Le polygone sera sauvegardé dans le fichier `polygons/commune_91111.poly`.
#### Format de sortie
Le fichier de sortie est au format .poly, qui est utilisé par Osmosis et d'autres outils OpenStreetMap. Il contient :
- Le nom de la commune
- Un numéro de section
- Les coordonnées des points du polygone (longitude, latitude)
- Des marqueurs "END" pour fermer le polygone et le fichier
Exemple de contenu :
```
commune_91111
1
2.1326337 48.6556426
2.1323684 48.6554398
...
2.1326337 48.6556426
END
END
```
## Dépendances
- Python 3.6+
- Modules Python : argparse, urllib, json
- Pour compare_osm_objects.sh : PostgreSQL, curl, jq
## Installation
Aucune installation spécifique n'est nécessaire pour ces scripts. Assurez-vous simplement que les dépendances sont
installées.
## Licence
Ces scripts sont distribués sous la même licence que le projet Osmose-Backend.

View file

@ -0,0 +1,71 @@
# Mise à jour du fichier historisé france internal
Ce document explique comment utiliser le script `update.py` pour mettre à jour le fichier historisé france internal (`france-internal.osh.pbf`) en utilisant l'outil `osmupdate`.
## Prérequis
- Python 3.6 ou supérieur
- L'outil `osmupdate` installé sur le système
- Un fichier `france-internal.osh.pbf` existant dans le répertoire `osm_data`
## Utilisation
Le script `update.py` peut être exécuté de deux façons :
```bash
# Méthode 1 : Exécution directe (le script est exécutable)
./update.py
# Méthode 2 : Exécution via Python
python3 update.py
```
### Options
- `--verbose` ou `-v` : Affiche la sortie des commandes en temps réel, ce qui permet de suivre la progression de la mise à jour.
Exemple :
```bash
./update.py --verbose
```
## Fonctionnement
Le script effectue les opérations suivantes :
1. Vérifie si le fichier `france-internal.osh.pbf` existe dans le répertoire `osm_data`.
2. Crée un répertoire temporaire `update_temp` s'il n'existe pas déjà.
3. Exécute la commande `osmupdate` pour mettre à jour le fichier avec les dernières modifications d'OpenStreetMap.
4. Crée une sauvegarde de l'ancien fichier avec l'extension `.bak`.
5. Remplace l'ancien fichier par le nouveau fichier mis à jour.
6. Affiche des informations sur la durée de la mise à jour.
## Logs
Le script utilise le module `logging` de Python pour enregistrer les informations importantes :
- Heure de début et de fin de la mise à jour
- Durée totale de la mise à jour
- Erreurs éventuelles lors de la mise à jour
## Intégration avec d'autres scripts
Le fichier `france-internal.osh.pbf` est utilisé comme fichier d'entrée par défaut dans le script `historize_zone.py`. Après avoir mis à jour ce fichier avec `update.py`, les analyses historiques effectuées par `historize_zone.py` utiliseront automatiquement les données les plus récentes.
## Automatisation
Pour automatiser la mise à jour régulière du fichier, vous pouvez ajouter une tâche cron :
```bash
# Exemple : mise à jour quotidienne à 3h du matin
0 3 * * * cd /chemin/vers/osm-commerce-sf/counting_osm_objects && ./update.py >> update.log 2>&1
```
## Dépannage
Si vous rencontrez des erreurs lors de l'exécution du script :
1. Vérifiez que `osmupdate` est correctement installé et accessible dans le PATH.
2. Assurez-vous que le fichier `france-internal.osh.pbf` existe dans le répertoire `osm_data`.
3. Vérifiez les permissions des répertoires `osm_data` et `update_temp`.
4. Consultez les logs pour plus d'informations sur l'erreur.

1050
counting_osm_objects/counting.sh Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
# on crée une page web présentant un tableau de bord pour la zone donnée
# le dashboard contient le nom de la ville et des graphiques pour l'évolution de chaque thématique

View file

@ -0,0 +1,448 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer un graphique montrant l'évolution du nombre d'objets OSM
à partir d'un fichier CSV
"""
import sys
import os
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
import argparse
import csv
def get_city_name(insee_code):
"""
Récupère le nom de la ville à partir du code INSEE.
Args:
insee_code: Code INSEE de la commune
Returns:
Nom de la ville ou None si non trouvé
"""
try:
csv_path = os.path.join(os.path.dirname(__file__), "osm-commerces-villes-export.csv")
if os.path.exists(csv_path):
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if row.get('zone') == insee_code:
return row.get('name')
return None
except Exception as e:
print(f"Erreur lors de la récupération du nom de la ville: {e}")
return None
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Génère un graphique à partir des données CSV d'objets OSM."
)
parser.add_argument(
"csv_file", help="Chemin vers le fichier CSV contenant les données"
)
parser.add_argument(
"--output", "-o", help="Chemin de sortie pour le graphique (PNG)", default=None
)
parser.add_argument(
"--insee", "-i", help="Code INSEE de la commune à analyser", default=None
)
parser.add_argument(
"--period",
"-p",
help="Période à analyser (annual, monthly, daily)",
default="monthly",
)
return parser.parse_args()
def load_data(csv_file, insee_code=None, period="monthly"):
"""
Charge les données depuis le fichier CSV.
Args:
csv_file: Chemin vers le fichier CSV
insee_code: Code INSEE de la commune à filtrer (optionnel)
period: Période à analyser (annual, monthly, daily)
Returns:
DataFrame pandas contenant les données filtrées
"""
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
try:
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
except TypeError: # Pour les versions plus récentes de pandas
df = pd.read_csv(csv_file, on_bad_lines="skip")
# Vérifier si le CSV a la structure attendue
if "date" in df.columns:
# Format de CSV avec colonne 'date' directement
try:
df["date"] = pd.to_datetime(df["date"])
except:
# Si la conversion échoue, essayer différents formats
try:
if df["date"].iloc[0].count("-") == 2: # Format YYYY-MM-DD
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")
elif df["date"].iloc[0].count("-") == 1: # Format YYYY-MM
df["date"] = pd.to_datetime(df["date"], format="%Y-%m")
else:
df["date"] = pd.to_datetime(df["date"])
except:
print("Erreur: Impossible de convertir la colonne 'date'.")
sys.exit(1)
elif "periode" in df.columns:
# Ancien format avec colonne 'periode'
# Filtrer par période
df = df[df["periode"] == period]
# Filtrer par code INSEE si spécifié
if insee_code and "code_insee" in df.columns:
df = df[df["code_insee"] == insee_code]
# Convertir les dates en objets datetime
if period == "annual" and "annee" in df.columns:
df["date"] = pd.to_datetime(df["annee"].astype(str), format="%Y")
elif period == "monthly":
# Vérifier si la première colonne contient déjà un format de date mensuel (YYYY-MM)
first_col = df.columns[0]
first_val = str(df.iloc[0, 0]) if not df.empty else ""
if first_col == "annee_mois" or (len(first_val) >= 7 and "-" in first_val):
# Si la première colonne est 'annee_mois' ou contient une date au format YYYY-MM
df["date"] = pd.to_datetime(df.iloc[:, 0].astype(str), format="%Y-%m")
elif "annee" in df.columns:
# Sinon, utiliser la colonne 'annee' et ajouter un mois fictif (janvier)
df["date"] = pd.to_datetime(
df["annee"].astype(str) + "-01", format="%Y-%m"
)
elif period == "daily":
# Vérifier si la première colonne contient déjà un format de date quotidien (YYYY-MM-DD)
first_col = df.columns[0]
first_val = str(df.iloc[0, 0]) if not df.empty else ""
if first_col == "jour" or (
len(first_val) >= 10 and first_val.count("-") == 2
):
# Si la première colonne est 'jour' ou contient une date au format YYYY-MM-DD
df["date"] = pd.to_datetime(
df.iloc[:, 0].astype(str), format="%Y-%m-%d"
)
elif "annee" in df.columns:
# Sinon, utiliser la colonne 'annee' et ajouter un jour fictif (1er janvier)
df["date"] = pd.to_datetime(
df["annee"].astype(str) + "-01-01", format="%Y-%m-%d"
)
else:
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
try:
df["date"] = pd.to_datetime(df.iloc[:, 0])
except:
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
sys.exit(1)
# Filtrer par code INSEE si spécifié et si la colonne 'zone' contient des codes INSEE
if insee_code and "zone" in df.columns and not "code_insee" in df.columns:
# Vérifier si la zone contient le code INSEE
if any(
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
):
df = df[df["zone"].str.endswith(insee_code)]
# Trier par date
df = df.sort_values("date")
return df
def generate_graph(df, output_path=None):
"""
Génère un graphique montrant l'évolution du nombre d'objets dans le temps.
Args:
df: DataFrame pandas contenant les données
output_path: Chemin de sortie pour le graphique (optionnel)
"""
# Créer une figure avec une taille adaptée
plt.figure(figsize=(12, 8))
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
type_column = "type_objet" if "type_objet" in df.columns else "theme"
label_objet = "objet"
# Obtenir la liste des types d'objets uniques
if type_column in df.columns:
object_types = df[type_column].unique()
# Créer un graphique pour chaque type d'objet
for obj_type in object_types:
# Filtrer les données pour ce type d'objet
obj_data = df[df[type_column] == obj_type]
# Filtrer les valeurs nulles
obj_data_filtered = obj_data[obj_data["nombre_total"].notna()]
if not obj_data_filtered.empty:
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
line, = plt.plot(
obj_data_filtered["date"],
obj_data_filtered["nombre_total"],
linestyle="-",
label=f"{obj_type}",
)
label_objet = obj_type
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
obj_data_filtered["date"],
obj_data_filtered["nombre_total"],
alpha=0.3,
color=line.get_color()
)
else:
# Si aucune colonne de type n'est trouvée, tracer simplement le nombre total
# Filtrer les valeurs nulles
df_filtered = df[df["nombre_total"].notna()]
if not df_filtered.empty:
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
line, = plt.plot(
df_filtered["date"],
df_filtered["nombre_total"],
linestyle="-",
label="",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
df_filtered["date"],
df_filtered["nombre_total"],
alpha=0.3,
color=line.get_color()
)
# Configurer les axes et les légendes
plt.xlabel("Date")
plt.ylabel("Nombre d'objets")
plt.grid(True, linestyle="--", alpha=0.7)
# Formater l'axe des x pour afficher les dates correctement
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.gcf().autofmt_xdate()
# Ajouter une légende
plt.legend()
zone_label = ""
city_name = None
# Ajouter des informations sur la commune
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
insee_code = df["code_insee"].iloc[0]
city_name = get_city_name(insee_code)
if city_name:
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
zone_label = f"{insee_code} - {city_name}"
else:
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
zone_label = insee_code
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
zone = df["zone"].iloc[0]
city_name = get_city_name(zone)
if city_name:
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
zone_label = f"{zone} - {city_name}"
else:
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
zone_label = zone
plt.title(f"{zone_label} : {label_objet} dans le temps")
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
# Ajuster la mise en page
plt.tight_layout()
# Sauvegarder ou afficher le graphique
if output_path:
plt.savefig(output_path, dpi=300, bbox_inches="tight")
print(f"Graphique sauvegardé: {output_path}")
else:
plt.show()
def generate_completion_graph(df, output_path=None):
"""
Génère un graphique montrant l'évolution du taux de complétion des attributs dans le temps.
Args:
df: DataFrame pandas contenant les données
output_path: Chemin de sortie pour le graphique (optionnel)
"""
# Vérifier si la colonne de pourcentage de complétion existe
if "pourcentage_completion" not in df.columns:
print(
"Avertissement: La colonne 'pourcentage_completion' n'existe pas dans le CSV. Le graphique de complétion ne sera pas généré."
)
return
# Créer une figure avec une taille adaptée
plt.figure(figsize=(12, 8))
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
type_column = "type_objet" if "type_objet" in df.columns else "theme"
# Obtenir la liste des types d'objets uniques
if type_column in df.columns:
object_types = df[type_column].unique()
# Créer un graphique pour chaque type d'objet
for obj_type in object_types:
# Filtrer les données pour ce type d'objet
obj_data = df[df[type_column] == obj_type]
# Filtrer les valeurs nulles
obj_data_filtered = obj_data[obj_data["pourcentage_completion"].notna()]
if not obj_data_filtered.empty:
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
line, = plt.plot(
obj_data_filtered["date"],
obj_data_filtered["pourcentage_completion"],
linestyle="-",
label=f"{obj_type} - Complétion (%)",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
obj_data_filtered["date"],
obj_data_filtered["pourcentage_completion"],
alpha=0.3,
color=line.get_color()
)
else:
# Si aucune colonne de type n'est trouvée, tracer simplement le taux de complétion global
# Filtrer les valeurs nulles
df_filtered = df[df["pourcentage_completion"].notna()]
if not df_filtered.empty:
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
line, = plt.plot(
df_filtered["date"],
df_filtered["pourcentage_completion"],
linestyle="-",
label="Complétion (%)",
)
# Ajouter un remplissage pastel sous la courbe
plt.fill_between(
df_filtered["date"],
df_filtered["pourcentage_completion"],
alpha=0.3,
color=line.get_color()
)
# Configurer les axes et les légendes
plt.xlabel("Date")
plt.ylabel("Complétion (%)")
plt.title("Évolution du taux de complétion dans le temps")
plt.grid(True, linestyle="--", alpha=0.7)
# Définir les limites de l'axe y entre 0 et 100%
plt.ylim(0, 100)
# Formater l'axe des x pour afficher les dates correctement
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
plt.gcf().autofmt_xdate()
# Ajouter une légende
plt.legend()
# Ajouter des informations sur la commune
zone_label = ""
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
insee_code = df["code_insee"].iloc[0]
city_name = get_city_name(insee_code)
if city_name:
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
zone_label = f"{insee_code} - {city_name}"
else:
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
zone_label = insee_code
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
zone = df["zone"].iloc[0]
city_name = get_city_name(zone)
if city_name:
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
zone_label = f"{zone} - {city_name}"
else:
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
zone_label = zone
# Mettre à jour le titre avec le nom de la zone
if zone_label:
plt.title(f"{zone_label} : Évolution du taux de complétion dans le temps")
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
# Ajuster la mise en page
plt.tight_layout()
# Sauvegarder ou afficher le graphique
if output_path:
# Modifier le nom du fichier pour indiquer qu'il s'agit du taux de complétion
base, ext = os.path.splitext(output_path)
completion_path = f"{base}_completion{ext}"
plt.savefig(completion_path, dpi=300, bbox_inches="tight")
print(f"Graphique de complétion sauvegardé: {completion_path}")
else:
plt.show()
def main():
"""Fonction principale."""
# Analyser les arguments de la ligne de commande
args = parse_args()
# Vérifier que le fichier CSV existe
if not os.path.isfile(args.csv_file):
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
sys.exit(1)
# Charger les données
df = load_data(args.csv_file, args.insee, args.period)
# Vérifier qu'il y a des données
if df.empty:
print(f"Aucune donnée trouvée pour la période {args.period}.")
sys.exit(1)
# Déterminer le chemin de sortie si non spécifié
if not args.output:
# Utiliser le même nom que le fichier CSV mais avec l'extension .png
base_name = os.path.splitext(args.csv_file)[0]
output_path = f"{base_name}_{args.period}_graph.png"
else:
output_path = args.output
# Générer les graphiques
generate_graph(df, output_path)
generate_completion_graph(df, output_path)
print("Graphiques générés avec succès!")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour récupérer les polygones de toutes les communes françaises listées dans un fichier CSV.
Ce script:
1. Ouvre le fichier osm-commerces-villes-export.csv
2. Extrait les codes INSEE (colonne 'zone')
3. Pour chaque code INSEE, vérifie si le polygone existe déjà
4. Si non, utilise get_poly.py pour récupérer le polygone
Usage:
python get_all_polys.py
"""
import os
import sys
import csv
from get_poly import query_overpass_api, extract_polygon, save_polygon_to_file
# Chemin vers le fichier CSV contenant les codes INSEE
CSV_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "osm-commerces-villes-export.csv"
)
# Chemin vers le dossier où sont stockés les polygones
POLYGONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "polygons")
def ensure_polygons_dir_exists():
"""
Vérifie que le dossier 'polygons' existe, sinon le crée.
"""
os.makedirs(POLYGONS_DIR, exist_ok=True)
print(f"Dossier de polygones: {POLYGONS_DIR}")
def polygon_exists(insee_code):
"""
Vérifie si le polygone pour le code INSEE donné existe déjà.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
bool: True si le polygone existe, False sinon
"""
polygon_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
return os.path.isfile(polygon_file)
def get_polygon(insee_code):
"""
Récupère le polygone pour le code INSEE donné.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
str: Le chemin du fichier polygone créé, ou None en cas d'erreur
"""
try:
print(f"Récupération du polygone pour la commune {insee_code}...")
# Interroger l'API Overpass
data = query_overpass_api(insee_code)
# Extraire le polygone
polygon = extract_polygon(data)
# Sauvegarder le polygone dans un fichier
output_file = save_polygon_to_file(polygon, insee_code)
print(f"Polygone pour la commune {insee_code} sauvegardé dans {output_file}")
return output_file
except Exception as e:
print(f"Erreur lors de la récupération du polygone pour {insee_code}: {e}")
return None
def read_insee_codes_from_csv():
"""
Lit le fichier CSV et extrait les codes INSEE (colonne 'zone').
Returns:
list: Liste des codes INSEE
"""
insee_codes = []
try:
print(f"Lecture du fichier CSV: {CSV_FILE}")
if not os.path.isfile(CSV_FILE):
print(f"Erreur: Le fichier {CSV_FILE} n'existe pas.")
return insee_codes
with open(CSV_FILE, "r", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
if "zone" in row and row["zone"]:
insee_codes.append(row["zone"])
print(f"Nombre de codes INSEE trouvés: {len(insee_codes)}")
return insee_codes
except Exception as e:
print(f"Erreur lors de la lecture du fichier CSV: {e}")
return insee_codes
def main():
"""
Fonction principale du script.
"""
try:
# S'assurer que le dossier des polygones existe
ensure_polygons_dir_exists()
# Lire les codes INSEE depuis le fichier CSV
insee_codes = read_insee_codes_from_csv()
if not insee_codes:
print("Aucun code INSEE trouvé dans le fichier CSV.")
return 1
# Compteurs pour les statistiques
total = len(insee_codes)
existing = 0
created = 0
failed = 0
# Pour chaque code INSEE, récupérer le polygone s'il n'existe pas déjà
for i, insee_code in enumerate(insee_codes, 1):
print(f"\nTraitement de la commune {i}/{total}: {insee_code}")
if polygon_exists(insee_code):
print(f"Le polygone pour la commune {insee_code} existe déjà.")
existing += 1
continue
# Récupérer le polygone
result = get_polygon(insee_code)
if result:
created += 1
else:
failed += 1
# Afficher les statistiques
print("\nRésumé:")
print(f"Total des communes traitées: {total}")
print(f"Polygones déjà existants: {existing}")
print(f"Polygones créés avec succès: {created}")
print(f"Échecs: {failed}")
return 0 # Succès
except KeyboardInterrupt:
print("\nOpération annulée par l'utilisateur.")
return 1 # Erreur
except Exception as e:
print(f"Erreur inattendue: {e}")
return 1 # Erreur
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,287 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour récupérer le polygone d'une commune française à partir de son code INSEE.
Ce script:
1. Demande un code INSEE
2. Interroge l'API Overpass Turbo pour obtenir les limites administratives
3. Extrait le polygone de la commune
4. Sauvegarde le polygone dans un fichier
Usage:
python get_poly.py [code_insee]
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
"""
import sys
import os
import json
import urllib.parse
import urllib.request
import argparse
def get_insee_code():
"""
Récupère le code INSEE soit depuis les arguments de ligne de commande,
soit en demandant à l'utilisateur.
Returns:
str: Le code INSEE de la commune
"""
parser = argparse.ArgumentParser(
description="Récupère le polygone d'une commune à partir de son code INSEE"
)
parser.add_argument("insee", nargs="?", help="Code INSEE de la commune")
args = parser.parse_args()
if args.insee:
return args.insee
# Si le code INSEE n'est pas fourni en argument, le demander
return input("Entrez le code INSEE de la commune: ")
def query_overpass_api(insee_code):
"""
Interroge l'API Overpass pour obtenir les limites administratives d'une commune.
Args:
insee_code (str): Le code INSEE de la commune
Returns:
dict: Les données GeoJSON de la commune
"""
print(f"Récupération des limites administratives pour la commune {insee_code}...")
# Construire la requête Overpass QL pour obtenir la relation administrative
query = f"""
[out:json][timeout:60];
(
relation["boundary"="administrative"]["admin_level"="8"]["ref:INSEE"="{insee_code}"];
way(r);
node(w);
);
out geom;
"""
# Encoder la requête pour l'URL
encoded_query = urllib.parse.quote(query)
# Construire l'URL de l'API Overpass
url = f"https://overpass-api.de/api/interpreter?data={encoded_query}"
try:
# Envoyer la requête à l'API
print("Envoi de la requête à Overpass API...")
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode("utf-8"))
# Afficher des informations sur la réponse (version réduite pour production)
print(
f"Réponse reçue de l'API Overpass. Nombre d'éléments: {len(data.get('elements', []))}"
)
return data
except Exception as e:
print(f"Erreur lors de la requête à l'API Overpass: {e}")
raise RuntimeError(f"Erreur lors de la requête à l'API Overpass: {e}")
def extract_polygon(data):
"""
Extrait le polygone des données GeoJSON.
Args:
data (dict): Les données GeoJSON de la commune
Returns:
list: Liste des coordonnées du polygone
"""
print("Extraction du polygone des données...")
# Vérifier si des éléments ont été trouvés
if not data.get("elements"):
print("Aucune limite administrative trouvée pour ce code INSEE.")
raise ValueError("Aucune limite administrative trouvée pour ce code INSEE.")
try:
# Collecter tous les nœuds (points) avec leurs coordonnées
nodes = {}
for element in data["elements"]:
if element["type"] == "node":
nodes[element["id"]] = (element["lon"], element["lat"])
# Trouver les ways qui forment le contour de la commune
ways = []
for element in data["elements"]:
if element["type"] == "way":
ways.append(element)
# Si aucun way n'est trouvé, essayer d'extraire directement les coordonnées des nœuds
if not ways and nodes:
print("Aucun way trouvé. Utilisation directe des nœuds...")
polygon = list(nodes.values())
return polygon
# Trouver la relation administrative
relation = None
for element in data["elements"]:
if (
element["type"] == "relation"
and element.get("tags", {}).get("boundary") == "administrative"
):
relation = element
break
if not relation:
print("Aucune relation administrative trouvée.")
# Si nous avons des ways, nous pouvons essayer de les utiliser directement
if ways:
print("Tentative d'utilisation directe des ways...")
# Prendre le premier way comme contour
way = ways[0]
polygon = []
for node_id in way.get("nodes", []):
if node_id in nodes:
polygon.append(nodes[node_id])
return polygon
raise ValueError(
"Impossible de trouver une relation administrative ou des ways"
)
# Extraire les ways qui forment le contour extérieur de la relation
outer_ways = []
for member in relation.get("members", []):
if member.get("role") == "outer" and member.get("type") == "way":
# Trouver le way correspondant
for way in ways:
if way["id"] == member["ref"]:
outer_ways.append(way)
break
# Si aucun way extérieur n'est trouvé, utiliser tous les ways
if not outer_ways:
print("Aucun way extérieur trouvé. Utilisation de tous les ways...")
outer_ways = ways
# Construire le polygone à partir des ways extérieurs
polygon = []
for way in outer_ways:
for node_id in way.get("nodes", []):
if node_id in nodes:
polygon.append(nodes[node_id])
if not polygon:
raise ValueError("Impossible d'extraire le polygone de la relation")
print(f"Polygone extrait avec {len(polygon)} points.")
return polygon
except Exception as e:
print(f"Erreur lors de l'extraction du polygone: {e}")
raise RuntimeError(f"Erreur lors de l'extraction du polygone: {e}")
def save_polygon_to_file(polygon, insee_code):
"""
Sauvegarde le polygone dans un fichier.
Args:
polygon (list): Liste des coordonnées du polygone
insee_code (str): Le code INSEE de la commune
Returns:
str: Le chemin du fichier créé
Raises:
ValueError: Si le polygone est vide ou invalide
IOError: Si une erreur survient lors de l'écriture du fichier
"""
if not polygon:
raise ValueError("Le polygone est vide")
try:
# Créer le répertoire de sortie s'il n'existe pas
output_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "polygons"
)
os.makedirs(output_dir, exist_ok=True)
# Définir le nom du fichier de sortie
output_file = os.path.join(output_dir, f"commune_{insee_code}.poly")
print(f"Sauvegarde du polygone dans le fichier {output_file}...")
# Écrire le polygone dans le fichier au format .poly (format utilisé par Osmosis)
with open(output_file, "w") as f:
f.write(f"commune_{insee_code}\n")
f.write("1\n") # Numéro de section
# Écrire les coordonnées
for i, (lon, lat) in enumerate(polygon):
f.write(f" {lon:.7f} {lat:.7f}\n")
# Fermer le polygone en répétant le premier point
if len(polygon) > 1 and polygon[0] != polygon[-1]:
lon, lat = polygon[0]
f.write(f" {lon:.7f} {lat:.7f}\n")
f.write("END\n")
f.write("END\n")
print(f"Polygone sauvegardé avec succès dans {output_file}")
return output_file
except IOError as e:
print(f"Erreur lors de l'écriture du fichier: {e}")
raise # Re-raise the IOError
except Exception as e:
print(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
raise RuntimeError(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
def main():
"""
Fonction principale du script.
"""
try:
# Récupérer le code INSEE
insee_code = get_insee_code()
# Vérifier que le code INSEE est valide (format numérique ou alphanumérique pour les DOM-TOM)
if not insee_code:
raise ValueError("Le code INSEE ne peut pas être vide")
if not insee_code.isalnum() or len(insee_code) not in [5, 3]:
raise ValueError(
"Code INSEE invalide. Il doit être composé de 5 chiffres (ou 3 pour certains territoires)."
)
# Interroger l'API Overpass
data = query_overpass_api(insee_code)
# Extraire le polygone
polygon = extract_polygon(data)
# Sauvegarder le polygone dans un fichier
output_file = save_polygon_to_file(polygon, insee_code)
print(
f"Terminé. Le polygone de la commune {insee_code} a été sauvegardé dans {output_file}"
)
return 0 # Succès
except ValueError as e:
print(f"Erreur de validation: {e}")
return 1 # Erreur
except KeyboardInterrupt:
print("\nOpération annulée par l'utilisateur.")
return 1 # Erreur
except Exception as e:
print(f"Erreur inattendue: {e}")
return 1 # Erreur
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,301 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script principal pour lancer l'analyse historique d'une ville.
Ce script:
1. Demande à l'utilisateur quelle ville il souhaite traiter
2. Trouve le code INSEE de la ville demandée
3. Vérifie si le polygone de la ville existe, sinon le récupère
4. Traite les données historiques OSM pour cette ville
Usage:
python historize_zone.py [--input fichier_historique.osh.pbf]
"""
import os
import sys
import csv
import argparse
import subprocess
from pathlib import Path
# Chemin vers le répertoire du script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Chemin vers le fichier CSV contenant les données des villes
CITIES_CSV = os.path.join(SCRIPT_DIR, "osm-commerces-villes-export.csv")
# Chemin vers le répertoire des polygones
POLYGONS_DIR = os.path.join(SCRIPT_DIR, "polygons")
# Chemin par défaut pour le fichier d'historique OSM France
DEFAULT_HISTORY_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-internal.osh.pbf")
def run_command(command):
"""Exécute une commande shell et retourne la sortie"""
print(f"Exécution: {command}")
try:
result = subprocess.run(
command,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Erreur lors de l'exécution de la commande: {e}")
print(f"Sortie de la commande: {e.stdout}")
print(f"Erreur de la commande: {e.stderr}")
return None
def load_cities():
"""
Charge les données des villes depuis le fichier CSV.
Returns:
dict: Dictionnaire des villes avec le nom comme clé et les données comme valeur
"""
cities = {}
try:
with open(CITIES_CSV, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row.get("name") and row.get("zone"):
cities[row["name"].lower()] = row
except Exception as e:
print(f"Erreur lors du chargement du fichier CSV des villes: {e}")
sys.exit(1)
return cities
def find_city(city_name, cities):
"""
Recherche une ville par son nom dans le dictionnaire des villes.
Args:
city_name (str): Nom de la ville à rechercher
cities (dict): Dictionnaire des villes
Returns:
dict: Données de la ville si trouvée, None sinon
"""
# Recherche exacte
if city_name.lower() in cities:
return cities[city_name.lower()]
# Recherche partielle
matches = []
for name, data in cities.items():
if city_name.lower() in name:
matches.append(data)
if not matches:
return None
# Si plusieurs correspondances, demander à l'utilisateur de choisir
if len(matches) > 1:
print(f"Plusieurs villes correspondent à '{city_name}':")
for i, city in enumerate(matches):
print(f"{i+1}. {city['name']} (INSEE: {city['zone']})")
choice = input("Entrez le numéro de la ville souhaitée (ou 'q' pour quitter): ")
if choice.lower() == "q":
sys.exit(0)
try:
index = int(choice) - 1
if 0 <= index < len(matches):
return matches[index]
else:
print("Choix invalide.")
return None
except ValueError:
print("Veuillez entrer un numéro valide.")
return None
return matches[0]
def check_polygon_exists(insee_code):
"""
Vérifie si le polygone d'une commune existe déjà.
Args:
insee_code (str): Code INSEE de la commune
Returns:
str: Chemin vers le fichier polygone s'il existe, None sinon
"""
poly_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
if os.path.isfile(poly_file):
return poly_file
return None
def get_polygon(insee_code):
"""
Récupère le polygone d'une commune à partir de son code INSEE.
Args:
insee_code (str): Code INSEE de la commune
Returns:
str: Chemin vers le fichier polygone créé, None en cas d'erreur
"""
get_poly_script = os.path.join(SCRIPT_DIR, "get_poly.py")
command = f"python3 {get_poly_script} {insee_code}"
output = run_command(command)
if output:
# Vérifier si le polygone a été créé
poly_file = check_polygon_exists(insee_code)
if poly_file:
return poly_file
return None
def process_city_history(input_file, poly_file, cleanup=False, benchmark=False):
"""
Traite l'historique OSM pour une ville.
Args:
input_file (str): Chemin vers le fichier d'historique OSM
poly_file (str): Chemin vers le fichier polygone de la ville
cleanup (bool): Si True, nettoie les fichiers temporaires après traitement
benchmark (bool): Si True, affiche des informations de performance détaillées
Returns:
bool: True si le traitement a réussi, False sinon
"""
loop_script = os.path.join(
SCRIPT_DIR, "loop_thematics_history_in_zone_to_counts.py"
)
output_dir = os.path.join(SCRIPT_DIR, "test_results")
temp_dir = os.path.join(SCRIPT_DIR, "test_temp")
# Créer les répertoires de sortie si nécessaires
os.makedirs(output_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)
# Construire la commande avec les options supplémentaires
command = f"python3 {loop_script} --input {input_file} --poly {poly_file} --output-dir {output_dir} --temp-dir {temp_dir} --max-dates 100"
# Ajouter les options de nettoyage et de benchmark si activées
if cleanup:
command += " --cleanup"
if benchmark:
command += " --benchmark"
print(f"Exécution de la commande: {command}")
output = run_command(command)
if output is not None:
return True
return False
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description="Analyse historique d'une ville dans OpenStreetMap."
)
parser.add_argument(
"--input",
"-i",
default=DEFAULT_HISTORY_FILE,
help=f"Fichier d'historique OSM (.osh.pbf). Par défaut: {DEFAULT_HISTORY_FILE}",
)
parser.add_argument(
"--cleanup",
"-c",
action="store_true",
help="Nettoyer les fichiers temporaires après traitement",
)
parser.add_argument(
"--benchmark",
"-b",
action="store_true",
help="Afficher des informations de performance détaillées",
)
parser.add_argument(
"--city",
"-v",
help="Nom de la ville à traiter (si non spécifié, demande interactive)",
)
args = parser.parse_args()
# Vérifier que le fichier d'historique existe
if not os.path.isfile(args.input):
print(f"Erreur: Le fichier d'historique {args.input} n'existe pas.")
print(
f"Veuillez spécifier un fichier d'historique valide avec l'option --input."
)
sys.exit(1)
# Charger les données des villes
cities = load_cities()
if not cities:
print("Aucune ville n'a été trouvée dans le fichier CSV.")
sys.exit(1)
print(f"Données chargées pour {len(cities)} villes.")
# Obtenir le nom de la ville à traiter
city_name = args.city
if not city_name:
# Mode interactif si aucune ville n'est spécifiée
city_name = input("Quelle ville souhaitez-vous traiter ? ")
# Rechercher la ville
city = find_city(city_name, cities)
if not city:
print(f"Aucune ville correspondant à '{city_name}' n'a été trouvée.")
sys.exit(1)
insee_code = city["zone"]
print(f"Ville trouvée: {city['name']} (INSEE: {insee_code})")
# Vérifier si le polygone existe
poly_file = check_polygon_exists(insee_code)
if poly_file:
print(f"Le polygone pour {city['name']} existe déjà: {poly_file}")
else:
print(f"Le polygone pour {city['name']} n'existe pas. Récupération en cours...")
poly_file = get_polygon(insee_code)
if not poly_file:
print(f"Erreur: Impossible de récupérer le polygone pour {city['name']}.")
sys.exit(1)
print(f"Polygone récupéré avec succès: {poly_file}")
# Afficher les options activées
if args.benchmark:
print("\n=== Options ===")
print(
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
)
print(f"Benchmark: Activé")
print("===============\n")
# Traiter l'historique pour cette ville
print(f"Traitement de l'historique OSM pour {city['name']}...")
success = process_city_history(args.input, poly_file, args.cleanup, args.benchmark)
if success:
print(f"Traitement terminé avec succès pour {city['name']}.")
else:
print(f"Erreur lors du traitement de l'historique pour {city['name']}.")
sys.exit(1)
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,9 @@
# get latest france with cli
source ./secrets.sh
python3 /home/poule/encrypted/stockage-syncable/www/development/html/sendfile_osm_oauth_protector/oauth_cookie_client.py \
-u "$OSM_USER" -p "$OSM_PASS" \
-c https://osm-internal.download.geofabrik.de/get_cookie \
-o cookie.txt
wget -N --no-cookies --header "Cookie: $(cat cookie.txt | cut -d ';' -f 1)" https://osm-internal.download.geofabrik.de/europe/france-latest-internal.osm.pbf -O osm_data/france-internal.osh.pbf

View file

@ -0,0 +1,952 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour compter les objets OSM par thématique sur une zone donnée à différentes dates.
Ce script utilise osmium pour:
1. Filtrer les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
2. Compter les objets correspondant à chaque thématique à chaque date
3. Calculer le pourcentage de complétion des attributs importants pour chaque thème
4. Sauvegarder les résultats dans des fichiers CSV
5. Générer des graphiques montrant l'évolution dans le temps
Exemple d'utilisation:
python3 loop_thematics_history_in_zone_to_counts.py --input france.osh.pbf --poly polygons/commune_91111.poly
"""
import os
import sys
import subprocess
import csv
import json
import argparse
import multiprocessing
from datetime import datetime, timedelta
from pathlib import Path
from functools import lru_cache
import time
# Définition des thématiques et leurs tags correspondants (repris de split_history_to_thematics.py)
THEMES = {
"borne-de-recharge": {
"tag_filter": "amenity=charging_station",
"important_tags": ["operator", "capacity"],
},
"borne-incendie": {
"tag_filter": "emergency=fire_hydrant",
"important_tags": ["ref", "colour"],
},
"arbres": {
"tag_filter": "natural=tree",
"important_tags": ["species", "leaf_type"],
},
"defibrillator": {
"tag_filter": "emergency=defibrillator",
"important_tags": ["operator", "access"],
},
"toilets": {
"tag_filter": "amenity=toilets",
"important_tags": ["access", "wheelchair"],
},
"bus_stop": {
"tag_filter": "highway=bus_stop",
"important_tags": ["name", "shelter"],
},
"camera": {
"tag_filter": "man_made=surveillance",
"important_tags": ["operator", "surveillance"],
},
"recycling": {
"tag_filter": "amenity=recycling",
"important_tags": ["recycling_type", "operator"],
},
"substation": {
"tag_filter": "power=substation",
"important_tags": ["operator", "voltage"],
},
"laboratory": {
"tag_filter": "healthcare=laboratory",
"important_tags": ["name", "operator"],
},
"school": {"tag_filter": "amenity=school", "important_tags": ["name", "operator"]},
"police": {"tag_filter": "amenity=police", "important_tags": ["name", "operator"]},
"healthcare": {
"tag_filter": "healthcare or amenity=doctors or amenity=pharmacy or amenity=hospital or amenity=clinic or amenity=social_facility",
"important_tags": ["name", "healthcare"],
},
"bicycle_parking": {
"tag_filter": "amenity=bicycle_parking",
"important_tags": ["capacity", "covered"],
},
"advertising_board": {
"tag_filter": "advertising=board and message=political",
"important_tags": ["operator", "content"],
},
"building": {
"tag_filter": "building",
"important_tags": ["building", "addr:housenumber"],
},
"email": {
"tag_filter": "email or contact:email",
"important_tags": ["email", "contact:email"],
},
"bench": {
"tag_filter": "amenity=bench",
# "important_tags": ["backrest", "material"]
},
"waste_basket": {
"tag_filter": "amenity=waste_basket",
# "important_tags": ["operator", "capacity"]
},
"street_lamp": {
"tag_filter": "highway=street_lamp",
# "important_tags": ["light:method", "operator"]
},
"drinking_water": {
"tag_filter": "amenity=drinking_water",
# "important_tags": ["drinking_water", "bottle"]
},
"power_pole": {
"tag_filter": "power=pole",
# "important_tags": ["ref", "operator"]
},
"manhole": {
"tag_filter": "man_made=manhole",
# "important_tags": ["man_made", "substance"]
},
"little_free_library": {
"tag_filter": "amenity=public_bookcase",
# "important_tags": ["amenity", "capacity"]
},
"playground": {
"tag_filter": "leisure=playground",
# "important_tags": ["name", "surface"]
},
"siret": {
"tag_filter": "ref:FR:SIRET",
# "important_tags": ["name", "surface"]
},
"restaurants": {
"tag_filter": "amenity=restaurant",
"important_tags": ["opening_hours", "contact:street", "contact:housenumber", "website", "contact:phone"]
},
"rnb": {
"tag_filter": "ref:FR:RNB",
# "important_tags": ["name", "surface"]
},
}
def run_command(command):
"""Exécute une commande shell et retourne la sortie"""
print(f"Exécution: {command}")
try:
result = subprocess.run(
command,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Erreur lors de l'exécution de la commande: {e}")
print(f"Sortie de la commande: {e.stdout}")
print(f"Erreur de la commande: {e.stderr}")
return None
@lru_cache(maxsize=128)
def run_command_cached(command):
"""Version mise en cache de run_command pour éviter les appels redondants"""
return run_command(command)
def count_objects_at_date(
input_file, date_str, tag_filter, output_dir=None, insee_code=None
):
"""
Compte les objets correspondant à un filtre de tag à une date spécifique.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
tag_filter: Filtre de tag (ex: "amenity=charging_station")
output_dir: Répertoire de sortie pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Nombre d'objets correspondant au filtre à la date spécifiée
"""
# Créer un répertoire temporaire si nécessaire
if output_dir:
os.makedirs(output_dir, exist_ok=True)
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
temp_file = os.path.join(
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
)
# Vérifier si le fichier temporaire existe déjà
if not os.path.exists(temp_file):
time_filter_cmd = f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
run_command(time_filter_cmd)
tags_count_cmd = f"osmium tags-count {temp_file} -F osm.pbf {tag_filter}"
else:
# Utiliser des pipes comme dans l'exemple
tags_count_cmd = f"osmium time-filter {input_file} {date_str} -O -o - -f osm.pbf | osmium tags-count - -F osm.pbf {tag_filter}"
# Exécuter la commande et récupérer le résultat (utiliser la version mise en cache)
output = run_command_cached(tags_count_cmd)
# Analyser la sortie pour obtenir le nombre d'objets
if output:
# La sortie d'osmium tags-count est au format "count "key" "value"" ou "count "key""
# Par exemple: "42 "amenity" "charging_station"" ou "42 "operator""
parts = output.strip().split()
if len(parts) >= 1:
try:
return int(parts[0])
except ValueError:
print(f"Impossible de convertir '{parts[0]}' en entier")
return None
return None
def export_to_geojson(
input_file, date_str, main_tag_filter, output_dir, insee_code=None
):
"""
Exporte les données OSM filtrées à une date spécifique vers un fichier GeoJSON.
Si le fichier GeoJSON ou les fichiers temporaires existent déjà, l'export est ignoré.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Chemin vers le fichier GeoJSON créé
"""
os.makedirs(output_dir, exist_ok=True)
# Ajouter le code INSEE au nom du fichier si disponible
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
# Définir le chemin du fichier GeoJSON de sortie
geojson_file = os.path.join(
output_dir, f"export_{date_str.replace(':', '_')}__{insee_suffix}.geojson"
)
# Vérifier si le fichier GeoJSON existe déjà
if os.path.exists(geojson_file):
return geojson_file
# Définir les chemins des fichiers temporaires
temp_file = os.path.join(
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
)
filtered_file = os.path.join(
output_dir,
f"filtered_{date_str.replace(':', '_')}__{main_tag_filter.replace('=', '_').replace(' ', '_')}__{insee_suffix}.osm.pbf",
)
# Vérifier si le fichier temporaire filtré par date existe déjà
if not os.path.exists(temp_file):
# Créer un fichier temporaire pour les données filtrées par date
time_filter_cmd = (
f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
)
run_command_cached(time_filter_cmd)
# Vérifier si le fichier filtré par tag existe déjà
if not os.path.exists(filtered_file):
# Filtrer les objets qui ont le tag principal
filter_cmd = f"osmium tags-filter {temp_file} {main_tag_filter} -f osm.pbf -O -o {filtered_file}"
run_command_cached(filter_cmd)
# Exporter vers GeoJSON
export_cmd = f"osmium export {filtered_file} -O -o {geojson_file} -f geojson --geometry-types point,linestring,polygon"
run_command_cached(export_cmd)
return geojson_file
def count_features_with_tags_in_geojson(geojson_file, attribute_tags):
"""
Compte les features dans un fichier GeoJSON qui ont des attributs spécifiques.
Args:
geojson_file: Chemin vers le fichier GeoJSON
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
Returns:
Dictionnaire avec le nombre de features ayant chaque attribut spécifié
"""
try:
with open(geojson_file, "r") as f:
geojson_data = json.load(f)
# Initialiser les compteurs pour chaque attribut
counts = {tag: 0 for tag in attribute_tags}
# Compter en une seule passe
for feature in geojson_data.get("features", []):
properties = feature.get("properties", {})
for tag in attribute_tags:
if tag in properties and properties[tag]:
counts[tag] += 1
return counts
except Exception as e:
print(
f"Erreur lors du comptage des features avec les attributs {attribute_tags}: {e}"
)
return {tag: None for tag in attribute_tags}
def count_objects_with_tags(
input_file,
date_str,
main_tag_filter,
attribute_tags,
output_dir=None,
insee_code=None,
):
"""
Compte les objets qui ont à la fois le tag principal et des attributs spécifiques.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Dictionnaire avec le nombre d'objets ayant chaque attribut spécifié
"""
# Utiliser l'export GeoJSON si un répertoire de sortie est spécifié
if output_dir:
# Exporter vers GeoJSON (la fonction export_to_geojson vérifie déjà si le fichier GeoJSON existe)
geojson_file = export_to_geojson(
input_file, date_str, main_tag_filter, output_dir, insee_code
)
# Compter les features avec les attributs spécifiques en une seule passe
return count_features_with_tags_in_geojson(geojson_file, attribute_tags)
else:
# Méthode alternative si pas de répertoire temporaire
counts = {}
for tag in attribute_tags:
combined_filter = f"{main_tag_filter} and {tag}"
counts[tag] = count_objects_at_date(
input_file, date_str, combined_filter, output_dir, insee_code
)
return counts
def count_objects_with_tag(
input_file,
date_str,
main_tag_filter,
attribute_tag,
output_dir=None,
insee_code=None,
):
"""
Compte les objets qui ont à la fois le tag principal et un attribut spécifique.
Version compatible avec l'ancienne API pour la rétrocompatibilité.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
attribute_tag: Attribut à vérifier (ex: "operator")
output_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Nombre d'objets ayant à la fois le tag principal et l'attribut spécifié
"""
result = count_objects_with_tags(
input_file, date_str, main_tag_filter, [attribute_tag], output_dir, insee_code
)
return result.get(attribute_tag, None)
def generate_time_slices(max_dates=None):
"""
Génère une liste de dates avec différentes fréquences selon l'ancienneté:
- 30 derniers jours: quotidien
- 12 derniers mois: mensuel
- Jusqu'à 2004: annuel
Args:
max_dates: Nombre maximum de dates à générer (pour les tests)
Returns:
Liste de dates au format ISO (YYYY-MM-DDThh:mm:ssZ)
"""
dates = []
now = datetime.now()
# 1. Dates quotidiennes pour les 30 derniers jours
current_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
for _ in range(30):
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
dates.append(date_str)
current_date -= timedelta(days=1)
# 2. Dates mensuelles pour les 12 derniers mois (en excluant le mois courant déjà couvert)
current_date = datetime(now.year, now.month, 1) - timedelta(
days=1
) # Dernier jour du mois précédent
current_date = datetime(
current_date.year, current_date.month, 1
) # Premier jour du mois précédent
for _ in range(
60
): # 11 mois supplémentaires (12 mois au total avec le mois courant)
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
if date_str not in dates: # Éviter les doublons
dates.append(date_str)
# Passer au mois précédent
if current_date.month == 1:
current_date = datetime(current_date.year - 1, 12, 1)
else:
current_date = datetime(current_date.year, current_date.month - 1, 1)
# 3. Dates annuelles de l'année précédente jusqu'à 2004
# Amélioration: deux mesures par an (1er janvier et 1er juillet) pour les périodes éloignées
start_year = min(
now.year - 1, 2023
) # Commencer à l'année précédente (ou 2023 si nous sommes en 2024)
for year in range(start_year, 2003, -1):
# Ajouter le 1er janvier
date_str_jan = f"{year}-01-01T00:00:00Z"
if date_str_jan not in dates: # Éviter les doublons
dates.append(date_str_jan)
# Ajouter le 1er juillet
date_str_jul = f"{year}-07-01T00:00:00Z"
if date_str_jul not in dates: # Éviter les doublons
dates.append(date_str_jul)
# Limiter le nombre de dates si spécifié
if max_dates is not None and max_dates > 0:
dates = dates[:max_dates]
# Trier les dates par ordre chronologique
dates.sort()
return dates
def extract_zone_data(input_file, poly_file, output_dir):
"""
Extrait les données pour une zone spécifique à partir d'un fichier d'historique OSM.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
poly_file: Fichier de polygone (.poly) définissant la zone
output_dir: Répertoire de sortie
Returns:
Chemin vers le fichier extrait
"""
os.makedirs(output_dir, exist_ok=True)
# Obtenir le nom de la zone à partir du nom du fichier poly
zone_name = Path(poly_file).stem
# Créer le fichier de sortie
output_file = os.path.join(output_dir, f"{zone_name}.osh.pbf")
# Exécuter la commande osmium extract seulement si le fichier n'existe pas déjà
if os.path.exists(output_file):
print(f"Le fichier {output_file} existe déjà, utilisation du fichier existant.")
else:
print(f"Extraction des données pour la zone {zone_name}...")
command = f"osmium extract -p {poly_file} -H {input_file} -O -o {output_file}"
run_command(command)
return output_file
def process_theme(
theme_name,
theme_info,
zone_file,
zone_name,
dates,
output_dir,
temp_dir,
insee_code=None,
):
"""
Traite une thématique spécifique pour une zone donnée.
Args:
theme_name: Nom de la thématique
theme_info: Informations sur la thématique (tag_filter, important_tags)
zone_file: Fichier de la zone à traiter
zone_name: Nom de la zone
dates: Liste des dates à traiter
output_dir: Répertoire pour les fichiers de sortie
temp_dir: Répertoire pour les fichiers temporaires
insee_code: Code INSEE de la commune (optionnel)
Returns:
Chemin vers le fichier CSV généré
"""
start_time = time.time()
print(f"Traitement de la thématique '{theme_name}' pour la zone '{zone_name}'...")
# Préparer le fichier CSV de sortie
csv_file = os.path.join(output_dir, f"{zone_name}_{theme_name}.csv")
# Entêtes du CSV - colonnes de base
headers = ["date", "zone", "theme", "nombre_total"]
# Ajouter une colonne pour chaque tag important
# Vérifier si la clé 'important_tags' existe, sinon utiliser une liste vide
important_tags = theme_info.get("important_tags", [])
for attr in important_tags:
headers.append(f"nombre_avec_{attr}")
# Ajouter la colonne de pourcentage de complétion
headers.append("pourcentage_completion")
# Vérifier si le fichier CSV existe déjà et lire les données existantes
existing_dates = set()
existing_rows = []
existing_data = {} # Dictionnaire pour stocker les données existantes par date
file_exists = os.path.exists(csv_file)
if file_exists:
try:
with open(csv_file, "r", newline="") as f:
reader = csv.reader(f)
existing_headers = next(reader) # Lire les entêtes
# Vérifier que les entêtes correspondent
if existing_headers == headers:
for row in reader:
if len(row) >= 1: # S'assurer que la ligne a au moins une date
date = row[0] # La date est dans la première colonne
existing_dates.add(date)
existing_rows.append(row)
# Stocker les données existantes dans un dictionnaire pour un accès facile
existing_data[date] = row
else:
print(f"Les entêtes du fichier existant ne correspondent pas, création d'un nouveau fichier.")
file_exists = False
except Exception as e:
print(f"Erreur lors de la lecture du fichier CSV existant: {e}")
file_exists = False
# Filtrer les dates qui n'ont pas encore été traitées
dates_to_process = [date_str for date_str in dates if date_str.split("T")[0] not in existing_dates]
# Mode d'ouverture du fichier (écriture ou ajout)
mode = "a" if file_exists else "w"
with open(csv_file, mode, newline="") as f:
writer = csv.writer(f)
# Écrire les entêtes si c'est un nouveau fichier
if not file_exists:
writer.writerow(headers)
# Traiter chaque date qui n'a pas encore été traitée
for date_str in dates_to_process:
tag_filter = theme_info["tag_filter"]
# Compter le nombre total d'objets
total_count = count_objects_at_date(
zone_file, date_str, tag_filter, temp_dir, insee_code
)
# Compter les objets avec chaque attribut important en une seule passe
# Vérifier si la clé 'important_tags' existe et n'est pas vide
important_tags = theme_info.get("important_tags", [])
if important_tags:
attr_counts_dict = count_objects_with_tags(
zone_file,
date_str,
tag_filter,
important_tags,
temp_dir,
insee_code,
)
attr_counts = [
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
]
else:
attr_counts = []
# Formater la date pour le CSV (YYYY-MM-DD)
csv_date = date_str.split("T")[0]
# Vérifier si le total_count est 0 et s'il était > 0 dans le passé
recalculate = False
if total_count == 0:
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
was_greater_than_zero = False
for existing_date in sorted(existing_data.keys()):
if existing_date >= csv_date:
break # Ne pas regarder les dates futures
existing_row = existing_data[existing_date]
if len(existing_row) >= 4: # S'assurer que la ligne a une valeur de nombre_total
try:
existing_total = existing_row[3]
if existing_total and existing_total != "" and int(existing_total) > 0:
was_greater_than_zero = True
break
except (ValueError, TypeError):
# Ignorer les valeurs qui ne peuvent pas être converties en entier
pass
if was_greater_than_zero:
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
total_count = ""
print(f"Valeur passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
# Relancer le calcul osmium
recalculate = True
# Vérifier également pour chaque attribut important
for i, (attr, count) in enumerate(attr_counts):
if count == 0:
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
was_greater_than_zero = False
for existing_date in sorted(existing_data.keys()):
if existing_date >= csv_date:
break # Ne pas regarder les dates futures
existing_row = existing_data[existing_date]
if len(existing_row) >= 4 + i + 1: # S'assurer que la ligne a une valeur pour cet attribut
try:
existing_attr_count = existing_row[4 + i]
if existing_attr_count and existing_attr_count != "" and int(existing_attr_count) > 0:
was_greater_than_zero = True
break
except (ValueError, TypeError):
# Ignorer les valeurs qui ne peuvent pas être converties en entier
pass
if was_greater_than_zero:
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
attr_counts[i] = (attr, "")
print(f"Valeur de {attr} passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
# Relancer le calcul osmium
recalculate = True
# Si on doit recalculer, relancer le calcul osmium
if recalculate:
print(f"Relancement du calcul osmium pour {theme_name} à la date {csv_date}...")
# Supprimer les fichiers temporaires pour forcer un recalcul
temp_file_pattern = os.path.join(temp_dir, f"temp_{date_str.replace(':', '_')}__*")
run_command(f"rm -f {temp_file_pattern}")
# Recalculer le nombre total d'objets
if total_count == "":
total_count = count_objects_at_date(
zone_file, date_str, tag_filter, temp_dir, insee_code
)
# Recalculer les objets avec chaque attribut important
if important_tags:
attr_counts_dict = count_objects_with_tags(
zone_file,
date_str,
tag_filter,
important_tags,
temp_dir,
insee_code,
)
attr_counts = [
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
]
# Calculer le pourcentage de complétion
if total_count is not None and total_count != "" and total_count > 0 and len(attr_counts) > 0:
# Filtrer les comptages None ou vides avant de calculer la moyenne
valid_counts = [(attr, count) for attr, count in attr_counts if count is not None and count != ""]
if valid_counts:
# Moyenne des pourcentages de présence de chaque attribut important
completion_pct = sum(
count / total_count * 100 for _, count in valid_counts
) / len(valid_counts)
else:
completion_pct = 0
else:
completion_pct = 0
# Préparer la ligne CSV avec les colonnes de base
# Si le comptage total a échoué (None), ajouter une chaîne vide au lieu de 0
row = [csv_date, zone_name, theme_name, "" if total_count is None else total_count]
# Ajouter les compteurs pour chaque attribut important
for attr, count in attr_counts:
# Si le comptage a échoué (None), ajouter une chaîne vide au lieu de 0
row.append("" if count is None else count)
# Ajouter le pourcentage de complétion
row.append(round(completion_pct, 2))
# Écrire la ligne dans le CSV
writer.writerow(row)
# Si aucune nouvelle date n'a été traitée, afficher un message
if not dates_to_process:
print(f"Toutes les dates pour la thématique '{theme_name}' sont déjà traitées dans le fichier CSV existant.")
print(f"Résultats sauvegardés dans {csv_file}")
# Générer un graphique pour cette thématique
generate_graph(csv_file, zone_name, theme_name)
end_time = time.time()
print(f"Thématique '{theme_name}' traitée en {end_time - start_time:.2f} secondes")
return csv_file
def cleanup_temp_files(temp_dir, keep_zone_files=True):
"""
Nettoie les fichiers temporaires dans le répertoire spécifié.
Args:
temp_dir: Répertoire contenant les fichiers temporaires
keep_zone_files: Si True, conserve les fichiers de zone extraits (.osh.pbf)
"""
print(f"Nettoyage des fichiers temporaires dans {temp_dir}...")
count = 0
for file in os.listdir(temp_dir):
file_path = os.path.join(temp_dir, file)
# Conserver les fichiers de zone extraits si demandé
if (
keep_zone_files
and file.endswith(".osh.pbf")
and not file.startswith("temp_")
and not file.startswith("filtered_")
):
continue
# Supprimer les fichiers temporaires
if (
file.startswith("temp_")
or file.startswith("filtered_")
or file.startswith("export_")
):
try:
os.remove(file_path)
count += 1
except Exception as e:
print(f"Erreur lors de la suppression du fichier {file_path}: {e}")
print(f"{count} fichiers temporaires supprimés.")
def process_zone(
input_file, poly_file, output_dir, temp_dir, max_dates=None, cleanup=False
):
"""
Traite une zone spécifique pour toutes les thématiques en parallèle.
Args:
input_file: Fichier d'historique OSM (.osh.pbf)
poly_file: Fichier de polygone (.poly) définissant la zone
output_dir: Répertoire pour les fichiers de sortie
temp_dir: Répertoire pour les fichiers temporaires
max_dates: Nombre maximum de dates à traiter (pour les tests)
cleanup: Si True, nettoie les fichiers temporaires après traitement
"""
start_time = time.time()
# Créer les répertoires nécessaires
os.makedirs(output_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)
# Obtenir le nom de la zone à partir du nom du fichier poly
zone_name = Path(poly_file).stem
# Extraire le code INSEE à partir du nom du fichier poly (format: commune_XXXXX.poly)
insee_code = None
if zone_name.startswith("commune_"):
insee_code = zone_name.replace("commune_", "")
# Extraire les données pour la zone
zone_file = extract_zone_data(input_file, poly_file, temp_dir)
# Générer les dates avec différentes fréquences selon l'ancienneté
dates = generate_time_slices(max_dates) # Limité par max_dates si spécifié
print(f"Traitement de {len(THEMES)} thématiques pour la zone '{zone_name}'...")
# Déterminer le nombre de processus à utiliser (nombre de cœurs disponibles - 1, minimum 1)
num_processes = max(1, multiprocessing.cpu_count() - 1)
print(f"Utilisation de {num_processes} processus pour le traitement parallèle")
# Créer un pool de processus
with multiprocessing.Pool(processes=num_processes) as pool:
# Préparer les arguments pour chaque thématique
theme_args = [
(
theme_name,
theme_info,
zone_file,
zone_name,
dates,
output_dir,
temp_dir,
insee_code,
)
for theme_name, theme_info in THEMES.items()
]
# Exécuter le traitement des thématiques en parallèle
pool.starmap(process_theme, theme_args)
# Nettoyer les fichiers temporaires si demandé
if cleanup:
cleanup_temp_files(temp_dir, keep_zone_files=True)
end_time = time.time()
total_time = end_time - start_time
print(f"Traitement de la zone '{zone_name}' terminé en {total_time:.2f} secondes")
return total_time
def generate_graph(csv_file, zone_name, theme_name):
"""
Génère un graphique à partir des données CSV.
Args:
csv_file: Fichier CSV contenant les données
zone_name: Nom de la zone
theme_name: Nom de la thématique
"""
# Vérifier si le script generate_graph.py existe
if os.path.exists(os.path.join(os.path.dirname(__file__), "generate_graph.py")):
# Construire le chemin de sortie pour le graphique
output_path = os.path.splitext(csv_file)[0] + "_graph.png"
# Exécuter le script pour générer le graphique
command = f"python3 {os.path.join(os.path.dirname(__file__), 'generate_graph.py')} {csv_file} --output {output_path}"
run_command(command)
print(f"Graphique généré: {output_path}")
else:
print(
"Le script generate_graph.py n'a pas été trouvé. Aucun graphique n'a été généré."
)
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description="Compte les objets OSM par thématique sur une zone donnée à différentes dates."
)
parser.add_argument(
"--input", "-i", required=True, help="Fichier d'historique OSM (.osh.pbf)"
)
parser.add_argument(
"--poly",
"-p",
required=True,
help="Fichier de polygone (.poly) définissant la zone",
)
parser.add_argument(
"--output-dir",
"-o",
default="resultats",
help="Répertoire pour les fichiers de sortie",
)
parser.add_argument(
"--temp-dir",
"-t",
default="temp",
help="Répertoire pour les fichiers temporaires",
)
parser.add_argument(
"--max-dates",
"-m",
type=int,
default=None,
help="Nombre maximum de dates à traiter (pour les tests, par défaut: toutes les dates)",
)
parser.add_argument(
"--cleanup",
"-c",
action="store_true",
help="Nettoyer les fichiers temporaires après traitement",
)
parser.add_argument(
"--benchmark",
"-b",
action="store_true",
help="Afficher des informations de performance détaillées",
)
args = parser.parse_args()
# Vérifier que les fichiers d'entrée existent
if not os.path.isfile(args.input):
print(f"Erreur: Le fichier d'entrée {args.input} n'existe pas.")
sys.exit(1)
if not os.path.isfile(args.poly):
print(f"Erreur: Le fichier de polygone {args.poly} n'existe pas.")
sys.exit(1)
# Afficher des informations sur la configuration
if args.benchmark:
print("\n=== Configuration ===")
print(f"Nombre de processeurs: {multiprocessing.cpu_count()}")
print(f"Nombre de thématiques: {len(THEMES)}")
print(
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
)
print("=====================\n")
# Mesurer le temps d'exécution
start_time = time.time()
# Traiter la zone
total_time = process_zone(
args.input,
args.poly,
args.output_dir,
args.temp_dir,
args.max_dates,
args.cleanup,
)
# Afficher des informations de performance
if args.benchmark:
print("\n=== Performance ===")
print(f"Temps total d'exécution: {total_time:.2f} secondes")
print(f"Temps moyen par thématique: {total_time / len(THEMES):.2f} secondes")
print("===================\n")
print("Traitement terminé.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,194 @@
zone,name,lat,lon,population,budgetAnnuel,completionPercent,placesCount,avecHoraires,avecAdresse,avecSite,avecAccessibilite,avecNote,siren,codeEpci,codesPostaux
91111,Briis-sous-Forges,48.6246916,2.1243349,3375,4708125.00,30,67,12,8,9,7,3,219101110,249100074,91640
79034,Bessines,46.3020750,-0.5167969,1882,2676204.00,58,56,36,26,34,4,1,217900349,200041317,79000
76216,Déville-lès-Rouen,49.4695338,1.0495889,10690,18386800.00,20,110,16,3,11,1,1,217602168,200023414,76250
91249,Forges-les-Bains,48.6290401,2.0996273,4138,6310450.00,33,31,7,5,2,5,0,219102498,249100074,91470
59140,Caullery,50.0817000,3.3737000,464,824528.00,,,,,,,,,,
08122,Chooz,50.0924000,4.7996000,792,1375704.00,,,,,,,,,,
12084,Creissels,44.0621000,3.0572000,1564,2493016.00,,,,,,,,,,
59183,Dunkerque,51.0183000,2.3431000,87013,153229893.00,,,,,,,,,,
86116,Jazeneuil,46.4749000,0.0735000,797,1212237.00,,,,,,,,,,
75113,,48.8303000,2.3656000,177735,239764515.00,,,,,,,,,,
06088,Nice,43.7032000,7.2528000,353701,557079075.00,,,,,,,,,,
38185,Grenoble,45.1842000,5.7155000,156389,231299331.00,,,,,,,,,,
75117,,48.8874000,2.3050000,161206,231975434.00,,,,,,,,,,
75116,,48.8572000,2.2630000,159733,287199934.00,,,,,,,,,,
35236,Redon,47.6557000,-2.0787000,9336,15030960.00,,,,,,,,,,
35238,Rennes,48.1159000,-1.6884000,227830,408727020.00,,,,,,,,,,
78646,Versailles,48.8039000,2.1191000,83918,142912354.00,,,,,,,,,,
12230,Saint-Jean-Delnous,44.0391000,2.4912000,382,665062.00,,,,,,,,,,
76193,"La Crique",49.6929000,1.2051000,367,651425.00,,,,,,,,,,
49007,Angers,47.4819000,-0.5629000,157555,207815045.00,,,,,,,,,,
79003,Aiffres,46.2831000,-0.4147000,5423,9631248.00,,,,,,,,,,
29151,Morlaix,48.5971000,-3.8215000,15220,22129880.00,,,,,,,,,,
38544,Vienne,45.5221000,4.8803000,31555,55252805.00,,,,,,,,,,
42100,"La Gimond",45.5551000,4.4144000,278,428676.00,,,,,,,,,,
76008,Ancourt,49.9110000,1.1835000,627,850839.00,,,,,,,,,,
76618,Petit-Caux,49.9612000,1.2343000,9626,15334218.00,,,,,,,,,,
13202,Marseille,43.3225000,5.3497000,24153,40842723.00,,,,,,,,,,
46102,Figeac,44.6067000,2.0231000,9757,17025965.00,,,,,,,,,,
75107,,48.8548000,2.3115000,48196,70992708.00,,,,,,,,,,
7812,"Les Clayes-sous-Bois",,,,,,,,,,,,,,
76192,Criel-sur-Mer,50.0221000,1.3215000,2592,3659904.00,,,,,,,,,,
94080,Vincennes,48.8471000,2.4383000,48368,64232704.00,,,,,,,,,,
13055,Marseille,43.2803000,5.3806000,877215,1184240250.00,,,,,,,,,,
93055,Pantin,48.9006000,2.4085000,60954,90272874.00,,,,,,,,,,
69385,,45.7560000,4.8012000,48277,83277825.00,,,,,,,,,,
75109,,48.8771000,2.3379000,58419,79625097.00,,,,,,,,,,
44184,Saint-Nazaire,47.2768000,-2.2392000,73111,104694952.00,,,,,,,,,,
13208,,43.2150000,5.3256000,83414,120866886.00,,,,,,,,,,
59271,Grande-Synthe,51.0157000,2.2938000,20347,34284695.00,,,,,,,,,,
77379,Provins,48.5629000,3.2845000,11824,20183568.00,,,,,,,,,,
75108,,48.8732000,2.3111000,35418,51710280.00,,,,,,,,,,
77122,Combs-la-Ville,48.6602000,2.5765000,22712,29661872.00,,,,,,,,,,
79298,Saint-Symphorien,46.2688000,-0.4842000,1986,2909490.00,,,,,,,,,,
51454,Reims,49.2535000,4.0551000,178478,310908676.00,,,,,,,,,,
64102,Bayonne,43.4844000,-1.4611000,53312,82740224.00,,,,,,,,,,
39300,Lons-le-Saunier,46.6758000,5.5574000,16942,25379116.00,,,,,,,,,,
85288,Talmont-Saint-Hilaire,46.4775000,-1.6299000,8327,10908370.00,,,,,,,,,,
13203,,43.3113000,5.3806000,55653,81642951.00,,,,,,,,,,
69387,,45.7321000,4.8393000,87491,134998613.00,,,,,,,,,,
64024,Anglet,43.4893000,-1.5193000,42288,73665696.00,,,,,,,,,,
69265,Ville-sur-Jarnioux,45.9693000,4.5942000,820,1209500.00,,,,,,,,,,
75112,,48.8342000,2.4173000,139788,228273804.00,,,,,,,,,,
69388,,45.7342000,4.8695000,84956,119703004.00,,,,,,,,,,
74010,Annecy,45.9024000,6.1264000,131272,235764512.00,,,,,,,,,,
74100,Desingy,46.0022000,5.8870000,759,1032240.00,,,,,,,,,,
75114,,48.8297000,2.3230000,137581,190412104.00,,,,,,,,,,
75118,,48.8919000,2.3487000,185825,275578475.00,,,,,,,,,,
85191,"La Roche-sur-Yon",46.6659000,-1.4162000,54699,80899821.00,,,,,,,,,,
85194,"Les Sables-d'Olonne",46.5264000,-1.7611000,48740,85002560.00,,,,,,,,,,
12220,Sainte-Eulalie-de-Cernon,43.9605000,3.1550000,321,558540.00,,,,,,,,,,
14341,Ifs,49.1444000,-0.3385000,11868,16377840.00,,,,,,,,,,
35288,Saint-Malo,48.6465000,-2.0066000,47255,81562130.00,,,,,,,,,,
86194,Poitiers,46.5846000,0.3715000,89472,130718592.00,,,,,,,,,,
91338,Limours,48.6463000,2.0823000,6408,8676432.00,,,,,,,,,,
91640,,,,,,,,,,,,,,,
79191,Niort,46.3274000,-0.4613000,60074,92934478.00,,,,,,,,,,
06004,Antibes,43.5823000,7.1048000,76612,134147612.00,,,,,,,,,,
13207,,43.2796000,5.3274000,34866,53031186.00,,,,,,,,,,
91657,Vigneux-sur-Seine,48.7021000,2.4274000,31233,41477424.00,,,,,,,,,,
73124,Gilly-sur-Isère,45.6549000,6.3487000,3109,4651064.00,,,,,,,,,,
92040,Issy-les-Moulineaux,48.8240000,2.2628000,67695,108108915.00,,,,,,,,,,
93051,Noisy-le-Grand,48.8327000,2.5560000,71632,104654352.00,,,,,,,,,,
14675,Soliers,49.1315000,-0.2898000,2190,2923650.00,,,,,,,,,,
76655,Saint-Valery-en-Caux,49.8582000,0.7094000,3884,5985244.00,,,,,,,,,,
34172,Montpellier,43.6100000,3.8742000,307101,508866357.00,,,,,,,,,,
76351,"Le Havre",49.4958000,0.1312000,166462,260346568.00,,,,,,,,,,
76665,Sauchay,49.9179000,1.2073000,448,618240.00,,,,,,,,,,
13204,,43.3063000,5.4002000,49744,75312416.00,,,,,,,,,,
75104,Paris,48.8541000,2.3569000,28039,44161425.00,,,,,,,,,,
45234,Orléans,47.8734000,1.9122000,116344,178355352.00,,,,,,,,,,
13100,Saint-Rémy-de-Provence,43.7815000,4.8455000,9547,16334917.00,,,,,,,,,,
27275,Gaillon,49.1602000,1.3405000,6785,11995880.00,,,,,,,,,,
44047,Couëron,47.2391000,-1.7472000,23541,31521399.00,,,,,,,,,,
16070,Chabanais,45.8656000,0.7076000,1564,2744820.00,,,,,,,,,,
12029,Bor-et-Bar,44.1971000,2.0822000,198,304128.00,,,,,,,,,,
59155,Coudekerque-Branche,51.0183000,2.3986000,20833,36707746.00,,,,,,,,,,
16106,Confolens,46.0245000,0.6639000,2726,3595594.00,,,,,,,,,,
75020,,,,,,,,,,,,,,,
76217,Dieppe,49.9199000,1.0838000,28599,48446706.00,,,,,,,,,,
95128,,,,,,,,,,,,,,,
44000,,,,,,,,,,,,,,,
79000,,,,,,,,,,,,,,,
27562,Saint-Marcel,49.0927000,1.4395000,4474,6259126.00,,,,,,,,,,
87011,Bellac,46.1013000,1.0325000,3569,5117946.00,,,,,,,,,,
94016,Cachan,48.7914000,2.3318000,30526,50642634.00,,,,,,,,,,
50237,"La Haye-Pesnel",48.8134000,-1.3732000,1261,2220621.00,,,,,,,,,,
91174,Corbeil-Essonnes,48.5973000,2.4646000,53712,72081504.00,,,,,,,,,,
11012,Argeliers,43.3090000,2.9137000,2128,3562272.00,,,,,,,,,,
94041,Ivry-sur-Seine,48.8125000,2.3872000,64526,85432424.00,,,,,,,,,,
95127,Cergy,49.0373000,2.0455000,69578,108263368.00,,,,,,,,,,
69001,Affoux,45.8448000,4.4116000,397,648698.00,,,,,,,,,,
44190,Saint-Sébastien-sur-Loire,47.2065000,-1.5023000,28373,39920811.00,,,,,,,,,,
69123,Lyon,45.7580000,4.8351000,520774,755643074.00,,,,,,,,,,
07336,Vernon,44.5074000,4.2251000,223,371295.00,,,,,,,,,,
91201,Draveil,48.6777000,2.4249000,29824,51506048.00,,,,,,,,,,
29174,Plonéour-Lanvern,47.9066000,-4.2678000,6403,9931053.00,,,,,,,,,,
75017,,,,,,,,,,,,,,,
17197,Jonzac,45.4413000,-0.4237000,3576,5188776.00,,,,,,,,,,
85195,,,,,,,,,,,,,,,
44162,Saint-Herblain,47.2246000,-1.6306000,50561,88481750.00,,,,,,,,,,
00000,"toutes les villes",,,,,,,,,,,,,,
57463,Metz,49.1048000,6.1962000,121695,192643185.00,,,,,,,,,,
57466,Metzing,49.1008000,6.9605000,693,945945.00,,,,,,,,,,
37000,,,,,,,,,,,,,,,
91312,Igny,48.7375000,2.2229000,10571,14831113.00,,,,,,,,,,
25462,Pontarlier,46.9167000,6.3796000,17928,23449824.00,,,,,,,,,,
33063,Bordeaux,44.8624000,-0.5848000,265328,467773264.00,,,,,,,,,,
16015,Angoulême,45.6458000,0.1450000,41423,66069685.00,,,,,,,,,,
02196,Clacy-et-Thierret,49.5546000,3.5651000,294,458934.00,,,,,,,,,,
24037,Bergerac,44.8519000,0.4883000,26852,47850264.00,,,,,,,,,,
78686,Viroflay,48.8017000,2.1725000,16943,27091857.00,,,,,,,,,,
83137,Toulon,43.1364000,5.9334000,180834,319172010.00,,,,,,,,,,
44026,Carquefou,47.2968000,-1.4687000,20535,28707930.00,,,,,,,,,,
87154,Saint-Junien,45.8965000,0.8853000,11382,19406310.00,,,,,,,,,,
79081,Chauray,46.3537000,-0.3862000,7173,12387771.00,,,,,,,,,,
73008,Aix-les-Bains,45.6943000,5.9035000,32175,53539200.00,,,,,,,,,,
86195,Port-de-Piles,46.9989000,0.5956000,548,744184.00,,,,,,,,,,
91470,,,,,,,,,,,,,,,
34008,"Les Aires",43.5687000,3.0676000,613,1081332.00,,,,,,,,,,
44270,,,,,,,,,,,,,,,
25056,Besançon,47.2602000,6.0123000,120057,158355183.00,,,,,,,,,,
75019,,,,,,,,,,,,,,,
78712,,,,,,,,,,,,,,,
11262,Narbonne,43.1493000,3.0337000,56692,79482184.00,,,,,,,,,,
60602,Saint-Valery,49.7251000,1.7303000,54,90450.00,,,,,,,,,,
91421,Montgeron,48.6952000,2.4638000,23890,40851900.00,,,,,,,,,,
82112,Moissac,44.1219000,1.1002000,13652,24027520.00,,,,,,,,,,
92033,Garches,48.8469000,2.1861000,17705,25442085.00,,,,,,,,,,
38053,Bourgoin-Jallieu,45.6025000,5.2747000,29816,41593320.00,,,,,,,,,,
34335,Villemagne-l'Argentière,43.6189000,3.1208000,420,586320.00,,,,,,,,,,
13213,,43.3528000,5.4301000,93425,144528475.00,,,,,,,,,,
44020,Bouguenais,47.1710000,-1.6181000,20590,34282350.00,,,,,,,,,,
36044,Châteauroux,46.8023000,1.6903000,43079,70864955.00,,,,,,,,,,
11164,Ginestas,43.2779000,2.8830000,1579,2837463.00,,,,,,,,,,
60009,Allonne,49.3952000,2.1157000,1737,2883420.00,,,,,,,,,,
41151,"Montrichard Val de Cher",47.3594000,1.1998000,3641,5559807.00,,,,,,,,,,
27554,"La Chapelle-Longueville",49.1085000,1.4115000,3283,4796463.00,,,,,,,,,,
34189,Olonzac,43.2816000,2.7431000,1683,2511036.00,,,,,,,,,,
34028,Bédarieux,43.6113000,3.1637000,5820,9550620.00,,,,,,,,,,
74112,"Épagny Metz-Tessy",45.9430000,6.0934000,8642,13956830.00,,,,,,,,,,
75102,,48.8677000,2.3411000,20433,35103894.00,,,,,,,,,,
60057,Beauvais,49.4425000,2.0877000,55906,84082624.00,,,,,,,,,,
59350,Lille,50.6311000,3.0468000,238695,403871940.00,,,,,,,,,,
91477,Igny,48.7155000,2.2293000,36067,46923167.00,,,,,,,,,,
12145,Millau,44.0982000,3.1176000,21859,34865105.00,,,,,,,,,,
12115,L'Hospitalet-du-Larzac,43.9755000,3.2074000,344,569320.00,,,,,,,,,,
79004,,,,,,,,,,,,,,,
75014,,,,,,,,,,,,,,,
75018,,,,,,,,,,,,,,,
55154,Dieue-sur-Meuse,49.0790000,5.4293000,1452,2099592.00,,,,,,,,,,
79192,,,,,,,,,,,,,,,
06018,Biot,43.6273000,7.0821000,10196,15192040.00,,,,,,,,,,
25393,Montécheroux,47.3469000,6.7965000,557,839399.00,,,,,,,,,,
14554,"Le Castelet",49.0870000,-0.2811000,1829,3076378.00,,,,,,,,,,
69384,,45.7805000,4.8260000,35232,49782816.00,,,,,,,,,,
78672,Villennes-sur-Seine,48.9372000,1.9975000,5792,9359872.00,,,,,,,,,,
78123,Carrières-sous-Poissy,48.9469000,2.0264000,19951,35752192.00,,,,,,,,,,
77000,,,,,,,,,,,,,,,
31056,Beauzelle,43.6680000,1.3753000,8184,11670384.00,,,,,,,,,,
38553,Villefontaine,45.6161000,5.1549000,19018,25579210.00,,,,,,,,,,
47001,Agen,44.2010000,0.6302000,32193,48965553.00,,,,,,,,,,
54700,,,,,,,,,,,,,,,
70279,Gray,47.4310000,5.6153000,5455,9071665.00,,,,,,,,,,
74000,,,,,,,,,,,,,,,
91339,Linas,48.6261000,2.2525000,7310,12412380.00,,,,,,,,,,
17306,Royan,45.6343000,-1.0127000,19322,33311128.00,,,,,,,,,,
17300,"La Rochelle",46.1620000,-1.1765000,79961,118182358.00,,,,,,,,,,
59000,,,,,,,,,,,,,,,
77284,Meaux,48.9573000,2.9035000,56659,84025297.00,,,,,,,,,,
76414,Martin-Église,49.9105000,1.1279000,1595,2177175.00,,,,,,,,,,
54528,Toul,48.6794000,5.8980000,15570,21626730.00,,,,,,,,,,
63124,Cournon-d'Auvergne,45.7420000,3.1885000,20020,30930900.00,,,,,,,,,,
87085,Limoges,45.8567000,1.2260000,129754,203194764.00,,,,,,,,,,
78000,,,,,,,,,,,,,,,
13205,,43.2925000,5.4006000,45020,61632380.00,,,,,,,,,,
31584,Villemur-sur-Tarn,43.8582000,1.4947000,6235,9813890.00,,,,,,,,,,
91228,Évry-Courcouronnes,48.6287000,2.4313000,66700,90245100.00,,,,,,,,,,
41018,Blois,47.5813000,1.3049000,47092,79350020.00,,,,,,,,,,
66136,Perpignan,42.6990000,2.9045000,120996,193593600.00,,,,,,,,,,
11041,Bize-Minervois,43.3364000,2.8719000,1292,1758412.00,,,,,,,,,,
75016,,,,,,,,,,,,,,,
76540,Rouen,
Can't render this file because it has a wrong number of fields in line 193.

View file

@ -0,0 +1,3 @@
<?xml version='1.0' encoding='UTF-8'?>
<osm version="0.6" generator="empty">
</osm>

View file

@ -0,0 +1,110 @@
local tables = {}
-- Table pour les bornes incendie
tables.fire_hydrants = osm2pgsql.define_node_table('fire_hydrants', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'ref', type = 'text' },
{ column = 'color', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les arbres
tables.trees = osm2pgsql.define_node_table('trees', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'species', type = 'text' },
{ column = 'height', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les bornes de recharge (nodes)
tables.charging_stations = osm2pgsql.define_node_table('charging_stations', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'point', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'operator', type = 'text' },
{ column = 'capacity', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Table pour les bornes de recharge (ways)
tables.charging_stations_ways = osm2pgsql.define_way_table('charging_stations_ways', {
{ column = 'id_column', type = 'id_type' },
{ column = 'geom', type = 'linestring', projection = 4326 },
{ column = 'tags', type = 'hstore' },
{ column = 'operator', type = 'text' },
{ column = 'capacity', type = 'text' },
{ column = 'insee', type = 'text' },
})
-- Function to determine the INSEE code from multiple possible sources
function get_insee_code(tags)
-- Try to get INSEE code from different tags
if tags['ref:INSEE'] then
return tags['ref:INSEE']
elseif tags['addr:postcode'] then
-- French postal codes often start with the department code
-- For example, 91150 is in department 91, which can help identify the INSEE code
return tags['addr:postcode'] and string.sub(tags['addr:postcode'], 1, 2) .. "111"
elseif tags['addr:city'] and tags['addr:city'] == 'Étampes' then
-- If the city is Étampes, use the INSEE code 91111
return "91111"
else
-- Default to 91111 (Étampes) for this specific use case
-- In a production environment, you would use a spatial query to determine the INSEE code
return "91111"
end
end
function osm2pgsql.process_node(object)
-- Check for fire hydrants with different tagging schemes
if object.tags.emergency == 'fire_hydrant' or object.tags.amenity == 'fire_hydrant' then
tables.fire_hydrants:insert({
tags = object.tags,
ref = object.tags.ref,
color = object.tags.color,
insee = get_insee_code(object.tags)
})
end
-- Check for trees
if object.tags.natural == 'tree' then
tables.trees:insert({
tags = object.tags,
species = object.tags.species,
height = object.tags.height,
insee = get_insee_code(object.tags)
})
end
-- Check for charging stations
if object.tags.amenity == 'charging_station' then
tables.charging_stations:insert({
tags = object.tags,
operator = object.tags.operator,
capacity = object.tags.capacity,
insee = get_insee_code(object.tags)
})
end
end
function osm2pgsql.process_way(object)
-- Check for charging stations that might be mapped as ways
if object.tags.amenity == 'charging_station' then
tables.charging_stations_ways:insert({
tags = object.tags,
operator = object.tags.operator,
capacity = object.tags.capacity,
insee = get_insee_code(object.tags)
})
end
end
function osm2pgsql.process_relation(object)
return
end

View file

@ -0,0 +1,309 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer un graphique interactif avec plotly montrant l'évolution du nombre d'objets OSM
à partir d'un fichier CSV thématique d'une ville (code INSEE 91111 par défaut).
Ce script utilise la bibliothèque plotly pour créer des graphiques interactifs à partir des données
contenues dans des fichiers CSV thématiques. Par défaut, il utilise le code INSEE 91111.
Le titre du graphique inclut le tag principal (thème) et le nom de la ville.
Utilisation:
python plotly_city.py chemin/vers/fichier.csv [options]
Options:
--output, -o : Chemin de sortie pour le graphique HTML (optionnel)
--insee, -i : Code INSEE de la commune à analyser (par défaut: 91111)
--city_name, -c : Nom de la ville (si non spécifié, sera généré à partir du code INSEE)
Exemple:
python plotly_city.py test_results/commune_91111_borne-de-recharge.csv
Dépendances requises:
- pandas
- plotly
Installation des dépendances:
pip install pandas plotly
"""
import sys
import os
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import argparse
from datetime import datetime
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Génère un graphique interactif avec plotly à partir des données CSV d'objets OSM."
)
parser.add_argument(
"csv_file", help="Chemin vers le fichier CSV contenant les données"
)
parser.add_argument(
"--output", "-o", help="Chemin de sortie pour le graphique (HTML)", default=None
)
parser.add_argument(
"--insee", "-i", help="Code INSEE de la commune à analyser", default="91111"
)
parser.add_argument(
"--city_name",
"-c",
help="Nom de la ville (si non spécifié, sera extrait du CSV)",
default=None,
)
return parser.parse_args()
def get_city_name(insee_code):
"""
Récupère le nom de la ville à partir du code INSEE.
Cette fonction pourrait être améliorée pour utiliser une API ou une base de données.
Args:
insee_code: Code INSEE de la commune
Returns:
Nom de la ville ou le code INSEE si le nom n'est pas trouvé
"""
# Pour l'instant, on retourne simplement le code INSEE
# Dans une version future, on pourrait implémenter une recherche dans une base de données
return f"Commune {insee_code}"
def load_data(csv_file, insee_code="91111"):
"""
Charge les données depuis le fichier CSV.
Args:
csv_file: Chemin vers le fichier CSV
insee_code: Code INSEE de la commune à filtrer
Returns:
DataFrame pandas contenant les données filtrées
"""
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
try:
df = pd.read_csv(csv_file, on_bad_lines="skip")
except TypeError: # Pour les versions plus anciennes de pandas
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
# Vérifier si le CSV a la structure attendue
if "date" in df.columns:
# Format de CSV avec colonne 'date' directement
df["date"] = pd.to_datetime(df["date"])
else:
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
try:
df["date"] = pd.to_datetime(df.iloc[:, 0])
except:
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
sys.exit(1)
# Filtrer par code INSEE si la colonne 'zone' contient des codes INSEE
if "zone" in df.columns:
# Vérifier si la zone contient le code INSEE
if any(
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
):
df = df[df["zone"].str.endswith(insee_code)]
# Trier par date
df = df.sort_values("date")
return df
def generate_plotly_graph(
df, city_name=None, output_path=None, insee_code="91111", csv_file=None
):
"""
Génère un graphique interactif avec plotly montrant l'évolution du nombre d'objets dans le temps.
Args:
df: DataFrame pandas contenant les données
city_name: Nom de la ville (optionnel)
output_path: Chemin de sortie pour le graphique (optionnel)
insee_code: Code INSEE de la commune
csv_file: Chemin vers le fichier CSV source (pour générer un nom de fichier de sortie par défaut)
"""
# Si le nom de la ville n'est pas fourni, essayer de le récupérer
if not city_name:
city_name = get_city_name(insee_code)
# Déterminer la colonne pour les types d'objets (theme)
theme_column = "theme"
# Créer une figure avec deux sous-graphiques (nombre total et taux de complétion)
fig = make_subplots(
rows=2,
cols=1,
subplot_titles=("Nombre d'objets OSM", "Taux de complétion des attributs (%)"),
vertical_spacing=0.15,
)
# Obtenir la liste des thèmes uniques
if theme_column in df.columns:
themes = df[theme_column].unique()
# Créer un graphique pour chaque thème
for theme in themes:
# Filtrer les données pour ce thème
theme_data = df[df[theme_column] == theme]
# Tracer la ligne pour le nombre total d'objets
fig.add_trace(
go.Scatter(
x=theme_data["date"],
y=theme_data["nombre_total"],
mode="lines+markers",
name=f"{theme} - Total",
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
),
row=1,
col=1,
)
# Tracer la ligne pour le taux de complétion si disponible
if "pourcentage_completion" in theme_data.columns:
fig.add_trace(
go.Scatter(
x=theme_data["date"],
y=theme_data["pourcentage_completion"],
mode="lines+markers",
name=f"{theme} - Complétion (%)",
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
),
row=2,
col=1,
)
else:
# Si aucune colonne de thème n'est trouvée, tracer simplement le nombre total
fig.add_trace(
go.Scatter(
x=df["date"],
y=df["nombre_total"],
mode="lines+markers",
name="Total",
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
),
row=1,
col=1,
)
# Tracer la ligne pour le taux de complétion si disponible
if "pourcentage_completion" in df.columns:
fig.add_trace(
go.Scatter(
x=df["date"],
y=df["pourcentage_completion"],
mode="lines+markers",
name="Complétion (%)",
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
),
row=2,
col=1,
)
# Configurer les axes et les légendes
fig.update_xaxes(title_text="Date", row=1, col=1)
fig.update_xaxes(title_text="Date", row=2, col=1)
fig.update_yaxes(title_text="Nombre d'objets", row=1, col=1)
fig.update_yaxes(title_text="Taux de complétion (%)", range=[0, 100], row=2, col=1)
# Obtenir le thème principal (premier thème trouvé)
main_tag = (
themes[0] if theme_column in df.columns and len(themes) > 0 else "Objets OSM"
)
# Mettre à jour le titre du graphique avec le tag principal et le nom de la ville
fig.update_layout(
title=f"{main_tag} - {city_name}",
hovermode="x unified",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
height=800,
width=1000,
margin=dict(t=100, b=50, l=50, r=50),
)
# Ajouter des annotations pour les informations supplémentaires
fig.add_annotation(
text=f"Code INSEE: {insee_code}",
xref="paper",
yref="paper",
x=0.01,
y=-0.15,
showarrow=False,
font=dict(size=10),
)
# Ajouter la date de génération
now = datetime.now().strftime("%Y-%m-%d %H:%M")
fig.add_annotation(
text=f"Généré le: {now}",
xref="paper",
yref="paper",
x=0.99,
y=-0.15,
showarrow=False,
font=dict(size=8),
align="right",
)
# Sauvegarder ou afficher le graphique
if output_path:
fig.write_html(output_path)
print(f"Graphique interactif sauvegardé: {output_path}")
elif csv_file:
# Déterminer un chemin de sortie par défaut basé sur le fichier CSV
base_name = os.path.splitext(csv_file)[0]
default_output = f"{base_name}_plotly.html"
fig.write_html(default_output)
print(f"Graphique interactif sauvegardé: {default_output}")
else:
# Si aucun chemin de sortie n'est spécifié et aucun fichier CSV n'est fourni,
# utiliser un nom par défaut basé sur le code INSEE et le thème
default_output = f"commune_{insee_code}_{main_tag}_plotly.html"
fig.write_html(default_output)
print(f"Graphique interactif sauvegardé: {default_output}")
def main():
"""Fonction principale."""
# Analyser les arguments de la ligne de commande
args = parse_args()
# Vérifier que le fichier CSV existe
if not os.path.isfile(args.csv_file):
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
sys.exit(1)
# Charger les données
df = load_data(args.csv_file, args.insee)
# Vérifier qu'il y a des données
if df.empty:
print(f"Aucune donnée trouvée pour le code INSEE {args.insee}.")
sys.exit(1)
# Déterminer le chemin de sortie si non spécifié
if not args.output:
# Utiliser le même nom que le fichier CSV mais avec l'extension .html
base_name = os.path.splitext(args.csv_file)[0]
output_path = f"{base_name}_plotly.html"
else:
output_path = args.output
# Générer le graphique
generate_plotly_graph(df, args.city_name, output_path, args.insee, args.csv_file)
print("Graphique interactif généré avec succès!")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,4 @@
osmupdate --verbose --keep-tempfiles --day -t=test_temp/ -v osm_data/france-internal.osh.pbf test_temp/changes.osc.gz
#osmium extract -p polyons/commune_91111.poly -s simple changes.osc.gz -O -o test_temp/changes.local.osc.gz
osmium apply-changes -H osm_data/france-internal.osh.pbf test_temp/changes.osc.gz -O -o osm_data/france-internal_updated.osh.pbf

194
counting_osm_objects/update.py Executable file
View file

@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour mettre à jour le fichier historisé france internal.
Ce script utilise osmupdate pour mettre à jour le fichier france-internal.osh.pbf
avec les dernières modifications d'OpenStreetMap.
Usage:
python update.py [--verbose]
"""
import os
import sys
import argparse
import subprocess
import logging
from datetime import datetime
# Chemin vers le répertoire du script
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Chemin vers le fichier historisé france internal
FRANCE_INTERNAL_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-internal.osh.pbf")
# Chemin vers le répertoire temporaire pour osmupdate
TEMP_DIR = os.path.join(SCRIPT_DIR, "update_temp")
# Configurer le logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
def run_command(command, verbose=False):
"""
Exécute une commande shell et retourne la sortie.
Args:
command (str): Commande à exécuter
verbose (bool): Si True, affiche la sortie de la commande en temps réel
Returns:
tuple: (code de retour, sortie standard, sortie d'erreur)
"""
logger.info(f"Exécution: {command}")
if verbose:
# Exécuter la commande avec sortie en temps réel
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1
)
stdout_lines = []
stderr_lines = []
# Lire la sortie standard en temps réel
for line in process.stdout:
line = line.strip()
stdout_lines.append(line)
print(line)
# Lire la sortie d'erreur en temps réel
for line in process.stderr:
line = line.strip()
stderr_lines.append(line)
print(f"ERREUR: {line}", file=sys.stderr)
# Attendre la fin du processus
return_code = process.wait()
return return_code, "\n".join(stdout_lines), "\n".join(stderr_lines)
else:
# Exécuter la commande sans sortie en temps réel
try:
result = subprocess.run(
command,
shell=True,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
return result.returncode, result.stdout, result.stderr
except Exception as e:
logger.error(f"Erreur lors de l'exécution de la commande: {e}")
return 1, "", str(e)
def update_france_internal(verbose=False):
"""
Met à jour le fichier historisé france internal avec osmupdate.
Args:
verbose (bool): Si True, affiche la sortie de la commande en temps réel
Returns:
bool: True si la mise à jour a réussi, False sinon
"""
# Vérifier si le fichier existe
if not os.path.isfile(FRANCE_INTERNAL_FILE):
logger.error(f"Le fichier {FRANCE_INTERNAL_FILE} n'existe pas.")
logger.error("Veuillez télécharger le fichier initial depuis Geofabrik ou une autre source.")
return False
# Créer le répertoire temporaire s'il n'existe pas
os.makedirs(TEMP_DIR, exist_ok=True)
# Chemin vers le fichier mis à jour
updated_file = os.path.join(TEMP_DIR, "france-internal-updated.osh.pbf")
# Construire la commande osmupdate
command = f"osmupdate --verbose --keep-tempfiles -t={TEMP_DIR}/temp {FRANCE_INTERNAL_FILE} {updated_file}"
# Exécuter la commande
logger.info("Mise à jour du fichier france-internal.osh.pbf en cours...")
return_code, stdout, stderr = run_command(command, verbose)
if return_code != 0:
logger.error(f"Erreur lors de la mise à jour: {stderr}")
return False
# Remplacer l'ancien fichier par le nouveau
if os.path.isfile(updated_file):
# Créer une sauvegarde de l'ancien fichier
backup_file = f"{FRANCE_INTERNAL_FILE}.bak"
try:
os.rename(FRANCE_INTERNAL_FILE, backup_file)
logger.info(f"Sauvegarde de l'ancien fichier créée: {backup_file}")
except Exception as e:
logger.error(f"Erreur lors de la création de la sauvegarde: {e}")
return False
# Déplacer le nouveau fichier
try:
os.rename(updated_file, FRANCE_INTERNAL_FILE)
logger.info(f"Fichier mis à jour avec succès: {FRANCE_INTERNAL_FILE}")
return True
except Exception as e:
logger.error(f"Erreur lors du déplacement du fichier mis à jour: {e}")
# Restaurer l'ancien fichier en cas d'erreur
try:
os.rename(backup_file, FRANCE_INTERNAL_FILE)
logger.info("Restauration de l'ancien fichier réussie.")
except Exception as e2:
logger.error(f"Erreur lors de la restauration de l'ancien fichier: {e2}")
return False
else:
logger.error(f"Le fichier mis à jour {updated_file} n'a pas été créé.")
return False
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description="Met à jour le fichier historisé france internal avec osmupdate."
)
parser.add_argument(
"--verbose", "-v", action="store_true", help="Affiche la sortie des commandes en temps réel"
)
args = parser.parse_args()
# Afficher l'heure de début
start_time = datetime.now()
logger.info(f"Début de la mise à jour: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
# Mettre à jour le fichier
success = update_france_internal(args.verbose)
# Afficher l'heure de fin et la durée
end_time = datetime.now()
duration = end_time - start_time
logger.info(f"Fin de la mise à jour: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"Durée totale: {duration}")
if success:
logger.info("Mise à jour terminée avec succès.")
return 0
else:
logger.error("Échec de la mise à jour.")
return 1
if __name__ == "__main__":
sys.exit(main())