From 86bff05081821f87c972fee28cd485a19dafcb65 Mon Sep 17 00:00:00 2001 From: Sven Schneider <icedoggy@gmx.de> Date: Fri, 25 Jun 2021 18:14:37 +0200 Subject: [PATCH] added aggregation method into appCharts.js --- index.html | 199 ++++++------- public/js/appCesium.js | 523 ++++++++++++++++---------------- public/js/appChart.js | 662 ++++++++++++++++++++++++++--------------- 3 files changed, 770 insertions(+), 614 deletions(-) diff --git a/index.html b/index.html index ba2082b..96efb39 100644 --- a/index.html +++ b/index.html @@ -1,27 +1,18 @@ <!DOCTYPE html> <html lang="en"> - <head> + +<head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1, shrink-to-fit=no" - /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="description" content="" /> <meta name="author" content="" /> <title>Dashboard - iCity Bosch</title> <link href="css/styles.css" rel="stylesheet" /> - <link - href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" - rel="stylesheet" - crossorigin="anonymous" - /> + <link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" /> <!-- Font Awesome icons --> - <script - src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" - crossorigin="anonymous" - ></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" crossorigin="anonymous"></script> <!-- Axios --> <!-- <script src="./node_modules/axios/dist/axios.min.js"></script> --> @@ -46,122 +37,100 @@ <!-- Cesium lib --> <script src="https://cesium.com/downloads/cesiumjs/releases/1.48/Build/Cesium/Cesium.js"></script> - <link - href="https://cesium.com/downloads/cesiumjs/releases/1.48/Build/Cesium/Widgets/widgets.css" - rel="stylesheet" - /> + <link href="https://cesium.com/downloads/cesiumjs/releases/1.48/Build/Cesium/Widgets/widgets.css" rel="stylesheet" /> <!-- Bootstrap dashboard template --> - <script - defer - src="https://code.jquery.com/jquery-3.5.1.slim.min.js" - crossorigin="anonymous" - ></script> - <script - defer - src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" - crossorigin="anonymous" - ></script> + <script defer src="https://code.jquery.com/jquery-3.5.1.slim.min.js" crossorigin="anonymous"></script> + <script defer src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script> <script defer src="js/thirdparty/scripts.js"></script> <!-- Custom JS --> <script defer type="module" src="js/appCesium.js"></script> <script defer type="module" src="js/appChart.js"></script> - </head> - <body class="sb-nav-fixed"> +</head> + +<body class="sb-nav-fixed"> <nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark"> - <a class="navbar-brand" href="index.html">iCity Bosch Dashboard</a> - <button - class="btn btn-link btn-sm order-1 order-lg-0" - id="sidebarToggle" - href="#" - > + <a class="navbar-brand" href="index.html">iCity Bosch Dashboard</a> + <button class="btn btn-link btn-sm order-1 order-lg-0" id="sidebarToggle" href="#"> <i class="fas fa-bars"></i> </button> </nav> <div id="layoutSidenav"> - <div id="layoutSidenav_nav"> - <nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion"> - <div class="sb-sidenav-menu"> - <div class="nav"> - <div class="sb-sidenav-menu-heading">Core</div> - <a class="nav-link" href="index.html"> - <div class="sb-nav-link-icon"> - <i class="fas fa-tachometer-alt"></i> + <div id="layoutSidenav_nav"> + <nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion"> + <div class="sb-sidenav-menu"> + <div class="nav"> + <div class="sb-sidenav-menu-heading">Core</div> + <a class="nav-link" href="index.html"> + <div class="sb-nav-link-icon"> + <i class="fas fa-tachometer-alt"></i> + </div> + Dashboard + </a> + </div> </div> - Dashboard - </a> - </div> - </div> - </nav> - </div> - <div id="layoutSidenav_content"> - <main> - <div class="container-fluid"> - <h1 class="mt-4">Dashboard</h1> - <ol class="breadcrumb mb-4"> - <li class="breadcrumb-item active">Dashboard</li> - </ol> + </nav> + </div> + <div id="layoutSidenav_content"> + <main> + <div class="container-fluid"> + <h1 class="mt-4">Dashboard</h1> + <ol class="breadcrumb mb-4"> + <li class="breadcrumb-item active">Dashboard</li> + </ol> - <div class="row"> - <div class="col-xl-12"> - <div class="card mb-4"> - <div class="card-header"> - <i class="fas fa-globe mr-1"></i> - 3D Visualization - </div> - <div class="card-body"> - <div - id="cesiumGlobeContainer" - width="100%" - height="40" - ></div> - </div> + <div class="row"> + <div class="col-xl-12"> + <div class="card mb-4"> + <div class="card-header"> + <i class="fas fa-globe mr-1"></i> 3D Visualization + </div> + <div class="card-body"> + <div id="cesiumGlobeContainer" width="100%" height="40"> + </div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-xl-6"> + <div class="card mb-4"> + <div class="card-header"> + <i class="fas fa-chart-line mr-1"></i> Line Chart Example + </div> + <div class="card-body"> + <div id="chart-line" width="100%" height="40"></div> + </div> + </div> + </div> + <div class="col-xl-6"> + <div class="card mb-4"> + <div class="card-header"> + <i class="fas fa-chart-area mr-1"></i> Area Chart Example + </div> + <div class="card-body"> + <div id="chart-heatmap" width="100%" height="40"></div> + </div> + </div> + </div> + </div> </div> - </div> - </div> - <div class="row"> - <div class="col-xl-6"> - <div class="card mb-4"> - <div class="card-header"> - <i class="fas fa-chart-line mr-1"></i> - Line Chart Example - </div> - <div class="card-body"> - <div id="chart-line" width="100%" height="40"></div> - </div> + </main> + <footer class="py-4 bg-light mt-auto"> + <div class="container-fluid"> + <div class="d-flex align-items-center justify-content-between small"> + <div class="text-muted">Copyright © HfT Stuttgart 2021</div> + <div> + <a href="#">Privacy Policy</a> · + <a href="#">Terms & Conditions</a> + </div> + </div> </div> - </div> - <div class="col-xl-6"> - <div class="card mb-4"> - <div class="card-header"> - <i class="fas fa-chart-area mr-1"></i> - Area Chart Example - </div> - <div class="card-body"> - <div id="chart-heatmap" width="100%" height="40"></div> - </div> - </div> - </div> - </div> - </div> - </main> - <footer class="py-4 bg-light mt-auto"> - <div class="container-fluid"> - <div - class="d-flex align-items-center justify-content-between small" - > - <div class="text-muted">Copyright © HfT Stuttgart 2021</div> - <div> - <a href="#">Privacy Policy</a> - · - <a href="#">Terms & Conditions</a> - </div> - </div> - </div> - </footer> - </div> + </footer> + </div> </div> - </body> -</html> +</body> + +</html> \ No newline at end of file diff --git a/public/js/appCesium.js b/public/js/appCesium.js index d49aa6e..ce706bb 100644 --- a/public/js/appCesium.js +++ b/public/js/appCesium.js @@ -2,26 +2,27 @@ // Functions import { - getDatastreamIdFromBuildingNumber, - getObservationsUrl, - createTemporalFilterString, - formatSTAResponseForHeatMap, - drawHeatMapHC, - formatSTAResponseForLineChart, - drawLineChartHC, - followNextLink, + getDatastreamIdFromBuildingNumber, + getObservationsUrl, + createTemporalFilterString, + formatSTAResponseForHeatMap, + drawHeatMapHC, + formatSTAResponseForLineChart, + drawLineChartHC, + followNextLink, + aggregateResponse, } from "./appChart.js"; // Constants import { - BASE_URL, - PARAM_RESULT_FORMAT, - PARAM_ORDER_BY, - PARAM_SELECT, + BASE_URL, + PARAM_RESULT_FORMAT, + PARAM_ORDER_BY, + PARAM_SELECT, } from "./appChart.js"; Cesium.Ion.defaultAccessToken = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyODgxYzJlNi1kNDZiLTQ3ZmQtYmUxYy0yMWI0OGM3NDA5MzAiLCJpZCI6NDczOSwic2NvcGVzIjpbImFzciIsImdjIl0sImlhdCI6MTU0MTUyMzU0MX0.shj2hM3pvsvcmE_wMb2aBDuk_cKWmFmbolltInGImwU"; + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyODgxYzJlNi1kNDZiLTQ3ZmQtYmUxYy0yMWI0OGM3NDA5MzAiLCJpZCI6NDczOSwic2NvcGVzIjpbImFzciIsImdjIl0sImlhdCI6MTU0MTUyMzU0MX0.shj2hM3pvsvcmE_wMb2aBDuk_cKWmFmbolltInGImwU"; // Flag to determine models that will be loaded // Set to `true` or `false` @@ -29,10 +30,10 @@ const LOAD_DETAILED_BLDG225 = false; // Global variable const viewer = new Cesium.Viewer("cesiumGlobeContainer", { - scene3DOnly: true, - imageryProvider: Cesium.createOpenStreetMapImageryProvider({ - url: "https://a.tile.openstreetmap.org/", - }), + scene3DOnly: true, + imageryProvider: Cesium.createOpenStreetMapImageryProvider({ + url: "https://a.tile.openstreetmap.org/", + }), }); /** @@ -40,26 +41,25 @@ const viewer = new Cesium.Viewer("cesiumGlobeContainer", { * @param {String} urlTiles URL to the 3DTiles to be loaded * @returns {undefined} undefined */ -const loadTiles = function (urlTiles) { - const tileset = new Cesium.Cesium3DTileset({ - url: urlTiles, - }); - viewer.scene.primitives.add(tileset); - - tileset.readyPromise.then(function () { - viewer - .zoomTo( - tileset, - new Cesium.HeadingPitchRange( - 0.0, - -0.5, - tileset.boundingSphere.radius / 0.5 - ) - ) - .otherwise(function (err) { - throw err; - }); - }); +const loadTiles = function(urlTiles) { + const tileset = new Cesium.Cesium3DTileset({ + url: urlTiles, + }); + viewer.scene.primitives.add(tileset); + + tileset.readyPromise.then(function() { + viewer + .zoomTo( + tileset, + new Cesium.HeadingPitchRange( + 0.0, -0.5, + tileset.boundingSphere.radius / 0.5 + ) + ) + .otherwise(function(err) { + throw err; + }); + }); }; /** @@ -67,12 +67,12 @@ const loadTiles = function (urlTiles) { * @param{*} * @returns {undefined} undefined */ -const loadNonDetailed = function () { - // Paths to data sources - const URL_3DTILES = "data_3d/3dtiles/1_full/tileset.json"; +const loadNonDetailed = function() { + // Paths to data sources + const URL_3DTILES = "data_3d/3dtiles/1_full/tileset.json"; - // Tileset with all buildings - loadTiles(URL_3DTILES); + // Tileset with all buildings + loadTiles(URL_3DTILES); }; /** @@ -81,19 +81,19 @@ const loadNonDetailed = function () { * @param {String} gltfId Name of the glTF model file without the extension i.e. exclude the `.gltf` suffix * @returns {undefined} undefined */ -const gltfLoad = function (gltfUrl, gltfId) { - const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame( - Cesium.Cartesian3.fromDegrees(9.083385, 48.881342, 0) - ); - - viewer.scene.primitives.add( - Cesium.Model.fromGltf({ - url: `${gltfUrl}/${gltfId}.gltf`, - modelMatrix: modelMatrix, - scale: 0.0254, - allowPicking: true, - }) - ); +const gltfLoad = function(gltfUrl, gltfId) { + const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame( + Cesium.Cartesian3.fromDegrees(9.083385, 48.881342, 0) + ); + + viewer.scene.primitives.add( + Cesium.Model.fromGltf({ + url: `${gltfUrl}/${gltfId}.gltf`, + modelMatrix: modelMatrix, + scale: 0.0254, + allowPicking: true, + }) + ); }; /** @@ -101,64 +101,64 @@ const gltfLoad = function (gltfUrl, gltfId) { * @param{*} * @returns {undefined} undefined */ -const loadDetailed = function () { - // Paths to data sources - const URL_3DTILES = "data_3d/3dtiles/2_partial/tileset.json"; - const URL_GLTF = "data_3d/gltf"; - - // Tileset without building 225 - loadTiles(URL_3DTILES); - - // Load Building 225 - gltfLoad(URL_GLTF, "bosch_si225_3"); - - // Load sensors in Building 225 - const gltfArray = [ - "sensor_013", - "sensor_023", - "sensor_033", - "sensor_053", - "sensor_063", - "sensor_073", - "sensor_083", - "sensor_093", - "sensor_103", - "sensor_113", - "sensor_123", - "sensor_133", - "sensor_143", - "sensor_153", - "sensor_163", - "sensor_173", - "sensor_183", - "sensor_213", - "sensor_223", - "sensor_233", - "sensor_253", - "sensor_263", - "sensor_273", - "sensor_283", - "sensor_293", - "sensor_303", - "sensor_313", - "sensor_323", - "sensor_333", - "sensor_343", - "sensor_353", - "sensor_363", - "sensor_373", - "sensor_383_v2", - ]; - - gltfArray.forEach((sensor) => gltfLoad(URL_GLTF, sensor)); +const loadDetailed = function() { + // Paths to data sources + const URL_3DTILES = "data_3d/3dtiles/2_partial/tileset.json"; + const URL_GLTF = "data_3d/gltf"; + + // Tileset without building 225 + loadTiles(URL_3DTILES); + + // Load Building 225 + gltfLoad(URL_GLTF, "bosch_si225_3"); + + // Load sensors in Building 225 + const gltfArray = [ + "sensor_013", + "sensor_023", + "sensor_033", + "sensor_053", + "sensor_063", + "sensor_073", + "sensor_083", + "sensor_093", + "sensor_103", + "sensor_113", + "sensor_123", + "sensor_133", + "sensor_143", + "sensor_153", + "sensor_163", + "sensor_173", + "sensor_183", + "sensor_213", + "sensor_223", + "sensor_233", + "sensor_253", + "sensor_263", + "sensor_273", + "sensor_283", + "sensor_293", + "sensor_303", + "sensor_313", + "sensor_323", + "sensor_333", + "sensor_343", + "sensor_353", + "sensor_363", + "sensor_373", + "sensor_383_v2", + ]; + + gltfArray.forEach((sensor) => gltfLoad(URL_GLTF, sensor)); }; if (!LOAD_DETAILED_BLDG225) { - // Default case: load only 3dTiles - loadNonDetailed(); + // Default case: load only 3dTiles + loadNonDetailed(); } else { - // Alternative case: load 3dTiles + glTF - loadDetailed(); + // Alternative case: load 3dTiles + glTF + loadDetailed(); } /** @@ -166,109 +166,109 @@ if (!LOAD_DETAILED_BLDG225) { * @param {*} * @returns {undefined} */ -const activate3DTileFeaturePicking = function () { - // HTML overlay for showing feature name on mouseover - const nameOverlay = document.createElement("div"); - viewer.container.appendChild(nameOverlay); - nameOverlay.className = "backdrop"; - nameOverlay.style.display = "none"; - nameOverlay.style.position = "absolute"; - nameOverlay.style.bottom = "0"; - nameOverlay.style.left = "0"; - nameOverlay.style["pointer-events"] = "none"; - nameOverlay.style.padding = "4px"; - nameOverlay.style.backgroundColor = "black"; - nameOverlay.style.color = "white"; - nameOverlay.style.fontFamily = "Fira Sans, sans-serif"; - nameOverlay.style.fontSize = "0.75em"; - - // Information about the currently selected feature - const selected = { - feature: undefined, - originalColor: new Cesium.Color(), - }; - - // An entity object which will hold info about the currently selected feature for infobox display - const selectedEntity = new Cesium.Entity(); - - // Get default left click handler for when a feature is not picked on left click - const clickHandler = viewer.screenSpaceEventHandler.getInputAction( - Cesium.ScreenSpaceEventType.LEFT_CLICK - ); - - // Change the feature color on mouse over - - // Information about the currently highlighted feature - const highlighted = { - feature: undefined, - originalColor: new Cesium.Color(), - }; - - // Color a feature on hover. - viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement) { - // If a feature was previously highlighted, undo the highlight - if (Cesium.defined(highlighted.feature)) { - highlighted.feature.color = highlighted.originalColor; - highlighted.feature = undefined; - } - // Pick a new feature - const pickedFeature = viewer.scene.pick(movement.endPosition); - if (!Cesium.defined(pickedFeature)) { - nameOverlay.style.display = "none"; - return; - } - // A feature was picked, so show it's overlay content - nameOverlay.style.display = "block"; - nameOverlay.style.bottom = - viewer.canvas.clientHeight - movement.endPosition.y + "px"; - nameOverlay.style.left = movement.endPosition.x + "px"; - let name = pickedFeature.getProperty("_gebaeude"); - if (!Cesium.defined(name)) { - name = pickedFeature.getProperty("id"); - } - nameOverlay.textContent = name; - // Highlight the feature if it's not already selected. - if (pickedFeature !== selected.feature) { - highlighted.feature = pickedFeature; - Cesium.Color.clone(pickedFeature.color, highlighted.originalColor); - pickedFeature.color = Cesium.Color.GREY; - } - }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); - - // Color a feature on selection and show metadata in the InfoBox. - viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement) { - // If a feature was previously selected, undo the highlight - if (Cesium.defined(selected.feature)) { - selected.feature.color = selected.originalColor; - selected.feature = undefined; - } - // Pick a new feature - const pickedFeature = viewer.scene.pick(movement.position); - if (!Cesium.defined(pickedFeature)) { - clickHandler(movement); - return; - } - // Select the feature if it's not already selected - if (selected.feature === pickedFeature) { - return; - } - selected.feature = pickedFeature; - // Save the selected feature's original color - if (pickedFeature === highlighted.feature) { - Cesium.Color.clone(highlighted.originalColor, selected.originalColor); - highlighted.feature = undefined; - } else { - Cesium.Color.clone(pickedFeature.color, selected.originalColor); - } - // Highlight newly selected feature - pickedFeature.color = Cesium.Color.LIME; - // Set feature infobox description - const featureName = pickedFeature.getProperty("name"); - selectedEntity.name = featureName; - selectedEntity.description = - 'Loading <div class="cesium-infoBox-loading"></div>'; - viewer.selectedEntity = selectedEntity; - selectedEntity.description = ` +const activate3DTileFeaturePicking = function() { + // HTML overlay for showing feature name on mouseover + const nameOverlay = document.createElement("div"); + viewer.container.appendChild(nameOverlay); + nameOverlay.className = "backdrop"; + nameOverlay.style.display = "none"; + nameOverlay.style.position = "absolute"; + nameOverlay.style.bottom = "0"; + nameOverlay.style.left = "0"; + nameOverlay.style["pointer-events"] = "none"; + nameOverlay.style.padding = "4px"; + nameOverlay.style.backgroundColor = "black"; + nameOverlay.style.color = "white"; + nameOverlay.style.fontFamily = "Fira Sans, sans-serif"; + nameOverlay.style.fontSize = "0.75em"; + + // Information about the currently selected feature + const selected = { + feature: undefined, + originalColor: new Cesium.Color(), + }; + + // An entity object which will hold info about the currently selected feature for infobox display + const selectedEntity = new Cesium.Entity(); + + // Get default left click handler for when a feature is not picked on left click + const clickHandler = viewer.screenSpaceEventHandler.getInputAction( + Cesium.ScreenSpaceEventType.LEFT_CLICK + ); + + // Change the feature color on mouse over + + // Information about the currently highlighted feature + const highlighted = { + feature: undefined, + originalColor: new Cesium.Color(), + }; + + // Color a feature on hover. + viewer.screenSpaceEventHandler.setInputAction(function onMouseMove(movement) { + // If a feature was previously highlighted, undo the highlight + if (Cesium.defined(highlighted.feature)) { + highlighted.feature.color = highlighted.originalColor; + highlighted.feature = undefined; + } + // Pick a new feature + const pickedFeature = viewer.scene.pick(movement.endPosition); + if (!Cesium.defined(pickedFeature)) { + nameOverlay.style.display = "none"; + return; + } + // A feature was picked, so show it's overlay content + nameOverlay.style.display = "block"; + nameOverlay.style.bottom = + viewer.canvas.clientHeight - movement.endPosition.y + "px"; + nameOverlay.style.left = movement.endPosition.x + "px"; + let name = pickedFeature.getProperty("_gebaeude"); + if (!Cesium.defined(name)) { + name = pickedFeature.getProperty("id"); + } + nameOverlay.textContent = name; + // Highlight the feature if it's not already selected. + if (pickedFeature !== selected.feature) { + highlighted.feature = pickedFeature; + Cesium.Color.clone(pickedFeature.color, highlighted.originalColor); + pickedFeature.color = Cesium.Color.GREY; + } + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + // Color a feature on selection and show metadata in the InfoBox. + viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(movement) { + // If a feature was previously selected, undo the highlight + if (Cesium.defined(selected.feature)) { + selected.feature.color = selected.originalColor; + selected.feature = undefined; + } + // Pick a new feature + const pickedFeature = viewer.scene.pick(movement.position); + if (!Cesium.defined(pickedFeature)) { + clickHandler(movement); + return; + } + // Select the feature if it's not already selected + if (selected.feature === pickedFeature) { + return; + } + selected.feature = pickedFeature; + // Save the selected feature's original color + if (pickedFeature === highlighted.feature) { + Cesium.Color.clone(highlighted.originalColor, selected.originalColor); + highlighted.feature = undefined; + } else { + Cesium.Color.clone(pickedFeature.color, selected.originalColor); + } + // Highlight newly selected feature + pickedFeature.color = Cesium.Color.LIME; + // Set feature infobox description + const featureName = pickedFeature.getProperty("name"); + selectedEntity.name = featureName; + selectedEntity.description = + 'Loading <div class="cesium-infoBox-loading"></div>'; + viewer.selectedEntity = selectedEntity; + selectedEntity.description = ` <table class="cesium-infoBox-defaultTable"> <tbody> <tr><th>Bau</th><td> @@ -290,57 +290,62 @@ const activate3DTileFeaturePicking = function () { </table> `; - const clickedBuilding = pickedFeature.getProperty("_gebaeude"); - const clickedBuildingDatastreamId = getDatastreamIdFromBuildingNumber( - clickedBuilding, - "vl", - "60min" - ); + const clickedBuilding = pickedFeature.getProperty("_gebaeude"); + const clickedBuildingDatastreamId = getDatastreamIdFromBuildingNumber( + clickedBuilding, + "vl", + "60min" + ); + + const BASE_URL_OBSERVATIONS = getObservationsUrl( + BASE_URL, + clickedBuildingDatastreamId + ); + const PARAM_FILTER = createTemporalFilterString("2020-01-01", "2021-01-01"); + + const axiosGetRequest = axios.get(BASE_URL_OBSERVATIONS, { + params: { + "$resultFormat": PARAM_RESULT_FORMAT, + "$orderBy": PARAM_ORDER_BY, + "$filter": PARAM_FILTER, + "$select": PARAM_SELECT, + }, + }); - const BASE_URL_OBSERVATIONS = getObservationsUrl( - BASE_URL, - clickedBuildingDatastreamId - ); - const PARAM_FILTER = createTemporalFilterString("2020-01-01", "2021-01-01"); - - const axiosGetRequest = axios.get(BASE_URL_OBSERVATIONS, { - params: { - "$resultFormat": PARAM_RESULT_FORMAT, - "$orderBy": PARAM_ORDER_BY, - "$filter": PARAM_FILTER, - "$select": PARAM_SELECT, - }, - }); - // Get "ALL" the Observations that satisfy our query - followNextLink(axiosGetRequest) - .then((success) => { - const successValue = success.data.value; + // Get "ALL" the Observations that satisfy our query + followNextLink(axiosGetRequest) + .then((success) => { + const successValue = success.data.value; - // Array that will hold the combined observations - const combinedObservations = []; + // 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; + successValue.forEach((dataObj) => { + // Each page of results will have a dataArray that holds the observations + const dataArrays = dataObj.dataArray; + + combinedObservations.push(...dataArrays); + }); + // DEBUG: Check total number of observations + console.log(combinedObservations.length); + // DEBUG: Print the array of observations + console.log(combinedObservations); + + return combinedObservations; + }) + .catch((err) => { + console.log(err); + }) + .then((observationArr) => { + var agg = aggregateResponse(observationArr, 0, 'mean'); + console.log(agg); + drawHeatMapHC(formatSTAResponseForHeatMap(observationArr)); + drawLineChartHC(formatSTAResponseForLineChart(observationArr)); + }); + + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); - combinedObservations.push(...dataArrays); - }); - // DEBUG: Check total number of observations - console.log(combinedObservations.length); - // DEBUG: Print the array of observations - console.log(combinedObservations); - - return combinedObservations; - }) - .catch((err) => { - console.log(err); - }) - .then((observationArr) => { - drawHeatMapHC(formatSTAResponseForHeatMap(observationArr)); - drawLineChartHC(formatSTAResponseForLineChart(observationArr)); - }); - }, Cesium.ScreenSpaceEventType.LEFT_CLICK); }; -activate3DTileFeaturePicking(); +activate3DTileFeaturePicking(); \ No newline at end of file diff --git a/public/js/appChart.js b/public/js/appChart.js index 953024e..3ab6c6a 100644 --- a/public/js/appChart.js +++ b/public/js/appChart.js @@ -13,72 +13,72 @@ export const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1"; * @param {*} samplingRate String representing the sampling rate of the observations * @returns {Number} Datastream corresponding to the input building */ -export const getDatastreamIdFromBuildingNumber = function ( - buildingNumber, - phenomenon, - samplingRate +export const getDatastreamIdFromBuildingNumber = function( + 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" }, - }, - "112, 118": { - 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" }, - }, - }; - - if (!buildingNumber) return; - - // check if building is contained in mapping object - if (!(buildingNumber in buildingToDatastreamMapping)) return; - - const datastreamIdMatched = Number( - buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate] - ); - - return datastreamIdMatched; + 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" }, + }, + "112, 118": { + 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" }, + }, + }; + + if (!buildingNumber) return; + + // check if building is contained in mapping object + if (!(buildingNumber in buildingToDatastreamMapping)) return; + + const datastreamIdMatched = Number( + buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate] + ); + + return datastreamIdMatched; }; /** @@ -86,10 +86,10 @@ export const getDatastreamIdFromBuildingNumber = function ( * @param {Number} datastreamID - Integer representing the Datastream ID * @returns {String} URL string for fetching the Observations corresponding to a Datastream */ -export const getObservationsUrl = function (baseUrl, datastreamID) { - if (!datastreamID) return; - const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`; - return fullDatastreamURL; +export const getObservationsUrl = function(baseUrl, datastreamID) { + if (!datastreamID) return; + const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`; + return fullDatastreamURL; }; /** @@ -98,10 +98,10 @@ export const getObservationsUrl = function (baseUrl, datastreamID) { * @param {String} dateStop Stop date in YYYY-MM-DD format * @returns {String} Temporal filter string */ -export const createTemporalFilterString = function (dateStart, dateStop) { - if (!dateStart || !dateStop) return; - const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`; - return filterString; +export const createTemporalFilterString = function(dateStart, dateStop) { + if (!dateStart || !dateStop) return; + const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`; + return filterString; }; // const BASE_URL_OBSERVATIONS = getObservationsUrl(80); @@ -119,31 +119,218 @@ export const PARAM_SELECT = "result,phenomenonTime"; // }, // }); + +/** + * + * @param {JSON} obj JSON object on which to replace a specific key. + * @param {String} oldKey is the old key in the JSON to be renamed to newKey + * @param {String} newKey is the key that should replace the oldKey + * usage: myjson.forEach((obj) => renameKey(obj, "oldkey", "newkey")); + */ + +function renameKey(obj, oldKey, newKey) { + obj[newKey] = obj[oldKey]; + delete obj[oldKey]; +} + + +/** + * + * @param {Array} arr is the Array to be converted into a JSON + * @returns {JSON} stringToJsonObject + */ +function convertArray2JSON(arr) { + var arrayToString = JSON.stringify(Object.assign({}, arr)); // convert array to string + var stringToJsonObject = JSON.parse(arrayToString); // convert string to json object + return stringToJsonObject; +} + + +function createDateFromDateTimeString(jsonData) { + let datx = []; + let daty = []; + const MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + for (var u = 0; u < jsonData.length; u++) { + daty.push(jsonData[u].temperature); + let date = new Date(jsonData[u].datetime); + let datum = date.getDate(); + let month = MONTH[date.getMonth()]; + let hour = date.getHours() + ":00"; + let newDateStr = datum + "/" + month + "-" + hour; + datx.push(newDateStr); + } + return [datx, daty]; +} + + + + +/** + * Format the response from SensorThings API to make it suitable for heatmap + * @param {Array} obsArray Response from SensorThings API as array + * @param {Int8} hours Number of hours to aggregate over. If hours=0, + * it will be aggregated by date (e.g. all values recorded on 1st of May etc...) + * if hours to any number (also hours=24) data will be aggregated over every 24hours, + * even if the date changes (e.g. data from 1st of May from 10pm up 9pm on 2nd of May etc.) + * @param {String} method Specify how to aggregate date. Use: 'mean' = default, 'sum', 'min' or 'max' + * @returns {Array} Aggregated Response + */ + +export const aggregateResponse = function(obsArray, hours, method) { + if (!obsArray) return; + if (hours < 0) return; + + // check if we have a defined method or the method specified is accepted, rest is handeled in switch/case below + if (method == undefined) method = 'mean'; + + + // convert obsArray to json + + let jsonFromArr = []; + for (var i = 0; i < obsArray.length; i++) { + jsonFromArr.push(convertArray2JSON(obsArray[i])); + } + + // rename the keys in the jason + jsonFromArr.forEach((obj) => renameKey(obj, "0", "datetime")); + let jsonData = jsonFromArr; + jsonData.forEach((obj) => renameKey(obj, "1", "temperature")); + + + let newOutput = []; + var aggDates = []; + var aggregatedVals = []; + var vals = []; // store values temporarily to use for processing + + if (hours == 0) { // i.e. aggregate over one Date / Day + + var currentDate; + var oldDate; + for (var d = 0; d < jsonData.length; d++) { + let tmpDate = new Date(jsonData[d].datetime); + currentDate = tmpDate.getDate(); // gets the day of the month 1...31 + if (d === 0) oldDate = currentDate; + + if (currentDate == oldDate) { + vals.push(jsonData[d].temperature); + } else { + aggDates.push(new Date(tmpDate - 1)); + + if (vals.length == 0) { + aggregatedVals.push(-1); + oldDate = currentDate; + continue; + } + + switch (method) { + case 'mean': + aggregatedVals.push(vals.reduce(function(a, b) { return a + b / vals.length; }, 0)); + break; + case 'sum': + aggregatedVals.push(vals.reduce(function(a, b) { return a + b; }, 0)); + break; + case 'min': + aggregatedVals.push(vals.reduce(function(a, b) { return Math.min(a, b); })); + break; + case 'max': + aggregatedVals.push(vals.reduce(function(a, b) { return Math.max(a, b); })); + break; + default: + aggregatedVals.push(vals.reduce(function(a, b) { return a + b / vals.length; }, 0)); + } + vals = []; // clear the daily value vector + vals.push(jsonData[d].temperature); // now push first entry of new day into my temp value vector. + } + oldDate = currentDate; + } // end of for loop + + // create output to be in the same List format as the original data from obsArray. + + for (let i = 0; i < aggregatedVals.length; i++) { + newOutput.push([aggDates[i].toISOString(), aggregatedVals[i]]); + } + + } else { // i.e. aggregate over X hours, irrespective of the day. + let cnt = 0; + let cumHours = 0; + + for (var d = 0; d < jsonData.length; d++) { + if (cnt < hours) { + vals.push(jsonData[d].temperature); + cnt++; + } else { + cumHours += cnt; + cnt = 0; + aggDates.push(cumHours); + + if (vals.length == 0) { + aggregatedVals.push(-1); + continue; + } + + switch (method) { + case 'mean': + aggregatedVals.push(vals.reduce(function(a, b) { return a + b / vals.length; }, 0)); + break; + case 'sum': + aggregatedVals.push(vals.reduce(function(a, b) { return a + b; }, 0)); + break; + case 'min': + aggregatedVals.push(vals.reduce(function(a, b) { return Math.min(a, b); })); + break; + case 'max': + aggregatedVals.push(vals.reduce(function(a, b) { return Math.max(a, b); })); + break; + default: + aggregatedVals.push(vals.reduce(function(a, b) { return a + b / vals.length; }, 0)); + } + vals = []; // clear the daily value vector + vals.push(jsonData[d].temperature); // now push first entry of new day into my temp value vector. + cnt++; + } + oldDate = currentDate; + } // end of for loop + // create output to be in the same List format as the original data from obsArray. + + for (let i = 0; i < aggregatedVals.length; i++) { + newOutput.push([aggDates[i], aggregatedVals[i]]); + } + } // end else + + + + + + return newOutput; +} + + /** * Format the response from SensorThings API to make it suitable for heatmap * @param {Array} obsArray Response from SensorThings API as array * @returns {Array} Array of formatted observations suitable for use in a heatmap */ -export const formatSTAResponseForHeatMap = function (obsArray) { - if (!obsArray) return; - 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; +export const formatSTAResponseForHeatMap = function(obsArray) { + if (!obsArray) return; + 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; }; /** @@ -151,94 +338,91 @@ export const formatSTAResponseForHeatMap = function (obsArray) { * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap * @returns {Object} Highcharts library heatmap object */ -export const drawHeatMapHC = function (formattedObsArrayForHeatmap) { - 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: [ - { - data: formattedObsArrayForHeatmap, - 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>", +export const drawHeatMapHC = function(formattedObsArrayForHeatmap) { + 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, }, - turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release - }, - ], - }); + + colorAxis: { + stops: [ + [0, "#3060cf"], + [0.5, "#fffbbc"], + [0.9, "#c4463a"], + [1, "#c4463a"], + ], + min: 60, + max: 85, + startOnTick: false, + endOnTick: false, + labels: { + format: "{value}℃", + }, + }, + + series: [{ + data: formattedObsArrayForHeatmap, + 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 + }, ], + }); }; /** @@ -246,15 +430,15 @@ export const drawHeatMapHC = function (formattedObsArrayForHeatmap) { * @param {Array} obsArray Response from SensorThings API as array * @returns {Array} Array of formatted observations suitable for use in a line chart */ -export const formatSTAResponseForLineChart = function (obsArray) { - if (!obsArray) return; - 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; +export const formatSTAResponseForLineChart = function(obsArray) { + if (!obsArray) return; + 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; }; /** @@ -262,38 +446,36 @@ export const formatSTAResponseForLineChart = function (obsArray) { * @param {Array} formattedObsArrayForLineChart - Response from SensorThings API formatted for use in a line chart * @returns {Object} Highcharts library line chart object */ -export const drawLineChartHC = function (formattedObsArrayForLineChart) { - // Create the chart - Highcharts.stockChart("chart-line", { - chart: { - zoomType: "x", - }, - - rangeSelector: { - selected: 1, - }, - - title: { - text: "Inlet flow (Vorlauf)", - "align": "left", - }, - - subtitle: { - text: "Temperature variation by hour in 2020", - align: "left", - }, - - series: [ - { - name: "AAPL", - data: formattedObsArrayForLineChart, - tooltip: { - valueDecimals: 2, +export const drawLineChartHC = function(formattedObsArrayForLineChart) { + // Create the chart + Highcharts.stockChart("chart-line", { + chart: { + zoomType: "x", + }, + + rangeSelector: { + selected: 1, }, - turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release - }, - ], - }); + + title: { + text: "Inlet flow (Vorlauf)", + "align": "left", + }, + + subtitle: { + text: "Temperature variation by hour in 2020", + align: "left", + }, + + series: [{ + name: "AAPL", + data: formattedObsArrayForLineChart, + tooltip: { + valueDecimals: 2, + }, + turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release + }, ], + }); }; /** @@ -303,26 +485,26 @@ export const drawLineChartHC = function (formattedObsArrayForLineChart) { * @param {Object} responsePromise Promise object * @returns {Object} - Object containing results from all the "@iot.nextLink" links */ -export const followNextLink = function (responsePromise) { - if (!responsePromise) return; - return responsePromise - .then(function (lastSuccess) { - if (lastSuccess.data["@iot.nextLink"]) { - return followNextLink( - axios.get(lastSuccess.data["@iot.nextLink"]) - ).then(function (nextLinkSuccess) { - nextLinkSuccess.data.value = lastSuccess.data.value.concat( - nextLinkSuccess.data.value - ); - return nextLinkSuccess; +export const followNextLink = function(responsePromise) { + if (!responsePromise) return; + return responsePromise + .then(function(lastSuccess) { + if (lastSuccess.data["@iot.nextLink"]) { + return followNextLink( + axios.get(lastSuccess.data["@iot.nextLink"]) + ).then(function(nextLinkSuccess) { + nextLinkSuccess.data.value = lastSuccess.data.value.concat( + nextLinkSuccess.data.value + ); + return nextLinkSuccess; + }); + } else { + return lastSuccess; + } + }) + .catch(function(err) { + console.log(err); }); - } else { - return lastSuccess; - } - }) - .catch(function (err) { - console.log(err); - }); }; // Get "ALL" the Observations that satisfy our query @@ -352,4 +534,4 @@ export const followNextLink = function (responsePromise) { // .then((observationArr) => { // drawHeatMapHC(observationArr); // drawLineChartHC(observationArr); -// }); +// }); \ No newline at end of file -- GitLab