diff --git a/README.md b/README.md index 298b4fbb..9b1b08d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ déployer sur un serveur ayant du php 8 - PHP 8.1 ou supérieur - Composer -- PostgreSQL 13 ou supérieur, ou Mysql / MariaDB +- PostgreSQL 13 ou supériesdfsdffgdfgfdgur, ou Mysql / MariaDB - Symfony 7.2 - Extensions PHP requises : - pdo_pgsql @@ -107,6 +107,29 @@ Ajoute une nouvelle ville à la base de données avec son code INSEE : php bin/console app:labourage [insee_code] ``` +### Création des Stats manquantes à partir du CSV +Examine le fichier CSV des communes et crée des objets Stats pour les communes qui n'en ont pas encore : +```shell +php bin/console app:create-missing-stats-from-csv [options] +``` + +Options disponibles : +- `--limit=N` ou `-l N` : Limite le nombre de communes à traiter +- `--dry-run` : Simule sans modifier la base de données + +Cette commande utilise le fichier `communes_france.csv` à la racine du projet et crée des objets Stats pour les communes qui n'en ont pas encore. Les objets sont créés avec les informations du CSV et complétés avec des données supplémentaires (coordonnées, budget, etc.). Les objets sont sauvegardés par paquets de 100 pour optimiser les performances. + +# Routes d'administration + +## Création des Stats manquantes à partir du CSV +Examine le fichier CSV des communes et crée des objets Stats pour les communes manquantes : +``` +/admin/create-missing-stats-from-csv +``` +Cette route lit le fichier `communes_france.csv` à la racine du projet et crée des objets Stats pour les communes qui n'en ont pas encore. Les objets sont créés avec les informations du CSV uniquement (sans labourage) et sont sauvegardés par paquets de 100. + +Pour plus de détails, consultez la [documentation dédiée](docs/create_missing_stats.md). + ## Commandes de maintenance ### Nettoyage du cache diff --git a/counting_osm_objects/.gitignore b/counting_osm_objects/.gitignore index 5fb9335d..ae2a1d0c 100644 --- a/counting_osm_objects/.gitignore +++ b/counting_osm_objects/.gitignore @@ -9,4 +9,6 @@ resultats/* osm_config.txt __pycache__ secrets.sh -cookie.txt \ No newline at end of file +cookie.txt +bin/venv +activate diff --git a/counting_osm_objects/city_analysis/analyse_commune_59140.json b/counting_osm_objects/city_analysis/analyse_commune_59140.json new file mode 100644 index 00000000..3b0b3850 --- /dev/null +++ b/counting_osm_objects/city_analysis/analyse_commune_59140.json @@ -0,0 +1,247 @@ +{ + "themes": { + "borne-de-recharge": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "borne-de-recharge", + "nombre_total": 0, + "nombre_avec_operator": 0, + "nombre_avec_capacity": 0, + "pourcentage_completion": 0 + }, + "borne-incendie": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "borne-incendie", + "nombre_total": 0, + "nombre_avec_ref": 0, + "nombre_avec_colour": 0, + "pourcentage_completion": 0 + }, + "arbres": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "arbres", + "nombre_total": 0, + "nombre_avec_species": 0, + "nombre_avec_leaf_type": 0, + "pourcentage_completion": 0 + }, + "defibrillator": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "defibrillator", + "nombre_total": 0, + "nombre_avec_operator": 0, + "nombre_avec_access": 0, + "pourcentage_completion": 0 + }, + "toilets": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "toilets", + "nombre_total": 0, + "nombre_avec_access": 0, + "nombre_avec_wheelchair": 0, + "pourcentage_completion": 0 + }, + "bus_stop": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "bus_stop", + "nombre_total": 3, + "nombre_avec_name": 3, + "nombre_avec_shelter": 3, + "pourcentage_completion": 100.0 + }, + "camera": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "camera", + "nombre_total": 0, + "nombre_avec_operator": 0, + "nombre_avec_surveillance": 0, + "pourcentage_completion": 0 + }, + "recycling": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "recycling", + "nombre_total": 0, + "nombre_avec_recycling_type": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0 + }, + "substation": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "substation", + "nombre_total": 2, + "nombre_avec_operator": 0, + "nombre_avec_voltage": 0, + "pourcentage_completion": 0.0 + }, + "laboratory": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "laboratory", + "nombre_total": 0, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0 + }, + "school": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "school", + "nombre_total": 1, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0.0 + }, + "police": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "police", + "nombre_total": 0, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0 + }, + "healthcare": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "healthcare", + "nombre_total": 0, + "nombre_avec_name": 0, + "nombre_avec_healthcare": 0, + "pourcentage_completion": 0 + }, + "bicycle_parking": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "bicycle_parking", + "nombre_total": 0, + "nombre_avec_capacity": 0, + "nombre_avec_covered": 0, + "pourcentage_completion": 0 + }, + "advertising_board": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "advertising_board", + "nombre_total": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0 + }, + "building": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "building", + "nombre_total": 620, + "nombre_avec_building": 0, + "nombre_avec_addr:housenumber": 0, + "pourcentage_completion": 0.0 + }, + "email": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "email", + "nombre_total": 2, + "nombre_avec_email": 0, + "nombre_avec_contact:email": 0, + "pourcentage_completion": 0.0 + }, + "bench": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "bench", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "waste_basket": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "waste_basket", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "street_lamp": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "street_lamp", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "drinking_water": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "drinking_water", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "power_pole": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "power_pole", + "nombre_total": 49, + "pourcentage_completion": 0 + }, + "manhole": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "manhole", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "little_free_library": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "little_free_library", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "playground": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "playground", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "siret": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "siret", + "nombre_total": 2, + "pourcentage_completion": 0 + }, + "restaurants": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "restaurants", + "nombre_total": 0, + "nombre_avec_opening_hours": 0, + "nombre_avec_contact:street": 0, + "nombre_avec_contact:housenumber": 0, + "nombre_avec_website": 0, + "nombre_avec_contact:phone": 0, + "pourcentage_completion": 0 + }, + "rnb": { + "date": "2025-08-21", + "zone": "commune_59140", + "theme": "rnb", + "nombre_total": 0, + "pourcentage_completion": 0 + } + }, + "metadata": { + "insee_code": "59140", + "creation_date": "2025-08-21 11:12:20", + "polygon_file": "commune_59140.poly", + "osm_data_file": "france-latest.osm.pbf", + "total_objects": 679, + "average_completion": 0.4418262150220913, + "theme_count": 7 + } +} \ No newline at end of file diff --git a/counting_osm_objects/city_analysis/analyse_commune_78123.json b/counting_osm_objects/city_analysis/analyse_commune_78123.json new file mode 100644 index 00000000..c106900f --- /dev/null +++ b/counting_osm_objects/city_analysis/analyse_commune_78123.json @@ -0,0 +1,247 @@ +{ + "themes": { + "borne-de-recharge": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "borne-de-recharge", + "nombre_total": 0, + "nombre_avec_operator": 0, + "nombre_avec_capacity": 0, + "pourcentage_completion": 0 + }, + "borne-incendie": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "borne-incendie", + "nombre_total": 14, + "nombre_avec_ref": 0, + "nombre_avec_colour": 0, + "pourcentage_completion": 0 + }, + "arbres": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "arbres", + "nombre_total": 78, + "nombre_avec_species": 0, + "nombre_avec_leaf_type": 2, + "pourcentage_completion": 1.28 + }, + "defibrillator": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "defibrillator", + "nombre_total": 0, + "nombre_avec_operator": 0, + "nombre_avec_access": 0, + "pourcentage_completion": 0 + }, + "toilets": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "toilets", + "nombre_total": 2, + "nombre_avec_access": 0, + "nombre_avec_wheelchair": 1, + "pourcentage_completion": 25.0 + }, + "bus_stop": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "bus_stop", + "nombre_total": 57, + "nombre_avec_name": 0, + "nombre_avec_shelter": 0, + "pourcentage_completion": 0.0 + }, + "camera": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "camera", + "nombre_total": 3, + "nombre_avec_operator": 0, + "nombre_avec_surveillance": 0, + "pourcentage_completion": 0.0 + }, + "recycling": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "recycling", + "nombre_total": 199, + "nombre_avec_recycling_type": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0.0 + }, + "substation": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "substation", + "nombre_total": 1, + "nombre_avec_operator": 0, + "nombre_avec_voltage": 0, + "pourcentage_completion": 0.0 + }, + "laboratory": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "laboratory", + "nombre_total": 1, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0.0 + }, + "school": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "school", + "nombre_total": 15, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0.0 + }, + "police": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "police", + "nombre_total": 1, + "nombre_avec_name": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0.0 + }, + "healthcare": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "healthcare", + "nombre_total": 7, + "nombre_avec_name": 0, + "nombre_avec_healthcare": 0, + "pourcentage_completion": 0.0 + }, + "bicycle_parking": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "bicycle_parking", + "nombre_total": 5, + "nombre_avec_capacity": 0, + "nombre_avec_covered": 0, + "pourcentage_completion": 0.0 + }, + "advertising_board": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "advertising_board", + "nombre_total": 0, + "nombre_avec_operator": 0, + "pourcentage_completion": 0 + }, + "building": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "building", + "nombre_total": 2713, + "nombre_avec_building": 0, + "nombre_avec_addr:housenumber": 0, + "pourcentage_completion": 0.0 + }, + "email": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "email", + "nombre_total": 1, + "nombre_avec_email": 0, + "nombre_avec_contact:email": 0, + "pourcentage_completion": 0.0 + }, + "bench": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "bench", + "nombre_total": 26, + "pourcentage_completion": 0 + }, + "waste_basket": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "waste_basket", + "nombre_total": 12, + "pourcentage_completion": 0 + }, + "street_lamp": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "street_lamp", + "nombre_total": 1, + "pourcentage_completion": 0 + }, + "drinking_water": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "drinking_water", + "nombre_total": 1, + "pourcentage_completion": 0 + }, + "power_pole": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "power_pole", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "manhole": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "manhole", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "little_free_library": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "little_free_library", + "nombre_total": 2, + "pourcentage_completion": 0 + }, + "playground": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "playground", + "nombre_total": 12, + "pourcentage_completion": 0 + }, + "siret": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "siret", + "nombre_total": 6, + "pourcentage_completion": 0 + }, + "restaurants": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "restaurants", + "nombre_total": 4, + "nombre_avec_opening_hours": 0, + "nombre_avec_contact:street": 0, + "nombre_avec_contact:housenumber": 0, + "nombre_avec_website": 0, + "nombre_avec_contact:phone": 0, + "pourcentage_completion": 0.0 + }, + "rnb": { + "date": "2025-08-21", + "zone": "commune_78123", + "theme": "rnb", + "nombre_total": 0, + "pourcentage_completion": 0 + } + }, + "metadata": { + "insee_code": "78123", + "creation_date": "2025-08-21 11:15:23", + "polygon_file": "commune_78123.poly", + "osm_data_file": "france-latest.osm.pbf", + "total_objects": 3161, + "average_completion": 0.04740272065801961, + "theme_count": 22 + } +} \ No newline at end of file diff --git a/counting_osm_objects/city_analysis/analyse_commune_94016.json b/counting_osm_objects/city_analysis/analyse_commune_94016.json new file mode 100644 index 00000000..793a42b4 --- /dev/null +++ b/counting_osm_objects/city_analysis/analyse_commune_94016.json @@ -0,0 +1,247 @@ +{ + "themes": { + "borne-de-recharge": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "borne-de-recharge", + "nombre_total": 12, + "nombre_avec_operator": 0, + "nombre_avec_capacity": 0, + "pourcentage_completion": 0 + }, + "borne-incendie": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "borne-incendie", + "nombre_total": 46, + "nombre_avec_ref": 12, + "nombre_avec_colour": 7, + "pourcentage_completion": 20.65 + }, + "arbres": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "arbres", + "nombre_total": 2317, + "nombre_avec_species": 18, + "nombre_avec_leaf_type": 1648, + "pourcentage_completion": 35.95 + }, + "defibrillator": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "defibrillator", + "nombre_total": 13, + "nombre_avec_operator": 0, + "nombre_avec_access": 0, + "pourcentage_completion": 0 + }, + "toilets": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "toilets", + "nombre_total": 4, + "nombre_avec_access": 4, + "nombre_avec_wheelchair": 3, + "pourcentage_completion": 87.5 + }, + "bus_stop": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "bus_stop", + "nombre_total": 49, + "nombre_avec_name": 0, + "nombre_avec_shelter": 0, + "pourcentage_completion": 0 + }, + "camera": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "camera", + "nombre_total": 22, + "nombre_avec_operator": 6, + "nombre_avec_surveillance": 12, + "pourcentage_completion": 40.91 + }, + "recycling": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "recycling", + "nombre_total": 33, + "nombre_avec_recycling_type": 0, + "nombre_avec_operator": 6, + "pourcentage_completion": 9.09 + }, + "substation": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "substation", + "nombre_total": 59, + "nombre_avec_operator": 6, + "nombre_avec_voltage": 0, + "pourcentage_completion": 5.08 + }, + "laboratory": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "laboratory", + "nombre_total": 3, + "nombre_avec_name": 0, + "nombre_avec_operator": 6, + "pourcentage_completion": 100.0 + }, + "school": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "school", + "nombre_total": 17, + "nombre_avec_name": 0, + "nombre_avec_operator": 6, + "pourcentage_completion": 17.65 + }, + "police": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "police", + "nombre_total": 2, + "nombre_avec_name": 0, + "nombre_avec_operator": 6, + "pourcentage_completion": 150.0 + }, + "healthcare": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "healthcare", + "nombre_total": 77, + "nombre_avec_name": 0, + "nombre_avec_healthcare": 0, + "pourcentage_completion": 0.0 + }, + "bicycle_parking": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "bicycle_parking", + "nombre_total": 68, + "nombre_avec_capacity": 0, + "nombre_avec_covered": 0, + "pourcentage_completion": 0.0 + }, + "advertising_board": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "advertising_board", + "nombre_total": 37, + "nombre_avec_operator": 6, + "pourcentage_completion": 16.22 + }, + "building": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "building", + "nombre_total": 4399, + "nombre_avec_building": 0, + "nombre_avec_addr:housenumber": 0, + "pourcentage_completion": 0.0 + }, + "email": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "email", + "nombre_total": 39, + "nombre_avec_email": 0, + "nombre_avec_contact:email": 0, + "pourcentage_completion": 0.0 + }, + "bench": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "bench", + "nombre_total": 439, + "pourcentage_completion": 0 + }, + "waste_basket": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "waste_basket", + "nombre_total": 260, + "pourcentage_completion": 0 + }, + "street_lamp": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "street_lamp", + "nombre_total": 1622, + "pourcentage_completion": 0 + }, + "drinking_water": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "drinking_water", + "nombre_total": 9, + "pourcentage_completion": 0 + }, + "power_pole": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "power_pole", + "nombre_total": 0, + "pourcentage_completion": 0 + }, + "manhole": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "manhole", + "nombre_total": 21, + "pourcentage_completion": 0 + }, + "little_free_library": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "little_free_library", + "nombre_total": 6, + "pourcentage_completion": 0 + }, + "playground": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "playground", + "nombre_total": 23, + "pourcentage_completion": 0 + }, + "siret": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "siret", + "nombre_total": 260, + "pourcentage_completion": 0 + }, + "restaurants": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "restaurants", + "nombre_total": 40, + "nombre_avec_opening_hours": 0, + "nombre_avec_contact:street": 0, + "nombre_avec_contact:housenumber": 0, + "nombre_avec_website": 0, + "nombre_avec_contact:phone": 0, + "pourcentage_completion": 0.0 + }, + "rnb": { + "date": "2025-08-21", + "zone": "commune_94016", + "theme": "rnb", + "nombre_total": 0, + "pourcentage_completion": 0 + } + }, + "metadata": { + "insee_code": "94016", + "creation_date": "2025-08-21 11:16:39", + "polygon_file": "commune_94016.poly", + "osm_data_file": "france-latest.osm.pbf", + "total_objects": 9877, + "average_completion": 8.868679761061053, + "theme_count": 26 + } +} \ No newline at end of file diff --git a/docs/create_missing_stats.md b/docs/create_missing_stats.md new file mode 100644 index 00000000..07233ca0 --- /dev/null +++ b/docs/create_missing_stats.md @@ -0,0 +1,79 @@ +# Création des objets Stats manquants à partir du CSV des communes + +Cette documentation explique comment utiliser la nouvelle fonctionnalité pour créer des objets Stats manquants à partir du fichier CSV des communes françaises. + +## Fonctionnalité + +La route `/admin/create-missing-stats-from-csv` permet d'examiner le fichier CSV des communes françaises (`communes_france.csv`) et de créer des objets Stats pour les communes qui n'en ont pas encore dans la base de données. Les objets Stats sont d'abord créés avec les informations du CSV, puis complétés avec des données supplémentaires (coordonnées, budget, etc.) lors de la sauvegarde par paquet de 100, sans effectuer de "labourage" (traitement complet des données OSM). + +Les objets Stats sont sauvegardés par paquets de 100 pour optimiser les performances et éviter les problèmes de mémoire. + +## Prérequis + +1. Le fichier `communes_france.csv` doit exister à la racine du projet +2. Le fichier doit contenir au minimum les colonnes suivantes : + - `code` : Le code INSEE de la commune + - `nom` : Le nom de la commune + +Si le fichier n'existe pas, vous pouvez le générer en exécutant le script `fetch_communes.py` : + +```bash +python fetch_communes.py +``` + +## Utilisation + +1. Accédez à la route `/admin/create-missing-stats-from-csv` dans votre navigateur +2. Le processus s'exécute automatiquement et affiche un message de succès une fois terminé +3. Vous serez redirigé vers la page d'administration principale + +## Données importées + +Les données suivantes sont importées du CSV pour chaque commune : + +- `code` → `zone` (code INSEE) +- `nom` → `name` (nom de la commune) +- `population` → `population` (nombre d'habitants) +- `codesPostaux` → `codesPostaux` (codes postaux, séparés par des virgules) +- `siren` → `siren` (numéro SIREN) +- `codeEpci` → `codeEpci` (code EPCI) + +Les données suivantes sont récupérées automatiquement lors de la sauvegarde par paquet de 100 : + +- `lat` et `lon` : Coordonnées géographiques (récupérées via l'API geo.api.gouv.fr) +- `budget_annuel` : Budget annuel de la commune (récupéré via le service BudgetService) +- `completion_percent` : Pourcentage de complétion (calculé automatiquement) + +Les objets Stats créés ont également les propriétés suivantes : +- `date_created` : Date de création (date actuelle) +- `date_modified` : Date de modification (date actuelle) +- `kind` : 'command' (indique que l'objet a été créé par une commande) + +## Vérification + +Après avoir exécuté la fonctionnalité, un message de succès s'affiche avec les informations suivantes : +- Nombre de communes ajoutées +- Nombre de communes déjà existantes (ignorées) +- Nombre d'erreurs rencontrées + +Vous pouvez également vérifier dans la base de données que les objets Stats ont bien été créés pour les communes manquantes. + +## Journalisation + +Les actions et erreurs sont journalisées via le service `ActionLogger` : +- `admin/create_missing_stats_from_csv` : Journalise le début de l'action +- `error_create_missing_stats_from_csv` : Journalise les erreurs rencontrées lors de la création des objets Stats +- `error_complete_stats_data` : Journalise les erreurs rencontrées lors de la récupération des données supplémentaires (coordonnées, budget, etc.) + +## Différence avec importStatsFromCsv + +Cette fonctionnalité est similaire à `importStatsFromCsv`, mais avec quelques différences importantes : +- Elle est spécifiquement conçue pour créer des objets Stats manquants +- Elle utilise 'command' comme valeur pour le champ `kind` (au lieu de 'request') +- Elle journalise les erreurs de manière plus détaillée + +## Exemple de message de succès + +``` +Création des Stats manquantes terminée : 123 communes ajoutées, 456 déjà existantes, 0 erreurs. +``` \ No newline at end of file diff --git a/docs/osmose_integration.md b/docs/osmose_integration.md new file mode 100644 index 00000000..65209a33 --- /dev/null +++ b/docs/osmose_integration.md @@ -0,0 +1,126 @@ +# Intégration des analyses Osmose + +Ce document décrit l'intégration des analyses Osmose dans les pages détaillées de thématique, à la fois pour les routes publiques et administratives. + +## Fonctionnalités implémentées + +1. Affichage d'une carte avec les analyses Osmose pour la thématique courante +2. Affichage des analyses sous forme de points violets sur la carte +3. Popups interactives avec: + - Titre et description de l'analyse + - Tags proposés (tableau clé-valeur) + - Bouton pour voir l'analyse sur Osmose + - Bouton pour réparer dans JOSM via la télécommande + +## Thèmes supportés + +Les thèmes suivants sont actuellement supportés avec leurs IDs d'items Osmose correspondants: + +| Thème | IDs Osmose | +|-------|------------| +| charging_station (Bornes de recharge) | 8410, 8411 | +| school (Écoles) | 8031 | +| healthcare (Santé) | 8211, 7220, 8331 | +| laboratory (Laboratoires d'analyses) | 7240, 8351 | +| police (Commissariats) | 8190, 8191 | +| defibrillator (Défibrillateurs) | 8370 | + +Pour ajouter de nouveaux thèmes, modifiez la constante `osmoseItemsMapping` dans les deux fichiers suivants: +- `templates/public/followup_graph.html.twig` (route publique) +- `templates/admin/followup_theme_graph.html.twig` (route administrative) + +Assurez-vous que les mappages sont identiques dans les deux fichiers pour garantir un comportement cohérent. + +## Fonctionnement technique + +L'intégration est implémentée à la fois sur la route publique (`templates/public/followup_graph.html.twig`) et sur la route administrative (`templates/admin/followup_theme_graph.html.twig`). Le fonctionnement est similaire sur les deux routes: + +1. La carte est initialisée avec MapLibre GL JS +2. Les coordonnées de la commune sont récupérées via l'API Geo.gouv.fr +3. Une bounding box est calculée autour du centre de la commune +4. Les analyses Osmose sont récupérées via l'API Osmose avec les paramètres: + - Les IDs d'items correspondant au thème + - La bounding box calculée + - Les niveaux de sévérité 1, 2 et 3 + - Une limite de 500 résultats +5. Les analyses sont affichées sous forme de points violets sur la carte +6. Au clic sur un point, une popup s'ouvre et charge les détails de l'analyse via l'API Osmose +7. La popup affiche les tags proposés et des boutons d'action + +### Différences entre les routes + +- **Route publique**: La carte affiche uniquement les analyses Osmose. +- **Route administrative**: La carte affiche à la fois les objets OSM existants (récupérés via Overpass API) et les analyses Osmose, permettant une comparaison directe entre les objets existants et les suggestions d'ajout. + +## Exemple d'URL d'API Osmose + +Pour les bornes de recharge (charging_station): +``` +https://osmose.openstreetmap.fr/api/0.3/issues?zoom=12&item=8410,8411&level=1,2,3&limit=500&bbox=-0.789642333984375,47.35905994178323,-0.3203201293945313,47.598060753627195 +``` + +Pour récupérer les détails d'une analyse: +``` +https://osmose.openstreetmap.fr/api/0.3/issue/5c319a16-3689-b8c7-5427-187e04a6042a?langs=auto +``` + +## Tests + +Pour tester l'intégration: + +### Route publique + +1. Accédez à une page de thématique détaillée publique, par exemple: + - `/stats/12345/followup-graph/charging_station` pour les bornes de recharge + - `/stats/12345/followup-graph/school` pour les écoles + - `/stats/12345/followup-graph/healthcare` pour les lieux de santé + +2. Vérifiez que: + - La carte s'affiche correctement + - Les points violets apparaissent sur la carte (s'il y a des analyses Osmose pour cette thématique dans la zone) + - Au clic sur un point, une popup s'ouvre avec les détails de l'analyse + - Les boutons "Voir sur Osmose" et "Réparer dans JOSM" fonctionnent correctement + +### Route administrative + +1. Accédez à une page de thématique détaillée administrative, par exemple: + - `/admin/stats/12345/followup-graph/charging_station` pour les bornes de recharge + - `/admin/stats/12345/followup-graph/school` pour les écoles + - `/admin/stats/12345/followup-graph/healthcare` pour les lieux de santé + +2. Vérifiez que: + - La carte s'affiche correctement avec les objets OSM existants + - Les points violets des analyses Osmose apparaissent également sur la carte + - Au clic sur un point violet, une popup s'ouvre avec les détails de l'analyse + - Les boutons "Voir sur Osmose" et "Réparer dans JOSM" fonctionnent correctement + +## Dépannage + +### Problèmes généraux + +Si la carte ne s'affiche pas correctement: +- Vérifiez que la variable d'environnement `MAPTILER_TOKEN` est correctement définie +- Vérifiez les erreurs dans la console JavaScript du navigateur + +Si les analyses Osmose ne s'affichent pas: +- Vérifiez que le thème a des IDs d'items Osmose associés dans `osmoseItemsMapping` +- Vérifiez qu'il y a des analyses Osmose pour ce thème dans la zone +- Vérifiez les erreurs dans la console JavaScript du navigateur + +### Erreurs liées à l'API Osmose + +Si vous rencontrez l'erreur "TypeError: right-hand side of 'in' should be an object, got undefined": +- Cette erreur peut se produire si l'API Osmose renvoie une réponse où `data.issue` est undefined +- Vérifiez que la fonction `loadOsmoseIssueDetails` contient bien la vérification `if (!data || !data.issue) return;` avant d'utiliser `data.issue` +- Si l'erreur persiste, vérifiez la structure de la réponse de l'API Osmose en ajoutant `console.log(data)` avant d'utiliser `data.issue` + +### Problèmes spécifiques à la route administrative + +Si les analyses Osmose s'affichent sur la route publique mais pas sur la route administrative: +- Vérifiez que la constante `osmoseItemsMapping` est correctement définie dans `templates/admin/followup_theme_graph.html.twig` +- Vérifiez que le code d'initialisation des analyses Osmose est appelé après l'initialisation de la carte +- Vérifiez que les analyses Osmose ne sont pas masquées par d'autres éléments de la carte (comme les marqueurs des objets OSM existants) + +Si les objets OSM existants et les analyses Osmose se chevauchent de manière confuse: +- Les analyses Osmose sont affichées avec des marqueurs violets pour les distinguer des objets OSM existants +- Vous pouvez ajuster la couleur des marqueurs Osmose en modifiant la valeur `color: '#8A2BE2'` dans la fonction `loadOsmoseAnalyses` \ No newline at end of file diff --git a/src/Command/CreateMissingStatsFromCsvCommand.php b/src/Command/CreateMissingStatsFromCsvCommand.php new file mode 100644 index 00000000..70b0043f --- /dev/null +++ b/src/Command/CreateMissingStatsFromCsvCommand.php @@ -0,0 +1,262 @@ +entityManager = $entityManager; + $this->statsRepository = $statsRepository; + $this->actionLogger = $actionLogger; + $this->budgetService = $budgetService; + } + + protected function configure(): void + { + $this + ->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de communes à traiter', 0) + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule sans modifier la base de données') + ->setHelp('Cette commande examine le fichier CSV des communes et crée des objets Stats pour les communes qui n\'en ont pas encore. Les objets sont créés avec les informations du CSV et complétés avec des données supplémentaires (coordonnées, budget, etc.).'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Création des objets Stats manquants à partir du fichier CSV'); + + $limit = (int) $input->getOption('limit'); + $dryRun = $input->getOption('dry-run'); + + $this->actionLogger->log('command/create_missing_stats_from_csv', [ + 'limit' => $limit, + 'dry_run' => $dryRun + ]); + + // Vérifier si le fichier CSV existe + $csvFile = 'communes_france.csv'; + if (!file_exists($csvFile)) { + $io->error('Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.'); + return Command::FAILURE; + } + + $createdCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // Ouvrir le fichier CSV + $handle = fopen($csvFile, 'r'); + if (!$handle) { + $io->error('Impossible d\'ouvrir le fichier CSV des communes.'); + return Command::FAILURE; + } + + // Lire l'en-tête pour déterminer les indices des colonnes + $header = fgetcsv($handle); + $indices = array_flip($header); + + // Vérifier que les colonnes nécessaires existent + $requiredColumns = ['code', 'nom']; + foreach ($requiredColumns as $column) { + if (!isset($indices[$column])) { + $io->error("La colonne '$column' est manquante dans le fichier CSV."); + fclose($handle); + return Command::FAILURE; + } + } + + $io->info(sprintf('Lecture du fichier CSV: %s', $csvFile)); + $io->info(sprintf('Colonnes trouvées: %s', implode(', ', $header))); + + // Compter le nombre total de lignes pour la barre de progression + $totalLines = 0; + $tempHandle = fopen($csvFile, 'r'); + if ($tempHandle) { + // Skip header + fgetcsv($tempHandle); + while (fgetcsv($tempHandle) !== false) { + $totalLines++; + } + fclose($tempHandle); + } + + $io->info(sprintf('Nombre total de communes dans le CSV: %d', $totalLines)); + + // Créer une barre de progression + $progressBar = $io->createProgressBar($totalLines); + $progressBar->start(); + + // Traiter chaque ligne du CSV + while (($data = fgetcsv($handle)) !== false) { + try { + $inseeCode = $data[$indices['code']]; + + // Vérifier si une Stats existe déjà pour ce code INSEE + $existingStat = $this->statsRepository->findOneBy(['zone' => $inseeCode]); + if ($existingStat) { + $skippedCount++; + $progressBar->advance(); + continue; + } + + // Créer un nouvel objet Stats + $stat = new Stats(); + $stat->setZone($inseeCode) + ->setDateCreated(new \DateTime()) + ->setDateModified(new \DateTime()) + ->setKind('command'); // Utiliser 'command' comme source + + // Ajouter le nom si disponible + if (isset($indices['nom']) && !empty($data[$indices['nom']])) { + $stat->setName($data[$indices['nom']]); + } + + // Ajouter la population si disponible + if (isset($indices['population']) && !empty($data[$indices['population']])) { + $stat->setPopulation((int)$data[$indices['population']]); + } + + // Ajouter les codes postaux si disponibles + if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) { + $stat->setCodesPostaux($data[$indices['codesPostaux']]); + } + + // Ajouter le SIREN si disponible + if (isset($indices['siren']) && !empty($data[$indices['siren']])) { + $stat->setSiren((int)$data[$indices['siren']]); + } + + // Ajouter le code EPCI si disponible + if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) { + $stat->setCodeEpci((int)$data[$indices['codeEpci']]); + } + + // Compléter les données manquantes (coordonnées, budget, etc.) + if (!$dryRun) { + $this->completeStatsData($stat); + } + + // Persister l'objet Stats + if (!$dryRun) { + $this->entityManager->persist($stat); + } + $createdCount++; + + // Appliquer la limite si spécifiée + if ($limit > 0 && $createdCount >= $limit) { + $io->info(sprintf('Limite de %d communes atteinte.', $limit)); + break; + } + + // Flush tous les 100 objets pour éviter de surcharger la mémoire + if (!$dryRun && $createdCount % 100 === 0) { + $this->entityManager->flush(); + $this->entityManager->clear(Stats::class); + $io->info(sprintf('Flush après création de %d objets Stats', $createdCount)); + } + } catch (\Exception $e) { + $errorCount++; + $this->actionLogger->log('error_command_create_missing_stats_from_csv', [ + 'insee_code' => $inseeCode ?? 'unknown', + 'error' => $e->getMessage() + ]); + + if ($output->isVerbose()) { + $io->warning(sprintf('Erreur pour la commune %s: %s', $inseeCode ?? 'unknown', $e->getMessage())); + } + } + + $progressBar->advance(); + } + + $progressBar->finish(); + $io->newLine(2); + + // Flush les derniers objets + if (!$dryRun && $createdCount > 0) { + $this->entityManager->flush(); + } + + fclose($handle); + + if ($dryRun) { + $io->success(sprintf('Simulation terminée. %d communes auraient été ajoutées, %d déjà existantes, %d erreurs.', $createdCount, $skippedCount, $errorCount)); + } else { + $io->success(sprintf('Création des Stats manquantes terminée : %d communes ajoutées, %d déjà existantes, %d erreurs.', $createdCount, $skippedCount, $errorCount)); + } + + return Command::SUCCESS; + } + + /** + * Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.) + */ + private function completeStatsData(Stats $stat): void + { + $insee_code = $stat->getZone(); + + // Compléter les coordonnées si manquantes + if (!$stat->getLat() || !$stat->getLon()) { + try { + $apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre'; + $response = @file_get_contents($apiUrl); + if ($response !== false) { + $data = json_decode($response, true); + if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) { + $stat->setLon((string)$data['centre']['coordinates'][0]); + $stat->setLat((string)$data['centre']['coordinates'][1]); + } + } + } catch (\Exception $e) { + $this->actionLogger->log('error_complete_stats_data', [ + 'insee_code' => $insee_code, + 'error' => 'Failed to fetch coordinates: ' . $e->getMessage() + ]); + } + } + + // Compléter le budget si manquant + if (!$stat->getBudgetAnnuel() && $this->budgetService !== null) { + try { + $budget = $this->budgetService->getBudgetAnnuel($insee_code); + if ($budget !== null) { + $stat->setBudgetAnnuel((string)$budget); + } + } catch (\Exception $e) { + $this->actionLogger->log('error_complete_stats_data', [ + 'insee_code' => $insee_code, + 'error' => 'Failed to fetch budget: ' . $e->getMessage() + ]); + } + } + + // Calculer le pourcentage de complétion + $stat->computeCompletionPercent(); + } +} \ No newline at end of file diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index a6fd01fa..dca54bd1 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -613,8 +613,60 @@ final class AdminController extends AbstractController { $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if (!$stats) { - $this->addFlash('error', '2 Aucune stats trouvée pour ce code INSEE.'); - return $this->redirectToRoute('app_admin'); + // Si aucune stats n'existe, rechercher dans l'API geo.api.gouv.fr + $apiUrl = "https://geo.api.gouv.fr/communes/{$insee_code}"; + $response = @file_get_contents($apiUrl); + + if ($response === false) { + $this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE et impossible de récupérer les informations depuis l\'API geo.api.gouv.fr.'); + return $this->redirectToRoute('app_admin'); + } + + $communeData = json_decode($response, true); + if (!$communeData || !isset($communeData['nom'])) { + $this->addFlash('error', 'Aucune commune trouvée avec ce code INSEE dans l\'API geo.api.gouv.fr.'); + return $this->redirectToRoute('app_admin'); + } + + // Créer un nouvel objet Stats avec les données de l'API + $stats = new Stats(); + $stats->setZone($insee_code) + ->setName($communeData['nom']) + ->setDateCreated(new \DateTime()) + ->setDateModified(new \DateTime()) + ->setKind('request'); + + // Ajouter la population si disponible + if (isset($communeData['population'])) { + $stats->setPopulation($communeData['population']); + } + + // Ajouter les coordonnées si disponibles + if (isset($communeData['centre']) && isset($communeData['centre']['coordinates'])) { + $stats->setLon((string)$communeData['centre']['coordinates'][0]); + $stats->setLat((string)$communeData['centre']['coordinates'][1]); + } + + // Ajouter les codes postaux si disponibles + if (isset($communeData['codesPostaux']) && !empty($communeData['codesPostaux'])) { + $stats->setCodesPostaux(implode(',', $communeData['codesPostaux'])); + } + + // Ajouter le code EPCI si disponible + if (isset($communeData['codeEpci'])) { + $stats->setCodeEpci((int)$communeData['codeEpci']); + } + + // Ajouter le SIREN si disponible + if (isset($communeData['siren'])) { + $stats->setSiren((int)$communeData['siren']); + } + + // Persister l'objet Stats + $this->entityManager->persist($stats); + $this->entityManager->flush(); + + $this->addFlash('success', 'Nouvelle commune ajoutée à partir des données de l\'API geo.api.gouv.fr.'); } $themes = \App\Service\FollowUpService::getFollowUpThemes(); @@ -1372,6 +1424,274 @@ final class AdminController extends AbstractController $this->addFlash('success', $budgetsMisAJour . ' budgets mis à jour.'); return $this->redirectToRoute('app_admin'); } + + #[Route('/admin/import-stats-from-csv', name: 'app_admin_import_stats_from_csv')] + public function importStatsFromCsv(): Response + { + $this->actionLogger->log('admin/import_stats_from_csv', []); + + $csvFile = 'communes_france.csv'; + if (!file_exists($csvFile)) { + $this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.'); + return $this->redirectToRoute('app_admin'); + } + + $statsRepo = $this->entityManager->getRepository(Stats::class); + $createdCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // Ouvrir le fichier CSV + $handle = fopen($csvFile, 'r'); + if (!$handle) { + $this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.'); + return $this->redirectToRoute('app_admin'); + } + + // Lire l'en-tête pour déterminer les indices des colonnes + $header = fgetcsv($handle); + $indices = array_flip($header); + + // Vérifier que les colonnes nécessaires existent + $requiredColumns = ['code', 'nom']; + foreach ($requiredColumns as $column) { + if (!isset($indices[$column])) { + $this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV."); + fclose($handle); + return $this->redirectToRoute('app_admin'); + } + } + + // Traiter chaque ligne du CSV + while (($data = fgetcsv($handle)) !== false) { + try { + $inseeCode = $data[$indices['code']]; + + // Vérifier si une Stats existe déjà pour ce code INSEE + $existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]); + if ($existingStat) { + $skippedCount++; + continue; + } + + // Créer un nouvel objet Stats + $stat = new Stats(); + $stat->setZone($inseeCode) + ->setDateCreated(new \DateTime()) + ->setDateModified(new \DateTime()) + ->setKind('request'); + + // Ajouter le nom si disponible + if (isset($indices['nom']) && !empty($data[$indices['nom']])) { + $stat->setName($data[$indices['nom']]); + } + + // Ajouter la population si disponible + if (isset($indices['population']) && !empty($data[$indices['population']])) { + $stat->setPopulation((int)$data[$indices['population']]); + } + + // Ajouter les codes postaux si disponibles + if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) { + $stat->setCodesPostaux($data[$indices['codesPostaux']]); + } + + // Ajouter le SIREN si disponible + if (isset($indices['siren']) && !empty($data[$indices['siren']])) { + $stat->setSiren((int)$data[$indices['siren']]); + } + + // Ajouter le code EPCI si disponible + if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) { + $stat->setCodeEpci((int)$data[$indices['codeEpci']]); + } + + // Ajouter les coordonnées si disponibles + if (isset($indices['longitude']) && isset($indices['latitude']) && + !empty($data[$indices['longitude']]) && !empty($data[$indices['latitude']])) { + $stat->setLon((string)$data[$indices['longitude']]) + ->setLat((string)$data[$indices['latitude']]); + } + + // Persister l'objet Stats + $this->entityManager->persist($stat); + $createdCount++; + + // Flush tous les 100 objets pour éviter de surcharger la mémoire + if ($createdCount % 100 === 0) { + $this->entityManager->flush(); + $this->entityManager->clear(Stats::class); + } + } catch (\Exception $e) { + $errorCount++; + } + } + + // Flush les derniers objets + $this->entityManager->flush(); + + fclose($handle); + + $this->addFlash('success', "Import terminé : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs."); + return $this->redirectToRoute('app_admin'); + } + + #[Route('/admin/create-missing-stats-from-csv', name: 'app_admin_create_missing_stats_from_csv')] + public function createMissingStatsFromCsv(): Response + { + $this->actionLogger->log('admin/create_missing_stats_from_csv', []); + + $csvFile = 'communes_france.csv'; + if (!file_exists($csvFile)) { + $this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.'); + return $this->redirectToRoute('app_admin'); + } + + $statsRepo = $this->entityManager->getRepository(Stats::class); + $createdCount = 0; + $skippedCount = 0; + $errorCount = 0; + + // Ouvrir le fichier CSV + $handle = fopen($csvFile, 'r'); + if (!$handle) { + $this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.'); + return $this->redirectToRoute('app_admin'); + } + + // Lire l'en-tête pour déterminer les indices des colonnes + $header = fgetcsv($handle); + $indices = array_flip($header); + + // Vérifier que les colonnes nécessaires existent + $requiredColumns = ['code', 'nom']; + foreach ($requiredColumns as $column) { + if (!isset($indices[$column])) { + $this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV."); + fclose($handle); + return $this->redirectToRoute('app_admin'); + } + } + + // Traiter chaque ligne du CSV + while (($data = fgetcsv($handle)) !== false) { + try { + $inseeCode = $data[$indices['code']]; + + // Vérifier si une Stats existe déjà pour ce code INSEE + $existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]); + if ($existingStat) { + $skippedCount++; + continue; + } + + // Créer un nouvel objet Stats + $stat = new Stats(); + $stat->setZone($inseeCode) + ->setDateCreated(new \DateTime()) + ->setDateModified(new \DateTime()) + ->setKind('command'); // Utiliser 'command' comme source + + // Ajouter le nom si disponible + if (isset($indices['nom']) && !empty($data[$indices['nom']])) { + $stat->setName($data[$indices['nom']]); + } + + // Ajouter la population si disponible + if (isset($indices['population']) && !empty($data[$indices['population']])) { + $stat->setPopulation((int)$data[$indices['population']]); + } + + // Ajouter les codes postaux si disponibles + if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) { + $stat->setCodesPostaux($data[$indices['codesPostaux']]); + } + + // Ajouter le SIREN si disponible + if (isset($indices['siren']) && !empty($data[$indices['siren']])) { + $stat->setSiren((int)$data[$indices['siren']]); + } + + // Ajouter le code EPCI si disponible + if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) { + $stat->setCodeEpci((int)$data[$indices['codeEpci']]); + } + + // Compléter les données manquantes (coordonnées, budget, etc.) + $this->completeStatsData($stat); + + // Persister l'objet Stats + $this->entityManager->persist($stat); + $createdCount++; + + // Flush tous les 100 objets pour éviter de surcharger la mémoire + if ($createdCount % 100 === 0) { + $this->entityManager->flush(); + $this->entityManager->clear(Stats::class); + } + } catch (\Exception $e) { + $errorCount++; + $this->actionLogger->log('error_create_missing_stats_from_csv', [ + 'insee_code' => $inseeCode ?? 'unknown', + 'error' => $e->getMessage() + ]); + } + } + + // Flush les derniers objets + $this->entityManager->flush(); + + fclose($handle); + + $this->addFlash('success', "Création des Stats manquantes terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs."); + return $this->redirectToRoute('app_admin'); + } + + /** + * Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.) + */ + private function completeStatsData(Stats $stat): void + { + $insee_code = $stat->getZone(); + + // Compléter les coordonnées si manquantes + if (!$stat->getLat() || !$stat->getLon()) { + try { + $apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre'; + $response = @file_get_contents($apiUrl); + if ($response !== false) { + $data = json_decode($response, true); + if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) { + $stat->setLon((string)$data['centre']['coordinates'][0]); + $stat->setLat((string)$data['centre']['coordinates'][1]); + } + } + } catch (\Exception $e) { + $this->actionLogger->log('error_complete_stats_data', [ + 'insee_code' => $insee_code, + 'error' => 'Failed to fetch coordinates: ' . $e->getMessage() + ]); + } + } + + // Compléter le budget si manquant + if (!$stat->getBudgetAnnuel() && property_exists($this, 'budgetService') && $this->budgetService !== null) { + try { + $budget = $this->budgetService->getBudgetAnnuel($insee_code); + if ($budget !== null) { + $stat->setBudgetAnnuel((string)$budget); + } + } catch (\Exception $e) { + $this->actionLogger->log('error_complete_stats_data', [ + 'insee_code' => $insee_code, + 'error' => 'Failed to fetch budget: ' . $e->getMessage() + ]); + } + } + + // Calculer le pourcentage de complétion + $stat->computeCompletionPercent(); + } #[Route('/admin/podium-contributeurs-osm', name: 'app_admin_podium_contributeurs_osm')] public function podiumContributeursOsm(): Response @@ -1401,7 +1721,7 @@ final class AdminController extends AbstractController $topContributors = array_slice($contributions, 0, 10, true); return $this->render('admin/podium_contributeurs_osm.html.twig', [ - 'contributors' => $topContributors + 'podium' => $topContributors ]); } diff --git a/src/Controller/PublicController.php b/src/Controller/PublicController.php index 8c5f8653..3e412079 100644 --- a/src/Controller/PublicController.php +++ b/src/Controller/PublicController.php @@ -1015,6 +1015,7 @@ class PublicController extends AbstractController 'count_data' => json_encode($countData), 'completion_data' => json_encode($completionData), 'icons' => \App\Service\FollowUpService::getFollowUpIcons(), + 'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null, ]); } diff --git a/templates/admin/followup_theme_graph.html.twig b/templates/admin/followup_theme_graph.html.twig index f5dc24e4..c4833dfa 100644 --- a/templates/admin/followup_theme_graph.html.twig +++ b/templates/admin/followup_theme_graph.html.twig @@ -105,6 +105,20 @@ font-size: 0.95em; } + .osmose-popup-tag { + margin-bottom: 5px; + } + + .osmose-popup-tag-key { + font-weight: bold; + } + + .osmose-popup-buttons { + margin-top: 10px; + display: flex; + gap: 5px; + } + /* Bouton flottant suggestion desktop */ .suggestion-float-btn { @@ -340,6 +354,17 @@ const mapToken = '{{ maptiler_token }}'; // Liste des tags attendus pour la complétion de ce thème const completionTags = {{ completion_tags[theme]|json_encode|raw }}; + + // Mapping des thèmes vers les IDs d'items Osmose + const osmoseItemsMapping = { + 'charging_station': [8410, 8411], + 'school': [8031], + 'healthcare': [8211, 7220, 8331], + 'laboratory': [7240, 8351], + 'police': [8190, 8191], + 'defibrillator': [8370] + // Ajouter d'autres thèmes selon les besoins + }; let mapInstance = null; const basemaps = { 'streets': 'https://api.maptiler.com/maps/streets/style.json?key=' + mapToken, @@ -361,6 +386,38 @@ zoom: 13 }); mapInstance.addControl(new maplibregl.NavigationControl()); + + // Charger les analyses Osmose si le thème est supporté + const currentTheme = '{{ theme }}'; + if (osmoseItemsMapping[currentTheme]) { + // Récupérer les coordonnées de la commune pour la bounding box + fetch(`https://geo.api.gouv.fr/communes?code={{ stats.zone }}&fields=centre,nom,code,codesPostaux,population,surface,contour`) + .then(response => response.json()) + .then(data => { + if (data && data.length > 0) { + const commune = data[0]; + if (commune.centre && commune.centre.coordinates) { + // Calculer la bounding box pour la requête Osmose + const lon = commune.centre.coordinates[0]; + const lat = commune.centre.coordinates[1]; + const offset = 0.05; // Environ 5km + const bbox = [ + lon - offset, + lat - offset, + lon + offset, + lat + offset + ]; + + // Charger les analyses Osmose + loadOsmoseAnalyses(mapInstance, currentTheme, bbox); + } + } + }) + .catch(error => { + console.error('Erreur lors du chargement des coordonnées de la commune:', error); + }); + } + // Gestion du changement de fond de carte const basemapSelect = document.getElementById('basemapSelect'); if (basemapSelect) { @@ -630,11 +687,14 @@ // Mettre à jour les statistiques function updateStats() { // Use current metrics from server if available - if (typeof currentCount !== 'undefined') { - document.getElementById('currentCount').textContent = currentCount; - } else if (Array.isArray(countData) && countData.length > 0) { - const latestCount = countData[countData.length - 1]; - document.getElementById('currentCount').textContent = latestCount.value; + if (document.getElementById('currentCount')) { + + if (typeof currentCount !== 'undefined') { + document.getElementById('currentCount').textContent = currentCount; + } else if (Array.isArray(countData) && countData.length > 0) { + const latestCount = countData[countData.length - 1]; + document.getElementById('currentCount').textContent = latestCount.value; + } } if (typeof currentCompletion !== 'undefined') { @@ -793,5 +853,122 @@ {% else %} console.log('[DEBUG][Overpass] Aucune requête Overpass transmise à la page.'); {% endif %} + + // Fonction pour charger les analyses Osmose + function loadOsmoseAnalyses(map, theme, bbox) { + const items = osmoseItemsMapping[theme]; + if (!items || items.length === 0) return; + + const itemsParam = items.join(','); + const bboxParam = bbox.join(','); + const osmoseUrl = `https://osmose.openstreetmap.fr/api/0.3/issues?zoom=12&item=${itemsParam}&level=1,2,3&limit=500&bbox=${bboxParam}`; + + fetch(osmoseUrl) + .then(response => response.json()) + .then(data => { + if (!data.issues || data.issues.length === 0) { + console.log('Aucune analyse Osmose trouvée pour ce thème dans cette zone.'); + return; + } + + console.log(`[Osmose] ${data.issues.length} analyses trouvées pour le thème ${theme}`); + + // Ajouter les marqueurs pour chaque analyse + data.issues.forEach(issue => { + if (issue.lat && issue.lon) { + + let lapopup = new maplibregl.Popup({offset: 25}) + .setHTML( + (() => { + + return `
${issue.subtitle || ''}
+ `; + + // Ajouter les tags proposés s'ils existent + if (issue.fixes && issue.fixes.length > 0 && issue.fixes[0].tags) { + popupContent += ' '; + } + + // Ajouter les boutons d'action + popupContent += ` + + `; + + // Mettre à jour le contenu du popup + popupElement.innerHTML = popupContent; + }) + .catch(error => { + console.error('Erreur lors du chargement des détails de l\'analyse Osmose:', error); + const popupElement = document.getElementById(`osmose-popup-${issueId}`); + if (popupElement) { + popupElement.innerHTML = 'Suggestions d'intégration de {{ theme_label|lower }} basées sur les analyses Osmose
+