diff --git a/app/scripts/components/exploration/atoms/atoms.ts b/app/scripts/components/exploration/atoms/atoms.ts index 2f3300213..b3c98a2bf 100644 --- a/app/scripts/components/exploration/atoms/atoms.ts +++ b/app/scripts/components/exploration/atoms/atoms.ts @@ -1,11 +1,87 @@ import { atom } from 'jotai'; +import { atomWithLocation } from 'jotai-location'; import { HEADER_COLUMN_WIDTH, RIGHT_AXIS_SPACE } from '../constants'; -import { DateRange, TimelineDataset, ZoomTransformPlain } from '../types.d.ts'; +import { + datasetLayers, + reconcileDatasets, + urlDatasetsDehydrate, + urlDatasetsHydrate +} from '../data-utils'; +import { + DateRange, + TimelineDataset, + TimelineDatasetForUrl, + ZoomTransformPlain +} from '../types.d.ts'; -// Datasets to show on the timeline and their settings -export const timelineDatasetsAtom = atom([]); -// Main timeline date. This is the date for the datasets shown on the map. +// This is the atom acting as a single source of truth for the AOIs. +const locAtom = atomWithLocation(); + +// Dataset data that is serialized to the url. Only the data needed to +// reconstruct the dataset (and user interaction data like settings) is stored +// in the url, otherwise it would be too long. +const datasetsUrlConfig = atom( + (get): TimelineDatasetForUrl[] => { + try { + const serialized = get(locAtom).searchParams?.get('datasets') ?? '[]'; + return urlDatasetsHydrate(serialized); + } catch (error) { + return []; + } + }, + (get, set, datasets: TimelineDataset[]) => { + // Extract need properties from the datasets and encode them. + const encoded = urlDatasetsDehydrate(datasets); + set(locAtom, (prev) => ({ + ...prev, + searchParams: new URLSearchParams([['datasets', encoded]]) + })); + } +); + +const timelineDatasetsStorageAtom = atom([]); + +// Datasets to show on the timeline and their settings. +export const timelineDatasetsAtom = atom( + (get) => { + const urlDatasets = get(datasetsUrlConfig); + const datasets = get(timelineDatasetsStorageAtom); + + // Reconcile what needs to be reconciled. + return urlDatasets.map((enc) => { + // We only want to do this on load. If the dataset was already + // initialized, skip. + // WARNING: This means that changing settings directly in the url without + // a page refresh will do nothing. + const readyDataset = datasets.find((d) => d.data.id === enc.id); + if (readyDataset) { + return readyDataset; + } + // Reconcile the dataset with the internal data (from VEDA config files) + // and then add the url stored settings. + const [reconciled] = reconcileDatasets([enc.id], datasetLayers, []); + if (enc.settings) { + reconciled.settings = enc.settings; + } + return reconciled; + }); + }, + ( + get, + set, + updates: TimelineDataset[] | ((prev: T[]) => T[]) + ) => { + const newData = + typeof updates === 'function' + ? updates(get(timelineDatasetsStorageAtom)) + : updates; + + set(datasetsUrlConfig, newData); + set(timelineDatasetsStorageAtom, newData); + } +); +// Main timeline date. This date defines the datasets shown on the map. export const selectedDateAtom = atom(null); // Compare date. This is the compare date for the datasets shown on the map. export const selectedCompareDateAtom = atom(null); diff --git a/app/scripts/components/exploration/data-utils.ts b/app/scripts/components/exploration/data-utils.ts index 57ebd33a2..e7b24c667 100644 --- a/app/scripts/components/exploration/data-utils.ts +++ b/app/scripts/components/exploration/data-utils.ts @@ -11,9 +11,13 @@ import { StacDatasetData, TimeDensity, TimelineDataset, + TimelineDatasetForUrl, TimelineDatasetStatus } from './types.d.ts'; -import { DataMetric, DATA_METRICS } from './components/datasets/analysis-metrics'; +import { + DataMetric, + DATA_METRICS +} from './components/datasets/analysis-metrics'; import { utcString2userTzDate } from '$utils/date'; @@ -30,7 +34,6 @@ export const datasetLayers = Object.values(datasets).flatMap( (dataset) => dataset!.data.layers ); - /** * Returns an array of metrics based on the given Dataset Layer configuration. * If the layer has metrics defined, it returns only the metrics that match the @@ -145,3 +148,20 @@ export function getTimeDensityStartDate(date: Date, timeDensity: TimeDensity) { return startOfDay(date); } + +export function urlDatasetsDehydrate(datasets: TimelineDataset[]) { + return JSON.stringify( + datasets.map((d) => ({ + id: d.data.id, + settings: d.settings + })) + ); +} + +export function urlDatasetsHydrate( + encoded: string | null | undefined +): TimelineDatasetForUrl[] { + if (!encoded) return []; + const parsed = JSON.parse(encoded); + return parsed; +} diff --git a/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts b/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts index edc95cd00..6f7fba9e7 100644 --- a/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts +++ b/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts @@ -144,7 +144,7 @@ export function useStacMetadataOnDatasets() { useEffectPrevious<[typeof datasetsQueryData, TimelineDataset[]]>( (prev) => { const prevQueryData = prev[0]; - if (!prevQueryData) return; + const hasPrev = !!prevQueryData; const { changed, data: updatedDatasets } = datasets .filter((d) => !(d as any).mocked) @@ -155,7 +155,9 @@ export function useStacMetadataOnDatasets() { (acc, dataset, idx) => { const curr = datasetsQueryData[idx]; - if (didDataChange(curr, prevQueryData[idx])) { + // We want to reconcile the data event if it is the first time. + // In practice data will have changes, since prev is undefined. + if (!hasPrev || didDataChange(curr, prevQueryData[idx])) { // Changed return { changed: true, diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index 80d8f5c07..07b874b4e 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useState } from 'react'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import styled from 'styled-components'; +import { useAtomValue } from 'jotai'; import { themeVal } from '@devseed-ui/theme-provider'; import { MockControls } from './datasets-mock'; import Timeline from './components/timeline/timeline'; import { ExplorationMap } from './components/map'; import { DatasetSelectorModal } from './components/dataset-selector-modal'; +import { timelineDatasetsAtom } from './atoms/atoms'; import { LayoutProps } from '$components/common/layout-root'; import PageHero from '$components/common/page-hero'; @@ -55,7 +57,10 @@ const Container = styled.div` `; function Exploration() { - const [datasetModalRevealed, setDatasetModalRevealed] = useState(true); + const datasets = useAtomValue(timelineDatasetsAtom); + const [datasetModalRevealed, setDatasetModalRevealed] = useState( + !datasets.length + ); const openModal = useCallback(() => setDatasetModalRevealed(true), []); const closeModal = useCallback(() => setDatasetModalRevealed(false), []); diff --git a/app/scripts/components/exploration/types.d.ts.ts b/app/scripts/components/exploration/types.d.ts.ts index 181df04d1..35a2ea706 100644 --- a/app/scripts/components/exploration/types.d.ts.ts +++ b/app/scripts/components/exploration/types.d.ts.ts @@ -138,6 +138,11 @@ export type TimelineDataset = // END TimelineDataset type discriminants +export interface TimelineDatasetForUrl { + id: string; + settings?: TimelineDatasetSettings; +} + export interface DateRange { start: Date; end: Date;