appChart.js 15.1 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
 * Format the response from SensorThings API to make it suitable for heatmap
168
169
 * @param {Array} obsArray Response from SensorThings API as array
 * @returns {Array} Array of formatted observations suitable for use in a heatmap
170
 */
171
export const formatSTAResponseForHeatMap = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
172
  if (!obsArray) return;
173
174

  const dataSTAFormatted = obsArray.map((obs) => {
175
176
177
178
179
180
181
182
183
184
185
186
187
    // 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];
188
    return [timestamp, hourOfDay, value];
189
  });
190

191
192
193
194
195
  return dataSTAFormatted;
};

/**
 * Draw a heatmap using Highcharts library
196
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
197
 * @param {Object} datastreamMetadata Object containing Datastream metadata
198
 * @returns {undefined} undefined
199
 */
200
201
202
203
export const drawHeatMapHC = function (
  formattedObsArrayForHeatmap,
  datastreamMetadata
) {
204
205
206
207
208
209
210
211
212
213
214
  Highcharts.chart("chart-heatmap", {
    chart: {
      type: "heatmap",
      zoomType: "x",
    },

    boost: {
      useGPUTranslations: true,
    },

    title: {
215
216
      // text: "Inlet flow (Vorlauf)",
      text: `${datastreamMetadata.description}`,
217
218
219
220
221
      align: "left",
      x: 40,
    },

    subtitle: {
222
223
      // text: "Temperature variation by day and hour in 2020",
      text: `${datastreamMetadata.name}`,
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
      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: {
        format: "{value}℃",
      },
    },

    series: [
      {
279
        data: formattedObsArrayForHeatmap,
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
        boostThreshold: 100,
        borderWidth: 0,
        nullColor: "#525252",
        colsize: 24 * 36e5, // one day
        tooltip: {
          headerFormat: "Temperature<br/>",
          pointFormat:
            "{point.x:%e %b, %Y} {point.y}:00: <b>{point.value} ℃</b>",
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
295
296
/**
 * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch
297
298
 * @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
299
 */
300
export const formatSTAResponseForLineChart = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
301
  if (!obsArray) return;
302
303

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

Pithon Kabiro's avatar
Pithon Kabiro committed
309
310
311
312
313
  return dataSTAFormatted;
};

/**
 * Draw a line chart using Highcharts library
314
 * @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart
315
 * @param {Object} datastreamMetadata Object containing Datastream metadata
316
 * @returns {undefined} undefined
Pithon Kabiro's avatar
Pithon Kabiro committed
317
 */
318
319
320
321
export const drawLineChartHC = function (
  formattedObsArrayForLineChart,
  datastreamMetadata
) {
Pithon Kabiro's avatar
Pithon Kabiro committed
322
323
324
325
326
327
328
329
330
331
  Highcharts.stockChart("chart-line", {
    chart: {
      zoomType: "x",
    },

    rangeSelector: {
      selected: 1,
    },

    title: {
332
      // text: "Inlet flow (Vorlauf)",
333
      text: `${datastreamMetadata.description}`,
334
335
336
337
      "align": "left",
    },

    subtitle: {
338
      // text: "Temperature variation by hour in 2020",
339
      text: `${datastreamMetadata.name}`,
340
      align: "left",
Pithon Kabiro's avatar
Pithon Kabiro committed
341
342
343
344
    },

    series: [
      {
345
        name: "Vorlauf",
346
        data: formattedObsArrayForLineChart,
Pithon Kabiro's avatar
Pithon Kabiro committed
347
348
349
350
351
352
353
354
355
        tooltip: {
          valueDecimals: 2,
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
356
/**
Pithon Kabiro's avatar
Pithon Kabiro committed
357
 * Follows "@iot.nextLink" links in SensorThingsAPI's response
Pithon Kabiro's avatar
Pithon Kabiro committed
358
 * Appends new results to existing results
Pithon Kabiro's avatar
Pithon Kabiro committed
359
 * @async
360
361
 * @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
362
 */
363
const followNextLink = function (responsePromise) {
Pithon Kabiro's avatar
Pithon Kabiro committed
364
  if (!responsePromise) return;
Pithon Kabiro's avatar
Pithon Kabiro committed
365
  return responsePromise
366
    .then((lastSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
367
368
369
      if (lastSuccess.data["@iot.nextLink"]) {
        return followNextLink(
          axios.get(lastSuccess.data["@iot.nextLink"])
370
        ).then((nextLinkSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
371
372
373
374
375
376
377
378
379
          nextLinkSuccess.data.value = lastSuccess.data.value.concat(
            nextLinkSuccess.data.value
          );
          return nextLinkSuccess;
        });
      } else {
        return lastSuccess;
      }
    })
380
    .catch((err) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
381
382
383
384
      console.log(err);
    });
};

385
386
387
/**
 * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links
 * @async
388
389
 * @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
390
391
 */
export const getCombinedObservationsFromAllNextLinks = function (
392
  httpGetRequestPromise
393
) {
394
  return followNextLink(httpGetRequestPromise)
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
    .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);
    });
};
413

414
415
416
417
418
419
/**
 * 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
 */
420
export const getMetadataPlusObservationsForChart = async function (
421
  metadataPlusObsPromiseArray
422
) {
423
  // Array to store our final result
424
425
  const combinedResolvedPromises = [];

426
427
  // Use for/of loop - we need to maintain the order of execution of the async operations
  for (const promise of metadataPlusObsPromiseArray) {
428
    try {
429
      // Resolved value of a single promise
430
431
432
433
434
435
436
437
438
      const resolvedPromise = await promise;
      combinedResolvedPromises.push(resolvedPromise);
    } catch (err) {
      console.log(err);
    }
  }
  return combinedResolvedPromises;
};

439
440
441
/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
442
 * @param {Promise} observationPromiseArray An array that contains N observation promises
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
 * @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;
};
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

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