Skip to content

Commit cc66d72

Browse files
KrupaRamiKrupa Ramijoncarlson
authored
GUUI 3495 review and implement data aware validation rules (#19)
* show an error if user selects invalid dates * Spatial picker restricts the map if selected spatial area is out of extent * data-aware componnents: no worning from variable picker when data not overlapping location * chore: cleanup to get working with feature/prepare-for-contributions --------- Co-authored-by: Krupa Rami <[email protected]> Co-authored-by: Jon Carlson <[email protected]>
1 parent 1254fd5 commit cc66d72

7 files changed

Lines changed: 301 additions & 35 deletions

File tree

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

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { getVariableEntryId } from '../../metadata-catalog/utilities.js'
2525
*
2626
* @event terra-date-range-change - Emitted whenever the date range of the date slider is updated
2727
*/
28+
29+
type LastChanged = 'variable' | 'location' | undefined
30+
2831
export default class TerraDataRods extends TerraElement {
2932
static styles: CSSResultGroup = [componentStyles, styles]
3033
static dependencies = {
@@ -90,8 +93,82 @@ export default class TerraDataRods extends TerraElement {
9093

9194
@state() catalogVariable: Variable
9295

96+
/**
97+
* anytime user selects invalid dates
98+
*/
99+
@state() private dateErrorMessage?: string
100+
101+
@state() private lastChanged?: LastChanged
102+
103+
/**
104+
* add a warning state
105+
*/
106+
@state() private spatialWarningMessage?: string
107+
93108
_fetchVariableTask = getFetchVariableTask(this)
94109

110+
get variableBoundingBox() {
111+
const variable = this.catalogVariable
112+
if (!variable) return undefined
113+
114+
const {
115+
dataProductWest,
116+
dataProductSouth,
117+
dataProductEast,
118+
dataProductNorth,
119+
} = variable
120+
121+
if (
122+
dataProductWest == null ||
123+
dataProductSouth == null ||
124+
dataProductEast == null ||
125+
dataProductNorth == null
126+
) {
127+
return undefined
128+
}
129+
130+
return `${dataProductWest}, ${dataProductSouth}, ${dataProductEast}, ${dataProductNorth}`
131+
}
132+
133+
private compatibilityWarning() {
134+
this.spatialWarningMessage = undefined
135+
136+
if (!this.catalogVariable || !this.location) return
137+
138+
const {
139+
dataProductWest,
140+
dataProductSouth,
141+
dataProductEast,
142+
dataProductNorth,
143+
} = this.catalogVariable
144+
145+
if (
146+
dataProductWest == null ||
147+
dataProductSouth == null ||
148+
dataProductEast == null ||
149+
dataProductNorth == null
150+
) {
151+
return
152+
}
153+
154+
const [latStr, lonStr] = this.location.split(',')
155+
const lat = Number(latStr)
156+
const lon = Number(lonStr)
157+
158+
const inside =
159+
lon >= dataProductWest &&
160+
lon <= dataProductEast &&
161+
lat >= dataProductSouth &&
162+
lat <= dataProductNorth
163+
164+
if (!inside) {
165+
this.spatialWarningMessage =
166+
this.lastChanged === 'variable'
167+
? 'This variable has no data at the selected location.'
168+
: 'The selected location is outside the coverage of this variable.'
169+
}
170+
}
171+
95172
render() {
96173
const minDate = this.catalogVariable
97174
? getUTCDate(this.catalogVariable.dataProductBeginDateTime)
@@ -101,6 +178,9 @@ export default class TerraDataRods extends TerraElement {
101178
: undefined
102179

103180
return html`
181+
${this.spatialWarningMessage && this.lastChanged === 'location'
182+
? html`<div class="warning">⚠️ ${this.spatialWarningMessage}</div>`
183+
: null}
104184
<terra-variable-combobox
105185
exportparts="base:variable-combobox__base, combobox:variable-combobox__combobox, button:variable-combobox__button, listbox:variable-combobox__listbox"
106186
.value=${getVariableEntryId(this)}
@@ -109,8 +189,12 @@ export default class TerraDataRods extends TerraElement {
109189
@terra-combobox-change="${this.#handleVariableChange}"
110190
></terra-variable-combobox>
111191
192+
${this.spatialWarningMessage && this.lastChanged === 'variable'
193+
? html`<div class="warning">⚠️ ${this.spatialWarningMessage}</div>`
194+
: null}
112195
<terra-spatial-picker
113196
initial-value=${this.location}
197+
.spatialConstraints=${this.variableBoundingBox}
114198
exportparts="map:spatial-picker__map, leaflet-bbox:spatial-picker__leaflet-bbox, leaflet-point:spatial-picker__leaflet-point"
115199
label="Select Point"
116200
@terra-map-change=${this.#handleMapChange}
@@ -120,12 +204,17 @@ export default class TerraDataRods extends TerraElement {
120204
variable-entry-id=${getVariableEntryId(this)}
121205
start-date=${this.startDate}
122206
end-date=${this.endDate}
123-
location=${this.location}
207+
.location=${this.location ?? undefined}
124208
bearer-token=${this.bearerToken}
125209
show-citation=${true}
126210
@terra-date-range-change=${this.#handleTimeSeriesDateRangeChange}
127211
>
128-
<li slot="help-links"><a href="https://disc.gsfc.nasa.gov/information/tools?title=Hydrology%20Time%20Series">User Guide</a></li>
212+
<li slot="help-links">
213+
<a
214+
href="https://disc.gsfc.nasa.gov/information/tools?title=Hydrology%20Time%20Series"
215+
>User Guide</a
216+
>
217+
</li>
129218
</terra-time-series>
130219
131220
<terra-date-range-slider
@@ -135,7 +224,13 @@ export default class TerraDataRods extends TerraElement {
135224
start-date=${this.startDate}
136225
end-date=${this.endDate}
137226
@terra-date-range-change="${this.#handleDateRangeSliderChangeEvent}"
227+
@terra-date-selection-invalid="${this.#handleInvalidDateSelection}"
138228
></terra-date-range-slider>
229+
${this.dateErrorMessage
230+
? html`<div class="date-error" style="color: red;">
231+
${this.dateErrorMessage}
232+
</div>`
233+
: null}
139234
`
140235
}
141236

@@ -147,14 +242,35 @@ export default class TerraDataRods extends TerraElement {
147242
this.endDate = event.detail.endDate
148243
}
149244

245+
/**
246+
* anytime user selects invalid dates outside variable date range
247+
*/
248+
#handleInvalidDateSelection(event: CustomEvent) {
249+
this.dateErrorMessage = event.detail.message
250+
}
251+
150252
#handleVariableChange(event: TerraComboboxChangeEvent) {
151-
this.variableEntryId = event.detail.entryId
253+
const newEntryId = event.detail.entryId
254+
255+
//Do nothing if this is just initial sync
256+
if (newEntryId === this.variableEntryId) {
257+
return
258+
}
259+
260+
this.variableEntryId = newEntryId
261+
this.location = undefined
262+
this.lastChanged = 'variable'
263+
264+
this.compatibilityWarning()
152265
}
153266

154267
#handleMapChange(event: TerraMapChangeEvent) {
155268
if (event.detail.type === MapEventType.POINT) {
156269
// TODO: we may want to pick a `toFixed()` length in the spatial picker and stick with it.
157270
this.location = `${event.detail.latLng.lat.toFixed(4)},${event.detail.latLng.lng.toFixed(4)}`
271+
this.lastChanged = 'location'
272+
273+
this.compatibilityWarning()
158274
}
159275
}
160276

src/components/data-rods/data-rods.styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@ export default css`
3030
terra-spatial-picker::part(leaflet-edit) {
3131
display: none;
3232
}
33+
34+
.warning {
35+
color: #dc2626;
36+
font-weight: 500;
37+
margin-bottom: 0.25rem;
38+
}
3339
`

src/components/date-picker/date-picker.component.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import TerraButton from '../button/button.component.js'
1717
*
1818
* @csspart base - The component's base wrapper.
1919
* @csspart input - The date input element.
20+
* @event terra-date-selection-invalid - Fired when user selects date outside allowed range
2021
*/
2122
export default class TerraDatePicker extends TerraElement {
2223
static styles: CSSResultGroup = [componentStyles, styles]
@@ -112,7 +113,31 @@ export default class TerraDatePicker extends TerraElement {
112113
}
113114

114115
private handleBlur() {
115-
this.handleChange(this.flatpickrElement._instance.selectedDates)
116+
const selectedDates = this.flatpickrElement._instance.selectedDates
117+
const min = this.minDate ? new Date(this.minDate).getTime() : null
118+
const max = this.maxDate ? new Date(this.maxDate).getTime() : null
119+
120+
const isValid = selectedDates.every((date: Date) => {
121+
const time = date.getTime()
122+
return (!min || time >= min) && (!max || time <= max)
123+
})
124+
125+
if (!isValid) {
126+
this.flatpickrElement._instance.clear()
127+
128+
this.emit(
129+
'terra-date-selection-invalid',
130+
{
131+
detail: {
132+
message: `You are not allowed to select dates outside ${this.minDate} to ${this.maxDate}.`,
133+
},
134+
}
135+
)
136+
137+
return
138+
}
139+
140+
this.handleChange(selectedDates)
116141
}
117142

118143
render() {
@@ -131,10 +156,10 @@ export default class TerraDatePicker extends TerraElement {
131156
.minDate=${this.minDate}
132157
.maxDate=${this.maxDate}
133158
.defaultDate=${this.range
134-
? ([this.startDate, this.endDate].filter(
135-
Boolean
136-
) as string[])
137-
: this.startDate}
159+
? ([this.startDate, this.endDate].filter(
160+
Boolean
161+
) as string[])
162+
: this.startDate}
138163
.allowInput=${this.allowInput}
139164
.altFormat=${this.altFormat}
140165
.altInput=${this.altInput}

src/components/date-picker/date-picker.styles.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ export default css`
8989
white-space: nowrap;
9090
border: 0;
9191
}
92+
.flatpickr-day.disabled {
93+
background: #f0f0f0;
94+
color: #ccc;
95+
cursor: not-allowed;
96+
}
9297
`

0 commit comments

Comments
 (0)