From 465739e24fa43337fb222ca6377f4ddab18a6c07 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 17 Oct 2023 14:47:36 +0200 Subject: [PATCH 1/3] Set up trigger button and modal --- .../analysis/define/aoi-selector.tsx | 2 +- .../common/map/controls/aoi/atoms.ts | 6 +- .../map/controls/aoi/custom-aoi-control.tsx | 52 ++++ .../common/map/controls/custom-aoi-modal.tsx | 221 +++++++++++++++++ .../common/map/controls/hooks/use-aois.ts | 15 +- .../map/controls/hooks/use-custom-aoi.tsx | 226 ++++++++++++++++++ app/scripts/components/common/map/utils.ts | 27 +++ .../exploration/components/map/index.tsx | 10 +- 8 files changed, 549 insertions(+), 10 deletions(-) create mode 100644 app/scripts/components/common/map/controls/aoi/custom-aoi-control.tsx create mode 100644 app/scripts/components/common/map/controls/custom-aoi-modal.tsx create mode 100644 app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 2915c87c4..5c36a75d9 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -120,7 +120,7 @@ export default function AoiSelector({ const [aoiModalRevealed, setAoIModalRevealed] = useState(false); return ( - + ((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[]) => { let newFeatures = [...get(aoisFeaturesAtom)]; updates.forEach(({ id, geometry }) => { const existingFeature = newFeatures.find((feature) => feature.id === id); @@ -37,7 +37,7 @@ export const aoisUpdateGeometryAtom = atom( } else { const newFeature: AoIFeature = { type: 'Feature', - id, + id: id as string, geometry, selected: true, properties: {} diff --git a/app/scripts/components/common/map/controls/aoi/custom-aoi-control.tsx b/app/scripts/components/common/map/controls/aoi/custom-aoi-control.tsx new file mode 100644 index 000000000..623e55cc7 --- /dev/null +++ b/app/scripts/components/common/map/controls/aoi/custom-aoi-control.tsx @@ -0,0 +1,52 @@ +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[]) => void; +} + +function CustomAoI({ onConfirm }: CustomAoIProps) { + const [aoiModalRevealed, setAoIModalRevealed] = useState(false); + return ( + <> + + setAoIModalRevealed(true)} + /> + + setAoIModalRevealed(false)} + /> + + ); +} + +export default function CustomAoIControl(props: CustomAoIProps) { + useThemedControl(() => , { + position: 'top-left' + }); + return null; +} diff --git a/app/scripts/components/common/map/controls/custom-aoi-modal.tsx b/app/scripts/components/common/map/controls/custom-aoi-modal.tsx new file mode 100644 index 000000000..449b1c701 --- /dev/null +++ b/app/scripts/components/common/map/controls/custom-aoi-modal.tsx @@ -0,0 +1,221 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { Feature, FeatureCollection, 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[]) => void; +} + +export default function CustomAoIModal({ + revealed, + onCloseClick, + onConfirm +}: CustomAoIModalProps) { + const { + features, + onUploadFile, + uploadFileError, + uploadFileWarnings, + fileInfo, + reset + } = useCustomAoI(); + console.log(features) + const fileInputRef = useRef(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 ( + ( + +

Upload custom area

+
+ )} + content={ + + +

+ You can upload a zipped shapefile (*.zip) or a GeoJSON file + (*.json, *.geojson) to define a custom area of interest. +

+ + + {fileInfo && ( + + File: {fileInfo.name} ({fileInfo.type}). + + )} + + +
+ + {hasInfo && ( + + + + + {uploadFileWarnings.map((w) => ( + + + {w} + + ))} + {features && ( + + + File uploaded successfully. + + )} + {uploadFileError && ( + + {uploadFileError} + + )} + + + )} +
+ } + renderFooter={() => ( + + + + + )} + /> + ); +} diff --git a/app/scripts/components/common/map/controls/hooks/use-aois.ts b/app/scripts/components/common/map/controls/hooks/use-aois.ts index 3032f5a48..ece9a9e70 100644 --- a/app/scripts/components/common/map/controls/hooks/use-aois.ts +++ b/app/scripts/components/common/map/controls/hooks/use-aois.ts @@ -1,6 +1,6 @@ import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; -import { Polygon } from 'geojson'; +import { Feature, Polygon } from 'geojson'; import { toAoIid } from '../../utils'; import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '../aoi/atoms'; @@ -8,15 +8,21 @@ export default function useAois() { const features = useAtomValue(aoisFeaturesAtom); const aoisUpdateGeometry = useSetAtom(aoisUpdateGeometryAtom); + const update = useCallback( + (features: Feature[]) => { + aoisUpdateGeometry(features); + } + , [aoisUpdateGeometry] + ); const onUpdate = useCallback( (e) => { const updates = e.features.map((f) => ({ id: toAoIid(f.id), geometry: f.geometry as Polygon })); - aoisUpdateGeometry(updates); + update(updates); }, - [aoisUpdateGeometry] + [update] ); const aoiDelete = useSetAtom(aoisDeleteAtom); @@ -36,5 +42,6 @@ export default function useAois() { }, [aoiSetSelected] ); - return { features, onUpdate, onDelete, onSelectionChange }; + + return { features, update, onUpdate, onDelete, onSelectionChange }; } diff --git a/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx b/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx new file mode 100644 index 000000000..eb2f34551 --- /dev/null +++ b/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx @@ -0,0 +1,226 @@ +import { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import shp from 'shpjs'; +import simplify from '@turf/simplify'; + +import { multiPolygonToPolygons } from '../../utils'; +import { round } from '$utils/format'; + +const extensions = ['geojson', 'json', 'zip']; +export const acceptExtensions = extensions.map((ext) => `.${ext}`).join(', '); + +export interface FileInfo { + name: string; + extension: string; + type: 'Shapefile' | 'GeoJSON'; +} + +function getNumPoints(feature: Feature): number { + return feature.geometry.coordinates.reduce((acc, current) => { + return acc + current.length; + }, 0); +} + +function useCustomAoI() { + const [fileInfo, setFileInfo] = useState(null); + const [uploadFileError, setUploadFileError] = useState(null); + const [uploadFileWarnings, setUploadFileWarnings] = useState([]); + const reader = useRef(); + const [features, setFeatures] = + useState[] | null>(null); + + useEffect(() => { + reader.current = new FileReader(); + + const setError = (error: string) => { + setUploadFileError(error); + setFeatures(null); + setUploadFileWarnings([]); + }; + + const onLoad = async () => { + if (!reader.current) return; + + let geojson; + if (typeof reader.current.result === 'string') { + const rawGeoJSON = reader.current.result; + if (!rawGeoJSON) { + setError('Error uploading file.'); + return; + } + try { + geojson = JSON.parse(rawGeoJSON as string); + } catch (e) { + setError('Error uploading file: invalid JSON'); + return; + } + } else { + try { + geojson = await shp(reader.current.result); + } catch (e) { + setError(`Error uploading file: invalid Shapefile (${e.message})`); + return; + } + } + + if (!geojson?.features?.length) { + setError('Error uploading file: Invalid GeoJSON'); + return; + } + + let warnings: string[] = []; + + if ( + geojson.features.some( + (feature) => + !['MultiPolygon', 'Polygon'].includes(feature.geometry.type) + ) + ) { + setError( + 'Wrong geometry type. Only polygons or multi polygons are accepted.' + ); + return; + } + + const features: Feature[] = geojson.features.reduce( + (acc, feature: Feature) => { + if (feature.geometry.type === 'MultiPolygon') { + return acc.concat( + multiPolygonToPolygons(feature as Feature) + ); + } + + return acc.concat(feature); + }, + [] + ); + + if (features.length > 200) { + setError('Only files with up to 200 polygons are accepted.'); + return; + } + + // Simplify features; + const originalTotalFeaturePoints = features.reduce( + (acc, f) => acc + getNumPoints(f), + 0 + ); + let numPoints = originalTotalFeaturePoints; + let tolerance = 0.001; + + // Remove holes from polygons as they're not supported. + let polygonHasRings = false; + let simplifiedFeatures = features.map>((feature) => { + if (feature.geometry.coordinates.length > 1) { + polygonHasRings = true; + return { + ...feature, + geometry: { + type: 'Polygon', + coordinates: [feature.geometry.coordinates[0]] + } + }; + } + + return feature; + }); + + if (polygonHasRings) { + warnings = [ + ...warnings, + 'Polygons with rings are not supported and were simplified to remove them' + ]; + } + + // If we allow up to 200 polygons and each polygon needs 4 points, we need + // at least 800, give an additional buffer and we get 1000. + while (numPoints > 1000 && tolerance < 5) { + simplifiedFeatures = simplifiedFeatures.map((feature) => + simplify(feature, { tolerance }) + ); + numPoints = simplifiedFeatures.reduce( + (acc, f) => acc + getNumPoints(f), + 0 + ); + tolerance = Math.min(tolerance * 1.8, 5); + } + + if (originalTotalFeaturePoints !== numPoints) { + warnings = [ + ...warnings, + `The geometry has been simplified (${round( + (1 - numPoints / originalTotalFeaturePoints) * 100 + )} % less).` + ]; + } + + setUploadFileWarnings(warnings); + setUploadFileError(null); + setFeatures( + simplifiedFeatures.map((feat, i) => ({ + id: `aoi-${i}`, + ...feat + })) + ); + }; + + const onError = () => { + setError('Error uploading file'); + }; + + reader.current.addEventListener('load', onLoad); + reader.current.addEventListener('error', onError); + + return () => { + if (!reader.current) return; + reader.current.removeEventListener('load', onLoad); + reader.current.removeEventListener('error', onError); + }; + }, []); + + const onUploadFile = useCallback((event) => { + if (!reader.current) return; + + const file = event.target.files[0]; + if (!file) return; + + const [, extension] = file.name.match(/^.*\.(json|geojson|zip)$/i) ?? []; + + if (!extensions.includes(extension)) { + setUploadFileError( + 'Wrong file type. Only zipped shapefiles and geojson files are accepted.' + ); + return; + } + + setFileInfo({ + name: file.name, + extension, + type: extension === 'zip' ? 'Shapefile' : 'GeoJSON' + }); + + if (extension === 'zip') { + reader.current.readAsArrayBuffer(file); + } else if (extension === 'json' || extension === 'geojson') { + reader.current.readAsText(file); + } + }, []); + + const reset = useCallback(() => { + setFeatures(null); + setUploadFileWarnings([]); + setUploadFileError(null); + setFileInfo(null); + }, []); + + return { + features, + onUploadFile, + uploadFileError, + uploadFileWarnings, + fileInfo, + reset + }; +} + +export default useCustomAoI; diff --git a/app/scripts/components/common/map/utils.ts b/app/scripts/components/common/map/utils.ts index 498d65a8a..2e29a3e29 100644 --- a/app/scripts/components/common/map/utils.ts +++ b/app/scripts/components/common/map/utils.ts @@ -2,6 +2,7 @@ import axios, { Method } from 'axios'; import { Map as MapboxMap } from 'mapbox-gl'; import { MapRef } from 'react-map-gl'; import { endOfDay, startOfDay } from 'date-fns'; +import { Feature, MultiPolygon, Polygon } from 'geojson'; import { DatasetDatumFn, DatasetDatumFnResolverBag, @@ -183,4 +184,30 @@ export function resolveConfigFunctions( export function toAoIid(drawId: string) { return drawId.slice(-6); +} + + +/** + * Converts a MultiPolygon to a Feature Collection of polygons. + * + * @param feature MultiPolygon feature + * + * @see combineFeatureCollection() for opposite + * + * @returns Feature Collection of Polygons + */ +export function multiPolygonToPolygons(feature: Feature) { + const polygons = feature.geometry.coordinates.map( + (coordinates) => + ({ + type: 'Feature', + properties: { ...feature.properties }, + geometry: { + type: 'Polygon', + coordinates: coordinates + } + } as Feature) + ); + + return polygons; } \ No newline at end of file diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 54eef4fe3..23a14d39c 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -20,6 +20,8 @@ import { projectionDefault } from '$components/common/map/controls/map-options/p import { useBasemap } from '$components/common/map/controls/hooks/use-basemap'; import DrawControl from '$components/common/map/controls/aoi'; import useAois from '$components/common/map/controls/hooks/use-aois'; +import CustomAoIControl from '$components/common/map/controls/aoi/custom-aoi-control'; +import { Feature, Polygon } from 'geojson'; export function ExplorationMap(props: { comparing: boolean }) { const [projection, setProjection] = useState(projectionDefault); @@ -48,8 +50,11 @@ export function ExplorationMap(props: { comparing: boolean }) { .slice() .reverse(); - const { onUpdate, onDelete, onSelectionChange } = useAois(); - // console.log(features); + const { update, onUpdate, onDelete, onSelectionChange } = useAois(); + + const onCustomAoIConfirm = (features: Feature[]) => { + update(features); + }; return ( @@ -83,6 +88,7 @@ export function ExplorationMap(props: { comparing: boolean }) { onDelete={onDelete} onSelectionChange={onSelectionChange} /> + Date: Tue, 17 Oct 2023 15:12:45 +0200 Subject: [PATCH 2/3] Use custom AoI --- .../common/map/controls/aoi/index.tsx | 39 ++++++++++++------- .../common/map/controls/custom-aoi-modal.tsx | 3 +- .../common/map/controls/hooks/use-aois.ts | 11 ++++-- .../map/controls/hooks/use-custom-aoi.tsx | 7 ++-- .../exploration/components/map/index.tsx | 14 +++---- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app/scripts/components/common/map/controls/aoi/index.tsx b/app/scripts/components/common/map/controls/aoi/index.tsx index 4e1d968c3..66dd6f471 100644 --- a/app/scripts/components/common/map/controls/aoi/index.tsx +++ b/app/scripts/components/common/map/controls/aoi/index.tsx @@ -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[]; } & MapboxDraw.DrawOptions; const Css = createGlobalStyle` @@ -23,6 +23,19 @@ const Css = createGlobalStyle` export default function DrawControl(props: DrawControlProps) { const control = useRef(); 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( () => { @@ -30,10 +43,10 @@ export default function DrawControl(props: DrawControlProps) { 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', @@ -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' diff --git a/app/scripts/components/common/map/controls/custom-aoi-modal.tsx b/app/scripts/components/common/map/controls/custom-aoi-modal.tsx index 449b1c701..b387440e3 100644 --- a/app/scripts/components/common/map/controls/custom-aoi-modal.tsx +++ b/app/scripts/components/common/map/controls/custom-aoi-modal.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef } from 'react'; import styled from 'styled-components'; -import { Feature, FeatureCollection, Polygon } from 'geojson'; +import { Feature, Polygon } from 'geojson'; import { Modal, ModalHeadline, ModalFooter } from '@devseed-ui/modal'; import { Heading, Subtitle } from '@devseed-ui/typography'; @@ -117,7 +117,6 @@ export default function CustomAoIModal({ fileInfo, reset } = useCustomAoI(); - console.log(features) const fileInputRef = useRef(null); const onUploadClick = useCallback(() => { diff --git a/app/scripts/components/common/map/controls/hooks/use-aois.ts b/app/scripts/components/common/map/controls/hooks/use-aois.ts index ece9a9e70..66668ef1f 100644 --- a/app/scripts/components/common/map/controls/hooks/use-aois.ts +++ b/app/scripts/components/common/map/controls/hooks/use-aois.ts @@ -2,7 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; import { Feature, Polygon } from 'geojson'; import { toAoIid } from '../../utils'; -import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '../aoi/atoms'; +import { + aoisDeleteAtom, + aoisFeaturesAtom, + aoisSetSelectedAtom, + aoisUpdateGeometryAtom +} from '../aoi/atoms'; export default function useAois() { const features = useAtomValue(aoisFeaturesAtom); @@ -11,8 +16,8 @@ export default function useAois() { const update = useCallback( (features: Feature[]) => { aoisUpdateGeometry(features); - } - , [aoisUpdateGeometry] + }, + [aoisUpdateGeometry] ); const onUpdate = useCallback( (e) => { diff --git a/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx b/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx index eb2f34551..d3e0f194f 100644 --- a/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx +++ b/app/scripts/components/common/map/controls/hooks/use-custom-aoi.tsx @@ -1,4 +1,4 @@ -import { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson'; +import { Feature, MultiPolygon, Polygon } from 'geojson'; import { useCallback, useEffect, useRef, useState } from 'react'; import shp from 'shpjs'; import simplify from '@turf/simplify'; @@ -26,8 +26,7 @@ function useCustomAoI() { const [uploadFileError, setUploadFileError] = useState(null); const [uploadFileWarnings, setUploadFileWarnings] = useState([]); const reader = useRef(); - const [features, setFeatures] = - useState[] | null>(null); + const [features, setFeatures] = useState[] | null>(null); useEffect(() => { reader.current = new FileReader(); @@ -158,7 +157,7 @@ function useCustomAoI() { setUploadFileError(null); setFeatures( simplifiedFeatures.map((feat, i) => ({ - id: `aoi-${i}`, + id: `${new Date().getTime().toString().slice(-4)}${i}`, ...feat })) ); diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 23a14d39c..67e66da79 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useAtomValue } from 'jotai'; import { addMonths } from 'date-fns'; +import { Feature, Polygon } from 'geojson'; import { useStacMetadataOnDatasets } from '../../hooks/use-stac-metadata-datasets'; import { selectedDateAtom, timelineDatasetsAtom } from '../../atoms/atoms'; @@ -9,7 +10,6 @@ import { TimelineDatasetSuccess } from '../../types.d.ts'; import { Layer } from './layer'; - import Map, { Compare } from '$components/common/map'; import { Basemap } from '$components/common/map/style-generators/basemap'; import GeocoderControl from '$components/common/map/controls/geocoder'; @@ -21,7 +21,6 @@ import { useBasemap } from '$components/common/map/controls/hooks/use-basemap'; import DrawControl from '$components/common/map/controls/aoi'; import useAois from '$components/common/map/controls/hooks/use-aois'; import CustomAoIControl from '$components/common/map/controls/aoi/custom-aoi-control'; -import { Feature, Polygon } from 'geojson'; export function ExplorationMap(props: { comparing: boolean }) { const [projection, setProjection] = useState(projectionDefault); @@ -50,10 +49,14 @@ export function ExplorationMap(props: { comparing: boolean }) { .slice() .reverse(); - const { update, onUpdate, onDelete, onSelectionChange } = useAois(); + const { update } = useAois(); + const [customAoIFeatures, setCustomAoIFeatures] = useState< + Feature[] + >([]); const onCustomAoIConfirm = (features: Feature[]) => { update(features); + setCustomAoIFeatures(features); }; return ( @@ -83,10 +86,7 @@ export function ExplorationMap(props: { comparing: boolean }) { trash: true } as any } - onCreate={onUpdate} - onUpdate={onUpdate} - onDelete={onDelete} - onSelectionChange={onSelectionChange} + customFeatures={customAoIFeatures} /> From d3d37bd83fbe334f568bac6328f8605ee6b50676 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Thu, 19 Oct 2023 13:38:16 +0200 Subject: [PATCH 3/3] Properly locate handler to trigger custom AoI modal Co-authored-by: Daniel da Silva --- app/scripts/components/analysis/define/aoi-selector.tsx | 2 +- .../common/map/controls/aoi/custom-aoi-control.tsx | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 5c36a75d9..2915c87c4 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -120,7 +120,7 @@ export default function AoiSelector({ const [aoiModalRevealed, setAoIModalRevealed] = useState(false); return ( - + - - setAoIModalRevealed(true)} - /> + setAoIModalRevealed(true)}> +