imported the gate and terminal related files to prep for the aviation map
This commit is contained in:
parent
e2e061151b
commit
505cb8e3aa
25 changed files with 4868 additions and 9 deletions
57
src/gate/AddGateFab.tsx
Normal file
57
src/gate/AddGateFab.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Fab, Theme } from "@mui/material";
|
||||
import Icon from "assets/products/Aviation/AddPlaneIconBlack.png";
|
||||
import { ImgIcon } from "common/ImgIcon";
|
||||
import { useMobile } from "hooks";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
pulse: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
"@keyframes pulsate": {
|
||||
to: {
|
||||
boxShadow: "0 0 0 16px" + theme.palette.primary.main + "00"
|
||||
}
|
||||
},
|
||||
fab: {
|
||||
background: theme.palette.primary.main,
|
||||
"&:hover": {
|
||||
background: theme.palette.primary.main
|
||||
},
|
||||
"&:focus": {
|
||||
background: theme.palette.primary.main
|
||||
},
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(8), //for mobile navigator
|
||||
right: theme.spacing(2),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
bottom: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
pulse: {
|
||||
boxShadow: "0 0 0 0 " + theme.palette.primary.main + "75",
|
||||
animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)"
|
||||
}
|
||||
}));
|
||||
|
||||
export default function AddFab(props: Props) {
|
||||
const { onClick, pulse } = props;
|
||||
const classes = useStyles();
|
||||
const isMobile = useMobile();
|
||||
|
||||
const pulseString = pulse ? classes.pulse : "";
|
||||
const classString = classes.fab + " " + pulseString;
|
||||
|
||||
return (
|
||||
<Fab
|
||||
onClick={onClick}
|
||||
aria-label="Add New Gate"
|
||||
className={classString}
|
||||
size={isMobile ? "medium" : "large"}>
|
||||
<ImgIcon alt="Add New Gate" src={Icon} />
|
||||
</Fab>
|
||||
);
|
||||
}
|
||||
203
src/gate/GateActions.tsx
Normal file
203
src/gate/GateActions.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import MoreIcon from "@mui/icons-material/MoreVert";
|
||||
import React, { useState } from "react";
|
||||
import GateSettings from "./GateSettings";
|
||||
import { Gate } from "models/Gate";
|
||||
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
|
||||
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
|
||||
import ObjectUsers from "user/ObjectUsers";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Scope } from "models";
|
||||
import ObjectTeams from "teams/ObjectTeams";
|
||||
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
|
||||
import ShareObject from "user/ShareObject";
|
||||
import { blue } from "@mui/material/colors";
|
||||
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
|
||||
import { Share } from "@mui/icons-material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
shareIcon: {
|
||||
color: blue["500"],
|
||||
"&:hover": {
|
||||
color: blue["600"]
|
||||
}
|
||||
},
|
||||
removeIcon: {
|
||||
color: "var(--status-alert)"
|
||||
},
|
||||
red: {
|
||||
color: "var(--status-alert)"
|
||||
},
|
||||
blueIcon: {
|
||||
color: blue["500"],
|
||||
"&:hover": {
|
||||
color: blue["600"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
interface OpenState {
|
||||
users: boolean;
|
||||
teams: boolean;
|
||||
settings: boolean;
|
||||
removeSelf: boolean;
|
||||
share: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
refreshCallback: () => void;
|
||||
permissions: pond.Permission[];
|
||||
}
|
||||
|
||||
export default function GateActions(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { gate, refreshCallback, permissions } = props;
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [openState, setOpenState] = useState<OpenState>({
|
||||
users: false,
|
||||
teams: false,
|
||||
settings: false,
|
||||
removeSelf: false,
|
||||
share: false
|
||||
});
|
||||
|
||||
const groupMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted
|
||||
disableAutoFocusItem>
|
||||
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, share: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
dense>
|
||||
<ListItemIcon>
|
||||
<Share className={classes.blueIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Share" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, users: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<ObjectUsersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Users" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, teams: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<ObjectTeamsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Teams" />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, removeSelf: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<RemoveSelfIcon className={classes.red} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Leave" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogs = () => {
|
||||
const key = gate.key;
|
||||
const label = gate.name;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GateSettings
|
||||
gate={gate}
|
||||
open={openState.settings}
|
||||
close={newGate => {
|
||||
if (newGate) {
|
||||
}
|
||||
setOpenState({ ...openState, settings: false });
|
||||
}}
|
||||
/>
|
||||
<ShareObject
|
||||
scope={{ kind: "gate", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.share}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
|
||||
/>
|
||||
<ObjectUsers
|
||||
scope={{ kind: "gate", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.users}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<ObjectTeams
|
||||
scope={{ kind: "gate", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.teams}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<RemoveSelfFromObject
|
||||
scope={{ kind: "gate", key: key } as Scope}
|
||||
path={"terminal"}
|
||||
label={label}
|
||||
isDialogOpen={openState.removeSelf}
|
||||
closeDialogCallback={() => {
|
||||
setOpenState({ ...openState, removeSelf: false });
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tooltip title="Settings">
|
||||
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
aria-owns={anchorEl ? "groupMenu" : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
{dialogs()}
|
||||
{groupMenu()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
413
src/gate/GateDevice.tsx
Normal file
413
src/gate/GateDevice.tsx
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Grid2 as Grid,
|
||||
MenuItem,
|
||||
Select,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { Component, Device } from "models";
|
||||
import { Gate } from "models/Gate";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGlobalState, useGateAPI } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMobile, useSnackbar } from "hooks";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import GateDeviceInteraction from "./GateDeviceInteraction";
|
||||
import GateSVG from "./GateSVG";
|
||||
import GateGraphs from "./GateGraphs";
|
||||
//import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
|
||||
import { getThemeType } from "theme";
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
comprehensiveDevice: pond.ComprehensiveDevice;
|
||||
linkedCompList: Component[];
|
||||
drawerView?: boolean;
|
||||
}
|
||||
|
||||
export default function GateDevice(props: Props) {
|
||||
const { gate, comprehensiveDevice, linkedCompList, drawerView } = props;
|
||||
const [device, setDevice] = useState<Device>(Device.create());
|
||||
const [componentOptions, setComponentOptions] = useState<Map<string, Component>>(
|
||||
new Map<string, Component>()
|
||||
);
|
||||
const gateAPI = useGateAPI();
|
||||
const [tempKey, setTempKey] = useState("");
|
||||
const [pressureKey, setPressureKey] = useState("");
|
||||
const [ambientKey, setAmbientKey] = useState("");
|
||||
const { openSnack } = useSnackbar();
|
||||
const [{ user }] = useGlobalState();
|
||||
const [interactionDialog, setInteractionDialog] = useState(false);
|
||||
const [densityTemp, setDensityTemp] = useState(0);
|
||||
const isMobile = useMobile();
|
||||
const [pcaState, setPCAState] = useState(false);
|
||||
const [pcaFanOn, setPCAFanOn] = useState(false);
|
||||
const [detail, setDetail] = useState<"sensors" | "analytics">("analytics");
|
||||
const [lastAmbient, setLastAmbient] = useState(0);
|
||||
const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 });
|
||||
const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 });
|
||||
|
||||
// const StyledToggleButtonGroup = withStyles(theme => ({
|
||||
// grouped: {
|
||||
// margin: theme.spacing(-0.5),
|
||||
// border: "none",
|
||||
// padding: theme.spacing(1),
|
||||
// "&:not(:first-child):not(:last-child)": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.5),
|
||||
// marginLeft: theme.spacing(0.5)
|
||||
// },
|
||||
// "&:first-child": {
|
||||
// borderRadius: 24,
|
||||
// marginLeft: theme.spacing(0.25)
|
||||
// },
|
||||
// "&:last-child": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.25)
|
||||
// }
|
||||
// },
|
||||
// root: {
|
||||
// backgroundColor: darken(
|
||||
// theme.palette.background.paper,
|
||||
// getThemeType() === "light" ? 0.05 : 0.25
|
||||
// ),
|
||||
// borderRadius: 24,
|
||||
// content: "border-box"
|
||||
// }
|
||||
// }))(ToggleButtonGroup);
|
||||
|
||||
// const StyledToggle = withStyles({
|
||||
// root: {
|
||||
// backgroundColor: "transparent",
|
||||
// overflow: "visible",
|
||||
// content: "content-box",
|
||||
// "&$selected": {
|
||||
// backgroundColor: "gold",
|
||||
// color: "black",
|
||||
// borderRadius: 24,
|
||||
// fontWeight: "bold"
|
||||
// },
|
||||
// "&$selected:hover": {
|
||||
// backgroundColor: "rgb(255, 255, 0)",
|
||||
// color: "black",
|
||||
// borderRadius: 24
|
||||
// }
|
||||
// },
|
||||
// selected: {}
|
||||
// })(ToggleButton);
|
||||
|
||||
useEffect(() => {
|
||||
if (comprehensiveDevice.device) {
|
||||
setDevice(Device.any(comprehensiveDevice.device));
|
||||
}
|
||||
if (comprehensiveDevice.components) {
|
||||
let components: Map<string, Component> = new Map<string, Component>();
|
||||
setAmbientKey("");
|
||||
setTempKey("");
|
||||
setPressureKey("");
|
||||
comprehensiveDevice.components.forEach(comp => {
|
||||
let c = Component.any(comp);
|
||||
if (linkedCompList.some(el => el.key() === c.key())) {
|
||||
components.set(c.key(), c);
|
||||
//determine the positioned components using the gates preferences for it components
|
||||
switch (gate.preferences[c.key()]) {
|
||||
case pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT:
|
||||
setAmbientKey(c.key());
|
||||
c.status.measurement.forEach(um => {
|
||||
let measurement = UnitMeasurement.any(um, user);
|
||||
if (measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
|
||||
if (measurement.values.length > 1 && measurement.values[0].values.length > 0) {
|
||||
setDensityTemp(measurement.values[0].values[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP:
|
||||
setTempKey(c.key());
|
||||
break;
|
||||
case pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE:
|
||||
setPressureKey(c.key());
|
||||
if (
|
||||
c.status.measurement[0] &&
|
||||
c.status.measurement[0].values[0] &&
|
||||
c.status.measurement[0].values[0].values[1]
|
||||
) {
|
||||
setPCAFanOn(c.status.measurement[0].values[0].values[1] > 250); //apx 1 iwg
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// type is unknown, do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
setComponentOptions(components);
|
||||
}
|
||||
}, [comprehensiveDevice, gate, linkedCompList, user]);
|
||||
|
||||
const gateComponentUpdate = (componentKey: string, gateCompType: pond.GateComponentType) => {
|
||||
if (componentKey !== "") {
|
||||
gateAPI
|
||||
.updatePrefs(
|
||||
gate.key,
|
||||
"component",
|
||||
device.id() + ":" + componentKey,
|
||||
gateCompType,
|
||||
[gate.key],
|
||||
["gate"]
|
||||
)
|
||||
.then(resp => {
|
||||
openSnack("Component Prefence Updated");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let tempComponent = componentOptions.get(tempKey);
|
||||
let t1 = 0;
|
||||
let t2 = 0;
|
||||
if (tempComponent) {
|
||||
tempComponent.status.measurement.forEach(um => {
|
||||
let measurement = UnitMeasurement.any(um, user);
|
||||
if (
|
||||
measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE &&
|
||||
measurement.values.length > 0
|
||||
) {
|
||||
let nodeVals = measurement.values[0].values;
|
||||
if (nodeVals.length > 1) {
|
||||
t1 = nodeVals[0]; //uses the first node
|
||||
t2 = nodeVals[1]; //uses second node node
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setLastTemps({ t1, t2 });
|
||||
}, [componentOptions, tempKey, user]);
|
||||
|
||||
useEffect(() => {
|
||||
let ambientComponent = componentOptions.get(ambientKey);
|
||||
let temp = 0;
|
||||
if (ambientComponent) {
|
||||
ambientComponent.status.measurement.forEach(um => {
|
||||
let measurement = UnitMeasurement.any(um, user);
|
||||
if (measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
|
||||
if (measurement.values.length > 0 && measurement.values[0].values.length > 0) {
|
||||
temp = measurement.values[0].values[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setLastAmbient(temp);
|
||||
}, [componentOptions, ambientKey, user]);
|
||||
|
||||
useEffect(() => {
|
||||
let pressureComponent = componentOptions.get(pressureKey);
|
||||
let p1 = 0;
|
||||
let p2 = 0;
|
||||
if (pressureComponent) {
|
||||
pressureComponent.status.measurement.forEach(um => {
|
||||
let measurement = UnitMeasurement.any(um, user);
|
||||
if (
|
||||
measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE &&
|
||||
measurement.values.length > 0
|
||||
) {
|
||||
let nodeVals = measurement.values[0].values;
|
||||
if (nodeVals.length > 1) {
|
||||
p1 = nodeVals[0];
|
||||
p2 = nodeVals[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setLastPressures({ p1, p2 });
|
||||
}, [componentOptions, pressureKey, user]);
|
||||
|
||||
const ambientSelector = () => {
|
||||
return (
|
||||
<Select
|
||||
id="ambientSelect"
|
||||
value={ambientKey}
|
||||
style={{ fontSize: 8 }}
|
||||
onChange={e => {
|
||||
var key = e.target.value as string;
|
||||
gateComponentUpdate(key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
|
||||
setAmbientKey(key);
|
||||
if (tempKey === key) {
|
||||
setTempKey("");
|
||||
}
|
||||
if (pressureKey === key) {
|
||||
setPressureKey("");
|
||||
}
|
||||
}}
|
||||
displayEmpty>
|
||||
<MenuItem value="">
|
||||
<em>Select Ambient Sensor</em>
|
||||
</MenuItem>
|
||||
{Array.from(componentOptions.values()).map(c => {
|
||||
return (
|
||||
<MenuItem key={c.key()} value={c.key()}>
|
||||
{c.name()}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const tempSelector = () => {
|
||||
return (
|
||||
<Select
|
||||
id="tempSelect"
|
||||
value={tempKey}
|
||||
style={{ fontSize: 8 }}
|
||||
onChange={e => {
|
||||
gateComponentUpdate(
|
||||
e.target.value as string,
|
||||
pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP
|
||||
);
|
||||
setTempKey(e.target.value as string);
|
||||
if (pressureKey === e.target.value) {
|
||||
setPressureKey("");
|
||||
}
|
||||
if (ambientKey === e.target.value) {
|
||||
setAmbientKey("");
|
||||
}
|
||||
}}
|
||||
displayEmpty>
|
||||
<MenuItem value="">
|
||||
<em>Select Temperature Chain</em>
|
||||
</MenuItem>
|
||||
{Array.from(componentOptions.values()).map(c => {
|
||||
return (
|
||||
<MenuItem key={c.key()} value={c.key()}>
|
||||
{c.name()}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const pressureSelector = () => {
|
||||
return (
|
||||
<Select
|
||||
id="pressureSelect"
|
||||
value={pressureKey}
|
||||
style={{ fontSize: 8 }}
|
||||
onChange={e => {
|
||||
gateComponentUpdate(
|
||||
e.target.value as string,
|
||||
pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
|
||||
);
|
||||
setPressureKey(e.target.value as string);
|
||||
if (tempKey === e.target.value) {
|
||||
setTempKey("");
|
||||
}
|
||||
if (ambientKey === e.target.value) {
|
||||
setAmbientKey("");
|
||||
}
|
||||
}}
|
||||
displayEmpty>
|
||||
<MenuItem value="">
|
||||
<em>Select Pressure Chain</em>
|
||||
</MenuItem>
|
||||
{Array.from(componentOptions.values()).map(c => {
|
||||
return (
|
||||
<MenuItem key={c.key()} value={c.key()}>
|
||||
{c.name()}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
//alignItems="center"
|
||||
>
|
||||
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 8}} style={{ padding: 10 }}>
|
||||
<Box
|
||||
paddingLeft={1}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
marginBottom={2}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 20 }}>{device.name()}</Typography>
|
||||
<ToggleButtonGroup value={detail} exclusive size="small" aria-label="detail">
|
||||
<ToggleButton
|
||||
onClick={() => setDetail("analytics")}
|
||||
value={"analytics"}
|
||||
aria-label="Analysis Graphs">
|
||||
Analysis
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
onClick={() => setDetail("sensors")}
|
||||
value={"sensors"}
|
||||
aria-label="Sensor Graphs">
|
||||
Sensors
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
<Card raised>
|
||||
<GateSVG
|
||||
finalTemp={
|
||||
gate.gateMutations[device.id().toString()] &&
|
||||
!isNaN(gate.gateMutations[device.id().toString()].temp)
|
||||
? gate.gateMutations[device.id().toString()].temp
|
||||
: 0
|
||||
}
|
||||
ambientTemp={lastAmbient}
|
||||
innerTemps={lastTemps}
|
||||
innerPressures={lastPressures}
|
||||
ambientSelector={ambientSelector}
|
||||
tempChainSelector={tempSelector}
|
||||
pressureChainSelector={pressureSelector}
|
||||
pcaState={pcaState}
|
||||
pcaFanState={pcaFanOn}
|
||||
checkInTime={device.status.lastActive}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
setInteractionDialog(true);
|
||||
}}>
|
||||
Set Interactions
|
||||
</Button>
|
||||
</Card>
|
||||
<GateDeviceInteraction
|
||||
open={interactionDialog}
|
||||
gate={gate}
|
||||
compDevice={comprehensiveDevice}
|
||||
densityTemp={densityTemp}
|
||||
close={canceled => {
|
||||
setInteractionDialog(false);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 8}} style={{ padding: 10 }}>
|
||||
<GateGraphs
|
||||
gate={gate}
|
||||
display={detail}
|
||||
compMap={componentOptions}
|
||||
setPCAState={(state: boolean) => {
|
||||
setPCAState(state);
|
||||
}}
|
||||
ambient={ambientKey}
|
||||
pressure={pressureKey}
|
||||
device={device.id()}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
255
src/gate/GateDeviceInteraction.tsx
Normal file
255
src/gate/GateDeviceInteraction.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { Component, Device } from "models";
|
||||
import { Gate } from "models/Gate";
|
||||
import moment from "moment";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
close: (canceled: boolean) => void;
|
||||
gate: Gate;
|
||||
compDevice: pond.ComprehensiveDevice;
|
||||
densityTemp: number;
|
||||
}
|
||||
|
||||
//map that the temp is the key and the air density is the value
|
||||
const densityMap = new Map<number, number>([
|
||||
[35, 1.15],
|
||||
[30, 1.16],
|
||||
[25, 1.18],
|
||||
[20, 1.2],
|
||||
[15, 1.23],
|
||||
[10, 1.25],
|
||||
[5, 1.27],
|
||||
[0, 1.29],
|
||||
[-5, 1.32],
|
||||
[-10, 1.34],
|
||||
[-15, 1.37],
|
||||
[-20, 1.39],
|
||||
[-25, 1.42]
|
||||
]);
|
||||
|
||||
//this entire component will only be used for manual setting of each device on the gate
|
||||
//once we have numbers for presets it may never be used again
|
||||
export default function GateDeviceInteraction(props: Props) {
|
||||
const { open, close, gate, compDevice, densityTemp } = props;
|
||||
const [{ user }] = useGlobalState();
|
||||
const [lowDelta, setLowDelta] = useState(0);
|
||||
const [highDelta, setHighDelta] = useState(0);
|
||||
const [greenComponent, setGreenComponent] = useState<Component>();
|
||||
const [redComponent, setRedComponent] = useState<Component>();
|
||||
const [pressureComponent, setPressureComponent] = useState<Component>();
|
||||
const interactionsAPI = useInteractionsAPI();
|
||||
const [adding, setAdding] = useState(false);
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
//math to determine what the delta pressures to set will be
|
||||
let celciusTemp = densityTemp;
|
||||
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
celciusTemp = densityTemp * 1.8 + 32;
|
||||
}
|
||||
let roundedTemp = Math.round(celciusTemp / 5) * 5;
|
||||
let airDensity = densityMap.get(roundedTemp);
|
||||
//have default values if the temp is outside the map
|
||||
if (roundedTemp > 35 && airDensity === undefined) airDensity = 1.1;
|
||||
if (roundedTemp < -25 && airDensity === undefined) airDensity = 1.5;
|
||||
|
||||
//calculate the c value for the equation
|
||||
if (airDensity !== undefined) {
|
||||
let diameterM = gate.ductDiameter() / 1000;
|
||||
let pieRadSquare = 3.14 * Math.pow(diameterM / 2, 2);
|
||||
let c = 0.98 * pieRadSquare * Math.sqrt(2 * airDensity);
|
||||
let qmHigh = gate.upperFlow();
|
||||
let qmLow = gate.lowerFlow();
|
||||
|
||||
setLowDelta(Math.round(Math.pow(qmLow / c, 2)));
|
||||
setHighDelta(Math.round(Math.pow(qmHigh / c, 2)));
|
||||
}
|
||||
|
||||
//addresses of controller LEDs on a V1 device
|
||||
let redAddr = "3-1-512";
|
||||
let greenAddr = "3-1-1024";
|
||||
if (compDevice.device) {
|
||||
let dev = Device.create(compDevice.device);
|
||||
if (dev.settings.product === pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2) {
|
||||
redAddr = "3-1-256";
|
||||
greenAddr = "3-1-512";
|
||||
}
|
||||
}
|
||||
|
||||
//need to find if they have LED's in both of the controller positions
|
||||
compDevice.components.forEach(comp => {
|
||||
let component = Component.any(comp);
|
||||
//checks the address for the LED components to get the red and green LED's
|
||||
|
||||
if (component.locationString() === redAddr && component.subType() === 1) {
|
||||
setRedComponent(component);
|
||||
} else if (component.locationString() === greenAddr && component.subType() === 1) {
|
||||
setGreenComponent(component);
|
||||
} else if (component.type() === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE) {
|
||||
if (
|
||||
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
|
||||
) {
|
||||
setPressureComponent(component);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [gate, densityTemp, user, compDevice]);
|
||||
|
||||
// useEffect(() => {
|
||||
// //load current interactions for the device
|
||||
// let deviceID = Device.any(compDevice.device).id();
|
||||
// interactionsAPI.listInteractionsByDevice(deviceID).then(resp => {
|
||||
// setCurrentInteractions(resp);
|
||||
// });
|
||||
// }, [compDevice.device, interactionsAPI]);
|
||||
|
||||
// const removeCurrentInteractions = () => {
|
||||
// let deviceID = Device.any(compDevice.device).id();
|
||||
// currentInteractions.forEach(interaction => {
|
||||
// interactionsAPI.removeInteraction(deviceID, interaction.key());
|
||||
// });
|
||||
// };
|
||||
|
||||
const createInteractions = async () => {
|
||||
//the interactions to be made
|
||||
|
||||
//TOGGLE green ON when pressure within range of upper and lower
|
||||
if (
|
||||
greenComponent !== undefined &&
|
||||
redComponent !== undefined &&
|
||||
pressureComponent !== undefined
|
||||
) {
|
||||
let greenToggle: pond.InteractionSettings = pond.InteractionSettings.create({
|
||||
sink: quack.ComponentID.create({
|
||||
type: greenComponent.type(),
|
||||
address: greenComponent.settings.address,
|
||||
addressType: greenComponent.settings.addressType
|
||||
}),
|
||||
source: quack.ComponentID.create({
|
||||
type: pressureComponent.type(),
|
||||
address: pressureComponent.settings.address,
|
||||
addressType: pressureComponent.settings.addressType
|
||||
}),
|
||||
conditions: [
|
||||
pond.InteractionCondition.create({
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||
value: -highDelta,
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||
}),
|
||||
pond.InteractionCondition.create({
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: -lowDelta,
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||
})
|
||||
],
|
||||
nodeOne: 2,
|
||||
nodeTwo: 1,
|
||||
subtype: 18,
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00"
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 1
|
||||
})
|
||||
});
|
||||
|
||||
//TOGGLE red OFF when pressure within range of upper and lower
|
||||
let redToggle: pond.InteractionSettings = pond.InteractionSettings.create({
|
||||
sink: quack.ComponentID.create({
|
||||
type: redComponent.type(),
|
||||
address: redComponent.settings.address,
|
||||
addressType: redComponent.settings.addressType
|
||||
}),
|
||||
source: quack.ComponentID.create({
|
||||
type: pressureComponent.type(),
|
||||
address: pressureComponent.settings.address,
|
||||
addressType: pressureComponent.settings.addressType
|
||||
}),
|
||||
conditions: [
|
||||
pond.InteractionCondition.create({
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||
value: -highDelta,
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||
}),
|
||||
pond.InteractionCondition.create({
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: -lowDelta,
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||
})
|
||||
],
|
||||
nodeOne: 2,
|
||||
nodeTwo: 1,
|
||||
subtype: 18,
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00"
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 0
|
||||
})
|
||||
});
|
||||
|
||||
let deviceID = Device.any(compDevice.device).id();
|
||||
if (deviceID !== undefined) {
|
||||
let multi = pond.MultiInteractionSettings.create({
|
||||
interactions: [greenToggle, redToggle]
|
||||
});
|
||||
interactionsAPI
|
||||
.addMultiInteractions(deviceID, multi)
|
||||
.then(resp => {
|
||||
openSnack("Interactions added");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("There was a problem adding interactions to the device");
|
||||
})
|
||||
.finally(() => {
|
||||
setAdding(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
close(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
close(true);
|
||||
}}>
|
||||
<DialogTitle>Set Interaction For Light Toggle</DialogTitle>
|
||||
<DialogContent>
|
||||
Your Delta Pressures, in pascals, will be:
|
||||
<Typography>low = {lowDelta}</Typography>
|
||||
<Typography>high = {highDelta}</Typography>
|
||||
<Typography>{greenComponent ? "" : "Green LED Component not found"}</Typography>
|
||||
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
|
||||
//removeCurrentInteractions();
|
||||
setAdding(true);
|
||||
createInteractions();
|
||||
}}
|
||||
disabled={greenComponent === undefined || redComponent === undefined || adding}>
|
||||
{adding ? "Adding Interaction" : "Create Interaction"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
437
src/gate/GateFlowGraph.tsx
Normal file
437
src/gate/GateFlowGraph.tsx
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CircularProgress,
|
||||
Grid2 as Grid,
|
||||
LinearProgress,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { teal } from "@mui/material/colors";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart";
|
||||
import { Gate } from "models/Gate";
|
||||
import moment, { Moment } from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGateAPI } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
device: string | number;
|
||||
start: Moment;
|
||||
end: Moment;
|
||||
ambient?: string;
|
||||
newXDomain?: number[] | string[];
|
||||
pressureComponent?: string;
|
||||
setPCAState: (state: boolean) => void;
|
||||
multiGraphZoom?: (domain: number[] | string[]) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
calcCard: {
|
||||
height: 100,
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
},
|
||||
eventCard: {
|
||||
height: 100,
|
||||
paddingX: 10,
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
},
|
||||
runtimeGrid: {
|
||||
marginBottom: 2
|
||||
},
|
||||
eventGrid: {
|
||||
marginTop: 2
|
||||
}
|
||||
}));
|
||||
|
||||
export default function GateFlowGraph(props: Props) {
|
||||
const {
|
||||
gate,
|
||||
device,
|
||||
ambient,
|
||||
pressureComponent,
|
||||
setPCAState,
|
||||
start,
|
||||
end,
|
||||
newXDomain,
|
||||
multiGraphZoom
|
||||
} = props;
|
||||
const gateAPI = useGateAPI();
|
||||
const [flowData, setFlowData] = useState<SSAreaDataPoint[]>([]);
|
||||
const [loadingChartData, setLoadingChartData] = useState(false);
|
||||
const [recent, setRecent] = useState<SSAreaDataPoint | undefined>();
|
||||
const [runtime, setRuntime] = useState<moment.Duration>();
|
||||
const classes = useStyles();
|
||||
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
|
||||
const [eventsLoading, setEventsLoading] = useState(false);
|
||||
const eventThreshold = 5;
|
||||
const idleFlow = 2.44;
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingChartData) return;
|
||||
if (ambient && pressureComponent) {
|
||||
let recent: SSAreaDataPoint | undefined;
|
||||
setLoadingChartData(true);
|
||||
gateAPI
|
||||
.listGateAirflow(
|
||||
gate.key,
|
||||
device,
|
||||
ambient,
|
||||
pressureComponent,
|
||||
start.toISOString(),
|
||||
end.toISOString()
|
||||
)
|
||||
.then(resp => {
|
||||
let data: SSAreaDataPoint[] = [];
|
||||
if (resp.data.values) {
|
||||
let start: Moment | undefined;
|
||||
let stop: Moment | undefined;
|
||||
let runtime = 0;
|
||||
resp.data.values.forEach((val, i) => {
|
||||
let time = moment(val.time);
|
||||
let newPoint: SSAreaDataPoint = {
|
||||
timestamp: time.valueOf(),
|
||||
value: val.airFlow ?? 0
|
||||
};
|
||||
|
||||
data.push(newPoint);
|
||||
if (!recent || recent.timestamp < newPoint.timestamp) {
|
||||
recent = newPoint;
|
||||
}
|
||||
|
||||
/** determine runtime */
|
||||
// set the start time if the values is greater than the idleFlow and start is not already set
|
||||
if (val.airFlow >= idleFlow && !start) {
|
||||
start = time;
|
||||
}
|
||||
// set the stop time when off or at the last measurements and the start time is set
|
||||
if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) {
|
||||
stop = time;
|
||||
}
|
||||
// if both start and stop are set calculate add the timeframe to the total runtime
|
||||
if (start && stop) {
|
||||
runtime = runtime + stop.diff(start);
|
||||
start = undefined;
|
||||
stop = undefined;
|
||||
}
|
||||
});
|
||||
setRuntime(moment.duration(runtime));
|
||||
let state = false;
|
||||
if (
|
||||
data[data.length - 1].value > gate.lowerFlow() &&
|
||||
data[data.length - 1].value < gate.upperFlow()
|
||||
) {
|
||||
state = true;
|
||||
}
|
||||
setPCAState(state);
|
||||
}
|
||||
setFlowData(data);
|
||||
setLoadingChartData(false);
|
||||
setRecent(recent);
|
||||
});
|
||||
}
|
||||
}, [gateAPI, gate, ambient, pressureComponent, start, end, device, setPCAState]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadFlowEvents = () => {
|
||||
if (ambient && pressureComponent) {
|
||||
setEventsLoading(true);
|
||||
gateAPI
|
||||
.listGateFlowEvents(
|
||||
gate.key,
|
||||
device,
|
||||
ambient,
|
||||
pressureComponent,
|
||||
start.toISOString(),
|
||||
end.toISOString(),
|
||||
idleFlow,
|
||||
eventThreshold
|
||||
)
|
||||
.then(resp => {
|
||||
console.log(resp);
|
||||
setFlowEvents(resp.data.events.map(e => pond.AirFlowEvent.fromObject(e)));
|
||||
})
|
||||
.catch(err => {})
|
||||
.finally(() => {
|
||||
setEventsLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const flowChart = () => {
|
||||
return (
|
||||
<Card raised style={{ padding: 10, marginBottom: 15 }}>
|
||||
<CardHeader
|
||||
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Mass Air Flow</Typography>}
|
||||
subheader={
|
||||
recent ? (
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
<span>
|
||||
{"Mass Flow Rate: "}
|
||||
<span style={{ color: teal[500], fontWeight: 500 }}>
|
||||
{recent.value.toFixed(2)} kg/s
|
||||
</span>
|
||||
<br />
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography color="textSecondary" variant={"caption"}>
|
||||
{moment(recent.timestamp).fromNow()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
No Data
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{flowData.length !== 0 ? (
|
||||
<SingleSetAreaChart
|
||||
data={flowData}
|
||||
maxRef={gate.upperFlow()}
|
||||
minRef={gate.lowerFlow()}
|
||||
newXDomain={newXDomain}
|
||||
multiGraphZoom={multiGraphZoom}
|
||||
/>
|
||||
) : (
|
||||
<Box display="flex" flexDirection="column" flexGrow="2">
|
||||
<div style={{ display: "flex", flexGrow: 2 }}></div>
|
||||
<Typography variant="subtitle1" color="textSecondary" align="center">
|
||||
A component may be missing or have no measurements, this data needs the ambient and
|
||||
pressure components to be set and measuring
|
||||
</Typography>
|
||||
<div style={{ display: "flex", flexGrow: 2 }}></div>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const eventCards = () => {
|
||||
let totalEvents = 0;
|
||||
let eventsInside = 0;
|
||||
let eventsOutside = 0;
|
||||
let totalTimeS = 0;
|
||||
let timeSInside = 0;
|
||||
let timeSOutside = 0;
|
||||
|
||||
flowEvents.forEach(event => {
|
||||
totalEvents++;
|
||||
totalTimeS =
|
||||
totalTimeS + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
|
||||
|
||||
let avg = 0;
|
||||
let count = 0;
|
||||
event.readings.forEach(reading => {
|
||||
avg = avg + reading.airFlow;
|
||||
count++;
|
||||
});
|
||||
avg = avg / count;
|
||||
//if the average of the readings for an event are within the range
|
||||
if (avg < gate.upperFlow() && avg > gate.lowerFlow()) {
|
||||
eventsInside++;
|
||||
timeSInside =
|
||||
timeSInside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
|
||||
} else {
|
||||
eventsOutside++;
|
||||
timeSOutside =
|
||||
timeSOutside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{flowEvents.length > 0 ? (
|
||||
<Grid container direction="row" spacing={2} className={classes.eventGrid}>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.eventCard}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Total Events:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{totalEvents}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Total Time:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{moment.duration(totalTimeS, "s").humanize()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.eventCard}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Events Inside:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{eventsInside}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Event Time:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{moment.duration(timeSInside, "s").humanize()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.eventCard}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Events Outside:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{eventsOutside}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={9}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography>Event Time:</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{moment.duration(timeSOutside, "s").humanize()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.eventCard}>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid>
|
||||
<Typography>Performance:</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{(eventsInside / totalEvents) * 100}%
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Box>
|
||||
{eventsLoading ? (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<CircularProgress size={100} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
loadFlowEvents();
|
||||
}}>
|
||||
Retrieve Flow Events
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{runtime && (
|
||||
<Grid container direction="row" spacing={2} className={classes.runtimeGrid}>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.calcCard}>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid>Approximate Runtime:</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{runtime.asHours().toFixed(2)} hr
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.calcCard}>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid>Cost to run PCA:</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
$
|
||||
{parseFloat(
|
||||
(runtime.asHours() * gate.settings.hourlyPcaCost).toFixed(2)
|
||||
).toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.calcCard}>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid>Cost to run APU:</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
$
|
||||
{parseFloat(
|
||||
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
|
||||
).toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card raised className={classes.calcCard}>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid>Total Cost:</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
$
|
||||
{parseFloat(
|
||||
(
|
||||
runtime.asHours() * gate.settings.hourlyApuCost +
|
||||
runtime.asHours() * gate.settings.hourlyPcaCost
|
||||
).toFixed(2)
|
||||
).toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid>Estimated Savings:</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
$
|
||||
{parseFloat(
|
||||
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
|
||||
).toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{loadingChartData ? <LinearProgress /> : flowChart()}
|
||||
{eventCards()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
357
src/gate/GateGraphs.tsx
Normal file
357
src/gate/GateGraphs.tsx
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
LinearProgress,
|
||||
Theme,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { ZoomOut } from "@mui/icons-material";
|
||||
import AreaGraph, { AreaData } from "charts/measurementCharts/AreaGraph";
|
||||
import MultiLineGraph, { LineData, Point } from "charts/measurementCharts/MultiLineGraph";
|
||||
import { GetDefaultDateRange } from "common/time/DateRange";
|
||||
import TimeBar from "common/time/TimeBar";
|
||||
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
|
||||
import { useThemeType } from "hooks";
|
||||
import { Component } from "models";
|
||||
import { Gate } from "models/Gate";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import moment, { Moment } from "moment";
|
||||
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
||||
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGateAPI, useGlobalState } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { avg } from "utils";
|
||||
import GateFlowGraph from "./GateFlowGraph";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
display: "analytics" | "sensors";
|
||||
compMap: Map<string, Component>;
|
||||
device: string | number;
|
||||
pressure: string;
|
||||
ambient: string;
|
||||
setPCAState: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
card: {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
overflow: "visible"
|
||||
},
|
||||
cardHeader: {
|
||||
padding: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
marginRight: 10
|
||||
},
|
||||
avatarIcon: {
|
||||
width: 33,
|
||||
height: 33
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default function GateGraphs(props: Props) {
|
||||
const { gate, display, compMap, device, pressure, ambient, setPCAState } = props;
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [zoomed, setZoomed] = useState(false);
|
||||
const defaultDateRange = GetDefaultDateRange();
|
||||
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
|
||||
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
|
||||
const themeType = useThemeType();
|
||||
const classes = useStyles();
|
||||
const [{ user }] = useGlobalState();
|
||||
const [compMeasurements, setCompMeasurements] = useState<Map<string, UnitMeasurement[]>>(
|
||||
new Map<string, UnitMeasurement[]>()
|
||||
);
|
||||
const gateAPI = useGateAPI();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
let measurementMap: Map<string, UnitMeasurement[]> = new Map<string, UnitMeasurement[]>();
|
||||
gateAPI
|
||||
.listGateMeasurements(gate.key, startDate.toISOString(), endDate.toISOString())
|
||||
.then(resp => {
|
||||
resp.data.measurements.forEach(um => {
|
||||
let unitMeasurement = UnitMeasurement.any(um, user);
|
||||
let entry = measurementMap.get(unitMeasurement.componentId);
|
||||
if (entry) {
|
||||
entry.push(unitMeasurement);
|
||||
} else {
|
||||
measurementMap.set(unitMeasurement.componentId, [unitMeasurement]);
|
||||
}
|
||||
});
|
||||
setLoading(false);
|
||||
setCompMeasurements(measurementMap);
|
||||
});
|
||||
}, [gate, endDate, gateAPI, startDate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const updateDateRange = (newStartDate: any, newEndDate: any) => {
|
||||
let range = GetDefaultDateRange();
|
||||
range.start = newStartDate;
|
||||
range.end = newEndDate;
|
||||
setStartDate(newStartDate);
|
||||
setEndDate(newEndDate);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
setZoomed(false);
|
||||
};
|
||||
|
||||
const lineGraph = (
|
||||
unitMeasurement: UnitMeasurement,
|
||||
describer: MeasurementDescriber,
|
||||
averages?: number
|
||||
) => {
|
||||
if (unitMeasurement.values.length > 0) {
|
||||
let firstTime: Moment | undefined;
|
||||
let lastTime: Moment | undefined;
|
||||
//build line data to pass to graph
|
||||
let lineData: LineData[] = [];
|
||||
let averagedData: LineData[] = [];
|
||||
let valMap: Map<number, number[]> = new Map<number, number[]>();
|
||||
|
||||
unitMeasurement.values.forEach((vals, i) => {
|
||||
let avgVals: Point[] = [];
|
||||
let newLineData: LineData = {
|
||||
timestamp: moment(unitMeasurement.timestamps[i]).valueOf(),
|
||||
points: vals.values.map((val, i) => ({ value: val, node: i }))
|
||||
};
|
||||
lineData.push(newLineData);
|
||||
if (averages) {
|
||||
vals.values.forEach((val, k) => {
|
||||
let entry = valMap.get(k);
|
||||
if (entry) {
|
||||
entry.push(val);
|
||||
if (entry.length >= averages) {
|
||||
lastTime = moment(unitMeasurement.timestamps[i]);
|
||||
avgVals.push({
|
||||
node: k,
|
||||
value: avg(entry)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
valMap.set(k, [val]);
|
||||
firstTime = moment(unitMeasurement.timestamps[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (firstTime && lastTime) {
|
||||
averagedData.push({
|
||||
points: avgVals,
|
||||
timestamp:
|
||||
Math.abs(firstTime.diff(lastTime)) / 2 +
|
||||
Math.min(firstTime.valueOf(), lastTime.valueOf())
|
||||
});
|
||||
firstTime = undefined;
|
||||
lastTime = undefined;
|
||||
valMap = new Map<number, number[]>();
|
||||
}
|
||||
});
|
||||
return (
|
||||
<MultiLineGraph
|
||||
customHeight={300}
|
||||
key={unitMeasurement.type}
|
||||
lineData={lineData}
|
||||
//averagedData={showAveraged ? averagedData : []}
|
||||
numLines={unitMeasurement.values.length > 1 ? unitMeasurement.values[0].values.length : 0}
|
||||
describer={describer}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
setXDomain(domain);
|
||||
setZoomed(true);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const areaGraph = (
|
||||
unitMeasurement: UnitMeasurement,
|
||||
describer: MeasurementDescriber,
|
||||
averages?: number
|
||||
) => {
|
||||
if (unitMeasurement.timestamps.length > 0) {
|
||||
let firstTime: Moment | undefined;
|
||||
let lastTime: Moment | undefined;
|
||||
//build line data to pass to graph
|
||||
let areaData: AreaData[] = [];
|
||||
let avgData: AreaData[] = [];
|
||||
let maxVals: number[] = [];
|
||||
let minVals: number[] = [];
|
||||
|
||||
unitMeasurement.values.forEach((val, j) => {
|
||||
let currentMax = Math.min(...val.values);
|
||||
let currentMin = Math.max(...val.values);
|
||||
let dataPoint: AreaData = {
|
||||
timestamp: moment(unitMeasurement.timestamps[j]).valueOf(),
|
||||
value: [currentMax, currentMin]
|
||||
};
|
||||
if (averages) {
|
||||
if (minVals.length === 0) {
|
||||
firstTime = moment(unitMeasurement.timestamps[j]);
|
||||
}
|
||||
maxVals.push(currentMax);
|
||||
minVals.push(currentMin);
|
||||
if (minVals.length >= averages) {
|
||||
lastTime = moment(unitMeasurement.timestamps[j]);
|
||||
}
|
||||
}
|
||||
if (firstTime && lastTime) {
|
||||
avgData.push({
|
||||
timestamp:
|
||||
Math.abs(firstTime.diff(lastTime)) / 2 +
|
||||
Math.min(firstTime.valueOf(), lastTime.valueOf()),
|
||||
value: [avg(maxVals), avg(minVals)]
|
||||
});
|
||||
firstTime = undefined;
|
||||
lastTime = undefined;
|
||||
maxVals = [];
|
||||
minVals = [];
|
||||
}
|
||||
areaData.push(dataPoint);
|
||||
});
|
||||
return (
|
||||
<AreaGraph
|
||||
key={unitMeasurement.type}
|
||||
customHeight={350}
|
||||
data={areaData}
|
||||
//averagedData={showAveraged ? avgData : []}
|
||||
describer={describer}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
setXDomain(domain);
|
||||
setZoomed(true);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const graphHeader = (comp: Component) => {
|
||||
const componentIcon = GetComponentIcon(comp.settings.type, comp.settings.subtype, themeType);
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={componentIcon}
|
||||
className={classes.avatarIcon}
|
||||
alt={comp.name()}
|
||||
/>
|
||||
}
|
||||
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>{comp.name()}</Typography>}
|
||||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={comp}
|
||||
reading={UnitMeasurement.convertLastMeasurement(
|
||||
comp.status.measurement.map(um => UnitMeasurement.create(um, user)) ?? []
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const sensorGraphs = () => {
|
||||
return (
|
||||
<Box>
|
||||
{Array.from(compMeasurements.values()).map((compMeasurement, i) => {
|
||||
let c = Component.create();
|
||||
if (compMeasurement[0]) {
|
||||
c = compMap.get(compMeasurement[0].componentId) ?? Component.create();
|
||||
}
|
||||
if (compMap.get(c.key())) {
|
||||
return (
|
||||
<Card raised key={i} style={{ padding: 10, marginBottom: 15 }}>
|
||||
{graphHeader(c)}
|
||||
{compMeasurement.map(um => {
|
||||
if (um.values[0] && um.values[0].values.length > 1) {
|
||||
return areaGraph(
|
||||
um,
|
||||
describeMeasurement(um.type, c.type(), c.subType()),
|
||||
c.settings.smoothingAverages
|
||||
);
|
||||
} else {
|
||||
return lineGraph(
|
||||
um,
|
||||
describeMeasurement(um.type, c.type(), c.subType()),
|
||||
c.settings.smoothingAverages
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
} else {
|
||||
return <Box></Box>;
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const analyticGraphs = () => {
|
||||
return (
|
||||
<Box>
|
||||
<GateFlowGraph
|
||||
newXDomain={xDomain}
|
||||
device={device}
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
gate={gate}
|
||||
ambient={ambient}
|
||||
pressureComponent={pressure}
|
||||
setPCAState={(state: boolean) => {
|
||||
setPCAState(state);
|
||||
}}
|
||||
multiGraphZoom={domain => {
|
||||
setXDomain(domain);
|
||||
setZoomed(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const showGraphs = () => {
|
||||
switch (display) {
|
||||
case "sensors":
|
||||
return sensorGraphs();
|
||||
case "analytics":
|
||||
return analyticGraphs();
|
||||
default:
|
||||
return <Box>Unknown Graph Type Selected</Box>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={2}>
|
||||
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
|
||||
{zoomed && (
|
||||
<Tooltip title="Zoom Out Graphs">
|
||||
<Button variant="outlined" onClick={zoomOut}>
|
||||
<ZoomOut />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
{loading ? <LinearProgress /> : showGraphs()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
143
src/gate/GateList.tsx
Normal file
143
src/gate/GateList.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { Gate } from "models/Gate";
|
||||
import { Box, Card, Theme } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMobile } from "hooks";
|
||||
//import MaterialTable from "material-table";
|
||||
//import { getTableIcons } from "common/ResponsiveTable";
|
||||
import { Terminal } from "models/Terminal";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
interface Props {
|
||||
gates: Gate[];
|
||||
terminals: Terminal[];
|
||||
useMobile: boolean;
|
||||
//duplicateGate: (gate: Gate) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
gridListTile: {
|
||||
minHeight: "184px",
|
||||
height: "auto !important",
|
||||
width: "184px",
|
||||
padding: 2
|
||||
},
|
||||
hidden: {
|
||||
visibility: "hidden"
|
||||
},
|
||||
gateCard: {
|
||||
marginBottom: 10,
|
||||
paddingLeft: 15
|
||||
}
|
||||
}));
|
||||
|
||||
export default function GateList(props: Props) {
|
||||
const { gates, terminals } = props;
|
||||
// const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useMobile();
|
||||
const classes = useStyles();
|
||||
const [terminalMap, setTerminalMap] = useState<Map<string, string>>(new Map<string, string>());
|
||||
|
||||
const goToGate = (gate: Gate) => {
|
||||
let path = "/terminals/" + gate.key;
|
||||
navigate(path, { state: gate })
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let map = new Map<string, string>();
|
||||
terminals.forEach(t => {
|
||||
map.set(t.key, t.name);
|
||||
});
|
||||
setTerminalMap(map);
|
||||
}, [terminals]);
|
||||
|
||||
const desktopView = () => {
|
||||
return (<Box>Desktop table</Box>)
|
||||
// return (
|
||||
// <MaterialTable
|
||||
// columns={[
|
||||
// {
|
||||
// title: "Gate",
|
||||
// field: "name",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// title: "Terminals",
|
||||
// field: "settings.terminal",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// },
|
||||
// render: rowData => terminalMap.get(rowData.terminal())
|
||||
// },
|
||||
// {
|
||||
// title: "Duct Type",
|
||||
// field: "settings.ductName",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// title: "Duct Size(mm)",
|
||||
// field: "settings.ductDiameter",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// title: "Duct Length(m)",
|
||||
// field: "settings.ductLength",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// title: "PCA Unit",
|
||||
// field: "settings.pcaType",
|
||||
// headerStyle: {
|
||||
// fontWeight: 650,
|
||||
// fontSize: 20
|
||||
// }
|
||||
// }
|
||||
// ]}
|
||||
// data={gates}
|
||||
// icons={getTableIcons()}
|
||||
// onRowClick={(_, gate) => {
|
||||
// gate && goToGate(gate);
|
||||
// }}
|
||||
// title={""}
|
||||
// options={{
|
||||
// pageSize: 10
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
};
|
||||
|
||||
const mobileView = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{gates.map((gate, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
className={classes.gateCard}
|
||||
onClick={() => {
|
||||
goToGate(gate);
|
||||
}}>
|
||||
<Box margin={2} fontSize={20} fontWeight={650}>
|
||||
{gate.name}
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return isMobile || props.useMobile ? mobileView() : desktopView();
|
||||
}
|
||||
374
src/gate/GateSVG.tsx
Normal file
374
src/gate/GateSVG.tsx
Normal file
File diff suppressed because one or more lines are too long
574
src/gate/GateSettings.tsx
Normal file
574
src/gate/GateSettings.tsx
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
import {
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Button,
|
||||
Grid2 as Grid,
|
||||
Box,
|
||||
InputAdornment,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Tabs,
|
||||
Tab
|
||||
} from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { Terminal } from "models/Terminal";
|
||||
import { Gate } from "models/Gate";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useTerminalAPI, useGateAPI } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSnackbar } from "hooks";
|
||||
//import { useHistory } from "react-router";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import DuctDescriber, { DuctOptions, FindDuctType } from "ducting/DuctDescriber";
|
||||
|
||||
interface DuctProps {
|
||||
diameter: number;
|
||||
length: number;
|
||||
thermalConductivity: number;
|
||||
thermalResistance: number;
|
||||
frictionFactor: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
close: (newGate?: Gate) => void;
|
||||
terminals?: Terminal[];
|
||||
gate?: Gate;
|
||||
long?: number;
|
||||
lat?: number;
|
||||
}
|
||||
|
||||
const steps = ["Gate", "Duct"];
|
||||
|
||||
export default function GateSettings(props: Props) {
|
||||
const { open, close, terminals, gate, long, lat } = props;
|
||||
const [gateName, setGateName] = useState("");
|
||||
const [lowerFlowBound, setLowerFlowBound] = useState(0);
|
||||
const [upperFlowBound, setUpperFlowBound] = useState(0);
|
||||
const [terminalOptions, setTerminalOptions] = useState(terminals);
|
||||
const [terminalKey, setTerminalKey] = useState("");
|
||||
const gateAPI = useGateAPI();
|
||||
const terminalAPI = useTerminalAPI();
|
||||
const [loadingTerminals, setLoadingTerminals] = useState(false);
|
||||
const [removeDialog, setRemoveDialog] = useState(false);
|
||||
const { openSnack } = useSnackbar();
|
||||
//const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const ductOptions = DuctOptions();
|
||||
const [ductType, setDuctType] = useState(0);
|
||||
const [ductName, setDuctName] = useState("");
|
||||
const [ductProps, setDuctProps] = useState<DuctProps>({
|
||||
diameter: 0,
|
||||
length: 0,
|
||||
frictionFactor: 0,
|
||||
thermalConductivity: 0,
|
||||
thermalResistance: 0
|
||||
});
|
||||
const [pcaUnit, setPcaUnit] = useState("");
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
//user set identifier to be shown on the marker on the map
|
||||
const [terminalIdentifier, setTerminalIdentifier] = useState("");
|
||||
const [gateIdentifier, setGateIdentifier] = useState("");
|
||||
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
|
||||
const [hourlyAPU, setHourlyAPU] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminals) {
|
||||
if (loadingTerminals) return;
|
||||
setLoadingTerminals(true);
|
||||
terminalAPI
|
||||
.listTerminals(500, 0)
|
||||
.then(resp => {
|
||||
setTerminalOptions(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
})
|
||||
.catch(err => {})
|
||||
.finally(() => {
|
||||
setLoadingTerminals(false);
|
||||
});
|
||||
} else {
|
||||
setTerminalOptions(terminals);
|
||||
}
|
||||
//eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [terminals, terminalAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gate) {
|
||||
setGateName(gate.name);
|
||||
setTerminalKey(gate.terminal());
|
||||
setDuctProps({
|
||||
diameter: gate.settings.ductDiameter,
|
||||
length: gate.settings.ductLength,
|
||||
frictionFactor: gate.settings.frictionFactor,
|
||||
thermalConductivity: gate.settings.thermalConductivity,
|
||||
thermalResistance: gate.settings.thermalResistance
|
||||
});
|
||||
let currentType = FindDuctType(
|
||||
gate.settings.thermalResistance,
|
||||
gate.settings.thermalConductivity,
|
||||
gate.settings.frictionFactor
|
||||
);
|
||||
setDuctType(currentType);
|
||||
setLowerFlowBound(gate.lowerFlow());
|
||||
setUpperFlowBound(gate.upperFlow());
|
||||
setPcaUnit(gate.settings.pcaType);
|
||||
setDuctName(gate.settings.ductName);
|
||||
setTerminalIdentifier(gate.settings.letterIdentifier ?? "");
|
||||
setGateIdentifier(gate.settings.numberIdentifier ?? "");
|
||||
setHourlyAPU(gate.settings.hourlyApuCost ?? 0);
|
||||
setHourlyPCA(gate.settings.hourlyPcaCost ?? 0);
|
||||
} else {
|
||||
setGateName("");
|
||||
setTerminalKey("");
|
||||
}
|
||||
}, [gate]);
|
||||
|
||||
const confirm = () => {
|
||||
if (gate) {
|
||||
let settings = gate.settings;
|
||||
settings.terminal = terminalKey;
|
||||
settings.ductDiameter = ductProps.diameter;
|
||||
settings.ductLength = ductProps.length;
|
||||
settings.frictionFactor = ductProps.frictionFactor;
|
||||
settings.thermalConductivity = ductProps.thermalConductivity;
|
||||
settings.thermalResistance = ductProps.thermalResistance;
|
||||
settings.lowerFlow = lowerFlowBound;
|
||||
settings.upperFlow = upperFlowBound;
|
||||
settings.ductName = ductName;
|
||||
settings.pcaType = pcaUnit;
|
||||
settings.letterIdentifier = terminalIdentifier;
|
||||
settings.numberIdentifier = gateIdentifier.toString();
|
||||
settings.hourlyApuCost = hourlyAPU;
|
||||
settings.hourlyPcaCost = hourlyPCA;
|
||||
gateAPI
|
||||
.updateGate(gate.key, gateName, settings)
|
||||
.then(resp => {
|
||||
openSnack("Gate update");
|
||||
close();
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to update gate");
|
||||
});
|
||||
} else {
|
||||
let settings: pond.GateSettings = pond.GateSettings.create({
|
||||
longitude: long,
|
||||
latitude: lat,
|
||||
terminal: terminalKey,
|
||||
ductDiameter: ductProps.diameter,
|
||||
ductLength: ductProps.length,
|
||||
frictionFactor: ductProps.frictionFactor,
|
||||
thermalConductivity: ductProps.thermalConductivity,
|
||||
thermalResistance: ductProps.thermalResistance,
|
||||
lowerFlow: lowerFlowBound,
|
||||
upperFlow: upperFlowBound,
|
||||
ductName: ductName,
|
||||
pcaType: pcaUnit,
|
||||
letterIdentifier: terminalIdentifier,
|
||||
numberIdentifier: gateIdentifier.toString(),
|
||||
hourlyApuCost: hourlyAPU,
|
||||
hourlyPcaCost: hourlyPCA
|
||||
});
|
||||
gateAPI
|
||||
.addGate(gateName, settings)
|
||||
.then(resp => {
|
||||
openSnack("New gate added");
|
||||
let newGate = Gate.create(
|
||||
pond.Gate.create({ key: resp.data.key, name: gateName, settings: settings })
|
||||
);
|
||||
close(newGate);
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("There was a problem adding your gate");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
if (gate) {
|
||||
gateAPI
|
||||
.removeGate(gate.key)
|
||||
.then(resp => {
|
||||
openSnack("Gate Deleted");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to delete gate");
|
||||
})
|
||||
.finally(() => {
|
||||
navigate("/terminals");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const stepper = () => {
|
||||
if (gate === undefined) {
|
||||
return (
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map(label => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
const labelProps: {
|
||||
optional?: React.ReactNode;
|
||||
} = {};
|
||||
return (
|
||||
<Step key={label} {...stepProps}>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<Tabs
|
||||
value={activeStep}
|
||||
onChange={(_, value) => setActiveStep(value)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="fullWidth"
|
||||
aria-label="bin tabs"
|
||||
//classes={{ root: classes.tabs }}
|
||||
>
|
||||
{steps.map((step, i) => (
|
||||
<Tab key={i} label={step} aria-label={step} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const stepperContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return ductProperties();
|
||||
default:
|
||||
return gateProperties();
|
||||
}
|
||||
};
|
||||
|
||||
const gateProperties = () => {
|
||||
//build list of letters for select box
|
||||
let tiOptions: JSX.Element[] = [];
|
||||
//adds the letters to the terminal id options
|
||||
for (let l = 65; l <= 90; l++) {
|
||||
tiOptions.push(
|
||||
<MenuItem key={String.fromCharCode(l)} value={String.fromCharCode(l)}>
|
||||
{String.fromCharCode(l)}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
//add numbers 1-9 for terminal id options
|
||||
for (let n = 1; n <= 9; n++) {
|
||||
tiOptions.push(
|
||||
<MenuItem key={n} value={n}>
|
||||
{n}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
let numberOptions: JSX.Element[] = [];
|
||||
for (let n = 0; n <= 99; n++) {
|
||||
numberOptions.push(
|
||||
<MenuItem key={n} value={n}>
|
||||
{n}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label="Gate Name"
|
||||
fullWidth
|
||||
value={gateName}
|
||||
onChange={e => setGateName(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
id="terminal"
|
||||
label="Terminal"
|
||||
fullWidth
|
||||
displayEmpty
|
||||
value={terminalKey}
|
||||
onChange={e => {
|
||||
setTerminalKey(e.target.value as string);
|
||||
}}>
|
||||
<MenuItem key="new" value="">
|
||||
Select Terminal
|
||||
</MenuItem>
|
||||
{terminalOptions?.map(terminal => (
|
||||
<MenuItem key={terminal.key} value={terminal.key}>
|
||||
{terminal.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<TextField
|
||||
label="Low Mass Air Flow"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={lowerFlowBound}
|
||||
onChange={e => setLowerFlowBound(+e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="High Mass Air Flow"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={upperFlowBound}
|
||||
onChange={e => setUpperFlowBound(+e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="PCA Unit"
|
||||
fullWidth
|
||||
value={pcaUnit}
|
||||
onChange={e => setPcaUnit(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
style={{ width: "45%" }}
|
||||
select
|
||||
label="Terminal Identifier"
|
||||
value={terminalIdentifier}
|
||||
onChange={e => {
|
||||
setTerminalIdentifier(e.target.value as string);
|
||||
}}>
|
||||
<MenuItem key="new" value="">
|
||||
Gate Letter
|
||||
</MenuItem>
|
||||
{tiOptions}
|
||||
</TextField>
|
||||
<TextField
|
||||
style={{ width: "45%", marginLeft: "10%" }}
|
||||
label="Gate Identifier"
|
||||
select
|
||||
value={gateIdentifier}
|
||||
onChange={e => setGateIdentifier(e.target.value)}>
|
||||
<MenuItem key="new" value="">
|
||||
Gate Number
|
||||
</MenuItem>
|
||||
{numberOptions}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Hourly PCA Cost"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={hourlyPCA}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
|
||||
}}
|
||||
onChange={e => setHourlyPCA(+e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Hourly APU Cost"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={hourlyAPU}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
|
||||
}}
|
||||
onChange={e => setHourlyAPU(+e.target.value)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ductProperties = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
id="ductType"
|
||||
label="Duct Type"
|
||||
fullWidth
|
||||
value={ductType}
|
||||
select
|
||||
onChange={e => {
|
||||
let type = +e.target.value;
|
||||
setDuctType(type);
|
||||
setDuctName("custom");
|
||||
if (type > 1) {
|
||||
let describer = DuctDescriber(type);
|
||||
setDuctName(describer.name);
|
||||
setDuctProps({
|
||||
diameter: ductProps.diameter,
|
||||
frictionFactor: describer.frictionFactor,
|
||||
length: ductProps.length,
|
||||
thermalConductivity: describer.thermalConductivity,
|
||||
thermalResistance: describer.thermalResistance
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<MenuItem key="new" value={pond.DuctType.DUCT_TYPE_INVALID}>
|
||||
Select Ducting Type
|
||||
</MenuItem>
|
||||
{ductOptions.map(duct => (
|
||||
<MenuItem key={duct.value} value={duct.value}>
|
||||
{duct.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem key="custom" value={pond.DuctType.DUCT_TYPE_NONE}>
|
||||
Custom
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
margin="normal"
|
||||
id="ductDiameter"
|
||||
label="Duct Diameter(mm)"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={isNaN(ductProps.diameter) ? "" : ductProps.diameter}
|
||||
error={isNaN(+ductProps.diameter)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">mm</InputAdornment>
|
||||
}}
|
||||
onChange={e => {
|
||||
setDuctProps({ ...ductProps, diameter: parseFloat(e.target.value) });
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
id="ductLength"
|
||||
label="Duct Length(m)"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={isNaN(ductProps.length) ? "" : ductProps.length}
|
||||
error={isNaN(+ductProps.length)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">m</InputAdornment>
|
||||
}}
|
||||
onChange={e => {
|
||||
setDuctProps({ ...ductProps, length: parseFloat(e.target.value) });
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
id="thermalCond"
|
||||
label="Thermal Conductivity"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={isNaN(ductProps.thermalConductivity) ? "" : ductProps.thermalConductivity}
|
||||
error={isNaN(+ductProps.thermalConductivity)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">W/mK</InputAdornment>
|
||||
}}
|
||||
onChange={e => {
|
||||
setDuctProps({ ...ductProps, thermalConductivity: parseFloat(e.target.value) });
|
||||
setDuctType(1);
|
||||
setDuctName("custom");
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
id="friction"
|
||||
label="Friction Factor"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={isNaN(ductProps.frictionFactor) ? "" : ductProps.frictionFactor}
|
||||
error={isNaN(+ductProps.frictionFactor)}
|
||||
onChange={e => {
|
||||
setDuctProps({ ...ductProps, frictionFactor: parseFloat(e.target.value) });
|
||||
setDuctType(1);
|
||||
setDuctName("custom");
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
id="thermalRes"
|
||||
label="Thermal Resistance"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={isNaN(ductProps.thermalResistance) ? "" : ductProps.thermalResistance}
|
||||
error={isNaN(+ductProps.thermalResistance)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">K/W</InputAdornment>
|
||||
}}
|
||||
onChange={e => {
|
||||
setDuctProps({ ...ductProps, thermalResistance: parseFloat(e.target.value) });
|
||||
setDuctType(1);
|
||||
setDuctName("custom");
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const removeConfirmation = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={removeDialog} onClose={() => setRemoveDialog(false)}>
|
||||
<DialogTitle>Delete Gate</DialogTitle>
|
||||
<DialogContent>Are you sure you wish to delete this gate?</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setRemoveDialog(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={remove}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={() => close()}>
|
||||
{removeConfirmation()}
|
||||
<DialogTitle></DialogTitle>
|
||||
<DialogContent>
|
||||
{stepper()}
|
||||
{stepperContent(activeStep)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container direction="row">
|
||||
<Grid>
|
||||
{gate && (
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: "red"
|
||||
}}
|
||||
onClick={() => {
|
||||
setRemoveDialog(true);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
{(gate || activeStep === 0) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
close();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{!gate && activeStep > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveStep(activeStep - 1);
|
||||
}}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{(gate || activeStep === 1) && (
|
||||
<Button variant="contained" color="primary" onClick={confirm}>
|
||||
Confirm
|
||||
</Button>
|
||||
)}
|
||||
{!gate && activeStep === 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setActiveStep(activeStep + 1);
|
||||
}}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue