appChart.js 41.4 KB
Newer Older
Pithon Kabiro's avatar
Pithon Kabiro committed
1
"use strict";
Pithon Kabiro's avatar
Pithon Kabiro committed
2

3
const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1";
4

5
6
/**
 * Retrieve the datastream ID that corresponds to a particular building
7
8
9
 * @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
10
11
 * @returns {Number} Datastream corresponding to the input building
 */
12
const getDatastreamIdFromBuildingNumber = function (
13
14
15
16
17
18
19
20
  buildingNumber,
  phenomenon,
  samplingRate
) {
  const buildingToDatastreamMapping = {
    101: {
      vl: { "15min": "69", "60min": "75" },
      rl: { "15min": "81", "60min": "87" },
21
22
23
24
25
26

      // 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" },
27
    },
28

29
30
31
    102: {
      vl: { "15min": "70", "60min": "76" },
      rl: { "15min": "82", "60min": "88" },
32
33
34
35
36
37

      // 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" },
38
    },
39

40
41
42
    107: {
      vl: { "15min": "71", "60min": "77" },
      rl: { "15min": "83", "60min": "89" },
43
44
45
46
47
48

      // 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" },
49
    },
50

51
    "112, 118": {
52
53
      vl: { "15min": "72", "60min": "78" },
      rl: { "15min": "84", "60min": "90" },
54
55
56
57
58
59

      // 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" },
60
    },
61

62
63
64
    125: {
      vl: { "15min": "73", "60min": "79" },
      rl: { "15min": "85", "60min": "91" },
65
66
67
68
69
70

      // 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" },
71
    },
72

73
74
75
76
77
78
79
80
    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" },
    },
81
82
83
84

    weather_station_521: {
      outside_temp: { "15min": "141", "60min": "142" },
    },
85
86
  };

87
88
89
90
91
92
  if (
    buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[
      samplingRate
    ] === undefined
  )
    return;
93

94
  return Number(
95
96
97
98
    buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
  );
};

99
100
101
102
103
104
/**
 * 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
 */
105
const createDatastreamUrl = function (baseUrl, datastreamID) {
106
  if (!datastreamID) return;
107
  return `${baseUrl}/Datastreams(${datastreamID})`;
108
109
};

110
/**
111
 * Create URL to fetch Observations
112
 * @param {String} baseUrl Base URL of the STA server
113
114
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching Observations
115
 */
116
const createObservationsUrl = function (baseUrl, datastreamID) {
117
  if (!datastreamID) return;
118
  return `${baseUrl}/Datastreams(${datastreamID})/Observations`;
119
120
121
122
};

/**
 * Create a temporal filter string for the fetched Observations
123
124
125
 * @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
126
 */
127
const createTemporalFilterString = function (dateStart, dateStop) {
128
  if (!dateStart || !dateStop) return;
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
  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,
  };
150
151
};

152
const QUERY_PARAMS_COMBINED = createUrlParametersForGetRequest(
153
154
155
  "2020-01-01",
  "2021-01-01"
);
Pithon Kabiro's avatar
Pithon Kabiro committed
156

157
158
/**
 * Perform a GET request using the Axios library
159
 * @param {String} urlObservations A URL that fetches Observations from an STA instance
160
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
161
 * @returns {Promise} A promise that contains the first page of results when fulfilled
162
 */
163
const performGetRequestUsingAxios = function (urlObservations, urlParamObj) {
164
165
166
167
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};
168

169
170
171
172
173
174
/**
 * 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
 */
175
const getMetadataFromSingleDatastream = async function (urlDatastream) {
176
177
178
179
  try {
    // Extract properties of interest
    const {
      data: { description, name, unitOfMeasurement },
180
    } = await performGetRequestUsingAxios(urlDatastream);
181
182
183

    return { description, name, unitOfMeasurement };
  } catch (err) {
184
    console.error(err);
185
186
187
  }
};

188
189
/**
 * Retrieve metadata from multiple datastreams
190
 * @async
191
192
193
194
195
196
197
198
199
200
201
 * @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
202
203
204
      const datastreamMetadata = await getMetadataFromSingleDatastream(
        datastreamUrl
      );
205
206
207
208
209
210
211
212
213
      datastreamMetadataArr.push(datastreamMetadata);
    }

    return datastreamMetadataArr;
  } catch (err) {
    console.error(err);
  }
};

Pithon Kabiro's avatar
Pithon Kabiro committed
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
 * 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
};

245
246
247
248
249
/**
 * 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
 */
