diff --git a/counting_osm_objects/.gitignore b/counting_osm_objects/.gitignore
new file mode 100644
index 0000000..7c30004
--- /dev/null
+++ b/counting_osm_objects/.gitignore
@@ -0,0 +1,8 @@
+osm_data/*pbf
+osm_data/*geojson
+polygons/*.poly
+test_data/*
+test_temp/*
+test_results/*
+osm_config.txt
+__pycache__
\ No newline at end of file
diff --git a/counting_osm_objects/README.md b/counting_osm_objects/README.md
new file mode 100644
index 0000000..7a526f1
--- /dev/null
+++ b/counting_osm_objects/README.md
@@ -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.
\ No newline at end of file
diff --git a/counting_osm_objects/counting.sh b/counting_osm_objects/counting.sh
new file mode 100755
index 0000000..090235a
--- /dev/null
+++ b/counting_osm_objects/counting.sh
@@ -0,0 +1,1050 @@
+#!/bin/bash
+
+# Script d'initialisation et de mise à jour d'une base PostgreSQL avec PostGIS
+# pour l'importation et l'analyse de données OpenStreetMap
+
+set -e # Arrêter en cas d'erreur
+
+# Fonction d'affichage
+log() {
+ echo "$(date +'%Y-%m-%d %H:%M:%S') - $1"
+}
+
+# Configuration de la base de données
+PGUSER="osm_user"
+PGDATABASE="osm_db"
+CODE_INSEE="91111" # Code INSEE par défaut (Étampes)
+OSM_OBJECT="fire_hydrant" # Objet OSM par défaut (bornes incendie)
+
+# Variable pour suivre si les identifiants ont été chargés depuis le fichier de configuration
+CONFIG_LOADED=false
+
+# Vérifier si le fichier de configuration existe et le charger
+if [ -f "osm_config.txt" ]; then
+ source osm_config.txt
+ CONFIG_LOADED=true
+ log "Utilisation des identifiants existants depuis osm_config.txt"
+else
+ # Génération d'un mot de passe aléatoire pour la première utilisation
+ PGPASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)
+fi
+
+# Vérification des prérequis
+check_prerequisites() {
+ log "Vérification des prérequis..."
+
+ # Vérifier si PostgreSQL est installé
+ if ! command -v psql &> /dev/null; then
+ log "PostgreSQL n'est pas installé. Installation en cours..."
+ sudo apt-get update && sudo apt-get install -y postgresql postgresql-contrib
+ fi
+
+ # Vérifier si PostGIS est installé
+ if ! sudo -u postgres psql -c "SELECT postgis_version();" &> /dev/null; then
+ log "PostGIS n'est pas installé. Installation en cours..."
+ sudo apt-get install -y postgis postgresql-14-postgis-3 postgresql-14-postgis-3-scripts
+ fi
+
+ # Vérifier si osmium-tool est installé
+ if ! command -v osmium &> /dev/null; then
+ log "osmium-tool n'est pas installé. Installation en cours..."
+ sudo apt-get install -y osmium-tool
+ fi
+}
+
+# Initialisation de la base de données
+init_database() {
+ log "Création de l'utilisateur et de la base de données PostgreSQL..."
+
+ # Vérifier si les identifiants ont été chargés depuis osm_config.txt
+ if [ "$CONFIG_LOADED" = false ]; then
+ # Créer l'utilisateur s'il n'existe pas déjà
+ sudo -u postgres psql -c "DO \$\$
+ BEGIN
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$PGUSER') THEN
+ CREATE USER $PGUSER WITH PASSWORD '$PGPASSWORD';
+ ELSE
+ ALTER USER $PGUSER WITH PASSWORD '$PGPASSWORD';
+ END IF;
+ END
+ \$\$;"
+ else
+ log "Utilisation de l'utilisateur existant depuis osm_config.txt"
+ fi
+
+ # Créer la base de données si elle n'existe pas déjà
+ sudo -u postgres psql -c "DROP DATABASE IF EXISTS $PGDATABASE;"
+ sudo -u postgres psql -c "CREATE DATABASE $PGDATABASE OWNER $PGUSER;"
+
+ # Activer les extensions nécessaires
+ sudo -u postgres psql -d $PGDATABASE -c "CREATE EXTENSION IF NOT EXISTS postgis;"
+ sudo -u postgres psql -d $PGDATABASE -c "CREATE EXTENSION IF NOT EXISTS hstore;"
+
+ # Donner tous les privilèges à l'utilisateur
+ sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PGDATABASE TO $PGUSER;"
+
+ # Initialiser la base de données avec osm2pgsql pour la rendre updatable
+ # Créer un fichier OSM vide pour l'initialisation
+ mkdir -p osm_data
+ cat > osm_data/empty.osm << EOF
+
+
+
+EOF
+
+ # Initialiser la base avec osm2pgsql en mode slim sans --drop
+ log "Initialisation de la base de données pour les mises à jour..."
+ PGPASSWORD=$PGPASSWORD osm2pgsql --create --database $PGDATABASE --user $PGUSER --host localhost --slim \
+ --style osm_data/style.lua --extra-attributes "osm_data/empty.osm" -O flex
+
+ log "Base de données initialisée avec succès!"
+ log "Nom d'utilisateur: $PGUSER"
+ log "Mot de passe: $PGPASSWORD"
+ log "Base de données: $PGDATABASE"
+
+ # Créer un fichier de configuration
+ cat > osm_config.txt << EOF
+PGUSER=$PGUSER
+PGPASSWORD=$PGPASSWORD
+PGDATABASE=$PGDATABASE
+EOF
+}
+
+# Téléchargement et importation des données OSM de l'Île-de-France
+import_osm_data() {
+ log "Téléchargement des données OpenStreetMap pour l'Île-de-France..."
+
+ # Créer un répertoire pour les données
+ mkdir -p osm_data
+
+ # Télécharger les données historiques de l'Île-de-France depuis Geofabrik
+ if [ ! -f osm_data/ile-de-france-internal.osh.pbf ]; then
+ wget -O osm_data/ile-de-france-internal.osh.pbf https://osm-internal.download.geofabrik.de/europe/france/ile-de-france-internal.osh.pbf
+ else
+ log "Le fichier historique existe déjà, vérification de la dernière version..."
+ wget -N -P osm_data https://osm-internal.download.geofabrik.de/europe/france/ile-de-france-internal.osh.pbf
+ fi
+
+ # Vérifier si la base de données existe, sinon la créer
+ if ! sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$PGDATABASE"; then
+ log "La base de données $PGDATABASE n'existe pas. Création en cours..."
+ sudo -u postgres psql -c "CREATE DATABASE $PGDATABASE OWNER $PGUSER;"
+ fi
+
+ # Vérifier et activer l'extension PostGIS si nécessaire
+ if ! PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "SELECT postgis_version();" &> /dev/null; then
+ log "L'extension PostGIS n'est pas activée. Activation en cours..."
+ sudo -u postgres psql -d $PGDATABASE -c "CREATE EXTENSION IF NOT EXISTS postgis;"
+ sudo -u postgres psql -d $PGDATABASE -c "CREATE EXTENSION IF NOT EXISTS hstore;"
+ sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PGDATABASE TO $PGUSER;"
+ fi
+
+ log "Importation des données dans PostgreSQL..."
+
+ # Création d'un fichier de style minimal pour osm2pgsql
+ cat > osm_data/style.lua << EOF
+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
+
+
+EOF
+
+ # Déterminer si le fichier est un fichier de changement ou d'historique
+ OSM_FILE="osm_data/test.osc"
+ FILE_EXT="${OSM_FILE##*.}"
+
+ # Utiliser le mode append pour les fichiers .osc ou .osh.pbf (fichiers de changement ou d'historique)
+ if [[ "$OSM_FILE" == *.osc || "$OSM_FILE" == *.osh.pbf ]]; then
+ log "Détection d'un fichier de changement ou d'historique. Utilisation du mode append..."
+ # Importation avec osm2pgsql en mode append pour les fichiers de changement
+ PGPASSWORD=$PGPASSWORD osm2pgsql --database $PGDATABASE --append --user $PGUSER --host localhost --slim \
+ --style osm_data/style.lua --extra-attributes "$OSM_FILE" -O flex
+ else
+ # Importation standard avec osm2pgsql pour les fichiers .osm ou .pbf
+ # Utiliser --slim sans --drop pour créer une base de données updatable
+ PGPASSWORD=$PGPASSWORD osm2pgsql --create --database $PGDATABASE --user $PGUSER --host localhost --slim \
+ --style osm_data/style.lua --extra-attributes "$OSM_FILE" -O flex
+ fi
+
+ log "Données importées avec succès!"
+}
+
+# Analyse des objets OpenStreetMap pour une commune spécifique
+analyse_osm_objects() {
+ # Créer une table pour stocker les résultats d'analyse
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ CREATE TABLE IF NOT EXISTS osm_analysis (
+ id SERIAL PRIMARY KEY,
+ date_analyse DATE DEFAULT CURRENT_DATE,
+ code_insee TEXT,
+ type_objet TEXT,
+ nombre_total INTEGER,
+ nombre_avec_attr1 INTEGER,
+ nombre_avec_attr2 INTEGER,
+ pourcentage_completion NUMERIC(5,2)
+ );"
+
+ # Fonction pour analyser un type d'objet spécifique
+ analyse_object_type() {
+ local object_type=$1
+ log "Analyse des $object_type pour la commune avec le code INSEE $CODE_INSEE..."
+
+ # Supprimer les analyses existantes pour éviter les doublons
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ DELETE FROM osm_analysis
+ WHERE code_insee = '$CODE_INSEE'
+ AND type_objet = '$object_type'
+ AND date_analyse::date = CURRENT_DATE;"
+
+ # Exécuter l'analyse en fonction du type d'objet
+ case $object_type in
+ "fire_hydrant")
+ # Analyse des bornes incendie (attributs: ref, color)
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (code_insee, type_objet, nombre_total, nombre_avec_ref, nombre_avec_color, pourcentage_completion)
+ SELECT
+ '$CODE_INSEE' as code_insee,
+ '$object_type' as type_objet,
+ COUNT(DISTINCT id_column) as total,
+ COUNT(DISTINCT CASE WHEN ref IS NOT NULL THEN id_column END) as avec_ref,
+ COUNT(DISTINCT CASE WHEN color IS NOT NULL THEN id_column END) as avec_color,
+ CASE
+ WHEN COUNT(DISTINCT id_column) = 0 THEN 0
+ ELSE ROUND(((COUNT(DISTINCT CASE WHEN ref IS NOT NULL THEN id_column END) +
+ COUNT(DISTINCT CASE WHEN color IS NOT NULL THEN id_column END))::numeric /
+ (COUNT(DISTINCT id_column) * 2)::numeric) * 100, 2)
+ END as pourcentage_completion
+ FROM (
+ SELECT id_column, ref, color, insee, version
+ FROM fire_hydrants
+ WHERE insee = '$CODE_INSEE'
+ AND version = (
+ SELECT MAX(version)
+ FROM fire_hydrants AS fh2
+ WHERE fh2.id_column = fire_hydrants.id_column
+ )
+ ) AS latest_versions;"
+ ;;
+ "tree")
+ # Analyse des arbres (attributs: species, height)
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (code_insee, type_objet, nombre_total, nombre_avec_ref, nombre_avec_color, pourcentage_completion)
+ SELECT
+ '$CODE_INSEE' as code_insee,
+ '$object_type' as type_objet,
+ COUNT(DISTINCT id_column) as total,
+ COUNT(DISTINCT CASE WHEN species IS NOT NULL THEN id_column END) as avec_species,
+ COUNT(DISTINCT CASE WHEN height IS NOT NULL THEN id_column END) as avec_height,
+ CASE
+ WHEN COUNT(DISTINCT id_column) = 0 THEN 0
+ ELSE ROUND(((COUNT(DISTINCT CASE WHEN species IS NOT NULL THEN id_column END) +
+ COUNT(DISTINCT CASE WHEN height IS NOT NULL THEN id_column END))::numeric /
+ (COUNT(DISTINCT id_column) * 2)::numeric) * 100, 2)
+ END as pourcentage_completion
+ FROM (
+ SELECT id_column, species, height, insee, version
+ FROM trees
+ WHERE insee = '$CODE_INSEE'
+ AND version = (
+ SELECT MAX(version)
+ FROM trees AS t2
+ WHERE t2.id_column = trees.id_column
+ )
+ ) AS latest_versions;"
+ ;;
+ "charging_station")
+ # Analyse des bornes de recharge (attributs: operator, capacity)
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (code_insee, type_objet, nombre_total, nombre_avec_ref, nombre_avec_color, pourcentage_completion)
+ SELECT
+ '$CODE_INSEE' as code_insee,
+ '$object_type' as type_objet,
+ COUNT(DISTINCT id_column) as total,
+ COUNT(DISTINCT CASE WHEN operator IS NOT NULL THEN id_column END) as avec_operator,
+ COUNT(DISTINCT CASE WHEN capacity IS NOT NULL THEN id_column END) as avec_capacity,
+ CASE
+ WHEN COUNT(DISTINCT id_column) = 0 THEN 0
+ ELSE ROUND(((COUNT(DISTINCT CASE WHEN operator IS NOT NULL THEN id_column END) +
+ COUNT(DISTINCT CASE WHEN capacity IS NOT NULL THEN id_column END))::numeric /
+ (COUNT(DISTINCT id_column) * 2)::numeric) * 100, 2)
+ END as pourcentage_completion
+ FROM (
+ SELECT id_column, operator, capacity, insee, version
+ FROM charging_stations
+ WHERE insee = '$CODE_INSEE'
+ AND version = (
+ SELECT MAX(version)
+ FROM charging_stations AS cs2
+ WHERE cs2.id_column = charging_stations.id_column
+ )
+ ) AS latest_versions;"
+ ;;
+ *)
+ log "Type d'objet non reconnu: $object_type"
+ return 1
+ ;;
+ esac
+
+ # Afficher les résultats pour ce type d'objet
+ log "Résultats de l'analyse pour $object_type :"
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ SELECT * FROM osm_analysis
+ WHERE code_insee = '$CODE_INSEE'
+ AND type_objet = '$object_type'
+ AND date_analyse::date = CURRENT_DATE;"
+ }
+
+ # Vérifier si on analyse tous les types d'objets ou un seul
+ if [ "$OSM_OBJECT" == "all" ]; then
+ log "Analyse de tous les types d'objets pour la commune avec le code INSEE $CODE_INSEE..."
+
+ # Analyser chaque type d'objet
+ analyse_object_type "fire_hydrant"
+ analyse_object_type "tree"
+ analyse_object_type "charging_station"
+
+ # Afficher un résumé de tous les objets
+ log "Résumé de tous les types d'objets :"
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ SELECT
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE code_insee = '$CODE_INSEE'
+ AND date_analyse::date = CURRENT_DATE
+ ORDER BY type_objet;"
+
+ # Afficher le total de tous les objets
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ SELECT
+ 'TOTAL' as type_objet,
+ SUM(nombre_total) as nombre_total,
+ SUM(nombre_avec_attr1) as nombre_avec_attr1,
+ SUM(nombre_avec_attr2) as nombre_avec_attr2,
+ CASE
+ WHEN SUM(nombre_total) = 0 THEN 0
+ ELSE ROUND(((SUM(nombre_avec_attr1) + SUM(nombre_avec_attr2))::numeric / (SUM(nombre_total) * 2)::numeric) * 100, 2)
+ END as pourcentage_completion
+ FROM osm_analysis
+ WHERE code_insee = '$CODE_INSEE'
+ AND date_analyse::date = CURRENT_DATE;"
+ else
+ # Analyser un seul type d'objet
+ analyse_object_type "$OSM_OBJECT"
+ fi
+}
+
+# Exportation des données historiques en CSV
+export_historical_data() {
+ log "Exportation des données historiques..."
+
+ # Création du fichier CSV pour les données annuelles (10 dernières années)
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ COPY (
+ WITH latest_annual_data AS (
+ SELECT DISTINCT ON (year_date, code_insee, type_objet)
+ EXTRACT(YEAR FROM date_analyse) AS year_date,
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion,
+ date_analyse
+ FROM osm_analysis
+ WHERE date_analyse >= (CURRENT_DATE - INTERVAL '10 years')
+ ORDER BY year_date, code_insee, type_objet, date_analyse DESC
+ )
+ SELECT
+ year_date AS annee,
+ code_insee,
+ type_objet,
+ nombre_total,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'ref'
+ WHEN type_objet = 'tree' THEN 'species'
+ WHEN type_objet = 'charging_station' THEN 'operator'
+ ELSE 'attr1'
+ END AS attr1_name,
+ nombre_avec_attr1,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'color'
+ WHEN type_objet = 'tree' THEN 'height'
+ WHEN type_objet = 'charging_station' THEN 'capacity'
+ ELSE 'attr2'
+ END AS attr2_name,
+ nombre_avec_attr2,
+ pourcentage_completion,
+ 'annual' AS periode
+ FROM latest_annual_data
+ ORDER BY year_date, code_insee, type_objet
+ ) TO STDOUT WITH CSV HEADER
+ " > historique_osm_$CODE_INSEE.csv
+
+ # Ajout des données mensuelles pour le premier jour de chaque mois des 10 dernières années
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ COPY (
+ WITH months AS (
+ SELECT generate_series(
+ date_trunc('month', CURRENT_DATE - INTERVAL '10 years'),
+ date_trunc('month', CURRENT_DATE),
+ interval '1 month'
+ ) AS month_start
+ ),
+ monthly_data AS (
+ SELECT DISTINCT ON (m.month_start, a.code_insee, a.type_objet)
+ m.month_start,
+ a.code_insee,
+ a.type_objet,
+ a.nombre_total,
+ a.nombre_avec_attr1,
+ a.nombre_avec_attr2,
+ a.pourcentage_completion
+ FROM months m
+ LEFT JOIN LATERAL (
+ SELECT *
+ FROM osm_analysis
+ WHERE date_analyse <= m.month_start
+ AND code_insee = '$CODE_INSEE'
+ ORDER BY date_analyse DESC
+ LIMIT 1
+ ) a ON true
+ WHERE a.id IS NOT NULL
+ ORDER BY m.month_start, a.code_insee, a.type_objet, a.date_analyse DESC
+ )
+ SELECT
+ TO_CHAR(month_start, 'YYYY-MM') AS annee_mois,
+ code_insee,
+ type_objet,
+ nombre_total,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'ref'
+ WHEN type_objet = 'tree' THEN 'species'
+ WHEN type_objet = 'charging_station' THEN 'operator'
+ ELSE 'attr1'
+ END AS attr1_name,
+ nombre_avec_attr1,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'color'
+ WHEN type_objet = 'tree' THEN 'height'
+ WHEN type_objet = 'charging_station' THEN 'capacity'
+ ELSE 'attr2'
+ END AS attr2_name,
+ nombre_avec_attr2,
+ pourcentage_completion,
+ 'monthly' AS periode
+ FROM monthly_data
+ ORDER BY month_start DESC, code_insee, type_objet
+ ) TO STDOUT WITH CSV
+ " >> historique_osm_$CODE_INSEE.csv
+
+ # Ajout des données quotidiennes (30 derniers jours)
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ COPY (
+ WITH daily_data AS (
+ SELECT DISTINCT ON (date_analyse, code_insee, type_objet)
+ date_analyse,
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse >= (CURRENT_DATE - INTERVAL '30 days')
+ ORDER BY date_analyse, code_insee, type_objet, id DESC
+ )
+ SELECT
+ TO_CHAR(date_analyse, 'YYYY-MM-DD') AS jour,
+ code_insee,
+ type_objet,
+ nombre_total,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'ref'
+ WHEN type_objet = 'tree' THEN 'species'
+ WHEN type_objet = 'charging_station' THEN 'operator'
+ ELSE 'attr1'
+ END AS attr1_name,
+ nombre_avec_attr1,
+ CASE
+ WHEN type_objet = 'fire_hydrant' THEN 'color'
+ WHEN type_objet = 'tree' THEN 'height'
+ WHEN type_objet = 'charging_station' THEN 'capacity'
+ ELSE 'attr2'
+ END AS attr2_name,
+ nombre_avec_attr2,
+ pourcentage_completion,
+ 'daily' AS periode
+ FROM daily_data
+ ORDER BY date_analyse DESC, code_insee, type_objet
+ ) TO STDOUT WITH CSV
+ " >> historique_osm_$CODE_INSEE.csv
+
+ # Générer les graphiques d'évolution
+ log "Génération des graphiques d'évolution..."
+
+ # Vérifier si Python est installé
+ if command -v python3 &> /dev/null; then
+ # Vérifier si les bibliothèques nécessaires sont installées
+ if ! python3 -c "import pandas, matplotlib" &> /dev/null; then
+ log "Installation des bibliothèques Python nécessaires..."
+ pip install pandas matplotlib
+ fi
+
+ # Générer les graphiques pour les différentes périodes
+ log "Génération du graphique pour les données mensuelles..."
+ python3 generate_graph.py "historique_osm_$CODE_INSEE.csv" --period monthly
+
+ log "Génération du graphique pour les données annuelles..."
+ python3 generate_graph.py "historique_osm_$CODE_INSEE.csv" --period annual
+
+ log "Graphiques générés avec succès!"
+ else
+ log "Python n'est pas installé. Les graphiques n'ont pas pu être générés."
+ log "Pour générer les graphiques manuellement, installez Python et exécutez:"
+ log "python3 generate_graph.py historique_osm_$CODE_INSEE.csv --period monthly"
+ fi
+
+ # Ajout des données avec les changements sur différentes périodes
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ COPY (
+ WITH current_data AS (
+ SELECT
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse = CURRENT_DATE
+ AND code_insee = '$CODE_INSEE'
+ ),
+ data_24h AS (
+ SELECT
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse = CURRENT_DATE - INTERVAL '1 day'
+ AND code_insee = '$CODE_INSEE'
+ ),
+ data_7d AS (
+ SELECT
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse = CURRENT_DATE - INTERVAL '7 days'
+ AND code_insee = '$CODE_INSEE'
+ ),
+ data_15d AS (
+ SELECT
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse = CURRENT_DATE - INTERVAL '15 days'
+ AND code_insee = '$CODE_INSEE'
+ ),
+ data_30d AS (
+ SELECT
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ FROM osm_analysis
+ WHERE date_analyse = CURRENT_DATE - INTERVAL '30 days'
+ AND code_insee = '$CODE_INSEE'
+ )
+ SELECT
+ TO_CHAR(CURRENT_DATE, 'YYYY-MM-DD') AS date_actuelle,
+ c.code_insee,
+ c.type_objet,
+ c.nombre_total,
+ c.nombre_avec_attr1,
+ c.nombre_avec_attr2,
+ c.pourcentage_completion,
+
+ -- Changements sur 24h
+ COALESCE(c.nombre_total - d24.nombre_total, 0) AS delta_total_24h,
+ COALESCE(c.nombre_avec_attr1 - d24.nombre_avec_attr1, 0) AS delta_attr1_24h,
+ COALESCE(c.nombre_avec_attr2 - d24.nombre_avec_attr2, 0) AS delta_attr2_24h,
+ COALESCE(c.pourcentage_completion - d24.pourcentage_completion, 0) AS delta_completion_24h,
+
+ -- Changements sur 7 jours
+ COALESCE(c.nombre_total - d7.nombre_total, 0) AS delta_total_7d,
+ COALESCE(c.nombre_avec_attr1 - d7.nombre_avec_attr1, 0) AS delta_attr1_7d,
+ COALESCE(c.nombre_avec_attr2 - d7.nombre_avec_attr2, 0) AS delta_attr2_7d,
+ COALESCE(c.pourcentage_completion - d7.pourcentage_completion, 0) AS delta_completion_7d,
+
+ -- Changements sur 15 jours
+ COALESCE(c.nombre_total - d15.nombre_total, 0) AS delta_total_15d,
+ COALESCE(c.nombre_avec_attr1 - d15.nombre_avec_attr1, 0) AS delta_attr1_15d,
+ COALESCE(c.nombre_avec_attr2 - d15.nombre_avec_attr2, 0) AS delta_attr2_15d,
+ COALESCE(c.pourcentage_completion - d15.pourcentage_completion, 0) AS delta_completion_15d,
+
+ -- Changements sur 30 jours
+ COALESCE(c.nombre_total - d30.nombre_total, 0) AS delta_total_30d,
+ COALESCE(c.nombre_avec_attr1 - d30.nombre_avec_attr1, 0) AS delta_attr1_30d,
+ COALESCE(c.nombre_avec_attr2 - d30.nombre_avec_attr2, 0) AS delta_attr2_30d,
+ COALESCE(c.pourcentage_completion - d30.pourcentage_completion, 0) AS delta_completion_30d,
+
+ 'changes' AS periode
+ FROM current_data c
+ LEFT JOIN data_24h d24 ON c.code_insee = d24.code_insee AND c.type_objet = d24.type_objet
+ LEFT JOIN data_7d d7 ON c.code_insee = d7.code_insee AND c.type_objet = d7.type_objet
+ LEFT JOIN data_15d d15 ON c.code_insee = d15.code_insee AND c.type_objet = d15.type_objet
+ LEFT JOIN data_30d d30 ON c.code_insee = d30.code_insee AND c.type_objet = d30.type_objet
+ ) TO STDOUT WITH CSV
+ " >> historique_osm_$CODE_INSEE.csv
+
+ log "Données exportées dans le fichier historique_osm_$CODE_INSEE.csv"
+}
+
+# Suppression des tables de la base de données
+delete_database_tables() {
+ log "Suppression des tables de la base de données..."
+
+ # Demander confirmation avant de supprimer
+ echo -n "Êtes-vous sûr de vouloir supprimer toutes les tables? Cette action est irréversible. (o/n): "
+ read confirm
+ if [[ "$confirm" != "o" && "$confirm" != "O" ]]; then
+ log "Suppression annulée."
+ return
+ fi
+
+ # Supprimer les tables principales
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ DROP TABLE IF EXISTS fire_hydrants CASCADE;
+ DROP TABLE IF EXISTS trees CASCADE;
+ DROP TABLE IF EXISTS charging_stations CASCADE;
+ DROP TABLE IF EXISTS charging_stations_ways CASCADE;
+ DROP TABLE IF EXISTS osm_analysis CASCADE;
+ "
+
+ log "Tables supprimées avec succès!"
+}
+
+# Simulation de données historiques (pour avoir des données à exporter)
+simulate_historical_data() {
+ log "Simulation de données historiques pour les 5 dernières années..."
+
+ # Déterminer les types d'objets à simuler
+ local object_types=()
+ if [ "$OSM_OBJECT" == "all" ]; then
+ object_types=("fire_hydrant" "tree" "charging_station")
+ else
+ object_types=("$OSM_OBJECT")
+ fi
+
+ # Générer des données pour chaque type d'objet
+ for obj_type in "${object_types[@]}"; do
+ log "Simulation de données pour $obj_type..."
+
+ # Générer des données annuelles (5 dernières années)
+ for i in {5..1}; do
+ year=$(($(date +%Y) - $i))
+ completion=$((50 + $i * 10)) # Augmentation progressive de la complétion
+
+ # Récupérer les valeurs réelles de la base de données
+ case $obj_type in
+ "fire_hydrant")
+ # Récupérer le nombre total de bornes incendie
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM fire_hydrants WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-20}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * i / 5)) # ref
+ attr2=$((total * i / 4)) # color
+ ;;
+ "tree")
+ # Récupérer le nombre total d'arbres
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM trees WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-50}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * i / 5)) # species
+ attr2=$((total * i / 8)) # height
+ ;;
+ "charging_station")
+ # Récupérer le nombre total de bornes de recharge
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM charging_stations WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-10}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * i / 5)) # operator
+ attr2=$((total * i / 4)) # capacity
+ ;;
+ esac
+
+ # Insérer les données annuelles
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (
+ date_analyse,
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ ) VALUES (
+ '$year-01-01',
+ '$CODE_INSEE',
+ '$obj_type',
+ $total,
+ $attr1,
+ $attr2,
+ $completion
+ );"
+ done
+
+ # Générer des données mensuelles (12 derniers mois)
+ for i in {12..1}; do
+ month_date=$(date -d "$(date +%Y-%m-01) -$i month" +%Y-%m-%d)
+ completion=$((70 + $i * 2)) # Variation mensuelle
+
+ # Récupérer les valeurs réelles de la base de données
+ case $obj_type in
+ "fire_hydrant")
+ # Récupérer le nombre total de bornes incendie
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM fire_hydrants WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-25}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (10 + $i) / 30)) # ref
+ attr2=$((total * (15 + $i) / 30)) # color
+ ;;
+ "tree")
+ # Récupérer le nombre total d'arbres
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM trees WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-60}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (30 + $i) / 60)) # species
+ attr2=$((total * (20 + $i) / 60)) # height
+ ;;
+ "charging_station")
+ # Récupérer le nombre total de bornes de recharge
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM charging_stations WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-15}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (5 + $i) / 15)) # operator
+ attr2=$((total * (8 + $i) / 15)) # capacity
+ ;;
+ esac
+
+ # Insérer les données mensuelles
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (
+ date_analyse,
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ ) VALUES (
+ '$month_date',
+ '$CODE_INSEE',
+ '$obj_type',
+ $total,
+ $attr1,
+ $attr2,
+ $completion
+ );"
+ done
+
+ # Générer des données quotidiennes (30 derniers jours)
+ for i in {30..1}; do
+ day_date=$(date -d "$(date +%Y-%m-%d) -$i day" +%Y-%m-%d)
+ completion=$((85 + ($i % 10))) # Petite variation quotidienne
+
+ # Récupérer les valeurs réelles de la base de données
+ case $obj_type in
+ "fire_hydrant")
+ # Récupérer le nombre total de bornes incendie
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM fire_hydrants WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-30}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (20 + ($i % 5)) / 30)) # ref
+ attr2=$((total * (25 + ($i % 3)) / 30)) # color
+ ;;
+ "tree")
+ # Récupérer le nombre total d'arbres
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM trees WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-70}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (40 + ($i % 8)) / 70)) # species
+ attr2=$((total * (30 + ($i % 6)) / 70)) # height
+ ;;
+ "charging_station")
+ # Récupérer le nombre total de bornes de recharge
+ total=$(PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -t -c "
+ SELECT COUNT(*) FROM charging_stations WHERE insee = '$CODE_INSEE';")
+ # Si aucun résultat, utiliser une valeur par défaut
+ total=${total:-20}
+ # Trim whitespace
+ total=$(echo $total | xargs)
+
+ # Calculer des valeurs simulées pour les attributs basées sur le total réel
+ attr1=$((total * (10 + ($i % 4)) / 20)) # operator
+ attr2=$((total * (15 + ($i % 3)) / 20)) # capacity
+ ;;
+ esac
+
+ # Insérer les données quotidiennes
+ PGPASSWORD=$PGPASSWORD psql -h localhost -U $PGUSER -d $PGDATABASE -c "
+ INSERT INTO osm_analysis (
+ date_analyse,
+ code_insee,
+ type_objet,
+ nombre_total,
+ nombre_avec_attr1,
+ nombre_avec_attr2,
+ pourcentage_completion
+ ) VALUES (
+ '$day_date',
+ '$CODE_INSEE',
+ '$obj_type',
+ $total,
+ $attr1,
+ $attr2,
+ $completion
+ );"
+ done
+ done
+
+ log "Données historiques simulées avec succès!"
+}
+
+# Menu principal
+show_menu() {
+ echo ""
+ echo "===== GESTIONNAIRE DE DONNÉES OSM ====="
+ echo "1. Initialiser la base de données PostgreSQL"
+ echo "2. Importer les données OSM de l'Île-de-France"
+ echo "3. Analyser les objets OSM pour une commune"
+ echo "4. Exporter les données historiques en CSV"
+ echo "5. Supprimer les tables de la base de données"
+ echo "6. Quitter"
+ echo "========================================"
+ echo -n "Votre choix: "
+ read choice
+
+ case $choice in
+ 1) check_prerequisites && init_database && show_menu ;;
+ 2) import_osm_data && show_menu ;;
+ 3)
+ echo -n "Entrez le code INSEE de la commune (ou appuyez sur Entrée pour utiliser $CODE_INSEE): "
+ read insee
+ if [ ! -z "$insee" ]; then
+ CODE_INSEE=$insee
+ fi
+
+ echo ""
+ echo "Sélectionnez le type d'objet à analyser:"
+ echo "1. Bornes incendie (fire_hydrant)"
+ echo "2. Arbres (tree)"
+ echo "3. Bornes de recharge (charging_station)"
+ echo "4. Tous les types d'objets"
+ echo -n "Votre choix (1-4): "
+ read object_choice
+
+ case $object_choice in
+ 1) OSM_OBJECT="fire_hydrant" ;;
+ 2) OSM_OBJECT="tree" ;;
+ 3) OSM_OBJECT="charging_station" ;;
+ 4) OSM_OBJECT="all" ;;
+ *) log "Choix invalide, utilisation du type par défaut: $OSM_OBJECT" ;;
+ esac
+
+ analyse_osm_objects && show_menu
+ ;;
+ 4)
+ echo ""
+ echo "Sélectionnez le type d'objet pour l'exportation (ou 'all' pour tous):"
+ echo "1. Bornes incendie (fire_hydrant)"
+ echo "2. Arbres (tree)"
+ echo "3. Bornes de recharge (charging_station)"
+ echo "4. Tous les types d'objets"
+ echo -n "Votre choix (1-4): "
+ read export_choice
+
+ case $export_choice in
+ 1) OSM_OBJECT="fire_hydrant" ;;
+ 2) OSM_OBJECT="tree" ;;
+ 3) OSM_OBJECT="charging_station" ;;
+ 4) OSM_OBJECT="all" ;;
+ *) log "Choix invalide, utilisation du type par défaut: $OSM_OBJECT" ;;
+ esac
+
+ # Utiliser les données réelles uniquement
+ log "Utilisation des données réelles pour l'exportation..."
+
+ export_historical_data && show_menu
+ ;;
+ 5) delete_database_tables && show_menu ;;
+ 6) log "Merci d'avoir utilisé le gestionnaire de données OSM!" && exit 0 ;;
+ *) log "Option invalide, veuillez réessayer." && show_menu ;;
+ esac
+}
+
+# Démarrer le script
+log "Démarrage du script de gestion des données OpenStreetMap..."
+show_menu
\ No newline at end of file
diff --git a/counting_osm_objects/dashboard_zone.py b/counting_osm_objects/dashboard_zone.py
new file mode 100644
index 0000000..7fb7b0e
--- /dev/null
+++ b/counting_osm_objects/dashboard_zone.py
@@ -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
diff --git a/counting_osm_objects/generate_graph.py b/counting_osm_objects/generate_graph.py
new file mode 100755
index 0000000..6e85b38
--- /dev/null
+++ b/counting_osm_objects/generate_graph.py
@@ -0,0 +1,395 @@
+#!/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
+
+
+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 = ""
+ # 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]
+ 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]
+ 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
+ if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
+ insee_code = df["code_insee"].iloc[0]
+ plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
+ elif "zone" in df.columns and len(df["zone"].unique()) == 1:
+ zone = df["zone"].iloc[0]
+ plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
+
+ # 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()
diff --git a/counting_osm_objects/get_all_polys.py b/counting_osm_objects/get_all_polys.py
new file mode 100755
index 0000000..68595e2
--- /dev/null
+++ b/counting_osm_objects/get_all_polys.py
@@ -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())
diff --git a/counting_osm_objects/get_poly.py b/counting_osm_objects/get_poly.py
new file mode 100644
index 0000000..9362712
--- /dev/null
+++ b/counting_osm_objects/get_poly.py
@@ -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())
diff --git a/counting_osm_objects/historize_zone.py b/counting_osm_objects/historize_zone.py
new file mode 100755
index 0000000..ae07dd5
--- /dev/null
+++ b/counting_osm_objects/historize_zone.py
@@ -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}"
+
+ # 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())
diff --git a/counting_osm_objects/loop_thematics_history_in_zone_to_counts.py b/counting_osm_objects/loop_thematics_history_in_zone_to_counts.py
new file mode 100644
index 0000000..d68ce62
--- /dev/null
+++ b/counting_osm_objects/loop_thematics_history_in_zone_to_counts.py
@@ -0,0 +1,945 @@
+#!/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
+ 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):
+ date_str = f"{year}-01-01T00:00:00Z"
+ if date_str not in dates: # Éviter les doublons
+ dates.append(date_str)
+
+ # 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()
diff --git a/counting_osm_objects/osm-commerces-villes-export.csv b/counting_osm_objects/osm-commerces-villes-export.csv
new file mode 100644
index 0000000..0e1316e
--- /dev/null
+++ b/counting_osm_objects/osm-commerces-villes-export.csv
@@ -0,0 +1,193 @@
+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,,,,,,,,,,
+55153,Dieppe-sous-Douaumont,49.2217000,5.5201000,193,302817.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,,,,,,,,,,,,,,,
diff --git a/counting_osm_objects/osm_data/empty.osm b/counting_osm_objects/osm_data/empty.osm
new file mode 100644
index 0000000..fe8c72e
--- /dev/null
+++ b/counting_osm_objects/osm_data/empty.osm
@@ -0,0 +1,3 @@
+
+
+
diff --git a/counting_osm_objects/osm_data/style.lua b/counting_osm_objects/osm_data/style.lua
new file mode 100644
index 0000000..8fe235c
--- /dev/null
+++ b/counting_osm_objects/osm_data/style.lua
@@ -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
+
+
diff --git a/counting_osm_objects/plotly_city.py b/counting_osm_objects/plotly_city.py
new file mode 100755
index 0000000..d165a25
--- /dev/null
+++ b/counting_osm_objects/plotly_city.py
@@ -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}
Nombre: %{y}",
+ ),
+ 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}
Complétion: %{y}%",
+ ),
+ 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}
Nombre: %{y}",
+ ),
+ 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}
Complétion: %{y}%",
+ ),
+ 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()