Commit 1c71824b authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Merge branch 'wip_aggregate-2' into 'master'

Improve logic for performing basic aggregation

See merge request !6
parents 4d484f25 92e0b3dd
......@@ -73,7 +73,6 @@
<script defer type="module" src="js/appCesium.js"></script>
<script defer type="module" src="js/appChart.js"></script>
<script defer type="module" src="js/dropDownList.js"></script>
<script defer type="module" src="js/aggregate.js"></script>
</head>
<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
......
This diff is collapsed.
"use strict";
import { BASE_URL, QUERY_PARAMS_COMBINED } from "./src_modules/createUrl.js";
import {
BASE_URL,
QUERY_PARAMS_COMBINED,
formatDatastreamMetadataForChart,
formatSensorThingsApiResponseForHeatMap,
drawHeatMapHighcharts,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
} from "./src_modules/fetchData.js";
import {
formatSensorThingsApiResponseForLineChart,
drawLineChartHighcharts,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
} from "./appChart.js";
} from "./src_modules/chartLine.js";
import {
formatSensorThingsApiResponseForHeatMap,
drawHeatMapHighcharts,
} from "./src_modules/chartHeatmap.js";
import {
showLoadingSpinner,
hideLoadingSpinner,
} from "./src_modules/loadingIndicator.js";
const buildingsAvailableSensorsArr = [
["--Select--", "", ""],
......@@ -228,30 +239,6 @@ const getBuildingSensorSamplingRateAbbreviation = function (
return [buildingAbbrev, phenomenonAbbrev, samplingRateAbbrev];
};
/**
* Show a loading indicator at the start of an async task. The indicator consists of a spinner and a transluscent mask placed on top of page elements
* @returns {undefined}
*/
const showLoadingSpinner = function () {
const loadingIndicatorMask = document.querySelector("#loadingIndicator");
const loadingIconSpinner = document.querySelector("#loadingIcon");
loadingIndicatorMask.style.display = "block";
loadingIconSpinner.style.display = "block";
};
/**
* Hide the loading indicator after completion of the async tasks
* @returns {undefined}
*/
const hideLoadingSpinner = function () {
const loadingIndicatorMask = document.querySelector("#loadingIndicator");
const loadingIconSpinner = document.querySelector("#loadingIcon");
loadingIndicatorMask.style.display = "none";
loadingIconSpinner.style.display = "none";
};
/**
* Callback function for chart selection using drop down list
* @returns {undefined}
......
"use strict";
import {
BASE_URL,
QUERY_PARAMS_COMBINED,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
} from "./appChart.js";
/**
* Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min)
* @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15 min", "60 min"
......@@ -49,6 +43,15 @@ const createIso8601DateTimeString = function (
return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`;
};
/**
* Check whether a year is a leap year or not
* @param {Number} year Integer representing the year
* @returns {Boolean} true if leap year, false if not
*/
const checkIfLeapYear = function (year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
};
/**
* Calculate the index of a timestamp in an array of timestamps
* @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
......@@ -71,14 +74,14 @@ const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) {
};
/**
* Aggregate observations within a time interval delimited by a start date and end date. The start date may be the same as the end date.
* Calculate the sum of observation values within a time interval delimited by a start date and end date. The start date may be the same as the end date.
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15 min", "60 min"
* @param {String} startDate A 24-hour date string representing the start date
* @param {String} endDate A 24-hour date string representing the end date
* @returns {Number} A floating-point number representing the aggregated observation value
* @returns {Number} A floating-point number representing the sum of observation values
*/
const aggregateObservationsWithinTimeInterval = function (
const calculateSumOfObservationValuesWithinDatesInterval = function (
obsArray,
samplingRate,
startDate,
......@@ -120,41 +123,112 @@ const aggregateObservationsWithinTimeInterval = function (
indexEndTimestamp + 1
);
// Calculate the aggregated observation value
const aggregatedObsValue = obsValuesForTimeIntervalArr.reduce(
return obsValuesForTimeIntervalArr.reduce(
(accumulator, currentValue) => accumulator + currentValue
);
return aggregatedObsValue;
};
/**
* Test aggregation of observations from a single datastream
* Calculate the sum of observation values within a time interval delimited by the start date and end date of a calendar month
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15 min", "60 min"
* @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
* @returns {Number} A floating-point number representing the sum of observation values
*/
const testAggregation = async function () {
const sensorOfInterestNestedArr = [["225", "vl", "60min"]];
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
QUERY_PARAMS_COMBINED,
sensorOfInterestNestedArr
const calculateSumOfObservationValuesWithinMonthInterval = function (
obsArray,
samplingRate,
calendarMonthStr
) {
// Extract year as integer
const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10);
// Extract month as integer
const monthNum = parseInt(calendarMonthStr.slice(-2), 10);
if (monthNum < 1 || monthNum > 12) return;
// All the months start on the first
const startDateStr = `${calendarMonthStr}-01`;
// February
if (monthNum === 2) {
// Leap year
if (checkIfLeapYear(yearNum))
return calculateSumOfObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-29`
);
// Non-leap year
return calculateSumOfObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-28`
);
}
// Extract the observations and metadata for each sensor
// Array elements in same order as input array
const [[obsSensorOneArr], [metadataSensorOne]] = observationsPlusMetadata;
// Aggregated observations
const observationsBau225VLAggregated =
aggregateObservationsWithinTimeInterval(
obsSensorOneArr,
"60 min",
"2020-02-01",
"2020-03-31"
// Months with 30 days
if (monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11)
return calculateSumOfObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-30`
);
// console.log(observationsBau225VLAggregated);
// Months with 31 days
return calculateSumOfObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-31`
);
};
/**
* Extract unique calendar dates from date/time strings in ISO 8601 format
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format
*/
const extractUniqueCalendarDatesFromTimestamp = function (obsArray) {
// The timestamp is the first element of the observation array
const timestampArr = obsArray.map((obs) => obs[0]);
// Extract the calendar date string from the timestamp string
const calendarDateArr = timestampArr.map((timestamp) =>
timestamp.slice(0, 10)
);
// Use a set to remove duplicates
const uniqueCalendarDates = new Set(calendarDateArr);
return [...uniqueCalendarDates];
};
// testAggregation();
/**
* Extract unique calendar months from calendar date strings
* @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format
* @returns {Array} An array of unique calendar month strings in "YYYY-MM" format
*/
const extractUniqueCalendarMonthsFromCalendarDates = function (
calendarDatesArr
) {
// Extract the calendar month strings
const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7));
// Use a set to remove duplicates
const uniqueCalendarMonths = new Set(calendarMonthsArr);
return [...uniqueCalendarMonths];
};
export {
calculateSumOfObservationValuesWithinDatesInterval,
calculateSumOfObservationValuesWithinMonthInterval,
extractUniqueCalendarDatesFromTimestamp,
extractUniqueCalendarMonthsFromCalendarDates,
};
"use strict";
/**
* Format the response from SensorThings API to make it suitable for use in a 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 formatSensorThingsApiResponseForHeatMap = 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 drawHeatMapHighcharts = 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}<br/>`,
valueDecimals: 2,
pointFormat:
// "{point.x:%e %b, %Y} {point.y}:00: <b>{point.value} ℃</b>",
`{point.x:%e %b, %Y} {point.y}:00: <b>{point.value} ${PHENOMENON_SYMBOL}</b>`,
nullFormat: `{point.x:%e %b, %Y} {point.y}:00: <b>null</b>`,
},
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
},
],
});
};
export { formatSensorThingsApiResponseForHeatMap, drawHeatMapHighcharts };
"use strict";
/**
* Format the response from SensorThings API to make it suitable for use in a line chart
* @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 line chart
*/
const formatSensorThingsApiResponseForLineChart = 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;
};
/**
* Extract the properties that make up the formatted datastream metadata object(s)
* @param {Array} formattedDatastreamsMetadataArr An array of formatted metadata object(s) from one or more datastreams
* @returns {Object} An object that contains array(s) of formatted datastream metadata properties
*/
const extractPropertiesFromDatastreamMetadata = function (
formattedDatastreamsMetadataArr
) {
// Create arrays from the properties of the formatted datastream metadata
const datastreamDescriptionsArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.datastreamDescription
);
const datastreamNamesArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.datastreamName
);
const phenomenonNamesArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.phenomenonName
);
const unitOfMeasurementSymbolsArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.unitOfMeasurementSymbol
);
return {
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
};
};
/**
* Extracts the sampling rate substring from a datastream name string
* @param {Array} datastreamNamesArr An array of datastream name(s)
* @returns {Array} An array containing the sampling rate substring(s)
*/
const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) {
// The sampling rate string is the last word in the Datastream name string
return datastreamNamesArr.map((datastreamName) =>
datastreamName.split(" ").pop()
);
};
/**
* Concatenates metadata properties to create a string for either the title or subtitle of a line chart
* @param {Array} datastreamMetadataPropArr An array of metadata property strings
* @returns {String} A string of comma separated metadata property strings
*/
const createCombinedTextForLineChartTitles = function (
datastreamMetadataPropArr
) {
return datastreamMetadataPropArr.join(", ");
};
/**
* Creates an options object for each series drawn in the line chart
* @param {Array} formattedObsArraysForLineChart An array of formatted observation array(s) from one or more datastreams
* @param {Array} phenomenonNamesArr An array of phenomenon name(s)
* @param {Array} phenomenonSymbolsArr An array of phenomenon symbol(s)
* @returns {Array} An array made up of series options object(s)
*/
const createSeriesOptionsForLineChart = function (
formattedObsArraysForLineChart,
phenomenonNamesArr,
phenomenonSymbolsArr
) {
// An array of colors provided by the Highcharts object
const seriesColors = Highcharts.getOptions().colors;
// Create an array of seriesOptions objects
// Assumes that the observation array of arrays, phenomenon names array and phenomenon symbols array are of equal length
// Use one of the arrays for looping
return formattedObsArraysForLineChart.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesArr[i]} (${phenomenonSymbolsArr[i]})`,
data: formattedObsArray,
color: seriesColors[i],
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
};
});
};
/**
* Draw a line chart using Highcharts library
* @param {Array} formattedObsArraysForLineChart An array made up of formatted observation array(s) suitable for use in a line chart
* @param {Object} formattedDatastreamMetadataArr An array made up of object(s) containing Datastream metadata
* @returns {undefined} undefined
*/
const drawLineChartHighcharts = function (
formattedObsArraysForLineChart,
formattedDatastreamMetadataArr
) {
// Arrays of datastream properties
const {
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractPropertiesFromDatastreamMetadata(formattedDatastreamMetadataArr);
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForLineChart(
formattedObsArraysForLineChart,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
Highcharts.stockChart("chart-line", {
chart: {
zoomType: "x",
},
rangeSelector: {
selected: 5,
},
title: {
text: createCombinedTextForLineChartTitles(phenomenonNamesArr),
"align": "left",
},
subtitle: {
text: `Sampling rate(s): ${createCombinedTextForLineChartTitles(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`,
align: "left",
},
tooltip: {
pointFormat:
'<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> <br/>',
valueDecimals: 2,
},
series: seriesOptionsArr,
});
};
export { formatSensorThingsApiResponseForLineChart, drawLineChartHighcharts };
"use strict";
/**
* 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)
* @returns {Array} The larger array with the unique indexes removed
*/
const removeUniqueObservationsFromLargerArray = function (
uniqueIndexesArr,
largerObsArr
) {
// Create a reversed copy of the indexes array, so that the larger index is removed first
const reversedUniqueIndexesArr = uniqueIndexesArr.reverse();
// Create a copy the larger observation array, will be modified in place
const processedLargerObsArr = largerObsArr;
reversedUniqueIndexesArr.forEach((index) => {
if (index > -1) {
processedLargerObsArr.splice(index, 1);
}
});
return processedLargerObsArr;
};
/**
* 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
const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray(
indexesMissingObsArr,
biggerObsArr
);
return [modifiedBiggerObsArr, 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 input 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 N*2 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 use in a 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 formatSensorThingsApiResponseForScatterPlot = 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 {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot
* @param {Object} formattedDatastreamMetadataSeriesOne Object containing Datastream metadata for the first chart series
* @param {Object} formattedDatastreamMetadataSeriesTwo Object containing Datastream metadata for the second chart series
* @returns {undefined}
*/
const drawScatterPlotHighcharts = 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}<br>",
pointFormat: `<b>{point.y:.2f} ${SERIES_1_SYMBOL}, {point.x:.2f} ${SERIES_2_SYMBOL}</b>`,
},
},
},
series: [
{
name: SERIES_COMBINED_NAME,
color: SERIES_COMBINED_SYMBOL_COLOR,
data: formattedObsArrayForSeriesOnePlusSeriesTwo,
},
],
});
};
export {
formatSensorThingsApiResponseForScatterPlot,
drawScatterPlotHighcharts,
};
"use strict";
const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1";
/**
* 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;
return `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
};
/**
* Create a query parameter object that should be sent together with a HTTP GET request using the Axios library
* @param {String} dateStart Start date (for temporal filter) in YYYY-MM-DD format
* @param {String} dateStop Stop date (for temporal filter) in YYYY-MM-DD format
* @returns {Object} A query parameter object
*/
const createUrlParametersForGetRequest = function (dateStart, dateStop) {
const QUERY_PARAM_RESULT_FORMAT = "dataArray";
const QUERY_PARAM_ORDER_BY = "phenomenonTime asc";
const QUERY_PARAM_FILTER = createTemporalFilterString(dateStart, dateStop);
const QUERY_PARAM_SELECT = "result,phenomenonTime";
return {
"$resultFormat": QUERY_PARAM_RESULT_FORMAT,
"$orderBy": QUERY_PARAM_ORDER_BY,
"$filter": QUERY_PARAM_FILTER,
"$select": QUERY_PARAM_SELECT,
};
};
const QUERY_PARAMS_COMBINED = createUrlParametersForGetRequest(
"2020-01-01",
"2021-01-01"
);
export { BASE_URL, QUERY_PARAMS_COMBINED };
"use strict";
/**
* 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;
return Number(
buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
);
};
/**
* 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 createDatastreamUrl = function (baseUrl, datastreamID) {
if (!datastreamID) return;
return `${baseUrl}/Datastreams(${datastreamID})`;
};
/**
* 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 createObservationsUrl = function (baseUrl, datastreamID) {
if (!datastreamID) return;
return `${baseUrl}/Datastreams(${datastreamID})/Observations`;
};
/**
* 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 performGetRequestUsingAxios = 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 performGetRequestUsingAxios(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,
};
};
/**
* Traverses all the pages that make up the response from a SensorThingsAPI instance. The link to the next page, if present, is denoted by the presence of a "@iot.nextLink" property in the response object. This function concatenates all the values so that the complete results are returned in one array.
* @async
* @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
* @returns {Promise} A promise that contains an object containing results from all the pages when fulfilled
*/
const combineResultsFromAllPages = async function (httpGetRequestPromise) {
try {
if (!httpGetRequestPromise) return;
const lastSuccess = await httpGetRequestPromise;
// The "success" objects contain a "data" object which in turn has a "value" property
// If the "data" object in turn has a "@iot.nextLink" property, then a next page exists
if (lastSuccess.data["@iot.nextLink"]) {
const nextLinkSuccess = await combineResultsFromAllPages(
axios.get(lastSuccess.data["@iot.nextLink"])
);
// The "data" object in turn has a "value" property
// The "value" property's value is an array
nextLinkSuccess.data.value = lastSuccess.data.value.concat(
nextLinkSuccess.data.value
);
return nextLinkSuccess;
} else {
return lastSuccess;
}
} catch (err) {
console.error(err);
}
};
/**
* Traverses all the pages that make up the response from a SensorThingsAPI instance and extracts the combined Observations
* @async
* @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
* @returns {Promise} A promise that contains an array of Observations when fulfilled
*/
const extractCombinedObservationsFromAllPages = async function (
httpGetRequestPromise
) {
try {
const successResponse = await combineResultsFromAllPages(
httpGetRequestPromise
);
// Extract value array from the success response object
const {
data,
data: { value: valueArr },
} = successResponse;
// Array that will hold the combined observations
const combinedObservations = [];
valueArr.forEach((val) => {
// Each page of results will have a dataArray that holds the observations
const { dataArray } = val;
combinedObservations.push(...dataArray);
});
return new Promise((resolve, reject) => {
resolve(combinedObservations);
});
} 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 a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s)
* @param {String} baseUrl Base URL of the STA server
* @param {Object} urlParamObj The URL parameters to be sent together with the GET request
* @param {Array} bldgSensorSamplingRateArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc
* @returns {Promise} A promise that contains a 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) when fulfilled
*/
const getMetadataPlusObservationsFromSingleOrMultipleDatastreams =
async function (baseUrl, urlParamObj, bldgSensorSamplingRateArr) {
try {
if (!bldgSensorSamplingRateArr) return;
// Datastreams IDs
const datastreamsIdsArr = bldgSensorSamplingRateArr.map(
(bldgSensorSamplingRate) =>
getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRate)
);
// Observations URLs
const observationsUrlArr = datastreamsIdsArr.map((datastreamId) =>
createObservationsUrl(baseUrl, datastreamId)
);
// Datastreams URLs
const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) =>
createDatastreamUrl(baseUrl, datastreamId)
);
// Promise objects - Observations
const observationsPromisesArr = observationsUrlArr.map((obsUrl) =>
extractCombinedObservationsFromAllPages(
performGetRequestUsingAxios(obsUrl, urlParamObj)
)
);
// Observations array
const observationsArr = await getObservationsFromMultipleDatastreams(
observationsPromisesArr
);
// Metadata array
const metadataArr = await getMetadataFromMultipleDatastreams(
datastreamsUrlArr
);
return [observationsArr, metadataArr];
} catch (err) {
console.error(err);
}
};
/**
* Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL)
* @param {String} baseUrl Base URL of the STA server
* @param {Object} urlParams The URL parameters to be sent together with the GET request
* @param {String} buildingId The building ID as a string
* @param {String} samplingRate The sampling rate as a string
* @returns {Promise} 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 (
baseUrl,
urlParams,
buildingId,
samplingRate
) {
try {
const bldgSensorSamplingRateArr = [
[buildingId, "vl", samplingRate],
[buildingId, "rl", samplingRate],
];
const BUILDING_ID = buildingId;
const SAMPLING_RATE = samplingRate;
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
baseUrl,
urlParams,
bldgSensorSamplingRateArr
);
// Extract Vorlauf temperature, Ruecklauf temperature and metadata
const [[vorlaufTemp, ruecklaufTemp], [metadataVorlauf, metadataRuecklauf]] =
observationsPlusMetadata;
// Extract the temperature values
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: unitOfMeasurementVorlauf,
} = metadataVorlauf;
// From Ruecklauf metadata, extract `name`
const { name: datastreamNameRuecklauf } = metadataRuecklauf;
// Extract the phenomenon names from the Datastream names
const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName(
datastreamNameVorlauf
);
const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName(
datastreamNameRuecklauf
);
// Create our custom datastream description text
// The resulting datastream description string has two `temperature` substrings;
// replace the first occurence with an empty string
const descriptionTempDifference =
`Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`.replace(
"temperature",
""
);
// Create our custom datastream name text
const nameTempDifference = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`;
// The datastream object that we return needs to have these property names
const description = descriptionTempDifference;
const name = nameTempDifference;
const unitOfMeasurement = unitOfMeasurementVorlauf;
return [
vorlaufMinusRuecklaufTemp,
{
description,
name,
unitOfMeasurement,
},
];
} catch (err) {
console.error(err);
}
};
export {
formatDatastreamMetadataForChart,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
calculateVorlaufMinusRuecklaufTemperature,
};
"use strict";
/**
* Show a loading indicator at the start of an async task. The indicator consists of a spinner and a transluscent mask placed on top of page elements
* @returns {undefined}
*/
const showLoadingSpinner = function () {
const loadingIndicatorMask = document.querySelector("#loadingIndicator");
const loadingIconSpinner = document.querySelector("#loadingIcon");
loadingIndicatorMask.style.display = "block";
loadingIconSpinner.style.display = "block";
};
/**
* Hide the loading indicator after completion of the async tasks
* @returns {undefined}
*/
const hideLoadingSpinner = function () {
const loadingIndicatorMask = document.querySelector("#loadingIndicator");
const loadingIconSpinner = document.querySelector("#loadingIcon");
loadingIndicatorMask.style.display = "none";
loadingIconSpinner.style.display = "none";
};
export { showLoadingSpinner, hideLoadingSpinner };
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment