frontend/src/bin/graphs/BinGraphsVPD.tsx

303 lines
9.1 KiB
TypeScript

import { Box, CircularProgress, MenuItem, TextField } from "@mui/material";
import GrainDryingChart, { GrainDryingPoint } from "charts/GrainDryingChart";
import { GrainCable } from "models/GrainCable";
import { Plenum } from "models/Plenum";
import { UnitMeasurement } from "models/UnitMeasurement";
import moment from "moment";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
import { avg } from "utils";
interface Props {
plenums: Plenum[];
cables: GrainCable[];
measurementMap: Map<string, UnitMeasurement[]>;
returnLastVPD: (lastVPD: GrainDryingPoint | undefined) => void;
newXDomain?: number[] | string[];
multiGraphZoom?: (domain: number[] | string[]) => void;
multiGraphZoomOut?: boolean;
//perBushelCFM?: number;
}
interface UnitMeasurementHelper {
timestamp: string;
compKey: string;
measurementType: quack.MeasurementType;
value: number;
}
interface NodeOption {
name: string;
number: number;
}
export default function BinGraphsVPD(props: Props) {
const {
plenums,
cables,
measurementMap,
newXDomain,
multiGraphZoom,
multiGraphZoomOut,
returnLastVPD
//perBushelCFM
} = props;
const [vpdData, setVpdData] = useState<GrainDryingPoint[]>([]);
const [plenum, setPlenum] = useState<string>("");
const [cable, setCable] = useState<string>("");
const [cableOptions, setCableOptions] = useState<GrainCable[]>([]);
const [calculating, setCalculating] = useState(false);
const [selectedNode, setSelectedNode] = useState(-1);
const [nodeOps, setNodeOps] = useState<NodeOption[]>([]);
useEffect(() => {
let plenum: string = "";
if (plenums[0]) {
plenum = plenums[0].key();
}
let ops: GrainCable[] = [];
let cable: string = "";
cables.forEach(c => {
if (c.humidities.length > 0) {
ops.push(c);
if (ops.length === 1) {
cable = c.key();
let nodes: NodeOption[] = [];
c.humidities.forEach((hum, i) => {
nodes.push({
name: "Node" + (i + 1),
number: i
});
});
setNodeOps(nodes);
}
}
});
setPlenum(plenum);
setCable(cable);
setCableOptions(ops);
}, [plenums, cables]);
const buildHelperArray = (
plenumUM: UnitMeasurement[],
cableUM: UnitMeasurement[],
node?: number
) => {
let helper: UnitMeasurementHelper[] = [];
//doing the plenum and the cable seperately because the plenum will always be done the same way however the cable will be done differently if there is a node selected
plenumUM.forEach(unitM => {
if (
unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ||
unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
) {
unitM.values.forEach((valSet, i) => {
helper.push({
timestamp: unitM.timestamps[i],
measurementType: unitM.type,
compKey: unitM.componentId,
value: avg(valSet.values)
});
});
}
});
cableUM.forEach(unitM => {
if (
unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ||
unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
) {
unitM.values.forEach((valSet, i) => {
let newHelper = {
timestamp: unitM.timestamps[i],
measurementType: unitM.type,
compKey: unitM.componentId,
value: node !== undefined ? valSet.values[node] : avg(valSet.values)
};
helper.push(newHelper);
});
}
});
helper.sort((a, b) => {
return moment(a.timestamp).valueOf() - moment(b.timestamp).valueOf();
});
return helper;
};
// T = temperature im degrees Celsius
// RH = relative humidity (%)
const calculateVPD = (T: number, RH: number) => {
const es = 0.6108 * Math.exp((17.27 * T) / (T + 237.3));
const ea = (RH / 100) * es;
return ea - es;
};
const buildData = (measurements: UnitMeasurementHelper[]) => {
let data: GrainDryingPoint[] = [];
let recent: GrainDryingPoint | undefined;
let currentPlenumTemp: number;
let currentPlenumHum: number;
let currentCableTemp: number;
let currentCableHum: number;
if (measurements[0]) {
let lastTime = moment(measurements[0].timestamp);
measurements.forEach(m => {
let thisTime = moment(m.timestamp);
if (m.compKey === plenum) {
if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
currentPlenumTemp = m.value;
} else if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
currentPlenumHum = m.value;
}
}
if (m.compKey === cable) {
if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
currentCableTemp = m.value;
} else if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
currentCableHum = m.value;
}
}
if (currentPlenumTemp && currentPlenumHum && currentCableTemp && currentCableHum) {
if (thisTime.diff(lastTime, "seconds") >= 2) {
lastTime = thisTime;
let plenumVPD = calculateVPD(currentPlenumTemp, currentPlenumHum);
let cableVPD = calculateVPD(currentCableTemp, currentCableHum);
let dryScore = cableVPD - plenumVPD;
// if (perBushelCFM) {
// dryScore = dryScore * perBushelCFM;
// }
let newPoint: GrainDryingPoint = {
dryScore: dryScore,
timestamp: moment(m.timestamp).valueOf()
};
data.push(newPoint);
if (!recent || recent.timestamp < newPoint.timestamp) {
recent = newPoint;
}
}
}
});
}
setVpdData(data);
returnLastVPD(recent);
};
//calculate the vpd using the average values of the nodes
useEffect(() => {
setSelectedNode(-1);
setVpdData([]);
if (plenum && cable) {
let plenumUM = measurementMap.get(plenum);
let cableUM = measurementMap.get(cable);
if (plenumUM && cableUM) {
setCalculating(true);
let measurements = buildHelperArray(plenumUM, cableUM);
buildData(measurements);
setCalculating(false);
}
}
}, [plenum, cable, measurementMap]); // eslint-disable-line react-hooks/exhaustive-deps
//calculate the vpd using the specified node value
const nodeVPD = (node: number) => {
if (plenum && cable) {
setVpdData([]);
let plenumUM = measurementMap.get(plenum);
let cableUM = measurementMap.get(cable);
if (plenumUM && cableUM) {
setCalculating(true);
let measurements: UnitMeasurementHelper[] = [];
if (node === -1) {
measurements = buildHelperArray(plenumUM, cableUM);
} else {
measurements = buildHelperArray(plenumUM, cableUM, node);
}
buildData(measurements);
setCalculating(false);
}
}
};
return (
<React.Fragment>
{plenums.length > 1 && (
<TextField
style={{ marginRight: 15 }}
label="Plenum"
variant="outlined"
select
id="plenumSelector"
value={plenum}
onChange={e => {
setPlenum(e.target.value as string);
}}>
{plenums.map(k => {
return (
<MenuItem key={k.key()} value={k.key()}>
{k.name()}
</MenuItem>
);
})}
</TextField>
)}
{cableOptions.length > 1 && (
<TextField
style={{ marginRight: 15 }}
label="Cable"
variant="outlined"
select
id="cableSelector"
value={cable}
onChange={e => {
setCable(e.target.value as string);
}}>
{cableOptions.map(k => {
return (
<MenuItem key={k.key()} value={k.key()}>
{k.name()}
</MenuItem>
);
})}
</TextField>
)}
<TextField
label="Node"
variant="outlined"
select
id="nodeSelector"
value={selectedNode}
onChange={e => {
setSelectedNode(+e.target.value);
nodeVPD(+e.target.value);
}}>
<MenuItem key={"noNode"} value={-1}>
Average Score
</MenuItem>
{nodeOps.map(n => {
return (
<MenuItem key={n.name} value={n.number}>
{n.name}
</MenuItem>
);
})}
</TextField>
{calculating ? (
<Box minHeight={100} display="flex" justifyContent="center" alignItems="center">
<CircularProgress />
</Box>
) : (
vpdData.length > 0 && (
<GrainDryingChart
data={vpdData}
showStroke
displayY
newXDomain={newXDomain}
multiGraphZoom={multiGraphZoom}
multiGraphZoomOut={multiGraphZoomOut}
/>
)
)}
</React.Fragment>
);
}