diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx index d729462..92a4f31 100644 --- a/src/bin/BinConditioningInteraction.tsx +++ b/src/bin/BinConditioningInteraction.tsx @@ -85,10 +85,14 @@ interface Props { sink: Component; grain?: pond.Grain; customGrain?: pond.GrainSettings + /** + * if true, the button to update the interaction will be hidden because it will be assumed that the parent will handle the interaction + */ + parentUpdate?: boolean } export default function BinConditioningInteraction(props: Props) { - const { interaction, source, sink, grain, deviceId, customGrain } = props; + const { interaction, source, sink, grain, deviceId, customGrain, parentUpdate} = props; const [sliderVals, setSliderVals] = useState>(new Map()); const [sliderMarks, setSliderMarks] = useState>(new Map()); //this is the emc value calculated from the interactions temp and humidity conditions @@ -143,13 +147,13 @@ export default function BinConditioningInteraction(props: Props) { const updateInteraction = () => { interactionAPI - .updateInteraction(deviceId, interaction.settings, as) - .then(resp => { - openSnack("Updated Interaction Conditions"); - }) - .catch(err => { - openSnack("Failed to Update Interaction Conditions"); - }); + .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) => { @@ -238,6 +242,7 @@ export default function BinConditioningInteraction(props: Props) { 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)); let sliders = cloneDeep(sliderVals); sliders.set(condition.measurementType, val as number); @@ -249,14 +254,16 @@ export default function BinConditioningInteraction(props: Props) { ); })} - + {!parentUpdate && + + } ); diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index 9bdfd0c..55316b8 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -11,6 +11,7 @@ import { CableData } from "bin/3dView/Data/BuildCableData"; import ModeChangeDialog from "bin/conditioning/modeChangeDialog"; import { cloneDeep } from "lodash"; import { useMobile } from "hooks"; +import BinModeController from "./binModes/BinModeController"; interface Props { bin: Bin @@ -35,6 +36,7 @@ export default function bin3dVisualizer(props: Props){ const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false) const [binDisplay, setBinDisplay] = useState("temp") const [binMode, setBinMode] = useState(pond.BinMode.BIN_MODE_NONE) + const [newMode, setNewMode] = useState(pond.BinMode.BIN_MODE_NONE) const [openModeChange, setOpenModeChange] = useState(false) const [selectedCable, setSelectedCable] = useState(undefined) const [selectedNode, setSelectedNode] = useState(undefined) @@ -58,10 +60,10 @@ export default function bin3dVisualizer(props: Props){ setBinMode(bin.settings.mode) },[bin]) - const updateBin = () => { + const updateBin = (newMode: pond.BinMode) => { if(updateBinCallback){ let clone = cloneDeep(bin) - clone.settings.mode = binMode + clone.settings.mode = newMode updateBinCallback(clone) } }; @@ -86,8 +88,10 @@ export default function bin3dVisualizer(props: Props){ '&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' }, }} onChange={event => { - setBinMode(event.target.value as pond.BinMode) + setNewMode(event.target.value as pond.BinMode) setOpenModeChange(true) + // for the re-built modes, each one will have its own dialog box and this will just control which one to open rather than the single dialog for all of them + // and having the mode determine what to show in the dialog }} > Select Mode.. @@ -327,7 +331,7 @@ export default function bin3dVisualizer(props: Props){ }} /> } - { setModeChangeInProgress(false) }} - + /> */} + { + updateBin(newMode) + }} + onClose={()=>{setOpenModeChange(false)}} /> void bin: Bin + binPrefs?: Map + devices: Device[] + componentDevices: Map + componentMap: Map + updateMode: (newMode: pond.BinMode) => void } -export default function BinModeControl(props: Props) { - const {open, onClose, newMode} = props +export default function BinModeController(props: Props) { + const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props + const [showProgress, setShowProgress] = useState(false) + const [deviceComponents, setDeviceComponents] = useState>(new Map()) + const [promiseStages, setPromiseStages] = useState([]) + const interactionAPI = useInteractionsAPI() + const componentAPI = useComponentAPI() - //the mode controller could handle the actual api call for adding the interactions etc but the dialogs would create them and pass them up + //the use effects for the initial setup of what will be needed + useEffect(()=>{ + let newMap:Map = new Map() + componentMap.forEach((comp, key) => { + //first we need to get the device id this component belongs to + let dev = componentDevices.get(key) + if(dev){ + //check if the key exists in the new map yet + if(newMap.has(dev)){ + newMap.get(dev)?.push(comp) + }else{ + newMap.set(dev, [comp]) + } + } + }) + setDeviceComponents(newMap) + },[componentDevices, componentMap]) //steps involved for devices when changing the bin mode: //remove conflicting interactions //add new interactions //updating the controllers - const renderContent = () => { + const closeDialog = () => { + setShowProgress(false) + onClose() + } + + const buildStages = (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => { + //build the stages to pass into the PromiseProgress + //stage one is to remove conflicting interactions + let stages: Stage[] = [] + let stage1: Stage = { + title: "Remove Conflicting Interactions", + steps: [] + } + conflictingInteractions.forEach(i => { + let newStep: Step = { + title: "Removing Interaction", + promise: interactionAPI.removeInteraction(deviceId, i.key()) + } + stage1.steps.push(newStep); + }); + if(stage1.steps.length > 0){ + stages.push(stage1) + } + //stage two is to add the new interactions + let stage2: Stage = { + title: "Add New Interactions", + steps: [ + { + title: "Adding Interactions", + promise: interactionAPI.addMultiInteractions(deviceId, newInteractions) + } + ] + } + if(stage2.steps.length > 0){ + stages.push(stage2) + } + + //stage three is to update the controller components + let stage3: Stage = { + title: "Update Controllers", + steps: [] + } + componentSets.forEach(set => { + set.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: componentAPI.update(deviceId, controller.settings) + } + stage3.steps.push(newStep) + }) + }) + if(stage3.steps.length > 0){ + stages.push(stage3) + } + //set those stages to a state variable + setPromiseStages(stages) + } + + /** + * the progress step is the last step of each mode change that shows what is being done and what succeeded/failed + * initially i was going to put it here because it would effectively be the same thing for each mode, however depending + * on how i decide to handle the api calls, and since i am thinking about passing the functions into the children it may go into each mode component + * + */ + const progressContent = () => { + return ( + + Change Mode + + { + //this is just a function that is rin as soo as the start button is clicked it can be used to disable things while the change is in progress if we want to + }} + onComplete={()=>{ + closeDialog() + }} + /> + + + + + + ) + } + + const modeContent = () => { switch(newMode){ case pond.BinMode.BIN_MODE_DRYING: - return + //setNumSteps(DryingStepCount) + return { + buildStages(conflicting, device, newInteractions, components) + setShowProgress(true) + }} + /> case pond.BinMode.BIN_MODE_HYDRATING: return case pond.BinMode.BIN_MODE_COOLDOWN: @@ -39,16 +167,8 @@ export default function BinModeControl(props: Props) { } return ( - - Change Bin Mode - - {renderContent()} - - - - + + {showProgress ? progressContent() : modeContent()} ) } \ No newline at end of file diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 7dd57a5..10895a1 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -1,9 +1,519 @@ -import { Box } from "@mui/material"; +import { ArrowBack, ArrowForward } from "@mui/icons-material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import ConditioningSelector from "bin/conditioning/conditioningSelector"; +import { Component, Device, Interaction } from "models"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { Plenum } from "models/Plenum"; +import { pond, quack } from "protobuf-ts/pond"; +import { useEffect, useMemo, useState } from "react"; +import { ComponentSet } from "bin/conditioning/pickComponentSet" +import { componentIDToString, sameComponentID } from "pbHelpers/Component"; +import moment from "moment"; +import { lowerCase } from "lodash"; +import { GetGrainExtensionMap } from "grain"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { useGlobalState, useInteractionsAPI } from "providers"; +import BinConditioningInteraction from "bin/BinConditioningInteraction"; +import ConditionDisplay from "./conditionDisplay"; +import React from "react"; -export default function DryingMode(){ - return ( - + +const useStyles = makeStyles((theme: Theme) => { + return ({ + stepper: { + padding: theme.spacing(0.5) + }, + }) +}); + +interface Step { + label: string; + completed?: boolean; +} + +export const DryingBaseStepCount = 3 + +interface Option { + label: string; + device: Device; + icon?: string; + } + +interface Props { + devices: Device[] + deviceComponents: Map + binPrefs?: Map + grain?: pond.Grain + cancel: () => void + confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void +} + +const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] + +//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing +export default function DryingMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props + const grainExtensionMap = GetGrainExtensionMap(); + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [dryingMethod, setDryingMethod] = useState("air")//whether they are using natural air or a heater + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + temp = ext.setTempC + hum = ext.targetMC + } + }else{//otherwise use the default drying interaction + temp = 40 + hum = 20 + } + + let conditions = []; + let humidityCondition = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: Math.round( + describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(hum) + ) + }); + conditions.push(humidityCondition); + + //since the measurement describers function to stored does a conversion into celsius if the users pref is fahrenheit for temperature we need to convert the preset value into fahrenheit + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + temp = Math.round((temp * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(temp); + + let tempCondition = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: Math.round(tempVal) //and then round the converted value since interaction conditions wont take floats + }); + conditions.push(tempCondition); + interaction.conditions = conditions; + //the the heater output to auto so that when the components are update it uses the new mode + heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO + return interaction; + }; + + const buildFanInteraction = ( + sensor: Plenum | Ambient, + fan: Controller, + tempComparison: "greater" | "less", + humidityComparison: "greater" | "less", + ) => { + let interaction = pond.InteractionSettings.create({ + source: sensor.location(), + sink: fan.location(), + schedule: pond.InteractionSchedule.create({ + timeOfDayStart: "00:00", + timeOfDayEnd: "24:00", + timezone: moment.tz.guess(), + weekdays: moment.weekdays().map(d => lowerCase(d)) + }), + notifications: pond.InteractionNotifications.create({ + reports: true + }), + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE, + value: 1 + }) + }); + + let tempPreset = 0; + let humidityPreset = 0; + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + tempPreset = ext.setTempC + humidityPreset = ext.targetMC + } + }else{//otherwise use the default drying interaction + tempPreset = 40 + humidityPreset = 20 + } + + + let conditions = []; + let fanConditionOne = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + comparison: + humidityComparison === "greater" + ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(humidityPreset) + }); + conditions.push(fanConditionOne); + + //since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + tempPreset = Math.round((tempPreset * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(tempPreset); + + let fanConditionTwo = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: + tempComparison === "greater" + ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals + }); + conditions.push(fanConditionTwo); + + interaction.conditions = conditions; + //set the output mode to auto in the fan so that when the components are updated it uses the new mode + fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO + return interaction; + }; + + //this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them + const createInteractions = (sets: ComponentSet[]) => { + if(!selectedDevice) return undefined //if there is no device that was selected, do nothing + let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() + let conflictingInteractions: Interaction[] = [] + let linkedComponents = deviceComponents.get(selectedDevice.id()); + //loop through the sets + sets.forEach((set) => { + //loop through the controllers + set.controllers.forEach(controller => { + //filter the conflicting interactions for that controller + let c = existingInteractions.filter(i => { + let conflicting = false; + if (linkedComponents) { + linkedComponents.forEach(comp => { + if (sameComponentID(comp.location(), i.settings.sink)) { + conflicting = true; + } + }); + } + return conflicting; + }); + conflictingInteractions = conflictingInteractions.concat(c) + // if the controller is a heater create heater interaction + if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ + multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller)) + }else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || + controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan + //if it is a combination set using a heater as well just turn the fan on + if(set.combo){ + controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON + }else{ + //otherwise make a fan interaction + multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less")) + } + } + }) + }) + setComponentSets([...sets]) + return { + conflicting: conflictingInteractions, + toAdd: multiInteractionSettings + } + } + + const stepper = () => { + return ( + + {steps.map((s, i) => ( + + {s.label} + + ))} + + ); + } + + const styleStep = () => { + return ( + + Select your drying method + { + setDryingMethod(value) + }}> + } + value={"air"} + label={"Natural Air"} + /> + } + value={"heat"} + label={"Supplemental Heat"} + /> + + + ) + } + + const deviceSelector = () => { + return ( + option.label || ""} + onChange={(_, newValue) => { + if(newValue){ + setSelectedDevice(newValue.device); + } + }} + renderInput={params => } + /> + ) + } + + const deviceStep = () => { + return ( + + Devices + {deviceSelector()} + {interactionLoading ? : + { + //update the component sets that will be used for the interactions + //when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list + //then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones + let i = createInteractions(sets) + if(i !== undefined){ + setConflictingInteractions(i.conflicting) + setInteractionsToAdd(i.toAdd) + } + }} + /> + } + + ) + } + + //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 + {interactionsToAdd?.interactions.map((interaction, index) => { + let temp = Interaction.create() + temp.settings = interaction + let sink = sinkMap.get(componentIDToString(interaction.sink)) + let source = sourceMap.get(componentIDToString(interaction.source)) + if(sink && source){ + return ( + { + console.log(newSettings) + }} + /> + ) + } + })} + + ) + } + + const stepperContent = () => { + switch(currentStep){ + case 1: + return deviceStep() + case 2: + return interactionStep() + default: + return styleStep() + } + } + + const actions = () => { + return ( + + {/* close - tells the parent to close the dialog without doing anything, always visible */} + + {/* back - goes back to the previous step, hidden on the forst step */} + {currentStep !== 0 && } + {/* next - goes to the next step, hidden on the last step */} + {currentStep !== steps.length - 1 && } + {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + + ) + } + + + + return ( + + Bin Drying + + {stepper()} + {stepperContent()} + + + {actions()} + + ) } \ No newline at end of file diff --git a/src/bin/binModes/conditionDisplay.tsx b/src/bin/binModes/conditionDisplay.tsx new file mode 100644 index 0000000..d44c3ba --- /dev/null +++ b/src/bin/binModes/conditionDisplay.tsx @@ -0,0 +1,313 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + darken, + Grid, + Slider, + Theme, + Typography + } from "@mui/material"; + import { ExpandMore } from "@mui/icons-material"; + import { ExtractMoisture } from "grain"; + import { cloneDeep } from "lodash"; + import { Component, 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 { 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 { + interaction: Interaction; + source: Component; + sink: Component; + grain?: pond.Grain; + customGrain?: pond.GrainSettings + changeConditions?: (newSettings: pond.InteractionSettings) => void + } + + export default function ConditionDisplay(props: Props) { + const { interaction, source, sink, grain, customGrain, changeConditions} = props; + const [sliderVals, setSliderVals] = useState>(new Map()); + const [sliderMarks, setSliderMarks] = useState>(new Map()); + //this is the emc value calculated from the interactions temp and humidity conditions + const [baseEMC, setBaseEMC] = useState(); + const [{ user }] = useGlobalState(); + const classes = useStyles(); + + useEffect(() => { + let passedInteraction = interaction; + let sliderVals: Map = new Map(); + let sliderMarks: Map = 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 customMark = (val: string, arrowColor: string) => { + return ( + + + + + {val} + + + + + + + + ); + }; + + + + const conditionDisplay = () => { + 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) + } + }} + /> + + + ); + })} + + ); + }; + + 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 ( + + + + {interactionResultText(interaction, sink)} + + {baseEMC !== undefined ? ( + + }> + + EMC {determineEMCRelation()} + + {baseEMC.toFixed(1)}% + + + + {conditionDisplay()} + + ) : ( + {conditionDisplay()} + )} + + + ); + } + \ No newline at end of file