Skip to content

Commit

Permalink
872 analysis preset (#921)
Browse files Browse the repository at this point in the history
  • Loading branch information
j08lue authored Apr 24, 2024
2 parents a841ee0 + 19bf62d commit 4c98710
Show file tree
Hide file tree
Showing 64 changed files with 911 additions and 110 deletions.
9 changes: 3 additions & 6 deletions app/scripts/components/analysis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { endOfDay, startOfDay, format } from 'date-fns';
import { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson';
import combine from '@turf/combine';
import { userTzDate2utcString } from '$utils/date';
import { fixAntimeridian } from '$utils/antimeridian';

Expand Down Expand Up @@ -84,15 +85,11 @@ export function multiPolygonToPolygons(feature: Feature<MultiPolygon>) {
export function combineFeatureCollection(
featureCollection: FeatureCollection<Polygon>
): Feature<MultiPolygon> {
const combined = combine(featureCollection);
return {
type: 'Feature',
properties: {},
geometry: {
type: 'MultiPolygon',
coordinates: [
featureCollection.features.map((f) => f.geometry.coordinates[0])
]
}
geometry: combined.features[0].geometry as MultiPolygon
};
}

Expand Down
121 changes: 111 additions & 10 deletions app/scripts/components/common/map/controls/aoi/custom-aoi-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Feature, Polygon } from 'geojson';
import styled, { css } from 'styled-components';
import { useSetAtom } from 'jotai';
import bbox from '@turf/bbox';
import centroid from '@turf/centroid';
import {
CollecticonPencil,
CollecticonTrashBin,
Expand All @@ -13,20 +14,23 @@ import { Toolbar, ToolbarLabel, VerticalDivider } from '@devseed-ui/toolbar';
import { Button } from '@devseed-ui/button';
import { themeVal, glsp, disabled } from '@devseed-ui/theme-provider';

import { AllGeoJSON } from '@turf/helpers';
import useMaps from '../../hooks/use-maps';
import useAois from '../hooks/use-aois';
import useThemedControl from '../hooks/use-themed-control';
import CustomAoIModal from './custom-aoi-modal';
import { aoiDeleteAllAtom } from './atoms';

import PresetSelector from './preset-selector';
import { TipToolbarIconButton } from '$components/common/tip-button';
import { Tip } from '$components/common/tip';
import { getZoomFromBbox } from '$components/common/map/utils';
import { ShortcutCode } from '$styles/shortcut-code';

const AnalysisToolbar = styled(Toolbar)<{ visuallyDisabled: boolean }>`
background-color: ${themeVal('color.surface')};
border-radius: ${themeVal('shape.rounded')};
padding: ${glsp(0, 0.5)};
padding: ${glsp(0.25)};
box-shadow: ${themeVal('boxShadow.elevationC')};
${({ visuallyDisabled }) =>
Expand Down Expand Up @@ -59,6 +63,9 @@ function CustomAoI({
disableReason?: React.ReactNode;
}) {
const [aoiModalRevealed, setAoIModalRevealed] = useState(false);
const [selectedState, setSelectedState] = useState('');
const [presetIds, setPresetIds] = useState([]);
const [fileUploadedIds, setFileUplaodedIds] = useState([]);

const { onUpdate, isDrawing, setIsDrawing, features } = useAois();
const aoiDeleteAll = useSetAtom(aoiDeleteAllAtom);
Expand All @@ -74,30 +81,120 @@ function CustomAoI({
};
}, []);

const onConfirm = (features: Feature<Polygon>[]) => {
const resetAoisOnMap = useCallback(() => {
const mbDraw = map?._drawControl;
if (!mbDraw) return;
mbDraw.deleteAll();
aoiDeleteAll();
}, [aoiDeleteAll]);

const resetForPresetSelect = useCallback(() => {
resetAoisOnMap();
setFileUplaodedIds([]);
},[resetAoisOnMap]);

const resetForFileUploaded = useCallback(()=> {
resetAoisOnMap();
setSelectedState('');
setPresetIds([]);
},[resetAoisOnMap]);

const resetForEmptyState = useCallback(()=> {
resetAoisOnMap();
setSelectedState('');
setPresetIds([]);
setFileUplaodedIds([]);
},[resetAoisOnMap]);

const resetForDrawingAoi = useCallback(() => {
const mbDraw = map?._drawControl;
if (!mbDraw) return;

if (fileUploadedIds.length) {
mbDraw.changeMode('simple_select', {
featureIds: fileUploadedIds
});
mbDraw.trash();
}

if (presetIds.length) {
mbDraw.changeMode('simple_select', {
featureIds: presetIds
});
mbDraw.trash();
}
setFileUplaodedIds([]);
setPresetIds([]);
setSelectedState('');
},[presetIds, fileUploadedIds]);

const onConfirm = useCallback((features: Feature<Polygon>[]) => {
const mbDraw = map?._drawControl;
setAoIModalRevealed(false);
if (!mbDraw) return;
resetForFileUploaded();
onUpdate({ features });
const fc = {
type: 'FeatureCollection',
features
};
const bounds = bbox(fc);
const center = centroid(fc as AllGeoJSON).geometry.coordinates;
map.flyTo({
center,
zoom: getZoomFromBbox(bounds)
});
const addedAoisId = mbDraw.add(fc);
mbDraw.changeMode('simple_select', {
featureIds: addedAoisId
});
setFileUplaodedIds(addedAoisId);
},[map, onUpdate, resetForFileUploaded]);

const onPresetConfirm = useCallback((features: Feature<Polygon>[]) => {
const mbDraw = map?._drawControl;
if (!mbDraw) return;
resetForPresetSelect();
onUpdate({ features });
const fc = {
type: 'FeatureCollection',
features
};
map.fitBounds(bbox(fc), { padding: 20 });
mbDraw.add(fc);
};
const bounds = bbox(fc);
const center = centroid(fc as AllGeoJSON).geometry.coordinates;
map.flyTo({
center,
zoom: getZoomFromBbox(bounds)
});
const pids = mbDraw.add(fc);
setPresetIds(pids);
mbDraw.changeMode('simple_select', {
featureIds: pids
});

},[map, onUpdate, resetForPresetSelect]);

const toggleDrawing = useCallback(() => {
const mbDraw = map?._drawControl;
if (!mbDraw) return;
resetForDrawingAoi();
setIsDrawing(!isDrawing);
}, [map, isDrawing, setIsDrawing, resetForDrawingAoi]);

const onTrashClick = useCallback(() => {
// We need to programmatically access the mapbox draw trash method which
// will do different things depending on the selected mode.
const mbDraw = map?._drawControl;
if (!mbDraw) return;

setSelectedState('');
setPresetIds([]);
setFileUplaodedIds([]);
// This is a peculiar situation:
// If we are in direct select (to select/add vertices) but not vertex is
// selected, the trash method doesn't do anything. So, in this case, we
// trigger the delete for the whole feature.
const selectedFeatures = mbDraw.getSelected().features;
const selectedFeatures = mbDraw.getSelected()?.features;
if (
mbDraw.getMode() === 'direct_select' &&
selectedFeatures.length &&
Expand All @@ -108,7 +205,6 @@ function CustomAoI({
featureIds: selectedFeatures.map((f) => f.id)
});
}

// If nothing selected, delete all.
if (features.every((f) => !f.selected)) {
mbDraw.deleteAll();
Expand All @@ -120,7 +216,7 @@ function CustomAoI({
mbDraw.trash();
}, [features, aoiDeleteAll, map]);

const isAreaSelected = !!map?._drawControl.getSelected().features.length;
const isAreaSelected = !!map?._drawControl?.getSelected().features.length;
const isPointSelected =
!!map?._drawControl.getSelectedPoints().features.length;
const hasFeatures = !!features.length;
Expand All @@ -134,13 +230,18 @@ function CustomAoI({
size='small'
data-tour='analysis-tour'
>
<ToolbarLabel>Analysis</ToolbarLabel>
<PresetSelector
selectedState={selectedState}
setSelectedState={setSelectedState}
onConfirm={onPresetConfirm}
resetPreset={resetForEmptyState}
/>
<VerticalDivider />
<TipToolbarIconButton
tipContent='Draw an area of interest'
tipProps={{ placement: 'bottom' }}
active={isDrawing}
onClick={() => setIsDrawing(!isDrawing)}
onClick={toggleDrawing}
>
<CollecticonPencil meaningful title='Draw AOI' />
</TipToolbarIconButton>
Expand Down
164 changes: 164 additions & 0 deletions app/scripts/components/common/map/controls/aoi/preset-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { useEffect } from 'react';
import styled, { css, keyframes } from 'styled-components';
import { Feature, Polygon } from 'geojson';
import { Button } from '@devseed-ui/button';
import {
CollecticonDiscXmark,
CollecticonArrowSpinCcw,
} from '@devseed-ui/collecticons';
import usePresetAOI from '../hooks/use-preset-aoi';

const analysisStatesPreset = ["Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"District of Columbia",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Puerto Rico",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming"
].map(e => ({label: e, value: e}));

// Disabling no mutating rule since we are mutating the copy
// eslint-disable-next-line fp/no-mutating-methods
const sortedPresets = [...analysisStatesPreset].sort((a,b) => {
return a.label.localeCompare(b.label);
});

const selectorHeight = '25px';

const SelectorWrapper = styled.div`
position: relative;
`;

const PresetSelect = styled.select`
max-width: 200px;
height: ${selectorHeight};
`;

const SelectorSubAction = css`
position: absolute;
top: 0;
right: 10px;
height: ${selectorHeight};
`;

const spinAnimation = keyframes`
from {
transform:rotate(360deg);
}
to {
transform:rotate(0deg);
}
`;

const CancelButton = styled(Button)`
${SelectorSubAction}
`;

const LoadingWrapper = styled.div`
${SelectorSubAction}
display: flex;
align-items: center;
right: 18px;
`;

const AnimatingCollecticonArrowSpinCcw = styled(CollecticonArrowSpinCcw)`
animation: ${spinAnimation} 1s infinite linear;
`;

export default function PresetSelector({ selectedState, setSelectedState, onConfirm, resetPreset }: {
selectedState: string,
setSelectedState: (state: string) => void,
onConfirm: (features: Feature<Polygon>[]) => void,
resetPreset: () => void
}) {
const { features, isLoading } = usePresetAOI(selectedState);

useEffect(() => {
if (features?.length) onConfirm(features);

// Excluding onConfirm from the dependencies array to prevent an infinite loop:
// onConfirm depends on the Map instance, and invoking it modifies the Map,
// which can re-trigger this effect if included as a dependency.
// eslint-disable-next-line react-hooks/exhaustive-deps
},[features]);

return (
<SelectorWrapper>
<PresetSelect
id='preset-selector'
name='presetSelector'
value={selectedState}
onChange={(e) => setSelectedState(e.target.value)}
>
<option> Analyze an area </option>
<optgroup label='Country' />
<option value='United States'> United States</option>
<option value='United States (Contiguous)'> Contiguous United States (CONUS)</option>
<optgroup label='State' />
{sortedPresets.map(e => {
return (<option key={`${e.value}-option-analysis`} value={e.value}>{e.label}</option>);
})}
</PresetSelect>
{(selectedState && !isLoading) &&
<CancelButton
fitting='skinny'
onClick={() => {
resetPreset();
}}
>
<CollecticonDiscXmark meaningful width='12px' height='12px' title='Clear preset' />
</CancelButton>}
{isLoading &&
<LoadingWrapper>
<AnimatingCollecticonArrowSpinCcw meaningful width='12px' height='12px' title='Loading' />
</LoadingWrapper>}
</SelectorWrapper>
);


}
Loading

0 comments on commit 4c98710

Please sign in to comment.