diff --git a/packages/website/src/common/Chart/MultipleSensorsCharts/__snapshots__/index.test.tsx.snap b/packages/website/src/common/Chart/MultipleSensorsCharts/__snapshots__/index.test.tsx.snap index 437f6901e..83ebefbea 100644 --- a/packages/website/src/common/Chart/MultipleSensorsCharts/__snapshots__/index.test.tsx.snap +++ b/packages/website/src/common/Chart/MultipleSensorsCharts/__snapshots__/index.test.tsx.snap @@ -141,9 +141,6 @@ exports[`MultipleSensorsCharts should render with given state from Redux store 1 type="button" > Download - diff --git a/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.test.tsx b/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.test.tsx index ceebef2af..0718acb39 100644 --- a/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.test.tsx +++ b/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.test.tsx @@ -16,12 +16,16 @@ describe('ReefCheckSurveyCard', () => { ); } - it('should render date', () => { + it('should render correctly', () => { const { getByText } = renderReefCheckSurveyCard(); expect( getByText(`Date: ${new Date(mockReefCheckSurvey.date).toLocaleString()}`), ).toBeInTheDocument(); + expect( + getByText(`Depth: ${mockReefCheckSurvey.depth}m`), + ).toBeInTheDocument(); + expect(getByText('User: Reef Check')).toBeInTheDocument(); }); it('should render user if submittedBy is present', () => { @@ -48,7 +52,7 @@ describe('ReefCheckSurveyCard', () => { 'Count', 'INVERTEBRATES (2)', 'Count', - 'BLEACHING AND CORAL DIDEASES', + 'BLEACHING AND CORAL DISEASES', 'YES/NO', 'IMPACT', 'YES/NO', diff --git a/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.tsx b/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.tsx index 5304a4fc2..c07f92663 100644 --- a/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.tsx +++ b/packages/website/src/common/SiteDetails/Surveys/ReefCheckSurveyCard/index.tsx @@ -57,10 +57,13 @@ const ReefCheckSurveyCardComponent = ({ return ( - Date: {new Date(survey.date).toLocaleString()} - {survey.submittedBy && ( - User: {survey.submittedBy} - )} + + + Date: {new Date(survey.date).toLocaleString()} + + Depth: {survey.depth}m + + User: {survey.submittedBy ?? 'Reef Check'} @@ -75,7 +78,7 @@ const ReefCheckSurveyCardComponent = ({ Count - BLEACHING AND CORAL DIDEASES + BLEACHING AND CORAL DISEASES YES/NOIMPACT diff --git a/packages/website/src/helpers/bleachingAlertIntervals.ts b/packages/website/src/helpers/bleachingAlertIntervals.ts index d2f4db7c8..5f1c89d5d 100644 --- a/packages/website/src/helpers/bleachingAlertIntervals.ts +++ b/packages/website/src/helpers/bleachingAlertIntervals.ts @@ -84,11 +84,6 @@ export const findIntervalByLevel = ( } }; -export const findMaxLevel = (intervals: Interval[]): number => { - const levels = intervals.map((item) => item.level); - return Math.max(...levels); -}; - export const getColorByLevel = (level: number): string => { return findIntervalByLevel(level).color; }; diff --git a/packages/website/src/routes/HomeMap/Map/Markers/SiteMarker.tsx b/packages/website/src/routes/HomeMap/Map/Markers/SiteMarker.tsx index 6e532c2c1..bd054ad0b 100644 --- a/packages/website/src/routes/HomeMap/Map/Markers/SiteMarker.tsx +++ b/packages/website/src/routes/HomeMap/Map/Markers/SiteMarker.tsx @@ -1,17 +1,18 @@ -import { Marker, useMap } from 'react-leaflet'; +import React from 'react'; +import { CircleMarker, Marker } from 'react-leaflet'; import { useDispatch, useSelector } from 'react-redux'; import { Site } from 'store/Sites/types'; import { - siteOnMapSelector, - setSiteOnMap, + isSelectedOnMapSelector, setSearchResult, + setSiteOnMap, } from 'store/Homepage/homepageSlice'; -import { useMarkerIcon } from 'helpers/map'; -import { hasDeployedSpotter } from 'helpers/siteUtils'; import { alertColorFinder, alertIconFinder, } from 'helpers/bleachingAlertIntervals'; +import { useMarkerIcon } from 'helpers/map'; +import { hasDeployedSpotter } from 'helpers/siteUtils'; import Popup from '../Popup'; // To make sure we can see all the sites all the time, and especially @@ -20,21 +21,54 @@ const LNG_OFFSETS = [-360, 0, 360]; interface SiteMarkerProps { site: Site; - setCenter: (inputMap: L.Map, latLng: [number, number], zoom: number) => void; } /** * All in one site marker with icon, offset duplicates, and popup built in. */ -export default function SiteMarker({ site, setCenter }: SiteMarkerProps) { - const siteOnMap = useSelector(siteOnMapSelector); - const map = useMap(); +export const CircleSiteMarker = React.memo(({ site }: SiteMarkerProps) => { + const isSelected = useSelector(isSelectedOnMapSelector(site.id)); + const dispatch = useDispatch(); + const { tempWeeklyAlert } = site.collectionData || {}; + + if (site.polygon.type !== 'Point') return null; + + const [lng, lat] = site.polygon.coordinates; + + return ( + <> + {LNG_OFFSETS.map((offset) => ( + { + dispatch(setSearchResult()); + dispatch(setSiteOnMap(site)); + }, + }} + key={`${site.id}-${offset}`} + center={[lat, lng + offset]} + radius={5} + fillColor={alertColorFinder(tempWeeklyAlert)} + fillOpacity={1} + color="black" + weight={1} + data-alert={tempWeeklyAlert} + > + {isSelected && } + + ))} + + ); +}); + +export const SensorSiteMarker = React.memo(({ site }: SiteMarkerProps) => { + const isSelected = useSelector(isSelectedOnMapSelector(site.id)); const dispatch = useDispatch(); const { tempWeeklyAlert } = site.collectionData || {}; const markerIcon = useMarkerIcon( hasDeployedSpotter(site), site.hasHobo, - siteOnMap?.id === site.id, + isSelected, alertColorFinder(tempWeeklyAlert), alertIconFinder(tempWeeklyAlert), ); @@ -42,26 +76,25 @@ export default function SiteMarker({ site, setCenter }: SiteMarkerProps) { if (site.polygon.type !== 'Point') return null; const [lng, lat] = site.polygon.coordinates; + return ( <> - {LNG_OFFSETS.map((offset) => { - return ( - { - if (map) setCenter(map, [lat, lng], 6); - dispatch(setSearchResult()); - dispatch(setSiteOnMap(site)); - }, - }} - key={`${site.id}-${offset}`} - icon={markerIcon} - position={[lat, lng + offset]} - > - - - ); - })} + {LNG_OFFSETS.map((offset) => ( + { + dispatch(setSearchResult()); + dispatch(setSiteOnMap(site)); + }, + }} + key={`${site.id}-${offset}`} + icon={markerIcon} + position={[lat, lng + offset]} + data-alert={tempWeeklyAlert} + > + {isSelected && } + + ))} ); -} +}); diff --git a/packages/website/src/routes/HomeMap/Map/Markers/index.tsx b/packages/website/src/routes/HomeMap/Map/Markers/index.tsx index 1767074ff..41fa4d937 100644 --- a/packages/website/src/routes/HomeMap/Map/Markers/index.tsx +++ b/packages/website/src/routes/HomeMap/Map/Markers/index.tsx @@ -1,35 +1,13 @@ import { useSelector } from 'react-redux'; -import { LayerGroup, useMap } from 'react-leaflet'; -import MarkerClusterGroup from 'react-leaflet-markercluster'; -import { useCallback, useEffect, useState, useMemo } from 'react'; -import L from 'leaflet'; +import { useMemo } from 'react'; import { sitesToDisplayListSelector } from 'store/Sites/sitesListSlice'; import { Site } from 'store/Sites/types'; -import { siteOnMapSelector } from 'store/Homepage/homepageSlice'; import 'leaflet/dist/leaflet.css'; import { CollectionDetails } from 'store/Collection/types'; -import { - findIntervalByLevel, - findMaxLevel, - getColorByLevel, - Interval, -} from 'helpers/bleachingAlertIntervals'; -import SiteMarker from './SiteMarker'; +import { hasDeployedSpotter } from 'helpers/siteUtils'; +import { CircleSiteMarker, SensorSiteMarker } from './SiteMarker'; -const clusterIcon = (cluster: any) => { - const alerts: Interval[] = cluster.getAllChildMarkers().map((marker: any) => { - const { site } = marker?.options?.children?.[0]?.props || {}; - const { tempWeeklyAlert } = site?.collectionData || {}; - return findIntervalByLevel(tempWeeklyAlert); - }); - const color = getColorByLevel(findMaxLevel(alerts)); - const count = cluster.getChildCount(); - return L.divIcon({ - html: `
${count}
`, - className: `leaflet-marker-icon marker-cluster custom-cluster-icon marker-cluster-small leaflet-zoom-animated leaflet-interactive`, - iconSize: L.point(40, 40, true), - }); -}; +const hasSpotter = (site: Site) => site.hasHobo || hasDeployedSpotter(site); export const SiteMarkers = ({ collection }: SiteMarkersProps) => { const storedSites = useSelector(sitesToDisplayListSelector); @@ -37,65 +15,17 @@ export const SiteMarkers = ({ collection }: SiteMarkersProps) => { () => collection?.sites || storedSites || [], [collection?.sites, storedSites], ); - const siteOnMap = useSelector(siteOnMapSelector); - const map = useMap(); - const [visibleSites, setVisibleSites] = useState(sitesList); - - const setCenter = useCallback( - (inputMap: L.Map, latLng: [number, number], zoom: number) => { - const maxZoom = Math.max(inputMap.getZoom() || 6, zoom); - const pointBounds = L.latLngBounds(latLng, latLng); - inputMap.flyToBounds(pointBounds, { - duration: 2, - maxZoom, - paddingTopLeft: L.point(0, 200), - }); - }, - [], - ); - - const filterSitesByViewport = useCallback(() => { - if (!map) return; - - const bounds = map.getBounds(); - const filtered = sitesList.filter((site: Site) => { - if (!site.polygon || site.polygon.type !== 'Point') return false; - const [lng, lat] = site.polygon.coordinates; - return bounds.contains([lat, lng]); - }); - setVisibleSites(filtered); - }, [map, sitesList]); - - useEffect(() => { - if (!map) return undefined; - - filterSitesByViewport(); - map.on('moveend', filterSitesByViewport); - - return () => { - map.off('moveend', filterSitesByViewport); - return undefined; - }; - }, [map, filterSitesByViewport]); - - useEffect(() => { - if (map && siteOnMap?.polygon.type === 'Point') { - const [lng, lat] = siteOnMap.polygon.coordinates; - setCenter(map, [lat, lng], 6); - } - }, [map, siteOnMap, setCenter]); return ( - - - {visibleSites.map((site: Site) => ( - - ))} - - + <> + {sitesList.map((site: Site) => + sitesList[site.id] && hasSpotter(site) ? ( + + ) : ( + + ), + )} + ); }; diff --git a/packages/website/src/routes/HomeMap/Map/index.tsx b/packages/website/src/routes/HomeMap/Map/index.tsx index 4f66eb8b1..43a834b9c 100644 --- a/packages/website/src/routes/HomeMap/Map/index.tsx +++ b/packages/website/src/routes/HomeMap/Map/index.tsx @@ -18,7 +18,10 @@ import withStyles, { } from '@mui/styles/withStyles'; import MyLocationIcon from '@mui/icons-material/MyLocation'; import { sitesListLoadingSelector } from 'store/Sites/sitesListSlice'; -import { searchResultSelector } from 'store/Homepage/homepageSlice'; +import { + searchResultSelector, + siteOnMapSelector, +} from 'store/Homepage/homepageSlice'; import { CollectionDetails } from 'store/Collection/types'; import { MapLayerName } from 'store/Homepage/types'; import { mapConstants } from 'constants/maps'; @@ -69,6 +72,7 @@ const HomepageMap = ({ const loading = useSelector(sitesListLoadingSelector); const searchResult = useSelector(searchResultSelector); const ref = useRef(null); + const siteOnMap = useSelector(siteOnMapSelector); const onLocationSearch = () => { if (navigator.geolocation) { @@ -119,6 +123,20 @@ const HomepageMap = ({ // const onBaseLayerChange = ({ name }: LayersControlEvent) => { // setLegendName(name); // }; + useEffect(() => { + const map = ref.current?.leafletElement; + if (map && siteOnMap?.polygon.type === 'Point') { + const [lng, lat] = siteOnMap.polygon.coordinates; + const latLng = [lat, lng] as [number, number]; + const pointBounds = L.latLngBounds(latLng, latLng); + const maxZoom = Math.max(map.getZoom() || 6); + map.flyToBounds(pointBounds, { + duration: 2, + maxZoom, + paddingTopLeft: L.point(0, 200), + }); + } + }, [siteOnMap]); const ExpandIcon = showSiteTable ? FullscreenIcon : FullscreenExitIcon; diff --git a/packages/website/src/routes/SiteRoutes/ReefCheckSurveys/ReefCheckSurveyDetails.tsx b/packages/website/src/routes/SiteRoutes/ReefCheckSurveys/ReefCheckSurveyDetails.tsx index 3f54b204e..a584d8a78 100644 --- a/packages/website/src/routes/SiteRoutes/ReefCheckSurveys/ReefCheckSurveyDetails.tsx +++ b/packages/website/src/routes/SiteRoutes/ReefCheckSurveys/ReefCheckSurveyDetails.tsx @@ -65,6 +65,11 @@ const surveyFields = [ // eslint-disable-next-line prettier/prettier ] satisfies Array>; +const formatFieldValue = (value: any, formatter?: (v: any) => string) => { + if (value != null && formatter) return formatter(value); + return value ?? ''; +}; + const ReefCheckSurveyDetailsComponent = ({ classes, }: ReefCheckSurveyDetailsProps) => { @@ -89,7 +94,7 @@ const ReefCheckSurveyDetailsComponent = ({ className={classes.skeleton} /> ) : ( - formatter?.(survey[field]) ?? survey[field] ?? '' + formatFieldValue(survey[field], formatter) )} diff --git a/packages/website/src/routes/SiteRoutes/SiteApplication/Form/__snapshots__/index.test.tsx.snap b/packages/website/src/routes/SiteRoutes/SiteApplication/Form/__snapshots__/index.test.tsx.snap index 7121e7bf1..1097cc923 100644 --- a/packages/website/src/routes/SiteRoutes/SiteApplication/Form/__snapshots__/index.test.tsx.snap +++ b/packages/website/src/routes/SiteRoutes/SiteApplication/Form/__snapshots__/index.test.tsx.snap @@ -784,7 +784,7 @@ exports[`renders as expected 1`] = `