303 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|