250
const formatDatastreamMetadataForChart = function (datastreamMetadata) {
251
252
253
254
255
256
257
  const {
    description: datastreamDescription,
    name: datastreamName,
    unitOfMeasurement,
  } = datastreamMetadata;

  // Extract phenomenon name from Datastream name
Pithon Kabiro's avatar
Pithon Kabiro committed
258
259
260
261
262
263
264
  const phenomenonName =
    extractPhenomenonNameFromDatastreamName(datastreamName);

  // Get the unitOfMeasurement's symbol
  const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol(
    unitOfMeasurement.symbol
  );
265
266
267
268
269
270
271
272
273

  return {
    datastreamDescription,
    datastreamName,
    phenomenonName,
    unitOfMeasurementSymbol,
  };
};

274
/**
275
 * Format the response from SensorThings API to make it suitable for use in a heatmap
Pithon Kabiro's avatar
Pithon Kabiro committed
276
 * @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API
277
 * @returns {Array} Array of formatted observations suitable for use in a heatmap
278
 */
279
const formatSensorThingsApiResponseForHeatMap = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
280
  if (!obsArray) return;
281
282

  const dataSTAFormatted = obsArray.map((obs) => {
283
284
285
286
287
288
289
290
291
292
293
294
295
    // 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];
296
    return [timestamp, hourOfDay, value];
297
  });
298

299
300
301
  return dataSTAFormatted;
};

Pithon Kabiro's avatar
Pithon Kabiro committed
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/**
 * 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 };
};

324
325
/**
 * Draw a heatmap using Highcharts library
326
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
327
 * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
328
 * @returns {undefined} undefined
329
 */
330
const drawHeatMapHighcharts = function (
331
  formattedObsArrayForHeatmap,
332
  formattedDatastreamMetadata
333
) {
334
335
336
337
338
339
340
  const {
    datastreamDescription: DATASTREAM_DESCRIPTION,
    datastreamName: DATASTREAM_NAME,
    phenomenonName: PHENOMENON_NAME,
    unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
  } = formattedDatastreamMetadata;

Pithon Kabiro's avatar
Pithon Kabiro committed
341
342
343
  const {
    minObsValue: MINIMUM_VALUE_COLOR_AXIS,
    maxObsValue: MAXIMUM_VALUE_COLOR_AXIS,
Pithon Kabiro's avatar
Pithon Kabiro committed
344
  } = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap);
Pithon Kabiro's avatar
Pithon Kabiro committed
345

346
347
348
349
350
351
352
353
354
355
356
  Highcharts.chart("chart-heatmap", {
    chart: {
      type: "heatmap",
      zoomType: "x",
    },

    boost: {
      useGPUTranslations: true,
    },

    title: {
357
      text: DATASTREAM_DESCRIPTION,
358
359
360
361
362
      align: "left",
      x: 40,
    },

    subtitle: {
363
      text: DATASTREAM_NAME,
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
      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"],
      ],
Pithon Kabiro's avatar
Pithon Kabiro committed
407
408
      min: MINIMUM_VALUE_COLOR_AXIS,
      max: MAXIMUM_VALUE_COLOR_AXIS,
409
410
411
      startOnTick: false,
      endOnTick: false,
      labels: {
412
413
        // format: "{value}℃",
        format: `{value}${PHENOMENON_SYMBOL}`,
414
415
416
417
418
      },
    },

    series: [
      {
419
        data: formattedObsArrayForHeatmap,
420
421
422
423
424
        boostThreshold: 100,
        borderWidth: 0,
        nullColor: "#525252",
        colsize: 24 * 36e5, // one day
        tooltip: {
425
426
          headerFormat: `${PHENOMENON_NAME}<br/>`,
          valueDecimals: 2,
427
          pointFormat:
428
429
            // "{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>`,
Pithon Kabiro's avatar
Pithon Kabiro committed
430
          nullFormat: `{point.x:%e %b, %Y} {point.y}:00: <b>null</b>`,
431
432
433
434
435
436
437
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
438
/**
439
 * Format the response from SensorThings API to make it suitable for use in a line chart
440
 * @param {Array} obsArray Response from SensorThings API as array
Pithon Kabiro's avatar
Pithon Kabiro committed
441
 * @returns {Array} Array of formatted observations suitable for use in a line chart
Pithon Kabiro's avatar
Pithon Kabiro committed
442
 */
443
const formatSensorThingsApiResponseForLineChart = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
444
  if (!obsArray) return;
445
446

  const dataSTAFormatted = obsArray.map((result) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
447
448
    const timestampObs = new Date(result[0].slice(0, -1)).getTime(); // slice() removes trailing "Z" character in timestamp
    const valueObs = result[1];
449
    return [timestampObs, valueObs];
Pithon Kabiro's avatar
Pithon Kabiro committed
450
  });
451

Pithon Kabiro's avatar
Pithon Kabiro committed
452
453
454
  return dataSTAFormatted;
};

455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
/**
 * 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
    };
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
539
540
/**
 * Draw a line chart using Highcharts library
541
542
 * @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
543
 * @returns {undefined} undefined
Pithon Kabiro's avatar
Pithon Kabiro committed
544
 */
545
const drawLineChartHighcharts = function (
546
547
  formattedObsArraysForLineChart,
  formattedDatastreamMetadataArr
548
) {
549
  // Arrays of datastream properties
550
  const {
551
552
553
554
555
556
557
558
559
560
561
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  } = extractPropertiesFromDatastreamMetadata(formattedDatastreamMetadataArr);

  // Create the array of series options object(s)
  const seriesOptionsArr = createSeriesOptionsForLineChart(
    formattedObsArraysForLineChart,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );
562

Pithon Kabiro's avatar
Pithon Kabiro committed
563
564
565
566
567
568
  Highcharts.stockChart("chart-line", {
    chart: {
      zoomType: "x",
    },

    rangeSelector: {
569
      selected: 5,
Pithon Kabiro's avatar
Pithon Kabiro committed
570
571
572
    },

    title: {
573
      text: createCombinedTextForLineChartTitles(phenomenonNamesArr),
574
575
576
577
      "align": "left",
    },

    subtitle: {
578
579
580
      text: `Sampling rate(s): ${createCombinedTextForLineChartTitles(
        extractSamplingRateFromDatastreamName(datastreamNamesArr)
      )}`,
581
      align: "left",
Pithon Kabiro's avatar
Pithon Kabiro committed
582
583
    },

584
585
586
587
588
589
590
    tooltip: {
      pointFormat:
        '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> <br/>',
      valueDecimals: 2,
    },

    series: seriesOptionsArr,
Pithon Kabiro's avatar
Pithon Kabiro committed
591
592
593
  });
};

594
595
596
597
598
599
600
601
602
603
604
605
606
607
/**
 * 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(
Pithon Kabiro's avatar
Pithon Kabiro committed
608
        (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
609
610
611
612
613
614
615
      )
    );

  return differenceBetweenArrays;
};

/**
Pithon Kabiro's avatar
Pithon Kabiro committed
616
617
618
 * 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
619
620
 * @returns {Array} An array of the indexes of the missing observations
 */
Pithon Kabiro's avatar
Pithon Kabiro committed
621
622
const getIndexesOfUniqueObservations = function (
  uniqueTimestampsArr,
623
624
  largerObsTimestampArr
) {
Pithon Kabiro's avatar
Pithon Kabiro committed
625
  const indexesMissingObs = uniqueTimestampsArr.map((index) =>
626
627
628
629
630
631
632
    largerObsTimestampArr.indexOf(index)
  );

  return indexesMissingObs;
};

/**
Pithon Kabiro's avatar
Pithon Kabiro committed
633
634
 * 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
635
636
 * @param {Array} largerObsArr The larger array of observations (timestamp + value)
 * @returns {Array} The larger array with the unique indexes removed
637
 */
Pithon Kabiro's avatar
Pithon Kabiro committed
638
639
const removeUniqueObservationsFromLargerArray = function (
  uniqueIndexesArr,
640
641
  largerObsArr
) {
642
643
644
645
646
  // 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;
Pithon Kabiro's avatar
Pithon Kabiro committed
647

648
  reversedUniqueIndexesArr.forEach((index) => {
649
    if (index > -1) {
650
      processedLargerObsArr.splice(index, 1);
651
652
    }
  });
653
654

  return processedLargerObsArr;
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
};

/**
 * 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;
};

Pithon Kabiro's avatar
Pithon Kabiro committed
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
/**
 * 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
733
734
735
736
  const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray(
    indexesMissingObsArr,
    biggerObsArr
  );
Pithon Kabiro's avatar
Pithon Kabiro committed
737

738
  return [modifiedBiggerObsArr, smallerObsArr];
Pithon Kabiro's avatar
Pithon Kabiro committed
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
};

/**
 * 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);
};

765
/**
766
 * Extracts and combines observation values from two input observation arrays of equal length
767
768
 * @param {Array} obsArrayOne First set of N observations (timestamp + value)
 * @param {Array} obsArrayTwo Second set of N observations (timestamp + value)
769
 * @returns {Array} A N*2 array of observation values from both input observation arrays
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
 */
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;
};

/**
785
 * Format the response from SensorThings API to make it suitable for use in a scatter plot
Pithon Kabiro's avatar
Pithon Kabiro committed
786
787
 * @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
788
789
 * @returns {Array} Array of formatted observations suitable for use in a scatter plot
 */
790
791
792
793
const formatSensorThingsApiResponseForScatterPlot = function (
  obsArrayOne,
  obsArrayTwo
) {
794
795
  // When our observation arrays have DIFFERENT lengths
  if (obsArrayOne.length !== obsArrayTwo.length) {
Pithon Kabiro's avatar
Pithon Kabiro committed
796
797
798
799
800
    const [obsArrayOneFinal, obsArrayTwoFinal] =
      checkForAndDeleteUniqueObservationsFromLargerArray(
        obsArrayOne,
        obsArrayTwo
      );
801

Pithon Kabiro's avatar
Pithon Kabiro committed
802
    return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal);
803
804
805
806
807
808
809
810
  }

  // When our observation arrays already have SAME lengths
  return createCombinedObservationValues(obsArrayOne, obsArrayTwo);
};

