Skip to content

Commit

Permalink
Merge branch 'feature/exploration' into feature/analysis-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfdsilva committed Oct 19, 2023
2 parents 3b7cae4 + ce2f577 commit c46bf5d
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 26 deletions.
6 changes: 3 additions & 3 deletions app/scripts/components/common/map/controls/aoi/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { atom } from "jotai";
import { atomWithLocation } from "jotai-location";
import { Polygon } from "geojson";
import { Feature, Polygon } from "geojson";
import { AoIFeature } from "../../types";
import { decodeAois, encodeAois } from "$utils/polygon-url";

Expand Down Expand Up @@ -28,7 +28,7 @@ export const aoisFeaturesAtom = atom<AoIFeature[]>((get) => {
// Setter atom to update AOIs geoometries, writing directly to the hash atom.
export const aoisUpdateGeometryAtom = atom(
null,
(get, set, updates: { id: string; geometry: Polygon }[]) => {
(get, set, updates: Feature<Polygon>[]) => {
let newFeatures = [...get(aoisFeaturesAtom)];
updates.forEach(({ id, geometry }) => {
const existingFeature = newFeatures.find((feature) => feature.id === id);
Expand All @@ -37,7 +37,7 @@ export const aoisUpdateGeometryAtom = atom(
} else {
const newFeature: AoIFeature = {
type: 'Feature',
id,
id: id as string,
geometry,
selected: true,
properties: {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { Feature, Polygon } from 'geojson';
import { CollecticonUpload2 } from '@devseed-ui/collecticons';
import { Button, createButtonStyles } from '@devseed-ui/button';
import styled from 'styled-components';
import { themeVal } from '@devseed-ui/theme-provider';
import useThemedControl from '../hooks/use-themed-control';
import CustomAoIModal from '../custom-aoi-modal';

const SelectorButton = styled(Button)`
&&& {
${createButtonStyles({ variation: 'primary-fill', fitting: 'skinny' })}
background-color: ${themeVal('color.surface')};
&:hover {
background-color: ${themeVal('color.surface')};
}
& path {
fill: ${themeVal('color.base')};
}
}
`;

interface CustomAoIProps {
onConfirm: (features: Feature<Polygon>[]) => void;
}

function CustomAoI({ onConfirm }: CustomAoIProps) {
const [aoiModalRevealed, setAoIModalRevealed] = useState(false);
return (
<>
<SelectorButton onClick={() => setAoIModalRevealed(true)}>
<CollecticonUpload2 title='Upload geoJSON' meaningful />
</SelectorButton>
<CustomAoIModal
revealed={aoiModalRevealed}
onConfirm={onConfirm}
onCloseClick={() => setAoIModalRevealed(false)}
/>
</>
);
}

export default function CustomAoIControl(props: CustomAoIProps) {
useThemedControl(() => <CustomAoI {...props} />, {
position: 'top-left'
});
return null;
}
39 changes: 26 additions & 13 deletions app/scripts/components/common/map/controls/aoi/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';
import React, { useEffect } from 'react';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { createGlobalStyle } from 'styled-components';
import { useAtomValue } from 'jotai';
import { useRef } from 'react';
import { useControl } from 'react-map-gl';
import { Feature, Polygon } from 'geojson';
import useAois from '../hooks/use-aois';
import { aoisFeaturesAtom } from './atoms';
import { encodeAois } from '$utils/polygon-url';

type DrawControlProps = {
onCreate?: (evt: { features: object[] }) => void;
onUpdate?: (evt: { features: object[]; action: string }) => void;
onDelete?: (evt: { features: object[] }) => void;
onSelectionChange?: (evt: { selectedFeatures: object[] }) => void;
customFeatures: Feature<Polygon>[];
} & MapboxDraw.DrawOptions;

const Css = createGlobalStyle`
Expand All @@ -23,17 +23,30 @@ const Css = createGlobalStyle`
export default function DrawControl(props: DrawControlProps) {
const control = useRef<MapboxDraw>();
const aoisFeatures = useAtomValue(aoisFeaturesAtom);
const { customFeatures } = props;

const { onUpdate, onDelete, onSelectionChange } = useAois();

const serializedCustomFeatures = encodeAois(customFeatures);
useEffect(() => {
if (!customFeatures.length) return;
control.current?.add({
type: 'FeatureCollection',
features: customFeatures
});
// Look at serialized version to only update when the features change
}, [serializedCustomFeatures]);

useControl<MapboxDraw>(
() => {
control.current = new MapboxDraw(props);
return control.current;
},
({ map }: { map: any }) => {
map.on('draw.create', props.onCreate);
map.on('draw.update', props.onUpdate);
map.on('draw.delete', props.onDelete);
map.on('draw.selectionchange', props.onSelectionChange);
map.on('draw.create', onUpdate);
map.on('draw.update', onUpdate);
map.on('draw.delete', onDelete);
map.on('draw.selectionchange', onSelectionChange);
map.on('load', () => {
control.current?.set({
type: 'FeatureCollection',
Expand All @@ -42,10 +55,10 @@ export default function DrawControl(props: DrawControlProps) {
});
},
({ map }: { map: any }) => {
map.off('draw.create', props.onCreate);
map.off('draw.update', props.onUpdate);
map.off('draw.delete', props.onDelete);
map.off('draw.selectionchange', props.onSelectionChange);
map.off('draw.create', onUpdate);
map.off('draw.update', onUpdate);
map.off('draw.delete', onDelete);
map.off('draw.selectionchange', onSelectionChange);
},
{
position: 'top-left'
Expand Down
220 changes: 220 additions & 0 deletions app/scripts/components/common/map/controls/custom-aoi-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import React, { useCallback, useEffect, useRef } from 'react';
import styled from 'styled-components';
import { Feature, Polygon } from 'geojson';
import { Modal, ModalHeadline, ModalFooter } from '@devseed-ui/modal';
import { Heading, Subtitle } from '@devseed-ui/typography';

import { Button } from '@devseed-ui/button';
import {
glsp,
listReset,
themeVal,
visuallyHidden
} from '@devseed-ui/theme-provider';
import {
CollecticonArrowUp,
CollecticonTickSmall,
CollecticonXmarkSmall,
CollecticonCircleExclamation,
CollecticonCircleTick,
CollecticonCircleInformation
} from '@devseed-ui/collecticons';
import useCustomAoI, { acceptExtensions } from './hooks/use-custom-aoi';
import { variableGlsp, variableProseVSpace } from '$styles/variable-utils';

const UploadFileModalFooter = styled(ModalFooter)`
display: flex;
justify-content: right;
flex-flow: row nowrap;
gap: ${variableGlsp(0.25)};
`;

const ModalBodyInner = styled.div`
display: flex;
flex-flow: column nowrap;
gap: ${variableGlsp()};
`;

const UploadFileIntro = styled.div`
display: flex;
flex-flow: column nowrap;
gap: ${variableProseVSpace()};
`;

const FileUpload = styled.div`
display: flex;
flex-flow: nowrap;
align-items: center;
gap: ${variableGlsp(0.5)};
${Button} {
flex-shrink: 0;
}
${Subtitle} {
overflow-wrap: anywhere;
}
`;

const FileInput = styled.input`
${visuallyHidden()}
`;

const UploadInformation = styled.div`
padding: ${variableGlsp()};
background: ${themeVal('color.base-50')};
box-shadow: ${themeVal('boxShadow.inset')};
border-radius: ${themeVal('shape.rounded')};
`;

const UploadListInfo = styled.ul`
${listReset()}
display: flex;
flex-flow: column nowrap;
gap: ${glsp(0.25)};
li {
display: flex;
flex-flow: row nowrap;
gap: ${glsp(0.5)};
align-items: top;
> svg {
flex-shrink: 0;
margin-top: ${glsp(0.25)};
}
}
`;

const UploadInfoItemSuccess = styled.li`
color: ${themeVal('color.success')};
`;

const UploadInfoItemWarnings = styled.li`
color: ${themeVal('color.info')};
`;

const UploadInfoItemError = styled.li`
color: ${themeVal('color.danger')};
`;

interface CustomAoIModalProps {
revealed: boolean;
onCloseClick: () => void;
onConfirm: (features: Feature<Polygon>[]) => void;
}

export default function CustomAoIModal({
revealed,
onCloseClick,
onConfirm
}: CustomAoIModalProps) {
const {
features,
onUploadFile,
uploadFileError,
uploadFileWarnings,
fileInfo,
reset
} = useCustomAoI();
const fileInputRef = useRef<HTMLInputElement>(null);

const onUploadClick = useCallback(() => {
if (fileInputRef.current) fileInputRef.current.click();
}, []);

const onConfirmClick = useCallback(() => {
if (!features) return;
onConfirm(features);
onCloseClick();
}, [features, onConfirm, onCloseClick]);

useEffect(() => {
if (revealed) reset();
}, [revealed, reset]);

const hasInfo = !!uploadFileWarnings.length || !!features || uploadFileError;

return (
<Modal
id='aoiUpload'
size='medium'
revealed={revealed}
onCloseClick={onCloseClick}
renderHeadline={() => (
<ModalHeadline>
<h2>Upload custom area</h2>
</ModalHeadline>
)}
content={
<ModalBodyInner>
<UploadFileIntro>
<p>
You can upload a zipped shapefile (*.zip) or a GeoJSON file
(*.json, *.geojson) to define a custom area of interest.
</p>
<FileUpload>
<Button variation='base-outline' onClick={onUploadClick}>
<CollecticonArrowUp />
Upload file...
</Button>
{fileInfo && (
<Subtitle as='p'>
File: {fileInfo.name} ({fileInfo.type}).
</Subtitle>
)}
<FileInput
type='file'
onChange={onUploadFile}
accept={acceptExtensions}
ref={fileInputRef}
/>
</FileUpload>
</UploadFileIntro>

{hasInfo && (
<UploadInformation>
<Heading hidden>Upload information</Heading>

<UploadListInfo>
{uploadFileWarnings.map((w) => (
<UploadInfoItemWarnings key={w}>
<CollecticonCircleInformation />
<span>{w}</span>
</UploadInfoItemWarnings>
))}
{features && (
<UploadInfoItemSuccess>
<CollecticonCircleTick />
<span>File uploaded successfully.</span>
</UploadInfoItemSuccess>
)}
{uploadFileError && (
<UploadInfoItemError>
<CollecticonCircleExclamation /> {uploadFileError}
</UploadInfoItemError>
)}
</UploadListInfo>
</UploadInformation>
)}
</ModalBodyInner>
}
renderFooter={() => (
<UploadFileModalFooter>
<Button variation='base-text' onClick={onCloseClick}>
<CollecticonXmarkSmall />
Cancel
</Button>
<Button
variation='primary-fill'
disabled={!features}
onClick={onConfirmClick}
>
<CollecticonTickSmall />
Add area
</Button>
</UploadFileModalFooter>
)}
/>
);
}
Loading

0 comments on commit c46bf5d

Please sign in to comment.