From 99495370eb39455626fb1f7a0fa3b47ec1dd9463 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 22 Sep 2023 13:17:04 +0100 Subject: [PATCH 1/7] Add layerOrder to raster timeseries layer --- .../style-generators/raster-timeseries.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/scripts/components/common/map/style-generators/raster-timeseries.tsx b/app/scripts/components/common/map/style-generators/raster-timeseries.tsx index ecc88e709..d7b658fc2 100644 --- a/app/scripts/components/common/map/style-generators/raster-timeseries.tsx +++ b/app/scripts/components/common/map/style-generators/raster-timeseries.tsx @@ -17,7 +17,7 @@ import { FIT_BOUNDS_PADDING, getFilterPayload, getMergedBBox, - requestQuickCache, + requestQuickCache } from '../utils'; import useFitBbox from '../hooks/use-fit-bbox'; import useLayerInteraction from '../hooks/use-layer-interaction'; @@ -32,7 +32,6 @@ import { S_SUCCEEDED } from '$utils/status'; - // Whether or not to print the request logs. const LOG = true; @@ -45,6 +44,7 @@ export interface RasterTimeseriesProps extends BaseGeneratorParams { bounds?: number[]; onStatusChange?: (result: { status: ActionStatus; id: string }) => void; isPositionSet?: boolean; + layerOrder: number; } enum STATUS_KEY { @@ -70,16 +70,16 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { onStatusChange, isPositionSet, hidden, + layerOrder } = props; - const { current: mapInstance } = useMaps(); const theme = useTheme(); const { updateStyle } = useMapStyle(); const minZoom = zoomExtent?.[0] ?? 0; - const generatorId = 'raster-timeseries' + id; + const generatorId = `#${layerOrder}-raster-timeseries-${id}`; // Status tracking. // A raster timeseries layer has a base layer and may have markers. @@ -288,11 +288,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { } LOG && /* eslint-disable-next-line no-console */ - console.log( - 'RasterTimeseries %cAborted Mosaic', - 'color: red;', - id - ); + console.log('RasterTimeseries %cAborted Mosaic', 'color: red;', id); return; } }; @@ -343,7 +339,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { const tileParams = qs.stringify( { assets: 'cog_default', - ...sourceParams + ...(sourceParams ?? {}) }, // Temporary solution to pass different tile parameters for hls data { @@ -416,7 +412,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { id: pointsSourceId, source: pointsSourceId, layout: { - ...MARKER_LAYOUT as any, + ...(MARKER_LAYOUT as any), 'icon-allow-overlap': true }, paint: { @@ -450,7 +446,8 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { controller.abort(); }; }, - // sourceParams not included, but using a stringified version of it to detect changes (haveSourceParamsChanged) + // sourceParams not included, but using a stringified version of it to + // detect changes (haveSourceParamsChanged) [ updateStyle, id, From ac17b50c9624552af6beb5f09a19fd27b335540c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 22 Sep 2023 13:17:22 +0100 Subject: [PATCH 2/7] Add opacity support to raster timeseries layer --- .../common/map/style-generators/raster-timeseries.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/components/common/map/style-generators/raster-timeseries.tsx b/app/scripts/components/common/map/style-generators/raster-timeseries.tsx index d7b658fc2..f249f0343 100644 --- a/app/scripts/components/common/map/style-generators/raster-timeseries.tsx +++ b/app/scripts/components/common/map/style-generators/raster-timeseries.tsx @@ -45,6 +45,7 @@ export interface RasterTimeseriesProps extends BaseGeneratorParams { onStatusChange?: (result: { status: ActionStatus; id: string }) => void; isPositionSet?: boolean; layerOrder: number; + opacity?: number; } enum STATUS_KEY { @@ -70,6 +71,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { onStatusChange, isPositionSet, hidden, + opacity, layerOrder } = props; @@ -372,12 +374,14 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { url: tilejsonUrl }; + const rasterOpacity = typeof opacity === 'number' ? opacity / 100 : 1; + const mosaicLayer: RasterLayer = { id: id, type: 'raster', source: id, paint: { - 'raster-opacity': Number(!hidden), + 'raster-opacity': hidden ? 0 : rasterOpacity, 'raster-opacity-transition': { duration: 320 } @@ -456,6 +460,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { points, haveSourceParamsChanged, hidden, + opacity, generatorId ] ); From 3be9f44fc00b2baf106230e9188cbd3ad439bde5 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 22 Sep 2023 13:17:52 +0100 Subject: [PATCH 3/7] Improve dataset settings hook typings --- .../components/exploration/atoms/hooks.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/scripts/components/exploration/atoms/hooks.ts b/app/scripts/components/exploration/atoms/hooks.ts index 6de88a1bc..e8eac26c9 100644 --- a/app/scripts/components/exploration/atoms/hooks.ts +++ b/app/scripts/components/exploration/atoms/hooks.ts @@ -73,17 +73,13 @@ export function useTimelineDatasetAtom(id: string) { return datasetAtom as PrimitiveAtom; } +type Settings = TimelineDataset['settings']; + type TimelineDatasetSettingsReturn = [ - ( - prop: keyof TimelineDataset['settings'] - ) => TimelineDataset['settings'][keyof TimelineDataset['settings']], - ( - prop: keyof TimelineDataset['settings'], - value: - | TimelineDataset['settings'][keyof TimelineDataset['settings']] - | (( - prev: TimelineDataset['settings'][keyof TimelineDataset['settings']] - ) => TimelineDataset['settings'][keyof TimelineDataset['settings']]) + (prop: T) => Settings[T], + ( + prop: T, + value: Settings[T] | ((prev: Settings[T]) => Settings[T]) ) => void ]; From 9a1c07c00898d6040bdebc9a07262b33ef8bca52 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 22 Sep 2023 13:18:19 +0100 Subject: [PATCH 4/7] Render selected datasets on map --- app/scripts/components/common/map/utils.ts | 81 ++++++++++++++--- .../components/exploration/data-utils.ts | 16 +++- app/scripts/components/exploration/index.tsx | 86 ++++++++++++++++--- 3 files changed, 158 insertions(+), 25 deletions(-) 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 ( +