-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: main
Are you sure you want to change the base?
Changes from 22 commits
679a931
cd094af
c00f016
e2a9df8
251897d
95f8d15
782b9d0
b357eec
f7e7759
5f6a101
24709ef
ee4f78c
123069a
d953cc5
5187504
c3d8e15
f0a4586
e062920
23dd145
87cf6c3
6fbef33
1b6f318
d45e623
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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(); | ||
}; | ||
|
||
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)} | ||
/> | ||
</> | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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?