Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text input for capture and mortality locations #1308

Merged
merged 6 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
/**
* 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 @@
deleteLayer: (layerId: number) => {
const featureGroup = getFeatureGroup();
featureGroup.removeLayer(layerId);
},
clearLayers: () => {
const featureGroup = getFeatureGroup();
featureGroup.clearLayers();

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

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/DrawControls.tsx#L212-L213

Added lines #L212 - L213 were not covered by tests
}
}),
[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 { 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;

Check warning on line 17 in app/src/components/map/components/ImportBoundaryDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/map/components/ImportBoundaryDialog.tsx#L17

Added line #L17 was not covered by tests
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 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 @@
}

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

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

Check warning on line 52 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L52

Added line #L52 was not covered by tests

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);

Check warning on line 60 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L59-L60

Added lines #L59 - L60 were not covered by tests

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);

Check warning on line 89 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L89

Added line #L89 was not covered by tests

// Update map bounds when the data changes
useEffect(() => {

Check warning on line 92 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L92

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

Check warning on line 94 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L94

Added line #L94 was not covered by tests

if (isValidCoordinates(latitude, longitude)) {
setUpdatedBounds(calculateUpdatedMapBounds([captureLocationGeoJson]));

Check warning on line 97 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L97

Added line #L97 was not covered by tests
}
} else {
// If the capture location is not a valid point, set the bounds to the entire province
setUpdatedBounds(calculateUpdatedMapBounds([ALL_OF_BC_BOUNDARY]));

Check warning on line 101 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L101

Added line #L101 was not covered by tests
}
}, [captureLocationGeoJson]);

const updateFormikLocationFromLatLon = useMemo(

Check warning on line 105 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L105

Added line #L105 was not covered by tests
() =>
debounce((name, feature) => {
setFieldValue(name, feature);

Check warning on line 108 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L107-L108

Added lines #L107 - L108 were not covered by tests
}, 500),
[setFieldValue]
);

// Update formik and map when latitude/longitude text inputs change
useEffect(() => {

Check warning on line 114 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L114

Added line #L114 was not covered by tests
const lat = latitudeInput && parseFloat(latitudeInput);
const lon = longitudeInput && parseFloat(longitudeInput);

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

Check warning on line 120 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L119-L120

Added lines #L119 - L120 were not covered by tests
}

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

Check warning on line 127 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L125-L127

Added lines #L125 - L127 were not covered by tests
}

const feature: Feature<Point> = {

Check warning on line 130 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L130

Added line #L130 was not covered by tests
id: 1,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lon, lat]
},
properties: {}
};

// Update formik through debounce function
updateFormikLocationFromLatLon(name, feature);

Check warning on line 141 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L141

Added line #L141 was not covered by tests
}, [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]));

Check warning on line 167 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L166-L167

Added lines #L166 - L167 were not covered by tests
}
}}
onFailure={(message) => {
setFieldError(name, message);
}}
/>

<Toolbar
disableGutters
sx={{
Expand All @@ -131,6 +185,17 @@
}}>
{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 @@
if (lastDrawn) {
drawControlsRef?.current?.deleteLayer(lastDrawn);
}
setFieldError(name, undefined);

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

Check warning on line 241 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L241

Added line #L241 was not covered by tests

setFieldError(name, undefined);

Check warning on line 243 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L243

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

Check warning on line 248 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L247-L248

Added lines #L247 - L248 were not covered by tests
}

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;

Check warning on line 255 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L255

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

Check warning on line 260 in app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/capture-form/components/location/CaptureLocationMapControl.tsx#L259-L260

Added lines #L259 - L260 were not covered by tests
}
});
}}
onLayerDelete={() => {
Expand All @@ -192,16 +268,14 @@
</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