appChart.js 14.4 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
/**
 * Create URL to fetch the Observations corresponding to a provided Datastream
86
 * @param {String} baseUrl Base URL of the STA server
87
88
 * @param {Number} datastreamID - Integer representing the Datastream ID
 * @returns {String} URL string for fetching the Observations corresponding to a Datastream
89
 */
90
export const getObservationsUrl = function (baseUrl, datastreamID) {
91
  if (!datastreamID) return;
92
  const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`;
93
94
95
96
97
  return fullDatastreamURL;
};

/**
 * Create a temporal filter string for the fetched Observations
98
99
100
 * @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
101
 */
102
const createTemporalFilterString = function (dateStart, dateStop) {
103
104
105
106
107
  if (!dateStart || !dateStop) return;
  const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
  return filterString;
};

108
// const BASE_URL_OBSERVATIONS = getObservationsUrl(80);
109
110
111
112
113
114
115
116
117
118
119
120
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,
121
};
Pithon Kabiro's avatar
Pithon Kabiro committed
122

123
124
125
126
/**
 * Perform a GET request using the Axios library
 * @param {String} urlObservations A URL that fetches Observations from STA instance
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
127
 * @returns {Promise} A promise that returns the first page of results
128
129
130
131
132
133
 */
export const axiosGetRequest = function (urlObservations, urlParamObj) {
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};
134
135

/**
136
 * Format the response from SensorThings API to make it suitable for heatmap
137
138
 * @param {Array} obsArray Response from SensorThings API as array
 * @returns {Array} Array of formatted observations suitable for use in a heatmap
139
 */
140
export const formatSTAResponseForHeatMap = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
141
  if (!obsArray) return;
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
  const dataSTAFormatted = [];
  obsArray.forEach((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];
    dataSTAFormatted.push([timestamp, hourOfDay, value]);
  });
  return dataSTAFormatted;
};

/**
 * Draw a heatmap using Highcharts library
164
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
165
 * @returns {undefined} undefined
166
 */
167
export const drawHeatMapHC = function (formattedObsArrayForHeatmap) {
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
202
203
204
205
206
207
208
209
210
211
212
213
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
  Highcharts.chart("chart-heatmap", {
    chart: {
      type: "heatmap",
      zoomType: "x",
    },

    boost: {
      useGPUTranslations: true,
    },

    title: {
      text: "Inlet flow (Vorlauf)",
      align: "left",
      x: 40,
    },

    subtitle: {
      text: "Temperature variation by day and hour in 2020",
      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: [
      {
241
        data: formattedObsArrayForHeatmap,
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
        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
257
258
/**
 * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch
259
260
 * @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
261
 */
262
export const formatSTAResponseForLineChart = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
263
  if (!obsArray) return;
Pithon Kabiro's avatar
Pithon Kabiro committed
264
265
266
267
268
269
270
271
272
273
274
  const dataSTAFormatted = [];
  obsArray.forEach((result) => {
    const timestampObs = new Date(result[0].slice(0, -1)).getTime(); // slice() removes trailing "Z" character in timestamp
    const valueObs = result[1];
    dataSTAFormatted.push([timestampObs, valueObs]);
  });
  return dataSTAFormatted;
};

/**
 * Draw a line chart using Highcharts library
275
276
 * @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart
 * @returns {undefined} undefined
Pithon Kabiro's avatar
Pithon Kabiro committed
277
 */
278
export const drawLineChartHC = function (formattedObsArrayForLineChart) {
Pithon Kabiro's avatar
Pithon Kabiro committed
279
280
281
282
283
284
285
286
287
288
289
  // Create the chart
  Highcharts.stockChart("chart-line", {
    chart: {
      zoomType: "x",
    },

    rangeSelector: {
      selected: 1,
    },

    title: {
290
291
292
293
294
295
296
      text: "Inlet flow (Vorlauf)",
      "align": "left",
    },

    subtitle: {
      text: "Temperature variation by hour in 2020",
      align: "left",
Pithon Kabiro's avatar
Pithon Kabiro committed
297
298
299
300
    },

    series: [
      {
301
        name: "Vorlauf",
302
        data: formattedObsArrayForLineChart,
Pithon Kabiro's avatar
Pithon Kabiro committed
303
304
305
306
307
308
309
310
311
        tooltip: {
          valueDecimals: 2,
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
312
/**
Pithon Kabiro's avatar
Pithon Kabiro committed
313
 * Follows "@iot.nextLink" links in SensorThingsAPI's response
Pithon Kabiro's avatar
Pithon Kabiro committed
314
 * Appends new results to existing results
Pithon Kabiro's avatar
Pithon Kabiro committed
315
 * @async
316
317
 * @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
318
 */
319
const followNextLink = function (responsePromise) {
Pithon Kabiro's avatar
Pithon Kabiro committed
320
  if (!responsePromise) return;
Pithon Kabiro's avatar
Pithon Kabiro committed
321
  return responsePromise
322
    .then((lastSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
323
324
325
      if (lastSuccess.data["@iot.nextLink"]) {
        return followNextLink(
          axios.get(lastSuccess.data["@iot.nextLink"])
326
        ).then((nextLinkSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
327
328
329
330
331
332
333
334
335
          nextLinkSuccess.data.value = lastSuccess.data.value.concat(
            nextLinkSuccess.data.value
          );
          return nextLinkSuccess;
        });
      } else {
        return lastSuccess;
      }
    })
336
    .catch((err) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
337
338
339
340
      console.log(err);
    });
};

341
342
343
/**
 * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links
 * @async
344
345
 * @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
346
347
 */
export const getCombinedObservationsFromAllNextLinks = function (
348
  httpGetRequestPromise
349
) {
350
  return followNextLink(httpGetRequestPromise)
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
    .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);
    });
};
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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
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

// Datastreams IDs
const building101_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "101",
  "rl",
  "60min"
);
const building102_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "102",
  "rl",
  "60min"
);
const building107_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "107",
  "rl",
  "60min"
);
const building112_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "112, 118",
  "rl",
  "60min"
);
const building125_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "125",
  "rl",
  "60min"
);
const building225_DatastreamId_RL = getDatastreamIdFromBuildingNumber(
  "225",
  "rl",
  "60min"
);

// Datastream URLs
const urlObservationsBuilding101_RL = getObservationsUrl(
  BASE_URL,
  building101_DatastreamId_RL
);
const urlObservationsBuilding102_RL = getObservationsUrl(
  BASE_URL,
  building102_DatastreamId_RL
);
const urlObservationsBuilding107_RL = getObservationsUrl(
  BASE_URL,
  building107_DatastreamId_RL
);
const urlObservationsBuilding112_RL = getObservationsUrl(
  BASE_URL,
  building112_DatastreamId_RL
);
const urlObservationsBuilding125_RL = getObservationsUrl(
  BASE_URL,
  building125_DatastreamId_RL
);
const urlObservationsBuilding225_RL = getObservationsUrl(
  BASE_URL,
  building225_DatastreamId_RL
);

// Promise objects - Observations / RL
const observationsPromise101_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding101_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromise102_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding102_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromise107_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding107_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromise112_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding112_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromise125_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding125_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromise225_RL = getCombinedObservationsFromAllNextLinks(
  axiosGetRequest(urlObservationsBuilding225_RL, QUERY_PARAMS_COMBINED)
);
const observationsPromiseCombined225_RL = [
  observationsPromise101_RL,
  observationsPromise102_RL,
  observationsPromise107_RL,
  observationsPromise112_RL,
  observationsPromise125_RL,
  observationsPromise225_RL,
];

/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
 * @param {Promise} observationPromiseArray A promise that contains an array of observations when fulfilled
 * @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;
};