frontend/src/bin/BinSVGV2.tsx

1059 lines
36 KiB
TypeScript

import { Box, Theme, ToggleButton, ToggleButtonGroup, darken } from "@mui/material";
import { makeStyles, withStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import { ExtractMoisture } from "grain";
import { GrainCable } from "models/GrainCable";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { useState } from "react";
import { getThemeType } from "theme/themeType";
import { avg } from "utils";
const GRAIN_COLOUR = "#fff302";
const BIN_ASPECT_RATIO = 0.45;
const useStyles = makeStyles((theme: Theme) => {
return ({
"@keyframes hotNode": {
from: {
fill: "#fff"
},
to: {
fill: "#c42605"
}
},
"@keyframes coldNode": {
from: {
fill: "#fff"
},
to: {
fill: "#0575E6"
}
},
"@keyframes headspaceWarning": {
from: {
fill: "#d18221"
},
to: {
fill: "#9a9d9f"
}
},
"@keyframes headspaceCritical": {
from: {
fill: "#ff0000"
},
to: {
fill: "#9a9d9f"
}
},
outerShell: {
fill: "url(#outerShellGradient)"
},
innerShell: {
fill: "url(#linearGradient2)"
},
container: {
fill: "#a1a9b1"
},
headspaceNormal: {
fill: "#9a9d9f"
},
headspaceWarning: {
fill: "#9a9d9f",
animation: "$headspaceWarning 0.5s alternate infinite"
},
headspaceCritical: {
fill: "#9a9d9f",
animation: "$headspaceCritical 0.5s alternate infinite"
},
bottom: {
fill: "#9c9fa2"
},
lidarGrainEstimate: {
stroke: "#FFFFFF",
strokeWidth: 10,
strokeOpacity: 0.85
},
grainInventory: {
fill: GRAIN_COLOUR,
opacity: 0.5
},
cableLine: {
strokeWidth: 10,
strokeOpacity: 0.85
},
cableNode: {
fill: "#fff"
},
excludedNode: {
fill: "grey"
},
hotNode: {
fill: "#fff",
animation: "$hotNode 1.0s infinite"
},
coldNode: {
fill: "#fff",
animation: "$coldNode 1.0s infinite"
},
smallToggle: {
position: "relative",
marginBottom: 10
},
fullToggle: {
position: "inherit",
marginBottom: 20
},
tempHum: {
fontSize: 30,
strokeWidth: 2.5,
alignmentBaseline: "middle",
textAnchor: "middle",
filter: "drop-shadow(20px 20px 20px rgb(0 0 0))"
},
clickableArea: {
fill: "transparent",
"&:hover": {
fill: "lightGrey",
opacity: 0.3,
cursor: "pointer"
}
},
cableGrainEstimate: {
stroke: "white",
strokeWidth: 20,
strokeOpacity: 0.85,
fill: "transparent"
}
});
});
interface Props {
height: number;
binShape?: pond.BinShape;
fillPercentage: number;
lidarEstimate?: number;
cables?: GrainCable[];
co2Sensors: pond.BinCO2[];
hasMinHeight?: boolean;
highTemp: number;
lowTemp: number;
showTempHum?: boolean;
isFullScreen?: boolean;
grainType?: pond.Grain;
cableNodeClicked?: (cable: GrainCable, fillNode: number) => void;
cableEstimate?: boolean;
hottestNodeTemp?: number;
coldestNodeTemp?: number;
inventoryControl?: pond.BinInventoryControl;
}
interface GrainNodePoint {
x: number;
y: number;
}
interface EllipseData {
key: string | number;
xCenter: string | number;
yCenter: string | number;
xRadius: string | number;
yRadius: string | number;
fillColour: string;
}
interface NodeData { //this could be either a box or a circle depending
key: string | number
x: number;
y: number
width: string | number
height: string | number
borderRad: string | number
nodeNumber: number
nodeTemp: number,
excluded: boolean,
}
interface CircleData {
key: string | number
centerX: string | number
centerY: string | number
radius: string | number
onClick?: () => void
}
interface TextData {
key: number | string,
x: number,
y: number,
stroke: string,
filter?: string,
value: string
}
export default function BinSVGV2(props: Props) {
const classes = useStyles();
const {
binShape,
height,
fillPercentage,
lidarEstimate,
cables,
hasMinHeight,
highTemp,
lowTemp,
showTempHum,
isFullScreen,
grainType,
cableNodeClicked,
cableEstimate,
hottestNodeTemp,
coldestNodeTemp,
inventoryControl,
co2Sensors
} = props;
const [{ user }] = useGlobalState();
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
const [nodeWarning, setNodeWarning] = useState(false);
// const StyledToggleButtonGroup = withStyles(theme => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }))(ToggleButtonGroup);
// const StyledToggle = withStyles({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// })(ToggleButton);
const gradients = () => {
const burning = "#8a1f0d";
const hot = "#c42605";
const warm = "#d75719";
const cool = "#429bb8";
const cold = "#0575E6";
const freezing = "#021B79";
return (
<React.Fragment>
<radialGradient id="nodeBurning">
<stop offset="0%" stopColor={burning} stopOpacity="0.8" />
<stop offset="60%" stopColor={hot} stopOpacity="0.4" />
<stop offset="80%" stopColor={warm} stopOpacity="0.2" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeHot">
<stop offset="10%" stopColor={hot} stopOpacity="0.8" />
<stop offset="55%" stopColor={warm} stopOpacity="0.4" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeWarm">
<stop offset="20%" stopColor={warm} stopOpacity="0.8" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeStable">
<stop offset="0%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeCool">
<stop offset="20%" stopColor={cool} stopOpacity="0.8" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeCold">
<stop offset="10%" stopColor={cold} stopOpacity="0.8" />
<stop offset="55%" stopColor={cool} stopOpacity="0.4" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<radialGradient id="nodeFreezing">
<stop offset="0%" stopColor={freezing} stopOpacity="0.8" />
<stop offset="60%" stopColor={cold} stopOpacity="0.4" />
<stop offset="80%" stopColor={cool} stopOpacity="0.2" />
<stop offset="100%" stopColor={GRAIN_COLOUR} stopOpacity="0" />
</radialGradient>
<linearGradient id="outerShellGradient" gradientTransform="rotate(90)">
<stop offset="10%" stopColor="#cacdd3" />
<stop offset="90%" stopColor="#b5bcc4" />
</linearGradient>
<linearGradient id="innerShellGradient" gradientTransform="rotate(90)">
<stop offset="10%" stopColor="#b5bcc4" />
<stop offset="90%" stopColor="#cacdd3" />
</linearGradient>
</React.Fragment>
);
};
const getGrainGradient = (temperature: number): string => {
let nodeTemp = temperature;
if (highTemp === 0 && lowTemp === 0) {
if (nodeTemp >= 30) return "url(#nodeBurning)";
else if (nodeTemp >= 25) return "url(#nodeHot)";
else if (nodeTemp >= 20) return "url(#nodeWarm)";
else if (nodeTemp >= 15) return "url(#nodeStable)";
else if (nodeTemp >= 10) return "url(#nodeCool)";
else if (nodeTemp >= 5) return "url(#nodeCold)";
else return "url(#nodeFreezing)";
} else {
if (nodeTemp >= highTemp + 10) return "url(#nodeBurning)";
else if (nodeTemp >= highTemp + 5) return "url(#nodeHot)";
else if (nodeTemp >= highTemp) return "url(#nodeWarm)";
else if (nodeTemp < highTemp && nodeTemp > lowTemp) return "url(#nodeStable)";
else if (nodeTemp >= lowTemp) return "url(#nodeCool)";
else if (nodeTemp >= lowTemp - 5) return "url(#nodeCold)";
else return "url(#nodeFreezing)";
}
};
const headspaceClass = () => {
if (!co2Sensors || co2Sensors.length === 0) return classes.headspaceNormal;
// let testSensor = [pond.BinCO2.create({
// device: 4,
// key: "testSensor",
// name: "test Sensor",
// ppm: 1600
// })]
let highestReading = 0;
//testSensor.forEach(sensor => {
co2Sensors.forEach(sensor => {
//if the current sensor is greater the the currently highest reading change it to the current sensor
highestReading = sensor.ppm > highestReading ? sensor.ppm : highestReading;
});
if (highestReading > 1500) return classes.headspaceCritical;
if (highestReading > 1000) return classes.headspaceWarning;
return classes.headspaceNormal;
};
const borderColor = () => {
//determine color of border based on node status: > high or < low = red, within both = green
return !cables || cables.length === 0 ? "#000000" : nodeWarning ? "#ff0000" : "#00ff00";
};
//determine the pixel values for where the bottom and the top of the inventory piece are (NOTE 0,0 is the top left corner)
const inventoryTopVal = () => {
return binShape === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM ? 160 : 180;
};
const inventoryBottomVal = () => {
return binShape === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM ? 1170.3 : 1155;
};
const lidarGrainEstimate = () => {
const binTop = inventoryTopVal();
const binBottom = inventoryBottomVal();
//the y axis for svg starts at the top and goes down so in order to 'flip' the line to use the bottom of the bin subtract it from 100 since the estimate is a percentage
const grainFillTo =
((lidarEstimate ? 100 - lidarEstimate : 0) * (binBottom - binTop)) / 100 + binTop;
let controlY = grainFillTo - 60;
return (
<g key={"estimate"} id={"estimate"} data-name={"estimate"}>
<path
d={
"M 54 " +
grainFillTo +
"C 54 " +
controlY +
", 528 " +
controlY +
", 527 " +
grainFillTo
}
className={classes.lidarGrainEstimate}
strokeDasharray="20"
fill="transparent"
/>
</g>
);
};
const topNodeHat = (x: number, y: number) => {
return (
<path
d={
"M " +
(x - 40) +
" " +
(y - 30) + //starting point of the curve
" C " +
(x - 2) +
" " +
(y - 50) +
", " + //first control point
(x + 20) +
" " +
(y - 50) +
", " + //second control point
(x + 40) +
" " +
(y - 30) // ending point of the curve
}
stroke="white"
strokeWidth={10}
fill="transparent"
/>
);
};
const cableGrainEstimate = (
points: GrainNodePoint[],
cableSpacing: number,
startY: number,
endY: number
) => {
let cableGrainPath = "M 54 " + startY; //set the start
points.forEach((pathPoint, i) => {
if (i === 0) {
//for the first point
cableGrainPath = cableGrainPath + " L " + pathPoint.x + " " + pathPoint.y;
} else {
//for all other points
if (pathPoint.y === points[i - 1].y) {
//if the elevation has not changed from the last point draw a straight line
cableGrainPath = cableGrainPath + " L " + pathPoint.x + " " + pathPoint.y;
} else {
// if it has changed draw a line with 2 opposite curves
//get the midpoint for the x and y values to draw the first curve too
let midX = pathPoint.x - cableSpacing / 2;
//subtract half the difference between the two points from the current y value
let midY = pathPoint.y - (pathPoint.y - points[i - 1].y) / 2;
//the control points x is the mid point
let controlX = midX;
//the control points y is where the line is starting
let controlY = points[i - 1].y;
//draw the first curve from the previous point to the midpoint
cableGrainPath =
cableGrainPath + " Q " + controlX + " " + controlY + ", " + midX + " " + midY;
/**
* draw the second curve from the midpoint to the current point
* using T makes the svg curv derive what the control point needs to be based on the curve before it to create the opposite curve
* if there is no curve in front (using Q) it should just draw a straight line
*/
cableGrainPath = cableGrainPath + " T " + pathPoint.x + " " + pathPoint.y;
}
}
});
cableGrainPath = cableGrainPath + " L 528 " + endY;
//setCableEstimatPath(cableGrainPath)
return cableGrainPath;
};
const nodeClass = (nodeTemp: number) => {
if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) {
return classes.hotNode;
}
if (coldestNodeTemp && coldestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) {
return classes.coldNode;
}
return classes.cableNode;
};
const renderText = (text: TextData[]) => {
return text.map(text => (
<text
key={text.key}
x={text.x}
y={text.y}
stroke={text.stroke}
filter={text.filter}
className={classes.tempHum}>
{text.value}
</text>
))
}
const multiCables = () => {
if (!cables) return;
//sort the cables by node number so the one with the most nodes is in the middle
let sortedCables = cables.sort((a, b) => {
//if the cables have the same number of nodes
if (b.temperatures.length === a.temperatures.length) {
//sort by temp only last and any type that has humidity first
if (
a.settings.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP &&
b.settings.subtype !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP
) {
return 1;
} else {
return a.key() > b.key() ? 1 : -1;
}
}
//otherwise sort by the longest cable first
return b.temperatures.length - a.temperatures.length;
});
let cableLines: JSX.Element[] = [];
const centerX = 290;
const minCableX = 80; //the farthest left position
const maxCableX = 502; //the farthest right position
const spacingX = (maxCableX - minCableX) / (cables.length + 1);
const containerTopY = binShape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? 80 : 58; //the highest the cable can be drawn
const containerBottomY = binShape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? 1155 : 1170; // the lowest the cable can be drawn
/**
* variables used for the cable grain line
*/
//this array will store the path coordinates to build the curved grain estimate using the day/night cycle top node stuff
let cableGrainPathPoints: GrainNodePoint[] = [];
let cableGrainStartY = 0;
let cableGrainEndY = 0;
let leftCables = 0; //starts at 0 because the first left will be the middle
let rightCables = 1;
sortedCables.forEach((cable, cableIndex) => {
//determine x for cable when the total is odd
let cablePos = centerX;
if (cableIndex % 2 === 0) {
cablePos = cablePos - spacingX * leftCables;
leftCables++;
} else {
cablePos = cablePos + spacingX * rightCables;
rightCables++;
}
//if an even number of cables offset them to the left by half of the spacing
if (sortedCables.length % 2 === 0) {
cablePos = cablePos - spacingX / 2;
}
//after the x position is determined determine the y positions based on the x and the width and angle of the hopper and headspace of the svg
//determine top y (assuming angle is about 30 degrees)
let topY = Math.abs(centerX - cablePos) * Math.tan(20 * (Math.PI / 180)) + containerTopY;
//determine bottom y if the bin has a hopper
let bottomY = containerBottomY;
if (binShape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM) {
bottomY = containerBottomY - Math.abs(centerX - cablePos) * Math.tan(40 * (Math.PI / 180));
}
//determine the colour of the line based on if the cable is measuring humidity
let cableCol = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour();
if (cable.humidities.length === 0 || avg(cable.humidities) === 0) {
cableCol = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour();
}
//go through the temperatures to build a node for each one and place it on the line
let numNodes = cable.temperatures.length;
let topNode = cable.topNode;
const minNodeY = topY; // the top of the cable
const maxNodeY = bottomY; //the bottom of the cable
const filledToY = (fillPercentage * (minNodeY - maxNodeY)) / 100 + maxNodeY;
const nodeSpacingY = (maxNodeY - minNodeY) / (cable.temperatures.length + 1); //removing the +1 here would place the nodes starting at the bottom of the cable line rather than from the center
//let nodeHeatMap: React.SVGProps<SVGEllipseElement>[] = [];
let nodeHeatMapData: EllipseData[] = [];
//let nodes: React.SVGProps<SVGCircleElement | SVGRectElement | SVGElement>[] = [];
let nodesData: NodeData[] = [];
//let temps: React.SVGProps<SVGTextElement>[] = [];
let tempData: TextData[] = []
//let hums: React.SVGProps<SVGTextElement>[] = [];
let humData: TextData[] = [];
//let nodeClickers: React.SVGProps<SVGCircleElement>[] = [];
let nodeClickData: CircleData[] = [];
cable.temperatures.forEach((temp, index) => {
//since the array goes from the top down set the nodeNumber we are currently working with to be the number of nodes we have left
//ie if we have 2 nodes left to draw we are on node 2
let nodeNumber = numNodes;
let displayTemp = temp;
//if the node is below the top node, if there is no top node assume no nodes in grain
if (nodeNumber <= topNode) {
//check if the node is hot (this is if we want to set the bin border colour based on any node in the grain)
if (!nodeWarning && temp > highTemp) {
setNodeWarning(true);
}
}
//convert to F if that is what is in the user settings
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
displayTemp = temp * 1.8 + 32;
}
//determine how high to draw the node on the cable
const nodeY = nodeSpacingY * (index + 1) + minNodeY;
//if we are using the auto top nodes only show the heatmap for nodes at or below the top node and it is not an ecluded node
if (!cable.excludedNodes.includes(nodeNumber - 1)){
if (cable.topNode > 0) {
if (nodeNumber <= cable.topNode) {
nodeHeatMapData.push({
key: index,
xCenter: cablePos,
yCenter: nodeY,
xRadius: "120",
yRadius: "60",
fillColour: getGrainGradient(temp)
})
}
} else if (nodeY > filledToY) {
nodeHeatMapData.push({
key: index,
xCenter: cablePos,
yCenter: nodeY,
xRadius: "120",
yRadius: "60",
fillColour: getGrainGradient(temp)
})
}
}
const displayWidth = 90;
const displayHeight = 45;
const cornerRad = 10;
//add the boxes to display the temps in fulscreen view
// nodes.push(
// showTempHum ? (
// <rect
// key={"node" + index}
// x={cablePos - displayWidth / 2}
// y={nodeY - displayHeight / 2}
// width={displayWidth}
// height={displayHeight}
// rx={cornerRad}
// className={classes.cableNode}></rect>
// ) : (
// <g key={"node" + index}>
// {nodeNumber === topNode && topNodeHat(cablePos, nodeY)}
// <circle cx={cablePos} cy={nodeY} className={nodeClass(temp)} r="16"></circle>
// </g>
// )
// );
//if showTempHum is true will render as a box otherwise will render as a circle which is why the x,y, and radius values will be different
nodesData.push({
key: "node" + index,
x: showTempHum ? cablePos - displayWidth / 2 : cablePos,
y: showTempHum ? nodeY - displayHeight / 2 : nodeY,
height: displayHeight,
width: displayWidth,
borderRad: showTempHum ? cornerRad : "16",
nodeNumber: nodeNumber,
nodeTemp: temp,
excluded: cable.excludedNodes.includes(nodeNumber - 1)
})
tempData.push({
key: "node" + index + "temp",
x: cablePos,
y: nodeY + 10,
stroke: "blue",
value: displayTemp.toFixed(1) +"°",
filter: "drop-shadow( 4px 4px 10px rgba(0, 0, 0, .7))"
})
humData.push({
key: "node" + index + "hum",
x: cablePos,
y: nodeY + 10,
stroke: "green",
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%",
})
!showTempHum &&
nodeClickData.push({
key: "node"+index,
centerX: cablePos,
centerY: nodeY,
radius: "50",
onClick: () => {
if (cable.key() !== "" && cableNodeClicked) {
cableNodeClicked(cable, nodeNumber);
}
}
})
//if the current node is the top node of the cable use its x and y to add to the cable grain path
if (nodeNumber === topNode) {
//need to determin the correct control points
//if it is an even cable prepend it to the string, if it is odd append it
if (cableIndex % 2 === 0) {
cableGrainStartY = nodeY - nodeSpacingY / 2;
cableGrainPathPoints.unshift({
x: cablePos,
y: nodeY - nodeSpacingY / 2
});
//cableGrainPathPoints = (" L " + cablePos + " " + (nodeY - nodeSpacingY/2)) + cableGrainPathPoints
} else {
cableGrainEndY = nodeY - nodeSpacingY / 2;
cableGrainPathPoints.push({
x: cablePos,
y: nodeY - nodeSpacingY / 2
});
//cableGrainPathPoints = cableGrainPathPoints + (" L " + cablePos + " " + (nodeY - nodeSpacingY/2))
}
}
//decrement the number of nodes left
numNodes--;
});
cableLines.push(
<g key={"cable" + cableIndex} id={"cable " + cableIndex} data-name={"cable " + cableIndex}>
{nodeHeatMapData.map(e => (
<ellipse
key={e.key}
cx={e.xCenter}
cy={e.yCenter}
rx={e.xRadius}
ry={e.yRadius}
fill={e.fillColour}
/>
))}
<line
stroke={cableCol}
x1={cablePos}
y1={topY}
x2={cablePos}
y2={bottomY}
className={classes.cableLine}
/>
{nodesData.map(e => {
if (showTempHum) {
return (
<rect
key={e.key}
x={e.x}
y={e.y}
width={e.width}
height={e.height}
rx={e.borderRad}
className={e.excluded ? classes.excludedNode : classes.cableNode}/>
)
} else {
return (
<g key={e.key}>
{e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)}
<circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp)} r="16"></circle>
</g>
)
}
})}
{showTempHum && (extraDetails === "temps" ? renderText(tempData) : renderText(humData))}
{nodeClickData.map((e) => (
<circle
key={e.key}
onClick={e.onClick}
cx={e.centerX}
cy={e.centerY}
className={classes.clickableArea}
r={e.radius}
/>
))}
</g>
);
});
//for single cable bins the endY would still be 0 so just make it the startY to draw a straight line across the bin
if (cableGrainEndY === 0) {
cableGrainEndY = cableGrainStartY;
}
if (cableEstimate && cables.length > 0 && cableGrainPathPoints.length === cables.length) {
let cablePath = cableGrainEstimate(
cableGrainPathPoints,
spacingX,
cableGrainStartY,
cableGrainEndY
);
cableLines.unshift(
<g key={"cableGrainLvl"} id={"cableGrainLvl"} data-name={"cableGrainLvl"}>
<path d={cablePath} className={classes.cableGrainEstimate} />
</g>
);
if (inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) {
cableLines.unshift(cableEstimateInventory(cablePath));
}
}
return cableLines;
};
//inventory fill for under the top node line
const cableEstimateInventory = (estimatePath: string) => {
let dPath = estimatePath;
//use the cable estimate path for the fist section to draw
if (binShape === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM) {
//if the bin is a flat bottom draw the line down to the bottom corner across and then up
dPath = dPath + " L 527.8, 1170.3 L 53.9, 1170.3 z";
} else if (binShape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM) {
//if the bin is a hopper draw down to the corner of the container then do the hopper curve then to the other container corner and then up
dPath =
dPath +
" L 528,964.32 L 365.921,1123.724 C 320,1165 262,1165 216.079,1123.734 L 54,964.32 z";
}
return (
<path key={"InventoryFill"} className={classes.grainInventory} d={dPath} id="cableFill" />
);
};
//inventory fill for the hopper cone
const grainHopperInventory = () => {
const hopperTop = 964.93;
const hopperBottom = 1155;
//the hopper is about 18 percent of the bin svg
const hopperPercent = fillPercentage > 18 ? 100 : (fillPercentage / 18) * 100;
const hopperfill = (hopperPercent * (hopperTop - hopperBottom)) / 100 + hopperBottom;
const hopperRad = 237;
const xOffset = (hopperPercent / 100) * hopperRad;
const startX = 291 - xOffset;
const endX = 291 + xOffset;
return (
<path
className={classes.grainInventory}
d={
"m " +
startX +
"," +
hopperfill +
" L 216.079,1123.734 C 262,1165 320,1165 365.921,1123.734 L " +
endX +
"," +
hopperfill +
" z"
}
id="hopper"
/>
);
};
//inventory fill for the main container of both flat and hopper bins
const grainContainerInventory = (hopperOffset?: boolean) => {
const inventoryBottom = inventoryBottomVal();
const inventoryTop = inventoryTopVal();
//let maxY = maxInventoryY();
const grainFillTo = (fillPercentage * (inventoryTop - inventoryBottom)) / 100 + inventoryBottom;
let controlY = grainFillTo - 60;
let x1 = 54;
let x2 = 528;
let yBottom = hopperOffset ? inventoryBottom - 190.063 : inventoryBottom;
return (
<path
className={classes.grainInventory}
id={"grainLevel"}
d={
"M " +
x1 +
" " +
yBottom +
" V " +
grainFillTo +
" C " +
x1 +
" " +
controlY +
", " +
x2 +
" " +
controlY +
", " +
x2 +
" " +
grainFillTo +
" V " +
yBottom +
" Z "
}
/>
);
};
const inventoryLayer = () => {
//use the manual/adjustable inventory layer
if (binShape === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM) {
return grainContainerInventory();
} else if (binShape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM) {
return (
<React.Fragment>
{fillPercentage > 18 && grainContainerInventory(true)}
{grainHopperInventory()}
</React.Fragment>
);
}
};
const hopperSVG = () => {
return (
<g id="g24">
<path
fill={borderColor()}
d="m 301.62573,2.389772 c -6.76183,-3.15302934 -14.48963,-3.15302934 -21.00996,0 l -280.61577,133.154858 v 869.02337 0 c 0,0 0,0 0.24149378,0 l 278.68381622,252.7275 c 6.76183,6.306 17.14606,6.306 24.14938,0 l 277.71784,-251.9999 c 0.48299,-0.2425 0.72448,-0.7276 1.20747,-0.9701 v 0 -868.78087 z"
id="border"
/>
<path
fill="url(#outerShellGradient)"
d="m 300.96847,20.297314 c -6.34357,-3.063085 -13.59337,-3.063085 -19.71039,0 l -263.25808,129.356466 v 844.23357 0 c 0,0 0,0 0.226556,0 l 261.445644,245.51805 c 6.34357,6.1261 16.08548,6.1261 22.6556,0 l 260.53942,-244.81108 c 0.45311,-0.23571 0.67967,-0.70697 1.13278,-0.94259 v 0 -843.99795 z"
id="outerShell"
/>
<path
fill="url(#innerShellGradient)"
d="m 409.13869,88.201438 c -74.45256,-37.601917 -160.94891,-37.601917 -235.40147,0 l -137.73722,69.365032 v 821.40349 0 c 0,0 0,0 0,0 l 174.30657,167.92394 c 45.76642,44.1415 115.40146,44.1415 161.16788,0 l 173.64964,-167.22341 c 0.43795,-0.23344 0.65693,-0.70053 0.87591,-0.93417 v 0 -821.16985 l -137.51824,-69.365032 z"
id="innerShell"
/>
<path
className={classes.container}
d="m 400.32684,106.51307 c -69.30828,-35.35076 -149.3454,-35.35076 -218.65368,0 l -127.67316,65.35237 v 793.29873 0 -0.23258 l 162.00543,158.84571 c 42.4862,41.6303 107.07379,41.6303 149.77456,0 47.99803,-57.5062 162.22001,-158.84571 162.22001,-158.84571 v 0 -793.06615 z"
id="container"
/>
{fillPercentage > 0 &&
(inventoryControl !== pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
!inventoryControl) ? (
inventoryLayer()
) : (
<path
className={classes.bottom}
d="m 54,964.93159 L 216.079,1123.734 C 258.584,1165.414 256.80,1165.414 365.921,1123.734 L 528,964.932 z"
id="hopper"
/>
)}
<path
className={headspaceClass()}
d="m 181.82986,106.67809 -127.82986,65.18735 v 0 c 0,0 474,0 474,0 v 0 c 0,0 -127.61538,-65.41933 -127.61538,-65.41933 -69.27693,-35.261481 -149.27783,-35.261481 -218.55476,0 z"
id="headspace"
/>
</g>
);
};
const flatBottomSVG = () => {
return (
<g id="flatBottomBin">
<path
fill={borderColor()}
d="m 280.59642,2.3587839 -280.59642,134.4253061 v 1077.02571 0 c 3.7012719,26.6822 132.54555,48.1902 291,48.1902 158.45445,0 287.39876,-21.508 291,-48.1902 v 0 -1077.02571 l -280.39636,-134.4253061 c -6.7023,-3.1450452 -14.40495,-3.1450452 -21.00722,0 z"
id="border"
/>
<path
fill="url(#outerShellGradient)"
d="m 281.23994,20.291497 -263.23994,130.590673 v 1046.30233 0 c 3.472327,25.921 124.34685,46.8155 273,46.8155 148.65315,0 269.62152,-20.8945 273,-46.8155 v 0 -1046.30233 l -263.05225,-130.590673 c -6.28773,-3.055329 -13.51392,-3.055329 -19.70781,0 z"
id="outerShell"
/>
<path
fill="url(#innerShellGradient)"
d="m 252.70462,41.634256 -216.70462,111.909164 v 1054.50588 c 30.309608,20.5364 132.94484,35.6164 255,35.6164 122.05516,0 224.69039,-15.08 255,-35.6164 v -1054.50588 l -216.70462,-111.909164 c -24.22954,-12.500491 -52.36122,-12.500491 -76.59076,0 z"
id="innerShell"
/>
<path
className={classes.container}
d="m 187.30687,84.199999 -133.30687,69.900001 v 1017 0 c 2.970376,23.7999 107.92366,42.9999 237,42.9999 129.07634,0 234.02962,-19.2 237,-42.9999 v 0 -1017 l -133.21686,-70.000001 c -65.70832,-34.499999 -141.94797,-34.499999 -207.65629,0 z"
id="container"
/>
{fillPercentage > 0 &&
(inventoryControl !== pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
!inventoryControl) &&
inventoryLayer()}
<ellipse
className={fillPercentage > 0 ? classes.grainInventory : classes.bottom}
cx="290.87698"
cy="1170.2799"
rx="237.00191"
ry="43.718052"
id="bottom"
/>
<path
className={headspaceClass()}
d="m 187.29264,84.203148 -133.24203,69.896852 c 0,0 319.83487,-22.49899 473.94939,0 l -133.15206,-69.996847 c -65.67637,-34.498446 -141.87893,-34.498446 -207.5553,0 z"
id="headspace"
/>
</g>
);
};
return (
<React.Fragment>
{showTempHum && (
<Box
display="flex"
alignContent="center"
justifyContent="center"
textAlign="center"
//visibility={showTempHum ? "visible" : "hidden"}
className={!isFullScreen ? classes.smallToggle : classes.fullToggle}>
<ButtonGroup
buttons={[
{
title: isFullScreen ? "Temperature" : "Temp",
function: () => setExtraDetails("temps")
},
{
title: grainType ? (isFullScreen ? "Moisture" : "EMC") : isFullScreen ? "Humidity" : "Hum",
function: () => setExtraDetails("hums")
}
]}
toggle
/>
{/* <StyledToggleButtonGroup
value={extraDetails}
exclusive
size="small"
aria-label="Bin Mode">
<StyledToggle
value={"temps"}
aria-label="Node Temperature"
onClick={() => setExtraDetails("temps")}>
{isFullScreen ? "Temperature" : "Temp"}
</StyledToggle>
<StyledToggle
onClick={() => setExtraDetails("hums")}
value={"hums"}
aria-label="Node Humidity">
{grainType ? (isFullScreen ? "Moisture" : "EMC") : isFullScreen ? "Humidity" : "Hum"}
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box>
)}
<Box
height={height}
minHeight={hasMinHeight ? "300px" : "none"}
width={height * BIN_ASPECT_RATIO}
minWidth={hasMinHeight ? 300 * BIN_ASPECT_RATIO : "none"}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 582 1262" height="100%" width="100%">
<defs>{gradients()}</defs>
<title>Bin</title>
<g id="Bin" data-name="Bin">
{binShape === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM ? flatBottomSVG() : hopperSVG()}
<g id="BinInterior" data-name="BinInterior">
{cables && multiCables()}
{lidarEstimate && lidarGrainEstimate()}
</g>
</g>
</svg>
</Box>
</React.Fragment>
);
}