perf: spots/overview — 100+ requêtes individuelles → 1 requête cachée 30 min
Avant : SargassesMap faisait 1 appel /api/spots (API Platform) puis
N appels /api/spots/{id}/score (un par spot, ~100+ pour la Guadeloupe).
Après : un seul GET /api/spots/overview (LATERAL JOIN spots+scores)
avec Cache-Control: public, max-age=1800, stale-while-revalidate=300.
Les marqueurs colorés s'affichent immédiatement depuis la réponse unique ;
/api/spots/{id}/score n'est appelé qu'au clic sur un spot (SpotPanel).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,46 @@ class SpotScoreController extends AbstractController
|
|||||||
private Connection $connection,
|
private Connection $connection,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/spots/overview
|
||||||
|
*
|
||||||
|
* Retourne tous les spots avec leur dernier score en UNE seule requête.
|
||||||
|
* Remplace les N appels individuels à /api/spots/{id}/score pour le rendu
|
||||||
|
* initial de la carte.
|
||||||
|
*
|
||||||
|
* Cache HTTP 30 min (scores mis à jour au max 1-2×/jour par ingestion).
|
||||||
|
*/
|
||||||
|
#[Route('/overview', name: 'overview', methods: ['GET'])]
|
||||||
|
public function overview(): JsonResponse
|
||||||
|
{
|
||||||
|
$rows = $this->connection->fetchAllAssociative(
|
||||||
|
"SELECT
|
||||||
|
cp.id::text,
|
||||||
|
cp.name,
|
||||||
|
cp.type,
|
||||||
|
cp.region,
|
||||||
|
ST_X(cp.geometry::geometry) AS longitude,
|
||||||
|
ST_Y(cp.geometry::geometry) AS latitude,
|
||||||
|
ist.score,
|
||||||
|
ist.level,
|
||||||
|
ROUND(CAST(ist.distance_to_nearest_sargassum AS numeric), 2) AS distance_km
|
||||||
|
FROM coastal_point cp
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT score, level, distance_to_nearest_sargassum
|
||||||
|
FROM impact_score
|
||||||
|
WHERE coastal_point_id = cp.id
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1
|
||||||
|
) ist ON true
|
||||||
|
ORDER BY cp.name"
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->json($rows);
|
||||||
|
$response->headers->set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=300');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/spots/{id}/score
|
* GET /api/spots/{id}/score
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ async function post(path, body) {
|
|||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
spots: {
|
spots: {
|
||||||
list: () => get('/spots'),
|
overview: () => get('/spots/overview'), // tous les spots + score en 1 requête cachée
|
||||||
get: (id) => get(`/spots/${id}`),
|
get: (id) => get(`/spots/${id}`),
|
||||||
score: (id, at) => get(`/spots/${id}/score`, { at }),
|
score: (id, at) => get(`/spots/${id}/score`, { at }),
|
||||||
},
|
},
|
||||||
observations: {
|
observations: {
|
||||||
list: (bbox, date, source) => get('/observations', { bbox, date, source }),
|
list: (bbox, date, source) => get('/observations', { bbox, date, source }),
|
||||||
|
|||||||
@@ -179,8 +179,9 @@ function SpotMarker({ level, scoreValue, scoreDist, isSelected, name }) {
|
|||||||
|
|
||||||
export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) {
|
export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) {
|
||||||
const mapRef = useRef(null);
|
const mapRef = useRef(null);
|
||||||
|
// spots : [{ id, name, type, region, longitude, latitude, score, level, distance_km }]
|
||||||
const [spots, setSpots] = useState([]);
|
const [spots, setSpots] = useState([]);
|
||||||
const [spotScores, setSpotScores] = useState({});
|
const [spotScores, setSpotScores] = useState({}); // id → { level, value, distance }
|
||||||
const [viewState, setViewState] = useState(INITIAL_VIEW);
|
const [viewState, setViewState] = useState(INITIAL_VIEW);
|
||||||
const [layerOpacity, setLayerOpacity] = useState(1);
|
const [layerOpacity, setLayerOpacity] = useState(1);
|
||||||
const [mapStyle, setMapStyle] = useState(null); // null = style not yet ready
|
const [mapStyle, setMapStyle] = useState(null); // null = style not yet ready
|
||||||
@@ -212,28 +213,28 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep,
|
|||||||
return () => ctrl.abort();
|
return () => ctrl.abort();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load coastal spots once
|
// Charge tous les spots + scores en UNE requête (cachée 30 min côté serveur)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.spots.list()
|
api.spots.overview()
|
||||||
.then(data => setSpots(Array.isArray(data) ? data : (data['hydra:member'] ?? data.member ?? [])))
|
.then(rows => {
|
||||||
|
if (!Array.isArray(rows)) return;
|
||||||
|
setSpots(rows);
|
||||||
|
// Pré-remplir spotScores depuis la même réponse — aucune requête supplémentaire
|
||||||
|
const scores = {};
|
||||||
|
rows.forEach(r => {
|
||||||
|
if (r.level) {
|
||||||
|
scores[r.id] = {
|
||||||
|
level: r.level,
|
||||||
|
value: r.score ?? 0,
|
||||||
|
distance: r.distance_km ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setSpotScores(scores);
|
||||||
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load scores progressively for marker colors
|
|
||||||
useEffect(() => {
|
|
||||||
if (!spots.length) return;
|
|
||||||
spots.forEach(s => {
|
|
||||||
api.spots.score(s.id)
|
|
||||||
.then(d => {
|
|
||||||
const level = d?.score?.level;
|
|
||||||
const value = d?.score?.value ?? 0;
|
|
||||||
const distance = d?.score?.distance ?? null;
|
|
||||||
if (level) setSpotScores(prev => ({ ...prev, [s.id]: { level, value, distance } }));
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
|
||||||
}, [spots]);
|
|
||||||
|
|
||||||
// Fade transition on step change
|
// Fade transition on step change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLayerOpacity(0);
|
setLayerOpacity(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user