imported the gate and terminal related files to prep for the aviation map

This commit is contained in:
csawatzky 2025-03-28 15:54:11 -06:00
parent e2e061151b
commit 505cb8e3aa
25 changed files with 4868 additions and 9 deletions

57
src/gate/AddGateFab.tsx Normal file
View 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
View 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
View 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>
);
}

View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

574
src/gate/GateSettings.tsx Normal file
View 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>
);
}