393 lines
No EOL
15 KiB
TypeScript
393 lines
No EOL
15 KiB
TypeScript
import { Box, Checkbox, FormControl, MenuItem, List, ListItem, InputLabel, Select, Typography, Stack } from "@mui/material";
|
|
import Bin3dView from "bin/3dView/Scene/Bin3dView";
|
|
import { Bin, Component, Device } from "models";
|
|
import { Controller } from "models/Controller";
|
|
import { pond, quack } from "protobuf-ts/pond";
|
|
import React, { useEffect } from "react";
|
|
import { useState } from "react";
|
|
import NodeControls from "./nodeControls";
|
|
import { NodeData } from "bin/3dView/Data/BuildNodeData";
|
|
import { CableData } from "bin/3dView/Data/BuildCableData";
|
|
import ModeChangeDialog from "bin/conditioning/modeChangeDialog";
|
|
import { cloneDeep } from "lodash";
|
|
import { useMobile } from "hooks";
|
|
|
|
interface Props {
|
|
bin: Bin
|
|
// cables?: GrainCable[]
|
|
devices: Device[]
|
|
fans?: Controller[]
|
|
heaters?: Controller[]
|
|
permissions: pond.Permission[]
|
|
componentDevices: Map<string, number>
|
|
componentMap: Map<string, Component>
|
|
binPrefs?: Map<string, pond.BinComponentPreferences>
|
|
updateBinCallback?: (bin: Bin) => void
|
|
}
|
|
export default function bin3dVisualizer(props: Props){
|
|
const {bin, fans, heaters, permissions, devices, componentDevices, componentMap, binPrefs, updateBinCallback} = props
|
|
const [showHeatmap, setShowHeatmap] = useState(true)
|
|
// const [showHotspots, setShowHotspots] = useState(false)
|
|
// const [showFill, setShowFill] = useState(false)
|
|
const [showTemp, setShowTemp] = useState(true)
|
|
const [showMoisture, setShowMoisture] = useState(false)
|
|
const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false)
|
|
const [binDisplay, setBinDisplay] = useState<string>("temp")
|
|
const [binMode, setBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
|
const [openModeChange, setOpenModeChange] = useState(false)
|
|
const [selectedCable, setSelectedCable] = useState<CableData | undefined>(undefined)
|
|
const [selectedNode, setSelectedNode] = useState<NodeData | undefined>(undefined)
|
|
const [selectedCableDevice, setSelectedCableDevice] = useState(Device.create()) //this is the device that the cable that has the node that was clicked on belongs to
|
|
const [filteredComponents, setFilteredComponents] = useState<Component[]>([]) //components that are filtered to only include ones on the same device
|
|
const [openNodeControls, setOpenNodeControls] = useState(false)
|
|
const [devMap, setDevMap] = useState<Map<number, Device>>(new Map())
|
|
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
|
|
const isMobile = useMobile()
|
|
|
|
useEffect(()=>{
|
|
let newMap: Map<number, Device> = new Map()
|
|
devices.forEach(d => {
|
|
newMap.set(d.id(), d)
|
|
})
|
|
setDevMap(newMap)
|
|
},[devices])
|
|
|
|
useEffect(()=>{
|
|
setBinMode(bin.settings.mode)
|
|
},[bin])
|
|
|
|
const updateBin = () => {
|
|
if(updateBinCallback){
|
|
let clone = cloneDeep(bin)
|
|
clone.settings.mode = binMode
|
|
updateBinCallback(clone)
|
|
}
|
|
};
|
|
|
|
const binModeControl = () => {
|
|
return (
|
|
<Box>
|
|
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>{bin.name()}</Typography>
|
|
<FormControl fullWidth>
|
|
<Select
|
|
value={binMode}
|
|
disabled={modeChangeInProgress}
|
|
sx={{
|
|
borderRadius: 1,
|
|
bgcolor: 'rgba(255,255,255,0.08)',
|
|
'& .MuiSelect-select': {
|
|
py: 1.5,
|
|
},
|
|
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
'&:hover .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
}}
|
|
onChange={event => {
|
|
console.log("change bin mode")
|
|
setBinMode(event.target.value as pond.BinMode)
|
|
setOpenModeChange(true)
|
|
}}
|
|
>
|
|
<MenuItem value={pond.BinMode.BIN_MODE_NONE}>Select Mode..</MenuItem>
|
|
<MenuItem value={pond.BinMode.BIN_MODE_STORAGE}>Storage</MenuItem>
|
|
<MenuItem value={pond.BinMode.BIN_MODE_DRYING}>Drying</MenuItem>
|
|
<MenuItem value={pond.BinMode.BIN_MODE_HYDRATING}>Hydrating</MenuItem>
|
|
<MenuItem value={pond.BinMode.BIN_MODE_COOLDOWN}>Cooldown</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const heatmapDisplay = () => {
|
|
return (
|
|
<Box>
|
|
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>Heatmap Display</Typography>
|
|
<FormControl fullWidth>
|
|
{/* <InputLabel>Display</InputLabel> */}
|
|
<Select
|
|
value={binDisplay}
|
|
sx={{
|
|
borderRadius: 1,
|
|
bgcolor: 'rgba(255,255,255,0.08)',
|
|
'& .MuiSelect-select': {
|
|
py: 1.5,
|
|
},
|
|
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
'&:hover .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
|
}}
|
|
onChange={event => {
|
|
let selection = event.target.value
|
|
setBinDisplay(selection)
|
|
if(selection === "temp"){
|
|
setShowHeatmap(true)
|
|
setShowTemp(true)
|
|
setShowMoisture(false)
|
|
setShowMoistureHeatmap(false)
|
|
}
|
|
if(selection === "moisture"){
|
|
setShowHeatmap(false)
|
|
setShowTemp(false)
|
|
setShowMoisture(true)
|
|
setShowMoistureHeatmap(true)
|
|
}
|
|
}}>
|
|
<MenuItem value={"temp"}>Temperature</MenuItem>
|
|
<MenuItem value={"moisture"}>Moisture</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const controllerState = (controller: Controller) => {
|
|
let state = false
|
|
controller.status.lastGoodMeasurement.forEach(um => {
|
|
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN){
|
|
if(um.values.length > 0){
|
|
let readings = um.values
|
|
if(readings[readings.length-1].values.length > 0){
|
|
let nodes = readings[readings.length-1].values
|
|
if (nodes[nodes.length -1] === 1){
|
|
state = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return state
|
|
}
|
|
|
|
const controllerDisplay = () => {
|
|
return (
|
|
<Box>
|
|
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>
|
|
Controllers
|
|
</Typography>
|
|
{fans && fans.map(fan => {
|
|
let isOn = controllerState(fan)
|
|
return (
|
|
<Box
|
|
key={fan.key()}
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
bgcolor: 'rgba(255,255,255,0.05)',
|
|
borderRadius: 1,
|
|
px: 2,
|
|
py: 1,
|
|
}}
|
|
>
|
|
<Typography sx={{fontSize: isMobile ? 13 : 15}}>{fan.name()}</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 15}}>
|
|
{isOn ? "ON" : "OFF"}
|
|
</Typography>
|
|
<Box sx={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: '50%',
|
|
bgcolor: isOn ? 'success.main' : 'error.main'
|
|
}} />
|
|
</Box>
|
|
</Box>
|
|
)})}
|
|
{heaters && heaters.map(heater => {
|
|
let isOn = controllerState(heater)
|
|
return (
|
|
<Box
|
|
key={heater.key()}
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
bgcolor: 'rgba(255,255,255,0.05)',
|
|
borderRadius: 1,
|
|
px: 2,
|
|
py: 1,
|
|
}}
|
|
>
|
|
<Typography sx={{fontSize: isMobile ? 13 : 15}}>{heater.name()}</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 15}}>
|
|
{isOn ? "ON" : "OFF"}
|
|
</Typography>
|
|
<Box sx={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: '50%',
|
|
bgcolor: isOn ? 'success.main' : 'error.main'
|
|
}} />
|
|
</Box>
|
|
</Box>
|
|
)})}
|
|
</Box>
|
|
// loop through controllers displaying each one
|
|
|
|
)
|
|
}
|
|
|
|
const desktopHUD = () => {
|
|
return (
|
|
<Box position={"absolute"} top={20} left={30} height={"80%"} width={"30%"} padding={2}>
|
|
<Stack direction={"column"} justifyContent={"space-between"} sx={{height: "100%", width: "100%"}}>
|
|
{binModeControl()}
|
|
{heatmapDisplay()}
|
|
{controllerDisplay()}
|
|
</Stack>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const mobileHUD = () => {
|
|
return (
|
|
<React.Fragment>
|
|
{/* Top-left: Bin Name / Mode */}
|
|
<Box
|
|
position="absolute"
|
|
top={12}
|
|
left={12}
|
|
width="40%"
|
|
sx={{ pointerEvents: 'none' }}
|
|
>
|
|
<Box sx={{ pointerEvents: 'auto' }}>
|
|
{binModeControl()}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Top-right: Heatmap Display */}
|
|
<Box
|
|
position="absolute"
|
|
top={12}
|
|
right={12}
|
|
width="40%"
|
|
sx={{ pointerEvents: 'none' }}
|
|
>
|
|
<Box sx={{ pointerEvents: 'auto' }}>
|
|
{heatmapDisplay()}
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Bottom-left: Controller Status */}
|
|
<Box
|
|
position="absolute"
|
|
bottom={12}
|
|
left={12}
|
|
width="45%"
|
|
sx={{ pointerEvents: 'none' }}
|
|
>
|
|
<Box sx={{ pointerEvents: 'auto' }}>
|
|
{controllerDisplay()}
|
|
</Box>
|
|
</Box>
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{(selectedCable && selectedNode) &&
|
|
<NodeControls
|
|
bin={bin}
|
|
cable={selectedCable}
|
|
node={selectedNode}
|
|
open={openNodeControls}
|
|
permissions={permissions}
|
|
device={selectedCableDevice}
|
|
filteredComponents={filteredComponents}
|
|
componentMap={componentMap}
|
|
onClose={() => {
|
|
setOpenNodeControls(false)
|
|
}}
|
|
/>
|
|
}
|
|
<ModeChangeDialog
|
|
binKey={bin.key()}
|
|
binMode={binMode}
|
|
grain={bin.settings.inventory?.grainType}
|
|
devices={devices}
|
|
open={openModeChange}
|
|
compDevMap={componentDevices}
|
|
preferences={binPrefs}
|
|
onClose={(refresh) => {
|
|
setOpenModeChange(false)
|
|
if(refresh) {
|
|
updateBin()
|
|
}else{
|
|
setBinMode(bin.settings.mode)
|
|
};
|
|
}}
|
|
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
|
|
// defaultOutdoorTemp={outdoorTemp}
|
|
// defaultOutdoorHumidity={outdoorHumidity}
|
|
// changeTargetMoisture={newMoisture => {
|
|
// setTargetMoisture(newMoisture)
|
|
// }}
|
|
// changeOutdoorTemp={newTemp => {
|
|
// setOutdoorTemp(newTemp)
|
|
// }}
|
|
// changeOutdoorHumidity={newHumidity => {
|
|
// setOutdoorHumidity(newHumidity)
|
|
// }}
|
|
startChange={() => {
|
|
setModeChangeInProgress(true)
|
|
}}
|
|
changeComplete={() => {
|
|
setModeChangeInProgress(false)
|
|
}}
|
|
|
|
/>
|
|
<Box position={"relative"} height={600}>
|
|
<Bin3dView
|
|
bin={bin}
|
|
fillPercent={bin.fillPercent()/100}//expects a decimal between 0 and 1
|
|
nodeClick={(node, cable) => {
|
|
//this will be how to control the dialog to update the top nodes and node exclusion etc (make new component called NodeControls for this)
|
|
//use the cable data to get the device it belongs to
|
|
let d = devMap.get(cable.grainCable.device)
|
|
if(d !== undefined){
|
|
setSelectedNode(node)
|
|
setSelectedCable(cable)
|
|
setSelectedCableDevice(d)
|
|
//filter the controls so that only components on THIS device are options for new interactions, a side note of this using the components that are in the bins status
|
|
//is that it will indirectly also filter by bin as well so only components that are both on the bin and the same device as the cable will appear
|
|
let filtered: Component[] = []
|
|
if(fans){
|
|
fans.forEach((f) => {
|
|
if(componentDevices.get(f.key()) === d.id()){
|
|
filtered.push(f.asComponent())
|
|
}
|
|
})
|
|
}
|
|
if(heaters){
|
|
heaters.forEach((h) => {
|
|
if(componentDevices.get(h.key()) === d.id()){
|
|
filtered.push(h.asComponent())
|
|
}
|
|
})
|
|
}
|
|
//also make sure to push the cable into the list of filtered components for the source
|
|
let c = componentMap.get(cable.grainCable.key)
|
|
if(c !== undefined){
|
|
filtered.push(c)
|
|
}
|
|
setFilteredComponents(filtered)
|
|
setOpenNodeControls(true)
|
|
}
|
|
}}
|
|
scale={100}
|
|
showTempHeatmap={showHeatmap}
|
|
showMoistureHeatmap={showMoistureHeatmap}
|
|
showTemp={showTemp}
|
|
showMoisture={showMoisture}
|
|
// showGrain={showFill}
|
|
// showHotspots={showHotspots}
|
|
xOffset={isMobile ? undefined : -150}
|
|
/>
|
|
{isMobile ? mobileHUD() : desktopHUD()}
|
|
</Box>
|
|
</React.Fragment>
|
|
)
|
|
} |