From 530acb175f98a1a98efa07c2cc374222ddb07b93 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 4 Oct 2023 14:56:05 +0200 Subject: [PATCH 01/10] Basic AoIs using URL hash --- .../common/map/controls/aoi/index.tsx | 34 ++++++++++ .../common/map/controls/hooks/use-aois.ts | 37 +++++++++++ .../components/common/map/controls/index.tsx | 2 +- .../common/map/mapbox-style-override.ts | 30 ++++++++- app/scripts/components/common/map/types.d.ts | 12 +++- app/scripts/components/common/map/utils.ts | 4 ++ .../components/exploration/atoms/atoms.ts | 56 +++++++++++++++++ .../exploration/components/map/index.tsx | 17 +++++ app/scripts/utils/polygon-url.ts | 63 ++++++++++++++----- package.json | 1 + yarn.lock | 5 ++ 11 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 app/scripts/components/common/map/controls/aoi/index.tsx create mode 100644 app/scripts/components/common/map/controls/hooks/use-aois.ts diff --git a/app/scripts/components/common/map/controls/aoi/index.tsx b/app/scripts/components/common/map/controls/aoi/index.tsx new file mode 100644 index 000000000..a59276d8f --- /dev/null +++ b/app/scripts/components/common/map/controls/aoi/index.tsx @@ -0,0 +1,34 @@ +import MapboxDraw from '@mapbox/mapbox-gl-draw'; +import { useControl } from 'react-map-gl'; + +// import type { MapRef } from 'react-map-gl'; + +type DrawControlProps = ConstructorParameters[0] & { + onCreate?: (evt: { features: object[] }) => void; + onUpdate?: (evt: { features: object[]; action: string }) => void; + onDelete?: (evt: { features: object[] }) => void; + onSelectionChange?: (evt: { selectedFeatures: object[] }) => void; +}; + +export default function DrawControl(props: DrawControlProps) { + useControl( + () => new MapboxDraw(props), + ({ 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 }: { 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); + }, + { + position: 'top-left' + } + ); + + return null; +} diff --git a/app/scripts/components/common/map/controls/hooks/use-aois.ts b/app/scripts/components/common/map/controls/hooks/use-aois.ts new file mode 100644 index 000000000..83fe80842 --- /dev/null +++ b/app/scripts/components/common/map/controls/hooks/use-aois.ts @@ -0,0 +1,37 @@ +import { useAtomValue, useSetAtom } from 'jotai'; +import { useCallback } from 'react'; +import { Polygon } from 'geojson'; +import { toAoIid } from '../../utils'; +import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '$components/exploration/atoms/atoms'; + +export default function useAois() { + const features = useAtomValue(aoisFeaturesAtom); + + const aoisUpdateGeometry = useSetAtom(aoisUpdateGeometryAtom); + const onUpdate = useCallback( + (e) => { + const updates = e.features.map((f) => ({ id: toAoIid(f.id), geometry: f.geometry as Polygon })); + aoisUpdateGeometry(updates); + }, + [aoisUpdateGeometry] + ); + + const aoiDelete = useSetAtom(aoisDeleteAtom); + const onDelete = useCallback( + (e) => { + const selectedIds = e.features.map((f) => toAoIid(f.id)); + aoiDelete(selectedIds); + }, + [aoiDelete] + ); + + const aoiSetSelected = useSetAtom(aoisSetSelectedAtom); + const onSelectionChange = useCallback( + (e) => { + const selectedIds = e.features.map((f) => toAoIid(f.id)); + aoiSetSelected(selectedIds); + }, + [aoiSetSelected] + ); + return { features, onUpdate, onDelete, onSelectionChange }; +} diff --git a/app/scripts/components/common/map/controls/index.tsx b/app/scripts/components/common/map/controls/index.tsx index e5d766236..563ff3314 100644 --- a/app/scripts/components/common/map/controls/index.tsx +++ b/app/scripts/components/common/map/controls/index.tsx @@ -10,4 +10,4 @@ export function NavigationControl() { export function ScaleControl() { return ; -} \ No newline at end of file +} diff --git a/app/scripts/components/common/map/mapbox-style-override.ts b/app/scripts/components/common/map/mapbox-style-override.ts index 00c7067a4..869068990 100644 --- a/app/scripts/components/common/map/mapbox-style-override.ts +++ b/app/scripts/components/common/map/mapbox-style-override.ts @@ -8,7 +8,9 @@ import { CollecticonPlusSmall, CollecticonMinusSmall, CollecticonMagnifierLeft, - CollecticonXmarkSmall + CollecticonXmarkSmall, + CollecticonPencil, + CollecticonTrashBin } from '@devseed-ui/collecticons'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { variableGlsp } from '$styles/variable-utils'; @@ -178,6 +180,32 @@ const MapboxStyleOverride = css` background-color: ${themeVal('color.base-400a')}; } + .mapbox-gl-draw_ctrl-draw-btn { + ${createButtonStyles({ variation: 'primary-fill', fitting: 'skinny' })} + } + + .mapbox-gl-draw_ctrl-draw-btn.active { + background-color: ${themeVal('color.base-400a')}; + } + + .mapbox-gl-draw_polygon.mapbox-gl-draw_polygon::before { + background-image: url(${({ theme }) => + iconDataURI(CollecticonPencil, { + color: theme.color?.surface + })}); + } + } + .mapbox-gl-draw_trash.mapbox-gl-draw_trash::before { + background-image: url(${({ theme }) => + iconDataURI(CollecticonTrashBin, { + color: theme.color?.surface + })}); + } + } + + + // mapbox-gl-draw_polygon" + /* GEOCODER styles */ .mapboxgl-ctrl.mapboxgl-ctrl-geocoder { background-color: ${themeVal('color.surface')}; diff --git a/app/scripts/components/common/map/types.d.ts b/app/scripts/components/common/map/types.d.ts index 89ae4cd77..b6a54f80b 100644 --- a/app/scripts/components/common/map/types.d.ts +++ b/app/scripts/components/common/map/types.d.ts @@ -1,4 +1,5 @@ -import { AnyLayer, AnySourceImpl } from "mapbox-gl"; +import { Feature, Polygon } from 'geojson'; +import { AnyLayer, AnySourceImpl } from 'mapbox-gl'; export interface ExtendedMetadata { layerOrderPosition?: LayerOrderPosition; @@ -29,10 +30,15 @@ export type LayerOrderPosition = | 'vector' | 'basemap-foreground'; -export type MapId = 'main' | 'compared' +export type MapId = 'main' | 'compared'; export interface StacFeature { bbox: [number, number, number, number]; } -export type OptionalBbox = number[] | undefined | null; \ No newline at end of file +export type OptionalBbox = number[] | undefined | null; + +export type AoIFeature = Feature & { + selected: boolean; + id: string; +}; diff --git a/app/scripts/components/common/map/utils.ts b/app/scripts/components/common/map/utils.ts index 4b70ecc96..498d65a8a 100644 --- a/app/scripts/components/common/map/utils.ts +++ b/app/scripts/components/common/map/utils.ts @@ -180,3 +180,7 @@ export function resolveConfigFunctions( return datum; } + +export function toAoIid(drawId: string) { + return drawId.slice(-6); +} \ No newline at end of file diff --git a/app/scripts/components/exploration/atoms/atoms.ts b/app/scripts/components/exploration/atoms/atoms.ts index d20be9f22..672bde717 100644 --- a/app/scripts/components/exploration/atoms/atoms.ts +++ b/app/scripts/components/exploration/atoms/atoms.ts @@ -1,4 +1,6 @@ import { atom } from 'jotai'; +import { atomWithHash } from 'jotai-location'; +import { Polygon } from 'geojson'; import { DataMetric, dataMetrics @@ -6,6 +8,8 @@ import { import { HEADER_COLUMN_WIDTH, RIGHT_AXIS_SPACE } from '../constants'; import { DateRange, TimelineDataset, ZoomTransformPlain } from '../types.d.ts'; +import { decodeAois, encodeAois } from '$utils/polygon-url'; +import { AoIFeature } from '$components/common/map/types'; // Datasets to show on the timeline and their settings export const timelineDatasetsAtom = atom([]); @@ -43,3 +47,55 @@ export const activeAnalysisMetricsAtom = atom(dataMetrics); // 🛑 Whether or not an analysis is being performed. Temporary!!! export const isAnalysisAtom = atom(false); + +// This is the atom acting as a single source of truth for the AOIs. +export const aoisHashAtom = atomWithHash('aois', ''); + +// Getter atom to get AoiS as GeoJSON features from the hash. +export const aoisFeaturesAtom = atom((get) => { + const hash = get(aoisHashAtom); + if (!hash) return []; + return decodeAois(hash); +}); + +// Setter atom to update AOIs geoometries, writing directly to the hash atom. +export const aoisUpdateGeometryAtom = atom( + null, + (get, set, updates: { id: string; geometry: Polygon }[]) => { + let newFeatures = [...get(aoisFeaturesAtom)]; + updates.forEach(({ id, geometry }) => { + const existingFeature = newFeatures.find((feature) => feature.id === id); + if (existingFeature) { + existingFeature.geometry = geometry; + } else { + const newFeature: AoIFeature = { + type: 'Feature', + id, + geometry, + selected: true, + properties: {} + }; + newFeatures = [...newFeatures, newFeature]; + } + }); + set(aoisHashAtom, encodeAois(newFeatures)); + } +); + +// Setter atom to update AOIs selected state, writing directly to the hash atom. +export const aoisSetSelectedAtom = atom(null, (get, set, ids: string[]) => { + const features = get(aoisFeaturesAtom); + const newFeatures = features.map((feature) => { + return { ...feature, selected: ids.includes(feature.id as string) }; + }); + set(aoisHashAtom, encodeAois(newFeatures)); +}); + +// Setter atom to delete AOIs, writing directly to the hash atom. +export const aoisDeleteAtom = atom(null, (get, set, ids: string[]) => { + const features = get(aoisFeaturesAtom); + const newFeatures = features.filter( + (feature) => !ids.includes(feature.id as string) + ); + set(aoisHashAtom, encodeAois(newFeatures)); +}); diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index d07f2f34e..9b37ff157 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -21,6 +21,8 @@ import MapCoordsControl from '$components/common/map/controls/coords'; import MapOptionsControl from '$components/common/map/controls/options'; import { projectionDefault } from '$components/common/map/controls/map-options/projections'; 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'; export function ExplorationMap(props: { comparing: boolean }) { const [projection, setProjection] = useState(projectionDefault); @@ -49,6 +51,9 @@ export function ExplorationMap(props: { comparing: boolean }) { .slice() .reverse(); + const { onUpdate, onDelete, onSelectionChange, features } = useAois(); + console.log(features); + return ( {/* Map layers */} @@ -81,6 +86,18 @@ export function ExplorationMap(props: { comparing: boolean }) { boundariesOption={boundariesOption} onOptionChange={onOptionChange} /> + {props.comparing && ( // Compare map layers diff --git a/app/scripts/utils/polygon-url.ts b/app/scripts/utils/polygon-url.ts index cb1e41faf..ea3d3e6f0 100644 --- a/app/scripts/utils/polygon-url.ts +++ b/app/scripts/utils/polygon-url.ts @@ -1,6 +1,20 @@ -import { FeatureCollection, Polygon } from 'geojson'; +import { Feature, FeatureCollection, Polygon } from 'geojson'; import gjv from 'geojson-validation'; import { decode, encode } from 'google-polyline'; +import { AoIFeature } from '$components/common/map/types'; +import { toAoIid } from '$components/common/map/utils'; + +function decodeFeature(polygon: string): Feature { + const coords = decode(polygon); + return { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [[...coords, coords[0]]] + } + } as Feature; +} /** * Decodes a multi polygon string converting it into a FeatureCollection of @@ -15,15 +29,7 @@ export function polygonUrlDecode(polygonStr: string) { const geojson = { type: 'FeatureCollection', features: polygonStr.split(';').map((polygon) => { - const coords = decode(polygon); - return { - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [[...coords, coords[0]]] - } - }; + return decodeFeature(polygon) as Feature; }) } as FeatureCollection; @@ -33,6 +39,13 @@ export function polygonUrlDecode(polygonStr: string) { }; } +function encodePolygon(polygon: Polygon) { + const points = polygon.coordinates[0] + // Remove last coordinate since it is repeated. + .slice(0, -1); + return encode(points); +} + /** * Converts a FeatureCollection of Polygons into a url string. * Removes the last point of the polygon as it is the same as the first. @@ -47,10 +60,32 @@ export function polygonUrlEncode( ) { return featureCollection.features .map((feature) => { - const points = feature.geometry.coordinates[0] - // Remove last coordinate since it is repeated. - .slice(0, -1); - return encode(points); + return encodePolygon(feature.geometry); }) .join(';'); } + +export function encodeAois(aois: AoIFeature[]): string { + const encoded = aois.reduce((acc, aoi) => { + const encodedGeom = encodePolygon(aoi.geometry); + return [...acc, encodedGeom, toAoIid(aoi.id), !!aoi.selected]; + }, []); + return JSON.stringify(encoded); +} + +export function decodeAois(aois: string): AoIFeature[] { + const decoded = JSON.parse(aois) as string[]; + const features = decoded.reduce((acc, current, i) => { + if (i % 3 === 0) { + const decodedFeature = decodeFeature(current) as AoIFeature; + return [...acc, decodedFeature]; + } else { + const lastFeature = acc[acc.length - 1]; + const prop = i % 3 === 1 ? 'id' : 'selected'; + const newFeature = { ...lastFeature, [prop]: current }; + acc[acc.length - 1] = newFeature; + return acc; + } + }, []); + return features!; +} diff --git a/package.json b/package.json index 1dfb39a01..9b3a59ada 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "intersection-observer": "^0.12.0", "jest-environment-jsdom": "^28.1.3", "jotai": "^2.2.3", + "jotai-location": "^0.5.1", "jotai-optics": "^0.3.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index ea7bcb271..c85640577 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8308,6 +8308,11 @@ jest@^28.1.3: import-local "^3.0.2" jest-cli "^28.1.3" +jotai-location@^0.5.1: + version "0.5.1" + resolved "http://verdaccio.ds.io:4873/jotai-location/-/jotai-location-0.5.1.tgz#1a08b683cd7823ce57f7fef8b98335f1ce5c7105" + integrity sha512-6b34X6PpUaXmHCcyxdMFUHgRLUEp+SFHq9UxHbg5HxHC1LddVyVZbPJI+P15+SOQJcUTH3KrsIeKmeLko+Vw/A== + jotai-optics@^0.3.1: version "0.3.1" resolved "http://verdaccio.ds.io:4873/jotai-optics/-/jotai-optics-0.3.1.tgz#7ff38470551429460cc41d9cd1320193665354e0" From 9bca0b44317015851209f5478a6dff92d84ba5c0 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Fri, 6 Oct 2023 15:44:23 +0200 Subject: [PATCH 02/10] Show URL polygons in the map on mount --- .../common/map/controls/aoi/index.tsx | 23 +++++++++++++++---- .../common/map/controls/hooks/use-aois.ts | 12 ++++++++-- .../common/map/mapbox-style-override.ts | 1 + .../exploration/components/map/index.tsx | 6 ++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/scripts/components/common/map/controls/aoi/index.tsx b/app/scripts/components/common/map/controls/aoi/index.tsx index a59276d8f..00c1dfbe8 100644 --- a/app/scripts/components/common/map/controls/aoi/index.tsx +++ b/app/scripts/components/common/map/controls/aoi/index.tsx @@ -1,23 +1,36 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw'; +import { useAtomValue } from 'jotai'; +import { useRef } from 'react'; import { useControl } from 'react-map-gl'; +import { aoisFeaturesAtom } from '$components/exploration/atoms/atoms'; -// import type { MapRef } from 'react-map-gl'; - -type DrawControlProps = ConstructorParameters[0] & { +type DrawControlProps = { onCreate?: (evt: { features: object[] }) => void; onUpdate?: (evt: { features: object[]; action: string }) => void; onDelete?: (evt: { features: object[] }) => void; onSelectionChange?: (evt: { selectedFeatures: object[] }) => void; -}; +} & MapboxDraw.DrawOptions; export default function DrawControl(props: DrawControlProps) { + const control = useRef(); + const aoisFeatures = useAtomValue(aoisFeaturesAtom); + useControl( - () => new MapboxDraw(props), + () => { + 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('load', () => { + control.current?.set({ + type: 'FeatureCollection', + features: aoisFeatures + }); + }); }, ({ map }: { map: any }) => { map.off('draw.create', props.onCreate); 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 83fe80842..df6a0339d 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 { Polygon } from 'geojson'; import { toAoIid } from '../../utils'; -import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '$components/exploration/atoms/atoms'; +import { + aoisDeleteAtom, + aoisFeaturesAtom, + aoisSetSelectedAtom, + aoisUpdateGeometryAtom +} from '$components/exploration/atoms/atoms'; export default function useAois() { const features = useAtomValue(aoisFeaturesAtom); @@ -10,7 +15,10 @@ export default function useAois() { const aoisUpdateGeometry = useSetAtom(aoisUpdateGeometryAtom); const onUpdate = useCallback( (e) => { - const updates = e.features.map((f) => ({ id: toAoIid(f.id), geometry: f.geometry as Polygon })); + const updates = e.features.map((f) => ({ + id: toAoIid(f.id), + geometry: f.geometry as Polygon + })); aoisUpdateGeometry(updates); }, [aoisUpdateGeometry] diff --git a/app/scripts/components/common/map/mapbox-style-override.ts b/app/scripts/components/common/map/mapbox-style-override.ts index 869068990..851922f0d 100644 --- a/app/scripts/components/common/map/mapbox-style-override.ts +++ b/app/scripts/components/common/map/mapbox-style-override.ts @@ -195,6 +195,7 @@ const MapboxStyleOverride = css` })}); } } + .mapbox-gl-draw_trash.mapbox-gl-draw_trash::before { background-image: url(${({ theme }) => iconDataURI(CollecticonTrashBin, { diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 9b37ff157..7aa026719 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -51,8 +51,8 @@ export function ExplorationMap(props: { comparing: boolean }) { .slice() .reverse(); - const { onUpdate, onDelete, onSelectionChange, features } = useAois(); - console.log(features); + const { onUpdate, onDelete, onSelectionChange } = useAois(); + // console.log(features); return ( @@ -91,7 +91,7 @@ export function ExplorationMap(props: { comparing: boolean }) { controls={{ polygon: true, trash: true - }} + } as any} defaultMode='draw_polygon' onCreate={onUpdate} onUpdate={onUpdate} From 27b91623ef2671c1b326784024b8f0640d8b3ec2 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 10 Oct 2023 14:49:16 +0200 Subject: [PATCH 03/10] Moved aoi atoms to own file --- .../common/map/controls/aoi/atoms.ts | 57 +++++++++++++++++++ .../common/map/controls/aoi/index.tsx | 2 +- .../common/map/controls/hooks/use-aois.ts | 7 +-- .../{options.tsx => map-options/index.tsx} | 10 ++-- .../components/exploration/atoms/atoms.ts | 56 ------------------ .../exploration/components/map/index.tsx | 2 +- 6 files changed, 65 insertions(+), 69 deletions(-) create mode 100644 app/scripts/components/common/map/controls/aoi/atoms.ts rename app/scripts/components/common/map/controls/{options.tsx => map-options/index.tsx} (96%) diff --git a/app/scripts/components/common/map/controls/aoi/atoms.ts b/app/scripts/components/common/map/controls/aoi/atoms.ts new file mode 100644 index 000000000..c3bed5a73 --- /dev/null +++ b/app/scripts/components/common/map/controls/aoi/atoms.ts @@ -0,0 +1,57 @@ +import { atom } from "jotai"; +import { atomWithHash } from "jotai-location"; +import { Polygon } from "geojson"; +import { AoIFeature } from "../../types"; +import { decodeAois, encodeAois } from "$utils/polygon-url"; + +// This is the atom acting as a single source of truth for the AOIs. +export const aoisHashAtom = atomWithHash('aois', ''); + +// Getter atom to get AoiS as GeoJSON features from the hash. +export const aoisFeaturesAtom = atom((get) => { + const hash = get(aoisHashAtom); + if (!hash) return []; + return decodeAois(hash); +}); + +// Setter atom to update AOIs geoometries, writing directly to the hash atom. +export const aoisUpdateGeometryAtom = atom( + null, + (get, set, updates: { id: string; geometry: Polygon }[]) => { + let newFeatures = [...get(aoisFeaturesAtom)]; + updates.forEach(({ id, geometry }) => { + const existingFeature = newFeatures.find((feature) => feature.id === id); + if (existingFeature) { + existingFeature.geometry = geometry; + } else { + const newFeature: AoIFeature = { + type: 'Feature', + id, + geometry, + selected: true, + properties: {} + }; + newFeatures = [...newFeatures, newFeature]; + } + }); + set(aoisHashAtom, encodeAois(newFeatures)); + } +); + +// Setter atom to update AOIs selected state, writing directly to the hash atom. +export const aoisSetSelectedAtom = atom(null, (get, set, ids: string[]) => { + const features = get(aoisFeaturesAtom); + const newFeatures = features.map((feature) => { + return { ...feature, selected: ids.includes(feature.id as string) }; + }); + set(aoisHashAtom, encodeAois(newFeatures)); +}); + +// Setter atom to delete AOIs, writing directly to the hash atom. +export const aoisDeleteAtom = atom(null, (get, set, ids: string[]) => { + const features = get(aoisFeaturesAtom); + const newFeatures = features.filter( + (feature) => !ids.includes(feature.id as string) + ); + set(aoisHashAtom, encodeAois(newFeatures)); +}); diff --git a/app/scripts/components/common/map/controls/aoi/index.tsx b/app/scripts/components/common/map/controls/aoi/index.tsx index 00c1dfbe8..85bd65e39 100644 --- a/app/scripts/components/common/map/controls/aoi/index.tsx +++ b/app/scripts/components/common/map/controls/aoi/index.tsx @@ -2,7 +2,7 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw'; import { useAtomValue } from 'jotai'; import { useRef } from 'react'; import { useControl } from 'react-map-gl'; -import { aoisFeaturesAtom } from '$components/exploration/atoms/atoms'; +import { aoisFeaturesAtom } from './atoms'; type DrawControlProps = { onCreate?: (evt: { features: object[] }) => void; 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 df6a0339d..3032f5a48 100644 --- a/app/scripts/components/common/map/controls/hooks/use-aois.ts +++ b/app/scripts/components/common/map/controls/hooks/use-aois.ts @@ -2,12 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; import { Polygon } from 'geojson'; import { toAoIid } from '../../utils'; -import { - aoisDeleteAtom, - aoisFeaturesAtom, - aoisSetSelectedAtom, - aoisUpdateGeometryAtom -} from '$components/exploration/atoms/atoms'; +import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '../aoi/atoms'; export default function useAois() { const features = useAtomValue(aoisFeaturesAtom); diff --git a/app/scripts/components/common/map/controls/options.tsx b/app/scripts/components/common/map/controls/map-options/index.tsx similarity index 96% rename from app/scripts/components/common/map/controls/options.tsx rename to app/scripts/components/common/map/controls/map-options/index.tsx index e332f2ae0..6d93d2224 100644 --- a/app/scripts/components/common/map/controls/options.tsx +++ b/app/scripts/components/common/map/controls/map-options/index.tsx @@ -12,15 +12,15 @@ import { Button, createButtonStyles } from '@devseed-ui/button'; import { FormSwitch } from '@devseed-ui/form'; import { Subtitle } from '@devseed-ui/typography'; +import useThemedControl from '../hooks/use-themed-control'; import { ProjectionItemConic, ProjectionItemCustom, ProjectionItemSimple -} from './map-options/projection-items'; -import { MapOptionsProps } from './map-options/types'; -import { projectionsList } from './map-options/projections'; -import { BASEMAP_STYLES } from './map-options/basemap'; -import useThemedControl from './hooks/use-themed-control'; +} from './projection-items'; +import { MapOptionsProps } from './types'; +import { projectionsList } from './projections'; +import { BASEMAP_STYLES } from './basemap'; import { ShadowScrollbarImproved as ShadowScrollbar } from '$components/common/shadow-scrollbar-improved'; const DropHeader = styled.div` diff --git a/app/scripts/components/exploration/atoms/atoms.ts b/app/scripts/components/exploration/atoms/atoms.ts index 672bde717..d20be9f22 100644 --- a/app/scripts/components/exploration/atoms/atoms.ts +++ b/app/scripts/components/exploration/atoms/atoms.ts @@ -1,6 +1,4 @@ import { atom } from 'jotai'; -import { atomWithHash } from 'jotai-location'; -import { Polygon } from 'geojson'; import { DataMetric, dataMetrics @@ -8,8 +6,6 @@ import { import { HEADER_COLUMN_WIDTH, RIGHT_AXIS_SPACE } from '../constants'; import { DateRange, TimelineDataset, ZoomTransformPlain } from '../types.d.ts'; -import { decodeAois, encodeAois } from '$utils/polygon-url'; -import { AoIFeature } from '$components/common/map/types'; // Datasets to show on the timeline and their settings export const timelineDatasetsAtom = atom([]); @@ -47,55 +43,3 @@ export const activeAnalysisMetricsAtom = atom(dataMetrics); // 🛑 Whether or not an analysis is being performed. Temporary!!! export const isAnalysisAtom = atom(false); - -// This is the atom acting as a single source of truth for the AOIs. -export const aoisHashAtom = atomWithHash('aois', ''); - -// Getter atom to get AoiS as GeoJSON features from the hash. -export const aoisFeaturesAtom = atom((get) => { - const hash = get(aoisHashAtom); - if (!hash) return []; - return decodeAois(hash); -}); - -// Setter atom to update AOIs geoometries, writing directly to the hash atom. -export const aoisUpdateGeometryAtom = atom( - null, - (get, set, updates: { id: string; geometry: Polygon }[]) => { - let newFeatures = [...get(aoisFeaturesAtom)]; - updates.forEach(({ id, geometry }) => { - const existingFeature = newFeatures.find((feature) => feature.id === id); - if (existingFeature) { - existingFeature.geometry = geometry; - } else { - const newFeature: AoIFeature = { - type: 'Feature', - id, - geometry, - selected: true, - properties: {} - }; - newFeatures = [...newFeatures, newFeature]; - } - }); - set(aoisHashAtom, encodeAois(newFeatures)); - } -); - -// Setter atom to update AOIs selected state, writing directly to the hash atom. -export const aoisSetSelectedAtom = atom(null, (get, set, ids: string[]) => { - const features = get(aoisFeaturesAtom); - const newFeatures = features.map((feature) => { - return { ...feature, selected: ids.includes(feature.id as string) }; - }); - set(aoisHashAtom, encodeAois(newFeatures)); -}); - -// Setter atom to delete AOIs, writing directly to the hash atom. -export const aoisDeleteAtom = atom(null, (get, set, ids: string[]) => { - const features = get(aoisFeaturesAtom); - const newFeatures = features.filter( - (feature) => !ids.includes(feature.id as string) - ); - set(aoisHashAtom, encodeAois(newFeatures)); -}); diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 7aa026719..325ee717a 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -18,7 +18,7 @@ import { ScaleControl } from '$components/common/map/controls'; import MapCoordsControl from '$components/common/map/controls/coords'; -import MapOptionsControl from '$components/common/map/controls/options'; +import MapOptionsControl from '$components/common/map/controls/map-options'; import { projectionDefault } from '$components/common/map/controls/map-options/projections'; import { useBasemap } from '$components/common/map/controls/hooks/use-basemap'; import DrawControl from '$components/common/map/controls/aoi'; From 88e0ef22979f6fe21ac4859dc829d432099e23f6 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 10 Oct 2023 14:53:34 +0200 Subject: [PATCH 04/10] Simplify url decoding --- app/scripts/utils/polygon-url.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/app/scripts/utils/polygon-url.ts b/app/scripts/utils/polygon-url.ts index ea3d3e6f0..dc7e9c01e 100644 --- a/app/scripts/utils/polygon-url.ts +++ b/app/scripts/utils/polygon-url.ts @@ -1,5 +1,6 @@ import { Feature, FeatureCollection, Polygon } from 'geojson'; import gjv from 'geojson-validation'; +import { chunk } from 'lodash'; import { decode, encode } from 'google-polyline'; import { AoIFeature } from '$components/common/map/types'; import { toAoIid } from '$components/common/map/utils'; @@ -75,17 +76,10 @@ export function encodeAois(aois: AoIFeature[]): string { export function decodeAois(aois: string): AoIFeature[] { const decoded = JSON.parse(aois) as string[]; - const features = decoded.reduce((acc, current, i) => { - if (i % 3 === 0) { - const decodedFeature = decodeFeature(current) as AoIFeature; - return [...acc, decodedFeature]; - } else { - const lastFeature = acc[acc.length - 1]; - const prop = i % 3 === 1 ? 'id' : 'selected'; - const newFeature = { ...lastFeature, [prop]: current }; - acc[acc.length - 1] = newFeature; - return acc; - } - }, []); + const features: AoIFeature[] = chunk(decoded, 3).map((data) => { + const [polygon, id, selected] = data; + const decodedFeature = decodeFeature(polygon) as AoIFeature; + return { ...decodedFeature, id, selected }; + }); return features!; } From 24dc07a0835966c5d5759a112bcae022a75748e9 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 10 Oct 2023 15:19:58 +0200 Subject: [PATCH 05/10] Centralize map cursor management --- .../common/map/hooks/use-layer-interaction.ts | 9 +++++---- app/scripts/components/common/map/map-component.tsx | 3 ++- app/scripts/components/common/map/maps.tsx | 13 +++++++++++-- app/scripts/components/common/map/types.d.ts | 2 ++ .../components/exploration/components/map/index.tsx | 1 - 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/scripts/components/common/map/hooks/use-layer-interaction.ts b/app/scripts/components/common/map/hooks/use-layer-interaction.ts index cddab9d80..6eba434cf 100644 --- a/app/scripts/components/common/map/hooks/use-layer-interaction.ts +++ b/app/scripts/components/common/map/hooks/use-layer-interaction.ts @@ -1,7 +1,7 @@ import { Feature } from 'geojson'; import { useEffect } from 'react'; -import useMaps from './use-maps'; +import useMaps, { useMapsContext } from './use-maps'; interface LayerInteractionHookOptions { layerId: string; @@ -12,6 +12,7 @@ export default function useLayerInteraction({ onClick }: LayerInteractionHookOptions) { const { current: mapInstance } = useMaps(); + const { setCursor } = useMapsContext(); useEffect(() => { if (!mapInstance) return; const onPointsClick = (e) => { @@ -20,11 +21,11 @@ export default function useLayerInteraction({ }; const onPointsEnter = () => { - mapInstance.getCanvas().style.cursor = 'pointer'; + setCursor('pointer'); }; const onPointsLeave = () => { - mapInstance.getCanvas().style.cursor = ''; + setCursor('grab'); }; mapInstance.on('click', layerId, onPointsClick); @@ -36,5 +37,5 @@ export default function useLayerInteraction({ mapInstance.off('mouseenter', layerId, onPointsEnter); mapInstance.off('mouseleave', layerId, onPointsLeave); }; - }, [layerId, mapInstance, onClick]); + }, [layerId, mapInstance, onClick, setCursor]); } \ No newline at end of file diff --git a/app/scripts/components/common/map/map-component.tsx b/app/scripts/components/common/map/map-component.tsx index ac6dec08e..b8133f872 100644 --- a/app/scripts/components/common/map/map-component.tsx +++ b/app/scripts/components/common/map/map-component.tsx @@ -16,7 +16,7 @@ export default function MapComponent({ isCompared?: boolean; projection?: ProjectionOptions; }) { - const { initialViewState, setInitialViewState, mainId, comparedId } = + const { initialViewState, setInitialViewState, mainId, comparedId, cursor } = useMapsContext(); const id = isCompared ? comparedId : mainId; @@ -52,6 +52,7 @@ export default function MapComponent({ mapStyle={style as any} onMove={onMove} projection={mapboxProjection} + // cursor={cursor} > {controls} diff --git a/app/scripts/components/common/map/maps.tsx b/app/scripts/components/common/map/maps.tsx index b3864132a..fe1db14f3 100644 --- a/app/scripts/components/common/map/maps.tsx +++ b/app/scripts/components/common/map/maps.tsx @@ -23,6 +23,7 @@ import { Styles } from './styles'; import useMapCompare from './hooks/use-map-compare'; import MapComponent from './map-component'; import useMaps, { useMapsContext } from './hooks/use-maps'; +import { Cursor } from './types'; const chevronRightURI = () => iconDataURI(CollecticonChevronRightSmall, { @@ -156,6 +157,8 @@ export default function MapsContextWrapper(props: MapsContextWrapperProps) { zoom: 1 }); + const [cursor, setCursor] = useState('grab'); + return ( {props.children} @@ -177,6 +182,8 @@ interface MapsContextType { mainId: string; comparedId: string; containerId: string; + cursor: Cursor; + setCursor: (cursor: Cursor) => void; } export const MapsContext = createContext({ @@ -184,5 +191,7 @@ export const MapsContext = createContext({ setInitialViewState: () => undefined, mainId: '', comparedId: '', - containerId: '' + containerId: '', + cursor: 'grab', + setCursor: () => undefined }); diff --git a/app/scripts/components/common/map/types.d.ts b/app/scripts/components/common/map/types.d.ts index b6a54f80b..f802400cb 100644 --- a/app/scripts/components/common/map/types.d.ts +++ b/app/scripts/components/common/map/types.d.ts @@ -42,3 +42,5 @@ export type AoIFeature = Feature & { selected: boolean; id: string; }; + +export type Cursor = 'auto' | 'default' | 'pointer' | 'grab' | ' move'; diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 325ee717a..bed9d0610 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -92,7 +92,6 @@ export function ExplorationMap(props: { comparing: boolean }) { polygon: true, trash: true } as any} - defaultMode='draw_polygon' onCreate={onUpdate} onUpdate={onUpdate} onDelete={onDelete} From 46c8d16036321c98507cb161fafe4c886809ecfd Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 11 Oct 2023 10:21:50 +0200 Subject: [PATCH 06/10] Fixed mapbox-gl-draw cursors --- app/scripts/components/common/map/maps.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/scripts/components/common/map/maps.tsx b/app/scripts/components/common/map/maps.tsx index fe1db14f3..f7a467c20 100644 --- a/app/scripts/components/common/map/maps.tsx +++ b/app/scripts/components/common/map/maps.tsx @@ -42,6 +42,16 @@ const MapsContainer = styled.div` .mapboxgl-map { position: absolute !important; inset: 0; + + &.mouse-add .mapboxgl-canvas-container { + cursor: crosshair; + } + &.mouse-pointer .mapboxgl-canvas-container { + cursor: pointer; + } + &.mouse-move .mapboxgl-canvas-container { + cursor: move; + } } .mapboxgl-compare .compare-swiper-vertical { From c7c3acecc51caeac1aa285e6af01bef468399a34 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 11 Oct 2023 11:08:06 +0200 Subject: [PATCH 07/10] Disable delete button when not applicable --- .../components/common/map/controls/aoi/index.tsx | 11 ++++++++++- .../components/common/map/mapbox-style-override.ts | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/common/map/controls/aoi/index.tsx b/app/scripts/components/common/map/controls/aoi/index.tsx index 85bd65e39..4e1d968c3 100644 --- a/app/scripts/components/common/map/controls/aoi/index.tsx +++ b/app/scripts/components/common/map/controls/aoi/index.tsx @@ -1,4 +1,6 @@ +import React 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'; @@ -11,6 +13,13 @@ type DrawControlProps = { onSelectionChange?: (evt: { selectedFeatures: object[] }) => void; } & MapboxDraw.DrawOptions; +const Css = createGlobalStyle` +.mapbox-gl-draw_trash { + opacity: .5; + pointer-events: none !important; +} +`; + export default function DrawControl(props: DrawControlProps) { const control = useRef(); const aoisFeatures = useAtomValue(aoisFeaturesAtom); @@ -43,5 +52,5 @@ export default function DrawControl(props: DrawControlProps) { } ); - return null; + return aoisFeatures.length ? null : ; } diff --git a/app/scripts/components/common/map/mapbox-style-override.ts b/app/scripts/components/common/map/mapbox-style-override.ts index 851922f0d..a6cf2ff0c 100644 --- a/app/scripts/components/common/map/mapbox-style-override.ts +++ b/app/scripts/components/common/map/mapbox-style-override.ts @@ -188,6 +188,10 @@ const MapboxStyleOverride = css` background-color: ${themeVal('color.base-400a')}; } + .mapbox-gl-draw_ctrl-draw-btn:not(:disabled):hover { + background-color: ${themeVal('color.base-400a')}; + } + .mapbox-gl-draw_polygon.mapbox-gl-draw_polygon::before { background-image: url(${({ theme }) => iconDataURI(CollecticonPencil, { @@ -204,9 +208,6 @@ const MapboxStyleOverride = css` } } - - // mapbox-gl-draw_polygon" - /* GEOCODER styles */ .mapboxgl-ctrl.mapboxgl-ctrl-geocoder { background-color: ${themeVal('color.surface')}; From 582d24cb45b6e31af236883b830e4ce470905e55 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 11 Oct 2023 11:09:57 +0200 Subject: [PATCH 08/10] Revert "Centralize map cursor management" This reverts commit 24dc07a0835966c5d5759a112bcae022a75748e9. --- .../common/map/hooks/use-layer-interaction.ts | 9 ++++----- app/scripts/components/common/map/map-component.tsx | 3 +-- app/scripts/components/common/map/maps.tsx | 13 ++----------- app/scripts/components/common/map/types.d.ts | 2 -- .../components/exploration/components/map/index.tsx | 1 + 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/scripts/components/common/map/hooks/use-layer-interaction.ts b/app/scripts/components/common/map/hooks/use-layer-interaction.ts index 6eba434cf..cddab9d80 100644 --- a/app/scripts/components/common/map/hooks/use-layer-interaction.ts +++ b/app/scripts/components/common/map/hooks/use-layer-interaction.ts @@ -1,7 +1,7 @@ import { Feature } from 'geojson'; import { useEffect } from 'react'; -import useMaps, { useMapsContext } from './use-maps'; +import useMaps from './use-maps'; interface LayerInteractionHookOptions { layerId: string; @@ -12,7 +12,6 @@ export default function useLayerInteraction({ onClick }: LayerInteractionHookOptions) { const { current: mapInstance } = useMaps(); - const { setCursor } = useMapsContext(); useEffect(() => { if (!mapInstance) return; const onPointsClick = (e) => { @@ -21,11 +20,11 @@ export default function useLayerInteraction({ }; const onPointsEnter = () => { - setCursor('pointer'); + mapInstance.getCanvas().style.cursor = 'pointer'; }; const onPointsLeave = () => { - setCursor('grab'); + mapInstance.getCanvas().style.cursor = ''; }; mapInstance.on('click', layerId, onPointsClick); @@ -37,5 +36,5 @@ export default function useLayerInteraction({ mapInstance.off('mouseenter', layerId, onPointsEnter); mapInstance.off('mouseleave', layerId, onPointsLeave); }; - }, [layerId, mapInstance, onClick, setCursor]); + }, [layerId, mapInstance, onClick]); } \ No newline at end of file diff --git a/app/scripts/components/common/map/map-component.tsx b/app/scripts/components/common/map/map-component.tsx index b8133f872..ac6dec08e 100644 --- a/app/scripts/components/common/map/map-component.tsx +++ b/app/scripts/components/common/map/map-component.tsx @@ -16,7 +16,7 @@ export default function MapComponent({ isCompared?: boolean; projection?: ProjectionOptions; }) { - const { initialViewState, setInitialViewState, mainId, comparedId, cursor } = + const { initialViewState, setInitialViewState, mainId, comparedId } = useMapsContext(); const id = isCompared ? comparedId : mainId; @@ -52,7 +52,6 @@ export default function MapComponent({ mapStyle={style as any} onMove={onMove} projection={mapboxProjection} - // cursor={cursor} > {controls} diff --git a/app/scripts/components/common/map/maps.tsx b/app/scripts/components/common/map/maps.tsx index f7a467c20..b1a2cfbfe 100644 --- a/app/scripts/components/common/map/maps.tsx +++ b/app/scripts/components/common/map/maps.tsx @@ -23,7 +23,6 @@ import { Styles } from './styles'; import useMapCompare from './hooks/use-map-compare'; import MapComponent from './map-component'; import useMaps, { useMapsContext } from './hooks/use-maps'; -import { Cursor } from './types'; const chevronRightURI = () => iconDataURI(CollecticonChevronRightSmall, { @@ -167,8 +166,6 @@ export default function MapsContextWrapper(props: MapsContextWrapperProps) { zoom: 1 }); - const [cursor, setCursor] = useState('grab'); - return ( {props.children} @@ -192,8 +187,6 @@ interface MapsContextType { mainId: string; comparedId: string; containerId: string; - cursor: Cursor; - setCursor: (cursor: Cursor) => void; } export const MapsContext = createContext({ @@ -201,7 +194,5 @@ export const MapsContext = createContext({ setInitialViewState: () => undefined, mainId: '', comparedId: '', - containerId: '', - cursor: 'grab', - setCursor: () => undefined + containerId: '' }); diff --git a/app/scripts/components/common/map/types.d.ts b/app/scripts/components/common/map/types.d.ts index f802400cb..b6a54f80b 100644 --- a/app/scripts/components/common/map/types.d.ts +++ b/app/scripts/components/common/map/types.d.ts @@ -42,5 +42,3 @@ export type AoIFeature = Feature & { selected: boolean; id: string; }; - -export type Cursor = 'auto' | 'default' | 'pointer' | 'grab' | ' move'; diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index bed9d0610..325ee717a 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -92,6 +92,7 @@ export function ExplorationMap(props: { comparing: boolean }) { polygon: true, trash: true } as any} + defaultMode='draw_polygon' onCreate={onUpdate} onUpdate={onUpdate} onDelete={onDelete} From 2c3544c1771e7ea9bbfd7087eefa24aa56d410ae Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 11 Oct 2023 11:11:43 +0200 Subject: [PATCH 09/10] Do not start drawing --- app/scripts/components/exploration/components/map/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 325ee717a..bed9d0610 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -92,7 +92,6 @@ export function ExplorationMap(props: { comparing: boolean }) { polygon: true, trash: true } as any} - defaultMode='draw_polygon' onCreate={onUpdate} onUpdate={onUpdate} onDelete={onDelete} From 2b89c941df9b8f638a36c3f149580b9950afaf91 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 11 Oct 2023 11:29:40 +0200 Subject: [PATCH 10/10] Use query params rather than hash --- .../common/map/controls/aoi/atoms.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/scripts/components/common/map/controls/aoi/atoms.ts b/app/scripts/components/common/map/controls/aoi/atoms.ts index c3bed5a73..6034a6b1b 100644 --- a/app/scripts/components/common/map/controls/aoi/atoms.ts +++ b/app/scripts/components/common/map/controls/aoi/atoms.ts @@ -1,15 +1,26 @@ import { atom } from "jotai"; -import { atomWithHash } from "jotai-location"; +import { atomWithLocation } from "jotai-location"; import { Polygon } from "geojson"; import { AoIFeature } from "../../types"; import { decodeAois, encodeAois } from "$utils/polygon-url"; // This is the atom acting as a single source of truth for the AOIs. -export const aoisHashAtom = atomWithHash('aois', ''); +export const aoisAtom = atomWithLocation(); + +const aoisSerialized = atom( + (get) => get(aoisAtom).searchParams?.get("aois"), + (get, set, aois) => { + set(aoisAtom, (prev) => ({ + ...prev, + searchParams: new URLSearchParams([["aois", aois as string]]) + })); + } +); + // Getter atom to get AoiS as GeoJSON features from the hash. export const aoisFeaturesAtom = atom((get) => { - const hash = get(aoisHashAtom); + const hash = get(aoisSerialized); if (!hash) return []; return decodeAois(hash); }); @@ -34,7 +45,7 @@ export const aoisUpdateGeometryAtom = atom( newFeatures = [...newFeatures, newFeature]; } }); - set(aoisHashAtom, encodeAois(newFeatures)); + set(aoisSerialized, encodeAois(newFeatures)); } ); @@ -44,7 +55,7 @@ export const aoisSetSelectedAtom = atom(null, (get, set, ids: string[]) => { const newFeatures = features.map((feature) => { return { ...feature, selected: ids.includes(feature.id as string) }; }); - set(aoisHashAtom, encodeAois(newFeatures)); + set(aoisSerialized, encodeAois(newFeatures)); }); // Setter atom to delete AOIs, writing directly to the hash atom. @@ -53,5 +64,5 @@ export const aoisDeleteAtom = atom(null, (get, set, ids: string[]) => { const newFeatures = features.filter( (feature) => !ids.includes(feature.id as string) ); - set(aoisHashAtom, encodeAois(newFeatures)); + set(aoisSerialized, encodeAois(newFeatures)); });