frontend/src/bin/binSummary/components/bin3dVisualizer.tsx

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>
)
}