finished some tweaks for the new bin page
This commit is contained in:
parent
87382dd5d8
commit
2cffc20daa
10 changed files with 225 additions and 130 deletions
|
|
@ -172,8 +172,9 @@ export function BuildCableData(bin: Bin, dims: BinDims): CableData[] {
|
||||||
distributed.forEach(orbit => {
|
distributed.forEach(orbit => {
|
||||||
const count = orbit.assignedCount ?? 0;
|
const count = orbit.assignedCount ?? 0;
|
||||||
|
|
||||||
// const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof
|
const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof
|
||||||
const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall
|
//const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall
|
||||||
|
|
||||||
const bottomY = getBottomY(orbit.radius, dims);
|
const bottomY = getBottomY(orbit.radius, dims);
|
||||||
|
|
||||||
if (orbit.orbit === 0 && count > 0) {
|
if (orbit.orbit === 0 && count > 0) {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export default function Bin3dView(props: Props) {
|
||||||
dims.diameter,
|
dims.diameter,
|
||||||
dims.sidewallHeight,
|
dims.sidewallHeight,
|
||||||
dims.hopperHeight,
|
dims.hopperHeight,
|
||||||
|
dims.roofHeight,
|
||||||
fillPercent
|
fillPercent
|
||||||
);
|
);
|
||||||
}, [isCableInventory, fillPercent, dims]);
|
}, [isCableInventory, fillPercent, dims]);
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,18 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
|
||||||
const binRadius = binDimensions.diameter / 2;
|
const binRadius = binDimensions.diameter / 2;
|
||||||
const sidewallHeight = binDimensions.sidewallHeight;
|
const sidewallHeight = binDimensions.sidewallHeight;
|
||||||
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
||||||
|
const roofHeight = binDimensions.roofHeight ?? 0;
|
||||||
|
|
||||||
const sidewallBaseY = -sidewallHeight / 2;
|
const sidewallBaseY = -sidewallHeight / 2;
|
||||||
|
const sidewallTopY = sidewallHeight / 2;
|
||||||
const hopperTipY = sidewallBaseY - hopperHeight;
|
const hopperTipY = sidewallBaseY - hopperHeight;
|
||||||
|
const roofTipY = sidewallTopY + roofHeight;
|
||||||
|
|
||||||
const maxRadiusAtY = (y: number) => {
|
const maxRadiusAtY = (y: number) => {
|
||||||
|
if (y > sidewallTopY) {
|
||||||
|
if (roofHeight <= 0 || y >= roofTipY) return 0;
|
||||||
|
return binRadius * (1 - (y - sidewallTopY) / roofHeight);
|
||||||
|
}
|
||||||
if (y >= sidewallBaseY) return binRadius;
|
if (y >= sidewallBaseY) return binRadius;
|
||||||
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
|
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
|
||||||
return binRadius * ((y - hopperTipY) / hopperHeight);
|
return binRadius * ((y - hopperTipY) / hopperHeight);
|
||||||
|
|
@ -191,7 +198,7 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
|
||||||
const getSurfaceY = (x: number, z: number): number => {
|
const getSurfaceY = (x: number, z: number): number => {
|
||||||
if (surfaceAnchors.length > 0) {
|
if (surfaceAnchors.length > 0) {
|
||||||
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
|
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
|
||||||
return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw));
|
return Math.max(grainFloorY, Math.min(roofTipY, raw));
|
||||||
}
|
}
|
||||||
return flatMaxY ?? sidewallBaseY;
|
return flatMaxY ?? sidewallBaseY;
|
||||||
};
|
};
|
||||||
|
|
@ -415,6 +422,8 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
|
||||||
binRadius,
|
binRadius,
|
||||||
targetMoisture,
|
targetMoisture,
|
||||||
hopperHeight,
|
hopperHeight,
|
||||||
|
roofHeight,
|
||||||
|
roofTipY,
|
||||||
flatMaxY,
|
flatMaxY,
|
||||||
flatMinY,
|
flatMinY,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -133,13 +133,23 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
|
||||||
const binRadius = binDimensions.diameter / 2;
|
const binRadius = binDimensions.diameter / 2;
|
||||||
const sidewallHeight = binDimensions.sidewallHeight;
|
const sidewallHeight = binDimensions.sidewallHeight;
|
||||||
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
||||||
|
const roofHeight = binDimensions.roofHeight ?? 0;
|
||||||
const upperThreshold = targetTemp;
|
const upperThreshold = targetTemp;
|
||||||
|
|
||||||
const sidewallBaseY = -sidewallHeight / 2;
|
const sidewallBaseY = -sidewallHeight / 2;
|
||||||
|
const sidewallTopY = sidewallHeight / 2;
|
||||||
const hopperTipY = sidewallBaseY - hopperHeight;
|
const hopperTipY = sidewallBaseY - hopperHeight;
|
||||||
|
const roofTipY = sidewallTopY + roofHeight;
|
||||||
|
|
||||||
const maxRadiusAtY = (y: number) => {
|
const maxRadiusAtY = (y: number) => {
|
||||||
|
// Inside the roof cone — tapers from binRadius at sidewallTopY to 0 at roofTipY
|
||||||
|
if (y > sidewallTopY) {
|
||||||
|
if (roofHeight <= 0 || y >= roofTipY) return 0;
|
||||||
|
return binRadius * (1 - (y - sidewallTopY) / roofHeight);
|
||||||
|
}
|
||||||
|
// Straight sidewall
|
||||||
if (y >= sidewallBaseY) return binRadius;
|
if (y >= sidewallBaseY) return binRadius;
|
||||||
|
// Inside the hopper cone — tapers from binRadius at sidewallBaseY to 0 at hopperTipY
|
||||||
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
|
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
|
||||||
return binRadius * ((y - hopperTipY) / hopperHeight);
|
return binRadius * ((y - hopperTipY) / hopperHeight);
|
||||||
};
|
};
|
||||||
|
|
@ -192,7 +202,7 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
|
||||||
const getSurfaceY = (x: number, z: number): number => {
|
const getSurfaceY = (x: number, z: number): number => {
|
||||||
if (surfaceAnchors.length > 0) {
|
if (surfaceAnchors.length > 0) {
|
||||||
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
|
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
|
||||||
return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw));
|
return Math.max(grainFloorY, Math.min(roofTipY, raw));
|
||||||
}
|
}
|
||||||
return flatMaxY ?? sidewallBaseY;
|
return flatMaxY ?? sidewallBaseY;
|
||||||
};
|
};
|
||||||
|
|
@ -416,6 +426,8 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
|
||||||
binRadius,
|
binRadius,
|
||||||
upperThreshold,
|
upperThreshold,
|
||||||
hopperHeight,
|
hopperHeight,
|
||||||
|
roofHeight,
|
||||||
|
roofTipY,
|
||||||
flatMaxY,
|
flatMaxY,
|
||||||
flatMinY,
|
flatMinY,
|
||||||
]);
|
]);
|
||||||
|
|
@ -456,4 +468,3 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ConeVolume, CylinderVolume } from "common/TrigFunctions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Y bounds of filled grain volume — matches GrainFillFlat / GrainCableFill fallback volume math.
|
* Y bounds of filled grain volume — matches GrainFillFlat / GrainCableFill fallback volume math.
|
||||||
*/
|
*/
|
||||||
|
|
@ -6,28 +8,40 @@ export interface GrainYBounds {
|
||||||
maxY: number;
|
maxY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grain never physically fills all the way to the roof tip — cap roof fill
|
||||||
|
// at this fraction of roofHeight so there is always visible breathing room.
|
||||||
|
const MAX_ROOF_FILL_FRACTION = 0.75;
|
||||||
|
|
||||||
export function grainYBoundsFromFillPercent(
|
export function grainYBoundsFromFillPercent(
|
||||||
diameter: number,
|
diameter: number,
|
||||||
sidewallHeight: number,
|
sidewallHeight: number,
|
||||||
hopperHeight: number,
|
hopperHeight: number,
|
||||||
|
roofHeight: number,
|
||||||
fillPercent: number
|
fillPercent: number
|
||||||
): GrainYBounds {
|
): GrainYBounds {
|
||||||
const radius = diameter / 2;
|
const radius = diameter / 2;
|
||||||
const sidewallBaseY = -sidewallHeight / 2;
|
const sidewallBaseY = -sidewallHeight / 2;
|
||||||
const hopperTipY = sidewallBaseY - hopperHeight;
|
const hopperTipY = sidewallBaseY - hopperHeight;
|
||||||
|
const roofBaseY = sidewallHeight / 2;
|
||||||
|
|
||||||
if (fillPercent <= 0) {
|
if (fillPercent <= 0) {
|
||||||
return { minY: sidewallBaseY, maxY: sidewallBaseY };
|
return { minY: sidewallBaseY, maxY: sidewallBaseY };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cylinderVolume = Math.PI * radius * radius * sidewallHeight;
|
const cylinderVolume = CylinderVolume(radius, sidewallHeight);
|
||||||
const hopperVolume =
|
const hopperVolume = hopperHeight > 0 ? ConeVolume(radius, hopperHeight) : 0;
|
||||||
hopperHeight > 0 ? (1 / 3) * Math.PI * radius * radius * hopperHeight : 0;
|
const roofVolume = roofHeight > 0 ? ConeVolume(radius, roofHeight) : 0;
|
||||||
const totalVolume = cylinderVolume + hopperVolume;
|
|
||||||
|
// Volume of the usable roof portion (up to MAX_ROOF_FILL_FRACTION of height).
|
||||||
|
// A cone fills as h³ so the capped volume fraction = MAX_ROOF_FILL_FRACTION³.
|
||||||
|
const roofVolumeCapped = roofVolume * Math.pow(MAX_ROOF_FILL_FRACTION, 3);
|
||||||
|
|
||||||
|
const totalVolume = cylinderVolume + hopperVolume + roofVolumeCapped;
|
||||||
const filledVolume = totalVolume * fillPercent;
|
const filledVolume = totalVolume * fillPercent;
|
||||||
|
|
||||||
|
// Fill the hopper first
|
||||||
if (hopperHeight > 0 && filledVolume <= hopperVolume) {
|
if (hopperHeight > 0 && filledVolume <= hopperVolume) {
|
||||||
const ratio = filledVolume / hopperVolume;
|
const ratio = filledVolume / hopperVolume;
|
||||||
const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3);
|
const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3);
|
||||||
return {
|
return {
|
||||||
minY: hopperTipY,
|
minY: hopperTipY,
|
||||||
|
|
@ -35,15 +49,23 @@ export function grainYBoundsFromFillPercent(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hopperFillHeight = hopperHeight;
|
// Then fill the cylinder
|
||||||
const remainingVolume = filledVolume - hopperVolume;
|
const volumeAfterHopper = filledVolume - hopperVolume;
|
||||||
const cylinderFillHeight = Math.max(
|
if (volumeAfterHopper <= cylinderVolume) {
|
||||||
0,
|
const cylinderFillHeight = volumeAfterHopper / (Math.PI * radius * radius);
|
||||||
remainingVolume / (Math.PI * radius * radius)
|
return {
|
||||||
);
|
minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY,
|
||||||
|
maxY: sidewallBaseY + cylinderFillHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally fill into the roof cone, capped at MAX_ROOF_FILL_FRACTION
|
||||||
|
const roofFilledVolume = volumeAfterHopper - cylinderVolume;
|
||||||
|
const roofRatio = Math.min(1, roofFilledVolume / roofVolumeCapped);
|
||||||
|
const roofFillHeight = roofHeight * MAX_ROOF_FILL_FRACTION * Math.pow(roofRatio, 1 / 3);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY,
|
minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY,
|
||||||
maxY: sidewallBaseY + cylinderFillHeight,
|
maxY: roofBaseY + roofFillHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -513,8 +513,8 @@ const dateSelector = () => {
|
||||||
<Typography textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography>
|
<Typography textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography>
|
||||||
<LinearProgress value={displayProgress} variant="determinate" />
|
<LinearProgress value={displayProgress} variant="determinate" />
|
||||||
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
<Box display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
<Typography>{startDate.format("MMM D, YYYY")}</Typography>
|
<Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography>
|
||||||
<Typography>{endDate.format("MMM D, YYYY")}</Typography>
|
<Typography variant="caption">{endDate.format("MMM D, YYYY")}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{/* date range selector */}
|
{/* date range selector */}
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,8 @@ export default function BinTableExpanded(props: Props) {
|
||||||
const inGrain = nodeIndex < cable.topNode
|
const inGrain = nodeIndex < cable.topNode
|
||||||
|
|
||||||
if (inGrain) {
|
if (inGrain) {
|
||||||
if (raw > targetTemp + 5) color = warning
|
if (raw > targetTemp + 10) color = warning
|
||||||
else if (raw > targetTemp) color = caution
|
else if (raw > targetTemp + 5) color = caution
|
||||||
else color = inBounds
|
else color = inBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +102,7 @@ export default function BinTableExpanded(props: Props) {
|
||||||
const inGrain = nodeIndex < cable.topNode
|
const inGrain = nodeIndex < cable.topNode
|
||||||
if (inGrain) {
|
if (inGrain) {
|
||||||
if (raw > targetMoisture + 1.5) color = warning
|
if (raw > targetMoisture + 1.5) color = warning
|
||||||
else if (raw > targetMoisture) color = caution
|
else if (raw > targetMoisture + 0.5) color = caution
|
||||||
else color = inBounds
|
else color = inBounds
|
||||||
}
|
}
|
||||||
return { display: raw.toFixed(2) + "%", color }
|
return { display: raw.toFixed(2) + "%", color }
|
||||||
|
|
@ -196,15 +196,15 @@ export default function BinTableExpanded(props: Props) {
|
||||||
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginY={2}>
|
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginY={2}>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Box sx={{ width: 40, height: 20, backgroundColor: inBounds, borderRadius: 1, flexShrink: 0 }} />
|
<Box sx={{ width: 40, height: 20, backgroundColor: inBounds, borderRadius: 1, flexShrink: 0 }} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>On target</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Box sx={{ width: 40, height: 20, backgroundColor: caution, borderRadius: 1, flexShrink: 0 }} />
|
<Box sx={{ width: 40, height: 20, backgroundColor: caution, borderRadius: 1, flexShrink: 0 }} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} above</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Box sx={{ width: 40, height: 20, backgroundColor: warning, borderRadius: 1, flexShrink: 0 }} />
|
<Box sx={{ width: 40, height: 20, backgroundColor: warning, borderRadius: 1, flexShrink: 0 }} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~20 °F" : "+10 °C"} above</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2, paddingBottom: 2}}>
|
<Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2, paddingBottom: 2}}>
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,7 @@ export default function BinSummary(props: Props){
|
||||||
<ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/>
|
<ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/>
|
||||||
{loading ? <Skeleton variant="rectangular" height={100} sx={{marginBottom: 2}} /> : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()}
|
{loading ? <Skeleton variant="rectangular" height={100} sx={{marginBottom: 2}} /> : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()}
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
|
{!isMobile &&
|
||||||
<Grid2 size={isMobile ? 12 : 7}>
|
<Grid2 size={isMobile ? 12 : 7}>
|
||||||
{loading ?
|
{loading ?
|
||||||
<Skeleton variant="rectangular" height={600}/>
|
<Skeleton variant="rectangular" height={600}/>
|
||||||
|
|
@ -406,8 +407,7 @@ export default function BinSummary(props: Props){
|
||||||
binPrefs={binPrefs}
|
binPrefs={binPrefs}
|
||||||
updateBinCallback={updateBinCallback}
|
updateBinCallback={updateBinCallback}
|
||||||
/>
|
/>
|
||||||
{/* only show the player on desktop */}
|
{setBin &&
|
||||||
{setBin && !isMobile &&
|
|
||||||
<Box paddingX={2} position="absolute" bottom={10} width="100%">
|
<Box paddingX={2} position="absolute" bottom={10} width="100%">
|
||||||
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
|
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -415,6 +415,7 @@ export default function BinSummary(props: Props){
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
|
}
|
||||||
<Grid2 size={isMobile ? 12 : 5}>
|
<Grid2 size={isMobile ? 12 : 5}>
|
||||||
{loading ?
|
{loading ?
|
||||||
<Skeleton variant="rectangular" height={600}/>
|
<Skeleton variant="rectangular" height={600}/>
|
||||||
|
|
@ -428,7 +429,12 @@ export default function BinSummary(props: Props){
|
||||||
linkedComponents={componentMap}
|
linkedComponents={componentMap}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
cables={cables}
|
cables={cables}
|
||||||
plenums={plenums}/>
|
plenums={plenums}
|
||||||
|
fans={fans}
|
||||||
|
heaters={heaters}
|
||||||
|
binPrefs={binPrefs}
|
||||||
|
componentMap={componentMap}
|
||||||
|
setBin={setBin}/>
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import BinAnalysisGraphs from "./binAnalysisGraphs";
|
||||||
import { GrainCable } from "models/GrainCable";
|
import { GrainCable } from "models/GrainCable";
|
||||||
import { Plenum } from "models/Plenum";
|
import { Plenum } from "models/Plenum";
|
||||||
import { Controller } from "models/Controller";
|
import { Controller } from "models/Controller";
|
||||||
|
import { useMobile } from "hooks";
|
||||||
|
import Bin3dVisualizer from "bin/bin3dVisualizer";
|
||||||
|
import BinPlayer from "bin/BinPlayer";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
|
@ -21,6 +24,12 @@ interface Props {
|
||||||
permissions: pond.Permission[]
|
permissions: pond.Permission[]
|
||||||
cables: GrainCable[]
|
cables: GrainCable[]
|
||||||
plenums: Plenum[]
|
plenums: Plenum[]
|
||||||
|
showBinTab?: boolean
|
||||||
|
fans?: Controller[]
|
||||||
|
heaters?: Controller[]
|
||||||
|
componentMap: Map<string, Component>
|
||||||
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||||
|
setBin?: React.Dispatch<React.SetStateAction<Bin>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
|
|
@ -51,8 +60,9 @@ function TabPanelMine(props: TabPanelProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinDetails (props: Props) {
|
export default function BinDetails (props: Props) {
|
||||||
const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums} = props
|
const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums, showBinTab, fans, heaters, componentMap, binPrefs, setBin} = props
|
||||||
const [currentTab, setCurrentTab] = useState(0)
|
const isMobile = useMobile()
|
||||||
|
const [currentTab, setCurrentTab] = useState((isMobile || showBinTab) ? "bin" : "table")
|
||||||
|
|
||||||
//this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis
|
//this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis
|
||||||
|
|
||||||
|
|
@ -65,22 +75,48 @@ export default function BinDetails (props: Props) {
|
||||||
textColor="primary"
|
textColor="primary"
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
sx={{ flexShrink: 0 }}>
|
sx={{ flexShrink: 0 }}>
|
||||||
<Tab label={"Table View"} value={0} />
|
{isMobile && <Tab label={"Bin"} value={"bin"} />}
|
||||||
<Tab label={"Alerts"} value={1} />
|
<Tab label={"Table"} value={"table"} />
|
||||||
<Tab label={"Controls"} value={2} />
|
<Tab label={"Alerts"} value={"alerts"} />
|
||||||
<Tab label={"Analysis"} value={3} />
|
<Tab label={"Controls"} value={"controls"} />
|
||||||
|
<Tab label={"Analysis"} value={"analysis"} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" }}>
|
||||||
<TabPanelMine value={currentTab} index={0}>
|
{(isMobile || showBinTab) &&
|
||||||
|
<TabPanelMine value={currentTab} index={"bin"}>
|
||||||
|
<Bin3dVisualizer
|
||||||
|
bin={bin}
|
||||||
|
fans={fans}
|
||||||
|
heaters={heaters}
|
||||||
|
permissions={permissions}
|
||||||
|
devices={devices}
|
||||||
|
componentDevices={componentDevices}
|
||||||
|
componentMap={componentMap}
|
||||||
|
binPrefs={binPrefs}
|
||||||
|
updateBinCallback={updateBinCallback}
|
||||||
|
/>
|
||||||
|
{setBin &&
|
||||||
|
<Box padding={1}>
|
||||||
|
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</TabPanelMine>
|
||||||
|
}
|
||||||
|
<TabPanelMine value={currentTab} index={"table"}>
|
||||||
<BinTableView bin={bin} updateBinCallback={updateBinCallback}/>
|
<BinTableView bin={bin} updateBinCallback={updateBinCallback}/>
|
||||||
|
{setBin && isMobile &&
|
||||||
|
<Box padding={1}>
|
||||||
|
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
<TabPanelMine value={currentTab} index={1}>
|
<TabPanelMine value={currentTab} index={"alerts"}>
|
||||||
<BinAlerts componentDevices={componentDevices} linkedComponents={linkedComponents} permissions={permissions}/>
|
<BinAlerts componentDevices={componentDevices} linkedComponents={linkedComponents} permissions={permissions}/>
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
<TabPanelMine value={currentTab} index={2}>
|
<TabPanelMine value={currentTab} index={"controls"}>
|
||||||
<BinControls componentDevices={componentDevices} devices={devices} linkedComponents={linkedComponents} permissions={permissions}/>
|
<BinControls componentDevices={componentDevices} devices={devices} linkedComponents={linkedComponents} permissions={permissions}/>
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
<TabPanelMine value={currentTab} index={3}>
|
<TabPanelMine value={currentTab} index={"analysis"}>
|
||||||
<BinAnalysisGraphs bin={bin} cables={cables} plenums={plenums} componentDevices={componentDevices}/>
|
<BinAnalysisGraphs bin={bin} cables={cables} plenums={plenums} componentDevices={componentDevices}/>
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
headerCellStyle: {
|
headerCellStyle: {
|
||||||
fontWeight: 650,
|
fontWeight: 650,
|
||||||
backgroundColor: darken(theme.palette.background.paper, 0.2),
|
backgroundColor: darken(theme.palette.background.paper, 0.2),
|
||||||
fontSize: 15,
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
borderTop: "1px solid black",
|
borderTop: "1px solid black",
|
||||||
borderLeft: "1px solid black",
|
borderLeft: "1px solid black",
|
||||||
|
|
@ -32,6 +31,18 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
borderRight: "1px solid black",
|
borderRight: "1px solid black",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mobileHeaderCellStyle: {
|
||||||
|
fontWeight: 650,
|
||||||
|
backgroundColor: darken(theme.palette.background.paper, 0.2),
|
||||||
|
textAlign: "center",
|
||||||
|
borderTop: "1px solid black",
|
||||||
|
borderLeft: "1px solid black",
|
||||||
|
borderBottom: "1px solid black",
|
||||||
|
"&:last-child": {
|
||||||
|
borderRight: "1px solid black",
|
||||||
|
},
|
||||||
|
padding: 2
|
||||||
|
},
|
||||||
cellStyle: {
|
cellStyle: {
|
||||||
padding: 2,
|
padding: 2,
|
||||||
fontWeight: 650,
|
fontWeight: 650,
|
||||||
|
|
@ -164,9 +175,9 @@ export default function BinTableView(props: Props) {
|
||||||
if(level.grainTemps.length > 0){
|
if(level.grainTemps.length > 0){
|
||||||
lvlTemp = avg(level.grainTemps)
|
lvlTemp = avg(level.grainTemps)
|
||||||
//if the average temp is 5 degrees above the bins threshold
|
//if the average temp is 5 degrees above the bins threshold
|
||||||
if(lvlTemp > (bin.targetTemp() + 5)){
|
if(lvlTemp > (bin.targetTemp() + 10)){
|
||||||
tempColour = warning
|
tempColour = warning
|
||||||
}else if(lvlTemp > bin.targetTemp()){
|
}else if(lvlTemp > bin.targetTemp() + 5){
|
||||||
//if the average temp is greater than 5 degrees over
|
//if the average temp is greater than 5 degrees over
|
||||||
tempColour = caution
|
tempColour = caution
|
||||||
}else {
|
}else {
|
||||||
|
|
@ -179,7 +190,7 @@ export default function BinTableView(props: Props) {
|
||||||
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
|
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
|
||||||
lvlTemp = celsiusToFahrenheit(lvlTemp)
|
lvlTemp = celsiusToFahrenheit(lvlTemp)
|
||||||
}
|
}
|
||||||
tempDisplay = lvlTemp.toFixed(2) + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")
|
tempDisplay = lvlTemp.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
//determine the moisture to show
|
//determine the moisture to show
|
||||||
|
|
@ -187,8 +198,7 @@ export default function BinTableView(props: Props) {
|
||||||
lvlMoisture = avg(level.grainMoistures)
|
lvlMoisture = avg(level.grainMoistures)
|
||||||
if(lvlMoisture > (bin.targetMoisture() + 1.5)){
|
if(lvlMoisture > (bin.targetMoisture() + 1.5)){
|
||||||
moistureColour = warning
|
moistureColour = warning
|
||||||
}else if(lvlMoisture > bin.targetMoisture()){
|
}else if(lvlMoisture > bin.targetMoisture() + 0.5){
|
||||||
//if the average temp is greater than 5 degrees over
|
|
||||||
moistureColour = caution
|
moistureColour = caution
|
||||||
}else {
|
}else {
|
||||||
moistureColour = inBounds
|
moistureColour = inBounds
|
||||||
|
|
@ -222,11 +232,11 @@ export default function BinTableView(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempThreshDisplay = () => {
|
const tempThreshDisplay = (hideUnit?: boolean) => {
|
||||||
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
|
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
|
||||||
return tempTarget.toFixed(2) + " °F"
|
return tempTarget.toFixed(2) + (hideUnit ? "" : " °F")
|
||||||
}
|
}
|
||||||
return tempTarget.toFixed(2) + " °C"
|
return tempTarget.toFixed(2) + (hideUnit ? "" : " °C")
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegendSwatch = ({ color }: { color: string }) => (
|
const LegendSwatch = ({ color }: { color: string }) => (
|
||||||
|
|
@ -287,7 +297,7 @@ export default function BinTableView(props: Props) {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box padding={2} sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
<Box padding={2} sx={{ display: "flex", flexDirection: "column", height: isMobile ? "88%" : "100%" }}>
|
||||||
{expandedTableDialog()}
|
{expandedTableDialog()}
|
||||||
{/* Level Table */}
|
{/* Level Table */}
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
|
@ -311,80 +321,80 @@ export default function BinTableView(props: Props) {
|
||||||
<Box sx={{ overflowY: "auto", flex: 1, minHeight: 0 }}>
|
<Box sx={{ overflowY: "auto", flex: 1, minHeight: 0 }}>
|
||||||
<Table className={classes.tableStyle}>
|
<Table className={classes.tableStyle}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow ref={headerRow1Ref}>
|
<TableRow ref={headerRow1Ref}>
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
|
{/* "Targets" cell — hide on mobile when either field is being edited */}
|
||||||
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
{!(isMobile && (enterTemp || enterMoisture)) && (
|
||||||
<GppGood />
|
<TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{top: 0, position: "sticky", zIndex: 3 }}>
|
||||||
<Typography fontWeight={650}>
|
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
Targets
|
<GppGood />
|
||||||
</Typography>
|
{!isMobile &&
|
||||||
</Box>
|
<Typography fontSize={16} fontWeight={650}>
|
||||||
</TableCell>
|
Targets
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
|
</Typography>
|
||||||
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
}
|
||||||
<TemperatureIcon heightWidth={30}/>
|
</Box>
|
||||||
{enterTemp ?
|
</TableCell>
|
||||||
updateTempEntry()
|
)}
|
||||||
:
|
|
||||||
<Typography fontWeight={650}>
|
{/* Temperature cell */}
|
||||||
{tempThreshDisplay()}
|
{!(isMobile && enterMoisture) && (
|
||||||
</Typography>
|
<TableCell
|
||||||
}
|
className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle}
|
||||||
{enterTemp ?
|
colSpan={isMobile && enterTemp ? 3 : 1}
|
||||||
<IconButton
|
sx={{ top: 0, position: "sticky", zIndex: 3 }}
|
||||||
onClick={() => {
|
>
|
||||||
setEnterTemp(false)
|
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
//and update the bin with the new temp target
|
<TemperatureIcon heightWidth={isMobile ? 25 : 30} />
|
||||||
update()
|
{enterTemp ? updateTempEntry() : (
|
||||||
}}
|
<Typography fontSize={isMobile ? 12 : 16} fontWeight={650}>
|
||||||
>
|
{tempThreshDisplay(isMobile)}
|
||||||
<Save />
|
</Typography>
|
||||||
</IconButton>
|
)}
|
||||||
:
|
{enterTemp ? (
|
||||||
<IconButton
|
<IconButton onClick={() => { setEnterTemp(false); update(); }}>
|
||||||
onClick={() => {
|
<Save />
|
||||||
setEnterTemp(true)
|
</IconButton>
|
||||||
}}>
|
) : (
|
||||||
<Edit />
|
<IconButton onClick={() => setEnterTemp(true)}>
|
||||||
</IconButton>
|
<Edit />
|
||||||
}
|
</IconButton>
|
||||||
</Box>
|
)}
|
||||||
</TableCell>
|
</Box>
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
|
</TableCell>
|
||||||
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
)}
|
||||||
<HumidityIcon height={30} width={20}/>
|
{/* Moisture cell — hide on mobile when temperature is being edited */}
|
||||||
{enterMoisture ?
|
{!(isMobile && enterTemp) && (
|
||||||
updateMoistureEntry()
|
<TableCell
|
||||||
:
|
className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle}
|
||||||
<Typography fontWeight={650} sx={{marginLeft: "10px"}}>
|
colSpan={isMobile && enterMoisture ? 3 : 1}
|
||||||
{bin.targetMoisture()}%
|
sx={{ top: 0, position: "sticky", zIndex: 3 }}
|
||||||
</Typography>
|
>
|
||||||
}
|
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
{enterMoisture ?
|
<HumidityIcon height={isMobile ? 25 : 30} width={20} />
|
||||||
<IconButton
|
{enterMoisture ? updateMoistureEntry() : (
|
||||||
onClick={() => {
|
<Typography fontSize={isMobile ? 12 : 16} fontWeight={650} sx={{ marginLeft: "10px" }}>
|
||||||
setEnterMoisture(false)
|
{bin.targetMoisture()}%
|
||||||
//and update the bin with the new temp target
|
</Typography>
|
||||||
update()
|
)}
|
||||||
}}
|
{enterMoisture ? (
|
||||||
>
|
<IconButton onClick={() => { setEnterMoisture(false); update(); }}>
|
||||||
<Save />
|
<Save />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
:
|
) : (
|
||||||
<IconButton
|
<IconButton onClick={() => setEnterMoisture(true)}>
|
||||||
onClick={() => {
|
<Edit />
|
||||||
setEnterMoisture(true)
|
</IconButton>
|
||||||
}}>
|
)}
|
||||||
<Edit />
|
</Box>
|
||||||
</IconButton>
|
</TableCell>
|
||||||
}
|
)}
|
||||||
</Box>
|
</TableRow>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Level</TableCell>
|
<TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Level</TableCell>
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Temperature</TableCell>
|
<TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>
|
||||||
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Moisture</TableCell>
|
{isMobile ? "T" : "Temperature"}{(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " (°F)" : " (°C)")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>EMC (%)</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
|
|
@ -402,18 +412,17 @@ export default function BinTableView(props: Props) {
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" marginTop={2} sx={{ flexShrink: 0 }}>
|
<Box display="flex" alignItems="center" justifyContent="space-between" marginTop={2} sx={{ flexShrink: 0 }}>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<LegendSwatch color={inBounds} />
|
<LegendSwatch color={inBounds} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>On target</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<LegendSwatch color={caution} />
|
<LegendSwatch color={caution} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} (0.5%) above</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<LegendSwatch color={warning} />
|
<LegendSwatch color={warning} />
|
||||||
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target</Typography>
|
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~20 °F" : "+10 °C"} (1.5%) above</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue