diff --git a/frontend/src/containers/map/content/map/index.tsx b/frontend/src/containers/map/content/map/index.tsx index 9c0208af..98d24109 100644 --- a/frontend/src/containers/map/content/map/index.tsx +++ b/frontend/src/containers/map/content/map/index.tsx @@ -26,7 +26,7 @@ import RegionsPopup from '@/containers/map/content/map/popup/regions'; import { useSyncMapLayers, useSyncMapSettings } from '@/containers/map/content/map/sync-settings'; import { sidebarAtom } from '@/containers/map/store'; import { - bboxLocation, + bboxLocationAtom, drawStateAtom, layersInteractiveAtom, layersInteractiveIdsAtom, @@ -51,8 +51,8 @@ const MainMap: FCWithMessages = () => { const isSidebarOpen = useAtomValue(sidebarAtom); const [popup, setPopup] = useAtom(popupAtom); const params = useParams(); - const [locationBbox, setLocationBbox] = useAtom(bboxLocation); - const resetLocationBbox = useResetAtom(bboxLocation); + const [locationBbox, setLocationBbox] = useAtom(bboxLocationAtom); + const resetLocationBbox = useResetAtom(bboxLocationAtom); const hoveredPolygonId = useRef[0] | null>(null); const [cursor, setCursor] = useState<'grab' | 'crosshair' | 'pointer'>('grab'); diff --git a/frontend/src/containers/map/content/map/popup/eez/index.tsx b/frontend/src/containers/map/content/map/popup/eez/index.tsx index 9b17b0fe..b920aa83 100644 --- a/frontend/src/containers/map/content/map/popup/eez/index.tsx +++ b/frontend/src/containers/map/content/map/popup/eez/index.tsx @@ -9,10 +9,9 @@ import type { Feature } from 'geojson'; import { useAtom, useAtomValue } from 'jotai'; import { useLocale, useTranslations } from 'next-intl'; -import { CustomMapProps } from '@/components/map/types'; import { PAGES } from '@/constants/pages'; import { useMapSearchParams } from '@/containers/map/content/map/sync-settings'; -import { bboxLocation, layersInteractiveIdsAtom, popupAtom } from '@/containers/map/store'; +import { layersInteractiveIdsAtom, popupAtom } from '@/containers/map/store'; import { formatPercentage, formatKM } from '@/lib/utils/formats'; import { FCWithMessages } from '@/types'; import { useGetLayersId } from '@/types/generated/layer'; @@ -29,7 +28,6 @@ const EEZLayerPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { const { default: map } = useMap(); const searchParams = useMapSearchParams(); const { push } = useRouter(); - const [, setLocationBBox] = useAtom(bboxLocation); const [popup, setPopup] = useAtom(popupAtom); const { locationCode } = useParams(); @@ -109,7 +107,7 @@ const EEZLayerPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { // @ts-ignore populate: { location: { - fields: ['code', 'marine_bounds', 'total_marine_area'], + fields: ['code', 'total_marine_area'], }, }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -164,12 +162,11 @@ const EEZLayerPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { const handleLocationSelected = useCallback(async () => { if (!protectionCoverageStats?.location?.data.attributes) return undefined; - const { code, marine_bounds: bounds } = protectionCoverageStats.location.data.attributes; + const { code } = protectionCoverageStats.location.data.attributes; await push(`${PAGES.progressTracker}/${code.toUpperCase()}?${searchParams.toString()}`); - setLocationBBox(bounds as CustomMapProps['bounds']['bbox']); setPopup({}); - }, [push, searchParams, setLocationBBox, protectionCoverageStats, setPopup]); + }, [push, searchParams, protectionCoverageStats, setPopup]); useEffect(() => { map?.on('render', handleMapRender); diff --git a/frontend/src/containers/map/content/map/popup/regions/index.tsx b/frontend/src/containers/map/content/map/popup/regions/index.tsx index 825e4296..03ca47ee 100644 --- a/frontend/src/containers/map/content/map/popup/regions/index.tsx +++ b/frontend/src/containers/map/content/map/popup/regions/index.tsx @@ -5,13 +5,12 @@ import { useMap } from 'react-map-gl'; import { useRouter } from 'next/router'; import type { Feature } from 'geojson'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { useLocale, useTranslations } from 'next-intl'; -import { CustomMapProps } from '@/components/map/types'; import { PAGES } from '@/constants/pages'; import { useMapSearchParams } from '@/containers/map/content/map/sync-settings'; -import { bboxLocation, layersInteractiveIdsAtom, popupAtom } from '@/containers/map/store'; +import { layersInteractiveIdsAtom, popupAtom } from '@/containers/map/store'; import { formatPercentage, formatKM } from '@/lib/utils/formats'; import { FCWithMessages } from '@/types'; import { useGetLayersId } from '@/types/generated/layer'; @@ -28,7 +27,6 @@ const RegionsPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { const { default: map } = useMap(); const searchParams = useMapSearchParams(); const { push } = useRouter(); - const setLocationBBox = useSetAtom(bboxLocation); const [popup, setPopup] = useAtom(popupAtom); const layersInteractiveIds = useAtomValue(layersInteractiveIdsAtom); @@ -112,7 +110,7 @@ const RegionsPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { // @ts-ignore populate: { location: { - fields: ['name', 'name_es', 'name_fr', 'code', 'marine_bounds', 'total_marine_area'], + fields: ['name', 'name_es', 'name_fr', 'code', 'total_marine_area'], }, }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -172,12 +170,11 @@ const RegionsPopup: FCWithMessages<{ layerId: number }> = ({ layerId }) => { const handleLocationSelected = useCallback(async () => { if (!protectionCoverageStats?.location?.data.attributes) return undefined; - const { code, marine_bounds: bounds } = protectionCoverageStats.location.data.attributes; + const { code } = protectionCoverageStats.location.data.attributes; await push(`${PAGES.progressTracker}/${code.toUpperCase()}?${searchParams.toString()}`); - setLocationBBox(bounds as CustomMapProps['bounds']['bbox']); setPopup({}); - }, [push, searchParams, setLocationBBox, protectionCoverageStats, setPopup]); + }, [push, searchParams, protectionCoverageStats, setPopup]); useEffect(() => { map?.on('render', handleMapRender); diff --git a/frontend/src/containers/map/sidebar/main-panel/panels/details/index.tsx b/frontend/src/containers/map/sidebar/main-panel/panels/details/index.tsx index 5ac50b14..c9fcb2f2 100644 --- a/frontend/src/containers/map/sidebar/main-panel/panels/details/index.tsx +++ b/frontend/src/containers/map/sidebar/main-panel/panels/details/index.tsx @@ -2,16 +2,22 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useRouter } from 'next/router'; +import { BBox } from '@turf/turf'; +import { useAtom } from 'jotai'; import { useLocale, useTranslations } from 'next-intl'; +import { CustomMapProps } from '@/components/map/types'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { PAGES } from '@/constants/pages'; import { useMapSearchParams } from '@/containers/map/content/map/sync-settings'; +import { bboxLocationAtom } from '@/containers/map/store'; import { useSyncMapContentSettings } from '@/containers/map/sync-settings'; import useScrollPosition from '@/hooks/use-scroll-position'; import { cn } from '@/lib/classnames'; +import { combineBoundingBoxes } from '@/lib/utils/geo'; import { FCWithMessages } from '@/types'; import { useGetLocations } from '@/types/generated/location'; +import { Location } from '@/types/generated/strapi.schemas'; import LocationSelector from '../../location-selector'; @@ -35,13 +41,23 @@ const SidebarDetails: FCWithMessages = () => { const searchParams = useMapSearchParams(); const [{ tab }, setSettings] = useSyncMapContentSettings(); + const [, setLocationBBox] = useAtom(bboxLocationAtom); const { data: locationsData } = useGetLocations({ locale, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + fields: ['name', 'name_es', 'name_fr', 'marine_bounds', 'terrestrial_bounds'], filters: { code: locationCode, }, - populate: 'members', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + populate: { + members: { + fields: ['code', 'name', 'name_es', 'name_fr'], + }, + }, }); const locationNameField = useMemo(() => { @@ -55,6 +71,25 @@ const SidebarDetails: FCWithMessages = () => { return res; }, [locale]); + const locationBounds = useMemo(() => { + const { terrestrial_bounds, marine_bounds } = + locationsData?.data[0]?.attributes ?? ({} as Location); + + if (tab === 'terrestrial') { + return terrestrial_bounds; + } + + if (tab === 'marine') { + return marine_bounds; + } + + if (terrestrial_bounds === undefined || marine_bounds === undefined) { + return null; + } + + return combineBoundingBoxes(terrestrial_bounds as BBox, marine_bounds as BBox); + }, [locationsData, tab]); + const memberCountries = useMemo(() => { return locationsData?.data[0]?.attributes?.members?.data?.map(({ attributes }) => ({ code: attributes?.code, @@ -80,6 +115,13 @@ const SidebarDetails: FCWithMessages = () => { containerRef.current?.scrollTo({ top: 0 }); }, [tab, locationCode]); + // Zoom the map to the location's bounds (terrestrial bounds, marine bounds or both) + useEffect(() => { + if (locationBounds) { + setLocationBBox(locationBounds as CustomMapProps['bounds']['bbox']); + } + }, [setLocationBBox, locationBounds]); + return (
([]); export const layersInteractiveIdsAtom = atom([]); -export const bboxLocation = atomWithReset([ +export const bboxLocationAtom = atomWithReset([ -180, -85.5624999997749, 180, 90, ]); export const popupAtom = atom>({}); diff --git a/frontend/src/lib/utils/geo.ts b/frontend/src/lib/utils/geo.ts new file mode 100644 index 00000000..1411fb47 --- /dev/null +++ b/frontend/src/lib/utils/geo.ts @@ -0,0 +1,16 @@ +import { BBox } from '@turf/helpers'; + +/** + * Combines two bounding boxes into a single bounding box that encompasses both + * @param bbox1 First bounding box [minLon, minLat, maxLon, maxLat] + * @param bbox2 Second bounding box [minLon, minLat, maxLon, maxLat] + * @returns Combined bounding box + */ +export const combineBoundingBoxes = (bbox1: BBox, bbox2: BBox): BBox => { + return [ + Math.min(bbox1[0], bbox2[0]), + Math.min(bbox1[1], bbox2[1]), + Math.max(bbox1[2], bbox2[2]), + Math.max(bbox1[3], bbox2[3]), + ]; +};