Skip to content

Commit

Permalink
Cmr layer (#805)
Browse files Browse the repository at this point in the history
- I ended up leaving the main logic as it is (CMR layer doesn't call CMR
endpoint directly - There seem to be some cases that stac collection id
doesn't match with cmr's short_name? ex.
https://cmr.earthdata.nasa.gov/search/collections.umm_json?short_name=TRMM_3B42_Daily.v7&version=07
- I get an empty response _ all in all, I thought it is better to keep
the source of truth in one place for now.)

- Make CMR layer (that shares zarr paint layer with Zarr layer)
- We currently have two map components (which we should consolidate in
the near future related issue: #712 ) This is why there are two places
for the layers.
- CMR layer won't show up for Analysis. (The timeline will fail for new
E&A. User can still explore the map.- you can check
new E&A page by putting a feature flagging variable in `.env`:
`FEATURE_NEW_EXPLORATION = 'TRUE'`. I also attached a screenshot below.)


![Screen Shot 2024-01-19 at 9 27 09
AM](https://github.com/NASA-IMPACT/veda-ui/assets/4583806/fe665936-c88b-4c1c-a422-eb7f808c04cb)
  • Loading branch information
hanbyul-here authored Jan 25, 2024
2 parents 5e725f5 + 76d9214 commit 21db1bd
Show file tree
Hide file tree
Showing 18 changed files with 493 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,45 +115,51 @@ export function useStacCollectionSearch({

function getInTemporalAndSpatialExtent(collectionData, aoi, timeRange) {
const matchingCollectionIds = collectionData.reduce((acc, col) => {
const { id, stacApiEndpoint } = col;

// Is is a dataset defined in the app?
// If not, skip other calculations.
const isAppDataset = allAvailableDatasetsLayers.some((l) => {
const stacApiEndpointUsed =
l.stacApiEndpoint ?? process.env.API_STAC_ENDPOINT;
return l.stacCol === id && stacApiEndpointUsed === stacApiEndpoint;
});

if (
!isAppDataset ||
!col.extent.spatial.bbox ||
!col.extent.temporal.interval
) {
return acc;
}

const bbox = col.extent.spatial.bbox[0];
const start = utcString2userTzDate(col.extent.temporal.interval[0][0]);
const end = utcString2userTzDate(col.extent.temporal.interval[0][1]);

const isInAOI = aoi.features.some((feature) =>
booleanIntersects(feature, bboxPolygon(bbox))
);

const isInTOI = areIntervalsOverlapping(
{ start: new Date(timeRange.start), end: new Date(timeRange.end) },
{
start: new Date(start),
end: new Date(end)
try {
const { id, stacApiEndpoint } = col;

// Is is a dataset defined in the app?
// If not, skip other calculations.
const isAppDataset = allAvailableDatasetsLayers.some((l) => {
const stacApiEndpointUsed =
l.stacApiEndpoint ?? process.env.API_STAC_ENDPOINT;
return l.stacCol === id && stacApiEndpointUsed === stacApiEndpoint;
});

if (
!isAppDataset ||
!col.extent.spatial.bbox ||
!col.extent.temporal.interval
) {
return acc;
}
);

if (isInAOI && isInTOI) {
return [...acc, id];
} else {

const bbox = col.extent.spatial.bbox[0];
const start = utcString2userTzDate(col.extent.temporal.interval[0][0]);
const end = utcString2userTzDate(col.extent.temporal.interval[0][1]);

const isInAOI = aoi.features.some((feature) =>
booleanIntersects(feature, bboxPolygon(bbox))
);

const isInTOI = areIntervalsOverlapping(
{ start: new Date(timeRange.start), end: new Date(timeRange.end) },
{
start: new Date(start),
end: new Date(end)
}
);

if (isInAOI && isInTOI) {
return [...acc, id];
} else {
return acc;
}
} catch (e) {
// If somehow the data is not in the shape we want, just skip it
return acc;
}

}, []);

const filteredDatasets = allAvailableDatasetsLayers.filter((l) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import { BaseGeneratorParams } from '../types';
import { ZarrPaintLayer } from './zarr-timeseries';
import { useCMR } from './hooks';
import { ActionStatus } from '$utils/status';

interface AssetUrlReplacement {
from: string;
to: string;
}

export interface CMRTimeseriesProps extends BaseGeneratorParams {
id: string;
stacCol: string;
date?: Date;
sourceParams?: Record<string, any>;
stacApiEndpoint?: string;
tileApiEndpoint?: string;
assetUrlReplacements?: AssetUrlReplacement;
zoomExtent?: number[];
onStatusChange?: (result: { status: ActionStatus; id: string }) => void;
}

export function CMRTimeseries(props:CMRTimeseriesProps) {
const {
id,
stacCol,
stacApiEndpoint,
date,
assetUrlReplacements,
onStatusChange,
} = props;

const stacApiEndpointToUse = stacApiEndpoint?? process.env.API_STAC_ENDPOINT;
const assetUrl = useCMR({ id, stacCol, stacApiEndpointToUse, date, assetUrlReplacements, stacApiEndpoint, onStatusChange });
return <ZarrPaintLayer {...props} assetUrl={assetUrl} />;
}
113 changes: 113 additions & 0 deletions app/scripts/components/common/map/style-generators/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {useState, useEffect} from 'react';
import { requestQuickCache } from '../utils';
import { S_FAILED, S_LOADING, S_SUCCEEDED } from '$utils/status';

interface AssetUrlReplacement {
from: string;
to: string;
}
interface ZarrResponseData {
assets: {
zarr: {
href: string
}
}
}
interface CMRResponseData {
features: {
assets: {
data: {
href: string
}
}
}[]
}

export function useZarr({ id, stacCol, stacApiEndpointToUse, date, onStatusChange }){
const [assetUrl, setAssetUrl] = useState('');

useEffect(() => {
const controller = new AbortController();

async function load() {
try {
onStatusChange?.({ status: S_LOADING, id });
const data:ZarrResponseData = await requestQuickCache({
url: `${stacApiEndpointToUse}/collections/${stacCol}`,
method: 'GET',
controller
});

setAssetUrl(data.assets.zarr.href);
onStatusChange?.({ status: S_SUCCEEDED, id });
} catch (error) {
if (!controller.signal.aborted) {
setAssetUrl('');
onStatusChange?.({ status: S_FAILED, id });
}
return;
}
}

load();

return () => {
controller.abort();
};
}, [id, stacCol, stacApiEndpointToUse, date, onStatusChange]);

return assetUrl;
}



export function useCMR({ id, stacCol, stacApiEndpointToUse, date, assetUrlReplacements, stacApiEndpoint, onStatusChange }){
const [assetUrl, setAssetUrl] = useState('');

const replaceInAssetUrl = (url: string, replacement: AssetUrlReplacement) => {
const {from, to } = replacement;
const cmrAssetUrl = url.replace(from, to);
return cmrAssetUrl;
};


useEffect(() => {
const controller = new AbortController();

async function load() {
try {
onStatusChange?.({ status: S_LOADING, id });
if (!assetUrlReplacements) throw (new Error('CMR layer requires asset url remplacement attributes'));

// Zarr collections in _VEDA_ should have a single entrypoint (zarr or virtual zarr / reference)
// CMR endpoints will be using individual items' assets, so we query for the asset url
const stacApiEndpointToUse = `${stacApiEndpoint}/search?collections=${stacCol}&datetime=${date?.toISOString()}`;

const data:CMRResponseData = await requestQuickCache({
url: stacApiEndpointToUse,
method: 'GET',
controller
});

const assetUrl = replaceInAssetUrl(data.features[0].assets.data.href, assetUrlReplacements);
setAssetUrl(assetUrl);
onStatusChange?.({ status: S_SUCCEEDED, id });
} catch (error) {
if (!controller.signal.aborted) {
setAssetUrl('');
onStatusChange?.({ status: S_FAILED, id });
}
return;
}
}

load();

return () => {
controller.abort();
};
}, [id, stacCol, stacApiEndpointToUse, date, assetUrlReplacements, stacApiEndpoint, onStatusChange]);

return assetUrl;

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';
import qs from 'qs';
import { RasterSource, RasterLayer } from 'mapbox-gl';

import { requestQuickCache } from '../utils';
import useMapStyle from '../hooks/use-map-style';
import useGeneratorParams from '../hooks/use-generator-params';
import { BaseGeneratorParams } from '../types';

import { ActionStatus, S_FAILED, S_LOADING, S_SUCCEEDED } from '$utils/status';
import { useZarr } from './hooks';
import { ActionStatus } from '$utils/status';

export interface ZarrTimeseriesProps extends BaseGeneratorParams {
id: string;
Expand All @@ -20,62 +20,31 @@ export interface ZarrTimeseriesProps extends BaseGeneratorParams {
onStatusChange?: (result: { status: ActionStatus; id: string }) => void;
}

export function ZarrTimeseries(props: ZarrTimeseriesProps) {
interface ZarrPaintLayerProps extends BaseGeneratorParams {
id: string;
date?: Date;
sourceParams?: Record<string, any>;
tileApiEndpoint?: string;
zoomExtent?: number[];
assetUrl: string;
}

export function ZarrPaintLayer(props: ZarrPaintLayerProps) {
const {
id,
stacCol,
stacApiEndpoint,
tileApiEndpoint,
date,
sourceParams,
zoomExtent,
onStatusChange,
assetUrl,
hidden,
opacity
} = props;

const { updateStyle } = useMapStyle();
const [assetUrl, setAssetUrl] = useState('');

const [minZoom] = zoomExtent ?? [0, 20];

const stacApiEndpointToUse = stacApiEndpoint ?? process.env.API_STAC_ENDPOINT;

const generatorId = `zarr-timeseries-${id}`;

//
// Get the asset url
//
useEffect(() => {
const controller = new AbortController();

async function load() {
try {
onStatusChange?.({ status: S_LOADING, id });
const data = await requestQuickCache<any>({
url: `${stacApiEndpointToUse}/collections/${stacCol}`,
method: 'GET',
controller
});

setAssetUrl(data.assets.zarr.href);
onStatusChange?.({ status: S_SUCCEEDED, id });
} catch (error) {
if (!controller.signal.aborted) {
setAssetUrl('');
onStatusChange?.({ status: S_FAILED, id });
}
return;
}
}

load();

return () => {
controller.abort();
};
}, [id, stacCol, stacApiEndpointToUse, date, onStatusChange]);

//
// Generate Mapbox GL layers and sources for raster timeseries
//
Expand All @@ -88,7 +57,7 @@ export function ZarrTimeseries(props: ZarrTimeseriesProps) {

useEffect(
() => {
if (!tileApiEndpoint) return;
if (!assetUrl) return;

const tileParams = qs.stringify({
url: assetUrl,
Expand Down Expand Up @@ -165,3 +134,17 @@ export function ZarrTimeseries(props: ZarrTimeseriesProps) {

return null;
}

export function ZarrTimeseries(props:ZarrTimeseriesProps) {
const {
id,
stacCol,
stacApiEndpoint,
date,
onStatusChange,
} = props;

const stacApiEndpointToUse = stacApiEndpoint?? process.env.API_STAC_ENDPOINT;
const assetUrl = useZarr({id, stacCol, stacApiEndpointToUse, date, onStatusChange});
return <ZarrPaintLayer {...props} assetUrl={assetUrl} />;
}
1 change: 1 addition & 0 deletions app/scripts/components/common/mapbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ function MapboxMapComponent(
id={`base-${baseLayerResolvedData.id}`}
stacApiEndpoint={baseLayerResolvedData.stacApiEndpoint}
tileApiEndpoint={baseLayerResolvedData.tileApiEndpoint}
assetUrlReplacements={baseLayerResolvedData.assetUrlReplacements}
stacCol={baseLayerResolvedData.stacCol}
mapInstance={mapRef.current}
isPositionSet={!!initialPosition}
Expand Down
Loading

0 comments on commit 21db1bd

Please sign in to comment.