documentation wiki osm, ajout dashboard issues osmose
This commit is contained in:
parent
b28f8eac63
commit
7665f1d99c
12 changed files with 1758 additions and 76 deletions
88
README.md
88
README.md
|
@ -119,6 +119,94 @@ Options disponibles :
|
|||
|
||||
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.
|
||||
|
||||
## Génération de statistiques pour toute la France
|
||||
|
||||
Le projet inclut un ensemble de commandes Symfony qui permettent de générer des statistiques de complétion pour toutes les communes de France. Ces commandes doivent être exécutées dans l'ordre suivant :
|
||||
|
||||
### 1. Récupération des polygones des villes
|
||||
Récupère les polygones des villes selon leur zone donnée par le code INSEE :
|
||||
```shell
|
||||
php bin/console app:retrieve-city-polygons [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force la récupération même si le polygone existe déjà
|
||||
|
||||
Cette commande :
|
||||
- Crée le dossier `counting_osm_objects/polygons` s'il n'existe pas
|
||||
- Utilise le script Python `get_poly.py` pour récupérer les polygones des communes
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### 2. Extraction des données OSM pour chaque zone INSEE
|
||||
Extrait les données OSM pour chaque zone INSEE à partir du fichier france-latest.osm.pbf :
|
||||
```shell
|
||||
php bin/console app:extract-insee-zones [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force l'extraction même si le fichier JSON existe déjà
|
||||
- `--keep-pbf` ou `-k` : Conserve les fichiers PBF intermédiaires
|
||||
|
||||
Cette commande :
|
||||
- Télécharge automatiquement le fichier france-latest.osm.pbf depuis Geofabrik s'il n'existe pas
|
||||
- Crée le dossier `insee_extracts` s'il n'existe pas
|
||||
- Utilise osmium pour extraire les données OSM pour chaque zone INSEE
|
||||
- Convertit les données extraites en format JSON
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### 3. Traitement des extraits JSON pour calculer les mesures de thèmes
|
||||
Traite les extraits JSON des zones INSEE pour calculer les mesures de thèmes :
|
||||
```shell
|
||||
php bin/console app:process-insee-extracts [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force le traitement même si déjà effectué
|
||||
|
||||
Cette commande :
|
||||
- Utilise le service Motocultrice pour traiter les données
|
||||
- Met à jour la date de labourage dans l'entité Stats
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### Exemple d'utilisation pour générer des statistiques pour toute la France
|
||||
|
||||
```shell
|
||||
# 1. Récupérer les polygones de toutes les communes
|
||||
php bin/console app:retrieve-city-polygons
|
||||
|
||||
# 2. Extraire les données OSM pour chaque zone INSEE
|
||||
php bin/console app:extract-insee-zones
|
||||
|
||||
# 3. Traiter les extraits JSON pour calculer les mesures de thèmes
|
||||
php bin/console app:process-insee-extracts
|
||||
```
|
||||
|
||||
Pour traiter une seule commune (par exemple avec le code INSEE 75056 pour Paris) :
|
||||
```shell
|
||||
php bin/console app:retrieve-city-polygons 75056
|
||||
php bin/console app:extract-insee-zones 75056
|
||||
php bin/console app:process-insee-extracts 75056
|
||||
```
|
||||
|
||||
### Dépendances
|
||||
|
||||
Pour exécuter ces commandes, vous aurez besoin de :
|
||||
- Python 3 avec les bibliothèques requises pour `get_poly.py`
|
||||
- Osmium Tool (`osmium`) installé sur votre système
|
||||
- Suffisamment d'espace disque pour stocker le fichier france-latest.osm.pbf (~4 Go) et les extraits JSON
|
||||
|
||||
# Routes d'administration
|
||||
|
||||
## Création des Stats manquantes à partir du CSV
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
# Mise à jour du fichier historisé france internal
|
||||
|
||||
Ce document explique comment utiliser le script `update.py` pour mettre à jour le fichier historisé france internal (`france-internal.osh.pbf`) en utilisant l'outil `osmupdate`.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Python 3.6 ou supérieur
|
||||
- L'outil `osmupdate` installé sur le système
|
||||
- Un fichier `france-internal.osh.pbf` existant dans le répertoire `osm_data`
|
||||
|
||||
## Utilisation
|
||||
|
||||
Le script `update.py` peut être exécuté de deux façons :
|
||||
|
||||
```bash
|
||||
# Méthode 1 : Exécution directe (le script est exécutable)
|
||||
./update.py
|
||||
|
||||
# Méthode 2 : Exécution via Python
|
||||
python3 update.py
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `--verbose` ou `-v` : Affiche la sortie des commandes en temps réel, ce qui permet de suivre la progression de la mise à jour.
|
||||
|
||||
Exemple :
|
||||
```bash
|
||||
./update.py --verbose
|
||||
```
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
Le script effectue les opérations suivantes :
|
||||
|
||||
1. Vérifie si le fichier `france-internal.osh.pbf` existe dans le répertoire `osm_data`.
|
||||
2. Crée un répertoire temporaire `update_temp` s'il n'existe pas déjà.
|
||||
3. Exécute la commande `osmupdate` pour mettre à jour le fichier avec les dernières modifications d'OpenStreetMap.
|
||||
4. Crée une sauvegarde de l'ancien fichier avec l'extension `.bak`.
|
||||
5. Remplace l'ancien fichier par le nouveau fichier mis à jour.
|
||||
6. Affiche des informations sur la durée de la mise à jour.
|
||||
|
||||
## Logs
|
||||
|
||||
Le script utilise le module `logging` de Python pour enregistrer les informations importantes :
|
||||
|
||||
- Heure de début et de fin de la mise à jour
|
||||
- Durée totale de la mise à jour
|
||||
- Erreurs éventuelles lors de la mise à jour
|
||||
|
||||
## Intégration avec d'autres scripts
|
||||
|
||||
Le fichier `france-internal.osh.pbf` est utilisé comme fichier d'entrée par défaut dans le script `historize_zone.py`. Après avoir mis à jour ce fichier avec `update.py`, les analyses historiques effectuées par `historize_zone.py` utiliseront automatiquement les données les plus récentes.
|
||||
|
||||
## Automatisation
|
||||
|
||||
Pour automatiser la mise à jour régulière du fichier, vous pouvez ajouter une tâche cron :
|
||||
|
||||
```bash
|
||||
# Exemple : mise à jour quotidienne à 3h du matin
|
||||
0 3 * * * cd /chemin/vers/osm-commerce-sf/counting_osm_objects && ./update.py >> update.log 2>&1
|
||||
```
|
||||
|
||||
## Dépannage
|
||||
|
||||
Si vous rencontrez des erreurs lors de l'exécution du script :
|
||||
|
||||
1. Vérifiez que `osmupdate` est correctement installé et accessible dans le PATH.
|
||||
2. Assurez-vous que le fichier `france-internal.osh.pbf` existe dans le répertoire `osm_data`.
|
||||
3. Vérifiez les permissions des répertoires `osm_data` et `update_temp`.
|
||||
4. Consultez les logs pour plus d'informations sur l'erreur.
|
|
@ -1,4 +1,6 @@
|
|||
|
||||
osmupdate --verbose --keep-tempfiles --day -t=test_temp/ -v osm_data/france-internal.osh.pbf test_temp/changes.osc.gz
|
||||
osmupdate --verbose --keep-tempfiles --day -t=test_temp/ -v osm_data/france-latest.osm.pbf test_temp/changes.osc.gz
|
||||
#osmium extract -p polyons/commune_91111.poly -s simple changes.osc.gz -O -o test_temp/changes.local.osc.gz
|
||||
osmium apply-changes -H osm_data/france-internal.osh.pbf test_temp/changes.osc.gz -O -o osm_data/france-internal_updated.osh.pbf
|
||||
osmium apply-changes -H osm_data/france-latest.osh.pbf test_temp/changes.osc.gz -O -o osm_data/france-latest_updated.osh.pbf
|
||||
|
|
230
src/Command/ExtractInseeZonesCommand.php
Normal file
230
src/Command/ExtractInseeZonesCommand.php
Normal file
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
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:extract-insee-zones',
|
||||
description: 'Extrait les données OSM pour chaque zone INSEE à partir du fichier france-latest.osm.pbf',
|
||||
)]
|
||||
class ExtractInseeZonesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force l\'extraction même si le fichier JSON existe déjà')
|
||||
->addOption('keep-pbf', 'k', InputOption::VALUE_NONE, 'Conserve les fichiers PBF intermédiaires')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
$keepPbf = $input->getOption('keep-pbf');
|
||||
|
||||
// Créer le dossier oss_data s'il n'existe pas
|
||||
$ossDataDir = __DIR__ . '/../../oss_data';
|
||||
if (!is_dir($ossDataDir)) {
|
||||
$io->note('Création du dossier oss_data');
|
||||
mkdir($ossDataDir, 0755, true);
|
||||
}
|
||||
|
||||
// Vérifier que le fichier france-latest.osm.pbf existe
|
||||
$francePbfFile = $ossDataDir . '/france-latest.osm.pbf';
|
||||
if (!file_exists($francePbfFile)) {
|
||||
$io->note('Le fichier france-latest.osm.pbf n\'existe pas. Téléchargement en cours depuis Geofabrik...');
|
||||
|
||||
// URL de téléchargement
|
||||
$downloadUrl = 'https://download.geofabrik.de/europe/france-latest.osm.pbf';
|
||||
|
||||
// Télécharger le fichier
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'header' => "User-Agent: OSM-Commerces/1.0\r\n"
|
||||
]
|
||||
]);
|
||||
|
||||
// Utiliser file_get_contents pour télécharger le fichier
|
||||
$io->section('Téléchargement du fichier france-latest.osm.pbf');
|
||||
$io->progressStart(100);
|
||||
|
||||
// Téléchargement par morceaux pour pouvoir afficher une progression
|
||||
$fileHandle = fopen($francePbfFile, 'w');
|
||||
$curlHandle = curl_init($downloadUrl);
|
||||
|
||||
curl_setopt($curlHandle, CURLOPT_FILE, $fileHandle);
|
||||
curl_setopt($curlHandle, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curlHandle, CURLOPT_USERAGENT, 'OSM-Commerces/1.0');
|
||||
curl_setopt($curlHandle, CURLOPT_NOPROGRESS, false);
|
||||
// Use a simpler progress reporting approach
|
||||
curl_setopt($curlHandle, CURLOPT_PROGRESSFUNCTION, function($resource, $downloadSize, $downloaded) use ($io) {
|
||||
static $lastProgress = 0;
|
||||
if ($downloadSize > 0) {
|
||||
$progress = round(($downloaded / $downloadSize) * 100);
|
||||
if ($progress > $lastProgress) {
|
||||
$io->progressAdvance($progress - $lastProgress);
|
||||
$lastProgress = $progress;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$success = curl_exec($curlHandle);
|
||||
curl_close($curlHandle);
|
||||
fclose($fileHandle);
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
if (!$success) {
|
||||
throw new \Exception('Échec du téléchargement');
|
||||
}
|
||||
|
||||
$io->success('Le fichier france-latest.osm.pbf a été téléchargé avec succès.');
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Erreur lors du téléchargement du fichier france-latest.osm.pbf: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier que le dossier polygons existe
|
||||
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
|
||||
if (!is_dir($polygonsDir)) {
|
||||
$io->error('Le dossier des polygones n\'existe pas. Veuillez d\'abord exécuter la commande app:retrieve-city-polygons.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Créer le dossier pour les extractions JSON si nécessaire
|
||||
$extractsDir = __DIR__ . '/../../insee_extracts';
|
||||
if (!is_dir($extractsDir)) {
|
||||
$io->note('Création du dossier insee_extracts');
|
||||
mkdir($extractsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$existingCount = 0;
|
||||
$createdCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, extraire les données si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
|
||||
$extractPbfFile = $extractsDir . '/commune_' . $inseeCode . '.osm.pbf';
|
||||
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
|
||||
|
||||
// Vérifier si le polygone existe
|
||||
if (!file_exists($polygonFile)) {
|
||||
$io->debug(sprintf('Polygone manquant pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si l'extraction JSON existe déjà
|
||||
if (file_exists($extractJsonFile) && !$force) {
|
||||
$existingCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Étape 1: Extraire les données de france-latest.osm.pbf vers un fichier PBF pour la zone
|
||||
$extractCommand = 'osmium extract -p ' . $polygonFile . ' ' . $francePbfFile . ' -o ' . $extractPbfFile;
|
||||
$outputLines = [];
|
||||
$returnVar = 0;
|
||||
exec($extractCommand, $outputLines, $returnVar);
|
||||
|
||||
if ($returnVar !== 0 || !file_exists($extractPbfFile)) {
|
||||
$io->debug(sprintf('Erreur lors de l\'extraction PBF pour %s: %s', $inseeCode, implode("\n", $outputLines)));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Étape 2: Convertir le fichier PBF en JSON
|
||||
$exportCommand = 'osmium export ' . $extractPbfFile . ' -f json -o ' . $extractJsonFile;
|
||||
$outputLines = [];
|
||||
$returnVar = 0;
|
||||
exec($exportCommand, $outputLines, $returnVar);
|
||||
|
||||
if ($returnVar === 0 && file_exists($extractJsonFile)) {
|
||||
$createdCount++;
|
||||
} else {
|
||||
$io->debug(sprintf('Erreur lors de l\'export JSON pour %s: %s', $inseeCode, implode("\n", $outputLines)));
|
||||
$errorCount++;
|
||||
}
|
||||
|
||||
// Supprimer le fichier PBF intermédiaire pour économiser de l'espace
|
||||
if (!$keepPbf && file_exists($extractPbfFile)) {
|
||||
unlink($extractPbfFile);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Extraction des zones INSEE terminée : %d extractions créées, %d déjà existantes, %d erreurs sur un total de %d communes.",
|
||||
$createdCount,
|
||||
$existingCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
149
src/Command/ProcessInseeExtractsCommand.php
Normal file
149
src/Command/ProcessInseeExtractsCommand.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
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:process-insee-extracts',
|
||||
description: 'Traite les extraits JSON des zones INSEE pour calculer les mesures de thèmes',
|
||||
)]
|
||||
class ProcessInseeExtractsCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private Motocultrice $motocultrice;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, Motocultrice $motocultrice)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->motocultrice = $motocultrice;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force le traitement même si déjà effectué')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
|
||||
// Vérifier que le dossier des extractions existe
|
||||
$extractsDir = __DIR__ . '/../../insee_extracts';
|
||||
if (!is_dir($extractsDir)) {
|
||||
$io->error('Le dossier des extractions n\'existe pas. Veuillez d\'abord exécuter la commande app:extract-insee-zones.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$processedCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, traiter les données si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
|
||||
|
||||
// Vérifier si l'extraction JSON existe
|
||||
if (!file_exists($extractJsonFile)) {
|
||||
$io->debug(sprintf('Fichier JSON manquant pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si le traitement a déjà été effectué
|
||||
if (!$force && $stat->getDateLabourageDone() !== null) {
|
||||
$skippedCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Utiliser la Motocultrice pour traiter les données
|
||||
$result = $this->motocultrice->labourer($inseeCode);
|
||||
|
||||
if ($result) {
|
||||
// Mettre à jour la date de labourage
|
||||
$stat->setDateLabourageDone(new \DateTime());
|
||||
$this->entityManager->persist($stat);
|
||||
$processedCount++;
|
||||
|
||||
// Flush tous les 10 objets pour éviter de surcharger la mémoire
|
||||
if ($processedCount % 10 === 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->debug(sprintf('Flush après %d traitements', $processedCount));
|
||||
}
|
||||
} else {
|
||||
$io->debug(sprintf('Échec du traitement pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
// Flush les derniers objets
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Traitement des extractions INSEE terminé : %d communes traitées, %d ignorées, %d erreurs sur un total de %d communes.",
|
||||
$processedCount,
|
||||
$skippedCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
131
src/Command/RetrieveCityPolygonsCommand.php
Normal file
131
src/Command/RetrieveCityPolygonsCommand.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
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:retrieve-city-polygons',
|
||||
description: 'Récupère les polygones des villes selon leur zone donnée par le code INSEE',
|
||||
)]
|
||||
class RetrieveCityPolygonsCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force la récupération même si le polygone existe déjà')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
|
||||
// Vérifier que le dossier polygons existe, sinon le créer
|
||||
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
|
||||
if (!is_dir($polygonsDir)) {
|
||||
$io->note('Création du dossier polygons');
|
||||
mkdir($polygonsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$existingCount = 0;
|
||||
$createdCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, récupérer le polygone si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
|
||||
|
||||
// Vérifier si le polygone existe déjà
|
||||
if (file_exists($polygonFile) && !$force) {
|
||||
$existingCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Utiliser le script Python existant pour récupérer le polygone
|
||||
$command = 'cd ' . __DIR__ . '/../../counting_osm_objects && python3 get_poly.py ' . $inseeCode;
|
||||
$output = [];
|
||||
$returnVar = 0;
|
||||
exec($command, $output, $returnVar);
|
||||
|
||||
if ($returnVar === 0 && file_exists($polygonFile)) {
|
||||
$createdCount++;
|
||||
} else {
|
||||
$errorCount++;
|
||||
if ($output) {
|
||||
$io->debug(sprintf('Erreur pour %s: %s', $inseeCode, implode("\n", $output)));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Récupération des polygones terminée : %d polygones créés, %d déjà existants, %d erreurs sur un total de %d communes.",
|
||||
$createdCount,
|
||||
$existingCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1826,11 +1826,48 @@ final class AdminController extends AbstractController
|
|||
{
|
||||
$this->actionLogger->log('admin/extract_insee_zones', []);
|
||||
|
||||
// Créer le dossier oss_data s'il n'existe pas
|
||||
$ossDataDir = __DIR__ . '/../../oss_data';
|
||||
if (!is_dir($ossDataDir)) {
|
||||
mkdir($ossDataDir, 0755, true);
|
||||
}
|
||||
|
||||
// Vérifier que le fichier france-latest.osm.pbf existe
|
||||
$francePbfFile = __DIR__ . '/../../france-latest.osm.pbf';
|
||||
$francePbfFile = $ossDataDir . '/france-latest.osm.pbf';
|
||||
if (!file_exists($francePbfFile)) {
|
||||
$this->addFlash('error', 'Le fichier france-latest.osm.pbf n\'existe pas. Veuillez le télécharger depuis https://download.geofabrik.de/europe/france.html');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
$this->actionLogger->log('admin/download_france_pbf', []);
|
||||
$this->addFlash('info', 'Le fichier france-latest.osm.pbf n\'existe pas. Téléchargement en cours depuis Geofabrik...');
|
||||
|
||||
// URL de téléchargement
|
||||
$downloadUrl = 'https://download.geofabrik.de/europe/france-latest.osm.pbf';
|
||||
|
||||
// Télécharger le fichier
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'header' => "User-Agent: OSM-Commerces/1.0\r\n"
|
||||
]
|
||||
]);
|
||||
|
||||
// Utiliser file_get_contents pour télécharger le fichier
|
||||
$fileContent = file_get_contents($downloadUrl, false, $context);
|
||||
if ($fileContent === false) {
|
||||
throw new \Exception('Échec du téléchargement');
|
||||
}
|
||||
|
||||
// Sauvegarder le fichier
|
||||
if (file_put_contents($francePbfFile, $fileContent) === false) {
|
||||
throw new \Exception('Échec de l\'écriture du fichier');
|
||||
}
|
||||
|
||||
$this->addFlash('success', 'Le fichier france-latest.osm.pbf a été téléchargé avec succès.');
|
||||
} catch (\Exception $e) {
|
||||
$this->actionLogger->log('error_download_france_pbf', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$this->addFlash('error', 'Erreur lors du téléchargement du fichier france-latest.osm.pbf: ' . $e->getMessage());
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier que le dossier polygons existe
|
||||
|
@ -2008,6 +2045,571 @@ final class AdminController extends AbstractController
|
|||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
#[Route('/admin/completion-statistics', name: 'app_admin_completion_statistics')]
|
||||
public function completionStatistics(Request $request): Response
|
||||
{
|
||||
$this->actionLogger->log('admin/completion_statistics', []);
|
||||
|
||||
// Récupérer le thème sélectionné (par défaut: 'places')
|
||||
$theme = $request->query->get('theme', 'places');
|
||||
|
||||
// Récupérer le niveau géographique sélectionné (par défaut: 'department')
|
||||
$level = $request->query->get('level', 'department');
|
||||
|
||||
// Récupérer tous les thèmes disponibles
|
||||
$themes = $this->followUpService->getFollowUpThemes();
|
||||
|
||||
// Récupérer les données de complétion selon le niveau géographique
|
||||
$completionData = [];
|
||||
$chartLabels = [];
|
||||
$chartData = [];
|
||||
|
||||
switch ($level) {
|
||||
case 'department':
|
||||
// Agréger les données par département
|
||||
$completionData = $this->aggregateCompletionByDepartment($theme);
|
||||
break;
|
||||
|
||||
case 'region':
|
||||
// Agréger les données par région
|
||||
$completionData = $this->aggregateCompletionByRegion($theme);
|
||||
break;
|
||||
|
||||
case 'country':
|
||||
// Agréger les données pour la France entière
|
||||
$completionData = $this->aggregateCompletionForCountry($theme);
|
||||
break;
|
||||
|
||||
case 'city':
|
||||
// Récupérer les données pour les villes (limité aux 50 plus grandes)
|
||||
$completionData = $this->aggregateCompletionForTopCities($theme, 50);
|
||||
break;
|
||||
}
|
||||
|
||||
// Préparer les données pour les graphiques
|
||||
foreach ($completionData as $key => $value) {
|
||||
$chartLabels[] = $key;
|
||||
$chartData[] = $value;
|
||||
}
|
||||
|
||||
return $this->render('admin/completion_statistics.html.twig', [
|
||||
'theme' => $theme,
|
||||
'level' => $level,
|
||||
'themes' => $themes,
|
||||
'chartLabels' => json_encode($chartLabels),
|
||||
'chartData' => json_encode($chartData),
|
||||
'completionData' => $completionData
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrège les données de complétion par département
|
||||
*/
|
||||
private function aggregateCompletionByDepartment(string $theme): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Récupérer toutes les Stats
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
$allStats = $statsRepo->findAll();
|
||||
|
||||
// Grouper par département (2 premiers chiffres du code INSEE)
|
||||
$departmentData = [];
|
||||
$departmentCounts = [];
|
||||
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode || strlen($inseeCode) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extraire le code département (2 premiers chiffres du code INSEE)
|
||||
$departmentCode = substr($inseeCode, 0, 2);
|
||||
|
||||
// Cas particuliers pour la Corse
|
||||
if ($departmentCode === '2A' || $departmentCode === '2B') {
|
||||
$departmentCode = substr($inseeCode, 0, 3);
|
||||
}
|
||||
|
||||
// Récupérer les mesures pour le thème spécifié
|
||||
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
|
||||
|
||||
if ($latestMeasure !== null) {
|
||||
if (!isset($departmentData[$departmentCode])) {
|
||||
$departmentData[$departmentCode] = 0;
|
||||
$departmentCounts[$departmentCode] = 0;
|
||||
}
|
||||
|
||||
$departmentData[$departmentCode] += $latestMeasure;
|
||||
$departmentCounts[$departmentCode]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer les moyennes par département
|
||||
foreach ($departmentData as $departmentCode => $total) {
|
||||
if ($departmentCounts[$departmentCode] > 0) {
|
||||
$result[$departmentCode] = round($total / $departmentCounts[$departmentCode], 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par code département
|
||||
ksort($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrège les données de complétion par région
|
||||
*/
|
||||
private function aggregateCompletionByRegion(string $theme): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Mapping des départements vers les régions
|
||||
$departmentToRegion = $this->getDepartmentToRegionMapping();
|
||||
|
||||
// Récupérer les données par département
|
||||
$departmentData = $this->aggregateCompletionByDepartment($theme);
|
||||
|
||||
// Grouper par région
|
||||
$regionData = [];
|
||||
$regionCounts = [];
|
||||
|
||||
foreach ($departmentData as $departmentCode => $value) {
|
||||
// Trouver la région correspondante
|
||||
$region = $departmentToRegion[$departmentCode] ?? 'Autre';
|
||||
|
||||
if (!isset($regionData[$region])) {
|
||||
$regionData[$region] = 0;
|
||||
$regionCounts[$region] = 0;
|
||||
}
|
||||
|
||||
$regionData[$region] += $value;
|
||||
$regionCounts[$region]++;
|
||||
}
|
||||
|
||||
// Calculer les moyennes par région
|
||||
foreach ($regionData as $region => $total) {
|
||||
if ($regionCounts[$region] > 0) {
|
||||
$result[$region] = round($total / $regionCounts[$region], 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par nom de région
|
||||
ksort($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrège les données de complétion pour la France entière
|
||||
*/
|
||||
private function aggregateCompletionForCountry(string $theme): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Récupérer toutes les Stats
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
$allStats = $statsRepo->findAll();
|
||||
|
||||
// Récupérer les mesures pour le thème spécifié
|
||||
$totalMeasure = 0;
|
||||
$count = 0;
|
||||
|
||||
foreach ($allStats as $stat) {
|
||||
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
|
||||
|
||||
if ($latestMeasure !== null) {
|
||||
$totalMeasure += $latestMeasure;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer la moyenne nationale
|
||||
if ($count > 0) {
|
||||
$result['France'] = round($totalMeasure / $count, 2);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrège les données de complétion pour les principales villes
|
||||
*/
|
||||
private function aggregateCompletionForTopCities(string $theme, int $limit = 50): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Récupérer les Stats triées par population (descendant)
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
$topCities = $statsRepo->findBy(
|
||||
['population' => null], // Critère (tous)
|
||||
['population' => 'DESC'], // Tri par population descendante
|
||||
$limit // Limite
|
||||
);
|
||||
|
||||
// Récupérer les mesures pour le thème spécifié
|
||||
foreach ($topCities as $stat) {
|
||||
$cityName = $stat->getName() ?? $stat->getZone();
|
||||
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
|
||||
|
||||
if ($latestMeasure !== null) {
|
||||
$result[$cityName] = $latestMeasure;
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par valeur décroissante
|
||||
arsort($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la dernière mesure pour un thème donné
|
||||
*/
|
||||
private function getLatestMeasureForTheme(Stats $stat, string $theme): ?float
|
||||
{
|
||||
$cityFollowUps = $stat->getCityFollowUps();
|
||||
|
||||
// Filtrer les CityFollowUp par thème et trier par date (descendant)
|
||||
$filteredFollowUps = $cityFollowUps->filter(function(CityFollowUp $followUp) use ($theme) {
|
||||
return $followUp->getName() === $theme;
|
||||
});
|
||||
|
||||
if ($filteredFollowUps->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trier par date (du plus récent au plus ancien)
|
||||
$iterator = $filteredFollowUps->getIterator();
|
||||
$iterator->uasort(function(CityFollowUp $a, CityFollowUp $b) {
|
||||
return $b->getDate() <=> $a->getDate();
|
||||
});
|
||||
|
||||
// Récupérer la mesure la plus récente
|
||||
$latestFollowUp = $iterator->current();
|
||||
return $latestFollowUp->getMeasure();
|
||||
}
|
||||
|
||||
#[Route('/admin/osmose-issues-map/{inseeCode}', name: 'app_admin_osmose_issues_map')]
|
||||
public function osmoseIssuesMap(Request $request, string $inseeCode = null): Response
|
||||
{
|
||||
$this->actionLogger->log('admin/osmose_issues_map', [
|
||||
'insee_code' => $inseeCode
|
||||
]);
|
||||
|
||||
// Si aucun code INSEE n'est fourni, rediriger vers la liste des villes
|
||||
if (!$inseeCode) {
|
||||
$this->addFlash('info', 'Veuillez sélectionner une ville pour afficher les problèmes Osmose.');
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
// Récupérer la ville correspondante
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
$city = $statsRepo->findOneBy(['zone' => $inseeCode]);
|
||||
|
||||
if (!$city) {
|
||||
$this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode);
|
||||
return $this->redirectToRoute('app_admin');
|
||||
}
|
||||
|
||||
// Récupérer le thème sélectionné (par défaut: tous)
|
||||
$theme = $request->query->get('theme', 'all');
|
||||
|
||||
// Récupérer tous les thèmes disponibles
|
||||
$themes = $this->followUpService->getFollowUpThemes();
|
||||
|
||||
// Récupérer les problèmes Osmose pour cette ville
|
||||
$osmoseIssues = $this->getOsmoseIssuesForCity($city, $theme);
|
||||
|
||||
return $this->render('admin/osmose_issues_map.html.twig', [
|
||||
'city' => $city,
|
||||
'theme' => $theme,
|
||||
'themes' => $themes,
|
||||
'osmoseIssues' => $osmoseIssues
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les problèmes Osmose pour une ville donnée
|
||||
*/
|
||||
private function getOsmoseIssuesForCity(Stats $city, string $theme = 'all'): array
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
// Coordonnées de la ville
|
||||
$lat = $city->getLat();
|
||||
$lon = $city->getLon();
|
||||
|
||||
if (!$lat || !$lon) {
|
||||
return $issues;
|
||||
}
|
||||
|
||||
// Construire l'URL de l'API Osmose
|
||||
$bbox = $this->calculateBoundingBox($lat, $lon, 5); // 5km autour du centre de la ville
|
||||
$osmoseApiUrl = sprintf(
|
||||
'https://osmose.openstreetmap.fr/api/0.3/issues?bbox=%f,%f,%f,%f&item=xxxx&limit=500',
|
||||
$bbox['min_lon'],
|
||||
$bbox['min_lat'],
|
||||
$bbox['max_lon'],
|
||||
$bbox['max_lat']
|
||||
);
|
||||
|
||||
// Récupérer les items Osmose correspondant aux thèmes
|
||||
$itemIds = $this->getOsmoseItemIdsForTheme($theme);
|
||||
if (!empty($itemIds)) {
|
||||
$osmoseApiUrl = str_replace('xxxx', implode(',', $itemIds), $osmoseApiUrl);
|
||||
} else {
|
||||
$osmoseApiUrl = str_replace('&item=xxxx', '', $osmoseApiUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
// Appeler l'API Osmose
|
||||
$response = file_get_contents($osmoseApiUrl);
|
||||
if ($response === false) {
|
||||
throw new \Exception('Échec de la récupération des données Osmose');
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['issues'])) {
|
||||
foreach ($data['issues'] as $issue) {
|
||||
// Vérifier si l'issue est dans les limites de la ville (approximativement)
|
||||
if ($this->isPointInCity($issue['lat'], $issue['lon'], $lat, $lon, 5)) {
|
||||
$issues[] = [
|
||||
'id' => $issue['id'],
|
||||
'title' => $issue['title'] ?? 'Problème sans titre',
|
||||
'subtitle' => $issue['subtitle'] ?? '',
|
||||
'lat' => $issue['lat'],
|
||||
'lon' => $issue['lon'],
|
||||
'item' => $issue['item'],
|
||||
'class' => $issue['class'],
|
||||
'level' => $issue['level'] ?? 2,
|
||||
'update_timestamp' => $issue['update_timestamp'] ?? null,
|
||||
'url' => sprintf('https://osmose.openstreetmap.fr/fr/issue/%s', $issue['uuid'])
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->actionLogger->log('error_osmose_api', [
|
||||
'insee_code' => $city->getZone(),
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule une bounding box autour d'un point
|
||||
*/
|
||||
private function calculateBoundingBox(string $lat, string $lon, int $distanceKm = 5): array
|
||||
{
|
||||
$lat = (float)$lat;
|
||||
$lon = (float)$lon;
|
||||
|
||||
// Approximation: 1 degré de latitude = 111 km
|
||||
$latDelta = $distanceKm / 111.0;
|
||||
|
||||
// Approximation: 1 degré de longitude = 111 * cos(latitude) km
|
||||
$lonDelta = $distanceKm / (111.0 * cos(deg2rad($lat)));
|
||||
|
||||
return [
|
||||
'min_lat' => $lat - $latDelta,
|
||||
'max_lat' => $lat + $latDelta,
|
||||
'min_lon' => $lon - $lonDelta,
|
||||
'max_lon' => $lon + $lonDelta
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un point est dans un rayon donné autour d'une ville
|
||||
*/
|
||||
private function isPointInCity(float $pointLat, float $pointLon, string $cityLat, string $cityLon, int $radiusKm = 5): bool
|
||||
{
|
||||
$cityLat = (float)$cityLat;
|
||||
$cityLon = (float)$cityLon;
|
||||
|
||||
// Calcul de la distance en km (formule de Haversine)
|
||||
$earthRadius = 6371; // Rayon de la Terre en km
|
||||
|
||||
$dLat = deg2rad($pointLat - $cityLat);
|
||||
$dLon = deg2rad($pointLon - $cityLon);
|
||||
|
||||
$a = sin($dLat/2) * sin($dLat/2) +
|
||||
cos(deg2rad($cityLat)) * cos(deg2rad($pointLat)) *
|
||||
sin($dLon/2) * sin($dLon/2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
$distance = $earthRadius * $c;
|
||||
|
||||
return $distance <= $radiusKm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les IDs des items Osmose correspondant à un thème
|
||||
*/
|
||||
private function getOsmoseItemIdsForTheme(string $theme): array
|
||||
{
|
||||
// Mapping des thèmes vers les items Osmose
|
||||
$themeToItemsMapping = [
|
||||
'places' => [8230, 8240, 8250, 8260], // Commerces et services
|
||||
'restaurants' => [8030, 8031, 8032], // Restaurants et cafés
|
||||
'hotels' => [8040, 8041, 8042], // Hébergements
|
||||
'tourism' => [8010, 8011, 8012, 8013], // Tourisme
|
||||
'leisure' => [8050, 8051, 8052], // Loisirs
|
||||
'healthcare' => [8060, 8061, 8062], // Santé
|
||||
'education' => [8070, 8071, 8072], // Éducation
|
||||
'transportation' => [4010, 4020, 4030, 4040], // Transport
|
||||
'amenities' => [8080, 8081, 8082], // Équipements
|
||||
// Si d'autres thèmes sont nécessaires, ajoutez-les ici
|
||||
];
|
||||
|
||||
// Si le thème est 'all' ou n'existe pas dans le mapping, retourner un tableau vide
|
||||
if ($theme === 'all' || !isset($themeToItemsMapping[$theme])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $themeToItemsMapping[$theme];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne un mapping des départements vers les régions
|
||||
*/
|
||||
private function getDepartmentToRegionMapping(): array
|
||||
{
|
||||
return [
|
||||
// Auvergne-Rhône-Alpes
|
||||
'01' => 'Auvergne-Rhône-Alpes', // Ain
|
||||
'03' => 'Auvergne-Rhône-Alpes', // Allier
|
||||
'07' => 'Auvergne-Rhône-Alpes', // Ardèche
|
||||
'15' => 'Auvergne-Rhône-Alpes', // Cantal
|
||||
'26' => 'Auvergne-Rhône-Alpes', // Drôme
|
||||
'38' => 'Auvergne-Rhône-Alpes', // Isère
|
||||
'42' => 'Auvergne-Rhône-Alpes', // Loire
|
||||
'43' => 'Auvergne-Rhône-Alpes', // Haute-Loire
|
||||
'63' => 'Auvergne-Rhône-Alpes', // Puy-de-Dôme
|
||||
'69' => 'Auvergne-Rhône-Alpes', // Rhône
|
||||
'73' => 'Auvergne-Rhône-Alpes', // Savoie
|
||||
'74' => 'Auvergne-Rhône-Alpes', // Haute-Savoie
|
||||
|
||||
// Bourgogne-Franche-Comté
|
||||
'21' => 'Bourgogne-Franche-Comté', // Côte-d'Or
|
||||
'25' => 'Bourgogne-Franche-Comté', // Doubs
|
||||
'39' => 'Bourgogne-Franche-Comté', // Jura
|
||||
'58' => 'Bourgogne-Franche-Comté', // Nièvre
|
||||
'70' => 'Bourgogne-Franche-Comté', // Haute-Saône
|
||||
'71' => 'Bourgogne-Franche-Comté', // Saône-et-Loire
|
||||
'89' => 'Bourgogne-Franche-Comté', // Yonne
|
||||
'90' => 'Bourgogne-Franche-Comté', // Territoire de Belfort
|
||||
|
||||
// Bretagne
|
||||
'22' => 'Bretagne', // Côtes-d'Armor
|
||||
'29' => 'Bretagne', // Finistère
|
||||
'35' => 'Bretagne', // Ille-et-Vilaine
|
||||
'56' => 'Bretagne', // Morbihan
|
||||
|
||||
// Centre-Val de Loire
|
||||
'18' => 'Centre-Val de Loire', // Cher
|
||||
'28' => 'Centre-Val de Loire', // Eure-et-Loir
|
||||
'36' => 'Centre-Val de Loire', // Indre
|
||||
'37' => 'Centre-Val de Loire', // Indre-et-Loire
|
||||
'41' => 'Centre-Val de Loire', // Loir-et-Cher
|
||||
'45' => 'Centre-Val de Loire', // Loiret
|
||||
|
||||
// Corse
|
||||
'2A' => 'Corse', // Corse-du-Sud
|
||||
'2B' => 'Corse', // Haute-Corse
|
||||
|
||||
// Grand Est
|
||||
'08' => 'Grand Est', // Ardennes
|
||||
'10' => 'Grand Est', // Aube
|
||||
'51' => 'Grand Est', // Marne
|
||||
'52' => 'Grand Est', // Haute-Marne
|
||||
'54' => 'Grand Est', // Meurthe-et-Moselle
|
||||
'55' => 'Grand Est', // Meuse
|
||||
'57' => 'Grand Est', // Moselle
|
||||
'67' => 'Grand Est', // Bas-Rhin
|
||||
'68' => 'Grand Est', // Haut-Rhin
|
||||
'88' => 'Grand Est', // Vosges
|
||||
|
||||
// Hauts-de-France
|
||||
'02' => 'Hauts-de-France', // Aisne
|
||||
'59' => 'Hauts-de-France', // Nord
|
||||
'60' => 'Hauts-de-France', // Oise
|
||||
'62' => 'Hauts-de-France', // Pas-de-Calais
|
||||
'80' => 'Hauts-de-France', // Somme
|
||||
|
||||
// Île-de-France
|
||||
'75' => 'Île-de-France', // Paris
|
||||
'77' => 'Île-de-France', // Seine-et-Marne
|
||||
'78' => 'Île-de-France', // Yvelines
|
||||
'91' => 'Île-de-France', // Essonne
|
||||
'92' => 'Île-de-France', // Hauts-de-Seine
|
||||
'93' => 'Île-de-France', // Seine-Saint-Denis
|
||||
'94' => 'Île-de-France', // Val-de-Marne
|
||||
'95' => 'Île-de-France', // Val-d'Oise
|
||||
|
||||
// Normandie
|
||||
'14' => 'Normandie', // Calvados
|
||||
'27' => 'Normandie', // Eure
|
||||
'50' => 'Normandie', // Manche
|
||||
'61' => 'Normandie', // Orne
|
||||
'76' => 'Normandie', // Seine-Maritime
|
||||
|
||||
// Nouvelle-Aquitaine
|
||||
'16' => 'Nouvelle-Aquitaine', // Charente
|
||||
'17' => 'Nouvelle-Aquitaine', // Charente-Maritime
|
||||
'19' => 'Nouvelle-Aquitaine', // Corrèze
|
||||
'23' => 'Nouvelle-Aquitaine', // Creuse
|
||||
'24' => 'Nouvelle-Aquitaine', // Dordogne
|
||||
'33' => 'Nouvelle-Aquitaine', // Gironde
|
||||
'40' => 'Nouvelle-Aquitaine', // Landes
|
||||
'47' => 'Nouvelle-Aquitaine', // Lot-et-Garonne
|
||||
'64' => 'Nouvelle-Aquitaine', // Pyrénées-Atlantiques
|
||||
'79' => 'Nouvelle-Aquitaine', // Deux-Sèvres
|
||||
'86' => 'Nouvelle-Aquitaine', // Vienne
|
||||
'87' => 'Nouvelle-Aquitaine', // Haute-Vienne
|
||||
|
||||
// Occitanie
|
||||
'09' => 'Occitanie', // Ariège
|
||||
'11' => 'Occitanie', // Aude
|
||||
'12' => 'Occitanie', // Aveyron
|
||||
'30' => 'Occitanie', // Gard
|
||||
'31' => 'Occitanie', // Haute-Garonne
|
||||
'32' => 'Occitanie', // Gers
|
||||
'34' => 'Occitanie', // Hérault
|
||||
'46' => 'Occitanie', // Lot
|
||||
'48' => 'Occitanie', // Lozère
|
||||
'65' => 'Occitanie', // Hautes-Pyrénées
|
||||
'66' => 'Occitanie', // Pyrénées-Orientales
|
||||
'81' => 'Occitanie', // Tarn
|
||||
'82' => 'Occitanie', // Tarn-et-Garonne
|
||||
|
||||
// Pays de la Loire
|
||||
'44' => 'Pays de la Loire', // Loire-Atlantique
|
||||
'49' => 'Pays de la Loire', // Maine-et-Loire
|
||||
'53' => 'Pays de la Loire', // Mayenne
|
||||
'72' => 'Pays de la Loire', // Sarthe
|
||||
'85' => 'Pays de la Loire', // Vendée
|
||||
|
||||
// Provence-Alpes-Côte d'Azur
|
||||
'04' => 'Provence-Alpes-Côte d\'Azur', // Alpes-de-Haute-Provence
|
||||
'05' => 'Provence-Alpes-Côte d\'Azur', // Hautes-Alpes
|
||||
'06' => 'Provence-Alpes-Côte d\'Azur', // Alpes-Maritimes
|
||||
'13' => 'Provence-Alpes-Côte d\'Azur', // Bouches-du-Rhône
|
||||
'83' => 'Provence-Alpes-Côte d\'Azur', // Var
|
||||
'84' => 'Provence-Alpes-Côte d\'Azur', // Vaucluse
|
||||
|
||||
// Départements d'outre-mer
|
||||
'971' => 'Guadeloupe',
|
||||
'972' => 'Martinique',
|
||||
'973' => 'Guyane',
|
||||
'974' => 'La Réunion',
|
||||
'976' => 'Mayotte'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.)
|
||||
*/
|
||||
|
|
196
templates/admin/completion_statistics.html.twig
Normal file
196
templates/admin/completion_statistics.html.twig
Normal file
|
@ -0,0 +1,196 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Statistiques de complétion{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.data-table {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.level-selector .btn {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.theme-selector {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Statistiques de complétion</h1>
|
||||
|
||||
<div class="filters">
|
||||
<form method="get" action="{{ path('app_admin_completion_statistics') }}" class="row">
|
||||
<div class="col-md-12">
|
||||
<h5>Niveau géographique</h5>
|
||||
<div class="level-selector">
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'department', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'department' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par département
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'region', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'region' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par région
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'country', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'country' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
France entière
|
||||
</a>
|
||||
<a href="{{ path('app_admin_completion_statistics', {'level': 'city', 'theme': theme}) }}"
|
||||
class="btn btn-sm {{ level == 'city' ? 'btn-primary' : 'btn-outline-primary' }}">
|
||||
Par ville
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 theme-selector">
|
||||
<h5>Thématique</h5>
|
||||
<select name="theme" class="form-select" onchange="this.form.submit()">
|
||||
{% for themeKey, themeLabel in themes %}
|
||||
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
||||
{{ themeLabel }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="completionChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="data-table">
|
||||
<h3>Données détaillées</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if level == 'department' %}
|
||||
<th>Code département</th>
|
||||
{% elseif level == 'region' %}
|
||||
<th>Région</th>
|
||||
{% elseif level == 'country' %}
|
||||
<th>Pays</th>
|
||||
{% else %}
|
||||
<th>Ville</th>
|
||||
{% endif %}
|
||||
<th>Taux de complétion (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, value in completionData %}
|
||||
<tr>
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('completionChart').getContext('2d');
|
||||
|
||||
// Définir les couleurs en fonction du niveau géographique
|
||||
let backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
||||
let borderColor = 'rgba(54, 162, 235, 1)';
|
||||
|
||||
{% if level == 'department' %}
|
||||
backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
||||
borderColor = 'rgba(54, 162, 235, 1)';
|
||||
{% elseif level == 'region' %}
|
||||
backgroundColor = 'rgba(255, 159, 64, 0.5)';
|
||||
borderColor = 'rgba(255, 159, 64, 1)';
|
||||
{% elseif level == 'country' %}
|
||||
backgroundColor = 'rgba(75, 192, 192, 0.5)';
|
||||
borderColor = 'rgba(75, 192, 192, 1)';
|
||||
{% else %}
|
||||
backgroundColor = 'rgba(153, 102, 255, 0.5)';
|
||||
borderColor = 'rgba(153, 102, 255, 1)';
|
||||
{% endif %}
|
||||
|
||||
// Créer le graphique
|
||||
const completionChart = new Chart(ctx, {
|
||||
type: {% if level == 'country' %}'bar'{% else %}'bar'{% endif %},
|
||||
data: {
|
||||
labels: {{ chartLabels|raw }},
|
||||
datasets: [{
|
||||
label: 'Taux de complétion (%)',
|
||||
data: {{ chartData|raw }},
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Taux de complétion (%)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: {% if level == 'department' %}
|
||||
'Départements'
|
||||
{% elseif level == 'region' %}
|
||||
'Régions'
|
||||
{% elseif level == 'country' %}
|
||||
'Pays'
|
||||
{% else %}
|
||||
'Villes'
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Taux de complétion par {{ level == "department" ? "département" : (level == "region" ? "région" : (level == "country" ? "pays" : "ville")) }} pour {{ themes[theme] }}',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': ' + context.raw + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
353
templates/admin/osmose_issues_map.html.twig
Normal file
353
templates/admin/osmose_issues_map.html.twig
Normal file
|
@ -0,0 +1,353 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Carte des problèmes Osmose - {{ city.name }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
#map {
|
||||
height: 70vh;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.issue-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.issue-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.issue-item.level-1 {
|
||||
border-left-color: #dc3545; /* Rouge pour les erreurs critiques */
|
||||
}
|
||||
.issue-item.level-2 {
|
||||
border-left-color: #fd7e14; /* Orange pour les erreurs importantes */
|
||||
}
|
||||
.issue-item.level-3 {
|
||||
border-left-color: #ffc107; /* Jaune pour les avertissements */
|
||||
}
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
.marker-level-1 {
|
||||
background-color: #dc3545;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.marker-level-2 {
|
||||
background-color: #fd7e14;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.marker-level-3 {
|
||||
background-color: #ffc107;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.no-issues {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-4">
|
||||
<h1>Problèmes Osmose pour {{ city.name }}</h1>
|
||||
|
||||
<div class="filters">
|
||||
<form method="get" action="{{ path('app_admin_osmose_issues_map', {'inseeCode': city.zone}) }}" class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="theme">Filtrer par thème</label>
|
||||
<select name="theme" id="theme" class="form-select" onchange="this.form.submit()">
|
||||
<option value="all" {{ theme == 'all' ? 'selected' : '' }}>Tous les thèmes</option>
|
||||
{% for themeKey, themeLabel in themes %}
|
||||
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
||||
{{ themeLabel }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Liste des problèmes ({{ osmoseIssues|length }})</h3>
|
||||
|
||||
{% if osmoseIssues|length > 0 %}
|
||||
<div class="issue-list">
|
||||
{% for issue in osmoseIssues %}
|
||||
<div class="issue-item level-{{ issue.level }}" data-lat="{{ issue.lat }}" data-lon="{{ issue.lon }}">
|
||||
<h5>{{ issue.title }}</h5>
|
||||
{% if issue.subtitle %}
|
||||
<p>{{ issue.subtitle }}</p>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="badge bg-secondary">Item: {{ issue.item }}</span>
|
||||
<a href="{{ issue.url }}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-issues">
|
||||
<p>Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialiser la carte avec MapLibre
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: 'https://demotiles.maplibre.org/style.json', // style URL
|
||||
center: [{{ city.lon }}, {{ city.lat }}], // Note: MapLibre uses [longitude, latitude] order
|
||||
zoom: 13
|
||||
});
|
||||
|
||||
// Ajouter les contrôles de navigation
|
||||
map.addControl(new maplibregl.NavigationControl());
|
||||
|
||||
// Attendre que la carte soit chargée
|
||||
map.on('load', function() {
|
||||
// Créer une source de données pour les marqueurs
|
||||
const features = [];
|
||||
|
||||
// Ajouter les marqueurs pour chaque problème
|
||||
{% for issue in osmoseIssues %}
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [{{ issue.lon }}, {{ issue.lat }}]
|
||||
},
|
||||
properties: {
|
||||
title: "{{ issue.title|e('js') }}",
|
||||
subtitle: "{{ issue.subtitle|e('js') }}",
|
||||
item: "{{ issue.item }}",
|
||||
url: "{{ issue.url }}",
|
||||
level: "{{ issue.level }}"
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Ajouter la source de données à la carte
|
||||
map.addSource('issues', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: features
|
||||
},
|
||||
cluster: true,
|
||||
clusterMaxZoom: 14,
|
||||
clusterRadius: 50
|
||||
});
|
||||
|
||||
// Ajouter une couche pour les clusters
|
||||
map.addLayer({
|
||||
id: 'clusters',
|
||||
type: 'circle',
|
||||
source: 'issues',
|
||||
filter: ['has', 'point_count'],
|
||||
paint: {
|
||||
'circle-color': [
|
||||
'step',
|
||||
['get', 'point_count'],
|
||||
'rgba(181, 226, 140, 0.6)',
|
||||
10,
|
||||
'rgba(241, 211, 87, 0.6)',
|
||||
30,
|
||||
'rgba(253, 156, 115, 0.6)'
|
||||
],
|
||||
'circle-radius': [
|
||||
'step',
|
||||
['get', 'point_count'],
|
||||
20,
|
||||
10,
|
||||
30,
|
||||
30,
|
||||
40
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter une couche pour le nombre de points dans chaque cluster
|
||||
map.addLayer({
|
||||
id: 'cluster-count',
|
||||
type: 'symbol',
|
||||
source: 'issues',
|
||||
filter: ['has', 'point_count'],
|
||||
layout: {
|
||||
'text-field': '{point_count_abbreviated}',
|
||||
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
|
||||
'text-size': 12
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter une couche pour les points individuels
|
||||
map.addLayer({
|
||||
id: 'unclustered-point',
|
||||
type: 'circle',
|
||||
source: 'issues',
|
||||
filter: ['!', ['has', 'point_count']],
|
||||
paint: {
|
||||
'circle-color': [
|
||||
'match',
|
||||
['get', 'level'],
|
||||
'1', '#dc3545',
|
||||
'2', '#fd7e14',
|
||||
'3', '#ffc107',
|
||||
'#007bff'
|
||||
],
|
||||
'circle-radius': 10,
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#fff'
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les clusters
|
||||
map.on('click', 'clusters', function(e) {
|
||||
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
|
||||
const clusterId = features[0].properties.cluster_id;
|
||||
|
||||
map.getSource('issues').getClusterExpansionZoom(clusterId, function(err, zoom) {
|
||||
if (err) return;
|
||||
|
||||
map.easeTo({
|
||||
center: features[0].geometry.coordinates,
|
||||
zoom: zoom
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les points individuels
|
||||
map.on('click', 'unclustered-point', function(e) {
|
||||
const coordinates = e.features[0].geometry.coordinates.slice();
|
||||
const title = e.features[0].properties.title;
|
||||
const subtitle = e.features[0].properties.subtitle;
|
||||
const item = e.features[0].properties.item;
|
||||
const url = e.features[0].properties.url;
|
||||
|
||||
// Créer le contenu de la popup
|
||||
let popupContent = `
|
||||
<h5>${title}</h5>
|
||||
`;
|
||||
|
||||
if (subtitle) {
|
||||
popupContent += `<p>${subtitle}</p>`;
|
||||
}
|
||||
|
||||
popupContent += `
|
||||
<div>
|
||||
<span class="badge bg-secondary">Item: ${item}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="${url}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assurer que si le zoom change, la popup reste à la bonne position
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
|
||||
}
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(popupContent)
|
||||
.addTo(map);
|
||||
});
|
||||
|
||||
// Changer le curseur au survol des clusters et des points
|
||||
map.on('mouseenter', 'clusters', function() {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
map.on('mouseleave', 'clusters', function() {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
map.on('mouseenter', 'unclustered-point', function() {
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
map.on('mouseleave', 'unclustered-point', function() {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
|
||||
// Ajouter un événement de clic sur les éléments de la liste
|
||||
{% for issue in osmoseIssues %}
|
||||
document.querySelector(`.issue-item[data-lat="{{ issue.lat }}"][data-lon="{{ issue.lon }}"]`)?.addEventListener('click', function() {
|
||||
map.flyTo({
|
||||
center: [{{ issue.lon }}, {{ issue.lat }}],
|
||||
zoom: 18
|
||||
});
|
||||
|
||||
// Simuler un clic sur le point pour ouvrir la popup
|
||||
const features = map.queryRenderedFeatures(
|
||||
map.project([{{ issue.lon }}, {{ issue.lat }}]),
|
||||
{ layers: ['unclustered-point'] }
|
||||
);
|
||||
|
||||
if (features.length > 0) {
|
||||
map.fire('click', {
|
||||
lngLat: { lng: {{ issue.lon }}, lat: {{ issue.lat }} },
|
||||
point: map.project([{{ issue.lon }}, {{ issue.lat }}]),
|
||||
features: [features[0]]
|
||||
});
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Ajuster la vue pour montrer tous les marqueurs si nécessaire
|
||||
if (features.length > 0) {
|
||||
// Calculer les limites de tous les points
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
features.forEach(function(feature) {
|
||||
bounds.extend(feature.geometry.coordinates);
|
||||
});
|
||||
|
||||
map.fitBounds(bounds, {
|
||||
padding: 50
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -47,6 +47,7 @@
|
|||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_podium_contributeurs_osm') }}"><i class="bi bi-trophy-fill"></i> Podium des contributeurs OSM</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('admin_followup_global_graph') }}"><i class="bi bi-globe"></i> Suivi global OSM</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_osmose_issues_map') }}"><i class="bi bi-exclamation-triangle-fill"></i> Problèmes Osmose</a></li>
|
||||
<li><a class="dropdown-item" href="{{ path('app_admin_import_stats') }}"><i class="bi bi-upload"></i> Import Stats</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
@ -7,7 +7,7 @@ jour ou de traductions, et publier des suggestions sur Mastodon pour encourager
|
|||
|
||||
Le projet comprend neuf scripts principaux :
|
||||
|
||||
1. **wiki_compare.py** : Récupère les 10 clés OSM les plus utilisées, compare leurs pages wiki en anglais et en
|
||||
1. **wiki_compare.py** : Récupère les 50 clés OSM les plus utilisées, compare leurs pages wiki en anglais et en
|
||||
français, et identifie celles qui ont besoin de mises à jour.
|
||||
2. **post_outdated_page.py** : Sélectionne aléatoirement une page wiki française qui n'est pas à jour et publie un
|
||||
message sur Mastodon pour suggérer sa mise à jour.
|
||||
|
|
|
@ -2388,7 +2388,8 @@
|
|||
"FR:Propriétés"
|
||||
],
|
||||
"common": []
|
||||
}
|
||||
},
|
||||
"proposed_translation": " Voici la traduction du texte anglais vers le français :\n\nType Description\nType de relation. Groupe : propriétés Utilisés sur ces éléments\nValeurs documentées : 20\nVoir aussi * :type =*\nStatut : de facto type = *\nPlus de détails à la page info des balises\nTools pour cette balise taginfo · AD · AT · BR · BY · CH · CN · CZ · DE · DK · FI · FR · GB · GR · HU · IN · IR · IT · LI · LU · JP · KP · KR · NL · PL · PT · RU · ES · AR · MX · CO · BO · CL · EC · PY · PE · UY · VE · TW · UA · US · VN\noverpass-turbo OSM Tag History\ntype =* sur un objet relation spécifie son type et les interactions entre ses membres. Les types de relations établis et proposés sont listés ci-dessous. type a été également occasionnellement utilisé comme balise supplémentaire pour spécifier une \"variante\" d'une catégorie de fonctionnalité sur les voies et points. Cette approche est en conflit avec son utilisation dans les relations et devrait être évitée . De plus, pour les éléments possédant plusieurs balises, il n'est pas clair à quelle balise le type correspond. Au lieu de cela, utiliser une approche basée sur un suffixe ou une sous-balise, comme décrit dans *:type =* .\nContenu\n1 Relations établies\n2 Relations peu utilisées\n3 Utilisations proposées\n3.1 Junctions, intersections, grade separated crossings, and embankments\n3.2 Area hierarchies et autres relations pour les zones\n3.3 Adressage\n3.4 Autres\n4 Possibles erreurs de balises\n5 Qualité Assurance\n6 Voir aussi\nRelations établies\nType Statut Membres Commentaires Statistics Image\nmultipolygon de facto ( )\nZones où l'enveloppe se compose de plusieurs voies, ou qui possèdent des trous.\nroute de facto\nUne route établie (généralement signalée) sur une voie\nvaleurs documentées : 10\nvoir aussi *:route =*\nturn_restriction =*\nOpposition à la balise\n3.5 Système de signalisation\n\nNota bene : Il y a plusieurs erreurs dans le texte original anglais, ainsi que des erreurs de traduction dans la version française. C'est pourquoi j'ai fourni une traduction corrigée ici."
|
||||
},
|
||||
{
|
||||
"key": "surface",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue