frontend/src/bin/BinConditioningInteraction.tsx

345 lines
12 KiB
TypeScript

import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
darken,
Grid,
Slider,
Theme,
Typography
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
import { ExtractMoisture } from "grain";
import { cloneDeep } from "lodash";
import { Component, Interaction } from "models";
import { UnitMeasurement } from "models/UnitMeasurement";
import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
import { avg, fahrenheitToCelsius } from "utils";
import { makeStyles } from "@mui/styles";
import { Mark } from "@mui/material/Slider/useSlider.types";
const useStyles = makeStyles((theme: Theme) => {
return ({
displayBG: {
marginTop: 10,
background: darken(theme.palette.background.default, 0.05),
borderRadius: 5,
padding: 10
},
markContainer: {
zIndex: 2
},
arrowDown: {
width: 0,
height: 0,
borderLeft: "10px solid transparent",
borderRight: "10px solid transparent"
//borderTop: "10px solid yellow"
},
sliderRoot: {},
sliderThumb: {
height: 15,
width: 15,
backgroundColor: "yellow"
},
sliderTrack: {
height: 3,
backgroundColor: "white"
},
sliderRail: {
height: 3,
backgroundColor: "white"
},
sliderValLabel: {
left: -20,
top: 40,
background: "transparent",
"& *": {
color: "#fff"
}
},
sliderMark: {
visibility: "hidden"
},
sliderMarked: {
marginTop: 25,
marginBottom: 0
},
sliderMarkLabel: {
top: -25
}
});
});
interface Props {
deviceId: number;
interaction: Interaction;
source: Component;
sink: Component;
grain?: pond.Grain;
customGrain?: pond.GrainSettings
}
export default function BinConditioningInteraction(props: Props) {
const { interaction, source, sink, grain, deviceId, customGrain } = props;
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map());
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
//this is the emc value calculated from the interactions temp and humidity conditions
const [baseEMC, setBaseEMC] = useState<number | undefined>();
const [{ user, as }] = useGlobalState();
const classes = useStyles();
const interactionAPI = useInteractionsAPI();
const { openSnack } = useSnackbar();
useEffect(() => {
let passedInteraction = interaction;
let sliderVals: Map<quack.MeasurementType, number> = new Map();
let sliderMarks: Map<quack.MeasurementType, number> = new Map();
passedInteraction.conditions().forEach(condition => {
let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user);
//NOTE: toDisplay will convert the temp value to fahrenheit
sliderVals.set(condition.measurementType, describer.toDisplay(condition.value));
});
source.status.lastGoodMeasurement.forEach(measurement => {
let m = pond.UnitMeasurementsForComponent.fromObject(measurement);
if (m.values[0]) {
let markVal = avg(m.values[0].values);
//do this since this is how interactions handle the values so that the slider can use the toDisplay method of the describer for the marks
if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) markVal = markVal * 10;
if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100;
sliderMarks.set(m.type, markVal);
}
});
setSliderVals(sliderVals);
setSliderMarks(sliderMarks);
}, [interaction, source]);
useEffect(() => {
let temp = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE);
let hum = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT);
if (
grain !== undefined &&
grain !== pond.Grain.GRAIN_INVALID &&
temp &&
hum
) {
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
//the emc calc needs the temp to be in celsius
temp = fahrenheitToCelsius(temp);
}
let emc = ExtractMoisture(grain, temp, hum, customGrain)
setBaseEMC(emc === hum ? undefined : emc);
}
}, [sliderVals, grain, user]);
const updateInteraction = () => {
interactionAPI
.updateInteraction(deviceId, interaction.settings, as)
.then(resp => {
openSnack("Updated Interaction Conditions");
})
.catch(err => {
openSnack("Failed to Update Interaction Conditions");
});
};
const customMark = (val: string, arrowColor: string) => {
return (
<Box className={classes.markContainer}>
<Grid container direction="column" alignContent="center" alignItems="center" spacing={1}>
<Grid item>
<Typography style={{ color: "white", fontSize: 15, fontWeight: 650, lineHeight: 1 }}>
{val}
</Typography>
</Grid>
<Grid item>
<Box className={classes.arrowDown} style={{ borderTop: "10px solid " + arrowColor }} />
</Grid>
</Grid>
</Box>
);
};
const conditionDisplay = () => {
return (
<Grid container>
{interaction.conditions().map((condition, i) => {
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user);
let labelTail = describer.unit();
let marks: Mark[] = [];
// if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
// if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
// labelTail = "°F";
// } else {
// labelTail = "°C";
// }
// }
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
labelTail = "%";
}
let mark = sliderMarks.get(condition.measurementType);
if (mark !== undefined) {
marks.push({
label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()),
value: describer.toDisplay(mark)
});
}
return (
<Grid key={i} container item xs={12} alignContent="center" alignItems="center">
<Grid item xs={12}>
{interactionConditionText(source, condition, false, user)}
</Grid>
<Grid item xs={12} style={{ marginBottom: 20 }}>
<Slider
classes={{
root: classes.sliderRoot,
rail: classes.sliderRail,
track: classes.sliderTrack,
thumb: classes.sliderThumb,
valueLabel: classes.sliderValLabel,
marked: classes.sliderMarked,
mark: classes.sliderMark,
markLabel: classes.sliderMarkLabel
}}
step={0.1}
valueLabelDisplay="on"
valueLabelFormat={value => {
if (
condition.measurementType ===
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
) {
if (
user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
) {
return value.toFixed(1) + "°F";
} else {
return value.toFixed(1) + "°C";
}
}
if (
condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
) {
return value.toFixed(1) + "%";
}
}}
marks={marks}
min={describer.min()}
max={describer.max()}
value={sliderVals.get(condition.measurementType) ?? describer.min()}
onChange={(_, val) => {
condition.value = Math.round(describer.toStored(val as number));
let sliders = cloneDeep(sliderVals);
sliders.set(condition.measurementType, val as number);
setSliderVals(sliders);
}}
/>
</Grid>
</Grid>
);
})}
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
onClick={() => {
updateInteraction();
}}>
Update Conditions
</Button>
</Grid>
</Grid>
);
};
const determineEMCRelation = () => {
let relation = "Approximately:";
let tempRelation: quack.RelationalOperator | undefined = undefined;
let humidRelation: quack.RelationalOperator | undefined = undefined;
interaction.conditions().forEach(condition => {
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
tempRelation = condition.comparison;
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT)
humidRelation = condition.comparison;
});
if (
(tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN ||
tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) &&
(humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN ||
humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO)
) {
relation = "Less Than: ";
}
if (
(tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN ||
tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) &&
(humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN ||
humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO)
) {
relation = "Greater Than: ";
}
return relation;
};
return (
<Grid key={interaction.key()} container className={classes.displayBG}>
<Grid item xs={6}>
<Typography style={{ fontWeight: 650 }}>{source.name()}</Typography>
<UnitMeasurementSummary
component={source}
reading={UnitMeasurement.convertLastMeasurement(
source.status.lastGoodMeasurement.map(um => UnitMeasurement.any(um, user))
)}
/>
</Grid>
<Grid item xs={6}>
<Typography style={{ fontWeight: 650 }}>{sink.name()}</Typography>
<UnitMeasurementSummary
component={sink}
reading={UnitMeasurement.convertLastMeasurement(
sink.status.lastGoodMeasurement.map(um => UnitMeasurement.any(um, user))
)}
/>
</Grid>
<Grid item xs={12}>
<Typography align="center" style={{ margin: 5, marginBottom: 10, fontWeight: 650 }}>
{interactionResultText(interaction, sink)}
</Typography>
{baseEMC !== undefined ? (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box display="flex">
<Typography style={{ fontWeight: 650 }}>EMC {determineEMCRelation()}</Typography>
<Typography
style={{
marginLeft: 5,
fontWeight: 650,
color: describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC
).colour()
}}>
{baseEMC.toFixed(1)}%
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>{conditionDisplay()}</AccordionDetails>
</Accordion>
) : (
<React.Fragment>{conditionDisplay()}</React.Fragment>
)}
</Grid>
</Grid>
);
}