From d6e12e8f52d531206330a99f0281c3c4817bb5bf Mon Sep 17 00:00:00 2001
From: mbucknel <mbucknell@usgs.gov>
Date: Tue, 26 Jul 2022 09:52:13 -0500
Subject: [PATCH] Used new service call to get active sites from sensor things
 rather than the observations API. Updated tests as needed for changes to
 function calls.

---
 assets/src/scripts/mock-service-data.js       | 74 ++++++++++++++++++-
 .../components/map/index.js                   | 30 ++++----
 .../components/map/index.test.js              |  2 +-
 .../store/hydrograph-data.js                  |  4 +-
 .../store/hydrograph-data.test.js             | 22 +++---
 .../src/scripts/web-services/sensor-things.js | 49 +++++++++---
 .../web-services/sensor-things.test.js        | 59 ++++++++++++---
 7 files changed, 188 insertions(+), 52 deletions(-)

diff --git a/assets/src/scripts/mock-service-data.js b/assets/src/scripts/mock-service-data.js
index 185871eb8..d847a6d72 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 5eed9e59f..cc93d8ef4 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 e69432075..9cc726804 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 e96adab7c..b263c52f6 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 836a2d446..9a175e38b 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 20cf36fef..1db010904 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 54c0b90b0..d1a87a00b 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({});
         });
     });
-- 
GitLab