((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
+
+ /* eslint-disable-next-line fp/no-mutating-methods */
+ queue.push([
+ key,
+ async () => {
+ try {
+ const result = await taskFn();
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ }
+ ]);
+
+ run();
+
+ return promise;
+ },
+ dequeue: (key: string) => {
+ queue = queue.filter(([k]) => k !== key);
+ }
+ };
+}
+
+// Global concurrency manager instance
+export const analysisConcurrencyManager = ConcurrencyManager();
diff --git a/app/scripts/components/exploration/data-utils.ts b/app/scripts/components/exploration/data-utils.ts
index cca52fd68..57ebd33a2 100644
--- a/app/scripts/components/exploration/data-utils.ts
+++ b/app/scripts/components/exploration/data-utils.ts
@@ -13,6 +13,7 @@ import {
TimelineDataset,
TimelineDatasetStatus
} from './types.d.ts';
+import { DataMetric, DATA_METRICS } from './components/datasets/analysis-metrics';
import { utcString2userTzDate } from '$utils/date';
@@ -23,10 +24,37 @@ export const findParentDataset = (layerId: string) => {
return parentDataset?.data;
};
+export const allDatasets = Object.values(datasets).map((d) => d!.data);
+
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
+ * ids. Otherwise, it returns all available metrics.
+ *
+ * @param data - The Datase tLayer object to get metrics for.
+ * @returns An array of metrics objects.
+ */
+function getInitialMetrics(data: DatasetLayer): DataMetric[] {
+ const metricsIds = data.analysis?.metrics ?? [];
+
+ const foundMetrics = metricsIds
+ .map((metric: string) => {
+ return DATA_METRICS.find((m) => m.id === metric)!;
+ })
+ .filter(Boolean);
+
+ if (!foundMetrics.length) {
+ return DATA_METRICS;
+ }
+
+ return foundMetrics;
+}
+
/**
* Converts the datasets to a format that can be used by the timeline, skipping
* the ones that have already been reconciled.
@@ -59,7 +87,8 @@ export function reconcileDatasets(
error: null,
settings: {
isVisible: true,
- opacity: 100
+ opacity: 100,
+ analysisMetrics: getInitialMetrics(dataset)
},
analysis: {
status: TimelineDatasetStatus.IDLE,
diff --git a/app/scripts/components/exploration/datasets-mock.tsx b/app/scripts/components/exploration/datasets-mock.tsx
index f1c5b2b19..ae9c6d1a9 100644
--- a/app/scripts/components/exploration/datasets-mock.tsx
+++ b/app/scripts/components/exploration/datasets-mock.tsx
@@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react';
import { eachDayOfInterval, eachMonthOfInterval } from 'date-fns';
-import { useSetAtom } from 'jotai';
+import { useAtom, useSetAtom } from 'jotai';
import styled from 'styled-components';
import { themeVal } from '@devseed-ui/theme-provider';
import { Button } from '@devseed-ui/button';
import {
- isAnalysisAtom,
isExpandedAtom,
timelineDatasetsAtom
} from './atoms/atoms';
@@ -15,6 +14,7 @@ import {
TimelineDatasetAnalysis,
TimelineDatasetStatus
} from './types.d.ts';
+import { useAnalysisController } from './hooks/use-analysis-data-request';
const chartData = {
status: 'success',
@@ -381,7 +381,8 @@ function makeDataset(
mocked: true,
status,
data,
- error: status === TimelineDatasetStatus.ERROR ? new Error('Mock error') : null,
+ error:
+ status === TimelineDatasetStatus.ERROR ? new Error('Mock error') : null,
settings: {
...settings,
isVisible: settings.isVisible === undefined ? true : settings.isVisible
@@ -416,9 +417,13 @@ const MockPanel = styled.div`
export function MockControls({ onCompareClick, comparing }: any) {
const [mockRevealed, setMockRevealed] = useState(false);
- const set = useSetAtom(timelineDatasetsAtom);
+ const [timelineDatasets, set] = useAtom(timelineDatasetsAtom);
const setIsExpanded = useSetAtom(isExpandedAtom);
- const setIsAnalysis = useSetAtom(isAnalysisAtom);
+
+ const { isObsolete, runAnalysis, cancelAnalysis, isAnalyzing } =
+ useAnalysisController();
+
+ const datasetIds = timelineDatasets.map((d) => d.data.id);
useEffect(() => {
const listener = (e) => {
@@ -600,17 +605,44 @@ export function MockControls({ onCompareClick, comparing }: any) {
>
Toggle expanded
-
+
+ {isAnalyzing ? (
+ <>
+ In Analysis (obsolete: {isObsolete.toString()})
+
+
+ >
+ ) : (
+ <>
+ NOT Analysis (obsolete: {isObsolete.toString()})
+
+ >
+ )}
+
);
}
diff --git a/app/scripts/components/exploration/hooks/use-analysis-data-request.ts b/app/scripts/components/exploration/hooks/use-analysis-data-request.ts
new file mode 100644
index 000000000..fd3385d35
--- /dev/null
+++ b/app/scripts/components/exploration/hooks/use-analysis-data-request.ts
@@ -0,0 +1,115 @@
+import { useCallback, useEffect } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import { FeatureCollection, Polygon } from 'geojson';
+import { PrimitiveAtom, useAtom, useAtomValue } from 'jotai';
+
+import { requestDatasetTimeseriesData } from '../analysis-data';
+import { analysisControllerAtom, selectedIntervalAtom } from '../atoms/atoms';
+import { useTimelineDatasetAnalysis } from '../atoms/hooks';
+import { analysisConcurrencyManager } from '../concurrency';
+import { TimelineDataset, TimelineDatasetStatus } from '../types.d.ts';
+import useAois from '$components/common/map/controls/hooks/use-aois';
+
+export function useAnalysisController() {
+ const [controller, setController] = useAtom(analysisControllerAtom);
+
+ return {
+ isAnalyzing: controller.isAnalyzing,
+ isObsolete: controller.isObsolete,
+ setObsolete: useCallback(
+ () => setController((v) => ({ ...v, isObsolete: true })),
+ [] // eslint-disable-line react-hooks/exhaustive-deps -- setController is stable
+ ),
+ runAnalysis: useCallback(
+ (datasetsIds) => {
+ const ids = Array.isArray(datasetsIds) ? datasetsIds : [datasetsIds];
+ setController((v) => ({
+ ...v,
+ // Increment each id count by 1
+ runIds: ids.reduce(
+ (acc, id) => ({ ...acc, [id]: (acc[id] ?? 0) + 1 }),
+ v.runIds
+ ),
+ isAnalyzing: true,
+ isObsolete: false
+ }));
+ },
+ [] // eslint-disable-line react-hooks/exhaustive-deps -- setController is stable
+ ),
+ cancelAnalysis: useCallback(
+ () =>
+ setController((v) => ({
+ ...v,
+ isAnalyzing: false,
+ isObsolete: false
+ })),
+ [] // eslint-disable-line react-hooks/exhaustive-deps -- setController is stable
+ ),
+ getRunId: (id: string) => controller.runIds[id] ?? 0,
+ };
+}
+
+export function useAnalysisDataRequest({
+ datasetAtom
+}: {
+ datasetAtom: PrimitiveAtom;
+}) {
+ const queryClient = useQueryClient();
+
+ const selectedInterval = useAtomValue(selectedIntervalAtom);
+ const { features } = useAois();
+ const selectedFeatures = features.filter((f) => f.selected);
+
+ const { getRunId, isAnalyzing } = useAnalysisController();
+
+ const dataset = useAtomValue(datasetAtom);
+ const datasetStatus = dataset.status;
+
+ const [, setAnalysis] = useTimelineDatasetAnalysis(datasetAtom);
+
+ const analysisRunId = getRunId(dataset.data.id);
+
+ useEffect(() => {
+ if (!isAnalyzing) {
+ queryClient.cancelQueries({
+ queryKey: ['analysis'],
+ fetchStatus: 'fetching'
+ });
+ analysisConcurrencyManager.clear();
+ }
+ }, [isAnalyzing]);
+
+ useEffect(() => {
+ if (
+ datasetStatus !== TimelineDatasetStatus.SUCCESS ||
+ !selectedInterval ||
+ !selectedFeatures.length
+ ) {
+ return;
+ }
+
+ const aoi: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: selectedFeatures
+ };
+
+ const { start, end } = selectedInterval;
+
+ requestDatasetTimeseriesData({
+ start,
+ end,
+ aoi,
+ dataset,
+ queryClient,
+ concurrencyManager: analysisConcurrencyManager,
+ onProgress: (data) => {
+ setAnalysis(data);
+ }
+ });
+ // We want great control when this effect run which is done by incrementing
+ // the analysisRun. This is done when the user refreshes the analysis or
+ // when they enter the analysis. It is certain that when this effect runs
+ // the other values will be up to date. Adding all dependencies would cause
+ // the hook to continuously run.
+ }, [analysisRunId, datasetStatus]);
+}
diff --git a/app/scripts/components/exploration/types.d.ts.ts b/app/scripts/components/exploration/types.d.ts.ts
index bfc8ddc17..181df04d1 100644
--- a/app/scripts/components/exploration/types.d.ts.ts
+++ b/app/scripts/components/exploration/types.d.ts.ts
@@ -1,4 +1,5 @@
import { DatasetLayer } from 'veda';
+import { DataMetric } from './components/datasets/analysis-metrics';
export enum TimeDensity {
YEAR = 'year',
@@ -19,9 +20,25 @@ export interface StacDatasetData {
domain: string[];
}
-export type AnalysisTimeseriesEntry = Record & {
+export interface AnalysisTimeseriesEntry {
date: Date;
-};
+ min: number;
+ max: number;
+ mean: number;
+ count: number;
+ sum: number;
+ std: number;
+ median: number;
+ majority: number;
+ minority: number;
+ unique: number;
+ histogram: [number[], number[]];
+ valid_percent: number;
+ masked_pixels: number;
+ valid_pixels: number;
+ percentile_2: number;
+ percentile_98: number;
+}
interface AnalysisMeta {
loaded: number;
@@ -75,6 +92,8 @@ export interface TimelineDatasetSettings {
isVisible?: boolean;
// Opacity of the layer on the map.
opacity?: number;
+ // Active metrics for the analysis chart.
+ analysisMetrics?: DataMetric[];
}
// TimelineDataset type discriminants
diff --git a/app/scripts/styles/theme.ts b/app/scripts/styles/theme.ts
index f5498e7a4..f33a17aa3 100644
--- a/app/scripts/styles/theme.ts
+++ b/app/scripts/styles/theme.ts
@@ -10,7 +10,7 @@ export const VEDA_OVERRIDE_THEME = {
hide: -1,
docked: 10,
sticky: 900,
- dropdown: 1000,
+ dropdown: 1550,
overlay: 1300,
modal: 1400,
popover: 1500,