diff --git a/assets/src/scripts/mock-service-data.js b/assets/src/scripts/mock-service-data.js index 185871eb8f9b5c088eff17ed631f9548d658dcd8..d847a6d7260ccee786c7f49aaa2606e4fd031214 100644 --- a/assets/src/scripts/mock-service-data.js +++ b/assets/src/scripts/mock-service-data.js @@ -151,7 +151,7 @@ export const MOCK_NLDI_UPSTREAM_BASIN_FEATURE = ` } `; -export const MOCK_SENSOR_THINGS_DATA = `{ +export const MOCK_SITE_DATASTREAM_DATA = `{ "value": [{ "description": "Dissolved oxygen / USGS-01646500-bb18f3558934465194f8da1d5f1c3df3", "@iot.id": 22420, @@ -203,6 +203,78 @@ export const MOCK_SENSOR_THINGS_DATA = `{ }] }`; +export const MOCK_SITES_GEOJSON = ` +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "id": "USGS-05372995", + "@iot.selfLink": "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-05372995')", + "name": "USGS-05372995", + "description": "Stream", + "properties/state": "Minnesota", + "properties/active": true, + "properties/agency": "U.S. Geological Survey", + "properties/county": "Olmsted County", + "properties/country": "US", + "properties/district": "Minnesota", + "properties/stateFIPS": "US:27", + "properties/agencyCode": "USGS", + "properties/countyFIPS": "US:27:109", + "properties/countryFIPS": "US", + "properties/districtCode": "27", + "properties/altitudeDatum": "North American Vertical Datum of 1988", + "properties/altitudeMethod": "GNSS2 - Level 2 Quality Survey Grade Global Navigation Satellite System", + "properties/hydrologicUnit": "070400040110", + "properties/altitudeAccuracy": ".1", + "properties/monitoringLocationUrl": "https://waterdata.usgs.gov/monitoring-location/05372995", + "properties/monitoringLocationName": "SOUTH FORK ZUMBRO RIVER AT ROCHESTER, MN", + "properties/monitoringLocationType": "Stream", + "properties/monitoringLocationNumber": "05372995", + "properties/monitoringLocationAltitudeLandSurface": "949.07", + "Locations/0/id": "db44c77a-2d23-11ec-a8de-4ff4a62f9372" + }, + "geometry": { + "type": "Point", + "coordinates": [-92.4662889, 44.061631] + } + }, { + "type": "Feature", + "properties": { + "id": "USGS-05374000", + "@iot.selfLink": "https://labs.waterdata.usgs.gov/sta/v1.1/Things('USGS-05374000')", + "name": "USGS-05374000", + "description": "Stream", + "properties/state": "Minnesota", + "properties/active": true, + "properties/agency": "U.S. Geological Survey", + "properties/county": "Wabasha County", + "properties/country": "US", + "properties/district": "Minnesota", + "properties/stateFIPS": "US:27", + "properties/agencyCode": "USGS", + "properties/countyFIPS": "US:27:157", + "properties/countryFIPS": "US", + "properties/districtCode": "27", + "properties/altitudeDatum": "North American Vertical Datum of 1988", + "properties/altitudeMethod": "GNSS2 - Level 2 Quality Survey Grade Global Navigation Satellite System", + "properties/hydrologicUnit": "070400040504", + "properties/altitudeAccuracy": ".1", + "properties/monitoringLocationUrl": "https://waterdata.usgs.gov/monitoring-location/05374000", + "properties/monitoringLocationName": "ZUMBRO RIVER AT ZUMBRO FALLS, MN", + "properties/monitoringLocationType": "Stream", + "properties/monitoringLocationNumber": "05374000", + "properties/monitoringLocationAltitudeLandSurface": "811.35", + "Locations/0/id": "569126d8-2d26-11ec-b026-1722c089e6de" + }, + "geometry": { + "type": "Point", + "coordinates": [-92.43, 44.2855555] + } + }] +}` + export const MOCK_STATISTICS_RDB = `# # # US Geological Survey, Water Resources Data diff --git a/assets/src/scripts/monitoring-location/components/map/index.js b/assets/src/scripts/monitoring-location/components/map/index.js index 5eed9e59f83787f9131df782515b7903cfffd681..cc93d8ef46a768b493d36d8f0cd7be760ed27395 100644 --- a/assets/src/scripts/monitoring-location/components/map/index.js +++ b/assets/src/scripts/monitoring-location/components/map/index.js @@ -8,7 +8,7 @@ import {createMap, createBaseLayer} from 'ui/leaflet-rendering/map'; import {legendControl} from 'ui/leaflet-rendering/legend-control'; import {link} from 'ui/lib/d3-redux'; -import {fetchNetworkMonitoringLocations} from 'ui/web-services/observations'; +import {fetchSitesGeoJson} from 'ui/web-services/sensor-things'; import {hasFloodData, getFloodExtent, getFloodStageHeight} from 'ml/selectors/flood-data-selector'; import {hasNldiData, getNldiDownstreamFlows, getNldiUpstreamFlows, getNldiUpstreamBasin} @@ -46,10 +46,10 @@ const getActiveMonitoringLocationsLayer = function(locations, markerOptions) { return L.circleMarker(latlng, markerOptions); }, onEachFeature: function(feature, layer) { - const url = feature.properties.monitoringLocationUrl; - const name = feature.properties.monitoringLocationName; - const id = feature.properties.monitoringLocationNumber; - const type = feature.properties.monitoringLocationType; + const url = feature.properties['properties/monitoringLocationUrl']; + const name = feature.properties['properties/monitoringLocationName']; + const id = feature.properties['properties/monitoringLocationNumber']; + const type = feature.properties.description; const popupText = `Monitoring Location: <a href="${url}">${name}</a> <br/>ID: ${id}<br/>Site type: ${type}`; layer.bindPopup(popupText, { @@ -181,18 +181,16 @@ const siteMap = function(node, {siteno, latitude, longitude, zoom}, store) { const south = bounds.getSouth().toPrecision(3); const east = bounds.getEast().toPrecision(3); const north = bounds.getNorth().toPrecision(3); - const queryParams = { - active: true, - bbox:`${west},${south},${east},${north}` - }; + const wktPolygon = + `Polygon((${west} ${north}, ${west} ${south}, ${east} ${south}, ${east} ${north}, ${west} ${north}))`; + activeSitesLayer.clearLayers(); - fetchNetworkMonitoringLocations('RTN', queryParams).then((rtnLocations) => { - const locationsToDraw = rtnLocations.filter((feature) => feature.properties.monitoringLocationNumber !== siteno); - activeSitesLayer.addLayer(getActiveMonitoringLocationsLayer(locationsToDraw, geojsonMarkerOptions)); - }); - fetchNetworkMonitoringLocations('RTS', queryParams).then((rtsLocations) => { - const locationsToDraw = rtsLocations.filter((feature) => feature.properties.monitoringLocationNumber !== siteno); - activeSitesLayer.addLayer(getActiveMonitoringLocationsLayer(locationsToDraw, geojsonMarkerOptions)); + fetchSitesGeoJson(wktPolygon).then((featureCollection) => { + if ('features' in featureCollection) { + const locationsToDraw = featureCollection.features.filter( + (feature) => feature.properties['properties/monitoringLocationNumber'] !== siteno); + activeSitesLayer.addLayer(getActiveMonitoringLocationsLayer(locationsToDraw, geojsonMarkerOptions)); + } }); }; diff --git a/assets/src/scripts/monitoring-location/components/map/index.test.js b/assets/src/scripts/monitoring-location/components/map/index.test.js index e69432075114850ce2b6a891cf13acd7c8b62bc8..9cc7268046ff9a1ad205ea29e7f34fbcca9425c3 100644 --- a/assets/src/scripts/monitoring-location/components/map/index.test.js +++ b/assets/src/scripts/monitoring-location/components/map/index.test.js @@ -16,7 +16,7 @@ describe('monitoring-location/components/map module', () => { config.TNM_USGS_IMAGERY_ONLY_ENDPOINT = 'https://fakeimageryonly.com'; config.TNM_USGS_IMAGERY_TOPO_ENDPOINT = 'https://fakeimagerytopo.com'; config.TNM_HYDRO_ENDPOINT = 'https://faketnmhydro.com'; - config.OBSERVATIONS_ENDPOINT = 'https://fakeogcservice.com/observations/'; + config.SENSOR_THINGS_ENDPOINT = 'https://fakesensorthings.com/sta/v1.1/'; config.FIM_GIS_ENDPOINT = 'https://fakelegendservice.com/'; config.NLDI_SERVICES_ENDPOINT = 'https://fakenldiservice.com/'; diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.js index e96adab7c8f08091ef47826e2e6cc6284f5b2de3..b263c52f6f40b93905131f5fbc5cd4cc339e4535 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-data.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.js @@ -6,7 +6,7 @@ import {convertCelsiusToFahrenheit} from 'ui/utils'; import {fetchTimeSeries} from 'ui/web-services/instantaneous-values'; import {fetchSiteStatistics} from 'ui/web-services/statistics-data'; -import {fetchDataFromSensorThings} from 'ui/web-services/sensor-things'; +import {fetchSiteDatastream} from 'ui/web-services/sensor-things'; import {isCalculatedTemperature, getConvertedTemperatureParameter} from 'ml/parameter-code-utils'; @@ -292,7 +292,7 @@ export const retrieveHydrographThresholds = function(agencySiteNumberCode, param }; return function(dispatch) { - return fetchDataFromSensorThings(agencySiteNumberCode, parameterCode).then((sensorThingsData) => { + return fetchSiteDatastream(agencySiteNumberCode, parameterCode).then((sensorThingsData) => { if (Object.keys(sensorThingsData).length !== 0) { const operatingLimits = sensorThingsData.value .filter(timeSeriesMetaData => timeSeriesMetaData.properties && timeSeriesMetaData.properties.Thresholds) diff --git a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js index 836a2d4460053701cdbb8b21e1c3b76e786464bb..9a175e38b2836cb339bc6f70cdd45807517bc8f8 100644 --- a/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js +++ b/assets/src/scripts/monitoring-location/store/hydrograph-data.test.js @@ -7,7 +7,7 @@ import { MOCK_IV_DATA, MOCK_TEMP_C_IV_DATA, MOCK_STATISTICS_JSON, - MOCK_SENSOR_THINGS_DATA + MOCK_SITE_DATASTREAM_DATA } from 'ui/mock-service-data'; import * as ivDataService from 'ui/web-services/instantaneous-values'; @@ -55,8 +55,8 @@ describe('monitoring-location/store/hydrograph-data', () => { jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_IV_DATA))); statisticsDataService.fetchSiteStatistics = jest.fn().mockReturnValue(Promise.resolve(MOCK_STATISTICS_JSON)); - sensorThingsService.fetchDataFromSensorThings = - jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SENSOR_THINGS_DATA))); + sensorThingsService.fetchSiteDatastream = + jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SITE_DATASTREAM_DATA))); }); it('Expects to retrieve primary IV data if in the period of record', () => { @@ -272,8 +272,8 @@ describe('monitoring-location/store/hydrograph-data', () => { jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_IV_DATA))); statisticsDataService.fetchSiteStatistics = jest.fn().mockReturnValue(Promise.resolve(MOCK_STATISTICS_JSON)); - sensorThingsService.fetchDataFromSensorThings = - jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SENSOR_THINGS_DATA))); + sensorThingsService.fetchSiteDatastream = + jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SITE_DATASTREAM_DATA))); }); it('Expect IV data is stored when available', () => { @@ -370,8 +370,8 @@ describe('monitoring-location/store/hydrograph-data', () => { beforeEach(() => { ivDataService.fetchTimeSeries = jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_TEMP_C_IV_DATA))); - sensorThingsService.fetchDataFromSensorThings = - jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SENSOR_THINGS_DATA))); + sensorThingsService.fetchSiteDatastream = + jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SITE_DATASTREAM_DATA))); config.ivPeriodOfRecord = { '00010': {begin_date: '2010-01-01', end_date: '2020-01-01'} }; @@ -429,8 +429,8 @@ describe('monitoring-location/store/hydrograph-data', () => { beforeEach(() => { ivDataService.fetchTimeSeries = jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_IV_DATA))); - sensorThingsService.fetchDataFromSensorThings = - jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SENSOR_THINGS_DATA))); + sensorThingsService.fetchSiteDatastream = + jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SITE_DATASTREAM_DATA))); config.ivPeriodOfRecord = { '00060': {begin_date: '2010-01-01', end_date: '2020-01-01'} }; @@ -498,8 +498,8 @@ describe('monitoring-location/store/hydrograph-data', () => { jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_IV_DATA))); statisticsDataService.fetchSiteStatistics = jest.fn().mockReturnValue(Promise.resolve(MOCK_STATISTICS_JSON)); - sensorThingsService.fetchDataFromSensorThings = - jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SENSOR_THINGS_DATA))); + sensorThingsService.fetchSiteDatastream = + jest.fn().mockReturnValue(Promise.resolve(JSON.parse(MOCK_SITE_DATASTREAM_DATA))); config.ivPeriodOfRecord = { '00060': {begin_date: '2010-01-01', end_date: '2020-01-01'} }; diff --git a/assets/src/scripts/web-services/sensor-things.js b/assets/src/scripts/web-services/sensor-things.js index 20cf36fefe89f9c33c5531946dd629dd0733bf6d..1db010904d813644e6764ac42f48100c0b7bad56 100644 --- a/assets/src/scripts/web-services/sensor-things.js +++ b/assets/src/scripts/web-services/sensor-things.js @@ -1,25 +1,56 @@ import config from 'ui/config'; + + /** - * Formats a URL to connect with the Sensor Things API. + * Returns the sensor things datastream URL used to fetch the datastream. Add the filter for parameter code if not the null string * @param {String} agencySiteNumberCode - combines the Agency Code with the Site Number for example USGS-01646500 * @param {String} parameterCode - five digit code that uniquely identifies the observed property (parameter) - * @return {String} URL for Sensor Things API + * @return {String} Sensor Things datastream URL for the site with parameter code. */ -export const getSensorThingsURL = function(agencySiteNumberCode, parameterCode) { - const queryString = `ParameterCode eq '${parameterCode}'`; - - return `${config.SENSOR_THINGS_ENDPOINT}Things('${agencySiteNumberCode}')/Datastreams?$filter=properties/${encodeURIComponent(queryString)} `; +const getSiteDatastreamURL = function(agencySiteNumberCode, parameterCode) { + const parameterQuery = parameterCode ? `ParameterCode eq '${parameterCode}'` : ''; + const queryString = parameterQuery ? `?$filter=properties/${encodeURIComponent(parameterQuery)}` : ''; + return `${config.SENSOR_THINGS_ENDPOINT}Things('${agencySiteNumberCode}')/Datastreams${queryString}`; }; /** - * Does the work of contacting the Sensor Things API and returning data + * Does the work of contacting the Sensor Things API and returning the datastream * @param {String} agencySiteNumberCode - combines the Agency Code with the Site Number for example USGS-01646500 * @param {String} parameterCode - five digit code that uniquely identifies the observed property * @return {Promise} resolves to an empty object or one containing threshold information (and other secondary information) */ -export const fetchDataFromSensorThings = async function(agencySiteNumberCode, parameterCode) { - const url = getSensorThingsURL(agencySiteNumberCode, parameterCode); +export const fetchSiteDatastream = async function(agencySiteNumberCode, parameterCode) { + const url = getSiteDatastreamURL(agencySiteNumberCode, parameterCode); + try { + const response = await fetch(url, { + method: 'GET' + }); + if (response.status === 200) { + return await response.json(); + + } else { + console.error(`Received bad status, ${response.status} at ${url}`); + return {}; + } + } catch (error) { + console.error(`Failed fetch for ${url}`); + return {}; + } +}; + +/** + * Fetch the sites that are within the polygon and return the geojson. + * @param {String} - Well Known Text Polygon string, for example 'Polygon((-92 44, -89 44, -89 43, -92 43, -92 44))' + * @returns {Promise} - resolves to a json object. If the call fails or returns an unexpected status the json object + * will be empty + */ +export const fetchSitesGeoJson = async function(wktPolygon) { + const expandQuery = encodeURIComponent('Locations($select=id,location)'); + const locationQuery = encodeURIComponent(`st_within(Location/location, geography'${wktPolygon}')`); + const url = + `${config.SENSOR_THINGS_ENDPOINT}/Things?$expand=${expandQuery}&$filter=${locationQuery}&$resultFormat=GeoJSON`; + try { const response = await fetch(url, { method: 'GET' diff --git a/assets/src/scripts/web-services/sensor-things.test.js b/assets/src/scripts/web-services/sensor-things.test.js index 54c0b90b0ca5395099c414c56c033c872ab31430..d1a87a00b0ddedc2e5974d57237914458d608d81 100644 --- a/assets/src/scripts/web-services/sensor-things.test.js +++ b/assets/src/scripts/web-services/sensor-things.test.js @@ -2,9 +2,9 @@ import {enableFetchMocks, disableFetchMocks} from 'jest-fetch-mock'; import mockConsole from 'jest-mock-console'; import config from 'ui/config'; -import {MOCK_SENSOR_THINGS_DATA} from 'ui/mock-service-data'; +import {MOCK_SITE_DATASTREAM_DATA, MOCK_SITES_GEOJSON} from 'ui/mock-service-data'; -import {getSensorThingsURL, fetchDataFromSensorThings} from './sensor-things'; +import {fetchSiteDatastream, fetchSitesGeoJson} from './sensor-things'; describe('web-services/sensor-things', () => { let restoreConsole; @@ -20,29 +20,64 @@ describe('web-services/sensor-things', () => { restoreConsole(); }); - describe('getSensorThingsURL', () => { - it('Expects url to contain combined agency and site number', () => { - const result = getSensorThingsURL('USGS-01646500', '00065'); - expect(result).toContain('00065'); - expect(result).toContain('USGS-01646500'); + describe('fetchSiteDatastream', () => { + beforeEach(() => { + fetch.resetMocks(); + }); + + it('Expects the url to contain the site identifier and the parameter code', () => { + fetchSiteDatastream('USGS-01646500', '00065'); + const url = fetch.mock.calls[0][0]; + + expect(url).toContain('USGS-01646500'); + expect(url.split('$filter')[1]).toContain('00065'); + }); + + it('expects the url to not contain a filter if no parameter code is specified', () => { + fetchSiteDatastream('USGS-01646500'); + const url = fetch.mock.calls[0][0]; + + expect(url.split('$filter')).toHaveLength(1); + }); + + it('Successful fetch returns a JSON object with threshold data', async() => { + fetch.once(MOCK_SITE_DATASTREAM_DATA, {status: 200}); + const resp = await fetchSiteDatastream('USGS-01646500', '00065'); + + expect(resp).toEqual(JSON.parse(MOCK_SITE_DATASTREAM_DATA)); + }); + + it('Bad fetch returns an empty object', async() => { + fetch.once('Internal server error', {status: 500}); + const resp = await fetchSiteDatastream('USGS-01646500', '00065'); + expect(resp).toEqual({}); }); }); - describe('fetchDataFromSensorThings', () => { + describe('fetchSitesGeoJson', () => { beforeEach(() => { fetch.resetMocks(); }); + const TEST_POLYGON = 'POLYGON((-92 43, -92 44, -91 44, 91 43, -92 43))'; + + it('Expects the url to contain the polygon', () => { + fetchSitesGeoJson(TEST_POLYGON); + const url = fetch.mock.calls[0][0]; + + expect(url.split('$filter')[1]).toContain( + encodeURIComponent(TEST_POLYGON)); + }); it('Successful fetch returns a JSON object with threshold data', async() => { - fetch.once(MOCK_SENSOR_THINGS_DATA, {status: 200}); - const resp = await fetchDataFromSensorThings('USGS-01646500', '00065'); + fetch.once(MOCK_SITES_GEOJSON, {status: 200}); + const resp = await fetchSitesGeoJson(TEST_POLYGON); - expect(resp).toEqual(JSON.parse(MOCK_SENSOR_THINGS_DATA)); + expect(resp).toEqual(JSON.parse(MOCK_SITES_GEOJSON)); }); it('Bad fetch returns an empty object', async() => { fetch.once('Internal server error', {status: 500}); - const resp = await fetchDataFromSensorThings('USGS-01646500', '00065'); + const resp = await fetchSitesGeoJson(TEST_POLYGON); expect(resp).toEqual({}); }); });