appChart.js 12.8 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

  const dataSTAFormatted = obsArray.map((obs) => {
144
145
146
147
148
149
150
151
152
153
154
155
156
    // 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];
157
    return [timestamp, hourOfDay, value];
158
  });
159

160
161
162
163
164
  return dataSTAFormatted;
};

/**
 * Draw a heatmap using Highcharts library
165
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
166
 * @returns {undefined} undefined
167
 */
168
export const drawHeatMapHC = function (formattedObsArrayForHeatmap) {
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
241
  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: [
      {
242
        data: formattedObsArrayForHeatmap,
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
        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
258
259
/**
 * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch
260
261
 * @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
262
 */
263
export const formatSTAResponseForLineChart = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
264
  if (!obsArray) return;
265
266

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

Pithon Kabiro's avatar
Pithon Kabiro committed
272
273
274
275
276
  return dataSTAFormatted;
};

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

    rangeSelector: {
      selected: 1,
    },

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

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

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

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

343
344
345
/**
 * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links
 * @async
346
347
 * @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
348
349
 */
export const getCombinedObservationsFromAllNextLinks = function (
350
  httpGetRequestPromise
351
) {
352
  return followNextLink(httpGetRequestPromise)
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
    .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);
    });
};
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

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

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