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()