Skip to content

Commit 903b2e8

Browse files
authored
WV-3235-Support Time-Limited Display of TEMPO Imagery (#5363)
* show granule only if selected date is within granule date range * Update action name for adding granule date ranges * Add functionality to retrieve and store granule date ranges * remove "cache: 'force-cache'"
1 parent 7e719ff commit 903b2e8

File tree

7 files changed

+189
-107
lines changed

7 files changed

+189
-107
lines changed

web/js/components/timeline/timeline-coverage/timeline-coverage.js

Lines changed: 7 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -19,97 +19,6 @@ import Switch from '../../util/switch';
1919
import LayerCoverageInfoModal from './info-modal';
2020
import CoverageItemList from './coverage-item-list';
2121

22-
function makeTime(date) {
23-
return new Date(date).getTime();
24-
}
25-
26-
function mergeSortedGranuleDateRanges(granules) {
27-
return granules.reduce((acc, [start, end]) => {
28-
if (!acc.length) return [[start, end]];
29-
const startTime = makeTime(start);
30-
const endTime = makeTime(end);
31-
const lastRangeEndTime = makeTime(acc.at(-1)[1]);
32-
const lastRangeStartTime = makeTime(acc.at(-1)[0]);
33-
if ((startTime >= lastRangeStartTime && startTime <= lastRangeEndTime) && (endTime >= lastRangeStartTime && endTime <= lastRangeEndTime)) { // within current range, ignore
34-
return acc;
35-
}
36-
if (startTime > lastRangeEndTime) { // discontinuous, add new range
37-
return [...acc, [start, end]];
38-
}
39-
if (startTime <= lastRangeEndTime && endTime > lastRangeEndTime) { // intersects current range, merge
40-
return acc.with(-1, [acc.at(-1)[0], end]);
41-
}
42-
return acc;
43-
}, []);
44-
}
45-
46-
async function requestGranules(params) {
47-
const {
48-
shortName,
49-
extent,
50-
startDate,
51-
endDate,
52-
} = params;
53-
const granules = [];
54-
let hits = Infinity;
55-
let searchAfter = false;
56-
const url = `https://cmr.earthdata.nasa.gov/search/granules.json?shortName=${shortName}&bounding_box=${extent.join(',')}&temporal=${startDate}/${endDate}&sort_key=start_date&pageSize=2000`;
57-
/* eslint-disable no-await-in-loop */
58-
do { // run the query at least once
59-
const headers = searchAfter ? { 'Cmr-Search-After': searchAfter, 'Client-Id': 'Worldview' } : { 'Client-Id': 'Worldview' };
60-
const res = await fetch(url, { headers });
61-
searchAfter = res.headers.get('Cmr-Search-After');
62-
hits = parseInt(res.headers.get('Cmr-Hits'), 10);
63-
const data = await res.json();
64-
granules.push(...data.feed.entry);
65-
} while (searchAfter || hits > granules.length); // searchAfter will not be present if there are no more results https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#search-after
66-
67-
return granules;
68-
}
69-
70-
async function getLayerGranuleRanges(layer) {
71-
const extent = [-180, -90, 180, 90];
72-
const startDate = new Date(layer.startDate).toISOString();
73-
const endDate = layer.endDate ? new Date(layer.endDate).toISOString() : new Date().toISOString();
74-
const shortName = layer.conceptIds?.[0]?.shortName;
75-
const nrtParams = {
76-
shortName,
77-
extent,
78-
startDate,
79-
endDate,
80-
};
81-
const nrtGranules = await requestGranules(nrtParams);
82-
let nonNRTGranules = [];
83-
if (shortName.includes('_NRT')) { // if NRT, also get non-NRT granules
84-
const nonNRTShortName = shortName.replace('_NRT', '');
85-
const nonNRTParams = {
86-
shortName: nonNRTShortName,
87-
extent,
88-
startDate,
89-
endDate,
90-
};
91-
nonNRTGranules = await requestGranules(nonNRTParams);
92-
}
93-
const granules = [...nonNRTGranules, ...nrtGranules];
94-
const granuleDateRanges = granules.map(({ time_start: timeStart, time_end: timeEnd }) => [timeStart, timeEnd]);
95-
const mergedGranuleDateRanges = mergeSortedGranuleDateRanges(granuleDateRanges); // merge overlapping granule ranges to simplify rendering
96-
97-
return mergedGranuleDateRanges;
98-
}
99-
100-
async function mapGranulesToLayers(layers) {
101-
const promises = layers.map(async (layer) => {
102-
if (!layer.cmrAvailability) return layer;
103-
104-
const ranges = await getLayerGranuleRanges(layer);
105-
106-
return { ...layer, granules: ranges };
107-
});
108-
const cmrLayers = await Promise.all(promises);
109-
110-
return cmrLayers;
111-
}
112-
11322
/*
11423
* Timeline Layer Coverage Panel for temporal coverage.
11524
*
@@ -120,7 +29,6 @@ class TimelineLayerCoveragePanel extends Component {
12029
constructor(props) {
12130
super(props);
12231
this.state = {
123-
cmrLayers: [],
12432
activeLayers: [],
12533
shouldIncludeHiddenLayers: false,
12634
};
@@ -226,8 +134,8 @@ class TimelineLayerCoveragePanel extends Component {
226134
futureTime, ongoing,
227135
} = layer;
228136

229-
if (layer.granules?.length) {
230-
return layer.granules.map(([startDate, endDate]) => {
137+
if (layer.granuleDateRanges?.length) {
138+
return layer.granuleDateRanges.map(([startDate, endDate]) => {
231139
const { gridWidth } = timeScaleOptions[timeScale].timeAxis;
232140
const axisFrontDate = new Date(frontDate).getTime();
233141
const axisBackDate = new Date(backDate).getTime();
@@ -357,11 +265,9 @@ class TimelineLayerCoveragePanel extends Component {
357265
// eslint-disable-next-line react/destructuring-assignment
358266
addMatchingCoverageToTimeline = async (isChecked, layers) => {
359267
const { setMatchingTimelineCoverage } = this.props;
360-
const cmrLayers = await mapGranulesToLayers(layers);
361-
const dateRange = this.getNewMatchingDatesRange(cmrLayers);
268+
const dateRange = this.getNewMatchingDatesRange(layers);
362269
setMatchingTimelineCoverage(dateRange, isChecked);
363270
this.setState({
364-
cmrLayers,
365271
activeLayers: layers,
366272
shouldIncludeHiddenLayers: isChecked,
367273
});
@@ -379,12 +285,12 @@ class TimelineLayerCoveragePanel extends Component {
379285
appNow,
380286
} = this.props;
381287
if (layers.length > 0) {
382-
return layers.flatMap(({ granules, startDate, endDate }) => {
383-
if (!granules?.length) {
288+
return layers.flatMap(({ granuleDateRanges, startDate, endDate }) => {
289+
if (!granuleDateRanges?.length) {
384290
return [{ startDate, endDate: endDate || appNow }];
385291
}
386292

387-
return granules.map(([start, end]) => ({ startDate: start, endDate: end }));
293+
return granuleDateRanges.map(([start, end]) => ({ startDate: start, endDate: end }));
388294
});
389295
}
390296
};
@@ -481,7 +387,6 @@ class TimelineLayerCoveragePanel extends Component {
481387
timeScale,
482388
} = this.props;
483389
const {
484-
cmrLayers,
485390
activeLayers,
486391
shouldIncludeHiddenLayers,
487392
} = this.state;
@@ -536,7 +441,7 @@ class TimelineLayerCoveragePanel extends Component {
536441
</header>
537442
<Scrollbars style={scrollbarStyle}>
538443
<CoverageItemList
539-
activeLayers={cmrLayers}
444+
activeLayers={activeLayers}
540445
appNow={appNow}
541446
axisWidth={axisWidth}
542447
backDate={backDate}

web/js/map/granule/granule-layer-builder.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import OlLayerGroup from 'ol/layer/Group';
22
import { throttle as lodashThrottle } from 'lodash';
33
import OlCollection from 'ol/Collection';
44
import { DEFAULT_NUM_GRANULES } from '../../modules/layers/constants';
5-
import { updateGranuleLayerState } from '../../modules/layers/actions';
5+
import { updateGranuleLayerState, addGranuleDateRanges } from '../../modules/layers/actions';
66
import { getGranuleLayer } from '../../modules/layers/selectors';
77
import {
88
startLoading,
@@ -21,6 +21,7 @@ import {
2121
datelineShiftGranules,
2222
transformGranulesForProj,
2323
} from './util';
24+
import { getLayerGranuleRanges } from '../util';
2425
import util from '../../util/util';
2526

2627
const { toISOStringSeconds } = util;
@@ -138,6 +139,14 @@ export default function granuleLayerBuilder(cache, store, createLayerWMTS) {
138139
});
139140
};
140141

142+
/**
143+
* Check if date is within a range
144+
* @param {Date} date - date to check
145+
* @param {array} ranges - array of date ranges
146+
* @returns {boolean} - true if date is within a range
147+
*/
148+
const isWithinRanges = (date, ranges) => ranges.some(([start, end]) => date >= new Date(start) && date <= new Date(end));
149+
141150
/**
142151
* Get granuleCount number of granules that have visible imagery based on
143152
* predetermined longitude bounds.
@@ -147,19 +156,21 @@ export default function granuleLayerBuilder(cache, store, createLayerWMTS) {
147156
* @param {Date} leadingEdgeDate - timeline date
148157
* @returns {array}
149158
*/
150-
const getVisibleGranules = (availableGranules, granuleCount, leadingEdgeDate) => {
159+
const getVisibleGranules = (availableGranules, granuleCount, leadingEdgeDate, granuleDateRanges) => {
151160
const { proj: { selected: { crs } } } = store.getState();
152161
const granules = [];
153162
const availableCount = availableGranules?.length;
154163
if (!availableCount) return granules;
155164
const count = granuleCount > availableCount ? availableCount : granuleCount;
156165
const sortedAvailableGranules = availableGranules.sort((a, b) => new Date(b.date) - new Date(a.date));
157-
158166
for (let i = 0; granules.length < count; i += 1) {
159167
const item = sortedAvailableGranules[i];
160168
if (!item) break;
161169
const { date } = item;
162-
if (new Date(date) <= leadingEdgeDate && isWithinBounds(crs, item)) {
170+
const dateDate = new Date(date);
171+
const leadingEdgeDateUTC = new Date(leadingEdgeDate.toUTCString());
172+
const isWithinRange = isWithinRanges(leadingEdgeDateUTC, granuleDateRanges);
173+
if (dateDate <= leadingEdgeDateUTC && isWithinRange && isWithinBounds(crs, item)) {
163174
granules.unshift(item);
164175
}
165176
}
@@ -182,16 +193,27 @@ export default function granuleLayerBuilder(cache, store, createLayerWMTS) {
182193
const { granuleCount, date, group } = options;
183194
const { count: currentCount } = getGranuleLayer(state, def.id) || {};
184195
const count = currentCount || granuleCount || def.count || DEFAULT_NUM_GRANULES;
196+
let granuleDateRanges = null;
185197

186198
// get granule dates waiting for CMR query and filtering (if necessary)
187199
const availableGranules = await getQueriedGranuleDates(def, date, group);
188-
const visibleGranules = getVisibleGranules(availableGranules, count, date);
200+
// if opted in to CMR availability, get granule date ranges if needed
201+
if (def.cmrAvailability) {
202+
if (!def.granuleDateRanges) {
203+
granuleDateRanges = await getLayerGranuleRanges(def);
204+
store.dispatch(addGranuleDateRanges(def, granuleDateRanges));
205+
} else {
206+
granuleDateRanges = def.granuleDateRanges;
207+
}
208+
}
209+
const visibleGranules = getVisibleGranules(availableGranules, count, date, granuleDateRanges);
189210
const transformedGranules = transformGranulesForProj(visibleGranules, crs);
190211

191212
return {
192213
count,
193214
granuleDates: transformedGranules.map((g) => g.date),
194215
visibleGranules: transformedGranules,
216+
granuleDateRanges,
195217
};
196218
};
197219

web/js/map/layerbuilder.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ import {
3333
createVectorUrl,
3434
getGeographicResolutionWMS,
3535
mergeBreakpointLayerAttributes,
36+
getLayerGranuleRanges,
3637
} from './util';
38+
import { addGranuleDateRanges } from '../modules/layers/actions';
3739
import { datesInDateRanges, prevDateInDateRange } from '../modules/layers/util';
3840
import { getSelectedDate } from '../modules/date/selectors';
3941
import {
@@ -1033,6 +1035,7 @@ export default function mapLayerBuilder(config, cache, store) {
10331035
const proj = state.proj.selected;
10341036
const {
10351037
breakPointLayer,
1038+
cmrAvailability,
10361039
id,
10371040
opacity,
10381041
period,
@@ -1045,6 +1048,17 @@ export default function mapLayerBuilder(config, cache, store) {
10451048
let { date } = dateOptions;
10461049
let layer = cache.getItem(key);
10471050
const isGranule = type === 'granule';
1051+
let granuleDateRanges = null;
1052+
1053+
// if opted in to CMR availability, get granule date ranges if needed
1054+
if (cmrAvailability) {
1055+
if (!def.granuleDateRanges) {
1056+
granuleDateRanges = await getLayerGranuleRanges(def);
1057+
store.dispatch(addGranuleDateRanges(def, granuleDateRanges));
1058+
} else {
1059+
granuleDateRanges = def.granuleDateRanges;
1060+
}
1061+
}
10481062

10491063
if (!layer || isGranule || def.type === 'titiler') {
10501064
if (!date) date = options.date || getSelectedDate(state);

web/js/map/util.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,109 @@ export function extractDateFromTileErrorURL(url) {
274274
console.error('Date not found in the URL.');
275275
return null;
276276
}
277+
278+
/**
279+
* @method makeTime
280+
* @param {string} date
281+
* @returns {number} time
282+
* @description
283+
* Convert date to time
284+
*/
285+
function makeTime(date) {
286+
return new Date(date).getTime();
287+
}
288+
289+
/**
290+
* @method mergeSortedGranuleDateRanges
291+
* @param {array} granules
292+
* @returns {array} mergedGranuleDateRanges
293+
* @description
294+
* Merge overlapping granule date ranges
295+
*/
296+
function mergeSortedGranuleDateRanges(granules) {
297+
return granules.reduce((acc, [start, end]) => {
298+
if (!acc.length) return [[start, end]];
299+
// round start time down and end time up by 1 minute to account for small range gaps
300+
const startTime = makeTime(start) - 60000;
301+
const endTime = makeTime(end) + 60000;
302+
const lastRangeEndTime = makeTime(acc.at(-1)[1]);
303+
const lastRangeStartTime = makeTime(acc.at(-1)[0]);
304+
if ((startTime >= lastRangeStartTime && startTime <= lastRangeEndTime) && (endTime >= lastRangeStartTime && endTime <= lastRangeEndTime)) { // within current range, ignore
305+
return acc;
306+
}
307+
if (startTime > lastRangeEndTime) { // discontinuous, add new range
308+
return [...acc, [start, end]];
309+
}
310+
if (startTime <= lastRangeEndTime && endTime > lastRangeEndTime) { // intersects current range, merge
311+
return acc.with(-1, [acc.at(-1)[0], end]);
312+
}
313+
return acc;
314+
}, []);
315+
}
316+
317+
/**
318+
* @method requestGranules
319+
* @param {object} params
320+
* @returns {array} granules
321+
* @description
322+
* Request granules from CMR
323+
*/
324+
async function requestGranules(params) {
325+
const {
326+
shortName,
327+
extent,
328+
startDate,
329+
endDate,
330+
} = params;
331+
const granules = [];
332+
let hits = Infinity;
333+
let searchAfter = false;
334+
const url = `https://cmr.earthdata.nasa.gov/search/granules.json?shortName=${shortName}&bounding_box=${extent.join(',')}&temporal=${startDate}/${endDate}&sort_key=start_date&pageSize=2000`;
335+
/* eslint-disable no-await-in-loop */
336+
do { // run the query at least once
337+
const headers = searchAfter ? { 'Cmr-Search-After': searchAfter, 'Client-Id': 'Worldview' } : { 'Client-Id': 'Worldview' };
338+
const res = await fetch(url, { headers });
339+
searchAfter = res.headers.get('Cmr-Search-After');
340+
hits = parseInt(res.headers.get('Cmr-Hits'), 10);
341+
const data = await res.json();
342+
granules.push(...data.feed.entry);
343+
} while (searchAfter || hits > granules.length); // searchAfter will not be present if there are no more results https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html#search-after
344+
345+
return granules;
346+
}
347+
/**
348+
* @method getLayerGranuleRanges
349+
* @param {object} layer
350+
* @returns {array} granuleDateRanges
351+
* @description
352+
* Get granule date ranges for a given layer
353+
*/
354+
export async function getLayerGranuleRanges(layer) {
355+
const extent = [-180, -90, 180, 90];
356+
const startDate = new Date(layer.startDate).toISOString();
357+
const endDate = layer.endDate ? new Date(layer.endDate).toISOString() : new Date().toISOString();
358+
const shortName = layer.conceptIds?.[0]?.shortName;
359+
const nrtParams = {
360+
shortName,
361+
extent,
362+
startDate,
363+
endDate,
364+
};
365+
const nrtGranules = await requestGranules(nrtParams);
366+
let nonNRTGranules = [];
367+
if (shortName.includes('_NRT')) { // if NRT, also get non-NRT granules
368+
const nonNRTShortName = shortName.replace('_NRT', '');
369+
const nonNRTParams = {
370+
shortName: nonNRTShortName,
371+
extent,
372+
startDate,
373+
endDate,
374+
};
375+
nonNRTGranules = await requestGranules(nonNRTParams);
376+
}
377+
const granules = [...nonNRTGranules, ...nrtGranules];
378+
const granuleDateRanges = granules.map(({ time_start: timeStart, time_end: timeEnd }) => [timeStart, timeEnd]);
379+
const mergedGranuleDateRanges = mergeSortedGranuleDateRanges(granuleDateRanges); // merge overlapping granule ranges to simplify rendering
380+
381+
return mergedGranuleDateRanges;
382+
}

0 commit comments

Comments
 (0)