node-red-contrib-tank-volume
Version:
A Node-RED node to calculate the volume of different tank types
793 lines (680 loc) • 47.1 kB
JavaScript
/**
* Copyright 2021 Bart Butenaers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
var settings = RED.settings;
const convert = require('convert-units');
const path = require('path');
function TankVolumeNode(config) {
RED.nodes.createNode(this, config);
this.inputField = config.inputField;
this.measurement = config.measurement;
this.outputField = config.outputField;
this.inputUnit1 = config.inputUnit1;
this.inputUnit2 = config.inputUnit2;
this.outputUnit = config.outputUnit;
this.topLimit = parseInt(config.topLimit);
this.bottomLimit = parseInt(config.bottomLimit);
this.tankType = config.tankType;
this.diameter = parseFloat(config.diameter);
this.length = parseFloat(config.length);
this.width = parseFloat(config.width);
this.height = parseFloat(config.height);
this.length2 = parseFloat(config.length2);
this.width2 = parseFloat(config.width2);
this.height2 = parseFloat(config.height2);
this.coneHeight = parseFloat(config.coneHeight);
this.cylinderHeight = parseFloat(config.cylinderHeight);
this.diameterTop = parseFloat(config.diameterTop);
this.diameterBottom = parseFloat(config.diameterBottom);
this.customTable = config.customTable || [];
var node = this;
if (this.tankType == "") this.tankType = "none";
// A few resources:
// https://keisan.casio.com/menu/system/000000000280
// https://www.omnicalculator.com/construction/tank-volume
// https://ijret.org/volumes/2016v05/i04/IJRET20160504001.pdf
function getTotalVolumeHorizonalCylinder(radius, length) {
if (radius == undefined) throw "The diameter of the horizontal cylinder is undefined";
if (length == undefined) throw "The length of the horizontal cylinder is undefined";
return Math.PI * Math.pow(radius, 2) * length;
}
function getPartialVolumeHorizonalCylinder(radius, length, fluidHeight) {
if (radius == undefined) throw "The diameter of the horizontal cylinder is undefined";
if (length == undefined) throw "The length of the horizontal cylinder is undefined";
if (fluidHeight == undefined) throw "The fill level of the horizontal cylinder is undefined";
var angle = 2 * Math.acos((radius - fluidHeight) / radius); // See θ
return 0.5 * Math.pow(radius, 2) * (angle - Math.sin(angle)) * length;
}
function getTotalVolumeVerticalCylinder(radius, height) {
if (radius == undefined) throw "The diameter of the vertical cylinder is undefined";
if (height == undefined) throw "The height of the vertical cylinder is undefined";
return Math.PI * Math.pow(radius, 2) * height;
}
function getPartialVolumeVerticalCylinder(radius, fluidHeight) {
if (radius == undefined) throw "The diameter of the vertical cylinder is undefined";
if (fluidHeight == undefined) throw "The fill level of the vertical cylinder is undefined";
return Math.PI * Math.pow(radius, 2) * fluidHeight;
}
function getTotalVolumeRectangularPrism(width, length, height) {
if (width == undefined) throw "The width of the rectangular prism is undefined";
if (length == undefined) throw "The length of the rectangular prism is undefined";
if (height == undefined) throw "The height of the rectangular prismr is undefined";
return height * width * length;
}
function getPartialVolumeRectangularPrism(width, length, fluidHeight) {
if (width == undefined) throw "The width of the rectangular prism is undefined";
if (length == undefined) throw "The length of the rectangular prism is undefined";
if (fluidHeight == undefined) throw "The fill level of the rectangular prism is undefined";
return fluidHeight * width * length;
}
function getTotalVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom) {
if (coneHeight == undefined) throw "The cone height of the cone top is undefined";
if (cylinderHeight == undefined) throw "The cylinder height of the cone top is undefined";
if (radiusTop == undefined) throw "The diameter top of the cone top is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the cone top is undefined";
// Add the volume of the frustum part to the volume of the cylindrical part.
return getTotalVolumeVerticalCylinder(radiusBottom, cylinderHeight) + getTotalVolumeFrustrum(coneHeight, radiusBottom, radiusTop);
}
function getPartialVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom, fluidHeight) {
if (coneHeight == undefined) throw "The cone height of the cone top is undefined";
if (cylinderHeight == undefined) throw "The cylinder height of the cone top is undefined";
if (radiusTop == undefined) throw "The diameter top of the cone top is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the cone top is undefined";
if (fluidHeight == undefined) throw "The fill level of the cone top is undefined";
if (fluidHeight <= cylinderHeight) {
return getPartialVolumeVerticalCylinder(radiusBottom, fluidHeight);
}
else {
var frustrum_fluidHeight = fluidHeight - cylinderHeight;
var frustrumVolume = getPartialVolumeFrustrum(coneHeight, radiusTop, radiusBottom, frustrum_fluidHeight);
return getTotalVolumeVerticalCylinder(radiusBottom, cylinderHeight) + frustrumVolume;
}
}
function getTotalVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom) {
if (coneHeight == undefined) throw "The cone height of the cone bottom is undefined";
if (cylinderHeight == undefined) throw "The cylinder height of the cone bottom is undefined";
if (radiusTop == undefined) throw "The diameter top of the cone bottom is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the cone bottom is undefined";
// Add the volume of the frustum part to the volume of the cylindrical part
return getTotalVolumeFrustrum(coneHeight, radiusTop, radiusBottom) + getTotalVolumeVerticalCylinder(radiusTop, cylinderHeight);
}
function getPartialVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom, fluidHeight) {
if (coneHeight == undefined) throw "The cone height of the cone bottom is undefined";
if (cylinderHeight == undefined) throw "The cylinder height of the cone bottom is undefined";
if (radiusTop == undefined) throw "The diameter top of the cone bottom is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the cone bottom is undefined";
if (fluidHeight == undefined) throw "The fill level of the cone bottom is undefined";
if (fluidHeight <= coneHeight) {
return getPartialVolumeFrustrum(coneHeight, radiusTop, radiusBottom, fluidHeight);
}
else {
var cylinder_fluidHeight = fluidHeight - coneHeight;
return getTotalVolumeFrustrum(coneHeight, radiusTop, radiusBottom) + getPartialVolumeVerticalCylinder(radiusTop, cylinder_fluidHeight);
}
}
function getTotalVolumeInversePiramid(width, length, height, width2, length2, height2) {
if (width == undefined) throw "The width of the rectangular prism is undefined";
if (length == undefined) throw "The length of the rectangular prism is undefined";
if (height == undefined) throw "The height of the rectangular prismr is undefined";
if (width == undefined) throw "The width 2 of the rectangular prism is undefined";
if (length == undefined) throw "The length 2 of the rectangular prism is undefined";
if (height == undefined) throw "The height 2 of the rectangular prismr is undefined";
return getTotalVolumeRectangularPrism(width2, length2, height2) + getTotalVolumeRectangularPrism(width, length, height);
}
function getPartialVolumeInversePiramid(width, length, height, width2, length2, height2, fluidHeight) {
if (width == undefined) throw "The width of the rectangular prism is undefined";
if (length == undefined) throw "The length of the rectangular prism is undefined";
if (fluidHeight == undefined) throw "The fill level of the rectangular prism is undefined";
if (fluidHeight <= height2) {
return getPartialVolumeRectangularPrism(width2, length2, fluidHeight);
}
else {
var fluidHeight1 = fluidHeight - height2;
return getTotalVolumeRectangularPrism(width2, length2, height2) + getPartialVolumeRectangularPrism(width, length, fluidHeight1);
}
}
function getTotalVolumeHorizontalCapsule(radius, length) {
if (radius == undefined) throw "The diameter of the horizontal capsule is undefined";
if (length == undefined) throw "The length of the horizontal capsule is undefined";
return Math.PI * Math.pow(radius, 2) * ((4/3) * radius + length);
}
function getPartialVolumeHorizontalCapsule(radius, length, fluidHeight) {
if (radius == undefined) throw "The diameter of the horizontal capsule is undefined";
if (length == undefined) throw "The length of the horizontal capsule is undefined";
if (fluidHeight == undefined) throw "The fill level of the horizontal capsule is undefined";
var angle = 2 * Math.acos((radius - fluidHeight) / radius); // See θ
// V_horizontal_cylinder_fluidHeight + V_spherical_cap_fluidHeight
return 0.5 * Math.pow(radius, 2) * (angle - Math.sin(angle)) * length + ((Math.PI * Math.pow(fluidHeight, 2)) / 3) * ((3 * radius) - fluidHeight);
}
function getTotalVolumeVerticalCapsule(radius, length) {
if (radius == undefined) throw "The diameter of the vertical capsule is undefined";
if (length == undefined) throw "The length of the vertical capsule is undefined";
return Math.PI * Math.pow(radius, 2) * ((4/3) * radius + length);
}
function getPartialVolumeVerticalCapsule(radius, length, fluidHeight) {
if (radius == undefined) throw "The diameter of the vertical capsule is undefined";
if (length == undefined) throw "The length of the vertical capsule is undefined";
if (fluidHeight == undefined) throw "The fill level of the vertical capsule is undefined";
if (fluidHeight < radius) {
// The liquid is only in the bottom hemisphere part
return ((Math.PI * Math.pow(fluidHeight, 2)) / 3) * ((3 * radius) - fluidHeight);
}
else if (radius < fluidHeight && fluidHeight < radius + length) {
// Add the hemisphere volume and "shorter" cylinder
var fluidHeightCylinder = fluidHeight - radius;
return (2/3) * Math.PI * Math.pow(radius, 3) + Math.PI * Math.pow(radius, 2) * fluidHeightCylinder;
}
else { // diameter/2 + length < fluidHeight
// Add the hemisphere volume and cylinder volume and part of upper hemisphere volume
// See formula 13 in https://ijret.org/volumes/2016v05/i04/IJRET20160504001.pdf
var fluidHeightHemisphere = fluidHeight - radius - length;
return (2/3) * Math.PI * Math.pow(radius, 3) + Math.PI * Math.pow(radius, 2) * length + ((Math.PI / 12) * ((3 * Math.pow(2 * radius, 2) * fluidHeightHemisphere) - (4 * Math.pow(fluidHeightHemisphere, 3))));
}
}
function getTotalVolumeOval(width, length, height) {
if (width == undefined) throw "The width of the oval is undefined";
if (length == undefined) throw "The length of the oval is undefined";
if (height == undefined) throw "The height of the oval is undefined";
return Math.PI * width * length * height / 4;
}
function getPartialVolumeOval(width, length, height, fluidHeight) {
if (width == undefined) throw "The width of the oval is undefined";
if (length == undefined) throw "The length of the oval is undefined";
if (height == undefined) throw "The height of the oval is undefined";
if (fluidHeight == undefined) throw "The fill level of the oval is undefined";
return length * height * width /4 * (Math.acos(1 - (2 * fluidHeight / height)) - (1 - (2 * fluidHeight / height)) * Math.sqrt((4 * fluidHeight / height - 4 * Math.pow(fluidHeight, 2) / Math.pow(height, 2))));
}
function getTotalVolumeFrustrum(height, radiusTop, radiusBottom) {
if (height == undefined) throw "The height of the frustrum is undefined";
if (radiusTop == undefined) throw "The diameter top of the frustrum is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the frustrum is undefined";
// See formula calculation here https://www.sccollege.edu/Departments/MATH/Documents/Math%20185/06-02-048_Volumes.pdf
return (1/3) * Math.PI * height * (Math.pow(radiusTop, 2) + radiusTop * radiusBottom + Math.pow(radiusBottom, 2));
}
function getPartialVolumeFrustrum(height, radiusTop, radiusBottom, fluidHeight) {
if (height == undefined) throw "The height of the frustrum is undefined";
if (radiusTop == undefined) throw "The diameter top of the frustrum is undefined";
if (radiusBottom == undefined) throw "The diameter bottom of the frustrum is undefined";
if (fluidHeight == undefined) throw "The fill level of the frustrum is undefined";
// Note that this formule is only valid for limited cases !!!!
if (radiusTop === radiusBottom) throw "The diameter top should be different from the diameter bottom (otherwise it becomes a cylinder...)";
var z, radius_fluidHeight;
// Calculate the top radius of the fluidHeight part, based on triangles similarity...
z = height * radiusBottom / (radiusTop - radiusBottom);
radius_fluidHeight = radiusTop * (fluidHeight + z) / (height + z);
// It is not clear on https://www.omnicalculator.com/construction/tank-volume that the fluidHeight need to be used (instead of height)
// for a "cone top" tank. But Hanna has confirmed that it is always like that...
return (1/3) * Math.PI * fluidHeight * (Math.pow(radius_fluidHeight, 2) + radius_fluidHeight * radiusBottom + Math.pow(radiusBottom, 2));
}
function getTotalVolumeSphere(radius) {
if (radius == undefined) throw "The diameter of the sphere is undefined";
return Math.PI * Math.pow(radius, 3) * 4 / 3;
}
function getPartialVolumeSphere(radius, fluidHeight) {
if (radius == undefined) throw "The diameter of the sphere is undefined";
if (fluidHeight == undefined) throw "The fill level of the sphere is undefined";
// https://www.onlineconversion.com/object_volume_partial_sphere.htm
return Math.PI * Math.pow(fluidHeight, 2) * radius - Math.PI * Math.pow(fluidHeight, 3) / 3;
}
function getTotalVolumeHorizontalElliptical(radius, length) {
if (radius == undefined) throw "The diameter of the horizontal elliptical is undefined";
if (length == undefined) throw "The length of the horizontal elliptical is undefined";
return getPartialVolumeHorizontalElliptical(radius, length, 2 * radius);
}
function getPartialVolumeHorizontalElliptical(radius, length, fluidHeight) {
if (radius == undefined) throw "The diameter of the horizontal elliptical is undefined";
if (length == undefined) throw "The length of the horizontal elliptical is undefined";
if (fluidHeight == undefined) throw "The fill level of the horizontal elliptical is undefined";
var diameter = 2 * radius;
var C = 0.5; // ASME 2:1 (Elliptical heads)
// https://neutrium.net/equipment/volume-and-wetted-area-of-partially-filled-horizontal-vessels/
var volumeHead = Math.pow(diameter, 3) * C * (Math.PI / 12) * ((3 * Math.pow(fluidHeight / diameter, 2)) - (2 * Math.pow(fluidHeight / diameter, 3)));
return 2 * volumeHead + + getPartialVolumeHorizonalCylinder(radius, length, fluidHeight);
}
function getTotalVolumeHorizontalStadium(width, length, height) {
if (width == undefined) throw "The width of the horizontal stadium is undefined";
if (length == undefined) throw "The length of the horizontal stadium is undefined";
if (height == undefined) throw "The height of the horizontal stadium is undefined";
var radius = height / 2;
var widthPrism = width - 2 * radius;
return (Math.PI * Math.pow(radius, 2) + height * widthPrism) * length;
}
function getPartialVolumeHorizontalStadium(width, length, height, fluidHeight) {
if (width == undefined) throw "The width of the horizontal stadium is undefined";
if (length == undefined) throw "The length of the horizontal stadium is undefined";
if (height == undefined) throw "The height of the horizontal stadium is undefined";
var radius = height / 2;
var widthPrism = width - 2 * radius;
// Look at the tank as two halve horizontal cylinders, separated by a rectangular prism
return getPartialVolumeHorizonalCylinder(radius, length, fluidHeight) + getPartialVolumeRectangularPrism(widthPrism, length, fluidHeight);
}
function getTotalVolumeVerticalStadium(width, length, height) {
if (width == undefined) throw "The width of the vertical stadium is undefined";
if (length == undefined) throw "The length of the vertical stadium is undefined";
if (height == undefined) throw "The height of the vertical stadium is undefined";
var radius = width / 2;
var heightPrism = height - 2 * radius;
return (Math.PI * Math.pow(radius, 2) + width * heightPrism) * length;
}
function getPartialVolumeVerticalStadium(width, length, height, fluidHeight) {
if (width == undefined) throw "The width of the vertical stadium is undefined";
if (length == undefined) throw "The length of the vertical stadium is undefined";
if (height == undefined) throw "The height of the vertical stadium is undefined";
var radius = width / 2;
var heightPrism = height - 2 * radius;
// Look at the tank as two halve horizontal cylinders, separated by a rectangular prism
if (fluidHeight < radius) {
return getPartialVolumeHorizonalCylinder(radius, length, fluidHeight);
}
else if (fluidHeight < radius + heightPrism) {
return getPartialVolumeHorizonalCylinder(radius, length, radius) + getPartialVolumeRectangularPrism(width, length, fluidHeight - radius);
}
else {
// Since the bottom half cylinder is full and the top half cylinder is partial full, we calculate these both parts as one (because two half cylinders is 1 complete cylinder)
return getPartialVolumeHorizonalCylinder(radius, length, fluidHeight - heightPrism) + getTotalVolumeRectangularPrism(width, length, heightPrism);
}
}
function getTotalVolumeCustomTable(customTable) {
// The last volume in the row will be considered as the total volume ...
var lastRow = customTable[customTable.length-1];
return lastRow.volume;
}
function getPartialVolumeCustomTable(customTable, fluidHeight) {
for (var i = 1; i < customTable.length; i++) {
var previousRow = customTable[i-1];
var currentRow = customTable[i];
if(fluidHeight >= previousRow.height && fluidHeight <= currentRow.height) {
// Calculate the volume via linear interpolation (https://en.wikipedia.org/wiki/Linear_interpolation)
return previousRow.volume + (fluidHeight - previousRow.height) * (currentRow.volume - previousRow.volume) / (currentRow.height - previousRow.height)
}
}
}
node.on("input", function(msg) {
var input;
var fluidHeight = 0;
var tankHeight = 0;
var totalVolume = 0;
var filledVolume = 0;
var minimumVolume = 0;
var maximumVolume = 0;
// Copy the tank dimensions and limits
var topLimit = node.topLimit;
var bottomLimit = node.bottomLimit;
var diameter = node.diameter;
var length = node.length;
var width = node.width;
var height = node.height;
var length2 = node.length2;
var width2 = node.width2;
var height2 = node.height2;
var coneHeight = node.coneHeight;
var cylinderHeight = node.cylinderHeight;
var diameterTop = node.diameterTop;
var diameterBottom = node.diameterBottom;
// Deep clone the custom table, to avoid converting the same tank dimensions over and over again.
// See https://github.com/bartbutenaers/node-red-contrib-tank-volume/issues/3
var customTable = JSON.parse(JSON.stringify(node.customTable));
try {
input = RED.util.getMessageProperty(msg, node.inputField);
}
catch(err) {
node.error("The input cannot be read from input msg." + node.inputField);
return;
}
if (typeof input === 'object' && input !== null) {
// Settings from input msg override the config screen settings, when the latter one are 0.
// This means that 0 dimensions are not allowed on the config screen!!!
diameter = diameter || input.diameter;
length = length || input.length;
width = width || input.width;
height = height || input.height;
length2 = length2 || input.length2;
width2 = width2 || input.width2;
height2 = height2 || input.height2;
coneHeight = coneHeight || input.cone_height;
cylinderHeight = cylinderHeight || input.cylinder_height;
diameterTop = diameterTop || input.diameter_top;
diameterBottom = diameterBottom || input.diameter_bottom;
// The measured height always arrives via the input message, never via the config screen
measuredHeight = input.measuredHeight;
if (customTable.length === 0) {
customTable = input.customTable || [];
}
}
else if (!isNaN(input)) {
// The input can be a single number, representing the measured height
measuredHeight = input;
}
else {
node.error("The input cannot only be a number or a Javascript object");
return;
}
if (node.tankType === "custom_table") {
if (!customTable || !Array.isArray(customTable) || customTable.length === 0) {
node.error("The custom table should contain at least one row");
return;
}
if (customTable.length === 1 && customTable[0].height == 0) {
node.error("When the custom table contains one row, then height 0 is not allowed");
return;
}
// TODO check whether each array element has a height and a volume property
// Sort the custom table by ascending height
customTable.sort(function(row1, row2) {
return parseFloat(row1.height) - parseFloat(row2.height);
});
if (customTable[0].height == 0) {
if (customTable[0].volume != 0) {
node.error("The volume of height 0 should also be 0 in the custom table");
return;
}
}
else {
// The table should start with height 0
customTable.unshift({height:0, volume:0});
}
}
// From here on we work with radius (instead of diameter), to simplify the formula's...
var radius = diameter / 2;
var radiusTop = diameterTop / 2;
var radiusBottom = diameterBottom / 2;
// When input dimensions (2D) and limits are specified in another dimension, convert them to centimeter
if (node.inputUnit1 !== "cm") {
topLimit = convert(topLimit).from(node.inputUnit1).to('cm');
bottomLimit = convert(bottomLimit).from(node.inputUnit1).to('cm');
diameter = convert(diameter).from(node.inputUnit1).to('cm');
length = convert(length).from(node.inputUnit1).to('cm');
width = convert(width).from(node.inputUnit1).to('cm');
height = convert(height).from(node.inputUnit1).to('cm');
length2 = convert(length2).from(node.inputUnit1).to('cm');
width2 = convert(width2).from(node.inputUnit1).to('cm');
height2 = convert(height2).from(node.inputUnit1).to('cm');
coneHeight = convert(coneHeight).from(node.inputUnit1).to('cm');
cylinderHeight = convert(cylinderHeight).from(node.inputUnit1).to('cm');
diameterTop = convert(diameterTop).from(node.inputUnit1).to('cm');
diameterBottom = convert(diameterBottom).from(node.inputUnit1).to('cm');
measuredHeight = convert(measuredHeight).from(node.inputUnit1).to('cm');
// Convert the unit of every height in the custom table
customTable.forEach(function(row, index, array) {
row.height = convert(row.height).from(node.inputUnit1).to('cm');
});
}
// When input dimensions (3D) are specified in another dimension, convert them to cm3.
// Currently this is only used for the volumes in the custom table...
if (node.inputUnit2 !== "cm3") {
// Convert the unit of every volume in the custom table
customTable.forEach(function(row, index, array) {
row.volume = convert(row.volume).from(node.inputUnit2).to('cm3');
});
}
var tankType = node.tankType;
// When no tank type specified on the config screen, thrn it should be specified in the input message
if (tankType ==="none") {
if (input.tankType) {
tankType = input.tankType;
}
else {
node.warn("No tank type specified in the input message");
return;
}
}
// Calculate the total tank height, depending on the tank type
switch(tankType) {
case "horiz_cylin":
tankHeight = length;
break;
case "vert_cylin":
tankHeight = 2 * radius;
break;
case "rect_prism":
tankHeight = height;
break;
case "cone_top":
tankHeight = cylinderHeight + coneHeight;
break;
case "cone_bottom":
tankHeight = coneHeight + cylinderHeight;
break;
case "inv_piram":
tankHeight = height + height2;
break;
case "horiz_caps":
tankHeight = 2 * radius;
break;
case "vert_caps":
tankHeight = length + 2 * radius; // TODO KLOPT DIT ?????
break;
case "horiz_oval":
case "vert_oval":
tankHeight = height;
break;
case "frustrum":
tankHeight = height;
break;
case "sphere":
tankHeight = 2 * radius;
break;
case "horiz_ellip":
tankHeight = 2 * radius;
break;
case "horiz_stad":
tankHeight = height;
break;
case "vert_stad":
tankHeight = height;
break;
case "custom_table":
tankHeight = customTable[customTable.length-1].height;
break;
default:
throw "Unsupported tank type";
}
if (measuredHeight > tankHeight) {
node.warn("The measured depth (" + measuredHeight + ") is larger than the tank height (" + tankHeight + ")");
return;
}
if (bottomLimit > tankHeight) {
node.warn("The bottom limit (" + bottomLimit + ") is larger than the tank height (" + tankHeight + ")");
return;
}
if (topLimit > tankHeight) {
node.warn("The top limit (" + topLimit + ") is larger than the tank height (" + tankHeight + ")");
return;
}
if ((bottomLimit + topLimit) >= tankHeight) {
node.warn("The sum of bottom and top limit (" + (bottomLimit + topLimit) + ") is larger than the tank height (" + tankHeight + ")");
return;
}
// Determine the fluid height
switch (node.measurement) {
case "above":
// The msg.payload contains the height of the air (above the fluid)
fluidHeight = tankHeight - measuredHeight;
break;
case "fluid":
// The msg.payload already contains the height of the fluid
fluidHeight = measuredHeight;
break;
}
try {
// Calculate the total tank volume and the (partially) filled volume, depending on the tank type
switch(tankType) {
case "horiz_cylin":
totalVolume = getTotalVolumeHorizonalCylinder(radius, length);
filledVolume = getPartialVolumeHorizonalCylinder(radius, length, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeHorizonalCylinder(radius, length, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeHorizonalCylinder(radius, length, tankHeight - topLimit);
break;
case "vert_cylin":
totalVolume = getTotalVolumeVerticalCylinder(radius, height);
filledVolume = getPartialVolumeVerticalCylinder(radius, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeVerticalCylinder(radius, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeVerticalCylinder(radius, tankHeight - topLimit);
break;
case "rect_prism":
totalVolume = getTotalVolumeRectangularPrism(width, length, height);
filledVolume = getPartialVolumeRectangularPrism(width, length, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeRectangularPrism(width, length, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeRectangularPrism(width, length, tankHeight - topLimit);
break;
case "cone_top":
totalVolume = getTotalVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom);
filledVolume = getPartialVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeConeTop(coneHeight, cylinderHeight, radiusTop, radiusBottom, tankHeight - topLimit);
break;
case "cone_bottom":
totalVolume = getTotalVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom);
filledVolume = getPartialVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeConeBottom(coneHeight, cylinderHeight, radiusTop, radiusBottom, tankHeight - topLimit);
break;
case "inv_piram":
totalVolume = getTotalVolumeInversePiramid(width, length, height, width2, length2, height2);
filledVolume = getPartialVolumeInversePiramid(width, length, height, width2, length2, height2, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeInversePiramid(width, length, height, width2, length2, height2, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeInversePiramid(width, length, height, width2, length2, height2, tankHeight - topLimit);
break;
case "horiz_caps":
totalVolume = getTotalVolumeHorizontalCapsule(radius, length);
filledVolume = getPartialVolumeHorizontalCapsule(radius, length, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeHorizontalCapsule(radius, length, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeHorizontalCapsule(radius, length, tankHeight - topLimit);
break;
case "vert_caps":
totalVolume = getTotalVolumeVerticalCapsule(radius, length);
filledVolume = getPartialVolumeVerticalCapsule(radius, length, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeVerticalCapsule(radius, length, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeVerticalCapsule(radius, length, tankHeight - topLimit);
break;
case "horiz_oval":
case "vert_oval":
totalVolume = getTotalVolumeOval(width, length, height);
filledVolume = getPartialVolumeOval(width, length, height, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeOval(width, length, height, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeOval(width, length, height, tankHeight - topLimit);
break;
case "frustrum":
totalVolume = getTotalVolumeFrustrum(height, radiusTop, radiusBottom);
filledVolume = getPartialVolumeFrustrum(height, radiusTop, radiusBottom, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeFrustrum(height, radiusTop, radiusBottom, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeFrustrum(height, radiusTop, radiusBottom, tankHeight - topLimit);
break;
case "sphere":
totalVolume = getTotalVolumeSphere(radius);
filledVolume = getPartialVolumeSphere(radius, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeSphere(radius, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeSphere(radius, tankHeight - topLimit);
break;
case "horiz_ellip":
totalVolume = getTotalVolumeHorizontalElliptical(radius, length);
filledVolume = getPartialVolumeHorizontalElliptical(radius, length, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeHorizontalElliptical(radius, length, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeHorizontalElliptical(radius, length, tankHeight - topLimit);
break;
case "horiz_stad":
totalVolume = getTotalVolumeHorizontalStadium(width, length, height);
filledVolume = getPartialVolumeHorizontalStadium(width, length, height, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeHorizontalStadium(width, length, height, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeHorizontalStadium(width, length, height, tankHeight - topLimit);
break;
case "vert_stad":
totalVolume = getTotalVolumeVerticalStadium(width, length, height);
filledVolume = getPartialVolumeVerticalStadium(width, length, height, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeVerticalStadium(width, length, height, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeVerticalStadium(width, length, height, tankHeight - topLimit);
break;
case "custom_table":
totalVolume = getTotalVolumeCustomTable(customTable);
filledVolume = getPartialVolumeCustomTable(customTable, fluidHeight);
minimumVolume = (bottomLimit == 0) ? 0 : getPartialVolumeCustomTable(customTable, bottomLimit);
maximumVolume = (topLimit == 0) ? totalVolume : getPartialVolumeCustomTable(customTable, tankHeight - topLimit);
break;
default:
throw "Unsupported tank type";
}
}
catch(err) {
node.error("Cannot calculate volume: " + err);
return;
}
// Calculate the empty volume above the fluid
var emptyVolume = (totalVolume - filledVolume);
// Calculate the usable volume, between the minimum and maximum tank limits
var usableVolume = maximumVolume - minimumVolume;
// Calculate the usable filled volume, which needs to be between minimumVolume and maximumVolume
var usableFilledVolume = Math.max(0, Math.min(maximumVolume, filledVolume) - minimumVolume);
// Calculate the usable empty volume, which is the remaining part in the usableVolume
var usableEmptyVolume = usableVolume - usableFilledVolume;
// Calculate how many percentage of the tank is filled
var fillPercentage = 100 * filledVolume / totalVolume;
// Calculate how many percentage of the tank is empty
var emptyPercentage = 100 - fillPercentage;
// Calculate how many percentage of the usable tank is filled
var usableFillPercentage = 100 * usableFilledVolume / usableVolume;
// Calculate how many percentage of the usable tank is empty
var usableEmptyPercentage = 100 - usableFillPercentage;
// Since all input values have been converted to cm, the calulated volumes will be cm3.
// If another volume unit is required, then convert the volumes to the specified format.
if (node.outputUnit !== "cm3") {
totalVolume = convert(totalVolume).from("cm3").to(node.outputUnit);
filledVolume = convert(filledVolume).from("cm3").to(node.outputUnit);
emptyVolume = convert(emptyVolume).from("cm3").to(node.outputUnit);
usableVolume = convert(usableVolume).from("cm3").to(node.outputUnit);
usableFilledVolume = convert(usableFilledVolume).from("cm3").to(node.outputUnit);
usableEmptyVolume = convert(usableEmptyVolume).from("cm3").to(node.outputUnit);
}
// Make sure all output numbers are rounded (i.e. no decimals)
totalVolume = Math.round(totalVolume);
filledVolume = Math.round(filledVolume);
emptyVolume = Math.round(emptyVolume);
usableVolume = Math.round(usableVolume);
usableFilledVolume = Math.round(usableFilledVolume);
usableEmptyVolume = Math.round(usableEmptyVolume);
fillPercentage = Math.round(fillPercentage);
emptyPercentage = Math.round(emptyPercentage);
usableFillPercentage = Math.round(usableFillPercentage);
usableEmptyPercentage = Math.round(usableEmptyPercentage);
try {
var result = {
totalVolume: totalVolume,
filledVolume: filledVolume,
emptyVolume: emptyVolume,
usableVolume: usableVolume,
usableFilledVolume: usableFilledVolume,
usableEmptyVolume: usableEmptyVolume,
fillPercentage: fillPercentage,
emptyPercentage: emptyPercentage,
usableFillPercentage: usableFillPercentage,
usableEmptyPercentage: usableEmptyPercentage
};
RED.util.setMessageProperty(msg, node.outputField, result);
}
catch(err) {
node.error("The output msg." + node.outputField + " field can not be set");
return;
}
node.send(msg);
});
}
RED.nodes.registerType("tank-volume",TankVolumeNode);
// Make all the available tank images accessible for the node's config screen.
// Don't check permissions (see https://discourse.nodered.org/t/not-sure-how-to-deal-with-httpadminroot/53473)
RED.httpAdmin.get('/tank-volume/:image', function(req, res){
const fullPath = path.join(__dirname, "tank_images", req.params.image);
res.sendFile(fullPath);
});
}