diff --git a/backend/src/Command/ImportCoastalPointsCommand.php b/backend/src/Command/ImportCoastalPointsCommand.php index 68e0d4f..f403eba 100644 --- a/backend/src/Command/ImportCoastalPointsCommand.php +++ b/backend/src/Command/ImportCoastalPointsCommand.php @@ -14,27 +14,30 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsCommand( name: 'app:import-coastal-points', - description: 'Importe les plages/ports/spots depuis OpenStreetMap (Overpass API) — idempotent', + description: 'Importe les plages/ports/spots depuis OpenStreetMap (1 seule requête Overpass) — idempotent', )] class ImportCoastalPointsCommand extends Command { private const OVERPASS_URL = 'https://overpass-api.de/api/interpreter'; - // bbox format Overpass : sud,ouest,nord,est + /** + * Bboxes par île : [sud, ouest, nord, est] + * Utilisées pour : + * 1. construire les statements Overpass (1 requête globale) + * 2. déterminer la région d'un point en PHP + */ private const ISLANDS = [ - // Guadeloupe : Basse-Terre + Grande-Terre séparés pour éviter que la bbox - // englobe trop de mer et rate des côtes - 'Basse-Terre' => ['bbox' => '15.82,-61.82,16.52,-61.55', 'region' => 'Guadeloupe'], - 'Grande-Terre' => ['bbox' => '15.97,-61.55,16.52,-61.07', 'region' => 'Guadeloupe'], - 'Marie-Galante' => ['bbox' => '15.83,-61.40,16.08,-61.10', 'region' => 'Guadeloupe'], - 'La Désirade' => ['bbox' => '16.27,-61.10,16.37,-60.82', 'region' => 'Guadeloupe'], - 'Les Saintes' => ['bbox' => '15.83,-61.68,15.90,-61.55', 'region' => 'Guadeloupe'], - 'Martinique' => ['bbox' => '14.38,-61.25,14.88,-60.80', 'region' => 'Martinique'], - 'Sainte-Lucie' => ['bbox' => '13.70,-61.10,14.15,-60.85', 'region' => 'Sainte-Lucie'], - 'Barbade' => ['bbox' => '13.00,-59.80,13.40,-59.40', 'region' => 'Barbade'], - 'Saint-Martin' => ['bbox' => '17.85,-63.20,18.18,-62.95', 'region' => 'Saint-Martin'], - 'Saint-Barthélemy' => ['bbox' => '17.85,-62.90,17.98,-62.78', 'region' => 'Saint-Barthélemy'], - 'Dominique' => ['bbox' => '15.20,-61.50,15.65,-61.24', 'region' => 'Dominique'], + 'Basse-Terre' => ['bbox' => [15.82, -61.82, 16.52, -61.55], 'region' => 'Guadeloupe'], + 'Grande-Terre' => ['bbox' => [15.97, -61.55, 16.52, -61.07], 'region' => 'Guadeloupe'], + 'Marie-Galante' => ['bbox' => [15.83, -61.40, 16.08, -61.10], 'region' => 'Guadeloupe'], + 'La Désirade' => ['bbox' => [16.27, -61.10, 16.37, -60.82], 'region' => 'Guadeloupe'], + 'Les Saintes' => ['bbox' => [15.83, -61.68, 15.90, -61.55], 'region' => 'Guadeloupe'], + 'Martinique' => ['bbox' => [14.38, -61.25, 14.88, -60.80], 'region' => 'Martinique'], + 'Sainte-Lucie' => ['bbox' => [13.70, -61.10, 14.15, -60.85], 'region' => 'Sainte-Lucie'], + 'Barbade' => ['bbox' => [13.00, -59.80, 13.40, -59.40], 'region' => 'Barbade'], + 'Saint-Martin' => ['bbox' => [17.85, -63.20, 18.18, -62.95], 'region' => 'Saint-Martin'], + 'Saint-Barthélemy' => ['bbox' => [17.85, -62.90, 17.98, -62.78], 'region' => 'Saint-Barthélemy'], + 'Dominique' => ['bbox' => [15.20, -61.50, 15.65, -61.24], 'region' => 'Dominique'], ]; public function __construct( @@ -46,93 +49,78 @@ class ImportCoastalPointsCommand extends Command protected function configure(): void { - $this - ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Affiche sans persister en base') - ->addOption('island', null, InputOption::VALUE_REQUIRED, 'Importer une seule île (ex: Guadeloupe)'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Affiche sans persister en base'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $dryRun = (bool) $input->getOption('dry-run'); - $filter = $input->getOption('island'); $io->title('Import OpenStreetMap — Points côtiers Antilles'); - if ($dryRun) { $io->note('Mode dry-run : aucune écriture en base'); } + $io->text('Envoi d\'une seule requête Overpass pour toutes les îles…'); + $elements = $this->queryOverpass($io); + + if (empty($elements)) { + $io->warning('Aucun résultat Overpass.'); + return Command::FAILURE; + } + + $io->text(sprintf('%d éléments OSM reçus, attribution des régions…', count($elements))); + $repo = $this->em->getRepository(CoastalPoint::class); $created = 0; $skipped = 0; - $islands = self::ISLANDS; - if (is_string($filter)) { - $islands = array_filter( - $islands, - static fn(string $k) => mb_strtolower($k) === mb_strtolower($filter), - ARRAY_FILTER_USE_KEY - ); - if (empty($islands)) { - $io->error("Île inconnue : {$filter}. Valeurs : " . implode(', ', array_keys(self::ISLANDS))); - return Command::FAILURE; + foreach ($elements as $el) { + $osmId = $el['type'] . '/' . $el['id']; + $osmName = $el['tags']['name'] ?? $el['tags']['name:fr'] ?? null; + if ($osmName === null) { + continue; } - } - foreach ($islands as $islandName => $config) { - $io->section("→ {$islandName}"); - $elements = $this->queryOverpass($config['bbox'], $io); - - foreach ($elements as $el) { - $osmId = $el['type'] . '/' . $el['id']; - $osmName = $el['tags']['name'] ?? $el['tags']['name:fr'] ?? null; - - if ($osmName === null) { - continue; - } - - // Idempotence par osmId - if ($repo->findOneBy(['osmId' => $osmId]) !== null) { - ++$skipped; - continue; - } - - // Idempotence par nom+région (couvre les points seedés manuellement) - if ($repo->findOneBy(['name' => $osmName, 'region' => $config['region']]) !== null) { - ++$skipped; - continue; - } - - $lat = $el['lat'] ?? ($el['center']['lat'] ?? null); - $lon = $el['lon'] ?? ($el['center']['lon'] ?? null); - - if ($lat === null || $lon === null) { - continue; - } - - $type = $this->mapType($el['tags']); - - $io->writeln(sprintf(' + %-45s [%s]', $osmName, $type)); - - if (!$dryRun) { - $point = new CoastalPoint(); - $point->setName($osmName); - $point->setGeometry(sprintf('SRID=4326;POINT(%f %f)', $lon, $lat)); - $point->setType($type); - $point->setRegion($config['region']); - $point->setOsmId($osmId); - $this->em->persist($point); - } - - ++$created; + $lat = $el['lat'] ?? ($el['center']['lat'] ?? null); + $lon = $el['lon'] ?? ($el['center']['lon'] ?? null); + if ($lat === null || $lon === null) { + continue; } + $region = $this->detectRegion((float) $lat, (float) $lon); + if ($region === null) { + continue; // hors de nos zones d'intérêt + } + + if ($repo->findOneBy(['osmId' => $osmId]) !== null) { + ++$skipped; + continue; + } + if ($repo->findOneBy(['name' => $osmName, 'region' => $region]) !== null) { + ++$skipped; + continue; + } + + $type = $this->mapType($el['tags']); + $io->writeln(sprintf(' + %-45s [%-8s] %s', $osmName, $type, $region)); + if (!$dryRun) { - $this->em->flush(); + $point = new CoastalPoint(); + $point->setName($osmName); + $point->setGeometry(sprintf('SRID=4326;POINT(%f %f)', $lon, $lat)); + $point->setType($type); + $point->setRegion($region); + $point->setOsmId($osmId); + $this->em->persist($point); } - sleep(1); // respect Overpass rate limit + ++$created; + } + + if (!$dryRun) { + $this->em->flush(); } $verb = $dryRun ? 'trouvé(s)' : 'créé(s)'; @@ -142,57 +130,69 @@ class ImportCoastalPointsCommand extends Command } /** + * Une seule requête Overpass avec toutes les bboxes en union. + * * @return array> */ - private function queryOverpass(string $bbox, SymfonyStyle $io): array + private function queryOverpass(SymfonyStyle $io): array { - $query = <<httpClient->request('POST', self::OVERPASS_URL, [ 'body' => ['data' => $query], - 'timeout' => 35, + 'timeout' => 65, ]); $data = $response->toArray(); - return $data['elements'] ?? []; } catch (\Throwable $e) { - $io->warning('Overpass error : ' . $e->getMessage()); + $io->error('Overpass error : ' . $e->getMessage()); return []; } } /** - * @param array $tags + * Détermine la région d'un point par appartenance à une bbox. + * En cas de recouvrement, prend la première bbox correspondante. */ + private function detectRegion(float $lat, float $lon): ?string + { + foreach (self::ISLANDS as $config) { + [$s, $w, $n, $e] = $config['bbox']; + if ($lat >= $s && $lat <= $n && $lon >= $w && $lon <= $e) { + return $config['region']; + } + } + return null; + } + + /** @param array $tags */ private function mapType(array $tags): string { - if (($tags['sport'] ?? '') === 'surfing') return 'surf'; - if (($tags['harbour'] ?? '') === 'fishing') return 'fishing'; - if (($tags['amenity'] ?? '') === 'ferry_terminal') return 'port'; - if (($tags['leisure'] ?? '') === 'marina') return 'port'; + if (($tags['sport'] ?? '') === 'surfing') return 'surf'; + if (($tags['harbour'] ?? '') === 'fishing') return 'fishing'; + if (($tags['amenity'] ?? '') === 'ferry_terminal') return 'port'; + if (($tags['leisure'] ?? '') === 'marina') return 'port'; return 'beach'; } }