Phase 2 : dérive, ImpactScore, slider temporel
Backend : - OpenMeteoClient : vent horaire 72h (gratuit, sans clé) - DriftSimulationService : échantillonnage ST_GeneratePoints, déplacement itératif Stokes 3%, reconstruction ST_ConcaveHull, 4 horizons H+6/12/24/48, confiance décroissante - ImpactScoreService : score 4 composantes (distance/densité/ vitesse/tendance), cache Redis TTL 3h, invalidation à chaque calcul - ComputeForecastsCommand : traite les observations sans forecast - SpotScoreController : score courant + horizons depuis forecasts Frontend : - TimelineSlider : navigation Now/+6h/+12h/+24h/+48h - SargassesMap : couche observations (orange) vs forecasts (violet) rechargée à chaque changement d'étape - SpotPanel : affichage score par horizon actif + indicateur confiance - Build → backend/public/spa/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
backend/src/Command/ComputeForecastsCommand.php
Normal file
89
backend/src/Command/ComputeForecastsCommand.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\SargassumObservation;
|
||||
use App\Service\Forecast\DriftSimulationService;
|
||||
use App\Service\Forecast\ImpactScoreService;
|
||||
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:compute-forecasts',
|
||||
description: 'Calcule les forecasts de dérive et les ImpactScores pour les dernières observations',
|
||||
)]
|
||||
class ComputeForecastsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private DriftSimulationService $drift,
|
||||
private ImpactScoreService $scores,
|
||||
private EntityManagerInterface $em,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('limit', 'l', InputOption::VALUE_OPTIONAL,
|
||||
'Nombre max d\'observations à traiter', 5);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$limit = (int) $input->getOption('limit');
|
||||
|
||||
$io->title('Calcul forecasts + ImpactScores');
|
||||
|
||||
// Observations récentes sans forecast
|
||||
$observations = $this->em->getRepository(SargassumObservation::class)
|
||||
->createQueryBuilder('o')
|
||||
->leftJoin('o.forecasts', 'f')
|
||||
->where('f.id IS NULL')
|
||||
->orderBy('o.detectedAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($observations)) {
|
||||
$io->info('Aucune observation sans forecast trouvée.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->progressStart(count($observations));
|
||||
$errors = 0;
|
||||
|
||||
foreach ($observations as $observation) {
|
||||
try {
|
||||
$forecasts = $this->drift->computeForecasts($observation);
|
||||
$io->text(sprintf(
|
||||
'Observation %s → %d forecast(s)',
|
||||
$observation->getId(),
|
||||
count($forecasts)
|
||||
));
|
||||
|
||||
$this->scores->computeForObservation($observation, $forecasts);
|
||||
} catch (\Throwable $e) {
|
||||
$io->error('Erreur sur ' . $observation->getId() . ' : ' . $e->getMessage());
|
||||
$errors++;
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
'%d observation(s) traitée(s), %d erreur(s).',
|
||||
count($observations) - $errors,
|
||||
$errors
|
||||
));
|
||||
|
||||
return $errors === 0 ? Command::SUCCESS : Command::FAILURE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user