Skip to content

Commit

Permalink
SIMSBIOHUB-354-355: Study area and Sampling sites on observations map (
Browse files Browse the repository at this point in the history
…#1157)

SIMSBIOHUB-354-355: Added sampling site and study area features to the observation map on the survey details page
---------

Co-authored-by: Nick Phura <nickphura@gmail.com>
  • Loading branch information
al-rosenthal and NickPhura authored Nov 3, 2023
1 parent 2876f50 commit 4051da8
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 41 deletions.
1 change: 1 addition & 0 deletions app/public/assets/icon/circle-medium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 22 additions & 6 deletions app/src/components/map/components/DrawControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,17 +90,32 @@ const DrawControls = forwardRef<IDrawControlsRef | undefined, IDrawControlsProps
const getDrawControls = (): L.Control.Draw => {
const featureGroup = getFeatureGroup();

Check warning on line 91 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L91

Added line #L91 was not covered by tests

const CustomMarker = L.Icon.extend({

Check warning on line 93 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L93

Added line #L93 was not covered by tests
// 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 = {

Check warning on line 105 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L105

Added line #L105 was not covered by tests
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);

Check warning on line 119 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L119

Added line #L119 was not covered by tests
};

Expand Down Expand Up @@ -170,7 +186,7 @@ const DrawControls = forwardRef<IDrawControlsRef | undefined, IDrawControlsProps
return new L.Circle([latlng.lat, latlng.lng], feature.properties.radius);

Check warning on line 186 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L186

Added line #L186 was not covered by tests
}

return new L.Marker([latlng.lat, latlng.lng]);
return coloredPoint({ latlng });

Check warning on line 189 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L189

Added line #L189 was not covered by tests
},
onEachFeature: function (_feature, layer) {
featureGroup.addLayer(layer);

Check warning on line 192 in app/src/components/map/components/DrawControls.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L192

Added line #L192 was not covered by tests
Expand Down
13 changes: 0 additions & 13 deletions app/src/components/map/components/MapBaseCss.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import L from 'leaflet';
import 'leaflet-fullscreen/dist/leaflet.fullscreen.css';
import 'leaflet-fullscreen/dist/Leaflet.fullscreen.js';
import iconRetina from 'leaflet/dist/images/marker-icon-2x.png';
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
import 'leaflet/dist/leaflet.css';
/*
Set react leaflet icon to leaflet images
React leaflet does not have these images in their package https://stackoverflow.com/questions/49441600/react-leaflet-marker-files-not-found
*/
L.Icon.Default.mergeOptions({
iconRetinaUrl: iconRetina,
iconUrl: icon,
shadowUrl: iconShadow
});

/**
* This component exists to keep the leaflet map container css/ changes required for each map container to render properly.
Expand Down
7 changes: 4 additions & 3 deletions app/src/components/map/components/StaticLayers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Tooltip,
TooltipProps
} from 'react-leaflet';
import { coloredPoint } from 'utils/mapUtils';

export interface IStaticLayerFeature {
geoJSON: Feature;
Expand Down Expand Up @@ -38,7 +39,7 @@ const StaticLayers: React.FC<React.PropsWithChildren<IStaticLayersProps>> = (pro

const layerControls: ReactElement[] = [];

props.layers.forEach((layer, index) => {
props.layers.forEach((layer) => {

Check warning on line 42 in app/src/components/map/components/StaticLayers.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/StaticLayers.tsx#L42

Added line #L42 was not covered by tests
if (!layer.features?.length) {
return;
}
Expand All @@ -57,12 +58,12 @@ const StaticLayers: React.FC<React.PropsWithChildren<IStaticLayersProps>> = (pro
return new L.Circle([latlng.lat, latlng.lng], feature.properties.radius);
}

return new L.Marker([latlng.lat, latlng.lng]);
return coloredPoint({ latlng });

Check warning on line 61 in app/src/components/map/components/StaticLayers.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/StaticLayers.tsx#L61

Added line #L61 was not covered by tests
}}
data={item.geoJSON}
{...item.GeoJSONProps}>
{item.tooltip && (
<Tooltip key={`static-feature-tooltip-${id}`} direction="top" {...item.TooltipProps}>
<Tooltip key={`static-feature-tooltip-${id}`} direction="top" sticky={true} {...item.TooltipProps}>
{item.tooltip}
</Tooltip>
)}
Expand Down
76 changes: 60 additions & 16 deletions app/src/features/surveys/observations/ObservationsMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Check warning on line 23 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L22-L23

Added lines #L22 - L23 were not covered by tests

const surveyObservations: INonEditableGeometries[] = useMemo(() => {
const observations = observationsContext.observationsDataLoader.data?.surveyObservations;

Check warning on line 26 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L25-L26

Added lines #L25 - L26 were not covered by tests
Expand Down Expand Up @@ -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;

Check warning on line 64 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L63-L64

Added lines #L63 - L64 were not covered by tests
if (!locations) {
return [];

Check warning on line 66 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L66

Added line #L66 was not covered by tests
}

if (!boundingBox) {
return;
return locations.flatMap((item) => item.geojson);

Check warning on line 69 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L69

Added line #L69 was not covered by tests
}, [surveyContext.surveyDataLoader.data]);

const sampleSiteFeatures: Feature[] = useMemo(() => {
const sites = surveyContext.sampleSiteDataLoader.data?.sampleSites;

Check warning on line 73 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L72-L73

Added lines #L72 - L73 were not covered by tests
if (!sites) {
return [];

Check warning on line 75 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L75

Added line #L75 was not covered by tests
}

return latLngBoundsFromBoundingBox(square(boundingBox));
}, [surveyObservations]);
return sites.map((item) => item.geojson);

Check warning on line 78 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L78

Added line #L78 was not covered by tests
}, [surveyContext.sampleSiteDataLoader.data]);

const [bounds, setBounds] = useState<LatLngBoundsExpression | undefined>(getDefaultMapBounds());
const getDefaultMapBounds = useCallback((): LatLngBoundsExpression | undefined => {
const features = surveyObservations.map((observation) => observation.feature);
return calculateUpdatedMapBounds([...features, ...studyAreaFeatures, ...sampleSiteFeatures]);

Check warning on line 83 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L81-L83

Added lines #L81 - L83 were not covered by tests
}, [surveyObservations, studyAreaFeatures, sampleSiteFeatures]);

// set default bounds to encompass all of BC
const [bounds, setBounds] = useState<LatLngBoundsExpression | undefined>(

Check warning on line 87 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L87

Added line #L87 was not covered by tests
calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY])
);

const zoomToBoundaryExtent = useCallback(() => {
setBounds(getDefaultMapBounds());

Check warning on line 92 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L91-L92

Added lines #L91 - L92 were not covered by tests
}, [getDefaultMapBounds]);

useEffect(() => {

Check warning on line 95 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L95

Added line #L95 was not covered by tests
// 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();

Check warning on line 102 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L102

Added line #L102 was not covered by tests
}
}, [
observationsContext.observationsDataLoader.isLoading,
surveyContext.sampleSiteDataLoader.isLoading,
surveyContext.surveyDataLoader.isLoading,
zoomToBoundaryExtent
]);

return (

Check warning on line 111 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L111

Added line #L111 was not covered by tests
<Box position="relative">
<LeafletMapContainer
data-testid="leaflet-survey_observations_map"
id="survey_observations_map"
zoom={6}
center={MAP_DEFAULT_CENTER}
scrollWheelZoom={false}
fullscreenControl={true}
Expand All @@ -93,15 +125,27 @@ const ObservationsMap = () => {
<GeoJSON

Check warning on line 125 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L125

Added line #L125 was not covered by tests
key={uuidv4()}
data={nonEditableGeo.feature}
pointToLayer={(feature, latlng) => coloredPoint({ feature, latlng })}>
pointToLayer={(_, latlng) => coloredPoint({ latlng, fillColor: '#F28C28' })}>

Check warning on line 128 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L128

Added line #L128 was not covered by tests
{nonEditableGeo.popupComponent}
</GeoJSON>
))}
<LayersControl position="bottomright">
<BaseLayerControls />
<StaticLayers
layers={[
{
layerName: 'Study Area',
features: studyAreaFeatures.map((feature) => ({ geoJSON: feature, tooltip: <>Study Area</> }))

Check warning on line 138 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L138

Added line #L138 was not covered by tests
},
{
layerName: 'Sample Sites',
features: sampleSiteFeatures.map((feature) => ({ geoJSON: feature, tooltip: <>Sample Site</> }))

Check warning on line 142 in app/src/features/surveys/observations/ObservationsMap.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/observations/ObservationsMap.tsx#L142

Added line #L142 was not covered by tests
}
]}
/>
</LayersControl>
</LeafletMapContainer>
{surveyObservations.length > 0 && (
{(surveyObservations.length > 0 || studyAreaFeatures.length > 0 || sampleSiteFeatures.length > 0) && (
<Box position="absolute" top="126px" left="10px" zIndex="999">
<IconButton
sx={{
Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/mapBoundaryUploadHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export const calculateFeatureBoundingBox = (features: Feature[]): BBox | undefin

/**
* If there are multiple points or a polygon and a point, we can automatically
* create a bouding box using Turf.
* create a bounding box using Turf.
*/
const allGeosFeatureCollection = {
type: 'FeatureCollection',
Expand Down
5 changes: 3 additions & 2 deletions app/src/utils/mapUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Feature } from 'geojson';
import L, { LatLng } from 'leaflet';

export const MapDefaultBlue = '#3388ff';

export const DefaultMapValues = {
zoom: 5,
center: [55, -128]
Expand All @@ -16,7 +18,6 @@ export interface IClusteredPointGeometries {
popupComponent?: JSX.Element;
}
export interface MapPointProps {
feature: Feature;
latlng: LatLng;
fillColor?: string;
borderColor?: string;
Expand All @@ -26,7 +27,7 @@ export const coloredPoint = (point: MapPointProps): L.CircleMarker<any> => {
return new L.CircleMarker(point.latlng, {
radius: 6,
fillOpacity: 1,
fillColor: point.fillColor ?? '#006edc',
fillColor: point.fillColor ?? MapDefaultBlue,
color: point.borderColor ?? '#ffffff',
weight: 1
});
Expand Down

0 comments on commit 4051da8

Please sign in to comment.