import { useQuery } from '@tanstack/react-query'; import { LatLngBounds } from 'leaflet'; import { useMemo } from 'react'; import type { BackendSite } from '@/schemas/backend/site'; import { getNearbySites } from '@/services/sites-api'; import { calculateDistance } from '@/utils/coordinates.ts'; /** * Calculate center and radius from map bounds */ function boundsToCenterAndRadius(bounds: LatLngBounds): { lat: number; lng: number; radius: number; } { const center = bounds.getCenter(); const ne = bounds.getNorthEast(); const sw = bounds.getSouthWest(); // Calculate radius as the distance from center to corner const radius = Math.max( calculateDistance(center.lat, center.lng, ne.lat, ne.lng), calculateDistance(center.lat, center.lng, sw.lat, sw.lng) ); // Add 20% buffer to ensure we get all sites near the edges return { lat: center.lat, lng: center.lng, radius: radius * 1.2, }; } /** * Hook to fetch sites within current map bounds * This enables viewport-based lazy loading of organizations * Optimized with better caching and query key stability */ export const useSitesByBounds = (bounds: LatLngBounds | null, enabled: boolean = true) => { const queryParams = useMemo(() => { if (!bounds) return null; const params = boundsToCenterAndRadius(bounds); // Round coordinates to reduce query key churn (0.001 degrees ≈ 111m) return { lat: Math.round(params.lat * 1000) / 1000, lng: Math.round(params.lng * 1000) / 1000, radius: Math.round(params.radius * 10) / 10, // Round to 0.1km }; }, [bounds]); return useQuery({ queryKey: ['sites', 'nearby', queryParams?.lat, queryParams?.lng, queryParams?.radius], queryFn: () => { if (!queryParams) return Promise.resolve([]); return getNearbySites({ lat: queryParams.lat, lng: queryParams.lng, radius: queryParams.radius, }); }, enabled: enabled && !!queryParams, placeholderData: [], // Render immediately with empty array - don't block on API staleTime: 60 * 1000, // 1 minute - sites don't change often gcTime: 10 * 60 * 1000, // 10 minutes - keep in cache longer refetchOnWindowFocus: false, // Don't refetch on window focus refetchOnMount: false, // Don't refetch if data exists retry: 1, // Only retry once on failure }); }; /** * Create a map of organization ID -> sites for organizations in viewport */ export const useOrganizationSitesByBounds = ( bounds: LatLngBounds | null, enabled: boolean = true ) => { const { data: sites, isLoading } = useSitesByBounds(bounds, enabled); const orgSitesMap = useMemo(() => { const map = new Map(); if (!sites) return map; sites.forEach((site) => { const orgId = site.OwnerOrganizationID; if (!orgId) return; const existing = map.get(orgId) || []; map.set(orgId, [...existing, site]); }); return map; }, [sites]); return { orgSitesMap, sites: sites || [], isLoading, }; };