added bin svg for the card
This commit is contained in:
parent
7edba7e25b
commit
e52a9a4fbf
3 changed files with 924 additions and 27 deletions
|
|
@ -20,7 +20,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { celsiusToFahrenheit, getGrainUnit, or } from "utils";
|
||||
//import { useHistory } from "react-router";
|
||||
//import BinModeDot from "./BinModeDot";
|
||||
// import BinSVGV2 from "./BinSVGV2";
|
||||
import BinSVGV2 from "./BinSVGV2";
|
||||
import { avg } from "utils";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
|
|
@ -57,7 +57,7 @@ interface GrainDetails {
|
|||
export default function BinCard(props: Props) {
|
||||
const { bin, valDisplay } = props;
|
||||
const [cables, setCables] = useState<GrainCable[]>();
|
||||
//const [modeDetails, setModeDetails] = useState("");
|
||||
const [modeDetails, setModeDetails] = useState("");
|
||||
const [lidarPercentage, setLidarPercentage] = useState<number | undefined>();
|
||||
const [lidarBushels, setLidarBushels] = useState<number | undefined>();
|
||||
const [{ user }] = useGlobalState();
|
||||
|
|
@ -169,23 +169,23 @@ export default function BinCard(props: Props) {
|
|||
}
|
||||
}, [bin]);
|
||||
|
||||
// useEffect(() => {
|
||||
// let now = moment();
|
||||
// let duration = moment.duration(moment(bin.status.lastModeChange).diff(now));
|
||||
// let days = duration.asDays();
|
||||
// days = Math.abs(days);
|
||||
// duration.subtract(moment.duration(days, "days"));
|
||||
// let hours = duration.hours();
|
||||
// hours = Math.abs(hours);
|
||||
useEffect(() => {
|
||||
let now = moment();
|
||||
let duration = moment.duration(moment(bin.status.lastModeChange).diff(now));
|
||||
let days = duration.asDays();
|
||||
days = Math.abs(days);
|
||||
duration.subtract(moment.duration(days, "days"));
|
||||
let hours = duration.hours();
|
||||
hours = Math.abs(hours);
|
||||
|
||||
// if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) {
|
||||
// setModeDetails("Calculating...");
|
||||
// } else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) {
|
||||
// setModeDetails("No Mode");
|
||||
// } else {
|
||||
// setModeDetails(Math.floor(days) + "d " + hours + "h");
|
||||
// }
|
||||
// }, [bin]);
|
||||
if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) {
|
||||
setModeDetails("Calculating...");
|
||||
} else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) {
|
||||
setModeDetails("No Mode");
|
||||
} else {
|
||||
setModeDetails(Math.floor(days) + "d " + hours + "h");
|
||||
}
|
||||
}, [bin]);
|
||||
|
||||
const typeDisplay = () => {
|
||||
let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE;
|
||||
|
|
@ -195,9 +195,9 @@ export default function BinCard(props: Props) {
|
|||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
{GrainDescriber(grainType).name}
|
||||
</Typography>
|
||||
{/* <Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
{bin.settings.inventory?.grainSubtype}
|
||||
</Typography> */}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -393,7 +393,7 @@ export default function BinCard(props: Props) {
|
|||
{bin.status.heaters.map(controller => heaterBox(controller))}
|
||||
</Box>
|
||||
<Box style={{ flex: 1, textAlign: "center", flexDirection: "column" }}>
|
||||
{/* <BinSVGV2
|
||||
<BinSVGV2
|
||||
cableEstimate={bin.settings.autoGrainNode}
|
||||
height={160}
|
||||
binShape={bin.settings.specs?.shape}
|
||||
|
|
@ -405,8 +405,7 @@ export default function BinCard(props: Props) {
|
|||
hottestNodeTemp={valDisplay === "high" ? hotNode?.temp : undefined}
|
||||
coldestNodeTemp={valDisplay === "low" ? coldNode?.temp : undefined}
|
||||
inventoryControl={bin.inventoryControl()}
|
||||
/> */}
|
||||
hi
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -517,9 +516,9 @@ export default function BinCard(props: Props) {
|
|||
{info()}
|
||||
{componentStateBanner()}
|
||||
</Box>
|
||||
{/* <Box style={{position: "absolute", right:5, bottom: 1}}>
|
||||
<Box style={{position: "absolute", right:5, bottom: 1}}>
|
||||
<Warning />
|
||||
</Box> */}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
900
src/bin/BinSVGV2.tsx
Normal file
900
src/bin/BinSVGV2.tsx
Normal file
|
|
@ -0,0 +1,900 @@
|
|||
import { Box, Theme, ToggleButton, ToggleButtonGroup, darken } from "@mui/material";
|
||||
import { makeStyles, withStyles } from "@mui/styles";
|
||||
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"
|
||||
}
|
||||
},
|
||||
outerShell: {
|
||||
fill: "url(#outerShellGradient)"
|
||||
},
|
||||
innerShell: {
|
||||
fill: "url(#linearGradient2)"
|
||||
},
|
||||
container: {
|
||||
fill: "#a1a9b1"
|
||||
},
|
||||
headspace: {
|
||||
fill: "#9a9d9f"
|
||||
},
|
||||
bottom: {
|
||||
fill: "#9c9fa2"
|
||||
},
|
||||
lidarGrainEstimate: {
|
||||
stroke: "#FFFFFF",
|
||||
strokeWidth: 10,
|
||||
strokeOpacity: 0.85
|
||||
},
|
||||
grainInventory: {
|
||||
fill: theme.palette.secondary.main,
|
||||
opacity: 0.5
|
||||
},
|
||||
cableLine: {
|
||||
strokeWidth: 10,
|
||||
strokeOpacity: 0.85
|
||||
},
|
||||
cableNode: {
|
||||
fill: "#fff"
|
||||
},
|
||||
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[];
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
} = 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 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 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 nodes: React.SVGProps<SVGCircleElement | SVGRectElement | SVGElement>[] = [];
|
||||
let temps: React.SVGProps<SVGTextElement>[] = [];
|
||||
let hums: React.SVGProps<SVGTextElement>[] = [];
|
||||
let nodeClickers: React.SVGProps<SVGCircleElement>[] = [];
|
||||
|
||||
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
|
||||
if (cable.topNode > 0) {
|
||||
if (nodeNumber <= cable.topNode) {
|
||||
nodeHeatMap.push(
|
||||
<ellipse
|
||||
key={index}
|
||||
cx={cablePos}
|
||||
cy={nodeY}
|
||||
rx="120"
|
||||
ry="60"
|
||||
fill={getGrainGradient(temp)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (nodeY > filledToY) {
|
||||
//otherwise use the fill level of the bin
|
||||
nodeHeatMap.push(
|
||||
<ellipse
|
||||
key={index}
|
||||
cx={cablePos}
|
||||
cy={nodeY}
|
||||
rx="120"
|
||||
ry="60"
|
||||
fill={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>
|
||||
)
|
||||
);
|
||||
|
||||
temps.push(
|
||||
<text
|
||||
filter="drop-shadow( 4px 4px 10px rgba(0, 0, 0, .7))"
|
||||
key={"node" + index + "temp"}
|
||||
x={cablePos}
|
||||
y={nodeY + 10}
|
||||
stroke="blue"
|
||||
className={classes.tempHum}>
|
||||
{displayTemp.toFixed(1)}°
|
||||
</text>
|
||||
);
|
||||
hums.push(
|
||||
<text
|
||||
key={"node" + index + "hum"}
|
||||
x={cablePos}
|
||||
y={nodeY + 10}
|
||||
stroke="green"
|
||||
className={classes.tempHum}>
|
||||
{ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)}%
|
||||
</text>
|
||||
);
|
||||
!showTempHum &&
|
||||
nodeClickers.push(
|
||||
<circle
|
||||
key={"node" + index}
|
||||
onClick={() => {
|
||||
if (cable.key() !== "" && cableNodeClicked) {
|
||||
cableNodeClicked(cable, nodeNumber);
|
||||
}
|
||||
}}
|
||||
cx={cablePos}
|
||||
cy={nodeY}
|
||||
className={classes.clickableArea}
|
||||
r="50"
|
||||
/>
|
||||
);
|
||||
|
||||
//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}>
|
||||
// {nodeHeatMap}
|
||||
// <line
|
||||
// stroke={cableCol}
|
||||
// x1={cablePos}
|
||||
// y1={topY}
|
||||
// x2={cablePos}
|
||||
// y2={bottomY}
|
||||
// className={classes.cableLine}
|
||||
// />
|
||||
// {nodes}
|
||||
// {showTempHum && (extraDetails === "temps" ? temps : hums)}
|
||||
// {nodeClickers}
|
||||
// </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) {
|
||||
console.log("calc cable path");
|
||||
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_MANUAL ||
|
||||
!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={classes.headspace}
|
||||
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_MANUAL ||
|
||||
!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={classes.headspace}
|
||||
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
|
||||
textAlign="center"
|
||||
//visibility={showTempHum ? "visible" : "hidden"}
|
||||
className={!isFullScreen ? classes.smallToggle : classes.fullToggle}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@ export default function ObjectTable(props: Props) {
|
|||
// const heaterAPI = useObjectHeaterAPI();
|
||||
|
||||
const load = useCallback(() => {
|
||||
console.log("Hello!")
|
||||
if (tableLoading) return;
|
||||
/**
|
||||
* switch case to use the correct api to load the object that was selected.
|
||||
|
|
@ -81,7 +80,6 @@ export default function ObjectTable(props: Props) {
|
|||
if (currentType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) {
|
||||
setTableLoading(true);
|
||||
}
|
||||
console.log("Hello!")
|
||||
switch (currentType) {
|
||||
case pond.ObjectType.OBJECT_TYPE_BIN:
|
||||
binAPI
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue