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 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 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
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
d45e623
Update drawId type
AliceR Jan 30, 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.

232 changes: 232 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,232 @@
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { Feature, Polygon } from 'geojson';
import styled, { css, useTheme } from 'styled-components';

import {
CollecticonTick,
CollecticonXmark,
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 { TipToolbarIconButton } from '$components/common/tip-button';
import { Tip } from '$components/common/tip';
import { USWDSButton, USWDSButtonGroup } 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;
`;

function AoiControl({
mapboxMap,
disableReason
}: {
mapboxMap: mapboxgl.Map;
disableReason?: React.ReactNode;
}) {
const theme = useTheme();
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?


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

return (
<>
<Tip disabled={!disableReason} content={disableReason} placement='bottom'>
<div>
<AnalysisToolbar
visuallyDisabled={!!disableReason}
size='small'
data-tour='analysis-tour'
>
{isDrawing ? (
<USWDSButtonGroup className='margin-neg-05 margin-right-0'>
<USWDSButton
onClick={drawingActions.confirm}
type='button'
inverse
disabled={!drawingIsValid}
size='small'
className='padding-top-05 padding-right-105 padding-bottom-05 padding-left-105'
>
<CollecticonTick aria-hidden='true' />
Confirm Area
</USWDSButton>
<USWDSButton
onClick={drawingActions.cancel}
type='button'
base
size='small'
className='padding-top-05 padding-right-105 padding-bottom-05 padding-left-105'
>
<CollecticonXmark aria-hidden='true' />
Cancel
</USWDSButton>
</USWDSButtonGroup>
) : (
<>
<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)}
/>
</>
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

⛏️ 🧹 : Further cleanup can be done by breaking these out for example: drawingMode and nonDrawingMode


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

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

export default function Wrapper({
disableReason
}: {
disableReason?: React.ReactNode;
}) {
const { main } = useMaps();

const control = useThemedControl(
() => <AoiControl mapboxMap={main} disableReason={disableReason} />,
{
position: 'top-left'
}
);

return control;
}
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 this wrapper abstraction and is it really needed or can it just be a part of the AoiControl component? If we keep it, can we give it a more helpful name like AoiControlWithThemedControl?

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