diff --git a/public/js/appChart.js b/public/js/appChart.js index c66a8b8f99a9f641f2b47ad44789c68f86799517..bd1cce104363e98396fd5ca8ca7ffd2fa50e9c81 100644 --- a/public/js/appChart.js +++ b/public/js/appChart.js @@ -206,6 +206,37 @@ const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) { } }; +/** + * 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 @@ -219,20 +250,13 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) { } = datastreamMetadata; // Extract phenomenon name from Datastream name - const regex = /\/ (.*) DS/; - const phenomenonName = datastreamName.match(regex)[1]; // use second element in array - - // Match the unitOfMeasurement's string representation of a symbol - // to an actual symbol, where necessary - const unitOfMeasurementSymbol = (() => { - if (unitOfMeasurement.symbol === "degC") { - return "℃"; - } else if (unitOfMeasurement.symbol === "m3/h") { - return "m3/h"; - } else { - return unitOfMeasurement.symbol; - } - })(); + const phenomenonName = + extractPhenomenonNameFromDatastreamName(datastreamName); + + // Get the unitOfMeasurement's symbol + const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol( + unitOfMeasurement.symbol + ); return { datastreamDescription, @@ -244,7 +268,7 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) { /** * Format the response from SensorThings API to make it suitable for heatmap - * @param {Array} obsArray Response from SensorThings API as array + * @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) { @@ -270,6 +294,28 @@ const formatSTAResponseForHeatMap = function (obsArray) { 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 @@ -287,24 +333,10 @@ const drawHeatMapHC = function ( unitOfMeasurementSymbol: PHENOMENON_SYMBOL, } = formattedDatastreamMetadata; - // Function returns the min and max observation values const { minObsValue: MINIMUM_VALUE_COLOR_AXIS, maxObsValue: MAXIMUM_VALUE_COLOR_AXIS, - } = (() => { - // The observation value is the third element in array - const obsValueArr = formattedObsArrayForHeatmap.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 }; - })(); + } = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap); Highcharts.chart("chart-heatmap", { chart: { @@ -399,11 +431,11 @@ const drawHeatMapHC = function ( }; /** - * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch + * 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 or scatter plot + * @returns {Array} Array of formatted observations suitable for use in a line chart */ -const formatSTAResponseForLineChartOrScatterPlot = function (obsArray) { +const formatSTAResponseForLineChart = function (obsArray) { if (!obsArray) return; const dataSTAFormatted = obsArray.map((result) => { @@ -478,7 +510,7 @@ const getSymmetricDifferenceBetweenArrays = function ( .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .concat( obsTimestampArrayTwo.filter( - (timestampTwo) => !obsTimestampArrayTwo.includes(timestampTwo) + (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) ) ); @@ -486,16 +518,16 @@ const getSymmetricDifferenceBetweenArrays = function ( }; /** - * Determines the indexes of timestamps that are missing from a smaller set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other - * @param {Array} missingTimestampsArr An array of strings representing the missing timestamps - * @param {Array} largerObsTimestampArr An array of timestamps for the larger array of observations + * 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 getIndexOfMissingObservation = function ( - missingTimestampsArr, +const getIndexesOfUniqueObservations = function ( + uniqueTimestampsArr, largerObsTimestampArr ) { - const indexesMissingObs = missingTimestampsArr.map((index) => + const indexesMissingObs = uniqueTimestampsArr.map((index) => largerObsTimestampArr.indexOf(index) ); @@ -503,16 +535,19 @@ const getIndexOfMissingObservation = function ( }; /** - * Removes observations (by modifying array in place) from a larger set of observations that are missing from a smaller set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other - * @param {Array} missingIndexesArr An array of the indexes of the observations missing from the smaller set of observations + * 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 removeMissingObservationFromLargerArray = function ( - missingIndexesArr, +const removeUniqueObservationsFromLargerArray = function ( + uniqueIndexesArr, largerObsArr ) { - missingIndexesArr.forEach((index) => { + // Reverse the indexes array so that the larger index is removed first + uniqueIndexesArr.reverse(); + + uniqueIndexesArr.forEach((index) => { if (index > -1) { largerObsArr.splice(index, 1); } @@ -533,6 +568,98 @@ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { 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) @@ -554,49 +681,20 @@ const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) { /** * Format the response from SensorThings API to make it suitable for scatter plot - * @param {Array} obsArrayOne Response from SensorThings API as array - * @param {Array} obsArrayTwo Response from SensorThings API as 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} 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) { - // 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 = getIndexOfMissingObservation( - missingTimestamp, - biggerObsTimestampArr - ); - - // Determine the larger observation array - const biggerObsArr = getLargerArrayBetweenTwoInputArrays( - obsArrayOne, - obsArrayTwo - ); - - // Remove the missing observation from the larger array of observations - // Modifies the array in place - removeMissingObservationFromLargerArray(indexesMissingObsArr, biggerObsArr); + const [obsArrayOneFinal, obsArrayTwoFinal] = + checkForAndDeleteUniqueObservationsFromLargerArray( + obsArrayOne, + obsArrayTwo + ); - return createCombinedObservationValues(obsArrayOne, obsArrayTwo); + return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal); } // When our observation arrays already have SAME lengths @@ -892,6 +990,93 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function ( } }; +/** + * 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 */ @@ -922,7 +1107,8 @@ const drawScatterPlotHCTest2 = async function () { }; (async () => { - await drawScatterPlotHCTest2(); + // await drawScatterPlotHCTest2(); + await drawHeatmapHCUsingTempDifference(); })(); export { @@ -937,7 +1123,7 @@ export { formatDatastreamMetadataForChart, formatSTAResponseForHeatMap, drawHeatMapHC, - formatSTAResponseForLineChartOrScatterPlot, + formatSTAResponseForLineChart, drawLineChartHC, getCombinedObservationsFromAllNextLinks, getMetadataPlusObservationsFromSingleDatastream, diff --git a/public/js/dropDownList.js b/public/js/dropDownList.js index ac3a750c2a202da17d00e82360584771320082ca..52817635b23a9d984a60d47cee77e0bb6abc4590 100644 --- a/public/js/dropDownList.js +++ b/public/js/dropDownList.js @@ -11,7 +11,7 @@ import { formatDatastreamMetadataForChart, formatSTAResponseForHeatMap, drawHeatMapHC, - formatSTAResponseForLineChartOrScatterPlot, + formatSTAResponseForLineChart, drawLineChartHC, getCombinedObservationsFromAllNextLinks, getMetadataPlusObservationsFromSingleDatastream, @@ -308,7 +308,7 @@ const selectChartTypeFromDropDown = async function () { if (selectedChartType === "Line") { drawLineChartHC( - formatSTAResponseForLineChartOrScatterPlot(combinedObs), + formatSTAResponseForLineChart(combinedObs), formatDatastreamMetadataForChart(datastreamMetadata) ); } else if (selectedChartType === "Heatmap") {