Phase 1 : pipeline d'ingestion Sentinel-2

- SentinelHubClient : auth OAuth2, Catalog API (cloud coverage),
  Process API avec evalscript AFAI binaire (UINT8, cloud-side)
- IngestionService : orchestration complète — job tracking,
  rejet nuages, téléchargement GeoTIFF, polygonize GDAL,
  simplification PostGIS, persistance SargassumObservation
- IngestSentinelCommand : 5 zones Antilles configurées,
  options --date et --zone, appelable via cron docker exec
- Dockerfile : ajout gdal-bin + python3-gdal
- Correction entity : paramètre immutable retiré de #[ORM\Column]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-01 02:45:12 -04:00
parent e5e729c487
commit bf64340525
10 changed files with 696 additions and 12 deletions

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Command;
use App\Service\Ingestion\IngestionService;
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:ingest-sentinel',
description: 'Ingestion des données Sentinel-2 pour la détection de sargasses',
)]
class IngestSentinelCommand extends Command
{
// Zones d'intérêt Antilles : [minLon, minLat, maxLon, maxLat]
private const ZONES = [
'martinique' => [-61.30, 14.30, -60.70, 14.90],
'guadeloupe' => [-61.90, 15.80, -61.00, 16.60],
'sainte-lucie' => [-61.10, 13.70, -60.80, 14.20],
'barbade' => [-59.80, 13.00, -59.30, 13.40],
'saint-martin' => [-63.20, 17.80, -62.90, 18.20],
];
public function __construct(private IngestionService $ingestionService)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('date', 'd', InputOption::VALUE_OPTIONAL,
'Date à traiter (YYYY-MM-DD). Défaut : hier.', null)
->addOption('zone', 'z', InputOption::VALUE_OPTIONAL,
'Zone spécifique. Défaut : toutes les zones.', null)
->addOption('source', 's', InputOption::VALUE_OPTIONAL,
'Source satellite (Sentinel-2 ou Sentinel-3)', 'Sentinel-2');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dateStr = $input->getOption('date');
$date = $dateStr
? new \DateTimeImmutable($dateStr)
: new \DateTimeImmutable('yesterday');
$source = $input->getOption('source');
$zones = self::ZONES;
if ($zoneName = $input->getOption('zone')) {
if (!isset(self::ZONES[$zoneName])) {
$io->error(sprintf(
"Zone inconnue : \"%s\". Zones disponibles : %s",
$zoneName,
implode(', ', array_keys(self::ZONES))
));
return Command::FAILURE;
}
$zones = [$zoneName => self::ZONES[$zoneName]];
}
$io->title(sprintf('Ingestion %s — %s', $source, $date->format('Y-m-d')));
$errors = 0;
foreach ($zones as $name => $bbox) {
$io->section("Zone : {$name}");
try {
$this->ingestionService->ingest($bbox, $date, $source);
$io->success("OK — {$name}");
} catch (\Throwable $e) {
$io->error("ÉCHEC — {$name} : " . $e->getMessage());
$errors++;
}
}
if ($errors > 0) {
$io->warning("{$errors} zone(s) en erreur.");
}
return $errors === 0 ? Command::SUCCESS : Command::FAILURE;
}
}