Skip to content

Commit 6c99ffe

Browse files
authored
Feature/gu UI 3663 (#23)
* cleanup datepicker validation * cleanup spatial picker validation for data access * cleanup subsetter datepicker and spatial picker * ensure data rods works as intended
1 parent 656f320 commit 6c99ffe

20 files changed

Lines changed: 1397 additions & 489 deletions

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,13 @@ Then open `notebooks/playground.ipynb` to test components in Jupyter.
168168
- **JS tests**
169169

170170
```bash
171-
npm test # web-test-runner, default group
172-
npm run test:watch # watch mode
171+
npm test # web-test-runner, default group
172+
npm run test:watch # watch mode
173+
npm run test:component <name> # test specific component in watch mode (e.g., npm run test:component data-access)
173174
```
174175

176+
**REQUIRED:** **MUST use `npm run test:component <name>` when testing a specific component** (e.g., `npm run test:component data-access`). This runs tests in watch mode for faster iteration.
177+
175178
**MUST run tests when making ANY changes** to component behavior, properties, events, or data services.
176179

177180
- **Static checks**

docs/pages/components/data-access.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ This component does not emit custom events.
4040
### Basic Usage
4141

4242
```html:preview
43+
<terra-data-access short-name="FLDAS_NOAHMP001_G_CA_D" version="001"></terra-data-access>
4344
<terra-data-access short-name="MODISA_L2_OC" version="2022.0"></terra-data-access>
45+
<terra-data-access short-name="NLDAS_FORA0125_H" version="2.0"></terra-data-access>
4446
```
4547

4648
### With Filters (Temporal, Spatial, Cloud Cover)
@@ -58,6 +60,14 @@ Use the top filter bar to:
5860

5961
The results grid updates as filters change. The selection summary displays file count and estimated total size.
6062

63+
### Sub-daily Data
64+
65+
```html:preview
66+
<terra-data-access short-name="GPM_3IMERGHHL" version="07"></terra-data-access>
67+
```
68+
69+
Datepicker supports sub-daily granules. Select a date and then choose specific times from the dropdown.
70+
6171
### Export Download Options
6272

6373
```html:preview

src/components/data-access/data-access.component.ts

Lines changed: 119 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,6 @@ export default class TerraDataAccess extends TerraElement {
120120
@state()
121121
cloudCoverPickerOpen = false
122122

123-
@state()
124-
datePickerOpen = false
125-
126-
@state()
127-
spatialPickerOpen = false
128-
129123
datePickerRef = createRef<TerraDatePicker>()
130124
spatialPickerRef = createRef<TerraSpatialPicker>()
131125
cloudCoverSliderRef = createRef<TerraSlider>()
@@ -304,6 +298,14 @@ export default class TerraDataAccess extends TerraElement {
304298
this.#gridApi?.purgeInfiniteCache()
305299
}
306300

301+
#handleSpatialDropdownShow() {
302+
// Trigger invalidateSize on the map when dropdown opens
303+
// This ensures the Leaflet map recalculates its size correctly
304+
setTimeout(() => {
305+
this.spatialPickerRef.value?.invalidateSize()
306+
}, 0)
307+
}
308+
307309
#handleDateRangeChange(event: CustomEvent) {
308310
const detail = event.detail
309311
this.startDate = detail.startDate || ''
@@ -331,6 +333,24 @@ export default class TerraDataAccess extends TerraElement {
331333
return 'Date Range'
332334
}
333335

336+
#formatAvailableRangeDate(dateStr: string): string {
337+
if (!dateStr) return ''
338+
339+
const date = new Date(dateStr)
340+
const year = date.getUTCFullYear()
341+
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
342+
const day = String(date.getUTCDate()).padStart(2, '0')
343+
344+
if (this.#controller.isSubDaily) {
345+
const hours = String(date.getUTCHours()).padStart(2, '0')
346+
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
347+
const seconds = String(date.getUTCSeconds()).padStart(2, '0')
348+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
349+
}
350+
351+
return `${year}-${month}-${day}`
352+
}
353+
334354
#getSpatialButtonText(): string {
335355
if (!this.location) {
336356
return 'Spatial Area'
@@ -380,29 +400,16 @@ export default class TerraDataAccess extends TerraElement {
380400
return 'Spatial Area'
381401
}
382402

383-
#toggleDatePicker() {
384-
this.datePickerOpen = !this.datePickerOpen
385-
this.datePickerRef.value?.setOpen(this.datePickerOpen)
386-
}
403+
// Date picker is now handled by dropdown component
387404

388405
#clearDateRange() {
389406
this.startDate = ''
390407
this.endDate = ''
391-
this.datePickerOpen = false
392408
this.#gridApi?.purgeInfiniteCache()
393409
}
394410

395-
#toggleSpatialPicker() {
396-
// Use setTimeout to ensure the click event has been processed
397-
setTimeout(() => {
398-
this.spatialPickerOpen = !this.spatialPickerOpen
399-
this.spatialPickerRef.value?.setOpen(this.spatialPickerOpen)
400-
}, 0)
401-
}
402-
403411
#clearSpatialFilter() {
404412
this.location = null
405-
this.spatialPickerOpen = false
406413
this.#gridApi?.purgeInfiniteCache()
407414
}
408415

@@ -624,12 +631,12 @@ export default class TerraDataAccess extends TerraElement {
624631
</div>
625632
626633
<div class="toggle-row">
627-
<div class="filter">
634+
<terra-dropdown>
628635
<button
636+
slot="trigger"
629637
class="filter-btn ${this.startDate && this.endDate
630638
? 'active'
631639
: ''}"
632-
@click=${this.#toggleDatePicker}
633640
>
634641
<terra-icon
635642
name="outline-calendar"
@@ -653,60 +660,97 @@ export default class TerraDataAccess extends TerraElement {
653660
: nothing}
654661
</button>
655662
656-
<!-- hidden date picker to show when clicking the filter -->
657-
<terra-date-picker
658-
${ref(this.datePickerRef)}
659-
range
660-
hide-label
661-
enable-time
662-
hide-input
663-
show-presets
664-
.startDate=${this.startDate}
665-
.endDate=${this.endDate}
666-
.minDate=${this.#controller.granuleMinDate}
667-
.maxDate=${this.#controller.granuleMaxDate}
668-
@terra-date-range-change=${this.#handleDateRangeChange}
669-
></terra-date-picker>
670-
</div>
671-
672-
<div class="filter">
673-
<button
674-
class="filter-btn ${this.location ? 'active' : ''}"
675-
@click=${(e: Event) => {
676-
e.stopPropagation()
677-
this.#toggleSpatialPicker()
678-
}}
679-
>
680-
<terra-icon
681-
name="outline-globe-alt"
682-
library="heroicons"
683-
font-size="18px"
684-
></terra-icon>
685-
<span>${this.#getSpatialButtonText()}</span>
686-
${this.location
687-
? html`
688-
<button
689-
class="clear-badge"
690-
@click=${(e: Event) => {
691-
e.stopPropagation()
692-
this.#clearSpatialFilter()
693-
}}
694-
aria-label="Clear spatial filter"
663+
<div class="datepicker-container">
664+
<terra-date-picker
665+
${ref(this.datePickerRef)}
666+
range
667+
?enable-time=${this.#controller.isSubDaily}
668+
show-presets
669+
split-inputs
670+
inline
671+
.startDate=${this.startDate}
672+
.endDate=${this.endDate}
673+
.startPlaceholder=${this.#controller.isSubDaily
674+
? 'YYYY-MM-DD HH:mm:ss'
675+
: 'YYYY-MM-DD'}
676+
.endPlaceholder=${this.#controller.isSubDaily
677+
? 'YYYY-MM-DD HH:mm:ss'
678+
: 'YYYY-MM-DD'}
679+
.minDate=${this.#controller.granuleMinDate}
680+
.maxDate=${this.#controller.granuleMaxDate}
681+
@terra-date-range-change=${this
682+
.#handleDateRangeChange}
683+
>
684+
${this.#controller.granuleMinDate &&
685+
this.#controller.granuleMaxDate
686+
? html` <p
687+
slot="additional-text"
688+
class="available-range"
695689
>
696-
×
697-
</button>
698-
`
699-
: nothing}
700-
</button>
701-
702-
<!-- hidden spatial picker to show when clicking the filter -->
703-
<terra-spatial-picker
704-
${ref(this.spatialPickerRef)}
705-
has-shape-selector
706-
hide-label
707-
@terra-map-change=${this.#handleMapChange}
708-
></terra-spatial-picker>
709-
</div>
690+
<strong>Available Range:</strong>
691+
${this.#formatAvailableRangeDate(
692+
this.#controller.granuleMinDate
693+
)}
694+
-
695+
${this.#formatAvailableRangeDate(
696+
this.#controller.granuleMaxDate
697+
)}
698+
</p>`
699+
: nothing}
700+
</terra-date-picker>
701+
</div>
702+
</terra-dropdown>
703+
704+
<terra-dropdown
705+
placement="bottom-start"
706+
distance="4"
707+
hoist
708+
@terra-show=${this.#handleSpatialDropdownShow}
709+
>
710+
<div slot="trigger" class="filter">
711+
<button
712+
class="filter-btn ${this.location ? 'active' : ''}"
713+
>
714+
<terra-icon
715+
name="outline-globe-alt"
716+
library="heroicons"
717+
font-size="18px"
718+
></terra-icon>
719+
<span>${this.#getSpatialButtonText()}</span>
720+
${this.location
721+
? html`
722+
<button
723+
class="clear-badge"
724+
@click=${(e: Event) => {
725+
e.stopPropagation()
726+
this.#clearSpatialFilter()
727+
}}
728+
aria-label="Clear spatial filter"
729+
>
730+
×
731+
</button>
732+
`
733+
: nothing}
734+
</button>
735+
</div>
736+
737+
<div class="spatialpicker-container">
738+
<terra-spatial-picker
739+
${ref(this.spatialPickerRef)}
740+
hide-label
741+
inline
742+
no-world-wrap
743+
.spatialConstraints=${this.#controller
744+
.spatialConstraints || '-180, -90, 180, 90'}
745+
@terra-map-change=${this.#handleMapChange}
746+
>
747+
<p class="available-range" slot="additional-text">
748+
<strong>Available range:</strong> ${this
749+
.#controller.spatialConstraints}
750+
</p>
751+
</terra-spatial-picker>
752+
</div>
753+
</terra-dropdown>
710754
711755
${this.#controller.cloudCoverRange
712756
? html`

src/components/data-access/data-access.controller.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,81 @@ export class DataAccessController {
211211
return this.#cloudCoverRange
212212
}
213213

214+
get isSubDaily() {
215+
if (!this.#sampling?.firstGranules?.items?.[0]) {
216+
return false
217+
}
218+
219+
const firstGranule = this.#sampling.firstGranules.items[0]
220+
221+
const timeStart =
222+
firstGranule.temporalExtent?.rangeDateTime?.beginningDateTime
223+
const timeEnd = firstGranule.temporalExtent?.rangeDateTime?.endingDateTime
224+
225+
if (!timeStart || !timeEnd) {
226+
return false
227+
}
228+
229+
// Parse the dates and calculate the difference in hours
230+
const start = new Date(timeStart)
231+
const end = new Date(timeEnd)
232+
const diffMs = end.getTime() - start.getTime()
233+
const diffHours = diffMs / (1000 * 60 * 60)
234+
235+
// If the temporal extent is less than 24 hours, it's sub-daily
236+
return diffHours < 24
237+
}
238+
239+
get spatialConstraints() {
240+
const boundingRects =
241+
this.#sampling?.spatialExtent?.horizontalSpatialDomain?.geometry
242+
?.boundingRectangles
243+
244+
if (!boundingRects || boundingRects.length === 0) {
245+
return '-180, -90, 180, 90'
246+
}
247+
248+
const boundingRect = boundingRects[0]
249+
const {
250+
westBoundingCoordinate,
251+
southBoundingCoordinate,
252+
eastBoundingCoordinate,
253+
northBoundingCoordinate,
254+
} = boundingRect
255+
256+
return `${westBoundingCoordinate}, ${southBoundingCoordinate}, ${eastBoundingCoordinate}, ${northBoundingCoordinate}`
257+
}
258+
259+
get spatialExtentDisplay() {
260+
const boundingRects =
261+
this.#sampling?.spatialExtent?.horizontalSpatialDomain?.geometry
262+
?.boundingRectangles
263+
264+
if (!boundingRects || boundingRects.length === 0) {
265+
return 'Global'
266+
}
267+
268+
const boundingRect = boundingRects[0]
269+
const {
270+
westBoundingCoordinate,
271+
southBoundingCoordinate,
272+
eastBoundingCoordinate,
273+
northBoundingCoordinate,
274+
} = boundingRect
275+
276+
// Check if it's global coverage
277+
if (
278+
westBoundingCoordinate === -180 &&
279+
southBoundingCoordinate === -90 &&
280+
eastBoundingCoordinate === 180 &&
281+
northBoundingCoordinate === 90
282+
) {
283+
return 'Global'
284+
}
285+
286+
return `${westBoundingCoordinate}, ${southBoundingCoordinate}, ${eastBoundingCoordinate}, ${northBoundingCoordinate}`
287+
}
288+
214289
async fetchGranules({
215290
collectionEntryId,
216291
startRow,

0 commit comments

Comments
 (0)