re-building the condition display to support adding extra conditions to the interaction for drying and passing the device in to the condition display so that it can use its max conditions function to determine if the extra conditions can be added

This commit is contained in:
csawatzky 2026-07-13 16:19:17 -06:00
parent aa346a6a5a
commit e91d2f6005
3 changed files with 400 additions and 167 deletions

View file

@ -465,7 +465,6 @@ export default function DryingMode(props: Props){
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes //this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
const interactionStep = () => { const interactionStep = () => {
console.log(interactionsToAdd)
return ( return (
<Box> <Box>
<Typography>Interactions</Typography> <Typography>Interactions</Typography>
@ -474,13 +473,14 @@ export default function DryingMode(props: Props){
temp.settings = interaction temp.settings = interaction
let sink = sinkMap.get(componentIDToString(interaction.sink)) let sink = sinkMap.get(componentIDToString(interaction.sink))
let source = sourceMap.get(componentIDToString(interaction.source)) let source = sourceMap.get(componentIDToString(interaction.source))
if(sink && source){ if(sink && source && selectedDevice){
return ( return (
<ConditionDisplay <ConditionDisplay
grain={grain} grain={grain}
customGrain={customGrain} customGrain={customGrain}
key={index} key={index}
interaction={temp} interaction={temp}//using temp because it wants a full interaction, not just its settings
device={selectedDevice}
sink={sink} sink={sink}
source={source} source={source}
changeConditions={(newSettings) => { changeConditions={(newSettings) => {

View file

@ -339,13 +339,14 @@ export default function DryingMode(props: Props){
temp.settings = interaction temp.settings = interaction
let sink = sinkMap.get(componentIDToString(interaction.sink)) let sink = sinkMap.get(componentIDToString(interaction.sink))
let source = sourceMap.get(componentIDToString(interaction.source)) let source = sourceMap.get(componentIDToString(interaction.source))
if(sink && source){ if(sink && source && selectedDevice){
return ( return (
<ConditionDisplay <ConditionDisplay
key={index} key={index}
grain={grain} grain={grain}
customGrain={customGrain} customGrain={customGrain}
interaction={temp} interaction={temp}
device={selectedDevice}
sink={sink} sink={sink}
source={source} source={source}
changeConditions={(newSettings) => { changeConditions={(newSettings) => {

View file

@ -5,19 +5,21 @@ import {
Box, Box,
darken, darken,
Grid, Grid,
IconButton,
Slider, Slider,
Theme, Theme,
Tooltip,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import { ExpandMore } from "@mui/icons-material"; import { AddCircle, ExpandMore, RemoveCircleOutline } from "@mui/icons-material";
import { ExtractMoisture } from "grain"; import { ExtractMoisture } from "grain";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Component, Interaction } from "models"; import { Component, Device, Interaction } from "models";
import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction"; import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond"; import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { avg, fahrenheitToCelsius } from "utils"; import { avg, fahrenheitToCelsius } from "utils";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Mark } from "@mui/material/Slider/useSlider.types"; import { Mark } from "@mui/material/Slider/useSlider.types";
@ -38,7 +40,6 @@ import {
height: 0, height: 0,
borderLeft: "10px solid transparent", borderLeft: "10px solid transparent",
borderRight: "10px solid transparent" borderRight: "10px solid transparent"
//borderTop: "10px solid yellow"
}, },
sliderRoot: {}, sliderRoot: {},
sliderThumb: { sliderThumb: {
@ -71,12 +72,18 @@ import {
}, },
sliderMarkLabel: { sliderMarkLabel: {
top: -25 top: -25
},
groupHeader: {
marginTop: 10,
marginBottom: 5,
fontWeight: 650
} }
}); });
}); });
interface Props { interface Props {
interaction: Interaction; interaction: Interaction;
device: Device;
source: Component; source: Component;
sink: Component; sink: Component;
grain?: pond.Grain; grain?: pond.Grain;
@ -84,57 +91,214 @@ import {
changeConditions?: (newSettings: pond.InteractionSettings) => void changeConditions?: (newSettings: pond.InteractionSettings) => void
} }
// A "corner" pairing: one temp condition + one humidity condition whose
// comparison directions are consistent with each other, so together they
// bound a single EMC value.
// temp > X && humid < Y -> bounds EMC from ABOVE (an upper limit)
// temp < X && humid > Y -> bounds EMC from BELOW (a lower limit)
interface EMCCornerPair {
temp: pond.InteractionCondition;
humid: pond.InteractionCondition;
}
interface PairedConditions {
upperBoundPair?: EMCCornerPair; // temp > X && humid < Y
lowerBoundPair?: EMCCornerPair; // temp < X && humid > Y
unmatched: pond.InteractionCondition[];
}
const isGTorEQ = (op: quack.RelationalOperator) =>
op === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN ||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO;
const isLTorEQ = (op: quack.RelationalOperator) =>
op === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN ||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO;
// Groups the flat condition array into (at most) two EMC-bounding corners,
// based purely on comparison direction, not array position/order.
function pairEMCConditions(conditions: pond.InteractionCondition[]): PairedConditions {
const temps = conditions.filter(
c => c.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
);
const humids = conditions.filter(
c => c.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
);
const hiTemp = temps.find(c => isGTorEQ(c.comparison)); // temp > X
const loTemp = temps.find(c => isLTorEQ(c.comparison)); // temp < X
const loHumid = humids.find(c => isLTorEQ(c.comparison)); // humid < Y
const hiHumid = humids.find(c => isGTorEQ(c.comparison)); // humid > Y
const result: PairedConditions = { unmatched: [] };
if (hiTemp && loHumid) result.upperBoundPair = { temp: hiTemp, humid: loHumid };
if (loTemp && hiHumid) result.lowerBoundPair = { temp: loTemp, humid: hiHumid };
const paired = new Set<pond.InteractionCondition>(
[hiTemp, loHumid, loTemp, hiHumid].filter((c): c is pond.InteractionCondition => !!c)
);
result.unmatched = conditions.filter(c => !paired.has(c));
return result;
}
export default function ConditionDisplay(props: Props) { export default function ConditionDisplay(props: Props) {
const { interaction, source, sink, grain, customGrain, changeConditions} = props; const { interaction, device, source, sink, grain, customGrain, changeConditions } = props;
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map()); const [sliderVals, setSliderVals] = useState<Map<pond.InteractionCondition, number>>(new Map());
const [sliderMarks, setSliderMarks] = 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 // EMC bound(s) calculated from the temp/humidity condition pairs.
const [baseEMC, setBaseEMC] = useState<number | undefined>(); // If only one pair exists, only one of these will be set.
const [upperEMC, setUpperEMC] = useState<number | undefined>();
const [lowerEMC, setLowerEMC] = useState<number | undefined>();
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const classes = useStyles(); const classes = useStyles();
// Bumped whenever conditions are added/removed so `pairs` recomputes.
// Needed because pushing into interaction.settings.conditions mutates
// the array in place without changing the `interaction` reference, so
// useMemo keyed only on [interaction] would never see the new conditions.
const [conditionsVersion, setConditionsVersion] = useState(0);
const pairs = useMemo(
() => pairEMCConditions(interaction.conditions()),
[interaction, conditionsVersion]
);
useEffect(() => { useEffect(() => {
let passedInteraction = interaction; let passedInteraction = interaction;
let sliderVals: Map<quack.MeasurementType, number> = new Map(); let vals: Map<pond.InteractionCondition, number> = new Map();
let sliderMarks: Map<quack.MeasurementType, number> = new Map(); let marks: Map<quack.MeasurementType, number> = new Map();
passedInteraction.conditions().forEach(condition => { passedInteraction.conditions().forEach(condition => {
let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user); let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user);
//NOTE: toDisplay will convert the temp value to fahrenheit // NOTE: toDisplay will convert the temp value to fahrenheit
sliderVals.set(condition.measurementType, describer.toDisplay(condition.value)); // keyed by condition object itself (not measurementType) since there
// can now be more than one condition per measurement type
vals.set(condition, describer.toDisplay(condition.value));
}); });
source.status.lastGoodMeasurement.forEach(measurement => { source.status.lastGoodMeasurement.forEach(measurement => {
let m = pond.UnitMeasurementsForComponent.fromObject(measurement); let m = pond.UnitMeasurementsForComponent.fromObject(measurement);
if (m.values[0]) { if (m.values[0]) {
let markVal = avg(m.values[0].values); 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 // 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_TEMPERATURE) markVal = markVal * 10;
if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100; if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100;
sliderMarks.set(m.type, markVal); marks.set(m.type, markVal);
} }
}); });
setSliderVals(sliderVals); setSliderVals(vals);
setSliderMarks(sliderMarks); setSliderMarks(marks);
}, [interaction, source]); }, [interaction, source]);
useEffect(() => { // Given a corner pair, calculate the EMC that pair implies. Returns
let temp = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE); // undefined if grain info or either value isn't available yet.
let hum = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT); const emcForPair = (pair?: EMCCornerPair): number | undefined => {
if ( if (!pair || grain === undefined || grain === pond.Grain.GRAIN_INVALID) return undefined;
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);
let temp = sliderVals.get(pair.temp);
let hum = sliderVals.get(pair.humid);
if (temp === undefined || hum === undefined) return undefined;
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
// the emc calc needs the temp to be in celsius
temp = fahrenheitToCelsius(temp);
} }
}, [sliderVals, grain, user]); let emc = ExtractMoisture(grain, temp, hum, customGrain);
return emc === hum ? undefined : emc;
};
useEffect(() => {
setUpperEMC(emcForPair(pairs.upperBoundPair));
setLowerEMC(emcForPair(pairs.lowerBoundPair));
}, [sliderVals, grain, user, pairs]);
// Builds a new InteractionCondition with sensible defaults, offset away
// from an existing reference condition's value so the new slider doesn't
// start stacked exactly on top of the one it's paired against.
// ASSUMPTION: InteractionCondition is a plain settable class instance,
// matching how the rest of this file mutates `condition.value` directly.
// If your codebase instead uses a factory (e.g. pond.InteractionCondition.create(...)),
// swap the `new ...()` + assignment below for that factory call.
const buildCondition = (
measurementType: quack.MeasurementType,
comparison: quack.RelationalOperator,
displayValue: number
): pond.InteractionCondition => {
let describer = describeMeasurement(measurementType, source?.type(), source.subType(), undefined, user);
const condition = new pond.InteractionCondition();
condition.measurementType = measurementType;
condition.comparison = comparison;
condition.value = Math.round(describer.toStored(displayValue));
return condition;
};
// Adds the missing opposite corner pair (temp + humidity condition) so
// the EMC display becomes a "between" range instead of a single bound.
// Only relevant when exactly one clean corner pair currently exists.
const addOppositeCornerPair = () => {
const existing = pairs.upperBoundPair ?? pairs.lowerBoundPair;
if (!existing) return; // nothing to mirror off of -- shouldn't happen if button is only shown in this case
const addingLowerBound = !!pairs.upperBoundPair; // if we have the "less than" pair, we're adding the "greater than" one
const tempComparison = addingLowerBound
? quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN
: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
const humidComparison = addingLowerBound
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
const tempDescriber = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, source?.type(), source.subType(), undefined, user);
const humidDescriber = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, source?.type(), source.subType(), undefined, user);
const existingTempVal = sliderVals.get(existing.temp) ?? tempDescriber.toDisplay(existing.temp.value);
const existingHumidVal = sliderVals.get(existing.humid) ?? humidDescriber.toDisplay(existing.humid.value);
// offset 10% of the slider's range away from the existing threshold,
// in the direction that keeps the two thresholds from overlapping
const tempRange = tempDescriber.max() - tempDescriber.min();
const humidRange = humidDescriber.max() - humidDescriber.min();
const tempOffsetDir = addingLowerBound ? -1 : 1;
const humidOffsetDir = addingLowerBound ? 1 : -1;
const newTempVal = existingTempVal + tempOffsetDir * tempRange * 0.1;
const newHumidVal = existingHumidVal + humidOffsetDir * humidRange * 0.1;
const newTemp = buildCondition(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, tempComparison, newTempVal);
const newHumid = buildCondition(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, humidComparison, newHumidVal);
// ASSUMPTION: interaction.settings.conditions is the backing array for
// interaction.conditions() -- push directly so changeConditions() sends
// the full, updated list. Adjust if conditions are stored/added differently.
interaction.settings.conditions.push(newTemp, newHumid);
const sliders = cloneDeep(sliderVals);
sliders.set(newTemp, newTempVal);
sliders.set(newHumid, newHumidVal);
setSliderVals(sliders);
setConditionsVersion(v => v + 1);
if (changeConditions) {
changeConditions(interaction.settings);
}
};
// Removes a corner pair's two conditions, collapsing back to a single
// bound. Only relevant when both pairs currently exist.
const removeCornerPair = (pair: EMCCornerPair) => {
interaction.settings.conditions = interaction.settings.conditions.filter(
c => c !== pair.temp && c !== pair.humid
);
const sliders = cloneDeep(sliderVals);
sliders.delete(pair.temp);
sliders.delete(pair.humid);
setSliderVals(sliders);
setConditionsVersion(v => v + 1);
if (changeConditions) {
changeConditions(interaction.settings);
}
};
const customMark = (val: string, arrowColor: string) => { const customMark = (val: string, arrowColor: string) => {
return ( return (
@ -153,151 +317,220 @@ import {
); );
}; };
// Renders a single condition's slider. Pulled out so both the grouped
// (paired) rendering path and the fallback flat rendering path can share it.
// onRemove/removeTooltip, when provided, render a small remove icon inline
// with this specific slider -- used when two pairs exist, so each row's
// control is unambiguous about which single condition it sits on, even
// though clicking it removes that condition's whole paired counterpart too.
const renderConditionSlider = (
condition: pond.InteractionCondition,
key: React.Key,
onRemove?: () => void,
removeTooltip?: string
) => {
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user);
let labelTail = describer.unit();
let marks: Mark[] = [];
let mark = sliderMarks.get(condition.measurementType);
if (mark !== undefined) {
marks.push({
label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()),
value: describer.toDisplay(mark)
});
}
const conditionDisplay = () => {
return ( return (
<Grid container> <Grid key={key} container item xs={12} alignItems="center" wrap="nowrap">
{interaction.conditions().map((condition, i) => { <Grid item xs style={{ marginBottom: 20 }}>
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user); <Slider
let labelTail = describer.unit(); classes={{
let marks: Mark[] = []; root: classes.sliderRoot,
rail: classes.sliderRail,
// if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) { track: classes.sliderTrack,
// if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { thumb: classes.sliderThumb,
// labelTail = "°F"; valueLabel: classes.sliderValLabel,
// } else { marked: classes.sliderMarked,
// labelTail = "°C"; mark: classes.sliderMark,
// } markLabel: classes.sliderMarkLabel
// } }}
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) { step={0.1}
labelTail = "%"; valueLabelDisplay="on"
} valueLabelFormat={value => {
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
let mark = sliderMarks.get(condition.measurementType); if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
if (mark !== undefined) { return value.toFixed(1) + "°F";
marks.push({ } else {
label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()), return value.toFixed(1) + "°C";
value: describer.toDisplay(mark) }
}); }
} if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
return ( return value.toFixed(1) + "%";
<Grid key={i} container item xs={12} alignContent="center" alignItems="center"> }
<Grid item xs={12}> }}
{interactionConditionText(source, condition, false, user)} marks={marks}
</Grid> min={describer.min()}
<Grid item xs={12} style={{ marginBottom: 20 }}> max={describer.max()}
<Slider value={sliderVals.get(condition) ?? describer.min()}
classes={{ onChange={(_, val) => {
root: classes.sliderRoot, // note that changing it here like this is what is changing it in the interaction itself
rail: classes.sliderRail, condition.value = Math.round(describer.toStored(val as number));
track: classes.sliderTrack, let sliders = cloneDeep(sliderVals);
thumb: classes.sliderThumb, sliders.set(condition, val as number);
valueLabel: classes.sliderValLabel, setSliderVals(sliders);
marked: classes.sliderMarked, }}
mark: classes.sliderMark, onChangeCommitted={() => {
markLabel: classes.sliderMarkLabel if (changeConditions) {
}} changeConditions(interaction.settings);
step={0.1} }
valueLabelDisplay="on" }}
valueLabelFormat={value => { />
if ( </Grid>
condition.measurementType === {onRemove && (
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE <Grid item>
) { <Tooltip title={removeTooltip ?? "Remove"}>
if ( <IconButton color="error" size="small" onClick={onRemove}>
user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT <RemoveCircleOutline />
) { </IconButton>
return value.toFixed(1) + "°F"; </Tooltip>
} else { </Grid>
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) => {
//note that changing it here like this is what is changing it in the interaction itself
condition.value = Math.round(describer.toStored(val as number));
// if(changeConditions){
// changeConditions(interaction.settings)
// }
let sliders = cloneDeep(sliderVals);
sliders.set(condition.measurementType, val as number);
setSliderVals(sliders);
}}
onChangeCommitted={(_, val) => {
if(changeConditions){
changeConditions(interaction.settings)
}
}}
/>
</Grid>
</Grid>
);
})}
</Grid> </Grid>
); );
}; };
const determineEMCRelation = () => { // Text like "Humidity is below 32.5% and above 60.0%" / "Humidity between 32.5% and 60.0%"
let relation = "Approximately:"; // Falls back gracefully if only one of the pair exists yet (e.g. mid-edit).
let tempRelation: quack.RelationalOperator | undefined = undefined; const groupLabel = (
let humidRelation: quack.RelationalOperator | undefined = undefined; measurementType: quack.MeasurementType,
interaction.conditions().forEach(condition => { loCondition: pond.InteractionCondition | undefined,
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) hiCondition: pond.InteractionCondition | undefined
tempRelation = condition.comparison; ) => {
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) let describer = describeMeasurement(measurementType, source?.type(), source.subType(), undefined, user);
humidRelation = condition.comparison; let name = measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ? "Temperature" : "Humidity";
}); let unit = describer.unit();
if ( if (loCondition && hiCondition) {
(tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || let loVal = sliderVals.get(loCondition);
tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && let hiVal = sliderVals.get(hiCondition);
(humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || if (loVal !== undefined && hiVal !== undefined) {
humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) return `${name} is between ${loVal.toFixed(1)}${unit} and ${hiVal.toFixed(1)}${unit}`;
) { }
relation = "Less Than: ";
} }
if ( // fall back to whichever single condition is present
(tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || let only = loCondition ?? hiCondition;
tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && if (only) return interactionConditionText(source, only, false, user);
(humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || return name;
humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO)
) {
relation = "Greater Than: ";
}
return relation;
}; };
const conditionDisplay = () => {
// If both corner pairs exist, we have two temp conditions and two humid
// conditions -- group them together (temp with temp, humid with humid)
// instead of rendering per-condition like the single-pair case.
const hasTwoPairs = !!pairs.upperBoundPair && !!pairs.lowerBoundPair;
if (hasTwoPairs) {
// upperBoundPair.temp is "temp > X" (the low end of the temp range)
// lowerBoundPair.temp is "temp < X" (the high end of the temp range)
const tempLo = pairs.upperBoundPair!.temp;
const tempHi = pairs.lowerBoundPair!.temp;
// upperBoundPair.humid is "humid < Y" (the high end doesn't apply here -
// it's the low end of the humidity range)
const humidLo = pairs.upperBoundPair!.humid;
const humidHi = pairs.lowerBoundPair!.humid;
return (
<Grid container>
<Grid item xs={12}>
<Typography>{groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, tempLo, tempHi)}</Typography>
</Grid>
{renderConditionSlider(tempLo, "temp-lo", () => removeCornerPair(pairs.upperBoundPair!), "Remove less than range (also removes matching humidity threshold)")}
{renderConditionSlider(tempHi, "temp-hi", () => removeCornerPair(pairs.lowerBoundPair!), "Remove greater than range (also removes matching humidity threshold)")}
<Grid item xs={12}>
<Typography>{groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, humidLo, humidHi)}</Typography>
</Grid>
{renderConditionSlider(humidLo, "humid-lo", () => removeCornerPair(pairs.upperBoundPair!), "Remove less than range (also removes matching temperature threshold)")}
{renderConditionSlider(humidHi, "humid-hi", () => removeCornerPair(pairs.lowerBoundPair!), "Remove greater than range (also removes matching temperature threshold)")}
{pairs.unmatched.map((c, i) => (
<React.Fragment key={`unmatched-${i}`}>
<Grid item xs={12}>
<Typography>{interactionConditionText(source, c, false, user)}</Typography>
</Grid>
{renderConditionSlider(c, `unmatched-slider-${i}`)}
</React.Fragment>
))}
</Grid>
);
}
// single pair (or no clean pair) -- original flat rendering
const hasExactlyOnePair =
(!!pairs.upperBoundPair && !pairs.lowerBoundPair) ||
(!pairs.upperBoundPair && !!pairs.lowerBoundPair);
return (
<Grid container>
{interaction.conditions().map((condition, i) => (
<React.Fragment key={i}>
<Grid item xs={12}>
<Typography>{interactionConditionText(source, condition, false, user)}</Typography>
</Grid>
{renderConditionSlider(condition, `slider-${i}`)}
</React.Fragment>
))}
{hasExactlyOnePair && (
<Grid item xs={12} container justifyContent="center" style={{ marginTop: 10 }}>
<Tooltip title={`Add ${pairs.upperBoundPair ? "greater than" : "less than"} range`}>
<IconButton disabled={device.maxConditions() < 4} color="primary" onClick={addOppositeCornerPair}>
<AddCircle fontSize="large" />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
);
};
const determineEMCLabel = () => {
if (upperEMC !== undefined && lowerEMC !== undefined) return "Between: ";
if (upperEMC !== undefined) return "Less Than: ";
if (lowerEMC !== undefined) return "Greater Than: ";
return "Approximately: ";
};
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
const emcValueDisplay = () => {
if (upperEMC !== undefined && lowerEMC !== undefined) {
// present low-to-high regardless of which pair produced which,
// since "greater than" bound is the smaller number and
// "less than" bound is the larger number
const lo = Math.min(upperEMC, lowerEMC);
const hi = Math.max(upperEMC, lowerEMC);
return `${lo.toFixed(1)}% and ${hi.toFixed(1)}%`;
}
const single = upperEMC ?? lowerEMC;
return single !== undefined ? `${single.toFixed(1)}%` : "";
};
const hasAnyEMC = upperEMC !== undefined || lowerEMC !== undefined;
return ( return (
<Grid key={interaction.key()} container className={classes.displayBG}> <Grid key={interaction.key()} container className={classes.displayBG}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography align="center" style={{ margin: 5, marginBottom: 10, fontWeight: 650 }}> <Typography align="center" style={{ margin: 5, marginBottom: 10, fontWeight: 650 }}>
{interactionResultText(interaction, sink)} {interactionResultText(interaction, sink)}
</Typography> </Typography>
{baseEMC !== undefined ? ( {hasAnyEMC ? (
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMore />}> <AccordionSummary expandIcon={<ExpandMore />}>
<Box display="flex"> <Box display="flex">
<Typography style={{ fontWeight: 650 }}>EMC {determineEMCRelation()}</Typography> <Typography style={{ fontWeight: 650 }}>EMC {determineEMCLabel()}</Typography>
<Typography <Typography style={{ marginLeft: 5, fontWeight: 650, color: emcColour }}>
style={{ {emcValueDisplay()}
marginLeft: 5,
fontWeight: 650,
color: describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC
).colour()
}}>
{baseEMC.toFixed(1)}%
</Typography> </Typography>
</Box> </Box>
</AccordionSummary> </AccordionSummary>
@ -310,4 +543,3 @@ import {
</Grid> </Grid>
); );
} }