nailed down the overall structure for the new bin modes now
This commit is contained in:
parent
19eb2a7faf
commit
bced38d454
5 changed files with 1011 additions and 45 deletions
|
|
@ -85,10 +85,14 @@ interface Props {
|
||||||
sink: Component;
|
sink: Component;
|
||||||
grain?: pond.Grain;
|
grain?: pond.Grain;
|
||||||
customGrain?: pond.GrainSettings
|
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) {
|
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<Map<quack.MeasurementType, number>>(new Map());
|
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, 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
|
//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 = () => {
|
const updateInteraction = () => {
|
||||||
interactionAPI
|
interactionAPI
|
||||||
.updateInteraction(deviceId, interaction.settings, as)
|
.updateInteraction(deviceId, interaction.settings, as)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
openSnack("Updated Interaction Conditions");
|
openSnack("Updated Interaction Conditions");
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
openSnack("Failed to Update Interaction Conditions");
|
openSnack("Failed to Update Interaction Conditions");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const customMark = (val: string, arrowColor: string) => {
|
const customMark = (val: string, arrowColor: string) => {
|
||||||
|
|
@ -238,6 +242,7 @@ export default function BinConditioningInteraction(props: Props) {
|
||||||
max={describer.max()}
|
max={describer.max()}
|
||||||
value={sliderVals.get(condition.measurementType) ?? describer.min()}
|
value={sliderVals.get(condition.measurementType) ?? describer.min()}
|
||||||
onChange={(_, val) => {
|
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));
|
condition.value = Math.round(describer.toStored(val as number));
|
||||||
let sliders = cloneDeep(sliderVals);
|
let sliders = cloneDeep(sliderVals);
|
||||||
sliders.set(condition.measurementType, val as number);
|
sliders.set(condition.measurementType, val as number);
|
||||||
|
|
@ -249,14 +254,16 @@ export default function BinConditioningInteraction(props: Props) {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button
|
{!parentUpdate &&
|
||||||
variant="contained"
|
<Button
|
||||||
color="primary"
|
variant="contained"
|
||||||
onClick={() => {
|
color="primary"
|
||||||
updateInteraction();
|
onClick={() => {
|
||||||
}}>
|
updateInteraction();
|
||||||
Update Conditions
|
}}>
|
||||||
</Button>
|
Update Conditions
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { CableData } from "bin/3dView/Data/BuildCableData";
|
||||||
import ModeChangeDialog from "bin/conditioning/modeChangeDialog";
|
import ModeChangeDialog from "bin/conditioning/modeChangeDialog";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { useMobile } from "hooks";
|
import { useMobile } from "hooks";
|
||||||
|
import BinModeController from "./binModes/BinModeController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
|
@ -35,6 +36,7 @@ export default function bin3dVisualizer(props: Props){
|
||||||
const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false)
|
const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false)
|
||||||
const [binDisplay, setBinDisplay] = useState<string>("temp")
|
const [binDisplay, setBinDisplay] = useState<string>("temp")
|
||||||
const [binMode, setBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
const [binMode, setBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
||||||
|
const [newMode, setNewMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
||||||
const [openModeChange, setOpenModeChange] = useState(false)
|
const [openModeChange, setOpenModeChange] = useState(false)
|
||||||
const [selectedCable, setSelectedCable] = useState<CableData | undefined>(undefined)
|
const [selectedCable, setSelectedCable] = useState<CableData | undefined>(undefined)
|
||||||
const [selectedNode, setSelectedNode] = useState<NodeData | undefined>(undefined)
|
const [selectedNode, setSelectedNode] = useState<NodeData | undefined>(undefined)
|
||||||
|
|
@ -58,10 +60,10 @@ export default function bin3dVisualizer(props: Props){
|
||||||
setBinMode(bin.settings.mode)
|
setBinMode(bin.settings.mode)
|
||||||
},[bin])
|
},[bin])
|
||||||
|
|
||||||
const updateBin = () => {
|
const updateBin = (newMode: pond.BinMode) => {
|
||||||
if(updateBinCallback){
|
if(updateBinCallback){
|
||||||
let clone = cloneDeep(bin)
|
let clone = cloneDeep(bin)
|
||||||
clone.settings.mode = binMode
|
clone.settings.mode = newMode
|
||||||
updateBinCallback(clone)
|
updateBinCallback(clone)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -86,8 +88,10 @@ export default function bin3dVisualizer(props: Props){
|
||||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
||||||
}}
|
}}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setBinMode(event.target.value as pond.BinMode)
|
setNewMode(event.target.value as pond.BinMode)
|
||||||
setOpenModeChange(true)
|
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
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value={pond.BinMode.BIN_MODE_NONE}>Select Mode..</MenuItem>
|
<MenuItem value={pond.BinMode.BIN_MODE_NONE}>Select Mode..</MenuItem>
|
||||||
|
|
@ -327,7 +331,7 @@ export default function bin3dVisualizer(props: Props){
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<ModeChangeDialog
|
{/* <ModeChangeDialog
|
||||||
binKey={bin.key()}
|
binKey={bin.key()}
|
||||||
binMode={binMode}
|
binMode={binMode}
|
||||||
grain={bin.settings.inventory?.grainType}
|
grain={bin.settings.inventory?.grainType}
|
||||||
|
|
@ -361,7 +365,19 @@ export default function bin3dVisualizer(props: Props){
|
||||||
changeComplete={() => {
|
changeComplete={() => {
|
||||||
setModeChangeInProgress(false)
|
setModeChangeInProgress(false)
|
||||||
}}
|
}}
|
||||||
|
/> */}
|
||||||
|
<BinModeController
|
||||||
|
bin={bin}
|
||||||
|
binPrefs={binPrefs}
|
||||||
|
devices={devices}
|
||||||
|
componentDevices={componentDevices}
|
||||||
|
componentMap={componentMap}
|
||||||
|
newMode={newMode}
|
||||||
|
open={openModeChange}
|
||||||
|
updateMode={(newMode) => {
|
||||||
|
updateBin(newMode)
|
||||||
|
}}
|
||||||
|
onClose={()=>{setOpenModeChange(false)}}
|
||||||
/>
|
/>
|
||||||
<Box position={"relative"} height={600}>
|
<Box position={"relative"} height={600}>
|
||||||
<Bin3dView
|
<Bin3dView
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,162 @@
|
||||||
import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
|
import { Box, Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
|
||||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
import { Bin } from "models";
|
import { Bin, Component, Device, Interaction } from "models";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import StorageMode from "./StorageMode";
|
import StorageMode from "./StorageMode";
|
||||||
import DryingMode from "./DryingMode";
|
import DryingMode, { DryingBaseStepCount } from "./DryingMode";
|
||||||
import HydratingMode from "./HydratingMode";
|
import HydratingMode from "./HydratingMode";
|
||||||
import CooldownMode from "./CooldownMode";
|
import CooldownMode from "./CooldownMode";
|
||||||
|
import { PromiseProgress, Stage, Step } from "common/PromiseProgress";
|
||||||
|
import { useComponentAPI, useInteractionsAPI } from "providers";
|
||||||
|
import { ComponentSet } from "bin/conditioning/pickComponentSet";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
newMode: pond.BinMode //used to control which dialog to open
|
newMode: pond.BinMode //used to control which dialog to open
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||||
|
devices: Device[]
|
||||||
|
componentDevices: Map<string, number>
|
||||||
|
componentMap: Map<string, Component>
|
||||||
|
updateMode: (newMode: pond.BinMode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinModeControl(props: Props) {
|
export default function BinModeController(props: Props) {
|
||||||
const {open, onClose, newMode} = props
|
const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props
|
||||||
|
const [showProgress, setShowProgress] = useState(false)
|
||||||
|
const [deviceComponents, setDeviceComponents] = useState<Map<number, Component[]>>(new Map())
|
||||||
|
const [promiseStages, setPromiseStages] = useState<Stage[]>([])
|
||||||
|
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<number, Component[]> = 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:
|
//steps involved for devices when changing the bin mode:
|
||||||
//remove conflicting interactions
|
//remove conflicting interactions
|
||||||
//add new interactions
|
//add new interactions
|
||||||
//updating the controllers
|
//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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<DialogTitle>Change Mode</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<PromiseProgress
|
||||||
|
stages={promiseStages}
|
||||||
|
description="These are the changes that will occur in order to change your bin mode with the given options. Press start to begin."
|
||||||
|
failFast
|
||||||
|
onStart={() => {
|
||||||
|
//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()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeDialog}>Cancel</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeContent = () => {
|
||||||
switch(newMode){
|
switch(newMode){
|
||||||
case pond.BinMode.BIN_MODE_DRYING:
|
case pond.BinMode.BIN_MODE_DRYING:
|
||||||
return <DryingMode />
|
//setNumSteps(DryingStepCount)
|
||||||
|
return <DryingMode
|
||||||
|
grain={bin.settings.inventory?.grainType}
|
||||||
|
devices={devices}
|
||||||
|
binPrefs={binPrefs}
|
||||||
|
deviceComponents={deviceComponents}
|
||||||
|
cancel={closeDialog}
|
||||||
|
confirm={(conflicting, device, newInteractions, components) => {
|
||||||
|
buildStages(conflicting, device, newInteractions, components)
|
||||||
|
setShowProgress(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
case pond.BinMode.BIN_MODE_HYDRATING:
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
||||||
return <HydratingMode />
|
return <HydratingMode />
|
||||||
case pond.BinMode.BIN_MODE_COOLDOWN:
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
||||||
|
|
@ -39,16 +167,8 @@ export default function BinModeControl(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveDialog open={open} onClose={onClose}>
|
<ResponsiveDialog open={open} onClose={closeDialog}>
|
||||||
<DialogTitle>Change Bin Mode</DialogTitle>
|
{showProgress ? progressContent() : modeContent()}
|
||||||
<DialogContent>
|
|
||||||
{renderContent()}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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) => {
|
||||||
<Box>
|
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<number, Component[]>
|
||||||
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||||
|
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<Option[]>([])
|
||||||
|
const [deviceOption, setDeviceOption] = useState<Option>()
|
||||||
|
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||||
|
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||||
|
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||||
|
const [heaters, setHeaters] = useState<Controller[]>([])
|
||||||
|
const [fans, setFans] = useState<Controller[]>([])
|
||||||
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
|
const [interactionLoading,setInteractionsLoading] = useState(false)
|
||||||
|
|
||||||
|
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
|
||||||
|
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||||
|
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||||
|
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||||
|
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||||
|
|
||||||
|
|
||||||
|
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||||
|
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//sort the components according to the bin preferences
|
||||||
|
useEffect(()=>{
|
||||||
|
if (!selectedDevice) return;
|
||||||
|
if (!deviceComponents.get(selectedDevice.id())) return;
|
||||||
|
var plenums: Plenum[] = [];
|
||||||
|
var ambients: Ambient[] = [];
|
||||||
|
//var grainCables: GrainCable[] = [];
|
||||||
|
var heaters: Controller[] = [];
|
||||||
|
var fans: Controller[] = [];
|
||||||
|
var sinkMap: Map<string, Component> = new Map()
|
||||||
|
var sourceMap: Map<string, Component> = new Map()
|
||||||
|
|
||||||
|
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
|
||||||
|
let pref = binPrefs?.get(comp.key());
|
||||||
|
if (pref) {
|
||||||
|
if (pref.type) {
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||||
|
plenums.push(Plenum.create(comp));
|
||||||
|
sourceMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||||
|
ambients.push(Ambient.create(comp));
|
||||||
|
sourceMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
// if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE)
|
||||||
|
// grainCables.push(GrainCable.create(comp));
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
|
||||||
|
let heater = Controller.create(comp);
|
||||||
|
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||||
|
heaters.push(heater);
|
||||||
|
sinkMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||||
|
let fan = Controller.create(comp)
|
||||||
|
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||||
|
fans.push(fan);
|
||||||
|
sinkMap.set(comp.locationString(), comp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPlenums(plenums);
|
||||||
|
setAmbients(ambients);
|
||||||
|
setHeaters(heaters);
|
||||||
|
setFans(fans);
|
||||||
|
setSourceMap(sourceMap)
|
||||||
|
setSinkMap(sinkMap)
|
||||||
|
|
||||||
|
//also load the interactions for the selected device so that we can find any conflicting ones
|
||||||
|
setInteractionsLoading(true)
|
||||||
|
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
|
||||||
|
setExistingInteractions(resp)
|
||||||
|
}).finally(() => {
|
||||||
|
setInteractionsLoading(false)
|
||||||
|
})
|
||||||
|
}, [deviceComponents, selectedDevice, binPrefs]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let o: Option[] = [];
|
||||||
|
devices.forEach(device => {
|
||||||
|
let newOption: Option = {device: device, label: device.name()}
|
||||||
|
if(deviceOption === undefined) {
|
||||||
|
setDeviceOption(newOption)
|
||||||
|
setSelectedDevice(device)
|
||||||
|
}
|
||||||
|
o.push(newOption);
|
||||||
|
});
|
||||||
|
setOptions(o);
|
||||||
|
}, [devices, setOptions]);
|
||||||
|
|
||||||
|
const buildHeaterInteraction = (
|
||||||
|
sensor: Plenum | Ambient,
|
||||||
|
heater: Controller,
|
||||||
|
): pond.InteractionSettings => {
|
||||||
|
let interaction = pond.InteractionSettings.create({
|
||||||
|
source: sensor.location(),
|
||||||
|
sink: heater.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 temp = 0;
|
||||||
|
let hum = 0;
|
||||||
|
|
||||||
</Box>
|
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 (
|
||||||
|
<Stepper
|
||||||
|
activeStep={currentStep}
|
||||||
|
alternativeLabel
|
||||||
|
classes={{
|
||||||
|
root: classes.stepper
|
||||||
|
}}>
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Step key={i}>
|
||||||
|
<StepLabel>{s.label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleStep = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography>Select your drying method</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
value={dryingMethod}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
setDryingMethod(value)
|
||||||
|
}}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Radio />}
|
||||||
|
value={"air"}
|
||||||
|
label={"Natural Air"}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Radio />}
|
||||||
|
value={"heat"}
|
||||||
|
label={"Supplemental Heat"}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceSelector = () => {
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
options={options}
|
||||||
|
value={deviceOption}
|
||||||
|
fullWidth
|
||||||
|
getOptionLabel={option => option.label || ""}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if(newValue){
|
||||||
|
setSelectedDevice(newValue.device);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceStep = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography>Devices</Typography>
|
||||||
|
{deviceSelector()}
|
||||||
|
{interactionLoading ? <CircularProgress /> :
|
||||||
|
<ConditioningSelector
|
||||||
|
plenums={plenums}
|
||||||
|
ambients={ambients}
|
||||||
|
fans={fans}
|
||||||
|
heaters={heaters}
|
||||||
|
//presets={presets}
|
||||||
|
binMode={pond.BinMode.BIN_MODE_DRYING}
|
||||||
|
updateSets={(sets) => {
|
||||||
|
//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)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 (
|
||||||
|
<Box>
|
||||||
|
<Typography>Interactions</Typography>
|
||||||
|
{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 (
|
||||||
|
<ConditionDisplay
|
||||||
|
key={index}
|
||||||
|
interaction={temp}
|
||||||
|
sink={sink}
|
||||||
|
source={source}
|
||||||
|
changeConditions={(newSettings) => {
|
||||||
|
console.log(newSettings)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepperContent = () => {
|
||||||
|
switch(currentStep){
|
||||||
|
case 1:
|
||||||
|
return deviceStep()
|
||||||
|
case 2:
|
||||||
|
return interactionStep()
|
||||||
|
default:
|
||||||
|
return styleStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||||
|
<Button onClick={cancel}>Close</Button>
|
||||||
|
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||||
|
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||||
|
{/* next - goes to the next step, hidden on the last step */}
|
||||||
|
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||||
|
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||||
|
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm(conflictingInteractions, selectedDevice.id(), interactionsToAdd, componentSets)}}>Confirm</Button>}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DialogTitle>Bin Drying</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{stepper()}
|
||||||
|
{stepperContent()}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{actions()}
|
||||||
|
</DialogActions>
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
313
src/bin/binModes/conditionDisplay.tsx
Normal file
313
src/bin/binModes/conditionDisplay.tsx
Normal file
|
|
@ -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<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 }] = useGlobalState();
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
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 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) => {
|
||||||
|
//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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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={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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue