import city followup from csv

This commit is contained in:
Tykayn 2025-08-08 17:45:20 +02:00 committed by tykayn
parent eee5d6349a
commit dfeaf123f4
4 changed files with 35253 additions and 1 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
# Commande d'import des mesures thématiques depuis CSV
Cette documentation décrit l'utilisation de la commande `app:import-cityfollowup-csv` qui permet d'importer des mesures thématiques depuis des fichiers CSV pour une ville donnée.
## Description
La commande `app:import-cityfollowup-csv` permet d'importer des données de suivi thématique (CityFollowUp) depuis des fichiers CSV générés par les scripts de comptage d'objets OSM. Elle compare les mesures existantes en base de données avec celles du fichier CSV et ajoute uniquement les nouvelles mesures, en excluant celles qui ont une valeur de 0.
## Prérequis
- Les fichiers CSV doivent être présents dans le répertoire `/counting_osm_objects/test_results/`
- Le format du nom de fichier doit être `commune_{code_insee}_{theme}.csv`
- La ville (entité Stats) doit exister en base de données avec le code INSEE correspondant
## Format du fichier CSV
Le fichier CSV doit contenir les colonnes suivantes :
- `date` : Date de la mesure (format YYYY-MM-DD)
- `zone` : Zone de la mesure (format commune_{code_insee})
- `theme` : Thématique de la mesure (ex: borne-de-recharge)
- `nombre_total` : Nombre total d'objets
- `nombre_avec_operator` : Nombre d'objets avec un opérateur (optionnel)
- `nombre_avec_capacity` : Nombre d'objets avec une capacité (optionnel)
- `pourcentage_completion` : Pourcentage de complétion (optionnel)
## Utilisation
```bash
php bin/console app:import-cityfollowup-csv <insee> <theme> [--dry-run]
```
### Arguments
- `insee` : Code INSEE de la ville (obligatoire)
- `theme` : Thématique à importer (ex: borne-de-recharge, defibrillator) (obligatoire)
### Options
- `--dry-run` : Simule l'import sans modifier la base de données
## Exemples
### Import des bornes de recharge pour Paris
```bash
php bin/console app:import-cityfollowup-csv 75056 borne-de-recharge
```
### Simulation d'import des défibrillateurs pour Paris
```bash
php bin/console app:import-cityfollowup-csv 75056 defibrillator --dry-run
```
## Comportement
1. La commande vérifie d'abord que la ville existe en base de données avec le code INSEE fourni.
2. Elle recherche ensuite le fichier CSV correspondant dans le répertoire des résultats.
3. Elle lit le contenu du fichier CSV et récupère les mesures existantes en base de données.
4. Pour chaque ligne du CSV :
- Les lignes avec des valeurs manquantes sont ignorées
- Les mesures avec une valeur de 0 sont ignorées
- Les mesures déjà existantes en base de données sont ignorées
- Les nouvelles mesures sont ajoutées en base de données
5. La commande affiche un résumé des opérations effectuées.
## Correspondance des thématiques
La commande effectue une conversion entre les noms de thématiques utilisés dans les fichiers CSV et ceux utilisés dans la base de données :
| Nom dans le CSV | Nom dans CityFollowUp |
|-----------------|------------------------|
| borne-de-recharge | charging_station |
| defibrillator | defibrillator |
| ... | ... |
## Notes
- Les mesures sont importées par lots de 50 pour éviter de surcharger la mémoire.
- Seules les mesures qui n'existent pas déjà en base de données sont importées.
- Les mesures avec une valeur de 0 sont ignorées conformément aux spécifications.

View file

@ -0,0 +1,235 @@
<?php
namespace App\Command;
use App\Entity\CityFollowUp;
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;
use Symfony\Component\Finder\Finder;
#[AsCommand(
name: 'app:import-cityfollowup-csv',
description: 'Importe les mesures thématiques depuis un fichier CSV pour une ville donnée'
)]
class ImportCityFollowupFromCsvCommand extends Command
{
private const CSV_DIR = '/home/poule/encrypted/stockage-syncable/www/development/html/osm-commerce-sf/counting_osm_objects/test_results';
public function __construct(
private EntityManagerInterface $entityManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('insee', InputArgument::REQUIRED, 'Code INSEE de la ville')
->addArgument('theme', InputArgument::REQUIRED, 'Thématique à importer (ex: borne-de-recharge, defibrillator)')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simuler l\'import sans modifier la base de données');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$insee = $input->getArgument('insee');
$theme = $input->getArgument('theme');
$dryRun = $input->getOption('dry-run');
// Vérifier que la ville existe
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee]);
if (!$stats) {
$io->error("Aucune ville trouvée avec le code INSEE $insee.");
return Command::FAILURE;
}
$io->title("Import des mesures thématiques pour {$stats->getName()} (INSEE: $insee) - Thème: $theme");
// Trouver le fichier CSV correspondant
$csvPath = $this->findCsvFile($insee, $theme);
if (!$csvPath) {
$io->error("Aucun fichier CSV trouvé pour la ville $insee et la thématique $theme.");
return Command::FAILURE;
}
$io->info("Fichier CSV trouvé: $csvPath");
// Lire le fichier CSV
$csvData = $this->readCsvFile($csvPath);
if (empty($csvData)) {
$io->error("Le fichier CSV est vide ou mal formaté.");
return Command::FAILURE;
}
$io->info(sprintf("Nombre de lignes dans le CSV: %d", count($csvData)));
// Récupérer les mesures existantes pour cette ville et cette thématique
$existingMeasures = $this->getExistingMeasures($stats, $theme);
$io->info(sprintf("Nombre de mesures existantes en base: %d", count($existingMeasures)));
// Comparer et importer les nouvelles mesures
$imported = 0;
$skipped = 0;
$zeroSkipped = 0;
foreach ($csvData as $row) {
// Ignorer les lignes avec des valeurs manquantes
if (empty($row['date']) || empty($row['nombre_total']) || $row['nombre_total'] === '') {
$skipped++;
continue;
}
// Convertir la date
try {
$date = new \DateTime($row['date']);
} catch (\Exception $e) {
$io->warning("Date invalide: {$row['date']}");
$skipped++;
continue;
}
// Ignorer les mesures qui valent 0
if ((float)$row['nombre_total'] === 0.0) {
$zeroSkipped++;
continue;
}
// Vérifier si la mesure existe déjà
$dateStr = $date->format('Y-m-d');
if (isset($existingMeasures[$dateStr])) {
$skipped++;
continue;
}
// Créer une nouvelle mesure pour le nombre total
if (!$dryRun) {
$followUp = new CityFollowUp();
$followUp->setName($this->convertThemeToCityFollowUpName($theme) . '_count')
->setMeasure((float)$row['nombre_total'])
->setDate($date)
->setStats($stats);
$this->entityManager->persist($followUp);
// Créer une mesure pour la complétion si disponible
if (isset($row['pourcentage_completion']) && $row['pourcentage_completion'] !== '') {
$followUpCompletion = new CityFollowUp();
$followUpCompletion->setName($this->convertThemeToCityFollowUpName($theme) . '_completion')
->setMeasure((float)$row['pourcentage_completion'])
->setDate($date)
->setStats($stats);
$this->entityManager->persist($followUpCompletion);
}
$imported++;
// Flush toutes les 50 entités pour éviter de surcharger la mémoire
if ($imported % 50 === 0) {
$this->entityManager->flush();
$this->entityManager->clear(CityFollowUp::class);
}
} else {
$imported++;
}
}
// Flush final
if (!$dryRun && $imported > 0) {
$this->entityManager->flush();
}
if ($dryRun) {
$io->success("Simulation terminée: $imported mesures seraient importées, $skipped mesures ignorées (déjà existantes), $zeroSkipped mesures ignorées (valeur zéro).");
} else {
$io->success("Import terminé: $imported mesures importées, $skipped mesures ignorées (déjà existantes), $zeroSkipped mesures ignorées (valeur zéro).");
}
return Command::SUCCESS;
}
/**
* Trouve le fichier CSV correspondant à la ville et à la thématique
*/
private function findCsvFile(string $insee, string $theme): ?string
{
$finder = new Finder();
$finder->files()
->in(self::CSV_DIR)
->name("commune_{$insee}_{$theme}.csv");
if (!$finder->hasResults()) {
return null;
}
foreach ($finder as $file) {
return $file->getRealPath();
}
return null;
}
/**
* Lit le fichier CSV et retourne un tableau de données
*/
private function readCsvFile(string $path): array
{
$data = [];
if (($handle = fopen($path, "r")) !== false) {
// Lire l'en-tête
$header = fgetcsv($handle, 1000, ",");
if ($header === false) {
return [];
}
// Lire les données
while (($row = fgetcsv($handle, 1000, ",")) !== false) {
$rowData = [];
foreach ($header as $i => $key) {
$rowData[$key] = $row[$i] ?? '';
}
$data[] = $rowData;
}
fclose($handle);
}
return $data;
}
/**
* Récupère les mesures existantes pour cette ville et cette thématique
*/
private function getExistingMeasures(Stats $stats, string $theme): array
{
$themeName = $this->convertThemeToCityFollowUpName($theme);
$existingMeasures = [];
foreach ($stats->getCityFollowUps() as $followUp) {
if ($followUp->getName() === $themeName . '_count') {
$date = $followUp->getDate()->format('Y-m-d');
$existingMeasures[$date] = $followUp;
}
}
return $existingMeasures;
}
/**
* Convertit le nom de thématique du CSV en nom utilisé dans CityFollowUp
*/
private function convertThemeToCityFollowUpName(string $theme): string
{
$themeMapping = [
'borne-de-recharge' => 'charging_station',
'defibrillator' => 'defibrillator',
// Ajouter d'autres mappings si nécessaire
];
return $themeMapping[$theme] ?? $theme;
}
}