diff --git a/app/scripts/components/common/map/utils.ts b/app/scripts/components/common/map/utils.ts index f533ece4a..0e84b27cf 100644 --- a/app/scripts/components/common/map/utils.ts +++ b/app/scripts/components/common/map/utils.ts @@ -1,19 +1,23 @@ 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 { StacFeature } from "./types"; -import { userTzDate2utcString } from "$utils/date"; -import { validateRangeNum } from "$utils/utils"; +import { endOfDay, startOfDay } from 'date-fns'; +import { + DatasetDatumFn, + DatasetDatumFnResolverBag, + DatasetDatumReturnType +} from 'veda'; +import { StacFeature } from './types'; + +import { userTzDate2utcString } from '$utils/date'; +import { validateRangeNum } from '$utils/utils'; export const FIT_BOUNDS_PADDING = 32; export const validateLon = validateRangeNum(-180, 180); export const validateLat = validateRangeNum(-90, 90); - - export function getMergedBBox(features: StacFeature[]) { const mergedBBox = [ Number.POSITIVE_INFINITY, @@ -32,8 +36,6 @@ export function getMergedBBox(features: StacFeature[]) { ) as [number, number, number, number]; } - - export function checkFitBoundsFromLayer( layerBounds?: [number, number, number, number], mapInstance?: MapboxMap | MapRef @@ -58,8 +60,6 @@ export function checkFitBoundsFromLayer( return layerExtentSmaller || isOutside; } - - /** * Creates the appropriate filter object to send to STAC. * @@ -87,7 +87,6 @@ export function getFilterPayload(date: Date, collection: string) { }; } - // There are cases when the data can't be displayed properly on low zoom levels. // In these cases instead of displaying the raster tiles, we display markers to // indicate whether or not there is data in a given location. When the user @@ -123,3 +122,63 @@ export async function requestQuickCache({ return quickCache.get(key); } +type Fn = (...args: any[]) => any; + +type ObjResMap = { + [K in keyof T]: Res; +}; + +type Res = T extends Fn + ? T extends DatasetDatumFn + ? DatasetDatumReturnType + : never + : T extends any[] + ? Res[] + : T extends object + ? ObjResMap + : T; + +export function resolveConfigFunctions( + datum: T, + bag: DatasetDatumFnResolverBag +): Res; +/* eslint-disable-next-line no-redeclare */ +export function resolveConfigFunctions( + datum: T, + bag: DatasetDatumFnResolverBag +): Res[]; +/* eslint-disable-next-line no-redeclare */ +export function resolveConfigFunctions( + datum: any, + bag: DatasetDatumFnResolverBag +): any { + if (Array.isArray(datum)) { + return datum.map((v) => resolveConfigFunctions(v, bag)); + } + + if (datum != null && typeof datum === 'object') { + // Use for loop instead of reduce as it faster. + const ready = {}; + for (const [k, v] of Object.entries(datum as object)) { + ready[k] = resolveConfigFunctions(v, bag); + } + return ready; + } + + if (typeof datum === 'function') { + try { + return datum(bag); + } catch (error) { + /* eslint-disable-next-line no-console */ + console.error( + 'Failed to resolve function %s(%o) with error %s', + datum.name, + bag, + error.message + ); + return null; + } + } + + return datum; +} diff --git a/app/scripts/components/exploration/data-utils.ts b/app/scripts/components/exploration/data-utils.ts index e151ef151..cca52fd68 100644 --- a/app/scripts/components/exploration/data-utils.ts +++ b/app/scripts/components/exploration/data-utils.ts @@ -1,7 +1,10 @@ import { eachDayOfInterval, eachMonthOfInterval, - eachYearOfInterval + eachYearOfInterval, + startOfDay, + startOfMonth, + startOfYear } from 'date-fns'; import { DatasetLayer, datasets } from 'veda'; import { @@ -102,3 +105,14 @@ export function resolveLayerTemporalExtent( ); } } + +export function getTimeDensityStartDate(date: Date, timeDensity: TimeDensity) { + switch (timeDensity) { + case TimeDensity.MONTH: + return startOfMonth(date); + case TimeDensity.YEAR: + return startOfYear(date); + } + + return startOfDay(date); +} diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index deadf1591..bd07446bc 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -1,12 +1,19 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, 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'; +// Avoid error: node_modules/date-fns/esm/index.js does not export 'default' +import * as dateFns from 'date-fns'; import { MockControls } from './datasets-mock'; import Timeline from './components/timeline/timeline'; import { DatasetSelectorModal } from './components/dataset-selector-modal'; import { useStacMetadataOnDatasets } from './hooks/use-stac-metadata-datasets'; +import { selectedDateAtom, timelineDatasetsAtom } from './atoms/atoms'; +import { TimelineDatasetStatus, TimelineDatasetSuccess } from './types.d.ts'; +import { getTimeDensityStartDate } from './data-utils'; +import { useTimelineDatasetAtom, useTimelineDatasetSettings } from './atoms/hooks'; import { LayoutProps } from '$components/common/layout-root'; import PageHero from '$components/common/page-hero'; @@ -23,6 +30,7 @@ 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 { RasterTimeseries } from '$components/common/map/style-generators/raster-timeseries'; +import { resolveConfigFunctions } from '$components/common/map/utils'; const Container = styled.div` display: flex; @@ -67,7 +75,7 @@ const Container = styled.div` `; function Exploration() { - const [compare, setCompare] = useState(true); + const [compare, setCompare] = useState(false); const [datasetModalRevealed, setDatasetModalRevealed] = useState(true); const openModal = useCallback(() => setDatasetModalRevealed(true), []); @@ -85,6 +93,14 @@ function Exploration() { useStacMetadataOnDatasets(); + const datasets = useAtomValue(timelineDatasetsAtom); + const selectedDay = useAtomValue(selectedDateAtom); + + const loadedDatasets = datasets.filter( + (d): d is TimelineDatasetSuccess => + d.status === TimelineDatasetStatus.SUCCESS + ); + return ( <> - + {selectedDay && + loadedDatasets.map((dataset, idx) => ( + + ))} {/* Map controls */} @@ -173,3 +187,49 @@ function Exploration() { ); } export default Exploration; + +interface LayerProps { + dataset: TimelineDatasetSuccess; + order: number; + selectedDay: Date; +} + +function Layer(props: LayerProps) { + const { dataset, order, selectedDay } = props; + + const datasetAtom = useTimelineDatasetAtom(dataset.data.id); + const [getSettings] = useTimelineDatasetSettings(datasetAtom); + + const isVisible = getSettings('isVisible'); + const opacity = getSettings('opacity'); + + // The date needs to match the dataset's time density. + const relevantDate = useMemo( + () => getTimeDensityStartDate(selectedDay, dataset.data.timeDensity), + [selectedDay, dataset.data.timeDensity] + ); + + // Resolve config functions. + const params = useMemo(() => { + const bag = { + date: relevantDate, + compareDatetime: relevantDate, + dateFns, + raw: dataset.data + }; + return resolveConfigFunctions(dataset.data, bag); + }, [dataset, relevantDate]); + + return ( +