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

feat: Simplified AOI Creation ✨ #1395

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
679a931
Move aoi files into same folder
AliceR Jan 20, 2025
cd094af
Run prettier formatting for aoi related files
AliceR Jan 20, 2025
c00f016
Remove unused file
AliceR Jan 20, 2025
e2a9df8
Remove draw controls and mbDraw references
AliceR Jan 20, 2025
251897d
Rename files and components for clarity
AliceR Jan 20, 2025
95f8d15
Export AOI as a single feature; display as AOI map layer
AliceR Jan 20, 2025
782b9d0
Run fitBounds as effect, when aoi changes
AliceR Jan 20, 2025
b357eec
Clean up and simplify AOI actions
AliceR Jan 20, 2025
f7e7759
Fix aoi id for uploaded geojson (for id: number)
AliceR Jan 20, 2025
5f6a101
Add hook for draw tools and drawing actions to control state
AliceR Jan 20, 2025
24709ef
Update analysis toolbar based on drawing state
AliceR Jan 20, 2025
ee4f78c
Use workaround for map offset to properly fit bounds
AliceR Jan 20, 2025
123069a
Remove unused file map-coords.tsx
AliceR Jan 21, 2025
d953cc5
Clean up dependency arrays to avoid unintended re-renders
AliceR Jan 21, 2025
5187504
Move aoi fly to effect into Aoi layer component
AliceR Jan 21, 2025
c3d8e15
Hide unrelated buttons while drawing
AliceR Jan 21, 2025
f0a4586
Clarify comment on fitBounds vs. flyTo
AliceR Jan 22, 2025
e062920
Use USWDS buttons in AOI Control
AliceR Jan 22, 2025
23dd145
Use custom styles for MapboxDraw layers
AliceR Jan 22, 2025
125b724
Upgrade mapbox-gl to ^3.9.3 (from 2.15.0)
AliceR Jan 23, 2025
e227a9d
Update typings after upgrading mapbox-gl > 3.5
AliceR Jan 23, 2025
a4f7383
Update projection options in parcel-resolver-veda
AliceR Jan 23, 2025
641463e
Fix type for projection parallels
AliceR Jan 27, 2025
87cf6c3
Update tooltips to indicate a new AOI will be created
AliceR Jan 27, 2025
6fbef33
Replace root.render in useThemedControl with React Portal
AliceR Jan 29, 2025
1b6f318
fix: Replace root.render in useThemedControl with React Portal (#1414)
AliceR Jan 29, 2025
045deb6
Remove unnecessary type casting
AliceR Jan 30, 2025
e40b575
Avoid type casting, mark metadata optional
AliceR Jan 30, 2025
d45e623
Update drawId type
AliceR Jan 30, 2025
9d0c45d
Refactor to remove unnecessary wrapper component
AliceR Feb 3, 2025
027d5c5
Extract draw tools into separate component
AliceR Feb 3, 2025
5e65894
fix: Upgrade mapbox-gl (#1400)
AliceR Feb 3, 2025
cd1bb5a
Use fitBounds instead of flyTo after upgrading mapbox-gl
AliceR Feb 3, 2025
58aa825
Catch uncaught exception in onStyleUpdate
AliceR Feb 3, 2025
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
134 changes: 0 additions & 134 deletions app/scripts/components/common/aoi/use-aoi-controls.ts

This file was deleted.

2 changes: 1 addition & 1 deletion app/scripts/components/common/blocks/block-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@

// zoom is not required, but if provided must be in the correct range.
const zoomError = zoom && (isNaN(zoom) || zoom < 0);
('- Invalid zoom. Use number greater than 0');

Check warning on line 90 in app/scripts/components/common/blocks/block-map.tsx

View workflow job for this annotation

GitHub Actions / lint

Expected an assignment or function call and instead saw an expression

const compareDateError =
compareDateTime &&
Expand Down Expand Up @@ -181,7 +181,7 @@
totalLayers = [...totalLayers, compareMapStaticData];
}
return totalLayers;
}, [layerId]);

Check warning on line 184 in app/scripts/components/common/blocks/block-map.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useMemo has a missing dependency: 'datasetLayers'. Either include it or remove the dependency array

const [layers, setLayers] = useState<VizDataset[]>(layersToFetch);

Expand All @@ -206,7 +206,7 @@
parallels: projectionParallels
});
return {
...projection,
...(projection as object),
id: projectionId
};
} else {
Expand Down
193 changes: 193 additions & 0 deletions app/scripts/components/common/map/controls/aoi/aoi-control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { Feature, Polygon } from 'geojson';
import styled, { css, useTheme } from 'styled-components';

import {
CollecticonPencil,
CollecticonTrashBin,
CollecticonUpload2
} from '@devseed-ui/collecticons';
import { Toolbar, ToolbarLabel, VerticalDivider } from '@devseed-ui/toolbar';
import { themeVal, glsp, disabled } from '@devseed-ui/theme-provider';

import useMaps from '../../hooks/use-maps';
import useAois from '../hooks/use-aois';
import useThemedControl from '../hooks/use-themed-control';
import { useDrawControl } from '../hooks/use-draw-control';
import CustomAoIModal from './custom-aoi-modal';
import PresetSelector from './preset-selector';

import { computeDrawStyles } from './style';
import { DrawTools } from './draw-tools';
import { TipToolbarIconButton } from '$components/common/tip-button';
import { Tip } from '$components/common/tip';
import { USWDSButton } from '$components/common/uswds';

const AnalysisToolbar = styled(Toolbar)<{ visuallyDisabled: boolean }>`
background-color: ${themeVal('color.surface')};
border-radius: ${themeVal('shape.rounded')};
padding: ${glsp(0.25)};
box-shadow: ${themeVal('boxShadow.elevationC')};

${({ visuallyDisabled }) =>
visuallyDisabled &&
css`
> * {
${disabled()}
pointer-events: none;
}
`}

${ToolbarLabel} {
text-transform: none;
}
`;

const FloatingBarSelf = styled.div`
position: absolute;
bottom: ${glsp()};
left: 50%;
transform: translateX(-50%);
z-index: 100;
`;

const AoiControl = ({ disableReason }: { disableReason?: React.ReactNode }) => {
const theme = useTheme();
const { main: mapboxMap } = useMaps();
const { isDrawing, setIsDrawing, aoi, updateAoi, aoiDeleteAll } = useAois();

const [aoiModalRevealed, setAoIModalRevealed] = useState(false);
const [selectedState, setSelectedState] = useState('');

const resetAoi = () => {
aoiDeleteAll();
setSelectedState('');
};

const onUploadConfirm = (features: Feature<Polygon>[]) => {
resetAoi(); // delete all previous AOIs and clear selections
setAoIModalRevealed(false); // close modal

updateAoi({ features });
};

const onPresetConfirm = (features: Feature<Polygon>[]) => {
aoiDeleteAll(); // delete all previous AOIs but keep preset selection

updateAoi({ features });
};

const onTrashClick = () => {
resetAoi();
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the point of having this? cant we just call resetAoi() directly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is originally a leftover artifact from old code, but I find it useful to keep event handlers in one place, for better organization and overview.
That said, I’m happy to inline it if you think it would improve clarity.


const [drawing, drawingIsValid] = useDrawControl({
mapboxMap: mapboxMap,
isDrawing,
styles: computeDrawStyles(theme)
});

const drawingActions = {
confirm() {
if (drawingIsValid) {
resetAoi(); // delete all previous AOIs and clear selections

updateAoi({ features: drawing }); // set the drawn AOI
setIsDrawing(false); // leave drawing mode
}
},
cancel() {
setIsDrawing(false); // leave drawing mode, nothing else
},
start() {
setIsDrawing(true); // start drawing
},
toggle() {
if (isDrawing) {
drawingActions.confirm(); // finish drawing
} else {
drawingActions.start(); // start drawing
}
}
};

const control = (
<>
<Tip disabled={!disableReason} content={disableReason} placement='bottom'>
<div>
<AnalysisToolbar
visuallyDisabled={!!disableReason}
size='small'
data-tour='analysis-tour'
>
{isDrawing ? (
<DrawTools {...{ drawingActions, drawingIsValid }} />
) : (
<>
<PresetSelector
selectedState={selectedState}
setSelectedState={setSelectedState}
onConfirm={onPresetConfirm}
resetPreset={resetAoi}
/>
<VerticalDivider />
<TipToolbarIconButton
tipContent='Draw a new area of interest'
tipProps={{ placement: 'bottom' }}
onClick={drawingActions.start}
>
<CollecticonPencil meaningful title='Draw new AOI' />
</TipToolbarIconButton>
<TipToolbarIconButton
tipContent='Upload a new area of interest'
tipProps={{ placement: 'bottom' }}
onClick={() => setAoIModalRevealed(true)}
>
<CollecticonUpload2 meaningful title='Upload geoJSON' />
</TipToolbarIconButton>
</>
)}
</AnalysisToolbar>
</div>
</Tip>

{!isDrawing && !!aoi && (
<FloatingBar container={mapboxMap.getContainer()}>
<USWDSButton
onClick={onTrashClick}
type='button'
base
size='small'
className='padding-top-05 padding-right-105 padding-bottom-05 padding-left-105'
>
<CollecticonTrashBin title='Delete area' />
Delete area
</USWDSButton>
</FloatingBar>
)}

<CustomAoIModal
revealed={aoiModalRevealed}
onConfirm={onUploadConfirm}
onCloseClick={() => setAoIModalRevealed(false)}
/>
</>
);

return useThemedControl(() => control, {
position: 'top-left'
});
};

interface FloatingBarProps {
children: React.ReactNode;
container: HTMLElement;
}

function FloatingBar(props: FloatingBarProps) {
const { container, children } = props;
return createPortal(<FloatingBarSelf>{children}</FloatingBarSelf>, container);
}

export default AoiControl;
10 changes: 6 additions & 4 deletions app/scripts/components/common/map/controls/aoi/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const aoisSerialized = atomWithUrlValueStability<string>({
initialValue: new URLSearchParams(window.location.search).get('aois') ?? '',
urlParam: 'aois',
hydrate: (v) => v ?? '',
dehydrate: (v) => v,
dehydrate: (v) => v
});

// Getter atom to get AoiS as GeoJSON features from the hash.
Expand Down Expand Up @@ -67,10 +67,12 @@ export const aoiDeleteAllAtom = atom(null, (get, set) => {

// Atom that tracks whether an AOI can be edited or not.
export const selectedForEditingAtom = atomWithUrlValueStability({
initialValue: (new URLSearchParams(window.location.search).get('selectedForEditing') !== 'false'),
initialValue:
new URLSearchParams(window.location.search).get('selectedForEditing') !==
'false',
urlParam: 'selectedForEditing',
hydrate: (value) => value !== 'false',
dehydrate: (value) => value ? 'true' : 'false'
dehydrate: (value) => (value ? 'true' : 'false')
});

export const isDrawingAtom = atom(false);
export const isDrawingAtom = atom(false);
Loading
Loading