"use strict"; const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1"; /** * Retrieve the datastream ID that corresponds to a particular building * @param {Number | String} buildingNumber Integer representing the building ID * @param {String} phenomenon String representing the phenomenon of interest * @param {String} samplingRate String representing the sampling rate of the observations * @returns {Number} Datastream corresponding to the input building */ const getDatastreamIdFromBuildingNumber = function ( buildingNumber, phenomenon, samplingRate ) { const buildingToDatastreamMapping = { 101: { vl: { "15min": "69", "60min": "75" }, rl: { "15min": "81", "60min": "87" }, // These Datastreams do not yet have Observations // flow: { "15min": "93", "60min": "99" }, // power: { "15min": "105", "60min": "111" }, // energy: { "15min": "117", "60min": "123" }, // energy_verb: { "15min": "129", "60min": "135" }, }, 102: { vl: { "15min": "70", "60min": "76" }, rl: { "15min": "82", "60min": "88" }, // These Datastreams do not yet have Observations // flow: { "15min": "94", "60min": "100" }, // power: { "15min": "106", "60min": "112" }, // energy: { "15min": "118", "60min": "124" }, // energy_verb: { "15min": "130", "60min": "136" }, }, 107: { vl: { "15min": "71", "60min": "77" }, rl: { "15min": "83", "60min": "89" }, // These Datastreams do not yet have Observations // flow: { "15min": "95", "60min": "101" }, // power: { "15min": "107", "60min": "113" }, // energy: { "15min": "119", "60min": "125" }, // energy_verb: { "15min": "131", "60min": "137" }, }, "112, 118": { vl: { "15min": "72", "60min": "78" }, rl: { "15min": "84", "60min": "90" }, // These Datastreams do not yet have Observations // flow: { "15min": "96", "60min": "102" }, // power: { "15min": "108", "60min": "114" }, // energy: { "15min": "120", "60min": "126" }, // energy_verb: { "15min": "132", "60min": "138" }, }, 125: { vl: { "15min": "73", "60min": "79" }, rl: { "15min": "85", "60min": "91" }, // These Datastreams do not yet have Observations // flow: { "15min": "97", "60min": "103" }, // power: { "15min": "109", "60min": "115" }, // energy: { "15min": "121", "60min": "127" }, // energy_verb: { "15min": "133", "60min": "139" }, }, 225: { vl: { "15min": "74", "60min": "80" }, rl: { "15min": "86", "60min": "92" }, flow: { "15min": "98", "60min": "104" }, power: { "15min": "110", "60min": "116" }, energy: { "15min": "122", "60min": "128" }, energy_verb: { "15min": "134", "60min": "140" }, }, weather_station_521: { outside_temp: { "15min": "141", "60min": "142" }, }, }; if ( buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[ samplingRate ] === undefined ) return; const datastreamIdMatched = Number( buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate] ); return datastreamIdMatched; }; /** * Create URL to fetch the details of single Datastream * @param {String} baseUrl Base URL of the STA server * @param {Number} datastreamID Integer representing the Datastream ID * @returns {String} URL string for fetching a single Datastream */ const getDatastreamUrl = function (baseUrl, datastreamID) { if (!datastreamID) return; const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})`; return fullDatastreamURL; }; /** * Create URL to fetch Observations * @param {String} baseUrl Base URL of the STA server * @param {Number} datastreamID Integer representing the Datastream ID * @returns {String} URL string for fetching Observations */ const getObservationsUrl = function (baseUrl, datastreamID) { if (!datastreamID) return; const fullObservationsURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`; return fullObservationsURL; }; /** * Create a temporal filter string for the fetched Observations * @param {String} dateStart Start date in YYYY-MM-DD format * @param {String} dateStop Stop date in YYYY-MM-DD format * @returns {String} Temporal filter string */ const createTemporalFilterString = function (dateStart, dateStop) { if (!dateStart || !dateStop) return; const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`; return filterString; }; // const BASE_URL_OBSERVATIONS = getObservationsUrl(80); const QUERY_PARAM_RESULT_FORMAT = "dataArray"; const QUERY_PARAM_ORDER_BY = "phenomenonTime asc"; const QUERY_PARAM_FILTER = createTemporalFilterString( "2020-01-01", "2021-01-01" ); const QUERY_PARAM_SELECT = "result,phenomenonTime"; const QUERY_PARAMS_COMBINED = { "$resultFormat": QUERY_PARAM_RESULT_FORMAT, "$orderBy": QUERY_PARAM_ORDER_BY, "$filter": QUERY_PARAM_FILTER, "$select": QUERY_PARAM_SELECT, }; /** * Perform a GET request using the Axios library * @param {String} urlObservations A URL that fetches Observations from an STA instance * @param {Object} urlParamObj The URL parameters to be sent together with the GET request * @returns {Promise} A promise that contains the first page of results when fulfilled */ const axiosGetRequest = function (urlObservations, urlParamObj) { return axios.get(urlObservations, { params: urlParamObj, }); }; /** * Retrieve the metadata for a single datastream * @async * @param {String} urlDatastream A URL that fetches a Datastream from an STA instance * @returns {Promise} A promise that contains a metadata object for a Datastream when fulfilled */ const getMetadataFromSingleDatastream = async function (urlDatastream) { try { // Extract properties of interest const { data: { description, name, unitOfMeasurement }, } = await axiosGetRequest(urlDatastream); return { description, name, unitOfMeasurement }; } catch (err) { console.error(err); } }; /** * Retrieve metadata from multiple datastreams * @async * @param {Array} datastreamsUrlArr An array that contains N Datastream URL strings * @returns {Promise} A promise that contains an array of N Datastream metadata objects when fulfilled */ const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) { try { // Array to store our final result const datastreamMetadataArr = []; // Use for/of loop - we need to maintain the order of execution of the async operations for (const datastreamUrl of datastreamsUrlArr) { // Metadata from a single Datastream const datastreamMetadata = await getMetadataFromSingleDatastream( datastreamUrl ); datastreamMetadataArr.push(datastreamMetadata); } return datastreamMetadataArr; } catch (err) { console.error(err); } }; /** * Match the unitOfMeasurement's string representation of a symbol to an actual symbol, where necessary * @param {String} unitOfMeasurementSymbolString String representation of the unitOfMeasurement's symbol * @returns {String} The unitOfMeasurement's symbol */ const matchUnitOfMeasurementSymbolStringToSymbol = function ( unitOfMeasurementSymbolString ) { const unicodeCodePointDegreeSymbol = "\u00B0"; const unicodeCodePointSuperscriptThree = "\u00B3"; if (unitOfMeasurementSymbolString === "degC") return `${unicodeCodePointDegreeSymbol}C`; if (unitOfMeasurementSymbolString === "m3/h") return `m${unicodeCodePointSuperscriptThree}/h`; // If no symbol exists return unitOfMeasurementSymbolString; }; /** * Extract the phenomenon name from a Datastream's name * @param {String} datastreamName A string representing the Datastream's name * @returns {String} The extracted phenomenon name */ const extractPhenomenonNameFromDatastreamName = function (datastreamName) { const regex = /\/ (.*) DS/; return datastreamName.match(regex)[1]; // use second element in array }; /** * Format the response containing a Datastream's metadata from Sensorthings API * @param {Object} datastreamMetadata An object containing a Datastream's metadata * @returns {Object} An object containing the formatted metadata that is suitable for use in a line chart or heatmap */ const formatDatastreamMetadataForChart = function (datastreamMetadata) { const { description: datastreamDescription, name: datastreamName, unitOfMeasurement, } = datastreamMetadata; // Extract phenomenon name from Datastream name const phenomenonName = extractPhenomenonNameFromDatastreamName(datastreamName); // Get the unitOfMeasurement's symbol const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol( unitOfMeasurement.symbol ); return { datastreamDescription, datastreamName, phenomenonName, unitOfMeasurementSymbol, }; }; /** * Format the response from SensorThings API to make it suitable for heatmap * @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Array of formatted observations suitable for use in a heatmap */ const formatSTAResponseForHeatMap = function (obsArray) { if (!obsArray) return; const dataSTAFormatted = obsArray.map((obs) => { // Get the date/time string; first element in input array; remove trailing "Z" const obsDateTimeInput = obs[0].slice(0, -1); // Get the "date" part of an observation const obsDateInput = obs[0].slice(0, 10); // Create Date objects const obsDateTime = new Date(obsDateTimeInput); const obsDate = new Date(obsDateInput); // x-axis -> timestamp; will be the same for observations from the same date const timestamp = Date.parse(obsDate); // y-axis -> hourOfDay const hourOfDay = obsDateTime.getHours(); // value -> the observation's value; second element in input array const value = obs[1]; return [timestamp, hourOfDay, value]; }); return dataSTAFormatted; }; /** * Calculate the minimum and maximum values for a heatmap's color axis * @param {Array} formattedObsArrHeatmap Response from SensorThings API formatted for use in a heatmap * @returns {Object} An object containing the minimum and maximum values */ const calculateMinMaxValuesForHeatmapColorAxis = function ( formattedObsArrHeatmap ) { // The observation value is the third element in array const obsValueArr = formattedObsArrHeatmap.map((obs) => obs[2]); // Extract integer part const minValue = Math.trunc(Math.min(...obsValueArr)); const maxValue = Math.trunc(Math.max(...obsValueArr)); // Calculate the closest multiple of 5 const minObsValue = minValue - (minValue % 5); const maxObsValue = maxValue + (5 - (maxValue % 5)); return { minObsValue, maxObsValue }; }; /** * Draw a heatmap using Highcharts library * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata * @returns {undefined} undefined */ const drawHeatMapHC = function ( formattedObsArrayForHeatmap, formattedDatastreamMetadata ) { const { datastreamDescription: DATASTREAM_DESCRIPTION, datastreamName: DATASTREAM_NAME, phenomenonName: PHENOMENON_NAME, unitOfMeasurementSymbol: PHENOMENON_SYMBOL, } = formattedDatastreamMetadata; const { minObsValue: MINIMUM_VALUE_COLOR_AXIS, maxObsValue: MAXIMUM_VALUE_COLOR_AXIS, } = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap); Highcharts.chart("chart-heatmap", { chart: { type: "heatmap", zoomType: "x", }, boost: { useGPUTranslations: true, }, title: { text: DATASTREAM_DESCRIPTION, align: "left", x: 40, }, subtitle: { text: DATASTREAM_NAME, align: "left", x: 40, }, xAxis: { type: "datetime", // min: Date.UTC(2017, 0, 1), // max: Date.UTC(2017, 11, 31, 23, 59, 59), labels: { align: "left", x: 5, y: 14, format: "{value:%B}", // long month }, showLastLabel: false, tickLength: 16, }, yAxis: { title: { text: null, }, labels: { format: "{value}:00", }, minPadding: 0, maxPadding: 0, startOnTick: false, endOnTick: false, tickPositions: [0, 3, 6, 9, 12, 15, 18, 21, 24], tickWidth: 1, min: 0, max: 23, reversed: true, }, colorAxis: { stops: [ [0, "#3060cf"], [0.5, "#fffbbc"], [0.9, "#c4463a"], [1, "#c4463a"], ], min: MINIMUM_VALUE_COLOR_AXIS, max: MAXIMUM_VALUE_COLOR_AXIS, startOnTick: false, endOnTick: false, labels: { // format: "{value}℃", format: `{value}${PHENOMENON_SYMBOL}`, }, }, series: [ { data: formattedObsArrayForHeatmap, boostThreshold: 100, borderWidth: 0, nullColor: "#525252", colsize: 24 * 36e5, // one day tooltip: { headerFormat: `${PHENOMENON_NAME}
`, valueDecimals: 2, pointFormat: // "{point.x:%e %b, %Y} {point.y}:00: {point.value} ℃", `{point.x:%e %b, %Y} {point.y}:00: {point.value} ${PHENOMENON_SYMBOL}`, nullFormat: `{point.x:%e %b, %Y} {point.y}:00: null`, }, turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release }, ], }); }; /** * Format the response from SensorThings API to make it suitable for line chart * @param {Array} obsArray Response from SensorThings API as array * @returns {Array} Array of formatted observations suitable for use in a line chart */ const formatSTAResponseForLineChart = function (obsArray) { if (!obsArray) return; const dataSTAFormatted = obsArray.map((result) => { const timestampObs = new Date(result[0].slice(0, -1)).getTime(); // slice() removes trailing "Z" character in timestamp const valueObs = result[1]; return [timestampObs, valueObs]; }); return dataSTAFormatted; }; /** * Draw a line chart using Highcharts library * @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata * @returns {undefined} undefined */ const drawLineChartHC = function ( formattedObsArrayForLineChart, formattedDatastreamMetadata ) { const { datastreamDescription: DATASTREAM_DESCRIPTION, datastreamName: DATASTREAM_NAME, phenomenonName: PHENOMENON_NAME, unitOfMeasurementSymbol: PHENOMENON_SYMBOL, } = formattedDatastreamMetadata; Highcharts.stockChart("chart-line", { chart: { zoomType: "x", }, rangeSelector: { selected: 5, }, title: { text: DATASTREAM_DESCRIPTION, "align": "left", }, subtitle: { text: DATASTREAM_NAME, align: "left", }, series: [ { name: `${PHENOMENON_NAME} (${PHENOMENON_SYMBOL})`, data: formattedObsArrayForLineChart, tooltip: { valueDecimals: 2, }, turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release }, ], }); }; /** * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations * @returns {Array} An array of timestamps missing from either set of observations */ const getSymmetricDifferenceBetweenArrays = function ( obsTimestampArrayOne, obsTimestampArrayTwo ) { const differenceBetweenArrays = obsTimestampArrayOne .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .concat( obsTimestampArrayTwo.filter( (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) ) ); return differenceBetweenArrays; }; /** * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations * @returns {Array} An array of the indexes of the missing observations */ const getIndexesOfUniqueObservations = function ( uniqueTimestampsArr, largerObsTimestampArr ) { const indexesMissingObs = uniqueTimestampsArr.map((index) => largerObsTimestampArr.indexOf(index) ); return indexesMissingObs; }; /** * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations * @param {Array} largerObsArr The larger array of observations (timestamp + value) which is modified in place * @returns {undefined} */ const removeUniqueObservationsFromLargerArray = function ( uniqueIndexesArr, largerObsArr ) { // Reverse the indexes array so that the larger index is removed first uniqueIndexesArr.reverse(); uniqueIndexesArr.forEach((index) => { if (index > -1) { largerObsArr.splice(index, 1); } }); }; /** * Compares the length of two input arrays to determine the larger one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The larger array */ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; if (firstArr.length > secondArr.length) return firstArr; if (firstArr.length < secondArr.length) return secondArr; }; /** * Compares the length of two input arrays to determine the smaller one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The smaller array */ const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; if (firstArr.length < secondArr.length) return firstArr; if (firstArr.length > secondArr.length) return secondArr; }; /** * Utility function for deleting the unique observations from a larger array * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const deleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { // Create arrays with timestamps only const obsArrayOneTimestamp = obsArrayOne.map( (obsTimeValue) => obsTimeValue[0] ); const obsArrayTwoTimestamp = obsArrayTwo.map( (obsTimeValue) => obsTimeValue[0] ); const missingTimestamp = getSymmetricDifferenceBetweenArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Determine the larger observation timestamp array const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Indexes of the missing observations const indexesMissingObsArr = getIndexesOfUniqueObservations( missingTimestamp, biggerObsTimestampArr ); // Determine the larger observation array const biggerObsArr = getLargerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Determine the smaller observation array const smallerObsArr = getSmallerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Remove the missing observation from the larger array of observations // Modifies the array in place removeUniqueObservationsFromLargerArray(indexesMissingObsArr, biggerObsArr); return [biggerObsArr, smallerObsArr]; }; /** * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const checkForAndDeleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { if (obsArrayOne.length === obsArrayTwo.length) return; // Case 1: obsArrayOne.length < obsArrayTwo.length if (obsArrayOne.length < obsArrayTwo.length) { const [biggerObsArr, smallerObsArr] = deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); return [smallerObsArr, biggerObsArr]; } // Case 2: obsArrayOne.length > obsArrayTwo.length return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); }; /** * Extracts and combines observation values from two imput observation arrays of equal length * @param {Array} obsArrayOne First set of N observations (timestamp + value) * @param {Array} obsArrayTwo Second set of N observations (timestamp + value) * @returns {Array} A 2*N array of observation values from both input observation arrays */ const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) { // Extract the values from the two observation arrays const obsValuesOne = obsArrayOne.map((result) => result[1]); const obsValuesTwo = obsArrayTwo.map((result) => result[1]); // Since the arrays are of equal length, we need only use one of the arrays for looping const obsValuesOnePlusTwo = obsValuesOne.map((obsValOne, i) => { return [obsValOne, obsValuesTwo[i]]; }); return obsValuesOnePlusTwo; }; /** * Format the response from SensorThings API to make it suitable for scatter plot * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Array of formatted observations suitable for use in a scatter plot */ const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { // When our observation arrays have DIFFERENT lengths if (obsArrayOne.length !== obsArrayTwo.length) { const [obsArrayOneFinal, obsArrayTwoFinal] = checkForAndDeleteUniqueObservationsFromLargerArray( obsArrayOne, obsArrayTwo ); return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal); } // When our observation arrays already have SAME lengths return createCombinedObservationValues(obsArrayOne, obsArrayTwo); }; /** * Draw a scatter plot using Highcharts library * @param {*} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot * @param {*} formattedDatastreamMetadataSeriesOne Object containing Datastream metadata for the first chart series * @param {*} formattedDatastreamMetadataSeriesTwo Object containing Datastream metadata for the second chart series * @returns {undefined} */ const drawScatterPlotHC = function ( formattedObsArrayForSeriesOnePlusSeriesTwo, formattedDatastreamMetadataSeriesOne, formattedDatastreamMetadataSeriesTwo ) { const { datastreamDescription: DATASTREAM_DESCRIPTION_SERIES_1, datastreamName: DATASTREAM_NAME_SERIES_1, phenomenonName: PHENOMENON_NAME_SERIES_1, unitOfMeasurementSymbol: PHENOMENON_SYMBOL_SERIES_1, } = formattedDatastreamMetadataSeriesOne; const { datastreamDescription: DATASTREAM_DESCRIPTION_SERIES_2, datastreamName: DATASTREAM_NAME_SERIES_2, phenomenonName: PHENOMENON_NAME_SERIES_2, unitOfMeasurementSymbol: PHENOMENON_SYMBOL_SERIES_2, } = formattedDatastreamMetadataSeriesTwo; // Order of axes // Y-Axis -- Series 2 // X-Axis -- Series 1 const CHART_TITLE = `${PHENOMENON_NAME_SERIES_2} Versus ${PHENOMENON_NAME_SERIES_1}`; const CHART_SUBTITLE = `Source: ${DATASTREAM_NAME_SERIES_2} & ${DATASTREAM_NAME_SERIES_1}`; const SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`; const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`; const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`; const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`; const SERIES_COMBINED_NAME = "Y, X"; const SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS = "223, 83, 83"; const SERIES_COMBINED_SYMBOL_COLOR_OPACITY = ".3"; const SERIES_COMBINED_SYMBOL_COLOR = `rgba(${SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS}, ${SERIES_COMBINED_SYMBOL_COLOR_OPACITY})`; const MARKER_RADIUS = 2; Highcharts.chart("chart-scatter-plot", { chart: { type: "scatter", zoomType: "xy", }, boost: { useGPUTranslations: true, usePreAllocated: true, }, title: { text: CHART_TITLE, }, subtitle: { text: CHART_SUBTITLE, }, xAxis: { labels: { format: `{value}`, }, title: { enabled: true, text: `${SERIES_1_NAME} [${SERIES_1_SYMBOL}]`, }, startOnTick: true, endOnTick: true, showLastLabel: true, }, yAxis: [ { labels: { format: `{value}`, }, title: { text: `${SERIES_2_NAME} [${SERIES_2_SYMBOL}]`, }, }, ], legend: { enabled: false, }, plotOptions: { scatter: { marker: { radius: MARKER_RADIUS, states: { hover: { enabled: true, lineColor: "rgb(100,100,100)", }, }, }, states: { hover: { marker: { enabled: false, }, }, }, tooltip: { headerFormat: "{series.name}
", pointFormat: `{point.y:.2f} ${SERIES_1_SYMBOL}, {point.x:.2f} ${SERIES_2_SYMBOL}`, }, }, }, series: [ { name: SERIES_COMBINED_NAME, color: SERIES_COMBINED_SYMBOL_COLOR, data: formattedObsArrayForSeriesOnePlusSeriesTwo, }, ], }); }; /** * Follows "@iot.nextLink" links in SensorThingsAPI's response * Appends new results to existing results * @async * @param {Promise} responsePromise Promise object resulting from an Axios GET request * @returns {Object} Object containing results from all the "@iot.nextLink" links */ const followNextLink = function (responsePromise) { if (!responsePromise) return; return responsePromise .then((lastSuccess) => { if (lastSuccess.data["@iot.nextLink"]) { return followNextLink( axios.get(lastSuccess.data["@iot.nextLink"]) ).then((nextLinkSuccess) => { nextLinkSuccess.data.value = lastSuccess.data.value.concat( nextLinkSuccess.data.value ); return nextLinkSuccess; }); } else { return lastSuccess; } }) .catch((err) => { console.error(err); }); }; /** * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links * @async * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request * @returns {Promise} A promise that contains an array of Observations from a single Datastream when fulfilled */ const getCombinedObservationsFromAllNextLinks = function ( httpGetRequestPromise ) { return followNextLink(httpGetRequestPromise) .then((success) => { const successValue = success.data.value; // Array that will hold the combined observations const combinedObservations = []; successValue.forEach((dataObj) => { // Each page of results will have a dataArray that holds the observations const dataArrays = dataObj.dataArray; combinedObservations.push(...dataArrays); }); return new Promise((resolve, reject) => { resolve(combinedObservations); }); }) .catch((err) => { console.error(err); }); }; /** * Retrieve the metadata for a Datastream as well as the Observations corresponding to it * @async * @param {Promise} metadataPlusObsPromiseArray An array that contains two promises, one for datastream metadata, the other for observations * @returns {Promise} A promise that contains two arrays when fulfilled, one for datastream metadata and the other for observations */ const getMetadataPlusObservationsFromSingleDatastream = async function ( metadataPlusObsPromiseArray ) { try { // Array to store our final result const combinedResolvedPromises = []; // Use for/of loop - we need to maintain the order of execution of the async operations for (const promise of metadataPlusObsPromiseArray) { // Resolved value of a single promise const resolvedPromise = await promise; combinedResolvedPromises.push(resolvedPromise); } return combinedResolvedPromises; } catch (err) { console.error(err); } }; /** * Retrieve all the Observations from an array of Observations promises * @async * @param {Promise} observationPromiseArray An array that contains N observation promises * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled */ const getObservationsFromMultipleDatastreams = async function ( observationPromiseArray ) { try { // Array to store our final result const observationsAllDatastreamsArr = []; // Use for/of loop - we need to maintain the order of execution of the async operations for (const observationPromise of observationPromiseArray) { // Observations from a single Datastream const observations = await observationPromise; observationsAllDatastreamsArr.push(observations); } return observationsAllDatastreamsArr; } catch (err) { console.error(err); } }; /** * Retrieve the metadata from multiple Datastreams and the Observations corresponding to these Datastreams * @async * @param {Array} bldgSensorSamplingRateArr A 3*N array containing buildings, sensors & sampling rates as strings, e.g. ["101", "rl", "60min"] * @returns {Promise} A promise that contains a N*2 array (the first element is an array of Observations and the second element is an array of Datastream metadata objects) when fulfilled */ const getMetadataPlusObservationsFromMultipleDatastreams = async function ( bldgSensorSamplingRateArr ) { try { if (!bldgSensorSamplingRateArr) return; // Datastreams IDs const datastreamsIdsArr = bldgSensorSamplingRateArr.map( (bldgSensorSamplingRate) => getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRate) ); // Observations URLs const observationsUrlArr = datastreamsIdsArr.map((datastreamId) => getObservationsUrl(BASE_URL, datastreamId) ); // Datastreams URLs const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) => getDatastreamUrl(BASE_URL, datastreamId) ); // Promise objects - Observations const observationsPromisesArr = observationsUrlArr.map((obsUrl) => getCombinedObservationsFromAllNextLinks( axiosGetRequest(obsUrl, QUERY_PARAMS_COMBINED) ) ); // Observations array const observationsArr = await getObservationsFromMultipleDatastreams( observationsPromisesArr ); // Metadata array const metadataArr = await getMetadataFromMultipleDatastreams( datastreamsUrlArr ); return [observationsArr, metadataArr]; } catch (err) { console.error(err); } }; /** * Calculates the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL) * @async * @param {String} buildingId The building ID as a string * @param {String} samplingRate The sampling rate as a string * @returns A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled */ const calculateVorlaufMinusRuecklaufTemperature = async function ( buildingId, samplingRate ) { const bldgSensorSamplingRateArr = [ [buildingId, "vl", samplingRate], [buildingId, "rl", samplingRate], ]; const BUILDING_ID = buildingId; const SAMPLING_RATE = samplingRate; const observationsPlusMetadata = await getMetadataPlusObservationsFromMultipleDatastreams( bldgSensorSamplingRateArr ); // Extract Vorlauf temperature and Ruecklauf temperature const [[vorlaufTemp, ruecklaufTemp], [metadataVorlauf, metadataRuecklauf]] = observationsPlusMetadata; const vorlaufTempValues = vorlaufTemp.map((obs) => obs[1]); const ruecklaufTempValues = ruecklaufTemp.map((obs) => obs[1]); // The arrays have equal length, we need only use one of them for looping // Resulting array contains the following pairs (timestamp + dT) const vorlaufMinusRuecklaufTemp = vorlaufTemp.map((obs, i) => [ obs[0], vorlaufTempValues[i] - ruecklaufTempValues[i], ]); // From Vorlauf metadata, extract `name` and `unitOfMeasurement` const { name: datastreamNameVorlauf, unitOfMeasurement } = metadataVorlauf; // From Ruecklauf metadata, extract `name` const { name: datastreamNameRuecklauf } = metadataRuecklauf; // Extract the phenomenon names from Datastream names const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName( datastreamNameVorlauf ); const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName( datastreamNameRuecklauf ); // Create our custom datastream description text const descriptionCombined = `Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`; // The resulting datastream description string has two `temperature` substrings; // replace the first occurence with an empty string const description = descriptionCombined.replace("temperature", ""); // Create our custom datastream name text const name = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`; return [ vorlaufMinusRuecklaufTemp, // The datastream metadata object needs to have these property names { description, name, unitOfMeasurement, }, ]; }; /** * Test plotting of temp difference (dT) using heatmap */ const drawHeatmapHCUsingTempDifference = async function () { const [tempDifferenceObsArrBau225, tempDifferenceMetadataBau225] = await calculateVorlaufMinusRuecklaufTemperature("225", "60min"); drawHeatMapHC( formatSTAResponseForHeatMap(tempDifferenceObsArrBau225), formatDatastreamMetadataForChart(tempDifferenceMetadataBau225) ); }; /** * Test drawing of scatter plot chart */ const drawScatterPlotHCTest2 = async function () { const sensorsOfInterestArr = [ ["225", "vl", "60min"], // ["125", "rl", "60min"], ["weather_station_521", "outside_temp", "60min"], ]; const observationsPlusMetadata = await getMetadataPlusObservationsFromMultipleDatastreams( sensorsOfInterestArr ); // Extract the observations and metadata for each sensor // Array elements in same order as input array const [ [obsSensorOneArr, obsSensorTwoArr], [metadataSensorOne, metadataSensorTwo], ] = observationsPlusMetadata; drawScatterPlotHC( formatSTAResponseForScatterPlot(obsSensorOneArr, obsSensorTwoArr), formatDatastreamMetadataForChart(metadataSensorOne), formatDatastreamMetadataForChart(metadataSensorTwo) ); }; (async () => { // await drawScatterPlotHCTest2(); await drawHeatmapHCUsingTempDifference(); })(); export { BASE_URL, QUERY_PARAMS_COMBINED, getDatastreamIdFromBuildingNumber, getDatastreamUrl, getObservationsUrl, createTemporalFilterString, axiosGetRequest, getMetadataFromSingleDatastream, formatDatastreamMetadataForChart, formatSTAResponseForHeatMap, drawHeatMapHC, formatSTAResponseForLineChart, drawLineChartHC, getCombinedObservationsFromAllNextLinks, getMetadataPlusObservationsFromSingleDatastream, };