ajout analyses osmose dans les pages de détail

This commit is contained in:
Tykayn 2025-08-21 16:07:02 +02:00 committed by tykayn
parent 359d4ba6b8
commit 0aaddb44c5
13 changed files with 1963 additions and 12 deletions

View file

@ -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

View file

@ -9,4 +9,6 @@ resultats/*
osm_config.txt
__pycache__
secrets.sh
cookie.txt
cookie.txt
bin/venv
activate

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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.
```

126
docs/osmose_integration.md Normal file
View file

@ -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`

View file

@ -0,0 +1,262 @@
<?php
namespace App\Command;
use App\Entity\Stats;
use App\Repository\StatsRepository;
use App\Service\ActionLogger;
use App\Service\BudgetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:create-missing-stats-from-csv',
description: 'Crée des objets Stats manquants à partir du fichier communes_france.csv',
)]
class CreateMissingStatsFromCsvCommand extends Command
{
private EntityManagerInterface $entityManager;
private StatsRepository $statsRepository;
private ActionLogger $actionLogger;
private ?BudgetService $budgetService;
public function __construct(
EntityManagerInterface $entityManager,
StatsRepository $statsRepository,
ActionLogger $actionLogger,
?BudgetService $budgetService = null
) {
parent::__construct();
$this->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();
}
}

View file

@ -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
]);
}

View file

@ -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,
]);
}

View file

@ -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 `<div id="osmose-popup-${issue.id}" onclick="loadOsmoseIssueDetails(${issue.id})">Chargement des détails... ${issue.id}</div>`
})());
lapopup.on('open', () => {
// Charger les détails de l'analyse lorsque le popup est ouvert
console.log('open popup', issue)
// loadOsmoseIssueDetails(issue.id);
});
// Créer un marqueur pour l'analyse
const marker = new maplibregl.Marker({
color: '#8A2BE2' // Violet
})
.setLngLat([issue.lon, issue.lat])
// Ajouter un popup au marqueur
.setPopup(
lapopup
)
.addTo(map);
console.log('marker', marker)
}
});
})
.catch(error => {
console.error('Erreur lors du chargement des analyses Osmose:', error);
});
}
// Fonction pour charger les détails d'une analyse Osmose
function loadOsmoseIssueDetails(issueId) {
const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`;
console.log('detailsUrl', detailsUrl)
fetch(detailsUrl)
.then(response => response.json())
.then(data => {
if (!data || !data.issue) return;
const issue = data.issue;
const popupElement = document.getElementById(`osmose-popup-${issueId}`);
if (!popupElement) return;
// Construire le contenu du popup
let popupContent = `
<h4>${issue.title || 'Analyse Osmose'}</h4>
<p>${issue.subtitle || ''}</p>
`;
// Ajouter les tags proposés s'ils existent
if (issue.fixes && issue.fixes.length > 0 && issue.fixes[0].tags) {
popupContent += '<div class="osmose-popup-tags">';
popupContent += '<h5>Tags proposés:</h5>';
Object.entries(issue.fixes[0].tags).forEach(([key, value]) => {
popupContent += `
<div class="osmose-popup-tag">
<span class="osmose-popup-tag-key">${key}</span>:
<span class="osmose-popup-tag-value">${value}</span>
</div>
`;
});
popupContent += '</div>';
}
// Ajouter les boutons d'action
popupContent += `
<div class="osmose-popup-buttons">
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Voir sur Osmose
</a>
<a href="http://localhost:8111/load_and_zoom?left=${issue.lon - 0.001}&right=${issue.lon + 0.001}&top=${issue.lat + 0.001}&bottom=${issue.lat - 0.001}" target="_blank" class="btn btn-sm btn-success">
<i class="bi bi-tools"></i> Réparer dans JOSM
</a>
</div>
`;
// 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 = '<div class="alert alert-danger">Erreur lors du chargement des détails.</div>';
}
});
}
</script>
{% endblock %}
{% endblock %}

View file

@ -32,6 +32,15 @@
</div>
<div class="card-body">
<div class="list-group">
<a href="{{ path('app_admin_import_stats_from_csv') }}" class="list-group-item list-group-item-action">
<i class="bi bi-file-earmark-spreadsheet"></i> Importer les Stats depuis le CSV des communes
</a>
<a href="{{ path('app_admin_labourer_tous_les_budgets') }}" class="list-group-item list-group-item-action">
<i class="bi bi-cash-coin"></i> Mettre à jour tous les budgets
</a>
<a href="{{ path('app_admin_podium_contributeurs_osm') }}" class="list-group-item list-group-item-action">
<i class="bi bi-trophy"></i> Podium des contributeurs OSM
</a>
<a href="{{ path('app_public_index') }}" class="list-group-item list-group-item-action">
<i class="bi bi-house"></i> Retour à l'accueil
</a>

View file

@ -5,6 +5,28 @@
{% block stylesheets %}
{{ parent() }}
<link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet'/>
<style>
#themeMap {
height: 400px;
width: 100%;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.maplibregl-popup-content {
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;
}
</style>
{% endblock %}
{% block body %}
@ -26,6 +48,21 @@
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h3><i class="bi bi-map"></i> Carte des analyses Osmose</h3>
<p class="mb-0">Suggestions d'intégration de {{ theme_label|lower }} basées sur les analyses Osmose</p>
</div>
<div class="card-body">
<div id="themeMap"></div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Informations :</strong> Les points violets représentent des suggestions d'intégration de {{ theme_label|lower }} basées sur les analyses Osmose.
Cliquez sur un point pour voir les détails et les actions possibles.
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3><i class="bi bi-graph-up"></i> Évolution du {{ theme_label|lower }}</h3>
@ -61,10 +98,27 @@
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<script>
// 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
};
document.addEventListener('DOMContentLoaded', function() {
const countData = {{ count_data|raw }};
const completionData = {{ completion_data|raw }};
const currentTheme = '{{ theme }}';
const statsZone = '{{ stats.zone }}';
// Initialiser la carte MapLibre
initMap(currentTheme, statsZone);
// Graphique du nombre d'objets
const countCtx = document.getElementById('countChart').getContext('2d');
@ -149,5 +203,162 @@
}
});
});
// Fonction pour initialiser la carte et charger les analyses Osmose
function initMap(theme, inseeCode) {
// Vérifier si le thème a des items Osmose associés
if (!osmoseItemsMapping[theme]) {
document.getElementById('themeMap').innerHTML = '<div class="alert alert-warning">Aucune analyse Osmose disponible pour ce thème.</div>';
return;
}
// Initialiser la carte MapLibre
const mapToken = '{{ maptiler_token }}';
const map = new maplibregl.Map({
container: 'themeMap',
style: 'https://api.maptiler.com/maps/streets/style.json?key=' + mapToken,
center: [2.3488, 48.8534], // Paris par défaut, sera ajusté en fonction des données
zoom: 12
});
map.addControl(new maplibregl.NavigationControl());
// Charger les coordonnées de la commune à partir du code INSEE
fetch(`https://geo.api.gouv.fr/communes?code=${inseeCode}&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) {
// Centrer la carte sur la commune
map.setCenter([commune.centre.coordinates[0], commune.centre.coordinates[1]]);
// Calculer la bounding box pour la requête Osmose
// Pour simplifier, on utilise un carré autour du centre
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(map, theme, bbox);
}
}
})
.catch(error => {
console.error('Erreur lors du chargement des coordonnées de la commune:', error);
document.getElementById('themeMap').innerHTML = '<div class="alert alert-danger">Erreur lors du chargement des coordonnées de la commune.</div>';
});
}
// 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) {
document.getElementById('themeMap').innerHTML += '<div class="alert alert-info mt-3">Aucune analyse Osmose trouvée pour ce thème dans cette zone.</div>';
return;
}
// Ajouter les marqueurs pour chaque analyse
data.issues.forEach(issue => {
if (issue.lat && issue.lon) {
// Créer un marqueur pour l'analyse
const marker = new maplibregl.Marker({
color: '#8A2BE2' // Violet
})
.setLngLat([issue.lon, issue.lat])
.addTo(map);
// Ajouter un popup au marqueur
marker.setPopup(
new maplibregl.Popup({ offset: 25 })
.setHTML(`<div id="osmose-popup-${issue.id}">Chargement des détails...</div>`)
.on('open', () => {
// Charger les détails de l'analyse lorsque le popup est ouvert
loadOsmoseIssueDetails(issue.id);
})
);
}
});
})
.catch(error => {
console.error('Erreur lors du chargement des analyses Osmose:', error);
document.getElementById('themeMap').innerHTML += '<div class="alert alert-danger mt-3">Erreur lors du chargement des analyses Osmose.</div>';
});
}
// Fonction pour charger les détails d'une analyse Osmose
function loadOsmoseIssueDetails(issueId) {
const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`;
fetch(detailsUrl)
.then(response => response.json())
.then(data => {
if (!data || !data.issue) return;
const issue = data.issue;
const popupElement = document.getElementById(`osmose-popup-${issueId}`);
if (!popupElement) return;
// Construire le contenu du popup
let popupContent = `
<h4>${issue.title || 'Analyse Osmose'}</h4>
<p>${issue.subtitle || ''}</p>
`;
// Ajouter les tags proposés s'ils existent
if (issue.fixes && issue.fixes.length > 0 && issue.fixes[0].tags) {
popupContent += '<div class="osmose-popup-tags">';
popupContent += '<h5>Tags proposés:</h5>';
Object.entries(issue.fixes[0].tags).forEach(([key, value]) => {
popupContent += `
<div class="osmose-popup-tag">
<span class="osmose-popup-tag-key">${key}</span>:
<span class="osmose-popup-tag-value">${value}</span>
</div>
`;
});
popupContent += '</div>';
}
// Ajouter les boutons d'action
popupContent += `
<div class="osmose-popup-buttons">
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Voir sur Osmose
</a>
<a href="http://localhost:8111/load_and_zoom?left=${issue.lon - 0.001}&right=${issue.lon + 0.001}&top=${issue.lat + 0.001}&bottom=${issue.lat - 0.001}" target="_blank" class="btn btn-sm btn-success">
<i class="bi bi-tools"></i> Réparer dans JOSM
</a>
</div>
`;
// 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 = '<div class="alert alert-danger">Erreur lors du chargement des détails.</div>';
}
});
}
</script>
{% endblock %}
{% endblock %}