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([]); const [displayedRows, setDisplayedRows] = useState([]); const [tablePage, setTablePage] = useState(0) const [pageSize, setPageSize] = useState(10) const [rowCount, setRowCount] = useState(0) const [orderBy, setOrderBy] = useState("") const [order, setOrder] = useState<"asc" | "desc">("asc") const [selectedBin, setSelectedBin] = useState(); 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({ 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[] = [ { title: "Manufacturer", sortKey: "Manufacturer", render: (row: jsonBin) => {row.Manufacturer} }, { title: "Model", sortKey: "Model", render: (row: jsonBin) => {row.Model} }, { title: "Capacity", sortKey: "Capacity", render: (row: jsonBin) => {row.Capacity} }, { title: "Hopper Angle", sortKey: "HopperAngle", render: (row: jsonBin) => {row.HopperAngle} }, { title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), sortKey: "Diameter", render: (row: jsonBin) => {distanceConversion(row.Diameter).toFixed(2)} }, { title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), sortKey: "Sidewall", render: (row: jsonBin) => {distanceConversion(row.Sidewall).toFixed(2)} }, { title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), sortKey: "Peak", render: (row: jsonBin) => {distanceConversion(row.Peak).toFixed(2)} }, { title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), sortKey: "EaveToPeak", render: (row: jsonBin) => {distanceConversion(row.EaveToPeak).toFixed(2)} }, { title: "Roof Angle", sortKey: "RoofAngle", render: (row: jsonBin) => {row.RoofAngle} }, { title: "Rings", sortKey: "Rings", render: (row: jsonBin) => {row.Rings} } ] 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 ( title={Bins} 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 ( {/* first row */} { let bin = cloneDeep(customBin); bin.Manufacturer = e.target.value; setCustomBin(bin); }} /> { let bin = cloneDeep(customBin); bin.Name = e.target.value; setCustomBin(bin); }} /> { let bin = cloneDeep(customBin); bin.Model = e.target.value; setCustomBin(bin); }} /> {/* second row */} {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} ) }} 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); }} /> {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} ) }} 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); }} /> {/* third row */} { 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); }} /> {"in"} }} 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); }} /> {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"} ) }} 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); }} /> {/* fourth row */} { 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); }} /> {"deg"} }} 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); }} /> { setHopperAngleFormEntry(newValue.toString()); let bin = cloneDeep(customBin); bin.HopperAngle = newValue as number; bin.Capacity = advancedGrainCalc(bin); setCustomBin(bin); }} /> {"deg"} }} 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); }} /> {/* bottom */} Shape { 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); }}> } label={"Flat Bottom"} /> } label={"Hopper"} /> {/* not strictly necessary for quoting */} {/* { 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 */} {/* { //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); }} /> */} Estimated Capacity: {customBin.Capacity} bushels ); }; const binDisplay = () => { let displayBin = selectedBin; if (useCustomBin) { displayBin = customBin; } if (displayBin) { return ( { setSelectedBin(newBin); }} /> ); } }; const quoteDisplay = () => { let displayBin = selectedBin; if (useCustomBin) { displayBin = customBin; } if (displayBin) { return ( { setQuoteOpen(false); }} /> ); } }; return ( { setUseCustomBin(checked); }} name="storage" /> } /> {/* grid of select boxes for pre-built options */} {!useCustomBin && ( { setBinOptions(ops); }} /> )} {useCustomBin ? customBinForm() : binOpTable()} {/* summary of cable data */} {binDisplay()} {quoteDisplay()} ); }