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

3
4
5
// DEBUG:
// Observations WITHOUT data gap - Bau 225 / Datastream ID = 80
// Observations WITH data gap - Bau 112 / Datastream ID = 78
6

7
export const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1";
8

9
10
/**
 * Retrieve the datastream ID that corresponds to a particular building
11
12
13
 * @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
14
15
 * @returns {Number} Datastream corresponding to the input building
 */
16
export const getDatastreamIdFromBuildingNumber = function (
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  buildingNumber,
  phenomenon,
  samplingRate
) {
  const buildingToDatastreamMapping = {
    101: {
      vl: { "15min": "69", "60min": "75" },
      rl: { "15min": "81", "60min": "87" },
      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" },
      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" },
      flow: { "15min": "95", "60min": "101" },
      power: { "15min": "107", "60min": "113" },
      energy: { "15min": "119", "60min": "125" },
      energy_verb: { "15min": "131", "60min": "137" },
    },
46
    "112, 118": {
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
      vl: { "15min": "72", "60min": "78" },
      rl: { "15min": "84", "60min": "90" },
      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" },
      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" },
    },
  };

72
73
74
75
76
  if (!buildingNumber) return;

  // check if building is contained in mapping object
  if (!(buildingNumber in buildingToDatastreamMapping)) return;

77
78
79
80
81
82
83
  const datastreamIdMatched = Number(
    buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
  );

  return datastreamIdMatched;
};

84
85
86
87
88
89
/**
 * 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
 */
90
export const getDatastreamUrl = function (baseUrl, datastreamID) {
91
92
93
94
95
  if (!datastreamID) return;
  const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})`;
  return fullDatastreamURL;
};

96
/**
97
 * Create URL to fetch Observations
98
 * @param {String} baseUrl Base URL of the STA server
99
100
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching Observations
101
 */
102
export const getObservationsUrl = function (baseUrl, datastreamID) {
103
  if (!datastreamID) return;
104
105
  const fullObservationsURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`;
  return fullObservationsURL;
106
107
108
109
};

/**
 * Create a temporal filter string for the fetched Observations
110
111
112
 * @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
113
 */
114
const createTemporalFilterString = function (dateStart, dateStop) {
115
116
117
118
119
  if (!dateStart || !dateStop) return;
  const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
  return filterString;
};

120
// const BASE_URL_OBSERVATIONS = getObservationsUrl(80);
121
122
123
124
125
126
127
128
129
130
131
132
const QUERY_PARAM_RESULT_FORMAT = "dataArray";
const QUERY_PARAM_ORDER_BY = "phenomenonTime asc";
const QUERY_PARAM_FILTER = createTemporalFilterString(
  "2020-01-01",
  "2021-01-01"
);
const QUERY_PARAM_SELECT = "result,phenomenonTime";
export const QUERY_PARAMS_COMBINED = {
  "$resultFormat": QUERY_PARAM_RESULT_FORMAT,
  "$orderBy": QUERY_PARAM_ORDER_BY,
  "$filter": QUERY_PARAM_FILTER,
  "$select": QUERY_PARAM_SELECT,
133
};
Pithon Kabiro's avatar
Pithon Kabiro committed
134

135
136
/**
 * Perform a GET request using the Axios library
137
 * @param {String} urlObservations A URL that fetches Observations from an STA instance
138
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
139
 * @returns {Promise} A promise that contains the first page of results when fulfilled
140
141
142
143
144
145
 */
export const axiosGetRequest = function (urlObservations, urlParamObj) {
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};
146

147
148
149
150
151
152
/**
 * 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
 */
153
export const getDatastreamMetadata = async function (urlDatastream) {
154
155
156
157
158
159
160
161
162
163
164
165
  try {
    // Extract properties of interest
    const {
      data: { description, name, unitOfMeasurement },
    } = await axiosGetRequest(urlDatastream);

    return { description, name, unitOfMeasurement };
  } catch (err) {
    console.log(err);
  }
};

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
 * 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
 */
