Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store selected exploration datasets on url #708

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 80 additions & 4 deletions app/scripts/components/exploration/atoms/atoms.ts
Original file line number Diff line number Diff line change
@@ -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<TimelineDataset[]>([]);
// 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<TimelineDataset[]>([]);

// 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[] | (<T extends 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<Date | null>(null);
// Compare date. This is the compare date for the datasets shown on the map.
export const selectedCompareDateAtom = atom<Date | null>(null);
Expand Down
24 changes: 22 additions & 2 deletions app/scripts/components/exploration/data-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion app/scripts/components/exploration/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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), []);
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/components/exploration/types.d.ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading