diff --git a/web/js/components/timeline/timeline-axis/timeline-axis.js b/web/js/components/timeline/timeline-axis/timeline-axis.js
index 2d08c46874..cdb2cb9d63 100644
--- a/web/js/components/timeline/timeline-axis/timeline-axis.js
+++ b/web/js/components/timeline/timeline-axis/timeline-axis.js
@@ -1335,52 +1335,50 @@ class TimelineAxis extends Component {
timeScale,
matchingTimelineCoverage,
} = this.props;
- const {
- startDate,
- endDate,
- } = matchingTimelineCoverage;
-
- const positionTransformX = position + transformX;
- const { gridWidth } = timeScaleOptions[timeScale].timeAxis;
- const axisFrontDate = new Date(frontDate).getTime();
- const axisBackDate = new Date(backDate).getTime();
- const layerStart = new Date(startDate).getTime();
- const layerEnd = new Date(endDate).getTime();
-
- let visible = true;
- if (layerStart >= axisBackDate || layerEnd <= axisFrontDate) {
- visible = false;
- }
-
- let leftOffset = 0;
- const layerStartBeforeAxisFront = layerStart < axisFrontDate;
- const layerEndBeforeAxisBack = layerEnd <= axisBackDate;
- // oversized width allows axis drag buffer
- let width = axisWidth * 2;
- if (visible) {
- if (layerStartBeforeAxisFront) {
- leftOffset = 0;
- } else {
- // positive diff means layerStart more recent than axisFrontDate
- const diff = moment.utc(layerStart).diff(axisFrontDate, timeScale, true);
- const gridDiff = gridWidth * diff;
- leftOffset = gridDiff + positionTransformX;
+ return matchingTimelineCoverage.map(({ startDate, endDate }) => {
+ const positionTransformX = position + transformX;
+ const { gridWidth } = timeScaleOptions[timeScale].timeAxis;
+ const axisFrontDate = new Date(frontDate).getTime();
+ const axisBackDate = new Date(backDate).getTime();
+ const layerStart = new Date(startDate).getTime();
+ const layerEnd = new Date(endDate).getTime();
+
+ let visible = true;
+ if (layerStart >= axisBackDate || layerEnd <= axisFrontDate) {
+ visible = false;
}
- if (layerEndBeforeAxisBack) {
- // positive diff means layerEnd earlier than back date
- const diff = moment.utc(layerEnd).diff(axisFrontDate, timeScale, true);
- const gridDiff = gridWidth * diff;
- width = Math.max(gridDiff + positionTransformX - leftOffset, 0);
+ let leftOffset = 0;
+ const layerStartBeforeAxisFront = layerStart < axisFrontDate;
+ const layerEndBeforeAxisBack = layerEnd <= axisBackDate;
+
+ // oversized width allows axis drag buffer
+ let width = axisWidth * 2;
+ if (visible) {
+ if (layerStartBeforeAxisFront) {
+ leftOffset = 0;
+ } else {
+ // positive diff means layerStart more recent than axisFrontDate
+ const diff = moment.utc(layerStart).diff(axisFrontDate, timeScale, true);
+ const gridDiff = gridWidth * diff;
+ leftOffset = gridDiff + positionTransformX;
+ }
+
+ if (layerEndBeforeAxisBack) {
+ // positive diff means layerEnd earlier than back date
+ const diff = moment.utc(layerEnd).diff(axisFrontDate, timeScale, true);
+ const gridDiff = gridWidth * diff;
+ width = Math.max(gridDiff + positionTransformX - leftOffset, 0);
+ }
}
- }
- return {
- visible,
- leftOffset,
- width,
- };
+ return {
+ visible,
+ leftOffset,
+ width,
+ };
+ });
};
/**
@@ -1389,32 +1387,30 @@ class TimelineAxis extends Component {
* @param {Number} transformX
* @returns {Object} DOM SVG object
*/
- createMatchingCoverageLineDOMEl = (lineCoverageOptions, transformX) => {
- const { leftOffset, visible, width } = lineCoverageOptions;
- return (
-
-
-
- );
- };
+ createMatchingCoverageLineDOMEl = (lineCoverageOptions, transformX) => lineCoverageOptions.map(({ leftOffset, visible, width }, i) => (
+
+
+
+ ));
render() {
const {
@@ -1565,7 +1561,7 @@ TimelineAxis.propTypes = {
isTimelineDragging: PropTypes.bool,
isTourActive: PropTypes.bool,
leftOffset: PropTypes.number,
- matchingTimelineCoverage: PropTypes.object,
+ matchingTimelineCoverage: PropTypes.array,
onDateChange: PropTypes.func,
parentOffset: PropTypes.number,
position: PropTypes.number,
diff --git a/web/js/components/timeline/timeline-coverage/coverage-item-container.js b/web/js/components/timeline/timeline-coverage/coverage-item-container.js
index e04f4da90a..3f188c7bef 100644
--- a/web/js/components/timeline/timeline-coverage/coverage-item-container.js
+++ b/web/js/components/timeline/timeline-coverage/coverage-item-container.js
@@ -126,7 +126,7 @@ class CoverageItemContainer extends Component {
} = getLayerItemStyles(visible, id);
// get line container dimensions
- const containerLineDimensions = getMatchingCoverageLineDimensions(layer);
+ const containerLineDimensions = getMatchingCoverageLineDimensions(layer).filter(({ visible }) => visible);
return (
visible);
// create DOM line element
const key = `${id}-${multiIndex}`;
- return multiLineRangeOptions.visible
- && (
-
-
-
- );
+ return (
+
+
+
+ );
})
- : containerLineDimensions.visible && (
-
+ : (
+
)}
diff --git a/web/js/components/timeline/timeline-coverage/coverage-line.js b/web/js/components/timeline/timeline-coverage/coverage-line.js
index c04535e462..4330375c82 100644
--- a/web/js/components/timeline/timeline-coverage/coverage-line.js
+++ b/web/js/components/timeline/timeline-coverage/coverage-line.js
@@ -148,11 +148,11 @@ class CoverageLine extends PureComponent {
layerPeriod,
index,
} = this.props;
- return (
+ return options.map((option) => (
{this.createMatchingCoverageLineDOMEl(
id,
- options,
+ option,
lineType,
startDate,
endDate,
@@ -161,7 +161,7 @@ class CoverageLine extends PureComponent {
index,
)}
- );
+ ));
}
}
@@ -172,7 +172,7 @@ CoverageLine.propTypes = {
index: PropTypes.string,
layerPeriod: PropTypes.string,
lineType: PropTypes.string,
- options: PropTypes.object,
+ options: PropTypes.array,
positionTransformX: PropTypes.number,
startDate: PropTypes.string,
};
diff --git a/web/js/components/timeline/timeline-coverage/timeline-coverage.js b/web/js/components/timeline/timeline-coverage/timeline-coverage.js
index 8b3ee40037..03076f29f2 100644
--- a/web/js/components/timeline/timeline-coverage/timeline-coverage.js
+++ b/web/js/components/timeline/timeline-coverage/timeline-coverage.js
@@ -11,7 +11,6 @@ import googleTagManager from 'googleTagManager';
import { timeScaleOptions } from '../../../modules/date/constants';
import {
filterProjLayersWithStartDate,
- getMaxLayerEndDates,
} from '../../../modules/date/util';
import { getActiveLayers } from '../../../modules/layers/selectors';
import { toggleCustomContent } from '../../../modules/modal/actions';
@@ -20,6 +19,67 @@ import Switch from '../../util/switch';
import LayerCoverageInfoModal from './info-modal';
import CoverageItemList from './coverage-item-list';
+function makeTime(date) {
+ return new Date(date).getTime();
+}
+
+function mergeSortedGranuleDateRanges(granules) {
+ return granules.reduce((acc, [start, end]) => {
+ if (!acc.length) return [[start, end]];
+ const startTime = makeTime(start);
+ const endTime = makeTime(end);
+ 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;
+ }, []);
+}
+
+async function getLayerGranuleRanges(layer) {
+ const conceptID = layer.conceptIds?.[0]?.value;
+ 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 granules = [];
+ let hits = Infinity;
+ let searchAfter = false;
+ const url = `https://cmr.earthdata.nasa.gov/search/granules.json?collection_concept_id=${conceptID}&bounding_box=${extent.join(',')}&temporal=${startDate}/${endDate}&sort_key=start_date&pageSize=2000`;
+ /* eslint-disable no-await-in-loop */
+ do {
+ const headers = searchAfter ? { 'Cmr-Search-After': searchAfter, 'Client-Id': 'worldview' } : { 'Client-Id': 'worldview' };
+ const res = await fetch(url, { headers });
+ 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);
+ const granuleDateRanges = granules.map(({ time_start: timeStart, time_end: timeEnd }) => [timeStart, timeEnd]);
+ const mergedGranuleDateRanges = mergeSortedGranuleDateRanges(granuleDateRanges);
+
+ return mergedGranuleDateRanges;
+}
+
+async function mapGranulesToLayers(layers) {
+ const promises = layers.map(async (layer) => {
+ if (layer.type !== 'granule') return layer;
+
+ const ranges = await getLayerGranuleRanges(layer);
+
+ return { ...layer, granules: ranges };
+ });
+ const cmrLayers = await Promise.all(promises);
+
+ return cmrLayers;
+}
+
/*
* Timeline Layer Coverage Panel for temporal coverage.
*
@@ -30,6 +90,7 @@ class TimelineLayerCoveragePanel extends Component {
constructor(props) {
super(props);
this.state = {
+ cmrLayers: [],
activeLayers: [],
shouldIncludeHiddenLayers: false,
};
@@ -119,7 +180,7 @@ class TimelineLayerCoveragePanel extends Component {
* @param {Object} layer
* @param {String} rangeStart
* @param {String} rangeEnd
- * @returns {Object} visible, leftOffset, width, isWidthGreaterThanRendered
+ * @returns {Array} visible, leftOffset, width, isWidthGreaterThanRendered
*/
getMatchingCoverageLineDimensions = (layer, rangeStart, rangeEnd) => {
const {
@@ -132,9 +193,62 @@ class TimelineLayerCoveragePanel extends Component {
timelineStartDateLimit,
} = this.props;
const {
- endDate, futureTime, startDate, ongoing,
+ futureTime, ongoing,
} = layer;
+ if (layer.granules?.length) {
+ return layer.granules.map(([startDate, endDate]) => {
+ const { gridWidth } = timeScaleOptions[timeScale].timeAxis;
+ const axisFrontDate = new Date(frontDate).getTime();
+ const axisBackDate = new Date(backDate).getTime();
+ let layerStart;
+ const layerEnd = new Date(endDate).getTime();
+
+ if (rangeStart || startDate) {
+ layerStart = new Date(startDate).getTime();
+ } else {
+ layerStart = new Date(timelineStartDateLimit).getTime();
+ }
+
+ let visible = true;
+ if (layerStart >= axisBackDate || layerEnd <= axisFrontDate) {
+ visible = false;
+ }
+
+ let leftOffset = 0;
+ const isWidthGreaterThanRendered = layerStart < axisFrontDate || layerEnd > axisBackDate;
+ const layerStartBeforeAxisFront = layerStart <= axisFrontDate;
+ const layerEndBeforeAxisBack = layerEnd <= axisBackDate;
+ // oversized width allows axis drag buffer
+ let width = axisWidth * 5;
+ if (visible) {
+ if (layerStartBeforeAxisFront) {
+ leftOffset = 0;
+ } else {
+ // positive diff means layerStart more recent than axisFrontDate
+ const diff = moment.utc(layerStart).diff(axisFrontDate, timeScale, true);
+ const gridDiff = gridWidth * diff;
+ leftOffset = gridDiff + positionTransformX;
+ }
+ if (layerEndBeforeAxisBack) {
+ // positive diff means layerEnd earlier than back date
+ const diff = moment.utc(layerEnd).diff(axisFrontDate, timeScale, true);
+ const gridDiff = gridWidth * diff;
+ width = gridDiff + positionTransformX - leftOffset;
+ }
+ }
+
+ return {
+ visible,
+ leftOffset,
+ width,
+ isWidthGreaterThanRendered,
+ layerStartBeforeAxisFront,
+ layerEndBeforeAxisBack,
+ };
+ });
+ }
+ const { startDate, endDate } = layer;
const { gridWidth } = timeScaleOptions[timeScale].timeAxis;
const axisFrontDate = new Date(frontDate).getTime();
const axisBackDate = new Date(backDate).getTime();
@@ -182,14 +296,14 @@ class TimelineLayerCoveragePanel extends Component {
}
}
- return {
+ return [{
visible,
leftOffset,
width,
isWidthGreaterThanRendered,
layerStartBeforeAxisFront,
layerEndBeforeAxisBack,
- };
+ }];
};
/**
@@ -211,11 +325,13 @@ class TimelineLayerCoveragePanel extends Component {
* @returns {void}
*/
// eslint-disable-next-line react/destructuring-assignment
- addMatchingCoverageToTimeline = (isChecked, layers) => {
+ addMatchingCoverageToTimeline = async (isChecked, layers) => {
const { setMatchingTimelineCoverage } = this.props;
- const dateRange = this.getNewMatchingDatesRange(layers);
+ const cmrLayers = await mapGranulesToLayers(layers);
+ const dateRange = this.getNewMatchingDatesRange(cmrLayers);
setMatchingTimelineCoverage(dateRange, isChecked);
this.setState({
+ cmrLayers,
activeLayers: layers,
shouldIncludeHiddenLayers: isChecked,
});
@@ -232,36 +348,14 @@ class TimelineLayerCoveragePanel extends Component {
const {
appNow,
} = this.props;
- let startDate;
- let endDate = new Date(appNow);
if (layers.length > 0) {
- // for each start date, find latest that is still below end date
- const startDates = layers.reduce((acc, x) => (x.startDate ? acc.concat(x.startDate) : acc), []);
- for (let i = 0; i < startDates.length; i += 1) {
- const date = new Date(startDates[i]);
- if (i === 0) {
- startDate = date;
+ return layers.flatMap(({ granules, startDate, endDate }) => {
+ if (!granules?.length) {
+ return [{ startDate, endDate: endDate || appNow }];
}
- if (date.getTime() > startDate.getTime()) {
- startDate = date;
- }
- }
- // for each end date, find earliest that is still after start date
- const endDates = getMaxLayerEndDates(layers, appNow);
- for (let i = 0; i < endDates.length; i += 1) {
- const date = new Date(endDates[i]);
- if (i === 0) {
- endDate = date;
- }
- if (date.getTime() < endDate.getTime()) {
- endDate = date;
- }
- }
- return {
- startDate: startDate.toISOString(),
- endDate: endDate.toISOString(),
- };
+ return granules.map(([start, end]) => ({ startDate: start, endDate: end }));
+ });
}
};
@@ -357,6 +451,7 @@ class TimelineLayerCoveragePanel extends Component {
timeScale,
} = this.props;
const {
+ cmrLayers,
activeLayers,
shouldIncludeHiddenLayers,
} = this.state;
@@ -411,7 +506,7 @@ class TimelineLayerCoveragePanel extends Component {
{
const { proj: { selected: { crs } } } = store.getState();
const granules = [];
- const availableCount = availableGranules.length;
+ const availableCount = availableGranules?.length;
if (!availableCount) return granules;
const count = granuleCount > availableCount ? availableCount : granuleCount;