add trend UX + PHPStan level 6 clean (0 errors)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-03 16:06:13 -04:00
parent 997e2d186c
commit 0d323b871b
26 changed files with 471 additions and 78 deletions

View File

@@ -63,6 +63,8 @@ class SpotScoreController extends AbstractController
/**
* Score à un instant précis (sans cache).
*
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null
*/
private function loadScoreAt(string $spotId, \DateTimeImmutable $at): ?array
{
@@ -81,6 +83,8 @@ class SpotScoreController extends AbstractController
/**
* Scores estimés par horizon à partir des forecasts les plus récents.
* Retourne null si aucun forecast disponible.
*
* @return array<string, array{score: int, level: string, distanceKm: float, confidence: float, validAt: string}>|null
*/
private function loadHorizonScores(string $spotId): ?array
{
@@ -125,6 +129,10 @@ class SpotScoreController extends AbstractController
return $horizons;
}
/**
* @param array<string, mixed> $row
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}
*/
private function formatScore(array $row): array
{
return [

View File

@@ -36,33 +36,38 @@ class CoastalPoint
#[ORM\Column(length: 150)]
#[Groups(['spot:read'])]
private ?string $name = null;
private string $name;
#[ORM\Column(type: 'geometry', options: ['geometry_type' => 'POINT', 'srid' => 4326])]
private mixed $geometry = null;
#[ORM\Column(length: 30)]
#[Groups(['spot:read'])]
private ?string $type = null;
private string $type;
#[ORM\Column(length: 100)]
#[Groups(['spot:read'])]
private ?string $region = null;
private string $region;
/** @var Collection<int, ImpactScore> */
#[ORM\OneToMany(targetEntity: ImpactScore::class, mappedBy: 'coastalPoint')]
private Collection $impactScores;
/** @var Collection<int, UserFeedback> */
#[ORM\OneToMany(targetEntity: UserFeedback::class, mappedBy: 'coastalPoint')]
private Collection $feedbacks;
public function __construct()
{
$this->name = '';
$this->type = '';
$this->region = '';
$this->impactScores = new ArrayCollection();
$this->feedbacks = new ArrayCollection();
}
public function getId(): ?Uuid { return $this->id; }
public function getName(): ?string { return $this->name; }
public function getName(): string { return $this->name; }
public function setName(string $name): static { $this->name = $name; return $this; }
public function getGeometry(): mixed { return $this->geometry; }
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
@@ -82,9 +87,9 @@ class CoastalPoint
preg_match('/POINT\([^ ]+ ([^)]+)\)/', (string) $this->geometry, $m);
return isset($m[1]) ? (float) $m[1] : null;
}
public function getType(): ?string { return $this->type; }
public function getType(): string { return $this->type; }
public function setType(string $type): static { $this->type = $type; return $this; }
public function getRegion(): ?string { return $this->region; }
public function getRegion(): string { return $this->region; }
public function setRegion(string $region): static { $this->region = $region; return $this; }
/** @return Collection<int, ImpactScore> */

View File

@@ -19,13 +19,13 @@ class DataIngestionJob
private ?Uuid $id = null;
#[ORM\Column(length: 50)]
private ?string $source = null;
private string $source;
#[ORM\Column(length: 100)]
private ?string $tileId = null;
private string $tileId;
#[ORM\Column]
private ?\DateTimeImmutable $requestedAt = null;
private \DateTimeImmutable $requestedAt;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $processedAt = null;
@@ -41,15 +41,17 @@ class DataIngestionJob
public function __construct()
{
$this->source = '';
$this->tileId = '';
$this->requestedAt = new \DateTimeImmutable();
}
public function getId(): ?Uuid { return $this->id; }
public function getSource(): ?string { return $this->source; }
public function getSource(): string { return $this->source; }
public function setSource(string $source): static { $this->source = $source; return $this; }
public function getTileId(): ?string { return $this->tileId; }
public function getTileId(): string { return $this->tileId; }
public function setTileId(string $tileId): static { $this->tileId = $tileId; return $this; }
public function getRequestedAt(): ?\DateTimeImmutable { return $this->requestedAt; }
public function getRequestedAt(): \DateTimeImmutable { return $this->requestedAt; }
public function getProcessedAt(): ?\DateTimeImmutable { return $this->processedAt; }
public function setProcessedAt(?\DateTimeImmutable $processedAt): static { $this->processedAt = $processedAt; return $this; }
public function getStatus(): string { return $this->status; }

View File

@@ -29,16 +29,16 @@ class ImpactScore
#[ORM\ManyToOne(inversedBy: 'impactScores')]
#[ORM\JoinColumn(nullable: false)]
private ?CoastalPoint $coastalPoint = null;
private CoastalPoint $coastalPoint;
#[ORM\Column]
private ?\DateTimeImmutable $timestamp = null;
private \DateTimeImmutable $timestamp;
#[ORM\Column]
private ?int $score = null;
private int $score;
#[ORM\Column(length: 20)]
private ?string $level = null;
private string $level;
#[ORM\Column(nullable: true)]
private ?float $distanceToNearestSargassum = null;
@@ -52,16 +52,18 @@ class ImpactScore
public function __construct()
{
$this->timestamp = new \DateTimeImmutable();
$this->score = 0;
$this->level = 'low';
}
public function getId(): ?Uuid { return $this->id; }
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
public function getTimestamp(): ?\DateTimeImmutable { return $this->timestamp; }
public function getCoastalPoint(): CoastalPoint { return $this->coastalPoint; }
public function setCoastalPoint(CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; }
public function setTimestamp(\DateTimeImmutable $timestamp): static { $this->timestamp = $timestamp; return $this; }
public function getScore(): ?int { return $this->score; }
public function getScore(): int { return $this->score; }
public function setScore(int $score): static { $this->score = $score; return $this; }
public function getLevel(): ?string { return $this->level; }
public function getLevel(): string { return $this->level; }
public function setLevel(string $level): static { $this->level = $level; return $this; }
public function getDistanceToNearestSargassum(): ?float { return $this->distanceToNearestSargassum; }
public function setDistanceToNearestSargassum(?float $d): static { $this->distanceToNearestSargassum = $d; return $this; }

View File

@@ -18,34 +18,37 @@ class PushSubscription
private ?Uuid $id = null;
#[ORM\Column(type: 'text', unique: true)]
private ?string $endpoint = null;
private string $endpoint;
#[ORM\Column(type: 'text')]
private ?string $authToken = null;
private string $authToken;
#[ORM\Column(type: 'text')]
private ?string $p256dhKey = null;
private string $p256dhKey;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?CoastalPoint $coastalPoint = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->endpoint = '';
$this->authToken = '';
$this->p256dhKey = '';
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?Uuid { return $this->id; }
public function getEndpoint(): ?string { return $this->endpoint; }
public function getEndpoint(): string { return $this->endpoint; }
public function setEndpoint(string $endpoint): static { $this->endpoint = $endpoint; return $this; }
public function getAuthToken(): ?string { return $this->authToken; }
public function getAuthToken(): string { return $this->authToken; }
public function setAuthToken(string $authToken): static { $this->authToken = $authToken; return $this; }
public function getP256dhKey(): ?string { return $this->p256dhKey; }
public function getP256dhKey(): string { return $this->p256dhKey; }
public function setP256dhKey(string $p256dhKey): static { $this->p256dhKey = $p256dhKey; return $this; }
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
}

View File

@@ -30,54 +30,58 @@ class SargassumForecast
#[ORM\ManyToOne(inversedBy: 'forecasts')]
#[ORM\JoinColumn(nullable: false)]
private ?SargassumObservation $sourceObservation = null;
private SargassumObservation $sourceObservation;
#[ORM\Column(type: 'geometry', options: ['geometry_type' => 'MULTIPOLYGON', 'srid' => 4326])]
private mixed $geometry = null;
#[ORM\Column]
private ?\DateTimeImmutable $validAt = null;
private \DateTimeImmutable $validAt;
#[ORM\Column]
private ?\DateTimeImmutable $computedAt = null;
private \DateTimeImmutable $computedAt;
#[ORM\Column(length: 50)]
private ?string $modelVersion = null;
private string $modelVersion;
#[ORM\Column]
private ?int $timeHorizon = null;
private int $timeHorizon;
#[ORM\Column]
private ?float $confidence = null;
private float $confidence;
#[ORM\Column(type: 'geometry', nullable: true, options: ['geometry_type' => 'POINT', 'srid' => 4326])]
private mixed $driftVectorAvg = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
$this->computedAt = new \DateTimeImmutable();
$this->validAt = new \DateTimeImmutable();
$this->computedAt = new \DateTimeImmutable();
$this->createdAt = new \DateTimeImmutable();
$this->modelVersion = '';
$this->timeHorizon = 0;
$this->confidence = 0.0;
}
public function getId(): ?Uuid { return $this->id; }
public function getSourceObservation(): ?SargassumObservation { return $this->sourceObservation; }
public function setSourceObservation(?SargassumObservation $sourceObservation): static { $this->sourceObservation = $sourceObservation; return $this; }
public function getSourceObservation(): SargassumObservation { return $this->sourceObservation; }
public function setSourceObservation(SargassumObservation $sourceObservation): static { $this->sourceObservation = $sourceObservation; return $this; }
public function getGeometry(): mixed { return $this->geometry; }
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
public function getValidAt(): ?\DateTimeImmutable { return $this->validAt; }
public function getValidAt(): \DateTimeImmutable { return $this->validAt; }
public function setValidAt(\DateTimeImmutable $validAt): static { $this->validAt = $validAt; return $this; }
public function getComputedAt(): ?\DateTimeImmutable { return $this->computedAt; }
public function getComputedAt(): \DateTimeImmutable { return $this->computedAt; }
public function setComputedAt(\DateTimeImmutable $computedAt): static { $this->computedAt = $computedAt; return $this; }
public function getModelVersion(): ?string { return $this->modelVersion; }
public function getModelVersion(): string { return $this->modelVersion; }
public function setModelVersion(string $modelVersion): static { $this->modelVersion = $modelVersion; return $this; }
public function getTimeHorizon(): ?int { return $this->timeHorizon; }
public function getTimeHorizon(): int { return $this->timeHorizon; }
public function setTimeHorizon(int $timeHorizon): static { $this->timeHorizon = $timeHorizon; return $this; }
public function getConfidence(): ?float { return $this->confidence; }
public function getConfidence(): float { return $this->confidence; }
public function setConfidence(float $confidence): static { $this->confidence = $confidence; return $this; }
public function getDriftVectorAvg(): mixed { return $this->driftVectorAvg; }
public function setDriftVectorAvg(mixed $driftVectorAvg): static { $this->driftVectorAvg = $driftVectorAvg; return $this; }
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
}

View File

@@ -6,7 +6,6 @@ use App\Repository\SargassumObservationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Jsor\Doctrine\PostGIS\Types\PostGISType;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
@@ -28,25 +27,25 @@ class SargassumObservation
private mixed $bbox = null;
#[ORM\Column]
private ?\DateTimeImmutable $detectedAt = null;
private \DateTimeImmutable $detectedAt;
#[ORM\Column(length: 50)]
private ?string $source = null;
private string $source;
#[ORM\Column(length: 100)]
private ?string $tileId = null;
private string $tileId;
#[ORM\Column]
private ?float $cloudCoverage = null;
private float $cloudCoverage;
#[ORM\Column]
private ?float $confidence = null;
private float $confidence;
#[ORM\Column]
private ?float $afaiMean = null;
private float $afaiMean;
#[ORM\Column]
private ?float $afaiStd = null;
private float $afaiStd;
#[ORM\Column(nullable: true)]
private ?float $coverageArea = null;
@@ -55,15 +54,23 @@ class SargassumObservation
private ?string $processingVersion = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
private \DateTimeImmutable $createdAt;
/** @var Collection<int, SargassumForecast> */
#[ORM\OneToMany(targetEntity: SargassumForecast::class, mappedBy: 'sourceObservation')]
private Collection $forecasts;
public function __construct()
{
$this->forecasts = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->detectedAt = new \DateTimeImmutable();
$this->source = '';
$this->tileId = '';
$this->cloudCoverage = 0.0;
$this->confidence = 0.0;
$this->afaiMean = 0.0;
$this->afaiStd = 0.0;
$this->createdAt = new \DateTimeImmutable();
$this->forecasts = new ArrayCollection();
}
public function getId(): ?Uuid { return $this->id; }
@@ -71,25 +78,25 @@ class SargassumObservation
public function setGeometry(mixed $geometry): static { $this->geometry = $geometry; return $this; }
public function getBbox(): mixed { return $this->bbox; }
public function setBbox(mixed $bbox): static { $this->bbox = $bbox; return $this; }
public function getDetectedAt(): ?\DateTimeImmutable { return $this->detectedAt; }
public function getDetectedAt(): \DateTimeImmutable { return $this->detectedAt; }
public function setDetectedAt(\DateTimeImmutable $detectedAt): static { $this->detectedAt = $detectedAt; return $this; }
public function getSource(): ?string { return $this->source; }
public function getSource(): string { return $this->source; }
public function setSource(string $source): static { $this->source = $source; return $this; }
public function getTileId(): ?string { return $this->tileId; }
public function getTileId(): string { return $this->tileId; }
public function setTileId(string $tileId): static { $this->tileId = $tileId; return $this; }
public function getCloudCoverage(): ?float { return $this->cloudCoverage; }
public function getCloudCoverage(): float { return $this->cloudCoverage; }
public function setCloudCoverage(float $cloudCoverage): static { $this->cloudCoverage = $cloudCoverage; return $this; }
public function getConfidence(): ?float { return $this->confidence; }
public function getConfidence(): float { return $this->confidence; }
public function setConfidence(float $confidence): static { $this->confidence = $confidence; return $this; }
public function getAfaiMean(): ?float { return $this->afaiMean; }
public function getAfaiMean(): float { return $this->afaiMean; }
public function setAfaiMean(float $afaiMean): static { $this->afaiMean = $afaiMean; return $this; }
public function getAfaiStd(): ?float { return $this->afaiStd; }
public function getAfaiStd(): float { return $this->afaiStd; }
public function setAfaiStd(float $afaiStd): static { $this->afaiStd = $afaiStd; return $this; }
public function getCoverageArea(): ?float { return $this->coverageArea; }
public function setCoverageArea(?float $coverageArea): static { $this->coverageArea = $coverageArea; return $this; }
public function getProcessingVersion(): ?string { return $this->processingVersion; }
public function setProcessingVersion(?string $processingVersion): static { $this->processingVersion = $processingVersion; return $this; }
public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
/** @return Collection<int, SargassumForecast> */
public function getForecasts(): Collection { return $this->forecasts; }

View File

@@ -25,10 +25,10 @@ class UserFeedback
private ?CoastalPoint $coastalPoint = null;
#[ORM\Column]
private ?\DateTimeImmutable $timestamp = null;
private \DateTimeImmutable $timestamp;
#[ORM\Column]
private ?bool $hasSeaweed = null;
private bool $hasSeaweed;
#[ORM\Column(length: 20, nullable: true)]
private ?string $density = null;
@@ -38,7 +38,8 @@ class UserFeedback
public function __construct()
{
$this->timestamp = new \DateTimeImmutable();
$this->timestamp = new \DateTimeImmutable();
$this->hasSeaweed = false;
}
public function getId(): ?Uuid { return $this->id; }
@@ -46,9 +47,9 @@ class UserFeedback
public function setLocation(mixed $location): static { $this->location = $location; return $this; }
public function getCoastalPoint(): ?CoastalPoint { return $this->coastalPoint; }
public function setCoastalPoint(?CoastalPoint $coastalPoint): static { $this->coastalPoint = $coastalPoint; return $this; }
public function getTimestamp(): ?\DateTimeImmutable { return $this->timestamp; }
public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; }
public function setTimestamp(\DateTimeImmutable $timestamp): static { $this->timestamp = $timestamp; return $this; }
public function getHasSeaweed(): ?bool { return $this->hasSeaweed; }
public function getHasSeaweed(): bool { return $this->hasSeaweed; }
public function setHasSeaweed(bool $hasSeaweed): static { $this->hasSeaweed = $hasSeaweed; return $this; }
public function getDensity(): ?string { return $this->density; }
public function setDensity(?string $density): static { $this->density = $density; return $this; }

