Skip to content

Commit

Permalink
Text input for capture and mortality locations (#1308)
Browse files Browse the repository at this point in the history
Add: Added input textfield boxes to manually enter capture and mortality latitude/longitude values. Updated validation of coordinates
Bug fix: remove overflow hidden from the telemetry deployments list

---------

Co-authored-by: Nick Phura <[email protected]>
  • Loading branch information
mauberti-bc and NickPhura authored Jun 21, 2024
1 parent d71f5f7 commit cfbc84f
Show file tree
Hide file tree
Showing 20 changed files with 375 additions and 91 deletions.
2 changes: 1 addition & 1 deletion api/src/paths/project/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('list', () => {
const requestHandler = list.getProjectList();

await requestHandler(sampleReq, sampleRes as any, null as unknown as any);
expect.fail();
expect.fail('Expected an error to be thrown');
} catch (actualError) {
expect(dbConnectionObj.release).to.have.been.called;

Expand Down
17 changes: 16 additions & 1 deletion app/src/components/map/components/DrawControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,22 @@ export interface IDrawControlsRef {
/**
* Adds a GeoJson feature to a new layer in the draw controls layer group.
*
* @memberof IDrawControlsRef
* @param feature The GeoJson feature to add as a layer.
* @param layerId A function to receive the unique ID assigned to the added layer.
*/
addLayer: (feature: Feature, layerId: (id: number) => void) => void;

/**
* Deletes a layer from the draw controls layer group by its unique ID.
*
* @param layerId The unique ID of the layer to delete.
*/
deleteLayer: (layerId: number) => void;

/**
* Clears all layers from the draw controls layer group.
*/
clearLayers: () => void;
}

/**
Expand Down Expand Up @@ -196,6 +207,10 @@ const DrawControls = forwardRef<IDrawControlsRef | undefined, IDrawControlsProps
deleteLayer: (layerId: number) => {
const featureGroup = getFeatureGroup();
featureGroup.removeLayer(layerId);
},
clearLayers: () => {
const featureGroup = getFeatureGroup();
featureGroup.clearLayers();
}
}),
[getFeatureGroup]
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/map/components/ImportBoundaryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import { Feature } from 'geojson';
import { boundaryUploadHelper } from 'utils/mapBoundaryUploadHelpers';

export interface IImportBoundaryDialogProps {
dialogTitle: string;
isOpen: boolean;
onClose: () => void;
onSuccess: (features: Feature[]) => void;
onFailure: (message: string) => void;
}

const ImportBoundaryDialog = (props: IImportBoundaryDialogProps) => {
const { isOpen, onClose, onSuccess, onFailure } = props;
const { dialogTitle, isOpen, onClose, onSuccess, onFailure } = props;
return (
<ComponentDialog open={isOpen} dialogTitle="Import Boundary" onClose={onClose}>
<ComponentDialog open={isOpen} dialogTitle={dialogTitle} onClose={onClose}>
<Box>
<Box mb={3}>
<Alert severity="info">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,38 @@ export const AnimalCaptureForm = <FormikValuesType extends ICreateCaptureRequest
// Points may have 3 coords for [lon, lat, elevation]
geometry: yup.object({
type: yup.string(),
coordinates: yup.array().of(yup.number()).min(2).max(3)
coordinates: yup
.array()
.of(yup.number())
.min(2)
.max(3)
.isValidPointCoordinates('Latitude or longitude values are outside of the valid range.')
.required('Latitude or longitude values are outside of the valid range.')
}),
properties: yup.object().optional()
})
.nullable()
.default(undefined)
.required('Capture location is required'),
release_location: yup
.array(
yup.object({
geojson: yup.array().min(1, 'Release location is required if it is different from the capture location')
})
)
.min(1, 'Release location is required if it is different from the capture location')
.object()
.shape({
type: yup.string(),
// Points may have 3 coords for [lon, lat, elevation]
geometry: yup.object({
type: yup.string(),
coordinates: yup
.array()
.of(yup.number())
.min(2)
.max(3)
.isValidPointCoordinates('Latitude or longitude values are outside of the valid range.')
.required('Latitude or longitude values are outside of the valid range.')
}),
properties: yup.object().optional()
})
.nullable()
.default(undefined)
}),
measurements: yup.array(
yup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ import ImportBoundaryDialog from 'components/map/components/ImportBoundaryDialog
import StaticLayers from 'components/map/components/StaticLayers';
import { MapBaseCss } from 'components/map/styles/MapBaseCss';
import { ALL_OF_BC_BOUNDARY, MAP_DEFAULT_CENTER, MAP_DEFAULT_ZOOM } from 'constants/spatial';
import LatitudeLongitudeTextFields from 'features/surveys/animals/profile/components/LatitudeLongitudeTextFields';
import { useFormikContext } from 'formik';
import { Feature } from 'geojson';
import { Feature, Point } from 'geojson';
import { ICreateCaptureRequest, IEditCaptureRequest } from 'interfaces/useCritterApi.interface';
import { DrawEvents, LatLngBoundsExpression } from 'leaflet';
import 'leaflet-fullscreen/dist/leaflet.fullscreen.css';
import 'leaflet-fullscreen/dist/Leaflet.fullscreen.js';
import 'leaflet/dist/leaflet.css';
import { get } from 'lodash-es';
import { debounce, get } from 'lodash-es';
import { useEffect, useMemo, useRef, useState } from 'react';
import { FeatureGroup, LayersControl, MapContainer as LeafletMapContainer } from 'react-leaflet';
import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers';
import { getCoordinatesFromGeoJson, isGeoJsonPointFeature, isValidCoordinates } from 'utils/spatial-utils';

export interface ICaptureLocationMapControlProps {
name: string;
Expand All @@ -35,7 +37,7 @@ export interface ICaptureLocationMapControlProps {
}

/**
* Capture location map control
* Capture location map control component.
*
* @param {ICaptureLocationMapControlProps} props
* @return {*}
Expand All @@ -47,76 +49,128 @@ export const CaptureLocationMapControl = <FormikValuesType extends ICreateCaptur
const [isOpen, setIsOpen] = useState<boolean>(false);
const [lastDrawn, setLastDrawn] = useState<null | number>(null);

const { values, setFieldValue, setFieldError, errors } = useFormikContext<FormikValuesType>();

const drawControlsRef = useRef<IDrawControlsRef>();

const { mapId } = props;

const { values, setFieldValue, setFieldError, errors } = useFormikContext<FormikValuesType>();

const [updatedBounds, setUpdatedBounds] = useState<LatLngBoundsExpression | undefined>(undefined);

// Array of capture location features
const captureLocationGeoJson: Feature | undefined = useMemo(() => {
const location: { latitude: number; longitude: number } | Feature = get(values, name);
// Define location as a GeoJson object using useMemo to memoize the value
const captureLocationGeoJson: Feature<Point> | undefined = useMemo(() => {
const location: { latitude: number; longitude: number } | Feature | undefined | null = get(values, name);

if (!location) {
return;
}

if ('latitude' in location && location.latitude !== 0 && location.longitude !== 0) {
if (
'latitude' in location &&
'longitude' in location &&
isValidCoordinates(location.latitude, location.longitude)
) {
return {
type: 'Feature',
geometry: { type: 'Point', coordinates: [location.longitude, location.latitude] },
properties: {}
};
}

if ('type' in location) {
if (isGeoJsonPointFeature(location)) {
return location;
}
}, [name, values]);

useEffect(() => {
setUpdatedBounds(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]));
const coordinates = captureLocationGeoJson && getCoordinatesFromGeoJson(captureLocationGeoJson);

// Initialize state based on formik context for the edit page
const [latitudeInput, setLatitudeInput] = useState<string>(coordinates ? String(coordinates.latitude) : '');
const [longitudeInput, setLongitudeInput] = useState<string>(coordinates ? String(coordinates.longitude) : '');

const [updatedBounds, setUpdatedBounds] = useState<LatLngBoundsExpression | undefined>(undefined);

// Update map bounds when the data changes
useEffect(() => {
if (captureLocationGeoJson) {
if ('type' in captureLocationGeoJson) {
if (captureLocationGeoJson.geometry.type === 'Point')
if (captureLocationGeoJson?.geometry.coordinates[0] !== 0) {
setUpdatedBounds(calculateUpdatedMapBounds([captureLocationGeoJson]));
}
const { latitude, longitude } = getCoordinatesFromGeoJson(captureLocationGeoJson);

if (isValidCoordinates(latitude, longitude)) {
setUpdatedBounds(calculateUpdatedMapBounds([captureLocationGeoJson]));
}
} else {
// If the capture location is not a valid point, set the bounds to the entire province
setUpdatedBounds(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]));
}
}, [captureLocationGeoJson]);

const updateFormikLocationFromLatLon = useMemo(
() =>
debounce((name, feature) => {
setFieldValue(name, feature);
}, 500),
[setFieldValue]
);

// Update formik and map when latitude/longitude text inputs change
useEffect(() => {
const lat = latitudeInput && parseFloat(latitudeInput);
const lon = longitudeInput && parseFloat(longitudeInput);

if (!(lat && lon)) {
setFieldValue(name, undefined);
return;
}

// If coordinates are invalid, reset the map to show nothing
if (!isValidCoordinates(lat, lon)) {
drawControlsRef.current?.clearLayers();
setUpdatedBounds(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]));
return;
}

const feature: Feature<Point> = {
id: 1,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lon, lat]
},
properties: {}
};

// Update formik through debounce function
updateFormikLocationFromLatLon(name, feature);
}, [latitudeInput, longitudeInput, name, setFieldValue, updateFormikLocationFromLatLon]);

return (
<Grid item xs={12}>
{get(errors, name) && !Array.isArray(get(errors, name)) && (
{typeof get(errors, name) === 'string' && !Array.isArray(get(errors, name)) && (
<Alert severity="error" variant="outlined" sx={{ mb: 2 }}>
<AlertTitle>Missing capture location</AlertTitle>
{get(errors, name) as string}
{get(errors, name)}
</Alert>
)}

<Box component="fieldset">
<Paper variant="outlined">
<ImportBoundaryDialog
dialogTitle={`Import ${title}`}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSuccess={(features) => {
setUpdatedBounds(calculateUpdatedMapBounds(features));
setFieldValue(name, features[0]);
setFieldError(name, undefined);
// Unset last drawn to show staticlayers, where the file geometry is loaded to
lastDrawn && drawControlsRef?.current?.deleteLayer(lastDrawn);
drawControlsRef?.current?.addLayer(features[0], () => 1);
setLastDrawn(1);
if ('coordinates' in features[0].geometry) {
setLatitudeInput(String(features[0].geometry.coordinates[1]));
setLongitudeInput(String(features[0].geometry.coordinates[0]));
}
}}
onFailure={(message) => {
setFieldError(name, message);
}}
/>

<Toolbar
disableGutters
sx={{
Expand All @@ -131,6 +185,17 @@ export const CaptureLocationMapControl = <FormikValuesType extends ICreateCaptur
}}>
{title}
</Typography>
<LatitudeLongitudeTextFields
sx={{ mx: 1 }}
latitudeValue={latitudeInput}
longitudeValue={longitudeInput}
onLatitudeChange={(event) => {
setLatitudeInput(event.currentTarget.value ?? '');
}}
onLongitudeChange={(event) => {
setLongitudeInput(event.currentTarget.value ?? '');
}}
/>
<Box display="flex">
<Button
color="primary"
Expand Down Expand Up @@ -172,17 +237,28 @@ export const CaptureLocationMapControl = <FormikValuesType extends ICreateCaptur
if (lastDrawn) {
drawControlsRef?.current?.deleteLayer(lastDrawn);
}
setFieldError(name, undefined);

const feature = event.layer.toGeoJSON();
const feature: Feature = event.layer.toGeoJSON();

setFieldError(name, undefined);
setFieldValue(name, feature);
// Set last drawn to remove it if a subsequent shape is added. There can only be one shape.

if ('coordinates' in feature.geometry) {
setLatitudeInput(String(feature.geometry.coordinates[1]));
setLongitudeInput(String(feature.geometry.coordinates[0]));
}

setLastDrawn(id);
}}
onLayerEdit={(event: DrawEvents.Edited) => {
event.layers.getLayers().forEach((layer: any) => {
const feature = layer.toGeoJSON() as Feature;
const feature: Feature = layer.toGeoJSON() as Feature;
setFieldValue(name, feature);
// Update the text box lat/lon inputs
if ('coordinates' in feature.geometry) {
setLatitudeInput(String(feature.geometry.coordinates[1]));
setLongitudeInput(String(feature.geometry.coordinates[0]));
}
});
}}
onLayerDelete={() => {
Expand All @@ -192,16 +268,14 @@ export const CaptureLocationMapControl = <FormikValuesType extends ICreateCaptur
</FeatureGroup>

<LayersControl position="bottomright">
{!lastDrawn && (
<StaticLayers
layers={[
{
layerName: 'Capture Location',
features: get(values, name) ? [{ geoJSON: get(values, name), key: Math.random() }] : []
}
]}
/>
)}
<StaticLayers
layers={[
{
layerName: `${title}`,
features: get(values, name) ? [{ geoJSON: get(values, name), key: Math.random() }] : []
}
]}
/>
<BaseLayerControls />
</LayersControl>
</LeafletMapContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ export const AnimalCaptureCardContainer = (props: IAnimalCaptureCardContainer) =
{capture.capture_location.latitude && capture.capture_location.longitude && (
<Box>
<Typography color="textSecondary" variant="body2">
{capture.capture_location.longitude},&nbsp;
{capture.capture_location.latitude}
{capture.capture_location.latitude},&nbsp;
{capture.capture_location.longitude}
</Typography>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const CaptureDetails = (props: ICaptureDetailsProps) => {
Capture location
</Typography>
<Typography color="textSecondary" variant="body2">
{captureLocation.longitude},&nbsp;{captureLocation.latitude}
{captureLocation.latitude},&nbsp;{captureLocation.longitude}
</Typography>
</Box>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const ReleaseDetails = (props: IReleaseDetailsProps) => {
</Typography>
{releaseLocation && (
<Typography color="textSecondary" variant="body2">
{releaseLocation.longitude},&nbsp;{releaseLocation.latitude}
{releaseLocation.latitude},&nbsp;{releaseLocation.longitude}
</Typography>
)}
</Box>
Expand Down
Loading

0 comments on commit cfbc84f

Please sign in to comment.