diff --git a/mapper/src/App.js b/mapper/src/App.js index e5332b4..63c60e6 100644 --- a/mapper/src/App.js +++ b/mapper/src/App.js @@ -112,45 +112,41 @@ function FitBounds({ coords, mapState, end, duration, loading }) { const prevEndRef = useRef(); const prevDurationRef = useRef(); const refitNeeded = useRef(false); + const isInitialLoad = useRef(mapState.center === null); - // Effect 1: Detects changes in `end` or `duration` and sets a flag. + // Effect 1: Flag the need for a refit when end/duration changes. useEffect(() => { - const prevEnd = prevEndRef.current; - const prevDuration = prevDurationRef.current; - - // Run only after initial render where refs are populated. - if (prevEnd && prevDuration) { - const endChanged = end.unix() !== prevEnd.unix(); - const durationChanged = duration.id !== prevDuration.id; + // Don't run on the very first render, wait until refs are populated. + if (prevEndRef.current && prevDurationRef.current) { + const endChanged = end.unix() !== prevEndRef.current.unix(); + const durationChanged = duration.id !== prevDurationRef.current.id; if (endChanged || durationChanged) { refitNeeded.current = true; } } - - // Update refs for the next render's comparison. prevEndRef.current = end; prevDurationRef.current = duration; }, [end, duration]); - // Effect 2: Acts on `coords` changes, but only if the flag is set and data isn't loading. + // Effect 2: Perform the fit when conditions are right. useEffect(() => { - // Do not run this effect if new data is being fetched. - if (loading) return; + // Don't do anything while new data is being fetched. + if (loading) { + return; + } - // A refit is needed on initial load (mapState.center is null) - // or if the flag has been set by the other effect. - if ((mapState.center === null || refitNeeded.current) && coords.length > 0) { + // Perform fit on initial load OR if a refit has been flagged. + if ((isInitialLoad.current || refitNeeded.current) && coords.length > 0) { const bounds = leaflet.latLngBounds(coords); if (bounds.isValid()) { map.fitBounds(bounds); } - // Reset the flag after the fit is performed. + // Reset flags after fitting. refitNeeded.current = false; + isInitialLoad.current = false; } - }, [coords, mapState.center, map, loading]); - - return null; + }, [coords, loading, map]); // Dependencies: run when data is ready. } function Map({end, duration, slider, mapState, setMapState}) { @@ -159,21 +155,36 @@ function Map({end, duration, slider, mapState, setMapState}) { const range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]); const coords = useMemo(() => { - if (!Array.isArray(data)) return []; + // 1. Guard against invalid top-level data + if (!Array.isArray(data)) { + return []; + } const result = []; const [startTime, endTime] = range; + // 2. Loop through the data for (const point of data) { - // Ensure the point and its time exist before checking the range - if (!point || !point.time) continue; + // 3. Guard against malformed points + if (!point || typeof point !== 'object') { + continue; + } - if (point.time >= startTime && point.time <= endTime) { - const { lat, lon } = point; - // Strictest possible check for valid, finite, numeric coordinates - if (typeof lat === 'number' && typeof lon === 'number' && isFinite(lat) && isFinite(lon)) { - result.push([lat, lon]); - } + const { lat, lon, time } = point; + + // 4. Guard against invalid time + if (typeof time !== 'string' || time.length === 0) { + continue; + } + + // 5. Guard against invalid coordinates + if (typeof lat !== 'number' || typeof lon !== 'number' || !isFinite(lat) || !isFinite(lon)) { + continue; + } + + // 6. Now that all data is known to be valid, filter by time + if (time >= startTime && time <= endTime) { + result.push([lat, lon]); } } return result;