View File

@@ -6,6 +6,7 @@ use App\Entity\CoastalPoint;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<CoastalPoint> */
class CoastalPointRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\DataIngestionJob;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<DataIngestionJob> */
class DataIngestionJobRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\ImpactScore;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<ImpactScore> */
class ImpactScoreRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\PushSubscription;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<PushSubscription> */
class PushSubscriptionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\SargassumForecast;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<SargassumForecast> */
class SargassumForecastRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\SargassumObservation;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<SargassumObservation> */
class SargassumObservationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -6,6 +6,7 @@ use App\Entity\UserFeedback;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/** @extends ServiceEntityRepository<UserFeedback> */
class UserFeedbackRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)

View File

@@ -102,6 +102,7 @@ class DriftSimulationService
// -------------------------------------------------------------------------
/** @return array{lat: float, lng: float}|null */
private function getCentroid(string $observationId): ?array
{
$row = $this->connection->fetchAssociative(
@@ -167,6 +168,7 @@ class DriftSimulationService
/**
* Reconstruit un MULTIPOLYGON depuis un nuage de points via PostGIS.
*/
/** @param array<int, array{lat: float, lng: float}> $points */
private function reconstructPolygon(array $points): ?string
{
if (count($points) < 3) {
@@ -196,6 +198,10 @@ class DriftSimulationService
/**
* Calcule le vecteur de dérive moyen (composantes lat/lng) sur une période.
*/
/**
* @param array<int, array{speed: float, direction: float}> $windData
* @return array{lat: float, lng: float}
*/
private function avgDriftVector(array $windData, int $fromHour, int $toHour): array
{
$count = $toHour - $fromHour;

View File

@@ -49,6 +49,8 @@ class ImpactScoreService
/**
* Calcule et sauvegarde les scores pour tous les CoastalPoints
* à partir d'une observation et de ses forecasts.
*
* @param array<int, SargassumForecast> $forecasts
*/
public function computeForObservation(SargassumObservation $observation, array $forecasts): void
{
@@ -87,6 +89,8 @@ class ImpactScoreService
/**
* Retourne le score mis en cache pour un spot (TTL 3h).
*
* @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null
*/
public function getCachedScore(string $spotId): ?array
{
@@ -100,6 +104,7 @@ class ImpactScoreService
// -------------------------------------------------------------------------
/** @return array{score: int, level: string, distance: float|null, density: float, trend: string} */
private function computeScore(CoastalPoint $spot, string $obsId, ?SargassumForecast $forecastH6): array
{
$spotId = (string) $spot->getId();
@@ -194,13 +199,21 @@ class ImpactScoreService
private function getPreviousDistance(string $spotId): ?float
{
// On cherche le score de la précédente "session" d'ingestion :
// au moins 6h d'ancienneté pour éviter de comparer avec un recalcul immédiat,
// et au plus 14 jours pour rester pertinent.
$since = (new \DateTimeImmutable())->modify('-14 days')->format('Y-m-d H:i:s');
$before = (new \DateTimeImmutable())->modify('-6 hours')->format('Y-m-d H:i:s');
$row = $this->connection->fetchAssociative(
'SELECT distance_to_nearest_sargassum
FROM impact_score
WHERE coastal_point_id = :spotId
AND timestamp <= :before
AND timestamp >= :since
ORDER BY timestamp DESC
LIMIT 1',
['spotId' => $spotId]
['spotId' => $spotId, 'before' => $before, 'since' => $since]
);
return ($row !== false && $row['distance_to_nearest_sargassum'] !== null)
@@ -208,6 +221,7 @@ class ImpactScoreService
: null;
}
/** @param array{score: int, level: string, distance: float|null, density: float, trend: string} $data */
private function persistScore(CoastalPoint $spot, array $data): void
{
$s = new ImpactScore();
@@ -221,6 +235,7 @@ class ImpactScoreService
$this->em->persist($s);
}
/** @return array{value: int, level: string, distance: float|null, density: float|null, trend: string|null, computedAt: string}|null */
private function loadLatestScore(string $spotId): ?array
{
$row = $this->connection->fetchAssociative(
@@ -265,6 +280,7 @@ class ImpactScoreService
};
}
/** @return array{score: int, level: string, distance: null, density: float, trend: string} */
private function emptyScore(): array
{
return ['score' => 0, 'level' => 'low', 'distance' => null, 'density' => 0.0, 'trend' => 'stable'];
@@ -291,10 +307,11 @@ class ImpactScoreService
return ($row !== false && (int) $row['positive_count'] > 0) ? (float) self::FEEDBACK_BONUS : 0.0;
}
/** @param array<int, SargassumForecast> $forecasts */
private function findForecastByHorizon(array $forecasts, int $horizon): ?SargassumForecast
{
foreach ($forecasts as $f) {
if ($f instanceof SargassumForecast && $f->getTimeHorizon() === $horizon) {
if ($f->getTimeHorizon() === $horizon) {
return $f;
}
}

View File

@@ -31,6 +31,7 @@ class IngestionService
* Tente l'ingestion Sentinel-2, puis Sentinel-3 OLCI en fallback si la
* couverture nuageuse est trop élevée ou si la scène S2 est indisponible.
*/
/** @param array<int, float> $bbox */
public function ingestWithFallback(array $bbox, \DateTimeImmutable $date): void
{
try {
@@ -41,6 +42,7 @@ class IngestionService
}
}
/** @param array<int, float> $bbox */
public function ingest(
array $bbox,
\DateTimeImmutable $date,
@@ -177,6 +179,9 @@ class IngestionService
}
}
/**
* @return array{type: string, coordinates: array<int, mixed>}|null
*/
private function buildMultiPolygon(string $geojsonPath): ?array
{
if (!file_exists($geojsonPath)) {

View File

@@ -48,6 +48,7 @@ class SentinelHubClient
* Retourne la couverture nuageuse (%) de la meilleure scène disponible.
* collection : 'sentinel-2-l2a' (défaut) ou 'sentinel-3-olci'
*/
/** @param array<int, float> $bbox */
public function getCloudCoverage(array $bbox, \DateTimeImmutable $date, string $collection = 'sentinel-2-l2a'): float
{
// Sentinel-3 OLCI n'expose pas eo:cloud_cover dans le Catalog ; on renvoie 0
@@ -83,6 +84,10 @@ class SentinelHubClient
* calculé via evalscript AFAI/FAI, et le sauvegarde dans $outputPath.
* collection : 'sentinel-2-l2a' (défaut) ou 'sentinel-3-olci'
*/
/**
* @param array<int, float> $bbox
* @return array{tileId: string, afaiMean: float, afaiStd: float}
*/
public function downloadAfaiBinary(
array $bbox,
\DateTimeImmutable $date,