/**
 * Draw a scatter plot using Highcharts library
811
812
813
 * @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
814
815
 * @returns {undefined}
 */
816
const drawScatterPlotHighcharts = function (
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
  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,
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
937
/**
938
 * 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.
Pithon Kabiro's avatar
Pithon Kabiro committed
939
 * @async
940
941
 * @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
Pithon Kabiro's avatar
Pithon Kabiro committed
942
 */
943
944
945
946
947
const combineResultsFromAllPages = async function (httpGetRequestPromise) {
  try {
    if (!httpGetRequestPromise) return;

    const lastSuccess = await httpGetRequestPromise;
948
949
950

    // 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
951
952
953
954
    if (lastSuccess.data["@iot.nextLink"]) {
      const nextLinkSuccess = await combineResultsFromAllPages(
        axios.get(lastSuccess.data["@iot.nextLink"])
      );
955
956
      // The "data" object in turn has a "value" property
      // The "value" property's value is an array
957
958
959
960
961
962
963
964
965
966
      nextLinkSuccess.data.value = lastSuccess.data.value.concat(
        nextLinkSuccess.data.value
      );
      return nextLinkSuccess;
    } else {
      return lastSuccess;
    }
  } catch (err) {
    console.error(err);
  }
Pithon Kabiro's avatar
Pithon Kabiro committed
967
968
};

969
/**
970
 * Traverses all the pages that make up the response from a SensorThingsAPI instance and extracts the combined Observations
971
 * @async
972
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
973
 * @returns {Promise} A promise that contains an array of Observations when fulfilled
974
 */
975
const extractCombinedObservationsFromAllPages = async function (
976
  httpGetRequestPromise
977
) {
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
  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);
1000
    });
1001
1002
1003
  } catch (err) {
    console.error(err);
  }
1004
};
1005
1006
1007
1008

/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
1009
 * @param {Promise} observationPromiseArray An array that contains N observation promises
1010
1011
1012
1013
1014
 * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled
 */
const getObservationsFromMultipleDatastreams = async function (
  observationPromiseArray
) {
1015
1016
1017
  try {
    // Array to store our final result
    const observationsAllDatastreamsArr = [];
1018

1019
1020
    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const observationPromise of observationPromiseArray) {
1021
1022
1023
1024
      // Observations from a single Datastream
      const observations = await observationPromise;
      observationsAllDatastreamsArr.push(observations);
    }
1025
1026
1027
1028

    return observationsAllDatastreamsArr;
  } catch (err) {
    console.error(err);
1029
1030
  }
};
1031

1032
/**
1033
1034
1035
1036
1037
 * 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
1038
 */
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
const getMetadataPlusObservationsFromSingleOrMultipleDatastreams =
  async function (baseUrl, urlParamObj, bldgSensorSamplingRateArr) {
    try {
      if (!bldgSensorSamplingRateArr) return;

      // Datastreams IDs
      const datastreamsIdsArr = bldgSensorSamplingRateArr.map(
        (bldgSensorSamplingRate) =>
          getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRate)
      );
1049

1050
1051
1052
1053
      // Observations URLs
      const observationsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createObservationsUrl(baseUrl, datastreamId)
      );
1054

1055
1056
1057
1058
      // Datastreams URLs
      const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createDatastreamUrl(baseUrl, datastreamId)
      );
1059

1060
1061
1062
1063
1064
1065
      // Promise objects - Observations
      const observationsPromisesArr = observationsUrlArr.map((obsUrl) =>
        extractCombinedObservationsFromAllPages(
          performGetRequestUsingAxios(obsUrl, urlParamObj)
        )
      );
1066

1067
1068
1069
1070
      // Observations array
      const observationsArr = await getObservationsFromMultipleDatastreams(
        observationsPromisesArr
      );
1071

1072
1073
1074
1075
      // Metadata array
      const metadataArr = await getMetadataFromMultipleDatastreams(
        datastreamsUrlArr
      );
1076

1077
1078
1079
1080
1081
      return [observationsArr, metadataArr];
    } catch (err) {
      console.error(err);
    }
  };
1082

Pithon Kabiro's avatar
Pithon Kabiro committed
1083
1084
1085
1086
1087
/**
 * 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
1088
 * @returns {Promise} A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled
Pithon Kabiro's avatar
Pithon Kabiro committed
1089
1090
1091
1092
1093
 */
const calculateVorlaufMinusRuecklaufTemperature = async function (
  buildingId,
  samplingRate
) {
1094
1095
1096
1097
1098
  try {
    const bldgSensorSamplingRateArr = [
      [buildingId, "vl", samplingRate],
      [buildingId, "rl", samplingRate],
    ];
Pithon Kabiro's avatar
Pithon Kabiro committed
1099

1100
1101
    const BUILDING_ID = buildingId;
    const SAMPLING_RATE = samplingRate;
Pithon Kabiro's avatar
Pithon Kabiro committed
1102

1103
    const observationsPlusMetadata =
1104
1105
1106
      await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
        BASE_URL,
        QUERY_PARAMS_COMBINED,
1107
1108
        bldgSensorSamplingRateArr
      );
Pithon Kabiro's avatar
Pithon Kabiro committed
1109

1110
1111
1112
    // Extract Vorlauf temperature, Ruecklauf temperature and metadata
    const [[vorlaufTemp, ruecklaufTemp], [metadataVorlauf, metadataRuecklauf]] =
      observationsPlusMetadata;
Pithon Kabiro's avatar
Pithon Kabiro committed
1113

1114
1115
1116
    // Extract the temperature values
    const vorlaufTempValues = vorlaufTemp.map((obs) => obs[1]);
    const ruecklaufTempValues = ruecklaufTemp.map((obs) => obs[1]);
Pithon Kabiro's avatar
Pithon Kabiro committed
1117

1118
1119
1120
1121
1122
1123
    // 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],
    ]);
Pithon Kabiro's avatar
Pithon Kabiro committed
1124

1125
1126
1127
1128
1129
    // From Vorlauf metadata, extract `name` and `unitOfMeasurement`
    const {
      name: datastreamNameVorlauf,
      unitOfMeasurement: unitOfMeasurementVorlauf,
    } = metadataVorlauf;
Pithon Kabiro's avatar
Pithon Kabiro committed
1130

1131
1132
    // From Ruecklauf metadata, extract `name`
    const { name: datastreamNameRuecklauf } = metadataRuecklauf;
Pithon Kabiro's avatar
Pithon Kabiro committed
1133

1134
1135
1136
1137
1138
1139
1140
    // Extract the phenomenon names from the Datastream names
    const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName(
      datastreamNameVorlauf
    );
    const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName(
      datastreamNameRuecklauf
    );
Pithon Kabiro's avatar
Pithon Kabiro committed
1141

1142
1143
1144
1145
1146
1147
1148
1149
    // 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",
        ""
      );
Pithon Kabiro's avatar
Pithon Kabiro committed
1150

1151
1152
    // Create our custom datastream name text
    const nameTempDifference = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`;
Pithon Kabiro's avatar
Pithon Kabiro committed
1153

1154
1155
1156
1157
    // The datastream object that we return needs to have these property names
    const description = descriptionTempDifference;
    const name = nameTempDifference;
    const unitOfMeasurement = unitOfMeasurementVorlauf;
Pithon Kabiro's avatar
Pithon Kabiro committed
1158

1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
    return [
      vorlaufMinusRuecklaufTemp,
      {
        description,
        name,
        unitOfMeasurement,
      },
    ];
  } catch (err) {
    console.error(err);
  }
Pithon Kabiro's avatar
Pithon Kabiro committed
1170
1171
1172
1173
1174
1175
1176
1177
1178
};

/**
 * Test plotting of temp difference (dT) using heatmap
 */
const drawHeatmapHCUsingTempDifference = async function () {
  const [tempDifferenceObsArrBau225, tempDifferenceMetadataBau225] =
    await calculateVorlaufMinusRuecklaufTemperature("225", "60min");

1179
1180
  drawHeatMapHighcharts(
    formatSensorThingsApiResponseForHeatMap(tempDifferenceObsArrBau225),
Pithon Kabiro's avatar
Pithon Kabiro committed
1181
1182
1183
1184
    formatDatastreamMetadataForChart(tempDifferenceMetadataBau225)
  );
};

1185
1186
1187
/**
 * Test drawing of scatter plot chart
 */
1188
const drawScatterPlotHCTest2 = async function () {
1189
1190
  const sensorsOfInterestArr = [
    ["225", "vl", "60min"],
1191
1192
    // ["125", "rl", "60min"],
    ["weather_station_521", "outside_temp", "60min"],
1193
1194
1195
  ];

  const observationsPlusMetadata =
1196
1197
1198
    await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
      BASE_URL,
      QUERY_PARAMS_COMBINED,
1199
      sensorsOfInterestArr
1200
1201
    );

1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
  // Extract the combined arrays for observations and metadata
  const [observationsArr, metadataArr] = observationsPlusMetadata;

  // Create formatted array(s) for observations
  // This function expects two arguments, these are unpacked using the spread operator
  const formattedObsScatterPlotArr =
    formatSensorThingsApiResponseForScatterPlot(...observationsArr);

  // Create formatted array(s) for metadata
  const formattedMetadataArr = metadataArr.map((metadata) =>
    formatDatastreamMetadataForChart(metadata)
  );
1214

1215
  // This function expects three arguments, the second and third are unpacked using the spread operator
1216
  drawScatterPlotHighcharts(
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
    formattedObsScatterPlotArr,
    ...formattedMetadataArr
  );
};

/**
 * Test drawing of line chart with multiple series
 */
const testLineChartMultipleSeries = async function () {
  const sensorsOfInterestArr = [
    ["225", "vl", "60min"],
    ["125", "rl", "60min"],
    ["weather_station_521", "outside_temp", "60min"],
  ];

  const observationsPlusMetadata =
    await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
      BASE_URL,
      QUERY_PARAMS_COMBINED,
      sensorsOfInterestArr
    );

  // Extract the observations and metadata arrays
  const [observationsArr, metadataArr] = observationsPlusMetadata;

  // Format the observations and metadata
  const formattedObservationsArr = observationsArr.map((observations) =>
    formatSensorThingsApiResponseForLineChart(observations)
1245
  );
1246
1247
1248
1249
1250
1251

  const formattedMetadataArr = metadataArr.map((metadata) =>
    formatDatastreamMetadataForChart(metadata)
  );

  drawLineChartHighcharts(formattedObservationsArr, formattedMetadataArr);
1252
1253
};

1254
1255
1256
// drawScatterPlotHCTest2();
// drawHeatmapHCUsingTempDifference();
// testLineChartMultipleSeries()
1257

1258
1259
1260
1261
export {
  BASE_URL,
  QUERY_PARAMS_COMBINED,
  formatDatastreamMetadataForChart,
1262
1263
1264
1265
  formatSensorThingsApiResponseForHeatMap,
  drawHeatMapHighcharts,
  formatSensorThingsApiResponseForLineChart,
  drawLineChartHighcharts,
1266
  getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
1267
};