fix: prevent map freeze and fix repositioning race condition
This commit is contained in:
@@ -112,45 +112,41 @@ function FitBounds({ coords, mapState, end, duration, loading }) {
|
|||||||
const prevEndRef = useRef();
|
const prevEndRef = useRef();
|
||||||
const prevDurationRef = useRef();
|
const prevDurationRef = useRef();
|
||||||
const refitNeeded = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const prevEnd = prevEndRef.current;
|
// Don't run on the very first render, wait until refs are populated.
|
||||||
const prevDuration = prevDurationRef.current;
|
if (prevEndRef.current && prevDurationRef.current) {
|
||||||
|
const endChanged = end.unix() !== prevEndRef.current.unix();
|
||||||
// Run only after initial render where refs are populated.
|
const durationChanged = duration.id !== prevDurationRef.current.id;
|
||||||
if (prevEnd && prevDuration) {
|
|
||||||
const endChanged = end.unix() !== prevEnd.unix();
|
|
||||||
const durationChanged = duration.id !== prevDuration.id;
|
|
||||||
|
|
||||||
if (endChanged || durationChanged) {
|
if (endChanged || durationChanged) {
|
||||||
refitNeeded.current = true;
|
refitNeeded.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update refs for the next render's comparison.
|
|
||||||
prevEndRef.current = end;
|
prevEndRef.current = end;
|
||||||
prevDurationRef.current = duration;
|
prevDurationRef.current = duration;
|
||||||
}, [end, 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(() => {
|
useEffect(() => {
|
||||||
// Do not run this effect if new data is being fetched.
|
// Don't do anything while new data is being fetched.
|
||||||
if (loading) return;
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// A refit is needed on initial load (mapState.center is null)
|
// Perform fit on initial load OR if a refit has been flagged.
|
||||||
// or if the flag has been set by the other effect.
|
if ((isInitialLoad.current || refitNeeded.current) && coords.length > 0) {
|
||||||
if ((mapState.center === null || refitNeeded.current) && coords.length > 0) {
|
|
||||||
const bounds = leaflet.latLngBounds(coords);
|
const bounds = leaflet.latLngBounds(coords);
|
||||||
if (bounds.isValid()) {
|
if (bounds.isValid()) {
|
||||||
map.fitBounds(bounds);
|
map.fitBounds(bounds);
|
||||||
}
|
}
|
||||||
// Reset the flag after the fit is performed.
|
// Reset flags after fitting.
|
||||||
refitNeeded.current = false;
|
refitNeeded.current = false;
|
||||||
|
isInitialLoad.current = false;
|
||||||
}
|
}
|
||||||
}, [coords, mapState.center, map, loading]);
|
}, [coords, loading, map]); // Dependencies: run when data is ready.
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Map({end, duration, slider, mapState, setMapState}) {
|
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 range = useMemo(() => parseSlider(end, duration, slider), [end, duration, slider]);
|
||||||
|
|
||||||
const coords = useMemo(() => {
|
const coords = useMemo(() => {
|
||||||
if (!Array.isArray(data)) return [];
|
// 1. Guard against invalid top-level data
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
const [startTime, endTime] = range;
|
const [startTime, endTime] = range;
|
||||||
|
|
||||||
|
// 2. Loop through the data
|
||||||
for (const point of data) {
|
for (const point of data) {
|
||||||
// Ensure the point and its time exist before checking the range
|
// 3. Guard against malformed points
|
||||||
if (!point || !point.time) continue;
|
if (!point || typeof point !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (point.time >= startTime && point.time <= endTime) {
|
const { lat, lon, time } = point;
|
||||||
const { lat, lon } = point;
|
|
||||||
// Strictest possible check for valid, finite, numeric coordinates
|
// 4. Guard against invalid time
|
||||||
if (typeof lat === 'number' && typeof lon === 'number' && isFinite(lat) && isFinite(lon)) {
|
if (typeof time !== 'string' || time.length === 0) {
|
||||||
result.push([lat, lon]);
|
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;
|
return result;
|
||||||
|
Reference in New Issue
Block a user