query->get('bbox'); if ($bboxParam !== null) { $parts = array_map('floatval', explode(',', $bboxParam)); if (count($parts) === 4) { [$minLon, $minLat, $maxLon, $maxLat] = $parts; $sql .= ' AND ST_Intersects(f.geometry, ST_MakeEnvelope(:minLon, :minLat, :maxLon, :maxLat, 4326))'; $params['minLon'] = $minLon; $params['minLat'] = $minLat; $params['maxLon'] = $maxLon; $params['maxLat'] = $maxLat; } } $horizonParam = $request->query->get('horizon'); if ($horizonParam !== null && in_array((int) $horizonParam, [6, 12, 24, 48], true)) { $sql .= ' AND f.time_horizon = :horizon'; $params['horizon'] = (int) $horizonParam; } $sql .= ' ORDER BY f.valid_at DESC LIMIT 50'; $rows = $this->connection->fetchAllAssociative($sql, $params); foreach ($rows as &$row) { $row['geometry'] = $row['geometry'] ? json_decode($row['geometry'], true) : null; } return $this->json(['items' => $rows, 'total' => count($rows)]); } /** * GET /api/forecasts/{id} */ #[Route('/{id}', name: 'item', methods: ['GET'])] public function item(string $id): JsonResponse { $row = $this->connection->fetchAssociative( 'SELECT f.id, f.time_horizon AS "timeHorizon", f.valid_at AS "validAt", f.computed_at AS "computedAt", f.model_version AS "modelVersion", f.confidence, f.source_observation_id AS "sourceObservationId", ST_AsGeoJSON(f.geometry)::json AS geometry FROM sargassum_forecast f WHERE f.id = :id', ['id' => $id] ); if ($row === false) { return $this->json(['error' => 'Forecast not found'], 404); } $row['geometry'] = $row['geometry'] ? json_decode($row['geometry'], true) : null; return $this->json($row); } }