Skip to content

Commit

Permalink
Add functionality to retrieve and store granule date ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
PatchesMaps committed Jul 24, 2024
1 parent e7f69e4 commit d1b1b2e
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 106 deletions.
107 changes: 1 addition & 106 deletions web/js/map/granule/granule-layer-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
datelineShiftGranules,
transformGranulesForProj,
} from './util';
import { getLayerGranuleRanges } from '../util';
import util from '../../util/util';

const { toISOStringSeconds } = util;
Expand Down Expand Up @@ -180,112 +181,6 @@ export default function granuleLayerBuilder(cache, store, createLayerWMTS) {
return granules;
};

/**
* @method makeTime
* @param {string} date
* @returns {number} time
* @description
* Convert date to time
*/
function makeTime(date) {
return new Date(date).getTime();
}

/**
* @method mergeSortedGranuleDateRanges
* @param {array} granules
* @returns {array} mergedGranuleDateRanges
* @description
* Merge overlapping granule date ranges
*/
function mergeSortedGranuleDateRanges(granules) {
return granules.reduce((acc, [start, end]) => {
if (!acc.length) return [[start, end]];
// round start time down and end time up by 1 minute to account for small range gaps
const startTime = makeTime(start) - 60000;
const endTime = makeTime(end) + 60000;
const lastRangeEndTime = makeTime(acc.at(-1)[1]);
const lastRangeStartTime = makeTime(acc.at(-1)[0]);
if ((startTime >= lastRangeStartTime && startTime <= lastRangeEndTime) && (endTime >= lastRangeStartTime && endTime <= lastRangeEndTime)) { // within current range, ignore
return acc;
}
if (startTime > lastRangeEndTime) { // discontinuous, add new range
return [...acc, [start, end]];
}
if (startTime <= lastRangeEndTime && endTime > lastRangeEndTime) { // intersects current range, merge
return acc.with(-1, [acc.at(-1)[0], end]);
}
return acc;
}, []);
}

