diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx
index 716e726..8712dca 100644
--- a/src/bin/binModes/DryingMode.tsx
+++ b/src/bin/binModes/DryingMode.tsx
@@ -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
const interactionStep = () => {
- console.log(interactionsToAdd)
return (
Interactions
@@ -474,13 +473,14 @@ export default function DryingMode(props: Props){
temp.settings = interaction
let sink = sinkMap.get(componentIDToString(interaction.sink))
let source = sourceMap.get(componentIDToString(interaction.source))
- if(sink && source){
+ if(sink && source && selectedDevice){
return (
{
diff --git a/src/bin/binModes/HydratingMode.tsx b/src/bin/binModes/HydratingMode.tsx
index 9f000ba..79a11ca 100644
--- a/src/bin/binModes/HydratingMode.tsx
+++ b/src/bin/binModes/HydratingMode.tsx
@@ -339,13 +339,14 @@ export default function DryingMode(props: Props){
temp.settings = interaction
let sink = sinkMap.get(componentIDToString(interaction.sink))
let source = sourceMap.get(componentIDToString(interaction.source))
- if(sink && source){
+ if(sink && source && selectedDevice){
return (
{
diff --git a/src/bin/binModes/conditionDisplay.tsx b/src/bin/binModes/conditionDisplay.tsx
index d44c3ba..0a26e51 100644
--- a/src/bin/binModes/conditionDisplay.tsx
+++ b/src/bin/binModes/conditionDisplay.tsx
@@ -5,19 +5,21 @@ import {
Box,
darken,
Grid,
+ IconButton,
Slider,
Theme,
+ Tooltip,
Typography
} from "@mui/material";
- import { ExpandMore } from "@mui/icons-material";
+ import { AddCircle, ExpandMore, RemoveCircleOutline } from "@mui/icons-material";
import { ExtractMoisture } from "grain";
import { cloneDeep } from "lodash";
- import { Component, Interaction } from "models";
+ import { Component, Device, Interaction } from "models";
import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
- import React, { useEffect, useState } from "react";
+ import React, { useEffect, useMemo, useState } from "react";
import { avg, fahrenheitToCelsius } from "utils";
import { makeStyles } from "@mui/styles";
import { Mark } from "@mui/material/Slider/useSlider.types";
@@ -38,7 +40,6 @@ import {
height: 0,
borderLeft: "10px solid transparent",
borderRight: "10px solid transparent"
- //borderTop: "10px solid yellow"
},
sliderRoot: {},
sliderThumb: {
@@ -71,12 +72,18 @@ import {
},
sliderMarkLabel: {
top: -25
+ },
+ groupHeader: {
+ marginTop: 10,
+ marginBottom: 5,
+ fontWeight: 650
}
});
});
interface Props {
interaction: Interaction;
+ device: Device;
source: Component;
sink: Component;
grain?: pond.Grain;
@@ -84,57 +91,214 @@ import {
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(
+ [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) {
- const { interaction, source, sink, grain, customGrain, changeConditions} = props;
- const [sliderVals, setSliderVals] = useState
);
};
-
-
- const conditionDisplay = () => {
+ // 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)
+ });
+ }
+
return (
-
- {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 (
-
-
- {interactionConditionText(source, condition, false, user)}
-
-
- {
- 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) => {
- //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)
- }
- }}
- />
-
-
- );
- })}
+
+
+ {
+ 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) ?? 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));
+ let sliders = cloneDeep(sliderVals);
+ sliders.set(condition, val as number);
+ setSliderVals(sliders);
+ }}
+ onChangeCommitted={() => {
+ if (changeConditions) {
+ changeConditions(interaction.settings);
+ }
+ }}
+ />
+
+ {onRemove && (
+
+
+
+
+
+
+
+ )}
);
};
- 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;
- });
+ // Text like "Humidity is below 32.5% and above 60.0%" / "Humidity between 32.5% and 60.0%"
+ // Falls back gracefully if only one of the pair exists yet (e.g. mid-edit).
+ const groupLabel = (
+ measurementType: quack.MeasurementType,
+ loCondition: pond.InteractionCondition | undefined,
+ hiCondition: pond.InteractionCondition | undefined
+ ) => {
+ let describer = describeMeasurement(measurementType, source?.type(), source.subType(), undefined, user);
+ let name = measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ? "Temperature" : "Humidity";
+ let unit = describer.unit();
- 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 (loCondition && hiCondition) {
+ let loVal = sliderVals.get(loCondition);
+ let hiVal = sliderVals.get(hiCondition);
+ if (loVal !== undefined && hiVal !== undefined) {
+ return `${name} is between ${loVal.toFixed(1)}${unit} and ${hiVal.toFixed(1)}${unit}`;
+ }
}
- 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;
+ // fall back to whichever single condition is present
+ let only = loCondition ?? hiCondition;
+ if (only) return interactionConditionText(source, only, false, user);
+ return name;
};
+ 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 (
+
+
+ {groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, tempLo, tempHi)}
+
+ {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)")}
+
+
+ {groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, humidLo, humidHi)}
+
+ {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) => (
+
+
+ {interactionConditionText(source, c, false, user)}
+
+ {renderConditionSlider(c, `unmatched-slider-${i}`)}
+
+ ))}
+
+ );
+ }
+
+ // single pair (or no clean pair) -- original flat rendering
+ const hasExactlyOnePair =
+ (!!pairs.upperBoundPair && !pairs.lowerBoundPair) ||
+ (!pairs.upperBoundPair && !!pairs.lowerBoundPair);
+
+ return (
+
+ {interaction.conditions().map((condition, i) => (
+
+
+ {interactionConditionText(source, condition, false, user)}
+
+ {renderConditionSlider(condition, `slider-${i}`)}
+
+ ))}
+ {hasExactlyOnePair && (
+
+
+
+
+
+
+
+ )}
+
+ );
+ };
+
+ 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 (
- {interactionResultText(interaction, sink)}
+ {interactionResultText(interaction, sink)}
- {baseEMC !== undefined ? (
+ {hasAnyEMC ? (
}>
- EMC {determineEMCRelation()}
-
- {baseEMC.toFixed(1)}%
+ EMC {determineEMCLabel()}
+
+ {emcValueDisplay()}
@@ -309,5 +542,4 @@ import {
);
- }
-
\ No newline at end of file
+ }
\ No newline at end of file