frontend/src/cableEstimator/cableEstimator.tsx

988 lines
34 KiB
TypeScript

import { useEffect, useState } from "react";
import {
Box,
Button,
Card,
FormControl,
FormControlLabel,
FormLabel,
Grid2 as Grid,
InputAdornment,
Radio,
RadioGroup,
Slider,
Switch,
TextField,
Typography
} from "@mui/material";
//import MaterialTable from "material-table";
//import { getTableIcons } from "common/ResponsiveTable";
import CableDisplay from "./cableDisplay";
import { distanceConversion, getDistanceUnit } from "utils";
import { pond } from "protobuf-ts/pond";
import CableQuote from "./cableQuote";
import { useMobile } from "hooks";
import { Cable, CableSum, jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import BinSelector from "bin/BinSelector";
import { cloneDeep } from "lodash";
import {
ConeVolume,
CylinderVolume,
RadiansToDegrees,
TriangleOppositeLength
} from "common/TrigFunctions";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles(() => ({
sliderThumb: {
// height: 15,
// width: 15,
marginTop: 0,
marginLeft: 0,
//backgroundColor: "yellow"
},
// sliderValLabel: {
// left: "calc(-50%)",
// top: 22,
// "& *": {
// background: "transparent",
// color: "#fff"
// }
// }
}))
export default function CableEstimator() {
//the conversion constant to convert cubic feet to bushels
const conversionConstantFT = 0.7786;
const classes = useStyles();
const [binOptions, setBinOptions] = useState<jsonBin[]>([]);
const [displayedRows, setDisplayedRows] = useState<jsonBin[]>([]);
const [tablePage, setTablePage] = useState(0)
const [pageSize, setPageSize] = useState(10)
const [rowCount, setRowCount] = useState(0)
const [orderBy, setOrderBy] = useState<string>("")
const [order, setOrder] = useState<"asc" | "desc">("asc")
const [selectedBin, setSelectedBin] = useState<jsonBin | undefined>();
const [quoteOpen, setQuoteOpen] = useState(false);
const [useCustomBin, setUseCustomBin] = useState(false);
const [diameterFormEntry, setDiameterFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30"
);
//const [capacityFormEntry, setCapacityFormEntry] = useState("0");
const [ringsFormEntry, setRingsFormEntry] = useState("0");
const [ringHeightFormEntry, setRingHeightFormEntry] = useState("0");
const [ringHeightFt, setRingHeightFt] = useState(0);
const [sidewallFormEntry, setSidewallFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7" : "23"
);
const [peakFormEntry, setPeakFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30"
);
//const [eaveToPeakFormEntry, setEaveToPeakFormEntry] = useState("0");
const [roofAngleFormEntry, setRoofAngleFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7.6" : "25"
);
const [hopperAngleFormEntry, setHopperAngleFormEntry] = useState("0");
const nodeSpacing = 4;
const [customBin, setCustomBin] = useState<jsonBin>({
ID: 0,
Manufacturer: "",
Type: "Farm", //all bins are currently farm use
Bottom: "Flat Bottom",
Diameter: 30,
Name: "",
Model: "",
Capacity: 13942,
Rings: 0,
Sidewall: 23,
Peak: 30, //absolute total height from the ground to the peak of the cone
EaveToPeak: 0, //the length along the roof from the peak to the eave (not actually used anywhere at the moment)
RoofAngle: 25,
HopperAngle: 0,
CableSums: [],
Cables: []
});
const isMobile = useMobile();
const [tableSearch, setTableSearch] = useState("")
const [loading, setLoading] = useState(false)
const columns: Column<jsonBin>[] = [
{
title: "Manufacturer",
sortKey: "Manufacturer",
render: (row: jsonBin) => <Box padding={2}>{row.Manufacturer}</Box>
},
{
title: "Model",
sortKey: "Model",
render: (row: jsonBin) => <Box padding={2}>{row.Model}</Box>
},
{
title: "Capacity",
sortKey: "Capacity",
render: (row: jsonBin) => <Box padding={2}>{row.Capacity}</Box>
},
{
title: "Hopper Angle",
sortKey: "HopperAngle",
render: (row: jsonBin) => <Box padding={2}>{row.HopperAngle}</Box>
},
{
title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
sortKey: "Diameter",
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Diameter).toFixed(2)}</Box>
},
{
title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
sortKey: "Sidewall",
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Sidewall).toFixed(2)}</Box>
},
{
title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
sortKey: "Peak",
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Peak).toFixed(2)}</Box>
},
{
title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
sortKey: "EaveToPeak",
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.EaveToPeak).toFixed(2)}</Box>
},
{
title: "Roof Angle",
sortKey: "RoofAngle",
render: (row: jsonBin) => <Box padding={2}>{row.RoofAngle}</Box>
},
{
title: "Rings",
sortKey: "Rings",
render: (row: jsonBin) => <Box padding={2}>{row.Rings}</Box>
}
]
useEffect(()=>{
setLoading(true)
//create a clone to manipulate
let clone: jsonBin[] = cloneDeep(binOptions)
//having the search filter in here is quite inefficient as it will filter each time you change pages as well
//TODO: re-work the filter so it only filters when the search is changed
// sort the array according to orderBy
clone.sort((a, b) => {
if (Object.keys(a).includes(orderBy)) {
let by = orderBy as keyof jsonBin
if (typeof(a[by]) === "string") {
// return a.localeCompare(b)
return (a[by].localeCompare(b[by] as string))
}
return ((b[by] as any) - (a[by] as any))
}
return 0
})
if (order === "desc") clone.reverse()
if(tableSearch !== ""){
//filter the data in the cloned list
clone = clone.filter(bin => {
//doing a partial search so if any string contains the search string it will return true
const lowerSearch = tableSearch.toLowerCase()
if(bin.Manufacturer.toLocaleLowerCase().includes(lowerSearch)) return true
if(bin.Model.toLowerCase().includes(lowerSearch)) return true
if(bin.Capacity.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.HopperAngle.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Diameter.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Sidewall.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Peak.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.EaveToPeak.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.RoofAngle.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Rings.toString().toLowerCase().includes(lowerSearch)) return true
return false
})
}
setRowCount(clone.length)
//split out the first x amount for the page
let rows = clone.splice(tablePage * pageSize, pageSize)
setDisplayedRows(rows)
setLoading(false)
},[binOptions, tablePage, pageSize, tableSearch, orderBy, order])
const binOpTable = () => {
return (
<ResponsiveTable<jsonBin>
title={<Typography style={{ fontSize: 20, fontWeight: 650 }}>Bins</Typography>}
isLoading={loading}
setSearchText={(search) => {setTableSearch(search)}}
onRowClick={(bin) => {
setSelectedBin(bin);
}}
page={tablePage}
columns={columns}
pageSize={pageSize}
setPage={(page) => {setTablePage(page)}}
handleRowsPerPageChange={(e) => {setPageSize(e.target.value)}}
rows={displayedRows}
total={rowCount}
order={order}
setOrder={setOrder}
orderBy={orderBy}
setOrderBy={setOrderBy}
/>
);
};
const cableSummaries = () => {
//numbers in the summaries are in feet
let summaries: CableSum[] = [];
if (customBin.Diameter < 24) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
} else if (customBin.Diameter < 36) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 7,
NumberOfCables: 3,
Bracket: 0
});
} else if (customBin.Diameter < 42) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 9,
NumberOfCables: 3,
Bracket: 0
});
} else if (customBin.Diameter < 48) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 14,
NumberOfCables: 4,
Bracket: 0
});
} else if (customBin.Diameter < 54) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 16,
NumberOfCables: 5,
Bracket: 0
});
} else if (customBin.Diameter < 60) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 18,
NumberOfCables: 6,
Bracket: 0
});
} else if (customBin.Diameter < 72) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 20,
NumberOfCables: 7,
Bracket: 0
});
} else if (customBin.Diameter < 78) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 10,
NumberOfCables: 4,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 27,
NumberOfCables: 9,
Bracket: 0
});
} else if (customBin.Diameter < 90) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 10,
NumberOfCables: 4,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 29,
NumberOfCables: 10,
Bracket: 0
});
} else if (customBin.Diameter < 105) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 18,
NumberOfCables: 6,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 36,
NumberOfCables: 12,
Bracket: 0
});
} else {
//diameter is 105 or larger
summaries.push({
Orbit: 1,
DistanceFromCenter: 9,
NumberOfCables: 3,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 26.5,
NumberOfCables: 9,
Bracket: 0
});
summaries.push({
Orbit: 3,
DistanceFromCenter: 44,
NumberOfCables: 14,
Bracket: 0
});
}
return summaries;
};
const buildCables = () => {
let bin = cloneDeep(customBin);
let binRadius = bin.Diameter / 2;
//use the diameter of the bin to build the cable summaries (orbit information)
let sums = cableSummaries();
//then use the orbits and the rest of the bins dimensions to build the cables
let hasCenter = false;
let roofRadians = bin.RoofAngle * (Math.PI / 180);
let hopperRadians = bin.HopperAngle * (Math.PI / 180);
let newCables: Cable[] = [];
sums.forEach((sum, i) => {
if (i === 0) {
hasCenter = true;
}
let hopperHeight = 0;
// calculate the hight of the cone section of the cable here
let topHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(roofRadians);
//if the hopper angle is greater than 0 calculate the hopper height for the orbit
if (bin.HopperAngle > 0) {
hopperHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(hopperRadians);
}
//get the maximum value for the top of the bin to the botton
let maxCablelength = Math.round((topHeight + bin.Sidewall + hopperHeight) * 10) / 10;
//shorten cable for the clearance from the bottom and round the cable down to the nearest even number
let lenX10 = (maxCablelength - 2) * 10;
let cableLen = (lenX10 - (lenX10 % 20)) / 10;
//set the bracket in the cableSum based on the length of the cable and whether the bin has a center cable
if (cableLen < 36) {
sum.Bracket = 3;
} else {
if (hasCenter) {
sum.Bracket = 2;
} else {
sum.Bracket = 1;
}
}
// divide the cable by the node spacing to get the number of nodes
let sensors = Math.ceil(cableLen / nodeSpacing);
//add in the cables for each orbit
//moisture cables
var moistureCables: Cable = {
Orbit: sum.Orbit,
Length: cableLen,
Nodes: sensors,
Type: 2,
Count: 1
};
newCables.push(moistureCables);
//temp Cables
if (sum.NumberOfCables > 1) {
let tempCables: Cable = {
Orbit: sum.Orbit,
Length: cableLen,
Nodes: sensors,
Type: 1,
Count: sum.NumberOfCables - 1
};
newCables.push(tempCables);
}
});
//add the summaries and cables to the bin
bin.CableSums = sums;
bin.Cables = newCables;
console.log(bin);
setCustomBin(bin);
};
const calcRoofAngle = (binRad: number, topConeHeight: number) => {
//calculate the angle using the the two given sides of a right triangle
let angle = RadiansToDegrees(Math.atan(topConeHeight / binRad)); //('arcTan(opposite / adjacent))' and convert to degrees
angle = Math.round(angle * 100) / 100; // round to 2 digits
return angle;
};
const calcConeHeight = (binRad: number, roofAngle: number) => {
return TriangleOppositeLength(binRad, roofAngle);
};
/**
* takes in the dimensions of the bin to calculate its bushel capacity
* @param diameter
* @param topConeHeight
* @param sidewallHeight
* @param hopperHeight
* @returns the bushel capacity of the bin with given dimensions
*/
const advancedGrainCalc = (bin: jsonBin) => {
let tcH = bin.Peak - bin.Sidewall;
let sH = bin.Sidewall;
let hH = TriangleOppositeLength(bin.Diameter / 2, bin.HopperAngle); //get the height of the hopper since it is not stored in the bin data
let d = bin.Diameter;
//calculate the volumes and convert them to bushels
let topConeBushels = ConeVolume(d / 2, tcH) * conversionConstantFT;
let sidewallBushels = CylinderVolume(d / 2, sH) * conversionConstantFT;
let hopperBushels = ConeVolume(d / 2, hH ?? 0) * conversionConstantFT;
return Math.round(topConeBushels + sidewallBushels + hopperBushels);
};
const customBinForm = () => {
return (
<Card style={{ padding: 20 }}>
<Grid container spacing={2}>
{/* first row */}
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Manufacturer"
value={customBin.Manufacturer}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Manufacturer = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Name"
value={customBin.Name}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Name = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Model"
value={customBin.Model}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Model = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
{/* second row */}
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Diameter"
helperText={"The full diameter of the bin"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
value={diameterFormEntry}
error={isNaN(parseFloat(diameterFormEntry))}
onChange={e => {
setDiameterFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
//when the diameter is changed use the current peak height to adjust the roof angle
let bin = cloneDeep(customBin);
bin.Diameter = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:6}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Peak Height"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
helperText={
"total height from the bottom of the sidewall to the top of the roof, calculated from the diameter, roof angle, and sidewall height of the bin"
}
value={peakFormEntry}
error={isNaN(parseFloat(peakFormEntry))}
onChange={e => {
setPeakFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
//when the peak height is changed use the diameter to calculate the roof angle
let bin = cloneDeep(customBin);
bin.Peak = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
{/* third row */}
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Rings"
helperText={"Number of rings along the sidewall"}
value={ringsFormEntry}
error={isNaN(parseFloat(ringsFormEntry))}
onChange={e => {
setRingsFormEntry(e.target.value);
let bin = cloneDeep(customBin);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
bin.Rings = val;
//use the new number of rings and ring height to get the sidewall height
let sidewallFt = val * ringHeightFt;
bin.Sidewall = sidewallFt;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
setSidewallFormEntry((sidewallFt / 3.281).toString());
} else {
setSidewallFormEntry(sidewallFt.toString());
}
//with the sidewall height changing the roof ange should also change
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Height of Ring"
helperText={"The Height of a single ring/band of the bin"}
InputProps={{
endAdornment: <InputAdornment position="end">{"in"}</InputAdornment>
}}
value={ringHeightFormEntry}
error={isNaN(parseFloat(ringHeightFormEntry))}
onChange={e => {
setRingHeightFormEntry(e.target.value);
let inVal = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
// if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
// val = val * 3.281;
// }
let ringFt = inVal / 12;
setRingHeightFt(ringFt);
//based on this entry and the number of rings calculate the sidwall
let bin = cloneDeep(customBin);
let sidewallFt = bin.Rings * ringFt;
bin.Sidewall = sidewallFt;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
setSidewallFormEntry((sidewallFt / 3.281).toString());
} else {
setSidewallFormEntry(sidewallFt.toString());
}
//with the sidewall height changing the roof angle should also change with it
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Bin Sidewall Height"
helperText={"Full height of all of the rings together"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
value={sidewallFormEntry}
error={isNaN(parseFloat(sidewallFormEntry))}
onChange={e => {
//need to handle conversions here
setSidewallFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
//if the value entered is meters convert it to feet for the bin
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
let bin = cloneDeep(customBin);
bin.Sidewall = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
{/* fourth row */}
<Grid size={{xs:12, md:6}}>
<Box display="flex" justifyContent="center" alignItems="center">
<Slider
classes={{
thumb: classes.sliderThumb,
//valueLabel: classes.sliderValLabel
}}
valueLabelDisplay="off"
min={20}
max={40}
name="roofAngle"
aria-label="roofAngle"
step={0.1}
color="secondary"
//track={false}
value={customBin.RoofAngle}
onChange={(_, newValue) => {
let val = newValue as number;
setRoofAngleFormEntry(val.toString());
let bin = cloneDeep(customBin);
bin.RoofAngle = val;
//adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin
let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle);
bin.Peak = bin.Sidewall + coneHeight; //assign it as feet
let peakHeightDisplay = bin.Sidewall + coneHeight;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
peakHeightDisplay = peakHeightDisplay / 3.281;
}
setPeakFormEntry(peakHeightDisplay.toFixed(1));
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
<TextField
style={{ marginLeft: 10 }}
type="number"
variant="outlined"
label="Roof Angle"
helperText={"Calculated from diameter, sidewall and peak height. Usually 25-35"}
InputProps={{
endAdornment: <InputAdornment position="end">{"deg"}</InputAdornment>
}}
value={roofAngleFormEntry}
error={isNaN(parseFloat(roofAngleFormEntry))}
onChange={e => {
setRoofAngleFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.RoofAngle = isNaN(parseFloat(e.target.value))
? 0
: parseFloat(e.target.value);
//adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin
let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle);
bin.Peak = bin.Sidewall + coneHeight;
let peakHeightDisplay = bin.Sidewall + coneHeight;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
peakHeightDisplay = peakHeightDisplay / 3.281;
}
setPeakFormEntry(peakHeightDisplay.toFixed(1));
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Box>
</Grid>
<Grid size={{xs:12, md:6}}>
<Box display="flex" justifyContent="center" alignItems="center">
<Slider
classes={{
thumb: classes.sliderThumb,
//valueLabel: classes.sliderValLabel,
}}
valueLabelDisplay="off"
min={20}
max={40}
name="hopperAngle"
aria-label="hopperAngle"
step={0.1}
color="secondary"
disabled={customBin.Bottom === "Flat Bottom"}
//track={false}
value={customBin.HopperAngle}
onChange={(_, newValue) => {
setHopperAngleFormEntry(newValue.toString());
let bin = cloneDeep(customBin);
bin.HopperAngle = newValue as number;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
<TextField
type="number"
style={{ marginLeft: 10 }}
fullWidth
variant="outlined"
disabled={customBin.Bottom === "Flat Bottom"}
label="Hopper Angle"
helperText={"Calculated from the hopper height and diameter"}
InputProps={{
endAdornment: <InputAdornment position="end">{"deg"}</InputAdornment>
}}
value={hopperAngleFormEntry}
error={isNaN(parseFloat(hopperAngleFormEntry))}
onChange={e => {
setHopperAngleFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.HopperAngle = isNaN(parseFloat(e.target.value))
? 0
: parseFloat(e.target.value);
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Box>
</Grid>
{/* bottom */}
<Grid>
<FormControl
component="fieldset"
variant="outlined">
<FormLabel component="legend">Shape</FormLabel>
<RadioGroup
aria-label="bin shape"
name="binShape"
value={customBin.Bottom}
onChange={(_, value) => {
let bin = cloneDeep(customBin);
bin.Bottom = value;
if (value === "Hopper Bottom") {
bin.HopperAngle = 25;
setHopperAngleFormEntry("25");
} else {
bin.HopperAngle = 0;
setHopperAngleFormEntry("0");
}
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}>
<FormControlLabel
key={"flat"}
value={"Flat Bottom"}
control={<Radio />}
label={"Flat Bottom"}
/>
<FormControlLabel
key={"hopper"}
value={"Hopper Bottom"}
control={<Radio />}
label={"Hopper"}
/>
</RadioGroup>
</FormControl>
</Grid>
</Grid>
{/* not strictly necessary for quoting */}
{/* <TextField
type="number"
variant="outlined"
label="Bin Capacity"
value={capacityFormEntry}
error={isNaN(parseFloat(capacityFormEntry))}
onChange={e => {
setCapacityFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.Capacity = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
setCustomBin(bin);
}}
/> */}
{/* this dimension isn't actually used for anything */}
{/* <TextField
type="number"
variant="outlined"
label="Eave to Peak"
value={eaveToPeakFormEntry}
error={isNaN(parseFloat(eaveToPeakFormEntry))}
onChange={e => {
//need to handle conversions here
setEaveToPeakFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
//if the value entered is meters convert it to feet for the bin
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
let bin = cloneDeep(customBin);
bin.EaveToPeak = val;
setCustomBin(bin);
}}
/> */}
<Button variant="contained" color="primary" onClick={buildCables}>
Generate Cables
</Button>
<Typography style={{ fontWeight: 650 }}>
Estimated Capacity: {customBin.Capacity} bushels
</Typography>
</Card>
);
};
const binDisplay = () => {
let displayBin = selectedBin;
if (useCustomBin) {
displayBin = customBin;
}
if (displayBin) {
return (
<CableDisplay
bin={displayBin}
updateBin={newBin => {
setSelectedBin(newBin);
}}
/>
);
}
};
const quoteDisplay = () => {
let displayBin = selectedBin;
if (useCustomBin) {
displayBin = customBin;
}
if (displayBin) {
return (
<CableQuote
bin={displayBin}
open={quoteOpen}
close={() => {
setQuoteOpen(false);
}}
/>
);
}
};
return (
<Box padding={2}>
<FormControlLabel
label="Custom Bin"
control={
<Switch
color="default"
value={useCustomBin}
checked={useCustomBin}
onChange={(_, checked) => {
setUseCustomBin(checked);
}}
name="storage"
/>
}
/>
{/* grid of select boxes for pre-built options */}
<Grid container direction="row" spacing={2} alignContent="center" alignItems="center">
{!useCustomBin && (
<Grid size={isMobile ? 12 : 10}>
<BinSelector
optionsChanged={ops => {
setBinOptions(ops);
}}
/>
</Grid>
)}
<Grid size={isMobile ? 12 : 2}>
<Button
variant="contained"
color="primary"
onClick={() => {
setQuoteOpen(true);
}}>
Generate Quote
</Button>
</Grid>
</Grid>
{useCustomBin ? customBinForm() : binOpTable()}
{/* summary of cable data */}
{binDisplay()}
{quoteDisplay()}
</Box>
);
}