export const formatDatastreamMetadataForChart = function (datastreamMetadata) {
  const {
    description: datastreamDescription,
    name: datastreamName,
    unitOfMeasurement,
  } = datastreamMetadata;

  // Extract phenomenon name from Datastream name
  const regex = /\/ (.*) DS/;
  const phenomenonName = datastreamName.match(regex)[1]; // use second element in array

  // Match the unitOfMeasurement's string representation of a symbol
  // to an actual symbol, where necessary
  const unitOfMeasurementSymbol = (() => {
    if (unitOfMeasurement.symbol === "degC") {
      return "";
    } else if (unitOfMeasurement.symbol === "m3/h") {
      return "m<sup>3</sup>/h";
    } else {
      return unitOfMeasurement.symbol;
    }
  })();

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

202
/**
203
 * Format the response from SensorThings API to make it suitable for heatmap
204
205
 * @param {Array} obsArray Response from SensorThings API as array
 * @returns {Array} Array of formatted observations suitable for use in a heatmap
206
 */
207
export const formatSTAResponseForHeatMap = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
208
  if (!obsArray) return;
209
210

  const dataSTAFormatted = obsArray.map((obs) => {
211
212
213
214
215
216
217
218
219
220
221
222
223
    // 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];
224
    return [timestamp, hourOfDay, value];
225
  });
226

227
228
229
230
231
  return dataSTAFormatted;
};

/**
 * Draw a heatmap using Highcharts library
232
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
233
 * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
234
 * @returns {undefined} undefined
235
 */
