Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/components/Common/Map/TemplateMap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import L, { Map as LeafletMap } from 'leaflet';
import { MapContainer, TileLayer } from 'react-leaflet';
import { useEffect } from 'react';
import { MapContainer, TileLayer, useMap } from 'react-leaflet';

import { useMapLayers } from '@/utils/useMapLayers';

Expand All @@ -14,6 +15,64 @@ interface CameraMapProps {
showLayerControl?: boolean;
}

// Leaflet computes the map size only when its container mounts. If the container
// is later resized (fullscreen toggle, responsive layout, panel open/close), Leaflet
// keeps the stale size and leaves blank/grey areas where tiles are missing.
// This component calls map.invalidateSize() on mount and on every container resize
// so Leaflet recomputes its size and loads the missing tiles.
const MapSizeInvalidator = () => {
const map = useMap();

useEffect(() => {
const container = map.getContainer();
// Tracks scheduled-but-not-yet-run frames so we can cancel them on unmount
// and never call invalidateSize on a destroyed map.
const animationFrameIds = new Set<number>();

const invalidateMapSize = () => {
map.invalidateSize({ pan: false });
};

// Defer the invalidate to the next frame, after the browser has finished
// its layout, so we read the container's final dimensions.
const scheduleInvalidateMapSize = () => {
const animationFrameId = window.requestAnimationFrame(() => {
animationFrameIds.delete(animationFrameId);
invalidateMapSize();
});
animationFrameIds.add(animationFrameId);
};

scheduleInvalidateMapSize();

// Fallback for environments without ResizeObserver (e.g. jsdom in tests):
// no resize tracking, just cancel any pending frame on unmount.
if (typeof ResizeObserver === 'undefined') {
return () => {
animationFrameIds.forEach((animationFrameId) => {
window.cancelAnimationFrame(animationFrameId);
});
};
}

// Re-invalidate whenever the container's size changes.
const resizeObserver = new ResizeObserver(scheduleInvalidateMapSize);

resizeObserver.observe(container);

// Cancel pending frames first, then stop observing — order matters so no
// queued frame can fire invalidateSize after the map is torn down.
return () => {
animationFrameIds.forEach((animationFrameId) => {
window.cancelAnimationFrame(animationFrameId);
});
resizeObserver.disconnect();
};
}, [map]);

return null;
};

export const TemplateMap = ({
height = '100%',
minHeight,
Expand Down Expand Up @@ -41,6 +100,7 @@ export const TemplateMap = ({
url={baseTileConfig.url}
maxZoom={baseTileConfig.maxZoom}
/>
<MapSizeInvalidator />

{children}

Expand Down
Loading