988 lines
34 KiB
TypeScript
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>
|
|
);
|
|
}
|