236
237
export const drawHeatMapHC = function (
  formattedObsArrayForHeatmap,
238
  formattedDatastreamMetadata
239
) {
240
241
242
243
244
245
246
  const {
    datastreamDescription: DATASTREAM_DESCRIPTION,
    datastreamName: DATASTREAM_NAME,
    phenomenonName: PHENOMENON_NAME,
    unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
  } = formattedDatastreamMetadata;

247
248
249
250
251
252
253
254
255
256
257
  Highcharts.chart("chart-heatmap", {
    chart: {
      type: "heatmap",
      zoomType: "x",
    },

    boost: {
      useGPUTranslations: true,
    },

    title: {
258
      text: DATASTREAM_DESCRIPTION,
259
260
261
262
263
      align: "left",
      x: 40,
    },

    subtitle: {
264
      text: DATASTREAM_NAME,
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
      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, 6, 12, 18, 24],
      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: 60,
      max: 85,
      startOnTick: false,
      endOnTick: false,
      labels: {
314
315
        // format: "{value}℃",
        format: `{value}${PHENOMENON_SYMBOL}`,
316
317
318
319
320
      },
    },

    series: [
      {
321
        data: formattedObsArrayForHeatmap,
322
323
324
325
326
        boostThreshold: 100,
        borderWidth: 0,
        nullColor: "#525252",
        colsize: 24 * 36e5, // one day
        tooltip: {
327
328
          headerFormat: `${PHENOMENON_NAME}<br/>`,
          valueDecimals: 2,
329
          pointFormat:
330
331
            // "{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>`,
332
333
334
335
336
337
338
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
339
340
/**
 * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch
341
342
 * @param {Array} obsArray Response from SensorThings API as array
 * @returns {Array} Array of formatted observations suitable for use in a line chart
Pithon Kabiro's avatar
Pithon Kabiro committed
343
 */
344
export const formatSTAResponseForLineChart = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
345
  if (!obsArray) return;
346
347

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

Pithon Kabiro's avatar
Pithon Kabiro committed
353
354
355
356
357
  return dataSTAFormatted;
};

/**
 * Draw a line chart using Highcharts library
358
 * @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart
359
 * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
360
 * @returns {undefined} undefined
Pithon Kabiro's avatar
Pithon Kabiro committed
361
 */
362
363
export const drawLineChartHC = function (
  formattedObsArrayForLineChart,
364
  formattedDatastreamMetadata
365
) {
366
367
368
369
370
371
372
  const {
    datastreamDescription: DATASTREAM_DESCRIPTION,
    datastreamName: DATASTREAM_NAME,
    phenomenonName: PHENOMENON_NAME,
    unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
  } = formattedDatastreamMetadata;

Pithon Kabiro's avatar
Pithon Kabiro committed
373
374
375
376
377
378
379
380
381
382
  Highcharts.stockChart("chart-line", {
    chart: {
      zoomType: "x",
    },

    rangeSelector: {
      selected: 1,
    },

    title: {
383
      text: DATASTREAM_DESCRIPTION,
384
385
386
387
      "align": "left",
    },

    subtitle: {
388
      text: DATASTREAM_NAME,
389
      align: "left",
Pithon Kabiro's avatar
Pithon Kabiro committed
390
391
392
393
    },

    series: [
      {
394
        name: `${PHENOMENON_NAME} (${PHENOMENON_SYMBOL})`,
395
        data: formattedObsArrayForLineChart,
Pithon Kabiro's avatar
Pithon Kabiro committed
396
397
398
399
400
401
402
403
404
        tooltip: {
          valueDecimals: 2,
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
405
/**
Pithon Kabiro's avatar
Pithon Kabiro committed
406
 * Follows "@iot.nextLink" links in SensorThingsAPI's response
Pithon Kabiro's avatar
Pithon Kabiro committed
407
 * Appends new results to existing results
Pithon Kabiro's avatar
Pithon Kabiro committed
408
 * @async
409
410
 * @param {Promise} responsePromise Promise object resulting from an Axios GET request
 * @returns {Object} Object containing results from all the "@iot.nextLink" links
Pithon Kabiro's avatar
Pithon Kabiro committed
411
 */
412
const followNextLink = function (responsePromise) {
Pithon Kabiro's avatar
Pithon Kabiro committed
413
  if (!responsePromise) return;
Pithon Kabiro's avatar
Pithon Kabiro committed
414
  return responsePromise
415
    .then((lastSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
416
417
418
      if (lastSuccess.data["@iot.nextLink"]) {
        return followNextLink(
          axios.get(lastSuccess.data["@iot.nextLink"])
419
        ).then((nextLinkSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
420
421
422
423
424
425
426
427
428
          nextLinkSuccess.data.value = lastSuccess.data.value.concat(
            nextLinkSuccess.data.value
          );
          return nextLinkSuccess;
        });
      } else {
        return lastSuccess;
      }
    })
429
    .catch((err) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
430
431
432
433
      console.log(err);
    });
};

434
435
436
/**
 * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links
 * @async
437
438
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an array of Observations from a single Datastream when fulfilled
439
440
 */
export const getCombinedObservationsFromAllNextLinks = function (
441
  httpGetRequestPromise
442
) {
443
  return followNextLink(httpGetRequestPromise)
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
    .then((success) => {
      const successValue = success.data.value;
      // Array that will hold the combined observations
      const combinedObservations = [];
      successValue.forEach((dataObj) => {
        // Each page of results will have a dataArray that holds the observations
        const dataArrays = dataObj.dataArray;
        combinedObservations.push(...dataArrays);
      });

      return new Promise((resolve, reject) => {
        resolve(combinedObservations);
      });
    })
    .catch((err) => {
      console.log(err);
    });
};
462

463
464
465
466
467
468
/**
 * Retrieve the metadata for a Datastream as well as the Observations corresponding to it
 * @async
 * @param {Promise} metadataPlusObsPromiseArray An array that contains two promises, one for datastream metadata, the other for observations
 * @returns {Promise} A promise that contains two arrays when fulfilled, one for datastream metadata and the other for observations
 */
469
export const getMetadataPlusObservationsForChart = async function (
470
  metadataPlusObsPromiseArray
471
) {
472
  // Array to store our final result
473
474
  const combinedResolvedPromises = [];

475
476
  // Use for/of loop - we need to maintain the order of execution of the async operations
  for (const promise of metadataPlusObsPromiseArray) {
477
    try {
478
      // Resolved value of a single promise
479
480
481
482
483
484
485
486
487
      const resolvedPromise = await promise;
      combinedResolvedPromises.push(resolvedPromise);
    } catch (err) {
      console.log(err);
    }
  }
  return combinedResolvedPromises;
};

488
489
490
/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
491
 * @param {Promise} observationPromiseArray An array that contains N observation promises
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
 * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled
 */
const getObservationsFromMultipleDatastreams = async function (
  observationPromiseArray
) {
  // 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) {
    try {
      // Observations from a single Datastream
      const observations = await observationPromise;
      observationsAllDatastreamsArr.push(observations);
    } catch (err) {
      console.log(err);
    }
  }
  return observationsAllDatastreamsArr;
};
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
539
540
541
542

// Building + phenomenon + sampling rate
const buildingsSensorSamplingRateRLArr = [
  ["101", "rl", "60min"],
  ["102", "rl", "60min"],
  ["107", "rl", "60min"],
  ["112, 118", "rl", "60min"],
  ["125", "rl", "60min"],
  ["225", "rl", "60min"],
];

// Datastreams IDs
const datastreamsRLArr = buildingsSensorSamplingRateRLArr.map((bldg) =>
  getDatastreamIdFromBuildingNumber(...bldg)
);

// Datastreams URLs
const datastreamsUrlRLArr = datastreamsRLArr.map((datastreamId) =>
  getObservationsUrl(BASE_URL, datastreamId)
);

// Promise objects - Observations / RL
const observationsPromisesRLArr = datastreamsUrlRLArr.map((obsUrl) =>
  getCombinedObservationsFromAllNextLinks(
    axiosGetRequest(obsUrl, QUERY_PARAMS_COMBINED)
  )
);

// getObservationsFromMultipleDatastreams(observationsPromisesRLArr).then((x) =>
//   console.log(x)
// );