diff --git a/backend/src/Command/ComputeForecastsCommand.php b/backend/src/Command/ComputeForecastsCommand.php index 7c37f6a..e5e23ed 100644 --- a/backend/src/Command/ComputeForecastsCommand.php +++ b/backend/src/Command/ComputeForecastsCommand.php @@ -70,8 +70,9 @@ class ComputeForecastsCommand extends Command ['ids' => $ids], ['ids' => \Doctrine\DBAL\ArrayParameterType::STRING] ); - // ImpactScore n'a pas de FK vers l'observation — on purge tout et on recalcule - $this->em->getConnection()->executeStatement('DELETE FROM impact_score'); + // Note : on ne supprime PAS les impact_scores existants. + // computeForObservation créera de nouveaux scores ; les anciens restent + // pour les spots où cette observation est trop loin (MAX_RELEVANT_M). $this->em->clear(); // Recharge les observations détachées diff --git a/backend/src/Service/Forecast/DriftSimulationService.php b/backend/src/Service/Forecast/DriftSimulationService.php index 345e129..fce2a17 100644 --- a/backend/src/Service/Forecast/DriftSimulationService.php +++ b/backend/src/Service/Forecast/DriftSimulationService.php @@ -27,7 +27,6 @@ class DriftSimulationService { private const HORIZONS = [6, 12, 24, 48]; private const WIND_FACTOR = 0.03; // 3% vitesse vent = dérive de Stokes sargassum - private const DIFFUSION_M = 1500; // m — coefficient de diffusion (rayon du buffer à 1h) private const MODEL_VERSION = '2.0.0'; public function __construct( @@ -73,10 +72,7 @@ class DriftSimulationService $dxDeg = $totalDxM / (111320.0 * cos(deg2rad($centroid['lat']))); $dyDeg = $totalDyM / 111320.0; - // Buffer de diffusion : croît comme √horizon (diffusion Brownienne) - $bufferM = self::DIFFUSION_M * sqrt($horizon); - - $geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg, $bufferM); + $geometry = $this->buildForecastGeometry($id, $dxDeg, $dyDeg); if ($geometry === null) { $this->logger->info("No polygon for H+{$horizon}", ['id' => $id]); $prevHorizon = $horizon; @@ -122,29 +118,28 @@ class DriftSimulationService } /** - * Traduit la géométrie source de $dxDeg/$dyDeg degrés, puis dilate de $bufferM mètres. + * Traduit la géométrie source de $dxDeg/$dyDeg degrés via ST_Translate. * - * ST_Translate conserve la structure MULTIPOLYGON : chaque patch individuel - * se déplace de façon identique → ST_Distance retourne la vraie distance - * au patch le plus proche, pas au centroïde global. + * Pas de ST_Buffer ici : buffer sur geography produit des géométries volumineuses + * et invalides sur de grands MULTIPOLYGON, corrompant les calculs ST_Distance. + * ST_Translate seul conserve la structure et la taille de chaque patch individuel. */ private function buildForecastGeometry( string $observationId, float $dxDeg, float $dyDeg, - float $bufferM, ): ?string { $row = $this->connection->fetchAssociative( 'SELECT ST_AsText( ST_Multi( - ST_Buffer( - ST_Translate(geometry, :dx, :dy)::geography, - :buffer - )::geometry + ST_SimplifyPreserveTopology( + ST_Translate(geometry, :dx, :dy), + 0.001 + ) ) ) AS wkt FROM sargassum_observation WHERE id = :id', - ['id' => $observationId, 'dx' => $dxDeg, 'dy' => $dyDeg, 'buffer' => $bufferM] + ['id' => $observationId, 'dx' => $dxDeg, 'dy' => $dyDeg] ); return ($row !== false && isset($row['wkt']) && $row['wkt'] !== null) diff --git a/backend/src/Service/Forecast/ImpactScoreService.php b/backend/src/Service/Forecast/ImpactScoreService.php index c23de81..783d104 100644 --- a/backend/src/Service/Forecast/ImpactScoreService.php +++ b/backend/src/Service/Forecast/ImpactScoreService.php @@ -27,7 +27,8 @@ use Symfony\Contracts\Cache\ItemInterface; */ class ImpactScoreService { - private const MAX_DISTANCE_M = 50_000; // 50 km : au-delà score distance = 0 + private const MAX_DISTANCE_M = 50_000; // 50 km : au-delà score distance = 0 + private const MAX_RELEVANT_M = 300_000; // 300 km : au-delà, on ne touche pas au score existant private const MAX_DENSITY_KM2 = 100; // 100 km² = densité maximale private const MAX_APPROACH_KMH = 5; // 5 km/h = vitesse d'approche max private const CACHE_TTL = 10_800; // 3h @@ -68,6 +69,13 @@ class ImpactScoreService $spotId = (string) $spot->getId(); try { + // Ne pas écraser le score d'un spot si cette observation est trop loin. + // Cela évite qu'une ingestion distante remplace un bon score existant. + $distanceCheck = $this->getDistanceToNearestSargassum($spotId, $obsId); + if ($distanceCheck === null || $distanceCheck > self::MAX_RELEVANT_M) { + continue; + } + $score = $this->computeScore($spot, $obsId, $forecasts); $this->persistScore($spot, $score); $this->invalidateCache($spotId);