/**
* @method requestGranules
* @param {object} params
* @returns {array} granules
* @description
* Request granules from CMR
*/
async function requestGranules(params) {
const {
shortName,
extent,
startDate,
endDate,
} = params;
const granules = [];
let hits = Infinity;
let searchAfter = false;
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`;
/* eslint-disable no-await-in-loop */
do { // run the query at least once
const headers = searchAfter ? { 'Cmr-Search-After': searchAfter, 'Client-Id': 'Worldview' } : { 'Client-Id': 'Worldview' };
const res = await fetch(url, { headers, cache: 'force-cache' });
searchAfter = res.headers.get('Cmr-Search-After');
hits = parseInt(res.headers.get('Cmr-Hits'), 10);
const data = await res.json();
granules.push(...data.feed.entry);
} 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

return granules;
}
/**
* @method getLayerGranuleRanges
* @param {object} layer
* @returns {array} granuleDateRanges
* @description
* Get granule date ranges for a given layer
*/
async function getLayerGranuleRanges(layer) {
const extent = [-180, -90, 180, 90];
const startDate = new Date(layer.startDate).toISOString();
const endDate = layer.endDate ? new Date(layer.endDate).toISOString() : new Date().toISOString();
const shortName = layer.conceptIds?.[0]?.shortName;
const nrtParams = {
shortName,
extent,
startDate,
endDate,
};
const nrtGranules = await requestGranules(nrtParams);
let nonNRTGranules = [];
if (shortName.includes('_NRT')) { // if NRT, also get non-NRT granules
const nonNRTShortName = shortName.replace('_NRT', '');
const nonNRTParams = {
shortName: nonNRTShortName,
extent,
startDate,
endDate,
};
nonNRTGranules = await requestGranules(nonNRTParams);
}
const granules = [...nonNRTGranules, ...nrtGranules];
const granuleDateRanges = granules.map(({ time_start: timeStart, time_end: timeEnd }) => [timeStart, timeEnd]);
const mergedGranuleDateRanges = mergeSortedGranuleDateRanges(granuleDateRanges); // merge overlapping granule ranges to simplify rendering

return mergedGranuleDateRanges;
}

/**
* @method getGranuleAttributes
* @param {object} def
Expand Down
14 changes: 14 additions & 0 deletions web/js/map/layerbuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ import {
createVectorUrl,
getGeographicResolutionWMS,
mergeBreakpointLayerAttributes,
getLayerGranuleRanges,
} from './util';
import { addGranuleDateRanges } from '../modules/layers/actions';
import { datesInDateRanges, prevDateInDateRange } from '../modules/layers/util';
import { getSelectedDate } from '../modules/date/selectors';
import {
Expand Down Expand Up @@ -1033,6 +1035,7 @@ export default function mapLayerBuilder(config, cache, store) {
const proj = state.proj.selected;
const {
breakPointLayer,
cmrAvailability,
id,
opacity,
period,
Expand All @@ -1045,6 +1048,17 @@ export default function mapLayerBuilder(config, cache, store) {
let { date } = dateOptions;
let layer = cache.getItem(key);
const isGranule = type === 'granule';
let granuleDateRanges = null;

// if opted in to CMR availability, get granule date ranges if needed
if (cmrAvailability) {
if (!def.granuleDateRanges) {
granuleDateRanges = await getLayerGranuleRanges(def);
store.dispatch(addGranuleDateRanges(def, granuleDateRanges));
} else {
granuleDateRanges = def.granuleDateRanges;
}
}

if (!layer || isGranule || def.type === 'titiler') {
if (!date) date = options.date || getSelectedDate(state);
Expand Down
106 changes: 106 additions & 0 deletions web/js/map/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,109 @@ export function extractDateFromTileErrorURL(url) {
console.error('Date not found in the URL.');
return null;
}

/**
* @method makeTime
* @param {string} date
* @returns {number} time
* @description
* Convert date to time
*/
function makeTime(date) {
return new Date(date).getTime();
}

/**
* @method mergeSortedGranuleDateRanges
* @param {array} granules
* @returns {array} mergedGranuleDateRanges
* @description
* Merge overlapping granule date ranges
*/
function mergeSortedGranuleDateRanges(granules) {
return granules.reduce((acc, [start, end]) => {
if (!acc.length) return [[start, end]];
// round start time down and end time up by 1 minute to account for small range gaps
const startTime = makeTime(start) - 60000;
const endTime = makeTime(end) + 60000;
const lastRangeEndTime = makeTime(acc.at(-1)[1]);
const lastRangeStartTime = makeTime(acc.at(-1)[0]);
if ((startTime >= lastRangeStartTime && startTime <= lastRangeEndTime) && (endTime >= lastRangeStartTime && endTime <= lastRangeEndTime)) { // within current range, ignore
return acc;
}
if (startTime > lastRangeEndTime) { // discontinuous, add new range
return [...acc, [start, end]];
}
if (startTime <= lastRangeEndTime && endTime > lastRangeEndTime) { // intersects current range, merge
return acc.with(-1, [acc.at(-1)[0], end]);
}
return acc;
}, []);
}

/**
* @method requestGranules
* @param {object} params
* @returns {array} granules
* @description
* Request granules from CMR
*/
async function requestGranules(params) {
const {
shortName,
extent,
startDate,
endDate,
} = params;
const granules = [];
let hits = Infinity;
let searchAfter = false;
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`;
/* eslint-disable no-await-in-loop */
do { // run the query at least once
const headers = searchAfter ? { 'Cmr-Search-After': searchAfter, 'Client-Id': 'Worldview' } : { 'Client-Id': 'Worldview' };
const res = await fetch(url, { headers, cache: 'force-cache' });
searchAfter = res.headers.get('Cmr-Search-After');
hits = parseInt(res.headers.get('Cmr-Hits'), 10);
const data = await res.json();
granules.push(...data.feed.entry);
} 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

return granules;
}
/**
* @method getLayerGranuleRanges
* @param {object} layer
* @returns {array} granuleDateRanges
* @description
* Get granule date ranges for a given layer
*/
export async function getLayerGranuleRanges(layer) {
const extent = [-180, -90, 180, 90];
const startDate = new Date(layer.startDate).toISOString();
const endDate = layer.endDate ? new Date(layer.endDate).toISOString() : new Date().toISOString();
const shortName = layer.conceptIds?.[0]?.shortName;
const nrtParams = {
shortName,
extent,
startDate,
endDate,
};
const nrtGranules = await requestGranules(nrtParams);
let nonNRTGranules = [];
if (shortName.includes('_NRT')) { // if NRT, also get non-NRT granules
const nonNRTShortName = shortName.replace('_NRT', '');
const nonNRTParams = {
shortName: nonNRTShortName,
extent,
startDate,
endDate,
};
nonNRTGranules = await requestGranules(nonNRTParams);
}
const granules = [...nonNRTGranules, ...nrtGranules];
const granuleDateRanges = granules.map(({ time_start: timeStart, time_end: timeEnd }) => [timeStart, timeEnd]);
const mergedGranuleDateRanges = mergeSortedGranuleDateRanges(granuleDateRanges); // merge overlapping granule ranges to simplify rendering

return mergedGranuleDateRanges;
}

0 comments on commit d1b1b2e

Please sign in to comment.