From 92f05747e5928f053201631fcc1ba22a16029eb7 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 22 Sep 2023 13:06:02 -0400 Subject: [PATCH 01/53] Thorw errors if too many statistics are requested --- app/scripts/components/analysis/results/timeseries-data.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/scripts/components/analysis/results/timeseries-data.ts b/app/scripts/components/analysis/results/timeseries-data.ts index eaae0ffc9..8fc621d32 100644 --- a/app/scripts/components/analysis/results/timeseries-data.ts +++ b/app/scripts/components/analysis/results/timeseries-data.ts @@ -85,6 +85,8 @@ interface StacDatasetsTimeseriesEvented { off: (event: 'data') => void; } +const MAX_QUERY_NUM = 300; + export function requestStacDatasetsTimeseries({ start, end, @@ -252,6 +254,11 @@ async function requestTimeseries({ const { assets, ...otherCollectionProps } = layerInfoFromSTAC; + if (assets.length > MAX_QUERY_NUM) + throw Error( + `Too many requests. We currently only allow requests up to ${MAX_QUERY_NUM} and this analysis requires ${assets.length} requests.` + ); + onData({ ...layersBase, status: 'loading', From 441f059f32c05892031cfdffb5c0017a98072229 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Mon, 25 Sep 2023 16:25:35 +0200 Subject: [PATCH 02/53] Return the guessed number of items based on collection metadata --- .../define/use-stac-collection-search.ts | 50 +++++++++++++++++-- .../analysis/results/timeseries-data.ts | 2 +- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index 2fed3097f..975172783 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -4,8 +4,9 @@ import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; import booleanIntersects from '@turf/boolean-intersects'; import bboxPolygon from '@turf/bbox-polygon'; -import { areIntervalsOverlapping } from 'date-fns'; +import { areIntervalsOverlapping, eachDayOfInterval, eachMonthOfInterval, eachYearOfInterval } from 'date-fns'; +import { TimeseriesDataResult } from '../results/timeseries-data'; import { allAvailableDatasetsLayers } from '.'; import { utcString2userTzDate } from '$utils/date'; @@ -16,6 +17,12 @@ interface UseStacSearchProps { aoi?: FeatureCollection | null; } +const DATE_INTERVAL_FN = { + day: eachDayOfInterval, + month: eachMonthOfInterval, + year: eachYearOfInterval +}; + const collectionUrl = `${process.env.API_STAC_ENDPOINT}/collections`; export function useStacCollectionSearch({ @@ -47,13 +54,36 @@ export function useStacCollectionSearch({ } }, [result.data, aoi, start, end]); + const selectableDatasetLayersWithNumberOfItems = selectableDatasetLayers.map( + (l) => { + const numberOfItems = getNumberOfItemsWithinTimeRange( + start, + end, + l + ); + return { ...l, numberOfItems }; + } + ); + return { - selectableDatasetLayers: selectableDatasetLayers, + selectableDatasetLayers: selectableDatasetLayersWithNumberOfItems, stacSearchStatus: result.status, readyToLoadDatasets }; } +function getNumberOfItemsWithinTimeRange(start, end, collection) { + const {isPeriodic, timeDensity, domain, timeseries} = collection; + if (!isPeriodic) { + return timeseries.length; // Check in with back-end team + } + const eachOf = DATE_INTERVAL_FN[timeDensity]; + const statStart = +(new Date(domain[0])) > +(new Date(start))? new Date(domain[0]): new Date(start); + const statEnd = +(new Date(domain[1])) < +(new Date(end))? new Date(domain[1]): new Date(end); + + return eachOf({start: statStart, end: statEnd}).length; +} + function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { const matchingCollectionIds = collectionData.reduce((acc, col) => { const id = col.id; @@ -95,7 +125,21 @@ function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { } }, []); - return allAvailableDatasetsLayers.filter((l) => + const filteredDatasets = allAvailableDatasetsLayers.filter((l) => matchingCollectionIds.includes(l.stacCol) ); + + const filteredDatasetsWithCollections: TimeseriesDataResult[] = + filteredDatasets.map((l) => { + const collection = collectionData.find((c) => c.id === l.stacCol); + return { + ...l, + isPeriodic: collection['dashboard:is_periodic'], + timeDensity: collection['dashboard:time_density'], + domain: collection.extent.temporal.interval[0], + timeseries: collection.summaries.datetime + }; + }); + + return filteredDatasetsWithCollections; } diff --git a/app/scripts/components/analysis/results/timeseries-data.ts b/app/scripts/components/analysis/results/timeseries-data.ts index 8fc621d32..628c7e4b4 100644 --- a/app/scripts/components/analysis/results/timeseries-data.ts +++ b/app/scripts/components/analysis/results/timeseries-data.ts @@ -30,7 +30,7 @@ export interface TimeseriesDataUnit { percentile_98: number; } -interface TimeseriesDataResult { +export interface TimeseriesDataResult { isPeriodic: boolean; timeDensity: TimeDensity; domain: string[]; From 42de6bc77f92cbd8b0e234b83b73dc8caa45ce83 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Mon, 25 Sep 2023 17:12:48 +0200 Subject: [PATCH 03/53] Fixed types and variable names --- .../analysis/{define => }/constants.ts | 2 + .../analysis/define/aoi-selector.tsx | 2 +- .../define/use-stac-collection-search.ts | 42 ++++++++++++------- .../analysis/results/timeseries-data.ts | 3 +- 4 files changed, 32 insertions(+), 17 deletions(-) rename app/scripts/components/analysis/{define => }/constants.ts (94%) diff --git a/app/scripts/components/analysis/define/constants.ts b/app/scripts/components/analysis/constants.ts similarity index 94% rename from app/scripts/components/analysis/define/constants.ts rename to app/scripts/components/analysis/constants.ts index 79c6101ba..bee2fe167 100644 --- a/app/scripts/components/analysis/define/constants.ts +++ b/app/scripts/components/analysis/constants.ts @@ -27,3 +27,5 @@ export const FeatureByRegionPreset: Record< } ]) }; + +export const MAX_QUERY_NUM = 300; \ No newline at end of file diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 2915c87c4..ce7b05da7 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -25,7 +25,7 @@ import { CollecticonPencil, CollecticonUpload2 } from '@devseed-ui/collecticons'; -import { FeatureByRegionPreset, RegionPreset } from './constants'; +import { FeatureByRegionPreset, RegionPreset } from '../constants'; import AoIUploadModal from './aoi-upload-modal'; import { Fold, diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index 975172783..a95d2b6d3 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -4,7 +4,13 @@ import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; import booleanIntersects from '@turf/boolean-intersects'; import bboxPolygon from '@turf/bbox-polygon'; -import { areIntervalsOverlapping, eachDayOfInterval, eachMonthOfInterval, eachYearOfInterval } from 'date-fns'; +import { + areIntervalsOverlapping, + eachDayOfInterval, + eachMonthOfInterval, + eachYearOfInterval +} from 'date-fns'; +import { DatasetLayer } from 'veda'; import { TimeseriesDataResult } from '../results/timeseries-data'; import { allAvailableDatasetsLayers } from '.'; @@ -17,6 +23,8 @@ interface UseStacSearchProps { aoi?: FeatureCollection | null; } +export type DatasetWithTimeseriesData = TimeseriesDataResult & DatasetLayer & { numberOfItems?: number }; + const DATE_INTERVAL_FN = { day: eachDayOfInterval, month: eachMonthOfInterval, @@ -54,13 +62,9 @@ export function useStacCollectionSearch({ } }, [result.data, aoi, start, end]); - const selectableDatasetLayersWithNumberOfItems = selectableDatasetLayers.map( + const selectableDatasetLayersWithNumberOfItems: DatasetWithTimeseriesData[] = selectableDatasetLayers.map( (l) => { - const numberOfItems = getNumberOfItemsWithinTimeRange( - start, - end, - l - ); + const numberOfItems = getNumberOfItemsWithinTimeRange(start, end, l); return { ...l, numberOfItems }; } ); @@ -72,16 +76,26 @@ export function useStacCollectionSearch({ }; } -function getNumberOfItemsWithinTimeRange(start, end, collection) { - const {isPeriodic, timeDensity, domain, timeseries} = collection; +/** + * For each collection, get the number of items within the time range, + * taking into account the time density. + */ +function getNumberOfItemsWithinTimeRange(userStart, userEnd, collection) { + const { isPeriodic, timeDensity, domain, timeseries } = collection; if (!isPeriodic) { return timeseries.length; // Check in with back-end team } const eachOf = DATE_INTERVAL_FN[timeDensity]; - const statStart = +(new Date(domain[0])) > +(new Date(start))? new Date(domain[0]): new Date(start); - const statEnd = +(new Date(domain[1])) < +(new Date(end))? new Date(domain[1]): new Date(end); - - return eachOf({start: statStart, end: statEnd}).length; + const start = + +new Date(domain[0]) > +new Date(userStart) + ? new Date(domain[0]) + : new Date(userStart); + const end = + +new Date(domain[1]) < +new Date(userEnd) + ? new Date(domain[1]) + : new Date(userEnd); + + return eachOf({ start, end }).length; } function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { @@ -129,7 +143,7 @@ function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { matchingCollectionIds.includes(l.stacCol) ); - const filteredDatasetsWithCollections: TimeseriesDataResult[] = + const filteredDatasetsWithCollections = filteredDatasets.map((l) => { const collection = collectionData.find((c) => c.id === l.stacCol); return { diff --git a/app/scripts/components/analysis/results/timeseries-data.ts b/app/scripts/components/analysis/results/timeseries-data.ts index 628c7e4b4..9d8b16ba8 100644 --- a/app/scripts/components/analysis/results/timeseries-data.ts +++ b/app/scripts/components/analysis/results/timeseries-data.ts @@ -3,6 +3,7 @@ import { QueryClient } from '@tanstack/react-query'; import { FeatureCollection, Polygon } from 'geojson'; import { DatasetLayer } from 'veda'; +import { MAX_QUERY_NUM } from '../constants'; import { getFilterPayload, combineFeatureCollection } from '../utils'; import EventEmitter from './mini-events'; import { ConcurrencyManager, ConcurrencyManagerInstance } from './concurrency'; @@ -85,8 +86,6 @@ interface StacDatasetsTimeseriesEvented { off: (event: 'data') => void; } -const MAX_QUERY_NUM = 300; - export function requestStacDatasetsTimeseries({ start, end, From c0f6b72288c6eec12435325ce8f5f720c6e711e0 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Mon, 25 Sep 2023 17:14:17 +0200 Subject: [PATCH 04/53] Memoize datasets --- .../define/use-stac-collection-search.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index a95d2b6d3..66bfd7d3c 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -23,7 +23,8 @@ interface UseStacSearchProps { aoi?: FeatureCollection | null; } -export type DatasetWithTimeseriesData = TimeseriesDataResult & DatasetLayer & { numberOfItems?: number }; +export type DatasetWithTimeseriesData = TimeseriesDataResult & + DatasetLayer & { numberOfItems?: number }; const DATE_INTERVAL_FN = { day: eachDayOfInterval, @@ -62,12 +63,13 @@ export function useStacCollectionSearch({ } }, [result.data, aoi, start, end]); - const selectableDatasetLayersWithNumberOfItems: DatasetWithTimeseriesData[] = selectableDatasetLayers.map( - (l) => { - const numberOfItems = getNumberOfItemsWithinTimeRange(start, end, l); - return { ...l, numberOfItems }; - } - ); + const selectableDatasetLayersWithNumberOfItems: DatasetWithTimeseriesData[] = + useMemo(() => { + return selectableDatasetLayers.map((l) => { + const numberOfItems = getNumberOfItemsWithinTimeRange(start, end, l); + return { ...l, numberOfItems }; + }); + }, [selectableDatasetLayers, start, end]); return { selectableDatasetLayers: selectableDatasetLayersWithNumberOfItems, @@ -143,17 +145,16 @@ function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { matchingCollectionIds.includes(l.stacCol) ); - const filteredDatasetsWithCollections = - filteredDatasets.map((l) => { - const collection = collectionData.find((c) => c.id === l.stacCol); - return { - ...l, - isPeriodic: collection['dashboard:is_periodic'], - timeDensity: collection['dashboard:time_density'], - domain: collection.extent.temporal.interval[0], - timeseries: collection.summaries.datetime - }; - }); + const filteredDatasetsWithCollections = filteredDatasets.map((l) => { + const collection = collectionData.find((c) => c.id === l.stacCol); + return { + ...l, + isPeriodic: collection['dashboard:is_periodic'], + timeDensity: collection['dashboard:time_density'], + domain: collection.extent.temporal.interval[0], + timeseries: collection.summaries.datetime + }; + }); return filteredDatasetsWithCollections; } From 64a81aaba7b36069ee225e97a564ff1a5b2be1dd Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Mon, 25 Sep 2023 16:57:08 -0400 Subject: [PATCH 05/53] Check only day level equality --- app/scripts/components/datasets/s-explore/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/scripts/components/datasets/s-explore/index.tsx b/app/scripts/components/datasets/s-explore/index.tsx index 7afcbe429..31fca123f 100644 --- a/app/scripts/components/datasets/s-explore/index.tsx +++ b/app/scripts/components/datasets/s-explore/index.tsx @@ -8,6 +8,7 @@ import React, { import styled from 'styled-components'; import { useNavigate } from 'react-router'; import useQsStateCreator from 'qs-state-hook'; +import { isSameDay } from 'date-fns'; import { themeVal } from '@devseed-ui/theme-provider'; import { CollecticonExpandFromLeft, @@ -113,9 +114,10 @@ const isSelectedDateValid = (dateList, selectedDate) => { // Since the available dates changes, check if the currently selected // one is valid. const validDate = !!dateList.find( - (d) => d.getTime() === selectedDate?.getTime() + // Only check if the date of the selected date is in the range. + // Because Dashboard can only provide daily level selection. + (d) => isSameDay(new Date(d), new Date(selectedDate)) ); - return !!validDate; } return true; @@ -175,7 +177,6 @@ const useDatePickerValue = ( }), [value] ); - return [val, onConfirm] as [typeof val, typeof onConfirm]; }; @@ -518,6 +519,7 @@ function DatasetsExplore() { selectedDatetime, setSelectedDatetime ); + const [datePickerCompareValue, datePickerCompareConfirm] = useDatePickerValue( selectedCompareDatetime, setSelectedCompareDatetime From f1e7171c64e8198772ee2c9848e4cb00f9ab7fb9 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 11:06:22 +0200 Subject: [PATCH 06/53] Updated analysis flow wording --- .../components/analysis/define/aoi-selector.tsx | 6 +++++- app/scripts/components/analysis/define/index.tsx | 14 +++++++++++--- app/scripts/components/common/fold.tsx | 4 +++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index ce7b05da7..3bbe5a8a3 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -128,7 +128,11 @@ export default function AoiSelector({ /> - Area + Select area of interest +

+ Use the pencil tool to draw a shape on the map or upload your own + shapefile. +

diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 503613123..0864735eb 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -237,7 +237,7 @@ export default function Analysis() { description='Generate timeseries data for your area of interest.' /> ( - Date + Pick a date period +

+ Select start and end date of time series, or choose a pre-set date + range. +

@@ -350,7 +354,11 @@ export default function Analysis() { - Datasets + Select datasets +

+ Select from available dataset layers for the area and date range + selected. +

diff --git a/app/scripts/components/common/fold.tsx b/app/scripts/components/common/fold.tsx index a00f7d393..7b412b1cf 100644 --- a/app/scripts/components/common/fold.tsx +++ b/app/scripts/components/common/fold.tsx @@ -49,7 +49,9 @@ export const FoldHeader = styled.div` `; export const FoldHeadline = styled.div` - /* styled-component */ + p { + margin: 1rem 0 0 0; + } `; export const FoldHeadActions = styled.div` From 9b24cd4e350edb8a5ee9853d4d5657c8b45ae72f Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 12:49:25 +0200 Subject: [PATCH 07/53] Added sticky footer --- .../components/analysis/define/index.tsx | 366 ++++++++++-------- .../analysis/define/page-footer.actions.tsx | 132 +++++++ .../analysis/define/page-hero-actions.tsx | 130 ------- .../components/analysis/results/index.tsx | 1 + .../analysis/saved-analysis-control.tsx | 10 +- 5 files changed, 340 insertions(+), 299 deletions(-) create mode 100644 app/scripts/components/analysis/define/page-footer.actions.tsx delete mode 100644 app/scripts/components/analysis/define/page-hero-actions.tsx diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 0864735eb..01b32028d 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -49,6 +49,9 @@ import { } from '$utils/date'; import DropMenuItemButton from '$styles/drop-menu-item-button'; import { MapboxMapRef } from '$components/common/mapbox'; +import PageFooterActions from './page-footer.actions'; +import SavedAnalysisControl from '../saved-analysis-control'; +import { ANALYSIS_PATH } from '$utils/routes'; const FormBlock = styled.div` display: flex; @@ -100,6 +103,23 @@ export const Note = styled.div` } `; +const FloatingFooter = styled.div<{ isSticky: boolean }>` + position: sticky; + left: 0; + right: 0; + // trick to get the IntersectionObserver to fire + bottom: -1px; + padding: ${variableGlsp(0.5)}; + background: ${themeVal('color.surface')}; + z-index: 99; + margin-bottom: ${variableGlsp(1)}; + ${(props) => + props.isSticky && + ` + box-shadow: 0 0 10px 0 #0003; + `} +`; + const findParentDataset = (layerId: string) => { const parentDataset = Object.values(datasets).find((dataset) => (dataset as VedaDatum).data.layers.find( @@ -206,7 +226,7 @@ export default function Analysis() { // read/set state loop }, [selectableDatasetLayers, setAnalysisParam]); - const showTip = !readyToLoadDatasets || !datasetsLayers?.length; + const notReady = !readyToLoadDatasets || !datasetsLayers?.length; const infoboxMessage = useMemo(() => { if ( @@ -230,171 +250,189 @@ export default function Analysis() { } }, [readyToLoadDatasets, stacSearchStatus, selectableDatasetLayers.length]); + const footerRef = useRef(null); + const [isFooterSticky, setIsFooterSticky] = React.useState(false); + useEffect(() => { + if (!footerRef.current) return; + const observer = new IntersectionObserver( + ([e]) => { + setIsFooterSticky(e.intersectionRatio < 1); + }, + { threshold: [1] } + ); + observer.observe(footerRef.current); + return () => observer.disconnect(); + }, []); + return ( - - - ( - - )} - /> - - - - - - - Pick a date period -

- Select start and end date of time series, or choose a pre-set date - range. -

-
- - - Actions - ( - - - - )} - > - Select a date preset - -
  • - onDatePresetClick(e, 'yearToDate')} - > - This year - -
  • -
  • - onDatePresetClick(e, 'last30Days')} - > - Last 30 days - -
  • -
  • - onDatePresetClick(e, 'lastYear')} - > - Last year - -
  • -
  • - onDatePresetClick(e, 'last10Years')} - > - Last 10 years - -
  • -
    -
    -
    -
    -
    - -
    - - - - - - - - - -
    -
    -
    - - - - - Select datasets -

    - Select from available dataset layers for the area and date range - selected. -

    -
    -
    - - {!infoboxMessage ? ( + <> + + + ( + + )} + /> + + + + + + + Pick a date period +

    + Select start and end date of time series, or choose a pre-set + date range. +

    +
    + + + Actions + ( + + + + )} + > + Select a date preset + +
  • + onDatePresetClick(e, 'yearToDate')} + > + This year + +
  • +
  • + onDatePresetClick(e, 'last30Days')} + > + Last 30 days + +
  • +
  • + onDatePresetClick(e, 'lastYear')} + > + Last year + +
  • +
  • + onDatePresetClick(e, 'last10Years')} + > + Last 10 years + +
  • +
    +
    +
    +
    +
    +
    - - {selectableDatasetLayers.map((datasetLayer) => ( - - - From: {findParentDataset(datasetLayer.id)?.name} - - {datasetLayer.name} - - ))} - + + + + + + + + +
    - ) : ( - - -

    {infoboxMessage}

    -
    - )} -
    -
    -
    +
    +
    + + + + + Select datasets +

    + Select from available dataset layers for the area and date range + selected. +

    +
    +
    + + {!infoboxMessage ? ( +
    + + {selectableDatasetLayers.map((datasetLayer) => ( + + + From: {findParentDataset(datasetLayer.id)?.name} + + {datasetLayer.name} + + ))} + +
    + ) : ( + + +

    {infoboxMessage}

    +
    + )} +
    +
    +
    + + + + ); } diff --git a/app/scripts/components/analysis/define/page-footer.actions.tsx b/app/scripts/components/analysis/define/page-footer.actions.tsx new file mode 100644 index 000000000..3febcada6 --- /dev/null +++ b/app/scripts/components/analysis/define/page-footer.actions.tsx @@ -0,0 +1,132 @@ +import React, { useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { format } from 'date-fns'; +import { Button, ButtonProps } from '@devseed-ui/button'; +import { CollecticonTickSmall } from '@devseed-ui/collecticons'; + +import { analysisParams2QueryString } from '../results/use-analysis-params'; +import useSavedSettings from '../use-saved-settings'; + +import { composeVisuallyDisabled } from '$utils/utils'; +import { ANALYSIS_RESULTS_PATH } from '$utils/routes'; +import { DatasetLayer } from 'veda'; +import { FeatureCollection, Polygon } from 'geojson'; +import styled from 'styled-components'; +import { calcFeatCollArea } from '$components/common/aoi/utils'; + +const SaveButton = composeVisuallyDisabled(Button); + +interface PageFooterActionsProps { + isNewAnalysis: boolean; + start?: Date; + end?: Date; + datasetsLayers?: DatasetLayer[]; + aoi?: FeatureCollection | null; + disabled?: boolean; +} + +const FooterActions = styled.div` + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + gap: 1rem; +`; + +const FooterRight = styled.div` + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: 1rem; +`; + +const AnalysisDescription = styled.div` + font-size: 0.875rem; + opacity: 0.5; +` + +export default function PageFooterActions({ + // size, + isNewAnalysis, + start, + end, + datasetsLayers, + aoi, + disabled +}: PageFooterActionsProps) { + const analysisParamsQs = useMemo(() => { + if (!start || !end || !datasetsLayers || !aoi) return ''; + return analysisParams2QueryString({ + start, + end, + datasetsLayers, + aoi + }); + }, [start, end, datasetsLayers, aoi]); + + const { onGenerateClick } = useSavedSettings({ + analysisParamsQs, + params: { + start, + end, + datasets: datasetsLayers?.map((d) => d.name), + aoi: aoi ?? undefined + } + }); + + const analysisDescription = useMemo(() => { + if (!start || !end || !datasetsLayers || !aoi) return ''; + const dataset = + datasetsLayers.length === 1 + ? datasetsLayers[0].name + : `${datasetsLayers.length} datasets`; + const area = `over a ${calcFeatCollArea(aoi)} kmĀ² area` + const dates = `from ${format(start, 'MMM d, yyyy')} to ${format(end, 'MMM d, yyyy')}` + return [dataset, area, dates].join(' '); + }, [start, end, datasetsLayers, aoi]); + + return ( + +
    + {!isNewAnalysis && ( + + )} +
    + + {analysisDescription} + + {disabled ? ( + + Generate analysis + + ) : ( + + )} + +
    + ); +} diff --git a/app/scripts/components/analysis/define/page-hero-actions.tsx b/app/scripts/components/analysis/define/page-hero-actions.tsx deleted file mode 100644 index 225ad2aab..000000000 --- a/app/scripts/components/analysis/define/page-hero-actions.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useMemo } from 'react'; -import { Link } from 'react-router-dom'; -import { sticky } from 'tippy.js'; -import { FeatureCollection, Polygon } from 'geojson'; -import { Button, ButtonProps } from '@devseed-ui/button'; -import { - CollecticonTickSmall, - CollecticonXmarkSmall -} from '@devseed-ui/collecticons'; -import { VerticalDivider } from '@devseed-ui/toolbar'; -import { DatasetLayer } from 'veda'; - -import { analysisParams2QueryString } from '../results/use-analysis-params'; -import useSavedSettings from '../use-saved-settings'; -import SavedAnalysisControl from '../saved-analysis-control'; - -import { Tip } from '$components/common/tip'; -import { composeVisuallyDisabled } from '$utils/utils'; -import { useMediaQuery } from '$utils/use-media-query'; -import { ANALYSIS_PATH, ANALYSIS_RESULTS_PATH } from '$utils/routes'; - -const SaveButton = composeVisuallyDisabled(Button); - -interface PageHeroActionsProps { - size: ButtonProps['size']; - isNewAnalysis: boolean; - showTip: boolean; - start?: Date; - end?: Date; - datasetsLayers?: DatasetLayer[]; - aoi?: FeatureCollection | null; -} - -export default function PageHeroActions({ - size, - isNewAnalysis, - showTip, - start, - end, - datasetsLayers, - aoi -}: PageHeroActionsProps) { - const { isLargeUp } = useMediaQuery(); - - const analysisParamsQs = useMemo(() => { - if (!start || !end || !datasetsLayers || !aoi) return ''; - return analysisParams2QueryString({ - start, - end, - datasetsLayers, - aoi - }); - }, [start, end, datasetsLayers, aoi]); - - const { onGenerateClick } = useSavedSettings({ - analysisParamsQs, - params: { - start, - end, - datasets: datasetsLayers?.map((d) => d.name), - aoi: aoi ?? undefined - } - }); - - let tipContents; - - if (showTip) { - tipContents = 'To get results, '; - let instructions: string[] = []; - if (!start || !end) - instructions = [...instructions, 'pick start and end dates']; - if (!aoi) instructions = [...instructions, 'define an area']; - if (!datasetsLayers?.length) - instructions = [...instructions, 'select datasets']; - - const instructionsString = instructions - .join(', ') - .replace(/,\s([^,]+)$/, ' and $1.'); - - tipContents = [tipContents, instructionsString].join(''); - } - - return ( - <> - {!isNewAnalysis && ( - - )} - {showTip ? ( - - - Generate - - - ) : ( - - )} - - - - - - ); -} diff --git a/app/scripts/components/analysis/results/index.tsx b/app/scripts/components/analysis/results/index.tsx index d99ad8477..264215d94 100644 --- a/app/scripts/components/analysis/results/index.tsx +++ b/app/scripts/components/analysis/results/index.tsx @@ -193,6 +193,7 @@ export default function AnalysisResults() { forwardedAs={Link} to={`${ANALYSIS_PATH}${analysisParamsQs}`} size={size} + radius='square' variation='achromic-outline' > Refine diff --git a/app/scripts/components/analysis/saved-analysis-control.tsx b/app/scripts/components/analysis/saved-analysis-control.tsx index 4a6463f23..635ecd275 100644 --- a/app/scripts/components/analysis/saved-analysis-control.tsx +++ b/app/scripts/components/analysis/saved-analysis-control.tsx @@ -4,10 +4,9 @@ import { FeatureCollection, Polygon } from 'geojson'; import styled, { useTheme } from 'styled-components'; import bbox from '@turf/bbox'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; -import { ButtonProps } from '@devseed-ui/button'; +import { Button, ButtonProps } from '@devseed-ui/button'; import { CollecticonClockBack } from '@devseed-ui/collecticons'; -import { ToolbarIconButton } from '@devseed-ui/toolbar'; import { Dropdown, DropMenu, @@ -91,14 +90,15 @@ export default function SavedAnalysisControl({ ( - - - + Past analyses + )} > Past analyses From 35bd7fadd2714cfa34dff03eda75f0f912af7b43 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 14:35:15 +0200 Subject: [PATCH 08/53] Prevent users from selecting datasets with too many data points --- .../components/analysis/define/index.tsx | 140 ++++++++++++++---- .../define/use-stac-collection-search.ts | 26 +++- 2 files changed, 128 insertions(+), 38 deletions(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 01b32028d..7ceb7eb4e 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -17,14 +17,14 @@ import { } from '@devseed-ui/form'; import { CollecticonCircleInformation, - CollecticonEllipsisVertical + CollecticonEllipsisVertical, + CollecticonSignDanger } from '@devseed-ui/collecticons'; import { Overline } from '@devseed-ui/typography'; import { datasets, DatasetLayer, VedaDatum, DatasetData } from 'veda'; import { useAnalysisParams } from '../results/use-analysis-params'; import AoiSelector from './aoi-selector'; -import PageHeroActions from './page-hero-actions'; import { useStacCollectionSearch } from './use-stac-collection-search'; import { variableGlsp } from '$styles/variable-utils'; @@ -103,6 +103,38 @@ export const Note = styled.div` } `; +const UnselectableInfo = styled.div` + font-size: 0.825rem; + font-weight: bold; + display: flex; + align-items: center; + gap: ${variableGlsp(0.5)}; + + & path { + fill: ${themeVal('color.danger')}; + } +`; + +const FormCheckableUnselectable = styled(FormCheckableCustom)` + pointer-events: none; + background: #f0f0f5; +`; + +const DataPointsWarning = styled.div` + display: flex; + align-items: center; + background: ${themeVal('color.danger-100')}; + border-radius: 99px; + font-size: 0.825rem; + font-weight: bold; + margin-top: ${variableGlsp(0.5)}; + paddding: 4px; + + & path { + fill: ${themeVal('color.danger')}; + } +`; + const FloatingFooter = styled.div<{ isSticky: boolean }>` position: sticky; left: 0; @@ -203,12 +235,16 @@ export default function Analysis() { [setAnalysisParam, datasetsLayers] ); - const { selectableDatasetLayers, stacSearchStatus, readyToLoadDatasets } = - useStacCollectionSearch({ - start, - end, - aoi: aoiDrawState.featureCollection - }); + const { + selectableDatasetLayers, + unselectableDatasetLayers, + stacSearchStatus, + readyToLoadDatasets + } = useStacCollectionSearch({ + start, + end, + aoi: aoiDrawState.featureCollection + }); // Update datasetsLayers when stac search is refreshed in case some // datasetsLayers are not available anymore @@ -390,30 +426,70 @@ export default function Analysis() {
    {!infoboxMessage ? ( -
    - - {selectableDatasetLayers.map((datasetLayer) => ( - - - From: {findParentDataset(datasetLayer.id)?.name} - - {datasetLayer.name} - - ))} - -
    + <> +
    + + {selectableDatasetLayers.map((datasetLayer) => ( + + + From: {findParentDataset(datasetLayer.id)?.name} + + {datasetLayer.name} + + ))} + +
    + {unselectableDatasetLayers.length && ( + <> + + + The current area and date selection has returned ( + {unselectableDatasetLayers.length}) datasets with a very + large number of data points. To make them available, + please define a smaller area or a select a shorter date + period. + + +
    + + {unselectableDatasetLayers.map((datasetLayer) => ( + + + From: {findParentDataset(datasetLayer.id)?.name} + + {datasetLayer.name} + + ~ + {datasetLayer.numberOfItems} data points + + + ))} + +
    + + )} + ) : ( diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index 66bfd7d3c..4e09e090e 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -16,6 +16,7 @@ import { TimeseriesDataResult } from '../results/timeseries-data'; import { allAvailableDatasetsLayers } from '.'; import { utcString2userTzDate } from '$utils/date'; +import { MAX_QUERY_NUM } from '../constants'; interface UseStacSearchProps { start?: Date; @@ -24,7 +25,7 @@ interface UseStacSearchProps { } export type DatasetWithTimeseriesData = TimeseriesDataResult & - DatasetLayer & { numberOfItems?: number }; + DatasetLayer & { numberOfItems: number }; const DATE_INTERVAL_FN = { day: eachDayOfInterval, @@ -52,7 +53,7 @@ export function useStacCollectionSearch({ enabled: readyToLoadDatasets }); - const selectableDatasetLayers = useMemo(() => { + const datasetLayersInRange = useMemo(() => { try { return getInTemporalAndSpatialExtent(result.data, aoi, { start, @@ -63,16 +64,29 @@ export function useStacCollectionSearch({ } }, [result.data, aoi, start, end]); - const selectableDatasetLayersWithNumberOfItems: DatasetWithTimeseriesData[] = + const datasetLayersInRangeWithNumberOfItems: DatasetWithTimeseriesData[] = useMemo(() => { - return selectableDatasetLayers.map((l) => { + return datasetLayersInRange.map((l) => { const numberOfItems = getNumberOfItemsWithinTimeRange(start, end, l); return { ...l, numberOfItems }; }); - }, [selectableDatasetLayers, start, end]); + }, [datasetLayersInRange, start, end]); + + const selectableDatasetLayers = useMemo(() => { + return datasetLayersInRangeWithNumberOfItems.filter( + (l) => l.numberOfItems <= MAX_QUERY_NUM + ); + }, [datasetLayersInRangeWithNumberOfItems]); + + const unselectableDatasetLayers = useMemo(() => { + return datasetLayersInRangeWithNumberOfItems.filter( + (l) => l.numberOfItems > MAX_QUERY_NUM + ); + }, [datasetLayersInRangeWithNumberOfItems]); return { - selectableDatasetLayers: selectableDatasetLayersWithNumberOfItems, + selectableDatasetLayers, + unselectableDatasetLayers, stacSearchStatus: result.status, readyToLoadDatasets }; From 2af6225281bac5e034edfd48214c15c99a727947 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 14:53:01 +0200 Subject: [PATCH 09/53] Fixed number of data points estimate when isPeriodic=false using timeseries --- app/scripts/components/analysis/define/index.tsx | 2 +- .../analysis/define/use-stac-collection-search.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 7ceb7eb4e..5127317ed 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -451,7 +451,7 @@ export default function Analysis() { ))} - {unselectableDatasetLayers.length && ( + {!!unselectableDatasetLayers.length && ( <> diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index 4e09e090e..b3353bed9 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -99,7 +99,15 @@ export function useStacCollectionSearch({ function getNumberOfItemsWithinTimeRange(userStart, userEnd, collection) { const { isPeriodic, timeDensity, domain, timeseries } = collection; if (!isPeriodic) { - return timeseries.length; // Check in with back-end team + const numberOfItems = timeseries.reduce((acc, t) => { + const date = new Date(t); + if (date >= userStart && date <= userEnd) { + return acc + 1; + } else { + return acc; + } + }, 0); + return numberOfItems; // Check in with back-end team } const eachOf = DATE_INTERVAL_FN[timeDensity]; const start = From 0a4a000d4bcbdc0b037248ee65489a85a39ffc1e Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 15:06:44 +0200 Subject: [PATCH 10/53] Changed date presets --- .../components/analysis/define/index.tsx | 63 +++++-------------- app/scripts/utils/date.ts | 8 ++- 2 files changed, 21 insertions(+), 50 deletions(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 5127317ed..0b5a7c4a4 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -7,8 +7,7 @@ import React, { } from 'react'; import styled from 'styled-components'; import { media, multiply, themeVal } from '@devseed-ui/theme-provider'; -import { Toolbar, ToolbarIconButton, ToolbarLabel } from '@devseed-ui/toolbar'; -import { Dropdown, DropMenu, DropTitle } from '@devseed-ui/dropdown'; +import { Toolbar, ToolbarLabel } from '@devseed-ui/toolbar'; import { Form, FormCheckable, @@ -17,7 +16,6 @@ import { } from '@devseed-ui/form'; import { CollecticonCircleInformation, - CollecticonEllipsisVertical, CollecticonSignDanger } from '@devseed-ui/collecticons'; import { Overline } from '@devseed-ui/typography'; @@ -52,6 +50,7 @@ import { MapboxMapRef } from '$components/common/mapbox'; import PageFooterActions from './page-footer.actions'; import SavedAnalysisControl from '../saved-analysis-control'; import { ANALYSIS_PATH } from '$utils/routes'; +import { Button, ButtonGroup } from '@devseed-ui/button'; const FormBlock = styled.div` display: flex; @@ -334,57 +333,25 @@ export default function Analysis() { - Actions - ( - - - - )} + Presets + - Select a date preset - -
  • - onDatePresetClick(e, 'yearToDate')} - > - This year - -
  • -
  • - onDatePresetClick(e, 'last30Days')} - > - Last 30 days - -
  • -
  • - onDatePresetClick(e, 'lastYear')} - > - Last year - -
  • -
  • - onDatePresetClick(e, 'last10Years')} - > - Last 10 years - -
  • -
    -
    + + +
    - + - + Date: Tue, 26 Sep 2023 15:09:37 +0200 Subject: [PATCH 11/53] Replace reset icon --- app/scripts/components/analysis/define/aoi-selector.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 3bbe5a8a3..e5a353888 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -19,7 +19,7 @@ import { import { Button, ButtonGroup } from '@devseed-ui/button'; import { Dropdown, DropMenu, DropTitle } from '@devseed-ui/dropdown'; import { - CollecticonArrowLoop, + CollecticonTrashBin, CollecticonHandPan, CollecticonMarker, CollecticonPencil, @@ -137,11 +137,11 @@ export default function AoiSelector({ onAoiEvent('aoi.clear')} disabled={!featureCollection?.features.length} > - + From fd884ee202ef013bf9ef039a029c98c5e6a78688 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Tue, 26 Sep 2023 15:39:55 +0200 Subject: [PATCH 12/53] Added north america preset --- app/scripts/components/analysis/constants.ts | 25 ++++++++++++++++--- .../analysis/define/aoi-selector.tsx | 7 ++++++ .../analysis/define/page-footer.actions.tsx | 2 +- package.json | 1 + 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/analysis/constants.ts b/app/scripts/components/analysis/constants.ts index bee2fe167..a5605fad5 100644 --- a/app/scripts/components/analysis/constants.ts +++ b/app/scripts/components/analysis/constants.ts @@ -1,13 +1,13 @@ import { FeatureCollection, Polygon } from 'geojson'; -import { makeFeatureCollection } from '$components/common/aoi/utils'; +import { featureCollection } from '@turf/helpers'; -export type RegionPreset = 'world'; +export type RegionPreset = 'world' | 'north-america'; export const FeatureByRegionPreset: Record< RegionPreset, FeatureCollection > = { - world: makeFeatureCollection([ + world: featureCollection([ { type: 'Feature', id: 'world', @@ -25,6 +25,25 @@ export const FeatureByRegionPreset: Record< type: 'Polygon' } } + ]), + 'north-america': featureCollection([ + { + type: 'Feature', + id: 'north-america', + properties: {}, + geometry: { + coordinates: [ + [ + [-180, 0], + [-180, 89], + [-60, 89], + [-60, 0], + [-180, 0] + ] + ], + type: 'Polygon' + } + } ]) }; diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index e5a353888..fca6fa35d 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -183,6 +183,13 @@ export default function AoiSelector({ World +
  • + onRegionPresetClick('north-america')} + > + North America + +
  • diff --git a/app/scripts/components/analysis/define/page-footer.actions.tsx b/app/scripts/components/analysis/define/page-footer.actions.tsx index 3febcada6..cb3f73ecc 100644 --- a/app/scripts/components/analysis/define/page-footer.actions.tsx +++ b/app/scripts/components/analysis/define/page-footer.actions.tsx @@ -75,7 +75,7 @@ export default function PageFooterActions({ }); const analysisDescription = useMemo(() => { - if (!start || !end || !datasetsLayers || !aoi) return ''; + if (!start || !end || !datasetsLayers || !aoi || !datasetsLayers.length) return ''; const dataset = datasetsLayers.length === 1 ? datasetsLayers[0].name diff --git a/package.json b/package.json index cec2d9cd4..3006101ee 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@turf/centroid": "^6.5.0", "@turf/simplify": "^6.5.0", "@turf/union": "^6.5.0", + "@turf/helpers": "^6.5.0", "@types/geojson": "^7946.0.10", "@types/mdx": "^2.0.1", "@types/react": "^18.2.12", From 30ec70b05a49f56e19ac87a7eadccba96bfd3052 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 26 Sep 2023 11:37:55 -0400 Subject: [PATCH 13/53] Fix getNumberOfItem logic when dataset is not periodic, add unit test for the function --- .../define/use-stac-collection-search.ts | 44 ++----------------- .../components/analysis/define/utils.test.ts | 38 ++++++++++++++++ .../components/analysis/define/utils.ts | 43 ++++++++++++++++++ 3 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 app/scripts/components/analysis/define/utils.test.ts create mode 100644 app/scripts/components/analysis/define/utils.ts diff --git a/app/scripts/components/analysis/define/use-stac-collection-search.ts b/app/scripts/components/analysis/define/use-stac-collection-search.ts index b3353bed9..86e324a7f 100644 --- a/app/scripts/components/analysis/define/use-stac-collection-search.ts +++ b/app/scripts/components/analysis/define/use-stac-collection-search.ts @@ -5,18 +5,16 @@ import { useQuery } from '@tanstack/react-query'; import booleanIntersects from '@turf/boolean-intersects'; import bboxPolygon from '@turf/bbox-polygon'; import { - areIntervalsOverlapping, - eachDayOfInterval, - eachMonthOfInterval, - eachYearOfInterval + areIntervalsOverlapping } from 'date-fns'; import { DatasetLayer } from 'veda'; +import { MAX_QUERY_NUM } from '../constants'; import { TimeseriesDataResult } from '../results/timeseries-data'; +import { getNumberOfItemsWithinTimeRange } from './utils'; import { allAvailableDatasetsLayers } from '.'; import { utcString2userTzDate } from '$utils/date'; -import { MAX_QUERY_NUM } from '../constants'; interface UseStacSearchProps { start?: Date; @@ -27,12 +25,6 @@ interface UseStacSearchProps { export type DatasetWithTimeseriesData = TimeseriesDataResult & DatasetLayer & { numberOfItems: number }; -const DATE_INTERVAL_FN = { - day: eachDayOfInterval, - month: eachMonthOfInterval, - year: eachYearOfInterval -}; - const collectionUrl = `${process.env.API_STAC_ENDPOINT}/collections`; export function useStacCollectionSearch({ @@ -92,36 +84,6 @@ export function useStacCollectionSearch({ }; } -/** - * For each collection, get the number of items within the time range, - * taking into account the time density. - */ -function getNumberOfItemsWithinTimeRange(userStart, userEnd, collection) { - const { isPeriodic, timeDensity, domain, timeseries } = collection; - if (!isPeriodic) { - const numberOfItems = timeseries.reduce((acc, t) => { - const date = new Date(t); - if (date >= userStart && date <= userEnd) { - return acc + 1; - } else { - return acc; - } - }, 0); - return numberOfItems; // Check in with back-end team - } - const eachOf = DATE_INTERVAL_FN[timeDensity]; - const start = - +new Date(domain[0]) > +new Date(userStart) - ? new Date(domain[0]) - : new Date(userStart); - const end = - +new Date(domain[1]) < +new Date(userEnd) - ? new Date(domain[1]) - : new Date(userEnd); - - return eachOf({ start, end }).length; -} - function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) { const matchingCollectionIds = collectionData.reduce((acc, col) => { const id = col.id; diff --git a/app/scripts/components/analysis/define/utils.test.ts b/app/scripts/components/analysis/define/utils.test.ts new file mode 100644 index 000000000..d4f6a2cb6 --- /dev/null +++ b/app/scripts/components/analysis/define/utils.test.ts @@ -0,0 +1,38 @@ +import { getNumberOfItemsWithinTimeRange } from './utils'; + +describe('Item number logic', () => { + + it('checks when the dataset is not periodic', () => { + const notPeriodicDataset = { + isPeriodic: false, + timeseries: [ + "2001-01-16T15:07:02Z", + "2001-12-02T15:05:04Z", + "2002-12-21T15:04:52Z", + "2004-02-26T15:05:40Z", + "2005-02-12T15:06:08Z", + "2006-12-16T15:06:44Z", + "2007-01-17T15:06:46Z", + "2008-01-04T15:06:55Z", + "2008-02-21T15:06:48Z", + "2008-12-05T15:05:57Z", + "2009-12-08T15:07:25Z", + "2010-01-09T15:07:59Z", // match + "2010-01-25T15:08:13Z", // match + "2010-02-10T15:08:25Z", // match + "2010-12-27T15:09:41Z", // match + "2011-01-12T15:09:50Z", // match + "2011-01-28T15:09:56Z", // match + "2011-11-12T15:10:06Z", + "2011-12-30T15:10:33Z"] + }; + + const userStart = "2010-01-01T00:00:00Z"; + const userEnd = "2011-11-01T00:00:00Z"; + + const numberOfDates = getNumberOfItemsWithinTimeRange(userStart, userEnd, notPeriodicDataset); + + + expect(numberOfDates).toEqual(6); + }); +}); diff --git a/app/scripts/components/analysis/define/utils.ts b/app/scripts/components/analysis/define/utils.ts new file mode 100644 index 000000000..dba32fb8b --- /dev/null +++ b/app/scripts/components/analysis/define/utils.ts @@ -0,0 +1,43 @@ +import { + eachDayOfInterval, + eachMonthOfInterval, + eachYearOfInterval +} from 'date-fns'; + +const DATE_INTERVAL_FN = { + day: eachDayOfInterval, + month: eachMonthOfInterval, + year: eachYearOfInterval +}; + +/** + * For each collection, get the number of items within the time range, + * taking into account the time density. + * Separated the function from use-stac-collection-search for easy unit test + */ + +export function getNumberOfItemsWithinTimeRange(userStart, userEnd, collection) { + + const { isPeriodic, timeDensity, domain, timeseries } = collection; + if (!isPeriodic) { + const numberOfItems = timeseries.reduce((acc, t) => { + const date = +new Date(t); + if (date >= +new Date(userStart) && date <= +new Date(userEnd)) { + return acc + 1; + } else { + return acc; + } + }, 0); + return numberOfItems; // Check in with back-end team + } + const eachOf = DATE_INTERVAL_FN[timeDensity]; + const start = + +new Date(domain[0]) > +new Date(userStart) + ? new Date(domain[0]) + : new Date(userStart); + const end = + +new Date(domain[1]) < +new Date(userEnd) + ? new Date(domain[1]) + : new Date(userEnd); + return eachOf({ start, end }).length; +} \ No newline at end of file From 7f5c11f9f6c5eab8eff5951d2e399b016df2c5db Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 26 Sep 2023 16:11:36 -0400 Subject: [PATCH 14/53] Style for 1,2,3 step --- .../analysis/define/aoi-selector.tsx | 7 +- .../components/analysis/define/index.tsx | 76 +++++++++++++++---- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index fca6fa35d..a130f22ab 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -27,6 +27,7 @@ import { } from '@devseed-ui/collecticons'; import { FeatureByRegionPreset, RegionPreset } from '../constants'; import AoIUploadModal from './aoi-upload-modal'; +import {FoldWOBottomPadding, FoldTitleWOAccent} from '.'; import { Fold, FoldHeader, @@ -120,7 +121,7 @@ export default function AoiSelector({ const [aoiModalRevealed, setAoIModalRevealed] = useState(false); return ( - + - Select area of interest + Select area of interest

    Use the pencil tool to draw a shape on the map or upload your own shapefile. @@ -205,6 +206,6 @@ export default function AoiSelector({ /> - + ); } diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 0b5a7c4a4..3dd3bb8d7 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -21,9 +21,12 @@ import { import { Overline } from '@devseed-ui/typography'; import { datasets, DatasetLayer, VedaDatum, DatasetData } from 'veda'; +import { Button, ButtonGroup } from '@devseed-ui/button'; import { useAnalysisParams } from '../results/use-analysis-params'; +import SavedAnalysisControl from '../saved-analysis-control'; import AoiSelector from './aoi-selector'; import { useStacCollectionSearch } from './use-stac-collection-search'; +import PageFooterActions from './page-footer.actions'; import { variableGlsp } from '$styles/variable-utils'; import { PageMainContent } from '$styles/page'; @@ -45,12 +48,9 @@ import { getRangeFromPreset, inputFormatToDate } from '$utils/date'; -import DropMenuItemButton from '$styles/drop-menu-item-button'; + import { MapboxMapRef } from '$components/common/mapbox'; -import PageFooterActions from './page-footer.actions'; -import SavedAnalysisControl from '../saved-analysis-control'; import { ANALYSIS_PATH } from '$utils/routes'; -import { Button, ButtonGroup } from '@devseed-ui/button'; const FormBlock = styled.div` display: flex; @@ -151,9 +151,54 @@ const FloatingFooter = styled.div<{ isSticky: boolean }>` `} `; +const FoldWithBullet = styled(Fold)<{number: string}>` + ${media.largeUp` + padding-left: ${variableGlsp(1)}; + > div { + padding-left: ${variableGlsp(2)}; + position: relative; + // bullet + &::after { + position: absolute; + top: 0; + left: -20px; + width: 40px; + height: 40px; + background-color: #1565EF; + color: ${themeVal('color.surface')}; + border-radius: ${themeVal('shape.ellipsoid')}; + font-size: 1.75rem; + display: flex; + justify-content: center; + align-items: center; + font-weight: 600; + ${(props) => `content: "${props.number}";`} + } + } +`} +`; +export const FoldWOBottomPadding = styled(FoldWithBullet)` + ${media.largeUp` + padding-bottom: 0; + > div { + border-left : 3px solid ${themeVal('color.base-200a')}; + padding-bottom: ${variableGlsp(2)}; + } + `} +`; + +export const FoldTitleWOAccent = styled(FoldTitle)` + ${media.largeUp` + &::before { + content: none; + } + `} +`; + + const findParentDataset = (layerId: string) => { const parentDataset = Object.values(datasets).find((dataset) => - (dataset as VedaDatum).data.layers.find( + (dataset!).data.layers.find( (l) => l.id === layerId ) ); @@ -163,7 +208,7 @@ const findParentDataset = (layerId: string) => { export const allAvailableDatasetsLayers: DatasetLayer[] = Object.values( datasets ) - .map((dataset) => (dataset as VedaDatum).data.layers) + .map((dataset) => (dataset!).data.layers) .flat() .filter((d) => d.type !== 'vector' && !d.analysis?.exclude); @@ -252,7 +297,7 @@ export default function Analysis() { const selectableDatasetLayersIds = selectableDatasetLayers.map( (layer) => layer.id ); - const cleanedDatasetsLayers = datasetsLayers?.filter((l) => + const cleanedDatasetsLayers = datasetsLayers.filter((l) => selectableDatasetLayersIds.includes(l.id) ); @@ -307,13 +352,12 @@ export default function Analysis() { description='Generate timeseries data for your area of interest.' /> ( )} /> - - + - Pick a date period + Pick a date period

    Select start and end date of time series, or choose a pre-set date range. @@ -338,7 +382,7 @@ export default function Analysis() { variation='base-outline' radius='square' > - - - - - + - +

    - + - + - + - +
    -
    - + + Presets + + + + + + + diff --git a/app/scripts/components/analysis/define/page-footer.actions.tsx b/app/scripts/components/analysis/define/page-footer.actions.tsx index cb3f73ecc..fdaa862ad 100644 --- a/app/scripts/components/analysis/define/page-footer.actions.tsx +++ b/app/scripts/components/analysis/define/page-footer.actions.tsx @@ -4,14 +4,14 @@ import { format } from 'date-fns'; import { Button, ButtonProps } from '@devseed-ui/button'; import { CollecticonTickSmall } from '@devseed-ui/collecticons'; +import { DatasetLayer } from 'veda'; +import { FeatureCollection, Polygon } from 'geojson'; +import styled from 'styled-components'; import { analysisParams2QueryString } from '../results/use-analysis-params'; import useSavedSettings from '../use-saved-settings'; import { composeVisuallyDisabled } from '$utils/utils'; import { ANALYSIS_RESULTS_PATH } from '$utils/routes'; -import { DatasetLayer } from 'veda'; -import { FeatureCollection, Polygon } from 'geojson'; -import styled from 'styled-components'; import { calcFeatCollArea } from '$components/common/aoi/utils'; const SaveButton = composeVisuallyDisabled(Button); @@ -43,7 +43,7 @@ const FooterRight = styled.div` const AnalysisDescription = styled.div` font-size: 0.875rem; opacity: 0.5; -` +`; export default function PageFooterActions({ // size, @@ -75,13 +75,17 @@ export default function PageFooterActions({ }); const analysisDescription = useMemo(() => { - if (!start || !end || !datasetsLayers || !aoi || !datasetsLayers.length) return ''; + if (!start || !end || !datasetsLayers || !aoi || !datasetsLayers.length) + return ''; const dataset = datasetsLayers.length === 1 ? datasetsLayers[0].name : `${datasetsLayers.length} datasets`; - const area = `over a ${calcFeatCollArea(aoi)} kmĀ² area` - const dates = `from ${format(start, 'MMM d, yyyy')} to ${format(end, 'MMM d, yyyy')}` + const area = `over a ${calcFeatCollArea(aoi)} kmĀ² area`; + const dates = `from ${format(start, 'MMM d, yyyy')} to ${format( + end, + 'MMM d, yyyy' + )}`; return [dataset, area, dates].join(' '); }, [start, end, datasetsLayers, aoi]); diff --git a/package.json b/package.json index 3006101ee..83f483556 100644 --- a/package.json +++ b/package.json @@ -121,9 +121,9 @@ "@turf/bbox-polygon": "^6.5.0", "@turf/boolean-intersects": "^6.5.0", "@turf/centroid": "^6.5.0", + "@turf/helpers": "^6.5.0", "@turf/simplify": "^6.5.0", "@turf/union": "^6.5.0", - "@turf/helpers": "^6.5.0", "@types/geojson": "^7946.0.10", "@types/mdx": "^2.0.1", "@types/react": "^18.2.12", From ee5a107a0529d72d446a1fc4fd8ce238cbf87daf Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 27 Sep 2023 10:48:50 +0200 Subject: [PATCH 17/53] Select 2018-2022 date range by default --- .../components/analysis/results/use-analysis-params.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/analysis/results/use-analysis-params.ts b/app/scripts/components/analysis/results/use-analysis-params.ts index b377be0f8..4877578ab 100644 --- a/app/scripts/components/analysis/results/use-analysis-params.ts +++ b/app/scripts/components/analysis/results/use-analysis-params.ts @@ -25,8 +25,8 @@ type AnyAnalysisParamsKey = keyof AnalysisParams; type AnyAnalysisParamsType = Date | DatasetLayer[] | FeatureCollection; const initialState: AnalysisParamsNull = { - start: undefined, - end: undefined, + start: new Date(2018, 0, 1), + end: new Date(2022, 11, 31), datasetsLayers: undefined, aoi: undefined, errors: null From a40244d3276809b665f535209ea67fcdd39785a4 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 27 Sep 2023 10:53:04 +0200 Subject: [PATCH 18/53] Hardcode min and max dates to 1980-01-01 through 2022-12-31 --- app/scripts/components/analysis/define/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 65248ed7d..3ec862248 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -404,7 +404,7 @@ export default function Analysis() { name='start-date' value={start ? dateToInputFormat(start) : ''} onChange={onStartDateChange} - min='1900-01-01' + min='1980-01-01' max={dateToInputFormat(end)} /> @@ -418,7 +418,7 @@ export default function Analysis() { value={end ? dateToInputFormat(end) : ''} onChange={onEndDateChange} min={dateToInputFormat(start)} - max={new Date().toISOString().split('T')[0]} + max='2022-12-31' /> From f9ae022c4782c65f6341cd1e54e4fd2a3c8e0d65 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 27 Sep 2023 11:05:57 +0200 Subject: [PATCH 19/53] Initialize the map with North America AoI --- .../components/analysis/define/aoi-selector.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 4ba1a3d87..6964dcde8 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, + useRef, useState } from 'react'; import styled from 'styled-components'; @@ -42,6 +43,7 @@ import { import DropMenuItemButton from '$styles/drop-menu-item-button'; import { makeFeatureCollection } from '$components/common/aoi/utils'; import { variableGlsp } from '$styles/variable-utils'; +import { useEffectPrevious } from '$utils/use-effect-previous'; const MapContainer = styled.div` position: relative; @@ -75,6 +77,19 @@ export default function AoiSelector({ }: AoiSelectorProps) { const { drawing, featureCollection } = aoiDrawState; + // TODO revise this. This is not a great hack aimed at initializing the AoI with North America when + // no qs AoI is set. Ideally this would be set in use-analysis-params initialState. + const timeOutRef = useRef | null>(null); + useEffect(() => { + if (!qsAoi) { + timeOutRef.current = setTimeout(() => { + setFeatureCollection(FeatureByRegionPreset['north-america']); + }, 100); + } else { + if (timeOutRef.current) clearTimeout(timeOutRef.current); + } + }, [qsAoi]); + // For the drawing tool, the features need an id. const qsFc: FeatureCollection | null = useMemo(() => { return qsAoi @@ -173,7 +188,7 @@ export default function AoiSelector({ )} > - Select a region (BETA) + Select a region
  • Date: Wed, 27 Sep 2023 12:48:06 +0200 Subject: [PATCH 20/53] Correct month --- app/scripts/components/analysis/results/use-analysis-params.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/components/analysis/results/use-analysis-params.ts b/app/scripts/components/analysis/results/use-analysis-params.ts index 4877578ab..e8a47eb8c 100644 --- a/app/scripts/components/analysis/results/use-analysis-params.ts +++ b/app/scripts/components/analysis/results/use-analysis-params.ts @@ -26,7 +26,7 @@ type AnyAnalysisParamsType = Date | DatasetLayer[] | FeatureCollection; const initialState: AnalysisParamsNull = { start: new Date(2018, 0, 1), - end: new Date(2022, 11, 31), + end: new Date(2022, 12, 31), datasetsLayers: undefined, aoi: undefined, errors: null From 15a32275c512e672c5fed5133f13109cb249e2e8 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 27 Sep 2023 13:01:05 +0200 Subject: [PATCH 21/53] Revert "Correct month" This reverts commit 5c7a87ddd15f2178c1c9dfcfd019429da26581d1. --- app/scripts/components/analysis/results/use-analysis-params.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/components/analysis/results/use-analysis-params.ts b/app/scripts/components/analysis/results/use-analysis-params.ts index e8a47eb8c..4877578ab 100644 --- a/app/scripts/components/analysis/results/use-analysis-params.ts +++ b/app/scripts/components/analysis/results/use-analysis-params.ts @@ -26,7 +26,7 @@ type AnyAnalysisParamsType = Date | DatasetLayer[] | FeatureCollection; const initialState: AnalysisParamsNull = { start: new Date(2018, 0, 1), - end: new Date(2022, 12, 31), + end: new Date(2022, 11, 31), datasetsLayers: undefined, aoi: undefined, errors: null From 240017f39c2768d16d2750cef0b29f9dec2b8fa5 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 27 Sep 2023 13:01:22 +0200 Subject: [PATCH 22/53] Revert "Initialize the map with North America AoI" This reverts commit f9ae022c4782c65f6341cd1e54e4fd2a3c8e0d65. --- .../components/analysis/define/aoi-selector.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index 6964dcde8..4ba1a3d87 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, - useRef, useState } from 'react'; import styled from 'styled-components'; @@ -43,7 +42,6 @@ import { import DropMenuItemButton from '$styles/drop-menu-item-button'; import { makeFeatureCollection } from '$components/common/aoi/utils'; import { variableGlsp } from '$styles/variable-utils'; -import { useEffectPrevious } from '$utils/use-effect-previous'; const MapContainer = styled.div` position: relative; @@ -77,19 +75,6 @@ export default function AoiSelector({ }: AoiSelectorProps) { const { drawing, featureCollection } = aoiDrawState; - // TODO revise this. This is not a great hack aimed at initializing the AoI with North America when - // no qs AoI is set. Ideally this would be set in use-analysis-params initialState. - const timeOutRef = useRef | null>(null); - useEffect(() => { - if (!qsAoi) { - timeOutRef.current = setTimeout(() => { - setFeatureCollection(FeatureByRegionPreset['north-america']); - }, 100); - } else { - if (timeOutRef.current) clearTimeout(timeOutRef.current); - } - }, [qsAoi]); - // For the drawing tool, the features need an id. const qsFc: FeatureCollection | null = useMemo(() => { return qsAoi @@ -188,7 +173,7 @@ export default function AoiSelector({ )} > - Select a region + Select a region (BETA)
  • Date: Wed, 27 Sep 2023 13:38:14 +0200 Subject: [PATCH 23/53] Some design polish --- .../components/analysis/define/index.tsx | 27 ++++++++++++------- app/scripts/styles/theme.ts | 2 ++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 3ec862248..1fd1a8104 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -115,18 +115,19 @@ const UnselectableInfo = styled.div` const FormCheckableUnselectable = styled(FormCheckableCustom)` pointer-events: none; - background: #f0f0f5; + background: #F0F0F5; `; const DataPointsWarning = styled.div` display: flex; align-items: center; - background: ${themeVal('color.danger-100')}; + background: #FC3D2119; border-radius: 99px; font-size: 0.825rem; font-weight: bold; margin-top: ${variableGlsp(0.5)}; - paddding: 4px; + padding: 2px 0 2px 6px; + color: ${themeVal('color.danger')}; & path { fill: ${themeVal('color.danger')}; @@ -159,10 +160,8 @@ const FoldWithBullet = styled(Fold)<{ number: string }>` // bullet &::after { position: absolute; - top: ${variableGlsp(-0.25)}; - left: ${variableGlsp(-1)}; - width: ${variableGlsp(2)}; - height: ${variableGlsp(2)}; + width: ${variableGlsp(1.5)}; + height: ${variableGlsp(1.5)}; background-color: #1565EF; color: ${themeVal('color.surface')}; border-radius: ${themeVal('shape.ellipsoid')}; @@ -181,8 +180,14 @@ export const FoldWGuideLine = styled(FoldWithBullet)` ${media.largeUp` padding-bottom: 0; > div { - border-left : 3px solid ${themeVal('color.base-200a')}; padding-bottom: ${variableGlsp(2)}; + &::before { + position: absolute; + content: ''; + height: 100%; + left: ${variableGlsp(0.7)}; + border-left : 3px solid ${themeVal('color.base-200a')}; + } } `} `; @@ -206,6 +211,10 @@ const FormGroupStructureCustom = styled(FormGroupStructure)` `} `; +const ToolbarLabelWithSpace = styled(ToolbarLabel)` + margin-right: ${variableGlsp(0.5)}; +`; + const FoldBodyCustom = styled(FoldBody)` ${media.largeUp` flex-flow: row; @@ -424,7 +433,7 @@ export default function Analysis() { - Presets + Presets diff --git a/app/scripts/components/analysis/define/page-footer.actions.tsx b/app/scripts/components/analysis/define/page-footer.actions.tsx index 15261a4d5..8c41a2e69 100644 --- a/app/scripts/components/analysis/define/page-footer.actions.tsx +++ b/app/scripts/components/analysis/define/page-footer.actions.tsx @@ -98,7 +98,6 @@ export default function PageFooterActions({ to={`${ANALYSIS_RESULTS_PATH}${location.search}`} type='button' size='xlarge' - radius='square' variation='primary-outline' > Cancel @@ -112,7 +111,6 @@ export default function PageFooterActions({ @@ -121,7 +119,6 @@ export default function PageFooterActions({ ) : (