diff --git a/app/public/assets/icon/circle-medium.svg b/app/public/assets/icon/circle-medium.svg new file mode 100644 index 0000000000..28796ab32e --- /dev/null +++ b/app/public/assets/icon/circle-medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/components/map/components/DrawControls.tsx b/app/src/components/map/components/DrawControls.tsx index 2d5aefdaa1..ab0d0f4839 100644 --- a/app/src/components/map/components/DrawControls.tsx +++ b/app/src/components/map/components/DrawControls.tsx @@ -4,6 +4,7 @@ import L from 'leaflet'; import 'leaflet-draw'; import 'leaflet-draw/dist/leaflet.draw.css'; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; +import { coloredPoint } from 'utils/mapUtils'; /** * Custom subset of `L.Control.DrawConstructorOptions` that omits `edit.featureGroup` as this will be added automatically @@ -89,17 +90,32 @@ const DrawControls = forwardRef { const featureGroup = getFeatureGroup(); + const CustomMarker = L.Icon.extend({ + // The preview icon rendered when you are in the process of adding a marker to the map + options: { + iconUrl: 'assets/icon/circle-medium.svg', + iconRetinaUrl: 'assets/icon/circle-medium.svg', + iconSize: new L.Point(36, 36), + iconAnchor: new L.Point(18, 18), + popupAnchor: [18, 18], + shadowUrl: null + } + }); + const drawOptions: L.Control.DrawConstructorOptions = { + draw: { + ...options?.draw, + marker: { + icon: new CustomMarker() + } + }, edit: { ...options?.edit, featureGroup: featureGroup - } + }, + position: options?.position || 'topright' }; - drawOptions.draw = { ...options?.draw }; - - drawOptions.position = drawOptions?.position || 'topright'; - return new L.Control.Draw(drawOptions); }; @@ -170,7 +186,7 @@ const DrawControls = forwardRef> = (pro const layerControls: ReactElement[] = []; - props.layers.forEach((layer, index) => { + props.layers.forEach((layer) => { if (!layer.features?.length) { return; } @@ -57,12 +58,12 @@ const StaticLayers: React.FC> = (pro return new L.Circle([latlng.lat, latlng.lng], feature.properties.radius); } - return new L.Marker([latlng.lat, latlng.lng]); + return coloredPoint({ latlng }); }} data={item.geoJSON} {...item.GeoJSONProps}> {item.tooltip && ( - + {item.tooltip} )} diff --git a/app/src/features/surveys/observations/ObservationsMap.tsx b/app/src/features/surveys/observations/ObservationsMap.tsx index 79b0598024..fb530ee3b9 100644 --- a/app/src/features/surveys/observations/ObservationsMap.tsx +++ b/app/src/features/surveys/observations/ObservationsMap.tsx @@ -2,23 +2,25 @@ import { mdiRefresh } from '@mdi/js'; import Icon from '@mdi/react'; import { IconButton } from '@mui/material'; import Box from '@mui/material/Box'; -import { square } from '@turf/turf'; import BaseLayerControls from 'components/map/components/BaseLayerControls'; import { SetMapBounds } from 'components/map/components/Bounds'; import FullScreenScrollingEventHandler from 'components/map/components/FullScreenScrollingEventHandler'; import { MapBaseCss } from 'components/map/components/MapBaseCss'; -import { MAP_DEFAULT_CENTER } from 'constants/spatial'; +import StaticLayers from 'components/map/components/StaticLayers'; +import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER } from 'constants/spatial'; import { ObservationsContext } from 'contexts/observationsContext'; -import { Position } from 'geojson'; +import { SurveyContext } from 'contexts/surveyContext'; +import { Feature, Position } from 'geojson'; import { LatLngBoundsExpression } from 'leaflet'; -import { useCallback, useContext, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { GeoJSON, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet'; -import { calculateFeatureBoundingBox, latLngBoundsFromBoundingBox } from 'utils/mapBoundaryUploadHelpers'; +import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; import { coloredPoint, INonEditableGeometries } from 'utils/mapUtils'; import { v4 as uuidv4 } from 'uuid'; const ObservationsMap = () => { const observationsContext = useContext(ObservationsContext); + const surveyContext = useContext(SurveyContext); const surveyObservations: INonEditableGeometries[] = useMemo(() => { const observations = observationsContext.observationsDataLoader.data?.surveyObservations; @@ -58,29 +60,59 @@ const ObservationsMap = () => { }); }, [observationsContext.observationsDataLoader.data]); - const getDefaultMapBounds = useCallback((): LatLngBoundsExpression | undefined => { - const features = surveyObservations.map((observation) => observation.feature); - const boundingBox = calculateFeatureBoundingBox(features); + const studyAreaFeatures: Feature[] = useMemo(() => { + const locations = surveyContext.surveyDataLoader.data?.surveyData.locations; + if (!locations) { + return []; + } - if (!boundingBox) { - return; + return locations.flatMap((item) => item.geojson); + }, [surveyContext.surveyDataLoader.data]); + + const sampleSiteFeatures: Feature[] = useMemo(() => { + const sites = surveyContext.sampleSiteDataLoader.data?.sampleSites; + if (!sites) { + return []; } - return latLngBoundsFromBoundingBox(square(boundingBox)); - }, [surveyObservations]); + return sites.map((item) => item.geojson); + }, [surveyContext.sampleSiteDataLoader.data]); - const [bounds, setBounds] = useState(getDefaultMapBounds()); + const getDefaultMapBounds = useCallback((): LatLngBoundsExpression | undefined => { + const features = surveyObservations.map((observation) => observation.feature); + return calculateUpdatedMapBounds([...features, ...studyAreaFeatures, ...sampleSiteFeatures]); + }, [surveyObservations, studyAreaFeatures, sampleSiteFeatures]); + + // set default bounds to encompass all of BC + const [bounds, setBounds] = useState( + calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]) + ); const zoomToBoundaryExtent = useCallback(() => { setBounds(getDefaultMapBounds()); }, [getDefaultMapBounds]); + useEffect(() => { + // Once all data loaders have finished loading it will zoom the map to include all features + if ( + !surveyContext.surveyDataLoader.isLoading && + !surveyContext.sampleSiteDataLoader.isLoading && + !observationsContext.observationsDataLoader.isLoading + ) { + zoomToBoundaryExtent(); + } + }, [ + observationsContext.observationsDataLoader.isLoading, + surveyContext.sampleSiteDataLoader.isLoading, + surveyContext.surveyDataLoader.isLoading, + zoomToBoundaryExtent + ]); + return ( { coloredPoint({ feature, latlng })}> + pointToLayer={(_, latlng) => coloredPoint({ latlng, fillColor: '#F28C28' })}> {nonEditableGeo.popupComponent} ))} + ({ geoJSON: feature, tooltip: <>Study Area })) + }, + { + layerName: 'Sample Sites', + features: sampleSiteFeatures.map((feature) => ({ geoJSON: feature, tooltip: <>Sample Site })) + } + ]} + /> - {surveyObservations.length > 0 && ( + {(surveyObservations.length > 0 || studyAreaFeatures.length > 0 || sampleSiteFeatures.length > 0) && ( => { return new L.CircleMarker(point.latlng, { radius: 6, fillOpacity: 1, - fillColor: point.fillColor ?? '#006edc', + fillColor: point.fillColor ?? MapDefaultBlue, color: point.borderColor ?? '#ffffff', weight: 1 });