import { LatLngBounds } from 'leaflet'; import { useCallback, useEffect, useRef } from 'react'; import { useMap, useMapEvents } from 'react-leaflet'; import { useMapActions } from '@/contexts/MapContexts.tsx'; /** * Component to track map bounds and update context * This enables viewport-based lazy loading of organizations * Optimized with better debouncing and bounds comparison */ const MapBoundsTracker = () => { const { setMapBounds } = useMapActions(); const boundsRef = useRef(null); const updateTimeoutRef = useRef(null); const isUpdatingRef = useRef(false); /** * Check if bounds have changed significantly * Uses a more efficient comparison that considers both area and position */ const hasBoundsChanged = useCallback( (newBounds: LatLngBounds, oldBounds: LatLngBounds | null): boolean => { if (!oldBounds) return true; // Compare center position const newCenter = newBounds.getCenter(); const oldCenter = oldBounds.getCenter(); const centerDiff = Math.abs(newCenter.lat - oldCenter.lat) + Math.abs(newCenter.lng - oldCenter.lng); // Compare bounds size (north-south and east-west spans) const newSize = { lat: newBounds.getNorth() - newBounds.getSouth(), lng: newBounds.getEast() - newBounds.getWest(), }; const oldSize = { lat: oldBounds.getNorth() - oldBounds.getSouth(), lng: oldBounds.getEast() - oldBounds.getWest(), }; const sizeDiff = Math.abs(newSize.lat - oldSize.lat) + Math.abs(newSize.lng - oldSize.lng); // Update if center moved significantly (>0.01 degrees) or size changed (>5%) return centerDiff > 0.01 || sizeDiff > Math.max(newSize.lat, newSize.lng) * 0.05; }, [] ); const map = useMap(); /** * Update bounds with debouncing */ const updateBounds = useCallback( (immediate: boolean = false) => { if (updateTimeoutRef.current) { clearTimeout(updateTimeoutRef.current); } const update = () => { if (isUpdatingRef.current) return; const bounds = map.getBounds(); if (hasBoundsChanged(bounds, boundsRef.current)) { boundsRef.current = bounds; isUpdatingRef.current = true; // Use requestAnimationFrame for smooth updates requestAnimationFrame(() => { setMapBounds(bounds); // Reset flag after a short delay setTimeout(() => { isUpdatingRef.current = false; }, 100); }); } }; if (immediate) { update(); } else { updateTimeoutRef.current = setTimeout(update, 300); } }, [map, setMapBounds, hasBoundsChanged] ); useMapEvents({ moveend: () => { updateBounds(false); }, zoomend: () => { // Faster update on zoom as it's a discrete action updateBounds(false); }, move: () => { // Throttle during active movement to reduce updates updateBounds(false); }, }); // Initialize bounds on mount useEffect(() => { // Wait for map to be fully initialized const timer = setTimeout(() => { updateBounds(true); }, 100); return () => { clearTimeout(timer); if (updateTimeoutRef.current) { clearTimeout(updateTimeoutRef.current); } }; }, [updateBounds]); return null; }; export default MapBoundsTracker;