diff --git a/assets/src/scripts/monitoring-location/components/hydrograph/index.js b/assets/src/scripts/monitoring-location/components/hydrograph/index.js index aef88f564b623a50462d9969d1d6ab6e95c58245..5292859033158f044f737ae5e514ff079e53d62f 100644 --- a/assets/src/scripts/monitoring-location/components/hydrograph/index.js +++ b/assets/src/scripts/monitoring-location/components/hydrograph/index.js @@ -2,6 +2,7 @@ * Hydrograph charting module. */ import {select} from 'd3-selection'; +import {DateTime} from 'luxon'; import {createStructuredSelector} from 'reselect'; import config from 'ui/config.js'; @@ -12,32 +13,33 @@ import {sortedParameters} from 'ui/utils'; import {drawWarningAlert, drawInfoAlert} from 'd3render/alerts'; import {drawLoadingIndicator} from 'd3render/loading-indicator'; -import {isPeriodWithinAcceptableRange, isPeriodCustom} from 'ml/iv-data-utils'; -import {renderTimeSeriesUrlParams} from 'ml/url-params'; - -import {hasAnyVariables, getCurrentVariableID, getCurrentParmCd, getVariables} from 'ml/selectors/time-series-selector'; - -import {Actions as ivTimeSeriesDataActions} from 'ml/store/instantaneous-value-time-series-data'; -import {Actions as ivTimeSeriesStateActions} from 'ml/store/instantaneous-value-time-series-state'; -import {Actions as statisticsDataActions} from 'ml/store/statistics-data'; -import {Actions as timeZoneActions} from 'ml/store/time-zone'; -import {Actions as floodDataActions} from 'ml/store/flood-inundation'; - -import {drawDateRangeControls} from './date-controls'; -import {drawDataTables} from './data-table'; -import {renderDownloadLinks} from './download-links'; -import {drawGraphBrush} from './graph-brush'; -import {drawGraphControls} from './graph-controls'; -import {drawTimeSeriesLegend} from './legend'; -import {drawMethodPicker} from './method-picker'; -import {plotSeriesSelectTable} from './parameters'; -import {drawTimeSeriesGraph} from './time-series-graph'; -import {drawTooltipCursorSlider} from './tooltip'; - -import {getLineSegmentsByParmCd} from './selectors/drawing-data'; -import {SPARK_LINE_DIM} from './selectors/layout'; -import {getAvailableParameterCodes} from './selectors/parameter-data'; -import {getTimeSeriesScalesByParmCd} from './selectors/scales'; +//import {isPeriodWithinAcceptableRange, isPeriodCustom} from 'ml/iv-data-utils'; +//import {renderTimeSeriesUrlParams} from 'ml/url-params'; + +//import {hasAnyVariables, getCurrentVariableID, getCurrentParmCd, getVariables} from 'ml/selectors/time-series-selector'; + +import {retrieveHydrographData} from 'ml/store/hydrograph-data'; +//import {Actions as ivTimeSeriesDataActions} from 'ml/store/instantaneous-value-time-series-data'; +//import {Actions as ivTimeSeriesStateActions} from 'ml/store/instantaneous-value-time-series-state'; +//import {Actions as statisticsDataActions} from 'ml/store/statistics-data'; +//import {Actions as timeZoneActions} from 'ml/store/time-zone'; +//import {Actions as floodDataActions} from 'ml/store/flood-inundation'; + +//import {drawDateRangeControls} from './date-controls'; +//import {drawDataTables} from './data-table'; +//import {renderDownloadLinks} from './download-links'; +//import {drawGraphBrush} from './graph-brush'; +//import {drawGraphControls} from './graph-controls'; +//import {drawTimeSeriesLegend} from './legend'; +//import {drawMethodPicker} from './method-picker'; +//import {plotSeriesSelectTable} from './parameters'; +//import {drawTimeSeriesGraph} from './time-series-graph'; +//import {drawTooltipCursorSlider} from './tooltip'; + +//import {getLineSegmentsByParmCd} from './selectors/drawing-data'; +//import {SPARK_LINE_DIM} from './selectors/layout'; +//import {getAvailableParameterCodes} from './selectors/parameter-data'; +//import {getTimeSeriesScalesByParmCd} from './selectors/scales'; /* * Renders the hydrograph on the node element using the Redux store for state information. The siteno, latitude, and @@ -62,8 +64,7 @@ export const attachToNode = function(store, timeSeriesId, // This must be converted to an integer showOnlyGraph = false, showMLName = false - } = {}, - loadPromise) { + } = {}) { const nodeElem = select(node); if (!siteno) { select(node).call(drawWarningAlert, {title: 'Hydrograph Alert', body: 'No data is available.'}); @@ -75,11 +76,19 @@ export const attachToNode = function(store, .select('.loading-indicator-container') .call(drawLoadingIndicator, {showLoadingIndicator: true, sizeClass: 'fa-3x'}); - // Fetch time zone - const fetchTimeZonePromise = store.dispatch(timeZoneActions.retrieveIanaTimeZone(latitude, longitude)); // Fetch waterwatch flood levels - store.dispatch(floodDataActions.retrieveWaterwatchData(siteno)); - let fetchDataPromise; + //store.dispatch(floodDataActions.retrieveWaterwatchData(siteno)); + // Need to set default parameter code in server and insert in markup */ + const fetchDataPromise = store.dispatch(retrieveHydrographData(siteno, { + parameterCode: parameterCode || '00060', + period: startDT && endDT ? null : period || 'P7D', + startTime: DateTime.fromISO(startDT, {zone: config.locationTimeZone}), + endTime: DateTime.fromISO(endDT, {zone: config.locationTimeZone}), + loadCompare: false, + loadMedian: false + })); + fetchDataPromise.then(() => console.log('Finished fetching Hydrograph data')); + /* if (showOnlyGraph) { // Only fetch what is needed if (parameterCode && period) { @@ -188,4 +197,5 @@ export const attachToNode = function(store, } } }); + */ }; diff --git a/assets/src/scripts/monitoring-location/index.js b/assets/src/scripts/monitoring-location/index.js index 9fe2776499eed5c8ba1853541a7c9507adf05eb8..977d174a14969d8f8b14a52335dead0028a63604 100644 --- a/assets/src/scripts/monitoring-location/index.js +++ b/assets/src/scripts/monitoring-location/index.js @@ -2,12 +2,9 @@ import 'ui/polyfills'; import wdfnviz from 'wdfn-viz'; -import config from 'ui/config'; - import {getParamString} from 'ml/url-params'; import {configureStore} from 'ml/store'; -import {retrieveGroundwaterLevels} from 'ml/store/discrete-data'; import {Actions as uiActions} from 'ml/store/ui-state'; import {attachToNode as CameraComponent} from 'ml/components/cameras'; @@ -26,31 +23,6 @@ const COMPONENTS = { 'network-list': NetworkListComponent }; -/* - * Returns a promise that is resolved once groundwater levels have been fetched. It is - * immediately resolved if no groundwater is fetchd. - */ -const loadAllGroundWaterData = function(nodes, store) { - const nodesWithGroundWaterLevels = - Array.from(nodes).filter(node => node.dataset.component === 'hydrograph' || node.dataset.component === 'dv-hydrograph'); - if (!nodesWithGroundWaterLevels.length) { - return Promise.resolve(); - } - - const siteno = nodesWithGroundWaterLevels[0].dataset.siteno; - if (config.gwPeriodOfRecord) { - - const loadPromises = Object.keys(config.gwPeriodOfRecord).map(function(parameterCode) { - const periodOfRecord = config.gwPeriodOfRecord[parameterCode]; - return store.dispatch( - retrieveGroundwaterLevels(siteno, parameterCode, periodOfRecord.begin_date, periodOfRecord.end_date)); - }); - return Promise.all(loadPromises); - } else { - return Promise.resolve(); - } -}; - const load = function() { let pageContainer = document.getElementById('monitoring-location-page-container'); let store = configureStore({ @@ -60,14 +32,13 @@ const load = function() { } }); let nodes = document.getElementsByClassName('wdfn-component'); - const loadPromise = loadAllGroundWaterData(nodes, store); for (let node of nodes) { // If options is specified on the node, expect it to be a JSON string. // Otherwise, use the dataset attributes as the component options. const options = node.dataset.options ? JSON.parse(node.dataset.options) : node.dataset; const hashOptions = Object.fromEntries(new window.URLSearchParams(getParamString())); - COMPONENTS[node.dataset.component](store, node, Object.assign({}, options, hashOptions), loadPromise); + COMPONENTS[node.dataset.component](store, node, Object.assign({}, options, hashOptions)); } window.onresize = function() { diff --git a/assets/src/scripts/monitoring-location/iv-data-utils.js b/assets/src/scripts/monitoring-location/iv-data-utils.js index 04a86339b663c3421e9d6e72e32e18834104a265..126275f9e1a97a7257e9535db8e86849bc1b9fdd 100644 --- a/assets/src/scripts/monitoring-location/iv-data-utils.js +++ b/assets/src/scripts/monitoring-location/iv-data-utils.js @@ -102,7 +102,19 @@ const checkForMeasuredFahrenheitParameters = function(parameterCode, NWISVariabl return isParameterMatching; }; +export const isCalculatedTemperature = function(parameterCode) { + return parameterCode.slice(-1) === config.CALCULATED_TEMPERATURE_VARIABLE_CODE; +}; +export const getConvertedTemperatureParameter = function(parameter) { + return { + ...parameter, + parameterCode: `${parameter.parameterCode}${config.CALCULATED_TEMPERATURE_VARIABLE_CODE}`, + name: parameter.name.replace('C', 'F (calculated)'), + description: parameter.description.replace('Celsius', 'Fahrenheit (calculated)'), + unit: parameter.unit.replace('C', 'F') + }; +}; /* * Converts a Celsius 'variable' from the NWIS system to one we can use to show Fahrenheit. * @param {Object} NWISVariable - Has various properties related to descriptions of data diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.js new file mode 100644 index 0000000000000000000000000000000000000000..f26ab939c6d9dca7f1da4659ffefd6b96cff39af --- /dev/null +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.js @@ -0,0 +1,270 @@ + +import {DateTime, Duration} from 'luxon'; + +import config from 'ui/config'; +import {convertCelsiusToFahrenheit} from 'ui/utils'; + +import {fetchGroundwaterLevels} from 'ui/web-services/groundwater-levels'; +import {fetchTimeSeries} from 'ui/web-services/instantaneous-values'; +import {fetchSiteStatistics} from 'ui/web-services/statistics-data'; + +import {isCalculatedTemperature, getConvertedTemperatureParameter} from 'ml/iv-data-utils'; + +const getParameterToFetch = function(parameterCode) { + return isCalculatedTemperature(parameterCode) ? parameterCode.slice(0, -1) : parameterCode; +}; + +export const setHydrographTimeRange = function(timeRange, kind) { + return { + type: 'SET_HYDROGRAPH_TIME_RANGE', + timeRange, + kind + }; +}; + +export const clearHydrographData = function() { + return { + type: 'CLEAR_HYDROGRAPH_DATA' + }; +}; + +export const addIVHydrographData = function(kind, ivData) { + return { + type: 'ADD_IV_HYDROGRAPH_DATA', + kind, + ivData + }; +}; + +export const addMedianStatisticsData = function(statsData) { + return { + type: 'ADD_MEDIAN_STATISTICS_DATA', + statsData + }; +}; + +export const addGroundwaterLevels = function(gwLevels) { + return { + type: 'ADD_GROUNDWATER_LEVELS', + gwLevels + }; +}; + +/* + * startTime and endTime should be ISO 8601 time strings for all of these retrieve functions + */ +export const retrieveIVData = function(siteno, kind, {parameterCode, period, startTime, endTime}) { + return function(dispatch) { + const isCalculatedTemperatureCode = isCalculatedTemperature(parameterCode); + + return fetchTimeSeries({ + sites: [siteno], + parameterCode: getParameterToFetch(parameterCode), + period: period, + startTime: startTime, + endTime: endTime + }).then(data => { + const tsData = data.value.timeSeries[0]; + let parameter = { + parameterCode: tsData.variable.variableCode[0].value, + name: tsData.variable.variableName, + description: tsData.variable.variableDescription, + unit: tsData.variable.unit.unitCode + }; + if (isCalculatedTemperatureCode) { + parameter = getConvertedTemperatureParameter(parameter); + } + const timeSeriesData = { + parameter : parameter, + values: tsData.values.reduce((valuesByMethodId, value) => { + valuesByMethodId[value.method[0].methodID] = { + points: value.value.map(point => { + let pointValue = point.value; + if (pointValue && isCalculatedTemperatureCode) { + pointValue = convertCelsiusToFahrenheit(pointValue).toFixed(2); + } + return { + value: pointValue, + qualifiers: point.qualifiers, + dateTime: DateTime.fromISO(point.dateTime).toMillis() + }; + }), + method: value.method[0] + }; + return valuesByMethodId; + }, {}) + }; + dispatch(addIVHydrographData(kind, timeSeriesData)); + }); + }; +}; + +export const retrievePriorYearIVData = function(siteno, {parameterCode, startTime, endTime}) { + return function(dispatch, getState) { + const priorYearStartTime = DateTime.fromISO(startTime).minus({days: 365}).toMillis(); + const priorYearEndTime = DateTime.fromISO(endTime).minus({days: 365}).toMillis(); + const currentPriorYearTimeRange = getState().hydrographData.compareTimeRange || null; + if (currentPriorYearTimeRange && priorYearStartTime === currentPriorYearTimeRange.start && + priorYearEndTime === currentPriorYearTimeRange.end) { + return Promise.resolve(); + } else { + dispatch(setHydrographTimeRange({startTime: priorYearStartTime, endTime: priorYearEndTime}, 'compare')); + return dispatch(retrieveIVData(siteno, 'compare', { + parameterCode: parameterCode, + startTime: DateTime.fromMillis(priorYearStartTime).toISO(), + endTime: DateTime.fromMillis(priorYearEndTime).toISO() + })); + } + }; +}; + +export const retrieveMedianStatistics = function(siteno, parameterCode) { + return function(dispatch, getState) { + if ('medianStatistics' in getState().hydrographData) { + return Promise.resolve(); + } else { + const isCalculatedParameterCode = isCalculatedTemperature(parameterCode); + const parameterToFetch = getParameterToFetch(parameterCode); + return fetchSiteStatistics({siteno: siteno, statType: 'median', params: [parameterToFetch]}) + .then(stats => { + let resultStats = {}; + if (isCalculatedParameterCode) { + Object.keys(stats[parameterToFetch]).forEach(methodID => { + resultStats[methodID] = stats[parameterToFetch][methodID].map(stat => { + return { + ...stat, + parameter_cd: parameterCode, + p50_va: convertCelsiusToFahrenheit(stat.p50_va) + }; + }); + }); + } else { + resultStats = stats[parameterToFetch]; + } + dispatch(addMedianStatisticsData(resultStats)); + }); + } + }; +}; + +export const retrieveGroundwaterLevels = function(site, {parameterCode, period, startTime, endTime}) { + return function(dispatch) { + return fetchGroundwaterLevels({site, parameterCode, period, startTime, endTime}) + .then(data => { + if (data.value && data.value.timeSeries && data.value.timeSeries.length) { + let values; + const timeSeries = data.value.timeSeries[0]; + if (!timeSeries.values.length || !timeSeries.values[0].value.length) { + values = []; + } else { + values = timeSeries.values[0].value.map((v) => { + const dateTime = DateTime.fromISO(v.dateTime, {zone: 'utc'}).toMillis(); + return { + value: v.value, + qualifiers: v.qualifiers, + dateTime: dateTime + }; + + }); + } + const parameter = { + parameterCode: timeSeries.variable.variableCode.value, + name: timeSeries.variable.variableName, + description: timeSeries.variable.variableDescription, + unit: timeSeries.variable.unit.unitCode + }; + dispatch(addGroundwaterLevels({ + parameter, + values + })); + } + }); + }; +}; + +/* + * @param {String} siteno + * @param {String} parameterCode} + * @param {String} period + * @param {String} startTime - ISO 8601 time string + * @param {String} endTime + */ +export const retrieveHydrographData = function(siteno, {parameterCode, period, startTime, endTime, loadCompare, loadMedian}) { + return function(dispatch) { + const parameterToFetch = getParameterToFetch(parameterCode); + const hasIVData = config.uvPeriodOfRecord && parameterToFetch in config.uvPeriodOfRecord; + const hasGWData = config.gwPeriodOfRecord && parameterToFetch in config.gwPeriodOfRecord; + dispatch(clearHydrographData()); + + let timeRange; + if (period) { + const now = DateTime.local(); + timeRange = { + start: now.minus(Duration.fromISO(period)).toMillis(), + end: now.toMillis() + }; + } else { + timeRange = { + start: DateTime.fromISO(startTime).toMillis(), + end: DateTime.fromISO(startTime).toMillis() + }; + } + dispatch(setHydrographTimeRange(timeRange, 'primary')); + + let fetchPromises = []; + if (hasIVData) { + fetchPromises.push(dispatch(retrieveIVData( + siteno, 'primary', {parameterCode, period, startTime, endTime}))); + } + if (hasGWData) { + fetchPromises.push(dispatch( + retrieveGroundwaterLevels(siteno, {parameterCode, period, startTime, endTime}))); + } + if (hasIVData && loadCompare) { + fetchPromises.push(dispatch( + retrievePriorYearIVData(siteno, { + parameterCode: parameterCode, + startTime: DateTime.fromMillis(timeRange.start).toISO(), + endTime: DateTime.fromMillis(timeRange.end).toISO() + }))); + } + if (hasIVData && loadMedian) { + fetchPromises.push(dispatch( + retrieveMedianStatistics(siteno, parameterCode))); + } + + return Promise.all(fetchPromises); + }; +}; + +export const hydrographDataReducer = function(hydrographData = {}, action) { + switch(action.type) { + case 'SET_HYDROGRAPH_TIME_RANGE': { + let newData = {}; + newData[`${action.kind}TimeRange`] = action.timeRange; + return Object.assign({}, hydrographData, newData); + } + case 'CLEAR_HYDROGRAPH_DATA': { + return {}; + } + case 'ADD_IV_HYDROGRAPH_DATA': { + let newData = {}; + newData[`${action.kind}IVData`] = action.ivData; + return Object.assign({}, hydrographData, newData); + } + case 'ADD_MEDIAN_STATISTICS_DATA': { + return { + ...hydrographData, + statisticsData: action.statsData + }; + } + case 'ADD_GROUNDWATER_LEVELS': { + return { + ...hydrographData, + groundwaterLevels: action.gwLevels + }; + } + default: + return hydrographData; + } +}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js new file mode 100644 index 0000000000000000000000000000000000000000..2e56098d7db72a0628e93bc4fc973fe8b849ed0e --- /dev/null +++ b/assets/src/scripts/monitoring-location/store/hydrograph-parameters.js @@ -0,0 +1,82 @@ + +import merge from 'lodash/merge'; + +import config from 'ui/config'; + +import {fetchGroundwaterLevels} from 'ui/web-services/groundwater-levels'; +import {fetchIVTimeSeries} from 'ui/web-services/instantaneous-values'; +import {fetchSiteStatistics} from 'ui/web-services/statistics-data'; + +import {getConvertedTemperatureParameter} from 'ml/iv-data-utils'; + +/* + * Synchronous Redux action - updatethe hydrograph variables + * @param {Object} variables - keys are parameter codes. + * @return {Object} - Redux action + */ +export const updateHydrographParameters = function(parameters) { + return { + type: 'UPDATE_HYDROGRAPH_PARAMETERS', + parameters + }; +}; + +/* + * Asynchronous Redux action - fetches the latest value for all parameter codes and + * updates the store hydrograph parameter codes. + */ +export const retrieveHydrographParameters = function(siteno) { + return function(dispatch) { + const fetchIVParameters = fetchIVTimeSeries({sites: [siteno]}).then(series => { + return series.value.timeSeries.reduce((varsByPCode, ts) => { + const parameterCode = ts.variable.variableCode.value; + varsByPCode[parameterCode] = { + parameterCode: parameterCode, + name: ts.variable.variableName, + description: ts.variable.variableDescription, + unit: ts.variable.unit.unitCode, + hasIVData: true, + ivMethods: ts.values.map(value => { + return { + description: value.method.methodDescription, + methodID: value.method.methodID + }; + }) + }; + // If it is a celsius parameterCode, add a variable for calculated Fahrenheit. + if (parameterCode in config.TEMPERATURE_PARAMETERS.celsius) { + const fahrenheitParameter = getConvertedTemperatureParameter(varsByPCode[parameterCode]); + varsByPCode[fahrenheitParameter.parameterCode] = fahrenheitParameter; + } + }, {}); + }); + const fetchGWLevelParameters = fetchGroundwaterLevels({site: siteno}).then(series => { + return series.value.timeSeries.reduce((varsByPCode, ts) => { + const parameterCode = ts.variable.variableCode.value; + varsByPCode[parameterCode] = { + parameterCode: parameterCode, + name: ts.variable.variableName, + description: ts.variable.variableDescription, + unit: ts.variable.unit.unitCode, + hasGWLevelsData: true + }; + }, {}); + }); + return Promise.all([fetchIVParameters, fetchGWLevelParameters]).then((ivVars, gwVars) => { + const mergedVars = merge({}, gwVars, ivVars); + dispatch(updateHydrographParameters(mergedVars)); + }); + }; +}; + +export const hydrographVariablesReducer = function(hydrographParameters={}, action) { + switch(action.type) { + case 'UPDATE_HYDROGRAPH_PARAMETERS': { + return { + ...action.parameters + }; + } + default: + return hydrographParameters; + } +}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-variables.js b/assets/src/scripts/monitoring-location/store/hydrograph-variables.js deleted file mode 100644 index c721c21502a84500c59a4895f404d802dae77e12..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/store/hydrograph-variables.js +++ /dev/null @@ -1,42 +0,0 @@ - -import config from 'ui/config'; -import {fetchGroundwaterLevels} from 'ui/web-services/groundwater-levels'; -import {fetchIVTimeSeries} from 'ui/web-services/instantaneous-values'; -/* - * Asynchronous Redux action - fetches the latest value for all parameter codes and - * updates the store hydrograph variables. - */ - -export const retrieveVariables = function(siteno) { - return function(dispatch, getState) { - const fetchIVVariables = fetchIVTimeSeries({sites: [siteno]}).then(series => { - return series.value.timeSeries.reduce((varsByPCode, ts) => { - varsByPCode[ts.variable.variableCode.value] = { - name: ts.variable.variableName, - description: ts.variable.variableDescription, - unit: ts.variable.unit.unitCode, - hasIVData: true, - ivMethods: ts.values.map(value => { - return { - description: value.method.methodDescription, - methodID: value.method.methodID - }; - }) - }; - }, {}); - }); - const fetchGWLevelVariables = fetchGroundwaterLevels({site: siteno}).then(series => { - return series.value.timeSeries.reduce((varsByPCode, ts) => { - varsByPCode[ts.variable.variableCode.value] = { - name: ts.variable.variableName, - description: ts.variable.variableDescription, - unit: ts.variable.unit.unitCode, - hasGWLevelsData: true - }; - }, {}); - }); - return Promise.all([fetchIVVariables, fetchGWLevelVariables]).then((ivVars, gwVars) => { - }); - }; - -}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/index.js b/assets/src/scripts/monitoring-location/store/index.js index 5dcc9025eb900c4fc5c6e15ff127df554639b11d..2fe9464969bd92aa1446853e736113cd6eb9954f 100644 --- a/assets/src/scripts/monitoring-location/store/index.js +++ b/assets/src/scripts/monitoring-location/store/index.js @@ -1,28 +1,22 @@ import {applyMiddleware, createStore, combineReducers, compose} from 'redux'; import {default as thunk} from 'redux-thunk'; +import {dailyValueTimeSeriesDataReducer as dailyValueTimeSeriesData} from './daily-value-time-series'; +import {dailyValueTimeSeriesStateReducer as dailyValueTimeSeriesState} from './daily-value-time-series'; import { floodDataReducer as floodData, floodStateReducer as floodState} from './flood-inundation'; -import {nldiDataReducer as nldiData} from './nldi-data'; -import {dailyValueTimeSeriesDataReducer as dailyValueTimeSeriesData} from './daily-value-time-series'; -import {dailyValueTimeSeriesStateReducer as dailyValueTimeSeriesState} from './daily-value-time-series'; -import {discreteDataReducer as discreteData} from './discrete-data'; -import {ivTimeSeriesDataReducer as ivTimeSeriesData} from './instantaneous-value-time-series-data'; +import {hydrographDataReducer as hydrographData} from './hydrograph-data'; import {ivTimeSeriesStateReducer as ivTimeSeriesState} from './instantaneous-value-time-series-state'; import {networkDataReducer as networkData} from './network'; -import {statisticsDataReducer as statisticsData} from './statistics-data'; -import {timeZoneReducer as ianaTimeZone} from './time-zone'; +import {nldiDataReducer as nldiData} from './nldi-data'; import {uiReducer as ui} from './ui-state'; const appReducer = combineReducers({ - ivTimeSeriesData, - ianaTimeZone, + hydrographData, dailyValueTimeSeriesData, - statisticsData, floodData, nldiData, - discreteData, ivTimeSeriesState, dailyValueTimeSeriesState, floodState, @@ -35,10 +29,8 @@ const MIDDLEWARES = [thunk]; export const configureStore = function(initialState) { initialState = { - ivTimeSeriesData: {}, - ianaTimeZone: null, + hydrographData: {}, dailyValueTimeSeriesData: {}, - discreteData: {}, floodData: { stages: [], extent: {}, @@ -51,7 +43,6 @@ export const configureStore = function(initialState) { downstreamSites: [], upstreamBasin: [] }, - statisticsData: {}, ivTimeSeriesState: { showIVTimeSeries: { current: true, diff --git a/assets/src/scripts/monitoring-location/store/time-zone.js b/assets/src/scripts/monitoring-location/store/time-zone.js deleted file mode 100644 index 683bcfc1c62fdf3f1acb5a2f267693abb0aa7fcb..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/store/time-zone.js +++ /dev/null @@ -1,47 +0,0 @@ -import {queryWeatherService} from 'ui/web-services/models'; - -/* - * Synchronous Redux action to update the IANA time zone - * @param {String} timeZone - * @return {Object} Redux action - */ -const setIanaTimeZone = function(timeZone) { - return { - type: 'SET_IANA_TIME_ZONE', - timeZone - }; -}; - -/* - * Asynchronous Redux action to fetch the time zone using the site's location - * @param {String} latitude - * @param {String} longitude - * @return {Function} which returns a promise which resolves when the fetch is completed - */ -const retrieveIanaTimeZone = function(latitude, longitude) { - return function(dispatch) { - return queryWeatherService(latitude, longitude).then( - resp => { - const tzIANA = resp.properties.timeZone || null; // set to time zone to null if unavailable - dispatch(setIanaTimeZone(tzIANA)); - }, - () => { - dispatch(setIanaTimeZone(null)); - } - ); - }; -}; - -export const timeZoneReducer = function(ianaTimeZone='', action) { - switch (action.type) { - case 'SET_IANA_TIME_ZONE': - return action.timeZone; - default: - return ianaTimeZone; - } -}; - -export const Actions = { - setIanaTimeZone, - retrieveIanaTimeZone -}; \ No newline at end of file diff --git a/assets/src/scripts/monitoring-location/store/time-zone.test.js b/assets/src/scripts/monitoring-location/store/time-zone.test.js deleted file mode 100644 index 036d50fe59caae6f6dba3bcf467ce51cbe9f846f..0000000000000000000000000000000000000000 --- a/assets/src/scripts/monitoring-location/store/time-zone.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import mockConsole from 'jest-mock-console'; -import {applyMiddleware, combineReducers, createStore} from 'redux'; -import {default as thunk} from 'redux-thunk'; -import sinon from 'sinon'; - -import {Actions, timeZoneReducer} from './time-zone'; - -describe('monitoring-location/store/time-zone module', () => { - let store; - let fakeServer; - let restoreConsole; - - beforeEach(() => { - store = createStore( - combineReducers({ - ianaTimeZone: timeZoneReducer - }), - { - ianaTimeZone: '' - }, - applyMiddleware(thunk) - ); - fakeServer = sinon.createFakeServer(); - restoreConsole = mockConsole(); - }); - - afterEach(() => { - restoreConsole(); - fakeServer.restore(); - }); - - describe('timeZoneReduer', () => { - describe('Actions.setIanaTimeZone', () => { - it('sets the time zone', () => { - store.dispatch(Actions.setIanaTimeZone('America/Chicago')); - - expect(store.getState().ianaTimeZone).toBe('America/Chicago'); - }); - }); - - describe('Actions.retrieveIanaTimeZone', () => { - it('Sets the latitude and longitude in the query', () => { - store.dispatch(Actions.retrieveIanaTimeZone('45.3', '-100.2')); - expect(fakeServer.requests[0].url).toContain('45.3,-100.2'); - }); - - it('Successful fetch assigns time zone', () => { - const promise = store.dispatch(Actions.retrieveIanaTimeZone('45.3', '-100.2')); - fakeServer.requests[0].respond(200, {}, '{"properties" : {"timeZone" : "America/Chicago"}}'); - return promise.then(() => { - expect(store.getState().ianaTimeZone).toBe('America/Chicago'); - }); - }); - - it('Failed fetch assigns time zone to be null', () => { - const promise = store.dispatch(Actions.retrieveIanaTimeZone('45.3', '-100.2')); - fakeServer.requests[0].respond(500, {}, '{"properties" : {}}'); - return promise.then(() => { - expect(store.getState().ianaTimeZone).toBeNull(); - }); - }); - }); - }); -}); \ No newline at end of file diff --git a/assets/src/scripts/web-services/groundwater-levels.js b/assets/src/scripts/web-services/groundwater-levels.js index 458541e4eb5b8d32a9560b6ffbbbc7f67b7113db..1e043437c335e8cbb918466aff3c1a8487489d01 100644 --- a/assets/src/scripts/web-services/groundwater-levels.js +++ b/assets/src/scripts/web-services/groundwater-levels.js @@ -10,9 +10,14 @@ import config from 'ui/config'; * @param {String} endDT - ISO-8601 date format * @return {Promise} resolves to Object that is retrieved with ground water levels */ -export const fetchGroundwaterLevels = function({site, parameterCode=null, startDT=null, endDT=null}) { +export const fetchGroundwaterLevels = function({site, parameterCode=null, period= null, startDT=null, endDT=null}) { const parameterCodeQuery = parameterCode ? `¶meterCd=${parameterCode}` : ''; - const timeQuery = startDT && endDT ? `&startDT=${startDT}&endDT=${endDT}` : ''; + let timeQuery; + if (period) { + timeQuery = `&period=${period}`; + } else { + timeQuery = startDT && endDT ? `&startDT=${startDT}&endDT=${endDT}` : ''; + } const url = `${config.GROUNDWATER_LEVELS_ENDPOINT}?sites=${site}${parameterCodeQuery}${timeQuery}&format=json`; return get(url) diff --git a/assets/src/scripts/web-services/instantaneous-values.js b/assets/src/scripts/web-services/instantaneous-values.js index cec98cb7a0dc444f75eed73a91fc326da84814a7..ce0c16ff653692a3e6527889271976dcfd1c9565 100644 --- a/assets/src/scripts/web-services/instantaneous-values.js +++ b/assets/src/scripts/web-services/instantaneous-values.js @@ -1,19 +1,14 @@ -import {utcFormat} from 'd3-time-format'; import {DateTime} from 'luxon'; import {get} from 'ui/ajax'; import config from 'ui/config'; -export const isoFormatTime = utcFormat('%Y-%m-%dT%H:%MZ'); - - -function olderThan120Days(date) { - return date < new Date() - 120; -} - -function tsServiceRoot(date) { - return olderThan120Days(date) ? config.PAST_SERVICE_ROOT : config.SERVICE_ROOT; +/* + * Return the past service root if the start dt is more than 120 days from now + */ +function tsServiceRoot(dateTime) { + return DateTime.local().diff(dateTime).as('days') > 120 ? config.PAST_SERVICE_ROOT : config.SERVICE_ROOT; } function getNumberOfDays(period) { @@ -27,14 +22,14 @@ function getNumberOfDays(period) { /** * Get a given time series dataset from Water Services. - * @param {Array} sites Array of site IDs to retrieve. - * @param {Array} params Optional array of parameter codes - * @param {Date} startDate - * @param {Date} endData - * @param {String} period + * @param {Array} sites Array of site IDs to retrieve. + * @param {Array} parameterCodes Optional array of parameter codes + * @param {String} period - ISO 8601 Duration + * @param {String} startTime - ISO 8601 time + * @param {String} endTime - ISO 8601 time * @return {Promise} resolves to an array of time series model object, rejects to an error */ -export const fetchTimeSeries = function({sites, params=null, startDate=null, endDate=null, period=null}) { +export const fetchTimeSeries = function({sites, parameterCode, period=null, startTime=null, endTime=null}) { let timeParams; let serviceRoot; @@ -43,28 +38,17 @@ export const fetchTimeSeries = function({sites, params=null, startDate=null, end const dayCount = getNumberOfDays(timePeriod); timeParams = `period=${timePeriod}`; serviceRoot = dayCount && dayCount < 120 ? config.SERVICE_ROOT : config.PAST_SERVICE_ROOT; - } else if (startDate && endDate) { - let startString = startDate ? isoFormatTime(startDate) : ''; - let endString = endDate ? isoFormatTime(endDate) : ''; - timeParams = `startDT=${startString}&endDT=${endString}`; - serviceRoot = tsServiceRoot(startDate); + } else if (startTime && endTime) { + const startDateTime = DateTime.fromISO(startTime); + timeParams = `startDT=${startTime}&endDT=${endTime}`; + serviceRoot = tsServiceRoot(startDateTime); } else { timeParams = ''; serviceRoot = config.SERVICE_ROOT; } - // Normal parameter codes have five numerical digits. If the parameter code has an alphabetical letter - // as a suffix, such as 00010F, it means that parameter has been altered in our application. - // Parameter codes with such a suffix are for use in our application only, so we need to remove - // any suffix before using the parameter code in a web call to a NWIS system. - if (params) { - params = params.map(function(param) { - return param.replace(`${config.CALCULATED_TEMPERATURE_VARIABLE_CODE}`, ''); - }); - } - - let paramCds = params !== null ? `¶meterCd=${params.join(',')}` : ''; - let url = `${serviceRoot}/iv/?sites=${sites.join(',')}${paramCds}&${timeParams}&siteStatus=all&format=json`; + let parameterCodeQuery = parameterCode ? `¶meterCd=${parameterCode}` : ''; + let url = `${serviceRoot}/iv/?sites=${sites.join(',')}${parameterCodeQuery}&${timeParams}&siteStatus=all&format=json`; return get(url) .then(response => JSON.parse(response)) @@ -74,21 +58,3 @@ export const fetchTimeSeries = function({sites, params=null, startDate=null, end }); }; -export const fetchPreviousYearTimeSeries = function({site, startTime, endTime, parameterCode}) { - const hoursInOneYear = 8760; - parameterCode = parameterCode ? [parameterCode] : null; - const lastYearStartTime = DateTime.fromMillis(startTime).minus({hours: hoursInOneYear}); - const lastYearEndTime = DateTime.fromMillis(endTime).minus({hours: hoursInOneYear}); - - return fetchTimeSeries({sites: [site], startDate: lastYearStartTime, endDate: lastYearEndTime, params: parameterCode}); -}; - -export const queryWeatherService = function(latitude, longitude) { - const url = `${config.WEATHER_SERVICE_ROOT}/points/${latitude},${longitude}`; - return get(url) - .then(response => JSON.parse(response)) - .catch(reason => { - console.error(reason); - return {properties: {}}; - }); -}; diff --git a/wdfn-server/waterdata/services/timezone.py b/wdfn-server/waterdata/services/timezone.py new file mode 100644 index 0000000000000000000000000000000000000000..5454a8f5af9ea44d2fe1f1391199705911641804 --- /dev/null +++ b/wdfn-server/waterdata/services/timezone.py @@ -0,0 +1,12 @@ +""" +Helpers to retrieve timezone information for a location +""" +from .. import app +from ..utils import execute_get_request + +def get_iana_time_zone(latitude, longitude): + resp = execute_get_request(app.config['WEATHER_SERVICE_ROOT'], f'points/{latitude},{longitude}') + if resp.status_code != 200: + return None + + return resp.properties.get('timeZone', None) if 'properties' in resp else None \ No newline at end of file diff --git a/wdfn-server/waterdata/templates/macros/components.html b/wdfn-server/waterdata/templates/macros/components.html index 8f06b73198717cd7194ba97a8992499dc86b7ecb..bb31a1c83a6d7904343817f85a0231a34a37bd35 100644 --- a/wdfn-server/waterdata/templates/macros/components.html +++ b/wdfn-server/waterdata/templates/macros/components.html @@ -1,5 +1,5 @@ {% macro TimeSeriesComponent(site_no, latitude, longitude) -%} - <div class="wdfn-component" data-component="hydrograph" data-siteno="{{ site_no }}" data-latitude="{{ latitude }}" data-longitude="{{ longitude }}"> + <div class="wdfn-component" data-component="hydrograph" data-siteno="{{ site_no }}", data-latitude="{{ latitude }}" data-longitude="{{ longitude }}"> <div class="loading-indicator-container"></div> <div class="graph-container"></div> <div class="provisional-data-statement"> diff --git a/wdfn-server/waterdata/templates/monitoring_location.html b/wdfn-server/waterdata/templates/monitoring_location.html index 08afab80013d5f49dbbe544bdb32315e4b1cd0f9..71d25904d25e6be3f0c1762c8a5a3cca17d68caa 100644 --- a/wdfn-server/waterdata/templates/monitoring_location.html +++ b/wdfn-server/waterdata/templates/monitoring_location.html @@ -31,6 +31,7 @@ {% block page_script %} <script type="application/javascript"> + CONFIG.locationTimeZone = "{{ time_zone }}" {% if uv_period_of_record %} CONFIG.uvPeriodOfRecord = {{ uv_period_of_record | tojson }}; {% endif %} diff --git a/wdfn-server/waterdata/views.py b/wdfn-server/waterdata/views.py index c2fa490c155f70230572592a4533de7009bc90f7..6bc42847e4df9f933f32ef83e0a9fdf14dad6494 100644 --- a/wdfn-server/waterdata/views.py +++ b/wdfn-server/waterdata/views.py @@ -14,8 +14,9 @@ from .location_utils import build_linked_data, get_disambiguated_values, rollup_ get_period_of_record_by_parm_cd from .utils import defined_when, parse_rdb, set_cookie_for_banner_message, create_message from .services import sifta, ogc -from .services.nwis import NwisWebServices from .services.camera import get_monitoring_location_camera_details +from .services.nwis import NwisWebServices +from .services.timezone import get_iana_time_zone # Station Fields Mapping to Descriptions from .constants import STATION_FIELDS_D @@ -159,6 +160,9 @@ def monitoring_location(site_no): else: email_for_data_questions = app.config['EMAIL_TO_REPORT_PROBLEM'] + # Get the time zone for the location + time_zone = get_iana_time_zone(station_record.get('dec_lat_va', ''), station_record.get('dec_long_va', '')) + context = { 'status_code': status, 'stations': site_data_list, @@ -166,6 +170,7 @@ def monitoring_location(site_no): 'STATION_FIELDS_D': STATION_FIELDS_D, 'json_ld': Markup(json.dumps(json_ld, indent=4)), 'available_data_types': available_data_types, + 'time_zone': time_zone, 'uv_period_of_record': get_period_of_record_by_parm_cd(parameter_data), 'gw_period_of_record': get_period_of_record_by_parm_cd(parameter_data, 'gw') if app.config[ 'GROUNDWATER_LEVELS_ENABLED'] else None,