diff --git a/.env b/.env index 9b9f3f8a0..0ae59083e 100644 --- a/.env +++ b/.env @@ -11,3 +11,5 @@ API_STAC_ENDPOINT='https://staging-stac.delta-backend.com' # Google form for feedback GOOGLE_FORM = 'https://docs.google.com/forms/d/e/1FAIpQLSfGcd3FDsM3kQIOVKjzdPn4f88hX8RZ4Qef7qBsTtDqxjTSkg/viewform?embedded=true' + +FEATURE_NEW_EXPLORATION = 'TRUE' \ No newline at end of file diff --git a/app/scripts/components/analysis/utils.ts b/app/scripts/components/analysis/utils.ts index d22c05fdb..98aba1361 100644 --- a/app/scripts/components/analysis/utils.ts +++ b/app/scripts/components/analysis/utils.ts @@ -110,6 +110,15 @@ export function fixAoiFcForStacSearch(aoi: FeatureCollection) { return aoiMultiPolygon; } +export function fixAoiForArcGISAnalysis(aoi: FeatureCollection) { + + const fixedAois = aoi.features.map(fixAntimeridian); + return { + type: 'FeatureCollection', + features: fixedAois + }; +} + export function getDateRangeFormatted(startDate, endDate) { const dFormat = 'yyyy-MM-dd'; const startDateFormatted = format(startDate, dFormat); diff --git a/app/scripts/components/common/map/style-generators/arc.tsx b/app/scripts/components/common/map/style-generators/arc.tsx new file mode 100644 index 000000000..2586dac6c --- /dev/null +++ b/app/scripts/components/common/map/style-generators/arc.tsx @@ -0,0 +1,161 @@ +import React, { useEffect, useMemo } from 'react'; +import qs from 'qs'; +import { RasterSource, RasterLayer } from 'mapbox-gl'; + +import useGeneratorParams from '../hooks/use-generator-params'; +import useMapStyle from '../hooks/use-map-style'; +import { BaseGeneratorParams } from '../types'; + +import { useArc } from '$components/common/map/style-generators/hooks'; +import { ActionStatus } from '$utils/status'; + +import { userTzDate2utcString } from '$utils/date'; + +// @NOTE: ArcGIS Layer doens't have a timestamp +export interface MapLayerArcProps extends BaseGeneratorParams { + id: string; + date?: Date; + stacCol: string; + sourceParams?: Record; + stacApiEndpoint?: string; + zoomExtent?: number[]; + onStatusChange?: (result: { status: ActionStatus; id: string }) => void; +} + +interface ArcPaintLayerProps extends BaseGeneratorParams{ + id: string; + sourceParams?: Record; + date?: Date; + zoomExtent?: number[]; + wmsUrl: string; +} + +export function ArcPaintLayer(props: ArcPaintLayerProps) { + const { + id, + date, + sourceParams, + zoomExtent, + wmsUrl, + generatorOrder, + hidden, + opacity + } = props; + + const { updateStyle } = useMapStyle(); + + const [minZoom] = zoomExtent ?? [0, 20]; + + const generatorId = 'arc-' + id; + + const generatorParams = useGeneratorParams( {generatorOrder, hidden, opacity }); + + // Generate Mapbox GL layers and sources for raster layer + // + const haveSourceParamsChanged = useMemo( + () => JSON.stringify(sourceParams), + [sourceParams] + ); + + useEffect( + () => { + if (!wmsUrl) return; + + const tileParams = qs.stringify({ + format: 'image/png', + service: "WMS", + request: "GetMap", + transparent: "true", // TODO: get from sourceparams maybe + width: "256", + height: "256", + ...(date && { DIM_StdTime: userTzDate2utcString(date) }), + ...sourceParams + }); + + const arcSource: RasterSource = { + type: 'raster', + tiles: [`${wmsUrl}?${tileParams}&bbox={bbox-epsg-3857}`], + tileSize: 256, + }; + + const rasterOpacity = typeof opacity === 'number' ? opacity / 100 : 1; + + const arcLayer: RasterLayer = { + id: id, + type: 'raster', + source: id, + layout: { + visibility: hidden ? 'none' : 'visible' + }, + paint: { + 'raster-opacity': hidden ? 0 : rasterOpacity, + 'raster-opacity-transition': { + duration: 320 + } + }, + minzoom: minZoom, + metadata: { + id, + layerOrderPosition: 'raster', + xyzTileUrl: '', + wmtsTileUrl: `${wmsUrl}?${tileParams}` + } + }; + + const sources = { + [id]: arcSource + }; + const layers = [arcLayer]; + + updateStyle({ + generatorId, + sources, + layers, + params: generatorParams + }); + }, + // sourceParams not included, but using a stringified version of it to detect changes (haveSourceParamsChanged) + [ + updateStyle, + id, + date, + wmsUrl, + minZoom, + haveSourceParamsChanged, + hidden, + opacity, + generatorId, + generatorParams, + sourceParams + ] + ); + + // + // Cleanup layers on unmount. + // + useEffect(() => { + return () => { + updateStyle({ + generatorId, + sources: {}, + layers: [] + }); + }; + }, [updateStyle, generatorId]); + + return null; +} + +export function Arc(props:MapLayerArcProps) { + const { + id, + stacCol, + stacApiEndpoint, + onStatusChange, + } = props; + + const stacApiEndpointToUse = stacApiEndpoint?? process.env.API_STAC_ENDPOINT; + const wmsUrl = useArc({ id, stacCol, stacApiEndpointToUse, onStatusChange }); + + return ; +} diff --git a/app/scripts/components/common/map/style-generators/hooks.ts b/app/scripts/components/common/map/style-generators/hooks.ts index 5fbbe1830..839ddb02b 100644 --- a/app/scripts/components/common/map/style-generators/hooks.ts +++ b/app/scripts/components/common/map/style-generators/hooks.ts @@ -13,6 +13,17 @@ interface ZarrResponseData { } } } +interface Link { + href: string, + rel: string, + type: string, + title: string, + "wms:layers": string[], + "wms:styles": string[] +} +interface ArcResponseData { + links: Link[] +} interface CMRResponseData { features: { assets: { @@ -110,4 +121,41 @@ export function useCMR({ id, stacCol, stacApiEndpointToUse, date, assetUrlReplac return assetUrl; -} \ No newline at end of file +} + +export function useArc({ id, stacCol, stacApiEndpointToUse, onStatusChange }){ + const [wmsUrl, setWmsUrl] = useState(''); + + useEffect(() => { + const controller = new AbortController(); + + async function load() { + try { + onStatusChange?.({ status: S_LOADING, id }); + const data:ArcResponseData = await requestQuickCache({ + url: `${stacApiEndpointToUse}/collections/${stacCol}`, + method: 'GET', + controller + }); + const wms = data.links.find(l => l.rel==='wms'); + if (wms) setWmsUrl(wms.href); + else throw new Error('no wms link'); + onStatusChange?.({ status: S_SUCCEEDED, id }); + } catch (error) { + if (!controller.signal.aborted) { + setWmsUrl(''); + onStatusChange?.({ status: S_FAILED, id }); + } + return; + } + } + + load(); + + return () => { + controller.abort(); + }; + }, [id, stacCol, stacApiEndpointToUse, onStatusChange]); + + return wmsUrl; +} 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 8daf4cb21..e0af2c5a9 100644 --- a/app/scripts/components/common/map/style-generators/raster-timeseries.tsx +++ b/app/scripts/components/common/map/style-generators/raster-timeseries.tsx @@ -39,7 +39,7 @@ const LOG = true; export interface RasterTimeseriesProps extends BaseGeneratorParams { id: string; stacCol: string; - date: Date; + date?: Date; sourceParams?: Record; zoomExtent?: number[]; bounds?: number[]; @@ -145,7 +145,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { // const [stacCollection, setStacCollection] = useState([]); useEffect(() => { - if (!id || !stacCol) return; + if (!id || !stacCol || !date) return; const controller = new AbortController(); @@ -238,7 +238,7 @@ export function RasterTimeseries(props: RasterTimeseriesProps) { // const [mosaicUrl, setMosaicUrl] = useState(null); useEffect(() => { - if (!id || !stacCol) return; + if (!id || !stacCol || !date) return; // If the search returned no data, remove anything previously there so we // don't run the risk that the selected date and data don't match, even diff --git a/app/scripts/components/common/map/style-generators/vector-timeseries.tsx b/app/scripts/components/common/map/style-generators/vector-timeseries.tsx index a558ffbbf..5bb9f7f1c 100644 --- a/app/scripts/components/common/map/style-generators/vector-timeseries.tsx +++ b/app/scripts/components/common/map/style-generators/vector-timeseries.tsx @@ -33,7 +33,7 @@ import { userTzDate2utcString } from '$utils/date'; export interface VectorTimeseriesProps extends BaseGeneratorParams { id: string; stacCol: string; - date: Date; + date?: Date; sourceParams?: Record; zoomExtent?: number[]; bounds?: number[]; diff --git a/app/scripts/components/common/map/style-generators/zarr-timeseries.tsx b/app/scripts/components/common/map/style-generators/zarr-timeseries.tsx index 38243442d..026103f4b 100644 --- a/app/scripts/components/common/map/style-generators/zarr-timeseries.tsx +++ b/app/scripts/components/common/map/style-generators/zarr-timeseries.tsx @@ -105,17 +105,16 @@ export function ZarrPaintLayer(props: ZarrPaintLayerProps) { [ updateStyle, id, - date, assetUrl, - minZoom, + date, tileApiEndpoint, + minZoom, haveSourceParamsChanged, - generatorParams - // generatorParams includes hidden and opacity - // hidden, - // opacity, - // generatorId, // - dependent on id - // sourceParams, // tracked by haveSourceParamsChanged + hidden, + opacity, + generatorId, + generatorParams, + sourceParams ] ); diff --git a/app/scripts/components/common/mapbox/layers/arc-imageserver.tsx b/app/scripts/components/common/mapbox/layers/arc-imageserver.tsx new file mode 100644 index 000000000..72518f1b3 --- /dev/null +++ b/app/scripts/components/common/mapbox/layers/arc-imageserver.tsx @@ -0,0 +1,154 @@ +import React, { useEffect, useMemo } from 'react'; +import qs from 'qs'; +import { RasterSource, RasterLayer } from 'mapbox-gl'; + +import { useMapStyle } from './styles'; +import { useArc } from '$components/common/map/style-generators/hooks'; + +import { userTzDate2utcString } from '$utils/date'; +import { ActionStatus } from '$utils/status'; + +export interface MapLayerArcImageServerProps { + id: string; + stacCol: string; + date?: Date; + sourceParams?: Record; + stacApiEndpoint?: string; + tileApiEndpoint?: string; + zoomExtent?: number[]; + onStatusChange?: (result: { status: ActionStatus; id: string }) => void; + isHidden?: boolean; + idSuffix?: string; +} + +interface ArcPaintLayerProps { + id: string; + date?: Date; + sourceParams?: Record; + tileApiEndpoint?: string; + zoomExtent?: number[]; + isHidden?: boolean; + idSuffix?: string; + wmsUrl: string; +} + +export function ArcImageServerPaintLayer(props: ArcPaintLayerProps) { + const { + id, + tileApiEndpoint, + date, + sourceParams, + zoomExtent, + isHidden, + wmsUrl, + idSuffix = '' + } = props; + + const { updateStyle } = useMapStyle(); + + const [minZoom] = zoomExtent ?? [0, 20]; + + const generatorId = 'arc-timeseries' + idSuffix; + + // Generate Mapbox GL layers and sources for raster timeseries + // + const haveSourceParamsChanged = useMemo( + () => JSON.stringify(sourceParams), + [sourceParams] + ); + + useEffect( + () => { + if (!wmsUrl) return; + //https://arcgis.asdc.larc.nasa.gov/server/services/POWER/power_901_annual_meterology_utc/ImageServer/WMSServer + // ?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&LAYERS=PS&DIM_StdTime=1981-12-31T00:00:00Z" + + // TODO: investigate For some reason the request being made is 6 hours ahead? Something to do with UTC <-> local conversion? + const tileParams = qs.stringify({ + format: 'image/png', + service: "WMS", + request: "GetMap", + transparent: "true", // TODO: get from sourceparams maybe + width: "256", + height: "256", + // DIM_StdTime: userTzDate2utcString(date), // TODO: better date conversion + ...sourceParams + }); + + const arcSource: RasterSource = { + type: 'raster', + tiles: [`${wmsUrl}?${tileParams}&bbox={bbox-epsg-3857}`] + }; + + const arcLayer: RasterLayer = { + id: id, + type: 'raster', + source: id, + layout: { + visibility: isHidden ? 'none' : 'visible' + }, + paint: { + 'raster-opacity': Number(!isHidden), + 'raster-opacity-transition': { + duration: 320 + } + }, + minzoom: minZoom, + metadata: { + layerOrderPosition: 'raster' + } + }; + + const sources = { + [id]: arcSource + }; + const layers = [arcLayer]; + + updateStyle({ + generatorId, + sources, + layers + }); + }, + // sourceParams not included, but using a stringified version of it to detect changes (haveSourceParamsChanged) + [ + updateStyle, + id, + date, + wmsUrl, + minZoom, + haveSourceParamsChanged, + isHidden, + generatorId, + tileApiEndpoint + ] + ); + + // + // Cleanup layers on unmount. + // + useEffect(() => { + return () => { + updateStyle({ + generatorId, + sources: {}, + layers: [] + }); + }; + }, [updateStyle, generatorId]); + + return null; +} + +export function MapLayerArcImageServer(props:MapLayerArcImageServerProps) { + const { + id, + stacCol, + stacApiEndpoint, + onStatusChange, + } = props; + + const stacApiEndpointToUse = stacApiEndpoint?? process.env.API_STAC_ENDPOINT; + const wmsUrl = useArc({id, stacCol, stacApiEndpointToUse, onStatusChange}); + return ; +} \ No newline at end of file diff --git a/app/scripts/components/common/mapbox/layers/arc.tsx b/app/scripts/components/common/mapbox/layers/arc.tsx new file mode 100644 index 000000000..2deb15c3f --- /dev/null +++ b/app/scripts/components/common/mapbox/layers/arc.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useMemo } from 'react'; +import qs from 'qs'; +import { RasterSource, RasterLayer } from 'mapbox-gl'; + +import { useMapStyle } from './styles'; +import { useArc } from '$components/common/map/style-generators/hooks'; + +import { userTzDate2utcString } from '$utils/date'; +import { ActionStatus } from '$utils/status'; + +export interface MapLayerArcProps { + id: string; + stacCol: string; + date?: Date; + sourceParams?: Record; + stacApiEndpoint?: string; + zoomExtent?: number[]; + onStatusChange?: (result: { status: ActionStatus; id: string }) => void; + isHidden?: boolean; + idSuffix?: string; +} + +interface ArcPaintLayerProps { + id: string; + date?: Date; + sourceParams?: Record; + zoomExtent?: number[]; + isHidden?: boolean; + idSuffix?: string; + wmsUrl: string; +} + +export function ArcPaintLayer(props: ArcPaintLayerProps) { + const { + id, + date, + sourceParams, + zoomExtent, + isHidden, + wmsUrl, + idSuffix = '' + } = props; + + const { updateStyle } = useMapStyle(); + + const [minZoom] = zoomExtent ?? [0, 20]; + + const generatorId = 'arc-timeseries' + idSuffix; + + // Generate Mapbox GL layers and sources for raster timeseries + // + const haveSourceParamsChanged = useMemo( + () => JSON.stringify(sourceParams), + [sourceParams] + ); + + useEffect( + () => { + if (!wmsUrl) return; + // @TODO: time + const tileParams = qs.stringify({ + format: 'image/png', + service: "WMS", + request: "GetMap", + transparent: "true", // TODO: get from sourceparams maybe + width: "256", + height: "256", + ...(date && { DIM_StdTime: userTzDate2utcString(date) }), + ...sourceParams + }); + + const arcSource: RasterSource = { + type: 'raster', + tiles: [`${wmsUrl}?${tileParams}&bbox={bbox-epsg-3857}`] + }; + + const arcLayer: RasterLayer = { + id: id, + type: 'raster', + source: id, + layout: { + visibility: isHidden ? 'none' : 'visible' + }, + paint: { + 'raster-opacity': Number(!isHidden), + 'raster-opacity-transition': { + duration: 320 + } + }, + minzoom: minZoom, + metadata: { + layerOrderPosition: 'raster' + } + }; + + const sources = { + [id]: arcSource + }; + const layers = [arcLayer]; + + updateStyle({ + generatorId, + sources, + layers + }); + }, + // sourceParams not included, but using a stringified version of it to detect changes (haveSourceParamsChanged) + [ + updateStyle, + id, + date, + wmsUrl, + minZoom, + haveSourceParamsChanged, + isHidden, + generatorId + ] + ); + + // + // Cleanup layers on unmount. + // + useEffect(() => { + return () => { + updateStyle({ + generatorId, + sources: {}, + layers: [] + }); + }; + }, [updateStyle, generatorId]); + + return null; +} + +export function MapLayerArc(props:MapLayerArcProps) { + const { + id, + stacCol, + stacApiEndpoint, + onStatusChange, + } = props; + + const stacApiEndpointToUse = stacApiEndpoint?? process.env.API_STAC_ENDPOINT; + const wmsUrl = useArc({id, stacCol, stacApiEndpointToUse, onStatusChange}); + return ; +} \ No newline at end of file diff --git a/app/scripts/components/common/mapbox/layers/utils.ts b/app/scripts/components/common/mapbox/layers/utils.ts index b8a310dc8..e9000795d 100644 --- a/app/scripts/components/common/mapbox/layers/utils.ts +++ b/app/scripts/components/common/mapbox/layers/utils.ts @@ -37,6 +37,10 @@ import { MapLayerCMRTimeseries, MapLayerCMRTimeseriesProps } from './cmr-timeseries'; +import { + MapLayerArc, + MapLayerArcProps +} from './arc'; import { userTzDate2utcString, utcString2userTzDate } from '$utils/date'; import { AsyncDatasetLayer } from '$context/layer-data'; @@ -51,12 +55,14 @@ export const getLayerComponent = ( | MapLayerVectorTimeseriesProps | MapLayerZarrTimeseriesProps | MapLayerCMRTimeseriesProps + | MapLayerArcProps > | null => { if (isTimeseries) { if (layerType === 'raster') return MapLayerRasterTimeseries; if (layerType === 'vector') return MapLayerVectorTimeseries; if (layerType === 'zarr') return MapLayerZarrTimeseries; if (layerType === 'cmr') return MapLayerCMRTimeseries; + if (layerType === 'arc') return MapLayerArc; } return null; diff --git a/app/scripts/components/datasets/s-explore/index.tsx b/app/scripts/components/datasets/s-explore/index.tsx index d620c7311..3d63e1b8d 100644 --- a/app/scripts/components/datasets/s-explore/index.tsx +++ b/app/scripts/components/datasets/s-explore/index.tsx @@ -406,13 +406,18 @@ function DatasetsExplore() { // Get the active layer timeseries data so we can render the date selector. // Done both for the base layer and the compare layer. const activeLayerTimeseries = useMemo( - // @ts-expect-error if there is activeLayer the the rest is loaded. - () => activeLayer?.baseLayer.data.timeseries ?? null, + () => { + // @ts-expect-error if there is activeLayer the the rest is loaded. + return activeLayer?.baseLayer.data.timeseries.isTimeless? null : activeLayer?.baseLayer.data.timeseries ?? null; + }, [activeLayer] ); + const activeLayerCompareTimeseries = useMemo( - // @ts-expect-error if there is activeLayer the the rest is loaded. - () => activeLayer?.compareLayer?.data.timeseries ?? null, + () => { + // @ts-expect-error if there is activeLayer the the rest is loaded. + return activeLayer?.compareLayer?.data.timeseries.isTimeless? null : activeLayer?.compareLayer?.data.timeseries ?? null; + }, [activeLayer] ); @@ -499,6 +504,7 @@ function DatasetsExplore() { // Available dates for the baseLayer of the currently active layer. // null if there's no active layer or it hasn't loaded yet. const availableActiveLayerDates = useMemo(() => { + if (activeLayer?.baseLayer.data?.timeseries.isTimeless) return undefined; if (!activeLayer) return undefined; return resolveLayerTemporalExtent(activeLayer.baseLayer.data) ?? undefined; }, [activeLayer]); @@ -588,7 +594,6 @@ function DatasetsExplore() { ); // End onAction callback. /** *********************************************************************** */ - return ( <> ; } + +function fromMultiPolygontoPolygon(feature) { + if (feature.geometry.type === 'Polygon') { + return feature; + } + return { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: feature.geometry.coordinates[0] + } + }; +} + /** * Gets the asset urls for all datasets in the results of a STAC search given by * the input parameters. @@ -95,200 +112,299 @@ export async function requestDatasetTimeseriesData({ : Promise { const datasetData = dataset.data; const datasetAnalysis = dataset.analysis; + const id = datasetData.id; + const arcFlag = datasetData.type === 'arc'; + if (!arcFlag) { + if (datasetData.type !== 'raster') { + return { + status: TimelineDatasetStatus.ERROR, + meta: {}, + error: new ExtendedError( + 'Analysis is only supported for raster datasets', + 'ANALYSIS_NOT_SUPPORTED' + ), + data: null + }; + } - if (datasetData.type !== 'raster') { - return { - status: TimelineDatasetStatus.ERROR, - meta: {}, - error: new ExtendedError( - 'Analysis is only supported for raster datasets', - 'ANALYSIS_NOT_SUPPORTED' - ), - data: null - }; - } + // const id = datasetData.id; - const id = datasetData.id; + onProgress({ + status: TimelineDatasetStatus.LOADING, + error: null, + data: null, + meta: {} + }); - onProgress({ - status: TimelineDatasetStatus.LOADING, - error: null, - data: null, - meta: {} - }); + const stacApiEndpointToUse = + datasetData.stacApiEndpoint ?? process.env.API_STAC_ENDPOINT ?? ''; - const stacApiEndpointToUse = - datasetData.stacApiEndpoint ?? process.env.API_STAC_ENDPOINT ?? ''; + try { + const layerInfoFromSTAC = await concurrencyManager.queue( + `${id}-analysis`, + () => { + return queryClient.fetchQuery( + ['analysis', 'dataset', id, aoi, start, end], + ({ signal }) => + getDatasetAssets( + { + stacEndpoint: stacApiEndpointToUse, + stacCol: datasetData.stacCol, + assets: datasetData.sourceParams?.assets || 'cog_default', + aoi, + dateStart: start, + dateEnd: end + }, + { signal } + ), + { + staleTime: Infinity + } + ); + } + ); - try { - const layerInfoFromSTAC = await concurrencyManager.queue( - `${id}-analysis`, - () => { - return queryClient.fetchQuery( - ['analysis', 'dataset', id, aoi, start, end], - ({ signal }) => - getDatasetAssets( - { - stacEndpoint: stacApiEndpointToUse, - stacCol: datasetData.stacCol, - assets: datasetData.sourceParams?.assets || 'cog_default', - aoi, - dateStart: start, - dateEnd: end - }, - { signal } - ), - { - staleTime: Infinity - } + const { assets } = layerInfoFromSTAC; + + onProgress({ + status: TimelineDatasetStatus.LOADING, + error: null, + data: null, + meta: { + total: assets.length, + loaded: 0 + } + }); + + if (assets.length > maxItems) { + const e = new ExtendedError( + 'Too many assets to analyze', + 'ANALYSIS_TOO_MANY_ASSETS' ); - } - ); + e.details = { + assetCount: assets.length + }; - const { assets } = layerInfoFromSTAC; + return { + ...datasetAnalysis, + status: TimelineDatasetStatus.ERROR, + error: e, + data: null + }; + } - onProgress({ - status: TimelineDatasetStatus.LOADING, - error: null, - data: null, - meta: { - total: assets.length, - loaded: 0 + if (!assets.length) { + return { + ...datasetAnalysis, + status: TimelineDatasetStatus.ERROR, + error: new ExtendedError( + 'No data in the given time range and area of interest', + 'ANALYSIS_NO_DATA' + ), + data: null + }; } - }); - if (assets.length > maxItems) { - const e = new ExtendedError( - 'Too many assets to analyze', - 'ANALYSIS_TOO_MANY_ASSETS' + let loaded = 0;//new Array(assets.length).fill(0); + + const tileEndpointToUse = + datasetData.tileApiEndpoint ?? process.env.API_RASTER_ENDPOINT ?? ''; + + const analysisParams = datasetData.analysis?.sourceParams ?? {}; + const layerStatistics = await Promise.all( + assets.map( + async ({ date, url }) => { + const statistics = await concurrencyManager.queue( + `${id}-analysis-asset`, + () => { + return queryClient.fetchQuery( + ['analysis', id, 'asset', url, aoi], + async ({ signal }) => { + const { data } = await axios.post( + `${tileEndpointToUse}/cog/statistics`, + // Making a request with a FC causes a 500 (as of 2023/01/20) + fixAoiFcForStacSearch(aoi), + { params: { url, ...analysisParams }, signal } + ); + return { + date, + + ...data.properties.statistics.b1 + }; + }, + { + staleTime: Infinity + } + ); + } + ); + onProgress({ + status: TimelineDatasetStatus.LOADING, + error: null, + data: null, + meta: { + total: assets.length, + loaded: ++loaded + } + }); + + return statistics; + } + ) ); - e.details = { - assetCount: assets.length - }; + if (layerStatistics.filter(e => e.mean).length === 0) { + return { + ...datasetAnalysis, + status: TimelineDatasetStatus.ERROR, + error: new ExtendedError( + 'The selected time and area of interest contains no valid data. Please adjust your selection.', + 'ANALYSIS_NO_VALID_DATA' + ), + data: null + }; + } + + onProgress({ + status: TimelineDatasetStatus.SUCCESS, + meta: { + total: assets.length, + loaded: assets.length + }, + error: null, + data: { + timeseries: layerStatistics + } + }); return { - ...datasetAnalysis, - status: TimelineDatasetStatus.ERROR, - error: e, - data: null + status: TimelineDatasetStatus.SUCCESS, + meta: { + total: assets.length, + loaded: assets.length + }, + error: null, + data: { + timeseries: layerStatistics + } }; - } - - if (!assets.length) { + + } catch (error) { + // Discard abort related errors. + if (error.revert) { + return { + status: TimelineDatasetStatus.LOADING, + error: null, + data: null, + meta: {} + }; + } + // Cancel any inflight queries. + queryClient.cancelQueries({ queryKey: ['analysis', id] }); + // Remove other requests from the queue. + concurrencyManager.dequeue(`${id}-analysis-asset`); return { ...datasetAnalysis, status: TimelineDatasetStatus.ERROR, - error: new ExtendedError( - 'No data in the given time range and area of interest', - 'ANALYSIS_NO_DATA' - ), + error, data: null }; } + // Isolate arc layer logic here for now + } else { + try { + onProgress({ + status: TimelineDatasetStatus.LOADING, + error: null, + data: null, + meta: { + total: undefined, + loaded: 0 + } + }); - let loaded = 0;//new Array(assets.length).fill(0); + const { data: stacData } = await axios.get(`${datasetData.stacApiEndpoint}/collections/${datasetData.stacCol}`); - const tileEndpointToUse = + const serverBaseUrl = stacData?.links.find(e => e.rel === 'via')?.href; + + const tileEndpointToUse = datasetData.tileApiEndpoint ?? process.env.API_RASTER_ENDPOINT ?? ''; + const collectionId = datasetData.stacCol; + const variable = datasetData.sourceParams?.layers; + const dateTimeRange = [userTzDate2utcString(start), userTzDate2utcString(end)]; + const ffAoi = fixAoiForArcGISAnalysis(aoi); + const fAoi = { + ...ffAoi, + features: ffAoi.features.map(fromMultiPolygontoPolygon) + }; - const analysisParams = datasetData.analysis?.sourceParams ?? {}; - - const layerStatistics = await Promise.all( - assets.map( - async ({ date, url }) => { - const statistics = await concurrencyManager.queue( - `${id}-analysis-asset`, - () => { - return queryClient.fetchQuery( - ['analysis', id, 'asset', url, aoi], - async ({ signal }) => { - const { data } = await axios.post( - `${tileEndpointToUse}/cog/statistics`, - // Making a request with a FC causes a 500 (as of 2023/01/20) - fixAoiFcForStacSearch(aoi), - { params: { url, ...analysisParams }, signal } - ); - return { - date, - - ...data.properties.statistics.b1 - }; - }, - { - staleTime: Infinity - } - ); - } - ); - onProgress({ - status: TimelineDatasetStatus.LOADING, - error: null, - data: null, - meta: { - total: assets.length, - loaded: ++loaded - } - }); + const reqBody = { + server_url: serverBaseUrl, collection_id: collectionId, variable, datetime_range: dateTimeRange, aoi: fAoi + }; - return statistics; - } - ) - ); + const stats = await queryClient.fetchQuery( + ['analysis', id, 'asset', variable, start, end, aoi], + async ({ signal }) => { + const { data } = await axios.post( + `${tileEndpointToUse}/statistics`, + // Making a request with a FC causes a 500 (as of 2023/01/20) + reqBody, + { signal } + ); + // Mimick statistics response + // Since we are mutating the copy + // eslint-disable-next-line fp/no-mutating-methods + return [...Object.keys(data).map(dataDate => ({ + date: new Date(dataDate), + ...data[dataDate] + }))].reverse(); + }, + { + staleTime: Infinity + } + ); + // return response 500 when there is no return :[ + // if (stats.length === 0) { + // return { + // ...datasetAnalysis, + // status: TimelineDatasetStatus.ERROR, + // error: new ExtendedError( + // 'No data in the given time range and area of interest', + // 'ANALYSIS_NO_DATA' + // ), + // data: null + // }; + // } - if (layerStatistics.filter(e => e.mean).length === 0) { + onProgress({ + status: TimelineDatasetStatus.SUCCESS, + meta: { + total: stats.length, + loaded: stats.length + }, + error: null, + data: { + timeseries: stats + } + }); + return { + status: TimelineDatasetStatus.SUCCESS, + meta: { + total: stats.length, + loaded: stats.length + }, + error: null, + data: { + timeseries: stats + } + }; + } catch (error) { return { ...datasetAnalysis, status: TimelineDatasetStatus.ERROR, - error: new ExtendedError( - 'The selected time and area of interest contains no valid data. Please adjust your selection.', - 'ANALYSIS_NO_VALID_DATA' - ), + error, data: null }; } - - onProgress({ - status: TimelineDatasetStatus.SUCCESS, - meta: { - total: assets.length, - loaded: assets.length - }, - error: null, - data: { - timeseries: layerStatistics - } - }); - return { - status: TimelineDatasetStatus.SUCCESS, - meta: { - total: assets.length, - loaded: assets.length - }, - error: null, - data: { - timeseries: layerStatistics - } - }; - } catch (error) { - // Discard abort related errors. - if (error.revert) { - return { - status: TimelineDatasetStatus.LOADING, - error: null, - data: null, - meta: {} - }; - } - - // Cancel any inflight queries. - queryClient.cancelQueries({ queryKey: ['analysis', id] }); - // Remove other requests from the queue. - concurrencyManager.dequeue(`${id}-analysis-asset`); - return { - ...datasetAnalysis, - status: TimelineDatasetStatus.ERROR, - error, - data: null - }; } -} \ No newline at end of file +} + diff --git a/app/scripts/components/exploration/components/map/layer.tsx b/app/scripts/components/exploration/components/map/layer.tsx index d2727abda..b1ea819c1 100644 --- a/app/scripts/components/exploration/components/map/layer.tsx +++ b/app/scripts/components/exploration/components/map/layer.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import * as dateFns from 'date-fns'; import { TimelineDatasetSuccess } from '../../types.d.ts'; -import { getTimeDensityStartDate } from '../../data-utils'; +import { getRelavantDate, getTimeDensityStartDate } from '../../data-utils'; import { useTimelineDatasetAtom, useTimelineDatasetSettings @@ -14,6 +14,8 @@ import { RasterTimeseries } from '$components/common/map/style-generators/raster import { VectorTimeseries } from '$components/common/map/style-generators/vector-timeseries'; import { ZarrTimeseries } from '$components/common/map/style-generators/zarr-timeseries'; import { CMRTimeseries } from '$components/common/map/style-generators/cmr-timeseries'; +import { Arc } from '$components/common/map/style-generators/arc'; + interface LayerProps { id: string; @@ -44,9 +46,10 @@ export function Layer(props: LayerProps) { } // The date needs to match the dataset's time density. + // But ArcGIS data? const relevantDate = useMemo( - () => getTimeDensityStartDate(selectedDay, dataset.data?.timeDensity), - [selectedDay, dataset.data?.timeDensity] + () => dataset.data.type === 'arc'? getRelavantDate(selectedDay, dataset.data.domain, dataset.data.timeDensity) : getTimeDensityStartDate(selectedDay, dataset.data.timeDensity), + [selectedDay, dataset.data.timeDensity, dataset.data.domain, dataset.data.type] ); // Resolve config functions. @@ -106,21 +109,35 @@ export function Layer(props: LayerProps) { opacity={opacity} /> ); - case 'raster': + case 'arc': return ( -