diff --git a/package-lock.json b/package-lock.json index b200d4640..a594f583d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5215,20 +5215,39 @@ } }, "airbnb-prop-types": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz", - "integrity": "sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", + "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", "requires": { - "array.prototype.find": "^2.1.0", - "function.prototype.name": "^1.1.1", - "has": "^1.0.3", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", + "array.prototype.find": "^2.1.1", + "function.prototype.name": "^1.1.2", + "is-regex": "^1.1.0", + "object-is": "^1.1.2", "object.assign": "^4.1.0", - "object.entries": "^1.1.0", + "object.entries": "^1.1.2", "prop-types": "^15.7.2", "prop-types-exact": "^1.2.0", - "react-is": "^16.9.0" + "react-is": "^16.13.1" + }, + "dependencies": { + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + } } }, "ajv": { @@ -9539,27 +9558,38 @@ } }, "enzyme-adapter-react-16": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz", - "integrity": "sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.4.tgz", + "integrity": "sha512-wPzxs+JaGDK2TPYzl5a9YWGce6i2SQ3Cg51ScLeyj2WotUZ8Obcq1ke/U1Y2VGpYlb9rrX2yCjzSMgtKCeAt5w==", "requires": { - "enzyme-adapter-utils": "^1.13.0", - "enzyme-shallow-equal": "^1.0.1", + "enzyme-adapter-utils": "^1.13.1", + "enzyme-shallow-equal": "^1.0.4", "has": "^1.0.3", "object.assign": "^4.1.0", "object.values": "^1.1.1", "prop-types": "^15.7.2", - "react-is": "^16.12.0", + "react-is": "^16.13.1", "react-test-renderer": "^16.0.0-0", "semver": "^5.7.0" + }, + "dependencies": { + "enzyme-shallow-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", + "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", + "requires": { + "has": "^1.0.3", + "object-is": "^1.1.2" + } + } } }, "enzyme-adapter-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz", - "integrity": "sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz", + "integrity": "sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==", "requires": { - "airbnb-prop-types": "^2.15.0", + "airbnb-prop-types": "^2.16.0", "function.prototype.name": "^1.1.2", "object.assign": "^4.1.0", "object.fromentries": "^2.0.2", @@ -16002,9 +16032,9 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" }, "nearley": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.2.tgz", - "integrity": "sha512-h6lygT0BWAGErDvoE2LfI+tDeY2+UUrqG5dcBPdCmjnjud9z1wE0P7ljb85iNbE93YA+xJLpoSYGMuUqhnSSSA==", + "version": "2.19.6", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.6.tgz", + "integrity": "sha512-OV3Lx+o5iIGWVY38zs+7aiSnBqaHTFAOQiz83VHJje/wOOaSgzE3H0S/xfISxJhFSoPcX611OEDV9sCT8F283g==", "requires": { "commander": "^2.19.0", "moo": "^0.5.0", diff --git a/package.json b/package.json index 174c9a8b6..f9a940897 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "clsx": "^1.1.0", "debug": "^4.1.1", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", + "enzyme-adapter-react-16": "^1.15.4", "eventemitter3": "^4.0.0", "leaflet": "^1.6.0", "react": "^16.12.0", diff --git a/src/app-contexts.ts b/src/app-contexts.ts deleted file mode 100644 index 19de11214..000000000 --- a/src/app-contexts.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react'; -import { ResourceConfigurationsType } from './resource-manager'; - -/* Declares the ResourcesContext which contains the resources used on the app*/ -export const ResourcesContext = createContext({}); diff --git a/src/components/app-contexts.tsx b/src/components/app-contexts.tsx new file mode 100644 index 000000000..f5ddc40b4 --- /dev/null +++ b/src/components/app-contexts.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { ResourceConfigurationsType } from '../resource-manager'; +import { defaultSettings, Settings } from '../settings'; +import { NotificationBarContext, NotificationBarProps } from './notification-bar'; + +/* Declares the ResourcesContext which contains the resources used on the app*/ +export const ResourcesContext = React.createContext({}); + +export const SettingsContext = React.createContext(defaultSettings()); + +export interface AppContextProviderProps extends React.PropsWithChildren<{}> { + settings: Settings; + notificationDispatch: React.Dispatch>; +} + +export function AppContextProvider(props: AppContextProviderProps): React.ReactElement { + const { settings, notificationDispatch, children } = props; + return ( + + + {children} + + + ); +} diff --git a/src/components/app.tsx b/src/components/app.tsx index a64d0c5c0..9523a4a55 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,7 +1,7 @@ import { AppBar, Fade, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core/'; import { Dashboard as DashboardIcon, Settings as SettingsIcon } from '@material-ui/icons'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; -import debug from 'debug'; +import Debug from 'debug'; import React from 'react'; import 'typeface-roboto'; import { AppConfig } from '../app-config'; @@ -9,8 +9,10 @@ import DispenserStateManager from '../dispenser-state-manager'; import DoorStateManager from '../door-state-manager'; import FleetManager from '../fleet-manager'; import LiftStateManager from '../lift-state-manager'; +import { ResourceConfigurationsType } from '../resource-manager'; import { RobotTrajectoryManager } from '../robot-trajectory-manager'; -import { loadSettings, saveSettings, Settings, SettingsContext } from '../settings'; +import { loadSettings, saveSettings, Settings } from '../settings'; +import { AppContextProvider } from './app-contexts'; import './app.css'; import CommandsPanel from './commands-panel'; import DispensersPanel from './dispensers-panel'; @@ -18,17 +20,16 @@ import DoorsPanel from './doors-panel'; import LiftsPanel from './lift-item/lifts-panel'; import LoadingScreen, { LoadingScreenProps } from './loading-screen'; import MainMenu from './main-menu'; +import NotificationBar, { NotificationBarProps } from './notification-bar'; import OmniPanel from './omni-panel'; import OmniPanelView from './omni-panel-view'; +import { RmfContextProvider } from './rmf-contexts'; import RobotsPanel from './robots-panel'; import ScheduleVisualizer from './schedule-visualizer'; import SettingsDrawer from './settings-drawer'; import { SpotlightValue } from './spotlight-value'; -import { DoorStateContext } from './schedule-visualizer/doors-overlay'; -import { LiftStateContext } from './schedule-visualizer/lift-overlay'; -import NotificationBar, { NotificationBarProps, NotificationBarContext } from './notification-bar'; -import { ResourceConfigurationsType } from '../resource-manager'; +const debug = Debug('App'); const borderRadius = 20; const useStyles = makeStyles(theme => ({ @@ -111,6 +112,8 @@ function makeViewMap(): ViewMap { const viewMap = makeViewMap(); export default function App(props: AppProps): JSX.Element { + debug('render'); + const classes = useStyles(); const { transportFactory, trajectoryManagerFactory, appResources } = props.appConfig; const [transport, setTransport] = React.useState(undefined); @@ -120,25 +123,28 @@ export default function App(props: AppProps): JSX.Element { const doorStateManager = React.useMemo(() => new DoorStateManager(), []); const [doorStates, setDoorStates] = React.useState(() => doorStateManager.doorStates()); - const [doors, setDoors] = React.useState([]); - + const [doors, setDoors] = React.useState([]); const [doorSpotlight, setDoorSpotlight] = React.useState | undefined>( undefined, ); const liftStateManager = React.useMemo(() => new LiftStateManager(), []); const [liftStates, setLiftStates] = React.useState(() => liftStateManager.liftStates()); - - const [lifts, setLifts] = React.useState([]); + const [lifts, setLifts] = React.useState([]); const [liftSpotlight, setLiftSpotlight] = React.useState | undefined>( undefined, ); const fleetManager = React.useMemo(() => new FleetManager(), []); const [fleets, setFleets] = React.useState(fleetManager.fleets()); + const fleetNames = React.useRef([]); const [robotSpotlight, setRobotSpotlight] = React.useState | undefined>( undefined, ); + const newFleetNames = fleets.map(fleet => fleet.name); + if (newFleetNames.some(fleetName => !fleetNames.current.includes(fleetName))) { + fleetNames.current = newFleetNames; + } const dispenserStateManager = React.useMemo(() => new DispenserStateManager(), []); const [dispenserStates, setDispenserStates] = React.useState< @@ -179,6 +185,9 @@ export default function App(props: AppProps): JSX.Element { fleetManager.on('updated', () => setFleets(fleetManager.fleets())); liftStateManager.on('updated', () => setLiftStates(liftStateManager.liftStates())); doorStateManager.on('updated', () => setDoorStates(doorStateManager.doorStates())); + dispenserStateManager.on('updated', () => + setDispenserStates(dispenserStateManager.dispenserStates()), + ); setTransport(x); }) .catch((e: CloseEvent) => { @@ -221,64 +230,28 @@ export default function App(props: AppProps): JSX.Element { })(); }, [appResources]); - React.useEffect(() => { - if (currentView === OmniPanelViewIndex.Doors) { - const listener = () => setDoorStates(doorStateManager.doorStates()); - doorStateManager.on('updated', listener); - debug.log('started tracking door states'); - return () => { - doorStateManager.off('updated', listener); - debug.log('stopped tracking door states'); - }; - } - }, [currentView, doorStateManager]); - - React.useEffect(() => { - if (currentView === OmniPanelViewIndex.Lifts) { - const listener = () => setLiftStates(liftStateManager.liftStates()); - liftStateManager.on('updated', listener); - debug.log('started tracking lift states'); - return () => { - liftStateManager.off('updated', listener); - debug.log('stopped tracking lift states'); - }; - } - }, [currentView, liftStateManager]); - - React.useEffect(() => { - if (currentView === OmniPanelViewIndex.Dispensers) { - const listener = () => setDispenserStates(dispenserStateManager.dispenserStates()); - dispenserStateManager.on('updated', listener); - debug.log('started tracking dispenser states'); - return () => { - dispenserStateManager.off('updated', listener); - debug.log('stopped tracking dispenser states'); - }; - } - }, [currentView, dispenserStateManager]); - React.useEffect(() => { setDoors(buildingMap ? buildingMap.levels.flatMap(x => x.doors) : []); setLifts(buildingMap ? buildingMap.lifts : []); }, [buildingMap]); - function handleDoorClick(door: RomiCore.Door): void { + const handleDoorClick = React.useCallback((door: RomiCore.Door) => { setShowOmniPanel(true); setCurrentView(OmniPanelViewIndex.Doors); setDoorSpotlight({ value: door.name }); - } + }, []); - function handleRobotClick(robot: RomiCore.RobotState): void { + const handleRobotClick = React.useCallback((fleet: string, robot: RomiCore.RobotState) => { setShowOmniPanel(true); setCurrentView(OmniPanelViewIndex.Robots); - setRobotSpotlight({ value: robot.name }); - } + setRobotSpotlight({ value: `${fleet}-${robot.name}` }); + }, []); - function handleLiftClick(lift: RomiCore.Lift): void { + const handleLiftClick = React.useCallback((lift: RomiCore.Lift) => { setShowOmniPanel(true); setCurrentView(OmniPanelViewIndex.Lifts); setLiftSpotlight({ value: lift.name }); - } + }, []); function clearSpotlights() { setDoorSpotlight(undefined); @@ -287,142 +260,136 @@ export default function App(props: AppProps): JSX.Element { setDispenserSpotlight(undefined); } - function handleClose() { + const handleClose = React.useCallback(() => { clearSpotlights(); setShowOmniPanel(false); - } - - function handleBack(index: number): void { - clearSpotlights(); - const parent = viewMap[index].parent; - if (!parent) { - return handleClose(); - } - setCurrentView(parent.value); - } + }, []); + + const handleBack = React.useCallback( + (index: number) => { + clearSpotlights(); + const parent = viewMap[index].parent; + if (!parent) { + return handleClose(); + } + setCurrentView(parent.value); + }, + [handleClose], + ); - function handleMainMenuDoorsClick(): void { - setDoorStates(doorStateManager.doorStates()); + const handleMainMenuDoorsClick = React.useCallback(() => { setCurrentView(OmniPanelViewIndex.Doors); - } + }, []); - function handleMainMenuLiftsClick(): void { - setLiftStates(liftStateManager.liftStates()); + const handleMainMenuLiftsClick = React.useCallback(() => { setCurrentView(OmniPanelViewIndex.Lifts); - } + }, []); - function handleMainMenuRobotsClick(): void { + const handleMainMenuRobotsClick = React.useCallback(() => { setCurrentView(OmniPanelViewIndex.Robots); - } + }, []); - function handleMainMenuDispensersClick(): void { + const handleMainMenuDispensersClick = React.useCallback(() => { setCurrentView(OmniPanelViewIndex.Dispensers); - } + }, []); - function handleMainMenuCommandsClick(): void { + const handleMainMenuCommandsClick = React.useCallback(() => { setCurrentView(OmniPanelViewIndex.Commands); - } + }, []); + + const omniPanelClasses = React.useMemo( + () => ({ backButton: classes.topLeftBorder, closeButton: classes.topRightBorder }), + [classes.topLeftBorder, classes.topRightBorder], + ); return ( - - - - {loading && } -
- - - - Dashboard - - setShowOmniPanel(!showOmniPanel)}> - - - setShowSettings(true)}> - - - - - {buildingMap && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - { - setSettings(newSettings); - saveSettings(newSettings); - }} - onClose={() => setShowSettings(false)} + + + {loading && } +
+ + + + Dashboard + + setShowOmniPanel(!showOmniPanel)}> + + + setShowSettings(true)}> + + + + + {buildingMap && ( + -
- + + + + + + + + + + + + + + + + + + + + + + { + setSettings(newSettings); + saveSettings(newSettings); + }} + onClose={() => setShowSettings(false)} /> - - - +
+ + + ); } diff --git a/src/components/commands-panel.tsx b/src/components/commands-panel.tsx index 531362339..06280f4d1 100644 --- a/src/components/commands-panel.tsx +++ b/src/components/commands-panel.tsx @@ -1,16 +1,19 @@ import { - makeStyles, ExpansionPanel, + ExpansionPanelDetails, ExpansionPanelSummary, + makeStyles, Typography, - ExpansionPanelDetails, } from '@material-ui/core'; import { ExpandMore as ExpandMoreIcon } from '@material-ui/icons'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import { LoopForm } from './loop-form'; import { v4 as uuidv4 } from 'uuid'; import { RobotDeliveryForm } from './delivery-form'; +import { LoopForm } from './loop-form'; + +const debug = Debug('OmniPanel:CommandsPanel'); export type TDeliveryRequest = ( pickupPlaceName: string, @@ -56,7 +59,7 @@ export function requestLoop( } /** -* The Delivery task is one where a robot is assigned to pick up an item at one location (pickup_place_name) and deliver it to another (dropoff_place_name). At each of these locations, there is an automation system called workcell/dispenser that loads and unload the item off the robot. +* The Delivery task is one where a robot is assigned to pick up an item at one location (pickup_place_name) and deliver it to another (dropoff_place_name). At each of these locations, there is an automation system called workcell/dispenser that loads and unload the item off the robot. Currently only these fields are being used in Delivery msg. * task_id: Unique id for the request. @@ -94,14 +97,15 @@ export function requestDelivery( } export interface CommandsPanelProps { - fleets: readonly RomiCore.FleetState[]; + allFleets: string[]; transport?: Readonly; } -export default function CommandsPanel(props: CommandsPanelProps): React.ReactElement { - const { fleets, transport } = props; +export const CommandsPanel = React.memo((props: CommandsPanelProps) => { + debug('render'); + + const { allFleets, transport } = props; const classes = useStyles(); - const allFleets = fleets.flatMap(fleet => fleet.name); const loopRequestPub = React.useMemo( () => (transport ? transport.createPublisher(RomiCore.loopRequests) : null), [transport], @@ -165,7 +169,9 @@ export default function CommandsPanel(props: CommandsPanelProps): React.ReactEle
); -} +}); + +export default CommandsPanel; export const useStyles = makeStyles(theme => ({ expansionSummaryContent: { diff --git a/src/components/dispenser-item.tsx b/src/components/dispenser-item.tsx index d5d6cb80f..e47f2651f 100644 --- a/src/components/dispenser-item.tsx +++ b/src/components/dispenser-item.tsx @@ -4,116 +4,122 @@ import { ExpansionPanelDetails, ExpansionPanelProps, ExpansionPanelSummary, - makeStyles, - Typography, List, ListItem, + makeStyles, + Typography, } from '@material-ui/core'; import { CSSProperties } from '@material-ui/core/styles/withStyles'; import { ExpandMore as ExpandMoreIcon } from '@material-ui/icons'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; - +import { colorPalette } from '../util/css-utils'; import DisableableTypography from './disableable-typography'; import OmniPanelStatusLabels from './omni-panel-status-labels'; -import { colorPalette } from '../util/css-utils'; + +const debug = Debug('OmniPanel:DispenserItem'); export interface DispenserItemProps extends Omit { dispenserState: Readonly; } -export const DispenserItem = React.forwardRef(function( - props: DispenserItemProps, - ref: React.Ref, -): React.ReactElement { - const { dispenserState, ...otherProps } = props; - const classes = useStyles(); - const dispenserModeLabelClasses = useDispenserModeLabelStyles(); - - function dispenserModeLabelClass(): string { - switch (dispenserState.mode) { - case RomiCore.DispenserState.IDLE: - return `${classes.dispenserLabel} ${dispenserModeLabelClasses.idle}`; - case RomiCore.DispenserState.BUSY: - return `${classes.dispenserLabel} ${dispenserModeLabelClasses.busy}`; - case RomiCore.DispenserState.OFFLINE: - return `${classes.dispenserLabel} ${dispenserModeLabelClasses.offline}`; - default: - return `${classes.dispenserLabel} ${dispenserModeLabelClasses.unknown}`; +export const DispenserItem = React.memo( + React.forwardRef(function( + props: DispenserItemProps, + ref: React.Ref, + ): React.ReactElement { + debug('render'); + + const { dispenserState, ...otherProps } = props; + const classes = useStyles(); + const dispenserModeLabelClasses = useDispenserModeLabelStyles(); + + function dispenserModeLabelClass(): string { + switch (dispenserState.mode) { + case RomiCore.DispenserState.IDLE: + return `${classes.dispenserLabel} ${dispenserModeLabelClasses.idle}`; + case RomiCore.DispenserState.BUSY: + return `${classes.dispenserLabel} ${dispenserModeLabelClasses.busy}`; + case RomiCore.DispenserState.OFFLINE: + return `${classes.dispenserLabel} ${dispenserModeLabelClasses.offline}`; + default: + return `${classes.dispenserLabel} ${dispenserModeLabelClasses.unknown}`; + } } - } - - function dispenserRequestQueueId(): React.ReactElement { - if (dispenserState.request_guid_queue.length === 0) { - return ( - - Unknown - - ); - } else { - return ( - - {dispenserState.request_guid_queue.map(id => ( - - {id} - - ))} - - ); + + function dispenserRequestQueueId(): React.ReactElement { + if (dispenserState.request_guid_queue.length === 0) { + return ( + + Unknown + + ); + } else { + return ( + + {dispenserState.request_guid_queue.map(id => ( + + {id} + + ))} + + ); + } } - } - - function dispenserModeToString(): string { - switch (dispenserState.mode) { - case RomiCore.DispenserState.IDLE: - return 'IDLE'; - case RomiCore.DispenserState.BUSY: - return 'ONLINE'; - case RomiCore.DispenserState.OFFLINE: - return 'OFFLINE'; - default: - return 'N/A'; + + function dispenserModeToString(): string { + switch (dispenserState.mode) { + case RomiCore.DispenserState.IDLE: + return 'IDLE'; + case RomiCore.DispenserState.BUSY: + return 'ONLINE'; + case RomiCore.DispenserState.OFFLINE: + return 'OFFLINE'; + default: + return 'N/A'; + } } - } - - return ( - - } - > - - - -
- Name: - {dispenserState.guid} -
- -
- No. Queued Requests: - - {String(dispenserState.request_guid_queue.length)} - -
- -
- Request Queue ID: - {dispenserRequestQueueId()} -
- -
- Seconds Remaining: - {String(dispenserState.seconds_remaining)} -
-
-
- ); -}); + + return ( + + } + > + + + +
+ Name: + {dispenserState.guid} +
+ +
+ No. Queued Requests: + + {String(dispenserState.request_guid_queue.length)} + +
+ +
+ Request Queue ID: + {dispenserRequestQueueId()} +
+ +
+ Seconds Remaining: + {String(dispenserState.seconds_remaining)} +
+
+
+ ); + }), +); export default DispenserItem; diff --git a/src/components/dispensers-panel.tsx b/src/components/dispensers-panel.tsx index 02c5aa5b5..01f6eac14 100644 --- a/src/components/dispensers-panel.tsx +++ b/src/components/dispensers-panel.tsx @@ -1,18 +1,48 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import DispenserItem from './dispenser-item'; +import DispenserItem, { DispenserItemProps } from './dispenser-item'; import { SpotlightValue } from './spotlight-value'; +const debug = Debug('OmniPanel:DispenserPanel'); + export interface DispenserPanelProps { dispenserStates: Readonly>; spotlight?: Readonly>; } -export default function DispenserPanel(props: DispenserPanelProps): JSX.Element { - const { spotlight } = props; +export const DispenserPanel = React.memo((props: DispenserPanelProps) => { + debug('render'); + + const { dispenserStates, spotlight } = props; const dispenserRefs = React.useRef>({}); const [expanded, setExpanded] = React.useState>({}); + const storeRef = React.useCallback((ref: HTMLElement | null) => { + if (!ref) { + return; + } + const guid = ref.getAttribute('data-guid'); + if (!guid) { + return; + } + dispenserRefs.current[guid] = ref; + }, []); + + const onChange = React.useCallback['onChange']>( + (event, newExpanded) => { + const guid = (event.currentTarget as HTMLElement).parentElement?.getAttribute('data-guid'); + if (!guid) { + return; + } + setExpanded(prev => ({ + ...prev, + [guid]: newExpanded, + })); + }, + [], + ); + React.useEffect(() => { if (!spotlight) { return; @@ -28,23 +58,21 @@ export default function DispenserPanel(props: DispenserPanelProps): JSX.Element ref.scrollIntoView({ behavior: 'smooth' }); }, [spotlight]); - const listItems = Object.keys(props.dispenserStates).map(guid => { + const listItems = Object.keys(dispenserStates).map((guid, i) => { const state = props.dispenserStates[guid]; return ( (dispenserRefs.current[state.guid] = ref)} + data-guid={state.guid} + ref={storeRef} dispenserState={state} expanded={Boolean(expanded[state.guid])} - onChange={(_, newExpanded) => - setExpanded(prev => ({ - ...prev, - [state.guid]: newExpanded, - })) - } + onChange={onChange} /> ); }); return {listItems}; -} +}); + +export default DispenserPanel; diff --git a/src/components/door-item.tsx b/src/components/door-item.tsx index c4dd75eb0..fd9df1fcc 100644 --- a/src/components/door-item.tsx +++ b/src/components/door-item.tsx @@ -12,10 +12,12 @@ import { } from '@material-ui/core'; import { ExpandMore as ExpandMoreIcon } from '@material-ui/icons'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; - -import OmniPanelStatusLabels from './omni-panel-status-labels'; import { colorPalette } from '../util/css-utils'; +import OmniPanelStatusLabels from './omni-panel-status-labels'; + +const debug = Debug('OmniPanel:DoorItem'); export interface DoorItemProps extends Omit { door: Readonly; @@ -30,6 +32,8 @@ export const DoorItem = React.forwardRef(function( props: DoorItemProps, ref: React.Ref, ): React.ReactElement { + debug('render'); + const { door, doorState, enableControls, onOpenClick, onCloseClick, ...otherProps } = props; const classes = useStyles(); const theme = useTheme(); diff --git a/src/components/doors-panel.tsx b/src/components/doors-panel.tsx index 1b31f1cf7..fbf826e27 100644 --- a/src/components/doors-panel.tsx +++ b/src/components/doors-panel.tsx @@ -1,39 +1,98 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import DoorItem from './door-item'; +import { makeCallbackArrayCallback } from '../util/react-helpers'; +import DoorItem_, { DoorItemProps } from './door-item'; import { SpotlightValue } from './spotlight-value'; +const debug = Debug('OmniPanel:DoorsPanel'); +const DoorItem = React.memo(DoorItem_); + export interface DoorsPanelProps { - doors: readonly RomiCore.Door[]; + doors: RomiCore.Door[]; doorStates: Readonly>; transport?: Readonly; spotlight?: Readonly>; onDoorClick?(door: RomiCore.Door): void; } -export default function DoorsPanel(props: DoorsPanelProps): JSX.Element { - const { transport, spotlight, onDoorClick } = props; - const doorRefs = React.useRef>({}); +function requestDoor( + publisher: RomiCore.Publisher, + requester_id: string, + door: RomiCore.Door, + mode: number, +): void { + publisher.publish({ + door_name: door.name, + requested_mode: { value: mode }, + requester_id: requester_id, + request_time: RomiCore.toRosTime(new Date()), + }); +} + +export const DoorsPanel = React.memo((props: DoorsPanelProps) => { + debug('render'); + + const { doors, doorStates, transport, spotlight, onDoorClick } = props; const [expanded, setExpanded] = React.useState>({}); const doorRequestPub = React.useMemo( () => (transport ? transport.createPublisher(RomiCore.adapterDoorRequests) : null), [transport], ); - function requestDoor(door: RomiCore.Door, mode: number): void { - doorRequestPub?.publish({ - door_name: door.name, - requested_mode: { value: mode }, - requester_id: transport!.name, - request_time: RomiCore.toRosTime(new Date()), - }); - } + const doorRefs = React.useMemo(() => { + const refs: Record> = {}; + doors.map(door => (refs[door.name] = React.createRef())); + return refs; + }, [doors]); + + const onChange = React.useMemo( + makeCallbackArrayCallback['onChange'], RomiCore.Door>( + doors, + (door, _, newExpanded) => + setExpanded(prev => ({ + ...prev, + [door.name]: newExpanded, + })), + ), + [doors], + ); + + const onClick = React.useMemo( + makeCallbackArrayCallback['onClick'], RomiCore.Door>( + doors, + door => onDoorClick && onDoorClick(door), + ), + [doors, onDoorClick], + ); + + const onOpenClick = React.useMemo( + makeCallbackArrayCallback['onOpenClick'], RomiCore.Door>( + doors, + door => + doorRequestPub && + transport && + requestDoor(doorRequestPub, transport.name, door, RomiCore.DoorMode.MODE_OPEN), + ), + [doors, onDoorClick], + ); + + const onCloseClick = React.useMemo( + makeCallbackArrayCallback['onCloseClick'], RomiCore.Door>( + doors, + door => + doorRequestPub && + transport && + requestDoor(doorRequestPub, transport.name, door, RomiCore.DoorMode.MODE_OPEN), + ), + [doors, onDoorClick], + ); React.useEffect(() => { if (!spotlight) { return; } - const ref = doorRefs.current[spotlight.value]; + const ref = doorRefs[spotlight.value]; if (!ref) { return; } @@ -41,31 +100,28 @@ export default function DoorsPanel(props: DoorsPanelProps): JSX.Element { ...prev, [spotlight.value]: true, })); - ref.scrollIntoView({ behavior: 'smooth' }); - }, [spotlight]); + ref.current?.scrollIntoView({ behavior: 'smooth' }); + }, [spotlight, doorRefs]); - const listItems = props.doors.map(door => { - const doorState = props.doorStates[door.name]; + const listItems = doors.map((door, i) => { + const doorState = doorStates[door.name]; return ( (doorRefs.current[door.name] = ref)} + ref={doorRefs[door.name]} door={door} doorState={doorState} enableControls={Boolean(transport)} - onOpenClick={() => requestDoor(door, RomiCore.DoorMode.MODE_OPEN)} - onCloseClick={() => requestDoor(door, RomiCore.DoorMode.MODE_CLOSED)} - onClick={() => onDoorClick && onDoorClick(door)} + onOpenClick={onOpenClick[i]} + onCloseClick={onCloseClick[i]} + onClick={onClick[i]} expanded={Boolean(expanded[door.name])} - onChange={(_, newExpanded) => - setExpanded(prev => ({ - ...prev, - [door.name]: newExpanded, - })) - } + onChange={onChange[i]} /> ); }); return {listItems}; -} +}); + +export default DoorsPanel; diff --git a/src/components/lift-item/lift-item.tsx b/src/components/lift-item/lift-item.tsx index 9dc3195a5..cf2024165 100644 --- a/src/components/lift-item/lift-item.tsx +++ b/src/components/lift-item/lift-item.tsx @@ -14,6 +14,9 @@ import LiftRequestForm from './lift-item-form'; import { LiftRequestManager } from '../../lift-state-manager'; import OmniPanelStatusLabels from '../omni-panel-status-labels'; import { colorPalette } from '../../util/css-utils'; +import Debug from 'debug'; + +const debug = Debug('OmniPanel:LiftItem'); export interface LiftItemProps extends Omit { id?: string; @@ -28,77 +31,78 @@ export interface LiftItemProps extends Omit { ): void; } -export const LiftItem = React.forwardRef(function( - props: LiftItemProps, - ref: React.Ref, -): React.ReactElement { - const { id, lift, liftState, enableRequest, onRequest, ...otherProps } = props; - const [tabValue, setTabValue] = React.useState(0); - const classes = useStyles(); - - function liftFloorLabel(liftState?: RomiCore.LiftState): string { - if (!liftState) { - return classes.liftFloorLabelUnknown; +export const LiftItem = React.memo( + React.forwardRef(function(props: LiftItemProps, ref: React.Ref): React.ReactElement { + debug('render'); + + const { id, lift, liftState, enableRequest, onRequest, ...otherProps } = props; + const [tabValue, setTabValue] = React.useState(0); + const classes = useStyles(); + + function liftFloorLabel(liftState?: RomiCore.LiftState): string { + if (!liftState) { + return classes.liftFloorLabelUnknown; + } + switch (liftState.motion_state) { + case RomiCore.LiftState.MOTION_UP: + case RomiCore.LiftState.MOTION_DOWN: + return classes.liftFloorLabelMoving; + default: + return classes.liftFloorLabelStopped; + } } - switch (liftState.motion_state) { - case RomiCore.LiftState.MOTION_UP: - case RomiCore.LiftState.MOTION_DOWN: - return classes.liftFloorLabelMoving; - default: - return classes.liftFloorLabelStopped; + + function handleRequest(doorState: number, requestType: number, destination: string): void { + !!onRequest && onRequest(lift, doorState, requestType, destination); } - } - function handleRequest(doorState: number, requestType: number, destination: string): void { - !!onRequest && onRequest(lift, doorState, requestType, destination); - } + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setTabValue(newValue); + }; - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setTabValue(newValue); - }; + const doorStates = React.useMemo(() => LiftRequestManager.getDoorModes(), []); + const requestTypes = React.useMemo(() => LiftRequestManager.getLiftRequestModes(), []); - const doorStates = React.useMemo(() => LiftRequestManager.getDoorModes(), []); - const requestTypes = React.useMemo(() => LiftRequestManager.getLiftRequestModes(), []); - - return ( - - } - > - - - - + } > - - - - - - - - {lift.levels && ( - - )} - - - - ); -}); + + + + + + + + + + + + {lift.levels && ( + + )} + + + + ); + }), +); const useStyles = makeStyles(theme => { const liftFloorLabelBase: CSSProperties = { diff --git a/src/components/lift-item/lifts-panel.tsx b/src/components/lift-item/lifts-panel.tsx index 1844a19ac..b4a07bd0f 100644 --- a/src/components/lift-item/lifts-panel.tsx +++ b/src/components/lift-item/lifts-panel.tsx @@ -1,10 +1,15 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import { LiftRequest } from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import { LiftItem } from './lift-item'; +import { makeCallbackArrayCallback } from '../../util/react-helpers'; import { SpotlightValue } from '../spotlight-value'; +import { LiftItem, LiftItemProps } from './lift-item'; + +const debug = Debug('OmniPanel:LiftsPanel'); export interface LiftsPanelProps { - lifts: readonly RomiCore.Lift[]; + lifts: RomiCore.Lift[]; liftStates: Readonly>; transport?: Readonly; spotlight?: Readonly>; @@ -12,37 +17,45 @@ export interface LiftsPanelProps { onLiftClick?(lift: RomiCore.Lift): void; } -export default function LiftsPanel(props: LiftsPanelProps): JSX.Element { - const { transport, spotlight, onLiftRequest, onLiftClick } = props; +function handleLiftRequest( + liftRequestPub: RomiCore.Publisher, + sessionId: string, + lift: RomiCore.Lift, + doorState: number, + requestType: number, + destination: string, +): void { + liftRequestPub.publish({ + lift_name: lift.name, + door_state: doorState, + request_type: requestType, + request_time: RomiCore.toRosTime(new Date()), + destination_floor: destination, + session_id: sessionId, + }); +} + +export const LiftsPanel = React.memo((props: LiftsPanelProps) => { + debug('render'); + + const { lifts, liftStates, transport, spotlight, onLiftRequest, onLiftClick } = props; const liftRequestPub = React.useMemo( () => (transport ? transport.createPublisher(RomiCore.adapterLiftRequests) : null), [transport], ); - const liftRefs = React.useRef>({}); const [expanded, setExpanded] = React.useState>>({}); - function handleLiftRequest( - lift: RomiCore.Lift, - doorState: number, - requestType: number, - destination: string, - ): void { - liftRequestPub?.publish({ - lift_name: lift.name, - door_state: doorState, - request_type: requestType, - request_time: RomiCore.toRosTime(new Date()), - destination_floor: destination, - session_id: transport!.name, - }); - onLiftRequest && onLiftRequest(lift, destination); - } + const liftRefs = React.useMemo(() => { + const refs: Record> = {}; + lifts.map(lift => (refs[lift.name] = React.createRef())); + return refs; + }, [lifts]); React.useEffect(() => { if (!spotlight) { return; } - const ref = liftRefs.current[spotlight.value]; + const ref = liftRefs[spotlight.value]; if (!ref) { return; } @@ -50,33 +63,70 @@ export default function LiftsPanel(props: LiftsPanelProps): JSX.Element { ...prev, [spotlight.value]: true, })); - ref.scrollIntoView({ behavior: 'smooth' }); - }, [spotlight]); + ref.current?.scrollIntoView({ behavior: 'smooth' }); + }, [spotlight, liftRefs]); + + const onChange = React.useMemo( + makeCallbackArrayCallback['onChange'], RomiCore.Lift>( + lifts, + (lift, _, newExpanded) => + setExpanded(prev => ({ + ...prev, + [lift.name]: newExpanded, + })), + ), + [lifts], + ); + + const onClick = React.useMemo( + makeCallbackArrayCallback['onClick'], RomiCore.Lift>( + lifts, + lift => onLiftClick && onLiftClick(lift), + ), + [lifts, onLiftClick], + ); + + const onRequest = React.useMemo( + makeCallbackArrayCallback['onRequest'], RomiCore.Lift>( + lifts, + (lift, _, doorState, requestType, destination) => { + liftRequestPub && + transport && + handleLiftRequest( + liftRequestPub, + transport.name, + lift, + doorState, + requestType, + destination, + ); + onLiftRequest && onLiftRequest(lift, destination); + }, + ), + [], + ); return ( - {props.lifts.map(lift => { - const liftState = props.liftStates[lift.name]; + {lifts.map((lift, i) => { + const liftState = liftStates[lift.name]; return ( (liftRefs.current[lift.name] = ref)} + ref={liftRefs[lift.name]} liftState={liftState} enableRequest={Boolean(transport)} - onRequest={handleLiftRequest} - onClick={() => onLiftClick && onLiftClick(lift)} + onRequest={onRequest[i]} + onClick={onClick[i]} expanded={Boolean(expanded[lift.name])} - onChange={(_, newExpanded) => - setExpanded(prev => ({ - ...prev, - [lift.name]: newExpanded, - })) - } + onChange={onChange[i]} /> ); })} ); -} +}); + +export default LiftsPanel; diff --git a/src/components/main-menu.tsx b/src/components/main-menu.tsx index 9cf9ede0c..f7652ce2a 100644 --- a/src/components/main-menu.tsx +++ b/src/components/main-menu.tsx @@ -1,5 +1,8 @@ import { Divider, List, ListItem, Typography } from '@material-ui/core'; import React from 'react'; +import Debug from 'debug'; + +const debug = Debug('MainMenu'); export interface MainMenuProps { onDoorsClick?(event: React.MouseEvent): void; @@ -9,7 +12,9 @@ export interface MainMenuProps { onCommandsClick?(event: React.MouseEvent): void; } -export default function MainMenu(props: MainMenuProps): React.ReactElement { +export const MainMenu = React.memo((props: MainMenuProps) => { + debug('render'); + return ( @@ -41,4 +46,6 @@ export default function MainMenu(props: MainMenuProps): React.ReactElement { ); -} \ No newline at end of file +}); + +export default MainMenu; diff --git a/src/components/omni-panel.tsx b/src/components/omni-panel.tsx index 33573d7a6..f67154aa7 100644 --- a/src/components/omni-panel.tsx +++ b/src/components/omni-panel.tsx @@ -2,6 +2,7 @@ import { Button, ButtonGroup, makeStyles, Slide } from '@material-ui/core'; import { Close as CloseIcon, KeyboardBackspace as BackIcon } from '@material-ui/icons'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; import React from 'react'; +import { isArray } from 'util'; import { OmniPanelViewProps } from './omni-panel-view'; const useStyles = makeStyles(() => ({ @@ -33,7 +34,7 @@ export interface OmniPanelProps extends React.HTMLAttributes { }; onBack?: (current: number) => void; onClose?: () => void; - children?: React.ReactElement[]; + children?: React.ReactElement[] | React.ReactElement; } export const OmniPanel = React.forwardRef(function( @@ -54,6 +55,39 @@ export const OmniPanel = React.forwardRef(function( props.onClose && props.onClose(); } + const renderChildren = () => { + if (!children) { + return null; + } + if (isArray(children)) { + return children.map(child => ( + +
{child}
+
+ )); + } else { + return ( + +
{children}
+
+ ); + } + }; + return (
@@ -74,20 +108,7 @@ export const OmniPanel = React.forwardRef(function( -
- {props.children?.map(child => ( - -
{child}
-
- ))} -
+
{renderChildren()}
); }); diff --git a/src/components/rmf-contexts.tsx b/src/components/rmf-contexts.tsx new file mode 100644 index 000000000..0e2adb67c --- /dev/null +++ b/src/components/rmf-contexts.tsx @@ -0,0 +1,19 @@ +import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import React from 'react'; + +export const DoorStateContext = React.createContext>({}); +export const LiftStateContext = React.createContext>({}); + +export interface RmfContextProviderProps extends React.PropsWithChildren<{}> { + doorStates: Record; + liftStates: Record; +} + +export function RmfContextProvider(props: RmfContextProviderProps): React.ReactElement { + const { doorStates, liftStates, children } = props; + return ( + + {children} + + ); +} diff --git a/src/components/robot-item.tsx b/src/components/robot-item.tsx index 2510f520d..39d44318e 100644 --- a/src/components/robot-item.tsx +++ b/src/components/robot-item.tsx @@ -6,11 +6,13 @@ import { makeStyles, } from '@material-ui/core'; import { ExpandMore as ExpandMoreIcon } from '@material-ui/icons'; -import { RobotInformation } from './robot-item-information'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; - import OmniPanelStatusLabels from './omni-panel-status-labels'; +import { RobotInformation } from './robot-item-information'; + +const debug = Debug('OmniPanel:RobotItem'); export interface RobotItemProps extends Omit { fleetName: string; @@ -18,30 +20,35 @@ export interface RobotItemProps extends Omit { onRobotClick?(robot: RomiCore.RobotState): void; } -export const RobotItem = React.forwardRef(function( - props: RobotItemProps, - ref: React.Ref, -): React.ReactElement { - const { robot, onRobotClick, fleetName, ...otherProps } = props; - const classes = useStyles(); - return ( - - } - > - - - - - - - ); -}); +export const RobotItem = React.memo( + React.forwardRef(function( + props: RobotItemProps, + ref: React.Ref, + ): React.ReactElement { + const { robot, onRobotClick, fleetName, ...otherProps } = props; + const classes = useStyles(); + + debug('render %s', robot.name); + + return ( + + } + > + + + + + + + ); + }), +); export default RobotItem; diff --git a/src/components/robots-panel.tsx b/src/components/robots-panel.tsx index 50b6c068a..afaf25604 100644 --- a/src/components/robots-panel.tsx +++ b/src/components/robots-panel.tsx @@ -1,7 +1,10 @@ -import { SpotlightValue } from './spotlight-value'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import RobotItem from './robot-item'; +import RobotItem, { RobotItemProps } from './robot-item'; +import { SpotlightValue } from './spotlight-value'; + +const debug = Debug('OmniPanel:RobotsPanel'); export interface RobotsPanelProps { fleets: readonly RomiCore.FleetState[]; @@ -9,11 +12,58 @@ export interface RobotsPanelProps { onRobotClick?(robot: RomiCore.RobotState): void; } -export default function RobotsPanel(props: RobotsPanelProps): React.ReactElement { +export const RobotsPanel = React.memo((props: RobotsPanelProps) => { + debug('render'); + const { fleets, spotlight, onRobotClick } = props; const robotRefs = React.useRef>({}); const [expanded, setExpanded] = React.useState>>({}); + const storeRef = React.useCallback((ref: HTMLElement | null) => { + if (!ref) { + return; + } + const fleetName = ref.getAttribute('data-fleet'); + const robotName = ref.getAttribute('data-name'); + if (!fleetName || !robotName) { + return; + } + robotRefs.current[`${fleetName}-${robotName}`] = ref; + }, []); + + const onChange = React.useCallback['onChange']>((event, newExpanded) => { + const fleetName = (event.currentTarget as HTMLElement).parentElement?.getAttribute( + 'data-fleet', + ); + const robotName = (event.currentTarget as HTMLElement).parentElement?.getAttribute('data-name'); + if (!fleetName || !robotName) { + return; + } + setExpanded(prev => ({ + ...prev, + [`${fleetName}-${robotName}`]: newExpanded, + })); + }, []); + + const onClick = React.useRef['onClick']>(event => { + const fleetName = (event.currentTarget as HTMLElement).getAttribute('data-fleet'); + const robotName = (event.currentTarget as HTMLElement).getAttribute('data-name'); + if (!fleetName || !robotName) { + return; + } + const fleet = fleets.find(f => f.name === fleetName); + if (!fleet) { + return; + } + const robot = fleet.robots.find(r => r.name === robotName); + if (!robot) { + return; + } + onRobotClick && onRobotClick(robot); + }); + + const transitionProps = React.useMemo(() => ({ unmountOnExit: true }), []); + React.useEffect(() => { if (!spotlight) { return; @@ -34,22 +84,21 @@ export default function RobotsPanel(props: RobotsPanelProps): React.ReactElement {fleets.flatMap(fleet => fleet.robots.map(robot => ( (robotRefs.current[robot.name] = ref)} + key={`${fleet.name}-${robot.name}`} + data-fleet={fleet.name} + data-name={robot.name} + ref={storeRef} fleetName={fleet.name} robot={robot} - onClick={() => onRobotClick && onRobotClick(robot)} - expanded={Boolean(expanded[robot.name])} - onChange={(_, newExpanded) => - setExpanded(prev => ({ - ...prev, - [robot.name]: newExpanded, - })) - } - TransitionProps={{ unmountOnExit: true }} + onClick={onClick.current} + expanded={Boolean(expanded[`${fleet.name}-${robot.name}`])} + onChange={onChange} + TransitionProps={transitionProps} /> )), )} ); -} +}); + +export default RobotsPanel; diff --git a/src/components/schedule-visualizer/door/door.tsx b/src/components/schedule-visualizer/door/door.tsx index fc0d48ede..51870f5c7 100644 --- a/src/components/schedule-visualizer/door/door.tsx +++ b/src/components/schedule-visualizer/door/door.tsx @@ -1,11 +1,14 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; +import React from 'react'; import DefaultDoor from './door-default'; import DoubleHingeDoor from './door-double-hinge'; import DoubleSlideDoor from './door-double-slide'; -import React from 'react'; import SingleHingeDoor from './door-single-hinge'; import SingleSlideDoor from './door-single-slide'; +const debug = Debug('ScheduleVisualizer:Door'); + /** * currentMode: Current mode of the door. E.g: 0 = DoorMode.CLOSE. * door: Door information provided by the map. @@ -38,69 +41,73 @@ export const getDoorStyle = (classes: Record, currentMode: numbe } }; -const Door = React.forwardRef(function( - props: DoorContainerProps, - ref: React.Ref, -): React.ReactElement { - const { door, onClick, currentMode } = props; - const { - DOOR_TYPE_UNDEFINED, - DOOR_TYPE_SINGLE_SLIDING, - DOOR_TYPE_DOUBLE_SLIDING, - DOOR_TYPE_SINGLE_TELESCOPE, - DOOR_TYPE_DOUBLE_TELESCOPE, - DOOR_TYPE_SINGLE_SWING, - DOOR_TYPE_DOUBLE_SWING, - } = RomiCore.Door; - const { door_type: doorType } = door; - const v1 = [door.v1_x, door.v1_y]; - const v2 = [door.v2_x, door.v2_y]; +const Door = React.memo( + React.forwardRef(function( + props: DoorContainerProps, + ref: React.Ref, + ): React.ReactElement { + const { door, onClick, currentMode } = props; + debug('render %s', door.name); + + const { + DOOR_TYPE_UNDEFINED, + DOOR_TYPE_SINGLE_SLIDING, + DOOR_TYPE_DOUBLE_SLIDING, + DOOR_TYPE_SINGLE_TELESCOPE, + DOOR_TYPE_DOUBLE_TELESCOPE, + DOOR_TYPE_SINGLE_SWING, + DOOR_TYPE_DOUBLE_SWING, + } = RomiCore.Door; + const { door_type: doorType } = door; + const v1 = [door.v1_x, door.v1_y]; + const v2 = [door.v2_x, door.v2_y]; - const handleClick = (event: React.MouseEvent) => { - onClick && onClick(event, door); - }; + const handleClick = (event: React.MouseEvent) => { + onClick && onClick(event, door); + }; - return ( - - {doorType === DOOR_TYPE_SINGLE_SWING && ( - - )} - {doorType === DOOR_TYPE_DOUBLE_SWING && ( - - )} - {(doorType === DOOR_TYPE_SINGLE_SLIDING || doorType === DOOR_TYPE_SINGLE_TELESCOPE) && ( - - )} - {(doorType === DOOR_TYPE_DOUBLE_SLIDING || doorType === DOOR_TYPE_DOUBLE_TELESCOPE) && ( - - )} - {doorType === DOOR_TYPE_UNDEFINED && } - - ); -}); + return ( + + {doorType === DOOR_TYPE_SINGLE_SWING && ( + + )} + {doorType === DOOR_TYPE_DOUBLE_SWING && ( + + )} + {(doorType === DOOR_TYPE_SINGLE_SLIDING || doorType === DOOR_TYPE_SINGLE_TELESCOPE) && ( + + )} + {(doorType === DOOR_TYPE_DOUBLE_SLIDING || doorType === DOOR_TYPE_DOUBLE_TELESCOPE) && ( + + )} + {doorType === DOOR_TYPE_UNDEFINED && } + + ); + }), +); export default Door; diff --git a/src/components/schedule-visualizer/doors-overlay.tsx b/src/components/schedule-visualizer/doors-overlay.tsx index 302eb0a44..16355a551 100644 --- a/src/components/schedule-visualizer/doors-overlay.tsx +++ b/src/components/schedule-visualizer/doors-overlay.tsx @@ -1,20 +1,21 @@ -/** - * TODO: Need door location, (v1_x, v1_y) only defines the location of the hinge, we also need the - * length and orientation of the door to draw it on the map. - */ - import * as RomiCore from '@osrf/romi-js-core-interfaces'; -import React, { createContext, useContext } from 'react'; +import Debug from 'debug'; +import React, { useContext } from 'react'; import { viewBoxFromLeafletBounds } from '../../util/css-utils'; +import { DoorStateContext } from '../rmf-contexts'; +import Door, { DoorContainerProps } from './door/door'; import SVGOverlay, { SVGOverlayProps } from './svg-overlay'; -import Door from './door/door'; +const debug = Debug('ScheduleVisualizer:DoorsOverlay'); + export interface DoorsOverlayProps extends SVGOverlayProps { doors: readonly RomiCore.Door[]; onDoorClick?(door: RomiCore.Door): void; } -export default function DoorsOverlay(props: DoorsOverlayProps): React.ReactElement { +export const DoorsOverlay = React.memo((props: DoorsOverlayProps) => { + debug('render'); + const { doors, onDoorClick, ...otherProps } = props; const viewBox = viewBoxFromLeafletBounds(props.bounds); const doorsState = useContext(DoorStateContext); @@ -23,6 +24,12 @@ export default function DoorsOverlay(props: DoorsOverlayProps): React.ReactEleme const currentDoor = doorsState && doorsState[doorName]; return currentDoor && currentDoor.current_mode.value; }; + + const handleDoorClick = React.useCallback['onClick']>( + (_, door) => onDoorClick && onDoorClick(door), + [onDoorClick], + ); + return ( <> @@ -31,7 +38,7 @@ export default function DoorsOverlay(props: DoorsOverlayProps): React.ReactEleme onDoorClick && onDoorClick(door)} + onClick={handleDoorClick} doorState={doorsState && doorsState[door.name]} currentMode={getCurrentDoorMode(door.name)} /> @@ -40,6 +47,6 @@ export default function DoorsOverlay(props: DoorsOverlayProps): React.ReactEleme ); -} +}); -export const DoorStateContext = createContext>>({}); +export default DoorsOverlay; diff --git a/src/components/schedule-visualizer/index.tsx b/src/components/schedule-visualizer/index.tsx index e25eaf733..a41da7d3d 100644 --- a/src/components/schedule-visualizer/index.tsx +++ b/src/components/schedule-visualizer/index.tsx @@ -1,8 +1,10 @@ import { makeStyles } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import * as L from 'leaflet'; import React from 'react'; import { AttributionControl, ImageOverlay, LayersControl, Map as LMap, Pane } from 'react-leaflet'; +import { ResourceConfigurationsType } from '../../resource-manager'; import { Conflict, DefaultTrajectoryManager, @@ -10,9 +12,12 @@ import { Trajectory, TrajectoryResponse, } from '../../robot-trajectory-manager'; -import { AnimationSpeed, SettingsContext, TrajectoryAnimation } from '../../settings'; +import { AnimationSpeed, TrajectoryAnimation } from '../../settings'; import { toBlobUrl } from '../../util'; +import { ResourcesContext, SettingsContext } from '../app-contexts'; import ColorManager from './colors'; +import DoorsOverlay from './doors-overlay'; +import LiftsOverlay from './lift-overlay'; import RobotTrajectoriesOverlay, { RobotTrajectoryContext } from './robot-trajectories-overlay'; import RobotTrajectory from './robot-trajectory'; import RobotsOverlay from './robots-overlay'; @@ -21,12 +26,10 @@ import { withFollowAnimation, withOutlineAnimation, } from './trajectory-animations'; -import DoorsOverlay from './doors-overlay'; -import LiftsOverlay from './lift-overlay'; -import { ResourcesContext } from '../../app-contexts'; -import { ResourceConfigurationsType } from '../../resource-manager'; import WaypointsOverlay from './waypoints-overlay'; +const debug = Debug('ScheduleVisualizer'); + const useStyles = makeStyles(() => ({ map: { height: '100%', @@ -49,7 +52,7 @@ export interface ScheduleVisualizerProps { appResources?: Readonly; onDoorClick?(door: RomiCore.Door): void; onLiftClick?(lift: RomiCore.Lift): void; - onRobotClick?(robot: RomiCore.RobotState): void; + onRobotClick?(fleet: string, robot: RomiCore.RobotState): void; } function calcMaxBounds(mapFloorLayers: readonly MapFloorLayer[]): L.LatLngBounds | undefined { @@ -62,6 +65,8 @@ function calcMaxBounds(mapFloorLayers: readonly MapFloorLayer[]): L.LatLngBounds } export default function ScheduleVisualizer(props: ScheduleVisualizerProps): React.ReactElement { + debug('render'); + const { appResources } = props; const classes = useStyles(); const mapRef = React.useRef(null); @@ -110,17 +115,18 @@ export default function ScheduleVisualizer(props: ScheduleVisualizerProps): Reac return 1000; } }, [settings]); - const TrajectoryComponent = React.useMemo(() => { + + const RobotTrajContextValue = React.useMemo(() => { const animationScale = trajLookahead / trajAnimDuration; switch (settings.trajectoryAnimation) { case TrajectoryAnimation.None: - return RobotTrajectory; + return { Component: RobotTrajectory }; case TrajectoryAnimation.Fill: - return withFillAnimation(RobotTrajectory, animationScale); + return { Component: withFillAnimation(RobotTrajectory, animationScale) }; case TrajectoryAnimation.Follow: - return withFollowAnimation(RobotTrajectory, animationScale); + return { Component: withFollowAnimation(RobotTrajectory, animationScale) }; case TrajectoryAnimation.Outline: - return withOutlineAnimation(RobotTrajectory, animationScale); + return { Component: withOutlineAnimation(RobotTrajectory, animationScale) }; } }, [settings.trajectoryAnimation, trajAnimDuration]); @@ -178,7 +184,9 @@ export default function ScheduleVisualizer(props: ScheduleVisualizerProps): Reac for (const p of promises) { await p; } + debug('set map floor layers'); setMapFloorLayers(mapFloorLayers); + debug('set max bounds'); setMaxBounds(calcMaxBounds(Object.values(mapFloorLayers))); })(); }, [props.buildingMap, mapElement]); @@ -200,6 +208,7 @@ export default function ScheduleVisualizer(props: ScheduleVisualizerProps): Reac trim: true, }, }); + debug('set trajectories'); setTrajectories(prev => ({ ...prev, [curMapFloorLayer.level.name]: resp, @@ -213,6 +222,7 @@ export default function ScheduleVisualizer(props: ScheduleVisualizerProps): Reac }, [props.trajManager, curMapFloorLayer, trajAnimDuration]); function handleBaseLayerChange(e: L.LayersControlEvent): void { + debug('set current level name'); setCurLevelName(e.name); } @@ -253,11 +263,14 @@ export default function ScheduleVisualizer(props: ScheduleVisualizerProps): Reac if (curMapFloorLayer) { const mapTrajectories = getTrajectory(curMapFloorLayer.level.name); const mapConflicts = getConflicts(curMapFloorLayer.level.name); + debug('set current map trajectories'); setCurMapTrajectories(mapTrajectories); + debug('set current map conflicts'); setCurMapConflicts(mapConflicts); + debug('set conflicting robots'); setConflictRobotNames(getConflictRobotsName(mapConflicts, mapTrajectories)); } - }, [curMapFloorLayer, trajectories, curMapConflicts, curMapTrajectories]); + }, [curMapFloorLayer, trajectories]); return ( {curMapFloorLayer && ( - + { + debug('render'); + const { lifts, onLiftClick, currentFloor, ...otherProps } = props; const viewBox = viewBoxFromLeafletBounds(props.bounds); const liftsState = useContext(LiftStateContext); + + const handleLiftClick = React.useCallback['onClick']>( + (_, lift) => onLiftClick && onLiftClick(lift), + [onLiftClick], + ); + return ( <> {lifts.map(lift => ( onLiftClick && onLiftClick(lift)} + onClick={handleLiftClick} liftState={liftsState && liftsState[lift.name]} currentFloor={currentFloor} /> @@ -32,6 +44,6 @@ export default function LiftsOverlay(props: LiftsOverlayProps): React.ReactEleme ); -} +}); -export const LiftStateContext = createContext>>({}); +export default LiftsOverlay; diff --git a/src/components/schedule-visualizer/lift.tsx b/src/components/schedule-visualizer/lift.tsx index da127c156..15b1b2d08 100644 --- a/src/components/schedule-visualizer/lift.tsx +++ b/src/components/schedule-visualizer/lift.tsx @@ -1,12 +1,15 @@ import { makeStyles } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; -import Door from './door/door'; -import { UpArrow, DownArrow } from './arrow'; import { radiansToDegrees, transformMiddleCoordsOfRectToSVGBeginPoint, } from '../../util/calculation-helpers'; +import { DownArrow, UpArrow } from './arrow'; +import Door from './door/door'; + +const debug = Debug('ScheduleVisualizer:Lift'); export interface LiftProps { id?: string; @@ -72,75 +75,80 @@ const getRelatedYCoord = (topY: number, height: number, percentage: number) => { return topY + height * percentage; }; -const Lift = React.forwardRef(function( - props: LiftProps, - ref: React.Ref, -): React.ReactElement { - const { id, lift, onClick, liftState, currentFloor } = props; - const { width, depth, ref_x: x, ref_y: y, ref_yaw, doors } = lift; - // The svg start drawing from the top left coordinate, and the backend sends us the middle X,Y so we need to transform that. - const { x: topVerticeX, y: topVerticeY } = transformMiddleCoordsOfRectToSVGBeginPoint( - x, - y, - width, - depth, - ); - // Since we are working with a plane with positive X and Negative Y we need to change the sign of the y. - const contextY = -y; - const contextTopVerticeY = -topVerticeY; - // Get properties from lift state - const currentMode = liftState?.current_mode; - const destinationFloor = liftState?.destination_floor; - const doorState = liftState?.door_state; - const isInCurrentFloor = liftState?.current_floor === currentFloor; - const motionState = liftState?.motion_state; +const Lift = React.memo( + React.forwardRef(function(props: LiftProps, ref: React.Ref): React.ReactElement { + const { id, lift, onClick, liftState, currentFloor } = props; + debug('render %s', lift.name); + + const { width, depth, ref_x: x, ref_y: y, ref_yaw, doors } = lift; + // The svg start drawing from the top left coordinate, and the backend sends us the middle X,Y so we need to transform that. + const { x: topVerticeX, y: topVerticeY } = transformMiddleCoordsOfRectToSVGBeginPoint( + x, + y, + width, + depth, + ); + // Since we are working with a plane with positive X and Negative Y we need to change the sign of the y. + const contextY = -y; + const contextTopVerticeY = -topVerticeY; + // Get properties from lift state + const currentMode = liftState?.current_mode; + const destinationFloor = liftState?.destination_floor; + const doorState = liftState?.door_state; + const isInCurrentFloor = liftState?.current_floor === currentFloor; + const motionState = liftState?.motion_state; - const classes = useStyles(); + const classes = useStyles(); - const liftStyle = getLiftStyle(classes, currentMode, isInCurrentFloor); - const liftMotionText = getLiftMotionText(liftState?.current_floor, destinationFloor, motionState); - const liftModeText = getLiftModeText(currentMode); - return ( - <> - onClick && onClick(e, lift)}> - - {liftMotionText && ( - - {liftMotionText} - + const liftStyle = getLiftStyle(classes, currentMode, isInCurrentFloor); + const liftMotionText = getLiftMotionText( + liftState?.current_floor, + destinationFloor, + motionState, + ); + const liftModeText = getLiftModeText(currentMode); + return ( + <> + onClick && onClick(e, lift)}> + + {liftMotionText && ( + + {liftMotionText} + + )} + {liftModeText && ( + + {liftModeText} + + )} + + {motionState === RomiCore.LiftState.MOTION_UP && ( + )} - {liftModeText && ( - - {liftModeText} - + {motionState === RomiCore.LiftState.MOTION_DOWN && ( + )} - - {motionState === RomiCore.LiftState.MOTION_UP && ( - - )} - {motionState === RomiCore.LiftState.MOTION_DOWN && ( - - )} - {doors.map(door => ( - - ))} - - ); -}); + {doors.map(door => ( + + ))} + + ); + }), +); export default Lift; diff --git a/src/components/schedule-visualizer/robot-trajectories-overlay.tsx b/src/components/schedule-visualizer/robot-trajectories-overlay.tsx index 72f1fe810..a1f4ccfbf 100644 --- a/src/components/schedule-visualizer/robot-trajectories-overlay.tsx +++ b/src/components/schedule-visualizer/robot-trajectories-overlay.tsx @@ -1,10 +1,13 @@ +import Debug from 'debug'; import * as L from 'leaflet'; import React, { useContext, useEffect } from 'react'; import { Conflict, Trajectory } from '../../robot-trajectory-manager'; +import { NotificationBarContext } from '../notification-bar'; import ColorManager from './colors'; import RobotTrajectory, { RobotTrajectoryProps } from './robot-trajectory'; import SVGOverlay, { SVGOverlayProps } from './svg-overlay'; -import { NotificationBarContext } from '../notification-bar'; + +const debug = Debug('ScheduleVisualizer:RobotTrajectoriesOverlay'); export interface RobotTrajectoriesOverlayProps extends SVGOverlayProps { trajs: readonly Trajectory[]; @@ -13,9 +16,9 @@ export interface RobotTrajectoriesOverlayProps extends SVGOverlayProps { conflictRobotNames: string[][]; } -export default function RobotTrajectoriesOverlay( - props: RobotTrajectoriesOverlayProps, -): React.ReactElement { +export const RobotTrajectoriesOverlay = React.memo((props: RobotTrajectoriesOverlayProps) => { + debug('render'); + const { trajs, conflicts, colorManager, conflictRobotNames, ...otherProps } = props; const trajectoryContext = useContext(RobotTrajectoryContext); const notificationDispatch = useContext(NotificationBarContext); @@ -61,7 +64,9 @@ export default function RobotTrajectoriesOverlay( ); -} +}); + +export default RobotTrajectoriesOverlay; export interface RobotTrajectoryContext { Component: React.ComponentType; diff --git a/src/components/schedule-visualizer/robot-trajectory.tsx b/src/components/schedule-visualizer/robot-trajectory.tsx index 7584ded6a..efd722055 100644 --- a/src/components/schedule-visualizer/robot-trajectory.tsx +++ b/src/components/schedule-visualizer/robot-trajectory.tsx @@ -1,8 +1,11 @@ +import { useTheme } from '@material-ui/core'; +import Debug from 'debug'; import React from 'react'; -import { Conflict, rawKnotsToKnots, Trajectory, RawKnot } from '../../robot-trajectory-manager'; +import { Conflict, RawKnot, rawKnotsToKnots, Trajectory } from '../../robot-trajectory-manager'; import { bezierControlPoints, knotsToSegmentCoefficientsArray } from '../../util/cublic-spline'; import { TrajectoryPath } from './trajectory-animations'; -import { useTheme } from '@material-ui/core'; + +const debug = Debug('ScheduleVisualizer:RobotTrajectory'); export interface RobotTrajectoryProps extends React.RefAttributes, @@ -12,42 +15,46 @@ export interface RobotTrajectoryProps footprint: number; } -export const RobotTrajectory = React.forwardRef(function( - props: RobotTrajectoryProps, - ref: React.Ref, -): React.ReactElement { - const { trajectory, conflicts, footprint, ...otherProps } = props; - const theme = useTheme(); +export const RobotTrajectory = React.memo( + React.forwardRef(function( + props: RobotTrajectoryProps, + ref: React.Ref, + ): React.ReactElement { + debug('render'); - const color = React.useMemo( - () => - conflicts.flat().includes(trajectory.id) - ? theme.palette.error.main - : theme.palette.success.main, - [trajectory, conflicts, theme], - ); + const { trajectory, conflicts, footprint, ...otherProps } = props; + const theme = useTheme(); - const pathD = React.useMemo(() => { - return trajectoryPath(trajectory.segments).d; - }, [trajectory]); + const color = React.useMemo( + () => + conflicts.flat().includes(trajectory.id) + ? theme.palette.error.main + : theme.palette.success.main, + [trajectory, conflicts, theme], + ); - return ( - - ); -}); + const pathD = React.useMemo(() => { + return trajectoryPath(trajectory.segments).d; + }, [trajectory]); + + return ( + + ); + }), +); export default RobotTrajectory; diff --git a/src/components/schedule-visualizer/robot.tsx b/src/components/schedule-visualizer/robot.tsx index 86a3d4268..973dc2337 100644 --- a/src/components/schedule-visualizer/robot.tsx +++ b/src/components/schedule-visualizer/robot.tsx @@ -1,13 +1,16 @@ import { makeStyles } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React, { useContext, useState } from 'react'; -import { ResourcesContext } from '../../app-contexts'; import ResourceManager from '../../resource-manager'; +import { ResourcesContext } from '../app-contexts'; import ColorManager from './colors'; import RobotDefaultIcon from './robot-default-icon'; import RobotImageIcon from './robot-image-icon'; import SvgText from './svg-text'; +const debug = Debug('ScheduleVisualizer:Robot'); + const useStyles = makeStyles(() => ({ robotText: { dominantBaseline: 'central', @@ -32,60 +35,61 @@ export interface RobotProps { footprint: number; fleetName: string; inConflict?: boolean; - onClick?(e: React.MouseEvent, robot: RomiCore.RobotState): void; + onClick?(e: React.MouseEvent, fleetName: string, robot: RomiCore.RobotState): void; } -const Robot = React.forwardRef(function( - props: RobotProps, - ref: React.Ref, -): React.ReactElement { - const resourcesContext = useContext(ResourcesContext); - const classes = useStyles(); - const { robot, footprint, colorManager, fleetName, inConflict, onClick } = props; - // The only image formats SVG software support are JPEG, PNG, and other SVG files. - const [renderCustomIcon, setRenderCustomIcon] = useState({ - path: ResourceManager.getRobotIconPath(resourcesContext, fleetName), - error: false, - }); +const Robot = React.memo( + React.forwardRef(function(props: RobotProps, ref: React.Ref): React.ReactElement { + const resourcesContext = useContext(ResourcesContext); + const classes = useStyles(); + const { robot, footprint, colorManager, fleetName, inConflict, onClick } = props; + debug('render %s', robot.name); + + // The only image formats SVG software support are JPEG, PNG, and other SVG files. + const [renderCustomIcon, setRenderCustomIcon] = useState({ + path: ResourceManager.getRobotIconPath(resourcesContext, fleetName), + error: false, + }); - return ( - <> - onClick && onClick(e, robot)} - transform={`translate(${robot.location.x} ${-robot.location.y}) + return ( + <> + onClick && onClick(e, fleetName, robot)} + transform={`translate(${robot.location.x} ${-robot.location.y}) rotate(${-(robot.location.yaw * 180) / Math.PI})`} - > - {!!renderCustomIcon.path && !renderCustomIcon.error ? ( - - ) : ( - - )} - - - - ); -}); + > + {!!renderCustomIcon.path && !renderCustomIcon.error ? ( + + ) : ( + + )} + + + + ); + }), +); export default Robot; diff --git a/src/components/schedule-visualizer/robots-overlay.tsx b/src/components/schedule-visualizer/robots-overlay.tsx index 01a33f7da..ed393283c 100644 --- a/src/components/schedule-visualizer/robots-overlay.tsx +++ b/src/components/schedule-visualizer/robots-overlay.tsx @@ -1,20 +1,25 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React, { useMemo } from 'react'; import { viewBoxFromLeafletBounds } from '../../util/css-utils'; import ColorManager from './colors'; import Robot_, { RobotProps } from './robot'; import SVGOverlay, { SVGOverlayProps } from './svg-overlay'; +const debug = Debug('ScheduleVisualizer:RobotsOverlay'); + export interface RobotsOverlayProps extends SVGOverlayProps { colorManager: ColorManager; fleets: readonly RomiCore.FleetState[]; - onRobotClick?(robot: RomiCore.RobotState): void; + onRobotClick?(fleet: string, robot: RomiCore.RobotState): void; conflictRobotNames: string[][]; currentFloorName: string; RobotComponent?: React.ElementType; } -export default function RobotsOverlay(props: RobotsOverlayProps): React.ReactElement { +export const RobotsOverlay = React.memo((props: RobotsOverlayProps) => { + debug('render'); + const { fleets, colorManager, @@ -51,26 +56,36 @@ export default function RobotsOverlay(props: RobotsOverlayProps): React.ReactEle if (!currentFloorName) { return []; } - return fleets.flatMap(x => x.robots.filter(r => r.location.level_name === currentFloorName)); + return fleets.flatMap(x => ({ + fleet: x.name, + robots: x.robots.filter(r => r.location.level_name === currentFloorName), + })); }, [fleets, currentFloorName]); + const handleRobotClick = React.useCallback['onClick']>( + (_, fleetName, robot) => onRobotClick && onRobotClick(fleetName, robot), + [onRobotClick], + ); + return ( - {robotsInCurLevel.map(robot => { - return ( + {robotsInCurLevel.map(({ fleet, robots }) => + robots.map(robot => ( onRobotClick && onRobotClick(robot_)} + onClick={handleRobotClick} inConflict={inConflict(robot.name)} /> - ); - })} + )), + )} ); -} +}); + +export default RobotsOverlay; diff --git a/src/components/schedule-visualizer/tests/__snapshots__/waypoint.test.tsx.snap b/src/components/schedule-visualizer/tests/__snapshots__/waypoint.test.tsx.snap index 1fbd983c7..b6ef63416 100644 --- a/src/components/schedule-visualizer/tests/__snapshots__/waypoint.test.tsx.snap +++ b/src/components/schedule-visualizer/tests/__snapshots__/waypoint.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Renders correctly 1`] = ` - - + `; diff --git a/src/components/schedule-visualizer/waypoint.tsx b/src/components/schedule-visualizer/waypoint.tsx index 695a66b76..dfceae223 100644 --- a/src/components/schedule-visualizer/waypoint.tsx +++ b/src/components/schedule-visualizer/waypoint.tsx @@ -1,14 +1,19 @@ import { makeStyles } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React from 'react'; +const debug = Debug('ScheduleVisualizer:Waypoint'); + export interface WaypointProps { waypoint: RomiCore.GraphNode; size: number; } -const Waypoint = (props: WaypointProps): React.ReactElement => { +const Waypoint = React.memo((props: WaypointProps) => { const { waypoint, size } = props; + debug('render %s', waypoint.name); + const classes = useStyles(); return ( @@ -34,7 +39,7 @@ const Waypoint = (props: WaypointProps): React.ReactElement => { ); -}; +}); export default Waypoint; diff --git a/src/components/schedule-visualizer/waypoints-overlay.tsx b/src/components/schedule-visualizer/waypoints-overlay.tsx index 29dab285f..ccc39af90 100644 --- a/src/components/schedule-visualizer/waypoints-overlay.tsx +++ b/src/components/schedule-visualizer/waypoints-overlay.tsx @@ -1,14 +1,19 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; +import Debug from 'debug'; import React, { useMemo } from 'react'; import { viewBoxFromLeafletBounds } from '../../util/css-utils'; import SVGOverlay, { SVGOverlayProps } from './svg-overlay'; import Waypoint from './waypoint'; +const debug = Debug('ScheduleVisualier:WaypointsOverlay'); + export interface WaypointsOverlayProps extends SVGOverlayProps { currentLevel: RomiCore.Level; } -export default function WaypointsOverlay(props: WaypointsOverlayProps): React.ReactElement { +export const WaypointsOverlay = React.memo((props: WaypointsOverlayProps) => { + debug('render'); + const { currentLevel, ...otherProps } = props; const viewBox = viewBoxFromLeafletBounds(props.bounds); // Set the size of the waypoint. At least for now we don't want for this to change. We left this here in case we want for this to change in the future. @@ -30,4 +35,6 @@ export default function WaypointsOverlay(props: WaypointsOverlayProps): React.Re ); -} +}); + +export default WaypointsOverlay; diff --git a/src/components/tests/commands-panel.test.tsx b/src/components/tests/commands-panel.test.tsx index 5592bdf74..98564b7c4 100644 --- a/src/components/tests/commands-panel.test.tsx +++ b/src/components/tests/commands-panel.test.tsx @@ -14,7 +14,7 @@ beforeEach(() => { }); it('renders loop form', () => { - const root = mount(); + const root = mount( fleet.name)} />); const formElements = root.find(LoopForm); expect(formElements.length).toBe(1); root.unmount(); diff --git a/src/components/tests/doors-panel.test.tsx b/src/components/tests/doors-panel.test.tsx index 1bdc8dec7..c27c2e493 100644 --- a/src/components/tests/doors-panel.test.tsx +++ b/src/components/tests/doors-panel.test.tsx @@ -23,7 +23,7 @@ beforeEach(async () => { it('renders doors', () => { const root = mount(); - const doorElements = root.find(DoorItem); + const doorElements = root.find('[data-component="DoorItem"]').hostNodes(); expect(doorElements.length).toBe(doors.length); root.unmount(); }); diff --git a/src/dispenser-state-manager.ts b/src/dispenser-state-manager.ts index cf28c1452..10a1a0509 100644 --- a/src/dispenser-state-manager.ts +++ b/src/dispenser-state-manager.ts @@ -11,11 +11,17 @@ export default class DispenserStateManager extends EventEmitter { } startSubscription(transport: RomiCore.Transport) { - transport.subscribe(RomiCore.dispenserStates, dispenserState => { - this._dispenserStates[dispenserState.guid] = dispenserState; - this.emit('updated'); - }); + this._subscriptions.push( + transport.subscribe(RomiCore.dispenserStates, dispenserState => { + this._dispenserStates[dispenserState.guid] = dispenserState; + this.emit('updated'); + }), + ); + } + stopAllSubscriptions(): void { + this._subscriptions.forEach(sub => sub.unsubscribe()); } private _dispenserStates: Record = {}; + private _subscriptions: RomiCore.Subscription[] = []; } diff --git a/src/door-state-manager.ts b/src/door-state-manager.ts index b97da8342..72657b841 100644 --- a/src/door-state-manager.ts +++ b/src/door-state-manager.ts @@ -11,11 +11,18 @@ export default class DoorStateManager extends EventEmitter { } startSubscription(transport: RomiCore.Transport) { - transport.subscribe(RomiCore.doorStates, doorState => { - this._doorStates[doorState.door_name] = doorState; - this.emit('updated'); - }); + this._subscriptions.push( + transport.subscribe(RomiCore.doorStates, doorState => { + this._doorStates[doorState.door_name] = doorState; + this.emit('updated'); + }), + ); + } + + stopAllSubscriptions(): void { + this._subscriptions.forEach(sub => sub.unsubscribe()); } private _doorStates: Record = {}; + private _subscriptions: RomiCore.Subscription[] = []; } diff --git a/src/fleet-manager.ts b/src/fleet-manager.ts index 5b6b8f940..29be0fa52 100644 --- a/src/fleet-manager.ts +++ b/src/fleet-manager.ts @@ -3,7 +3,7 @@ import EventEmitter from 'eventemitter3'; type Events = { updated: []; -} +}; export default class FleetManager extends EventEmitter { fleets(): RomiCore.FleetState[] { @@ -11,11 +11,18 @@ export default class FleetManager extends EventEmitter { } startSubscription(transport: RomiCore.Transport) { - transport.subscribe(RomiCore.fleetStates, fleetState => { - this._fleets[fleetState.name] = fleetState; - this.emit('updated'); - }); + this._subscriptions.push( + transport.subscribe(RomiCore.fleetStates, fleetState => { + this._fleets[fleetState.name] = fleetState; + this.emit('updated'); + }), + ); + } + + stopAllSubscriptions(): void { + this._subscriptions.forEach(sub => sub.unsubscribe()); } private _fleets: Record = {}; + private _subscriptions: RomiCore.Subscription[] = []; } diff --git a/src/lift-state-manager.ts b/src/lift-state-manager.ts index 61be96e84..93da9d90e 100644 --- a/src/lift-state-manager.ts +++ b/src/lift-state-manager.ts @@ -11,11 +11,16 @@ export default class LiftStateManager extends EventEmitter { } startSubscription(transport: RomiCore.Transport) { - // TODO remove the skip validation when the type problem of available_floors is solved - transport.subscribe(RomiCore.skipValidation(RomiCore.liftStates), liftState => { - this._liftStates[liftState.lift_name] = liftState; - this.emit('updated'); - }); + this._subscriptions.push( + transport.subscribe(RomiCore.liftStates, liftState => { + this._liftStates[liftState.lift_name] = liftState; + this.emit('updated'); + }), + ); + } + + stopAllSubscriptions(): void { + this._subscriptions.forEach(sub => sub.unsubscribe()); } static liftModeToString(liftMode: number): string { @@ -62,6 +67,7 @@ export default class LiftStateManager extends EventEmitter { } private _liftStates: Record = {}; + private _subscriptions: RomiCore.Subscription[] = []; } export interface requestManagerModes { @@ -74,27 +80,24 @@ export class LiftRequestManager { RomiCore.LiftRequest.REQUEST_HUMAN_MODE, ]; - static readonly doorModes = [ - RomiCore.LiftRequest.DOOR_CLOSED, - RomiCore.LiftRequest.DOOR_OPEN, - ] + static readonly doorModes = [RomiCore.LiftRequest.DOOR_CLOSED, RomiCore.LiftRequest.DOOR_OPEN]; static getLiftRequestModes(): requestManagerModes { let modes: requestManagerModes = {}; this.liftRequestModes.forEach(element => { const key = this.requestModeToString(element); - modes[key] = element + modes[key] = element; }); - return modes + return modes; } static getDoorModes(): requestManagerModes { let modes: requestManagerModes = {}; this.doorModes.forEach(element => { const key = this.doorStateToString(element); - modes[key] = element + modes[key] = element; }); - return modes + return modes; } static requestModeToString(requestMode: number): string { diff --git a/src/mock/fake-traj-manager.ts b/src/mock/fake-traj-manager.ts index c4fdd266b..d83b3ff88 100644 --- a/src/mock/fake-traj-manager.ts +++ b/src/mock/fake-traj-manager.ts @@ -1,3 +1,4 @@ +import Debug from 'debug'; import { RobotTrajectoryManager, TimeRequest, @@ -7,14 +8,18 @@ import { } from '../robot-trajectory-manager'; import trajectories from './data/trajectories.json'; +const debug = Debug('FakeTrajectoryManager'); + export default class FakeTrajectoryManager implements RobotTrajectoryManager { async latestTrajectory(request: TrajectoryRequest): Promise { + debug('sending trajectory'); if (request.param.map_name === 'L1') { const traj = trajectories[this.currentTraj++]; this.currentTraj %= trajectories.length; // "deep clone" object return JSON.parse(JSON.stringify(traj)) as any; } + return { response: 'trajectory', values: [], diff --git a/src/mock/fake-transport.ts b/src/mock/fake-transport.ts index d6a016b63..aa9c369c1 100644 --- a/src/mock/fake-transport.ts +++ b/src/mock/fake-transport.ts @@ -1,20 +1,29 @@ import * as RomiCore from '@osrf/romi-js-core-interfaces'; -import debug from 'debug'; +import Debug from 'debug'; import buildingMap from './data/building-map'; import fakeDispenserStates from './data/dispenser-states'; import fakeDoorStates from './data/door-states'; import fakeFleets from './data/fleets'; import fakeLiftStates from './data/lift-states'; +const debug = Debug('FakeTransport'); + +export type DoorStateFactory = () => Record; + export class FakeTransport extends RomiCore.TransportEvents implements RomiCore.Transport { name: string = 'fake'; + constructor(doorStateFactory?: DoorStateFactory) { + super(); + this._doorStateFactory = doorStateFactory || fakeDoorStates; + } + createPublisher( topic: RomiCore.RomiTopic, options?: RomiCore.Options | undefined, ): RomiCore.Publisher { return { - publish: debug.log, + publish: debug, }; } @@ -23,12 +32,14 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. cb: RomiCore.SubscriptionCb, options?: RomiCore.Options | undefined, ): RomiCore.Subscription { - debug.log('subscribe:', topic); + debug('subscribe %s', topic.topic); + let timer: number; switch (topic) { case RomiCore.doorStates: { - const doorStates = fakeDoorStates(); - setInterval(() => { + const doorStates = this._doorStateFactory(); + timer = window.setInterval(() => { for (const state of Object.values(doorStates)) { + debug('publishing door state, %s', state.door_name); cb(state as Message); } }, 1000); @@ -36,8 +47,9 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. } case RomiCore.liftStates: { const liftStates = fakeLiftStates(); - setInterval(() => { + timer = window.setInterval(() => { for (const state of Object.values(liftStates)) { + debug('publishing lift state, %s', state.lift_name); cb(state as Message); } }, 1000); @@ -45,8 +57,9 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. } case RomiCore.fleetStates: { const fleets = fakeFleets(); - setInterval(() => { + timer = window.setInterval(() => { for (const fleet of fleets) { + debug('publishing fleet state, %s', fleet.name); cb(fleet as Message); } }, 1000); @@ -54,8 +67,9 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. } case RomiCore.dispenserStates: { const dispenserStates = fakeDispenserStates(); - setInterval(() => { + timer = window.setInterval(() => { for (const state of Object.values(dispenserStates)) { + debug('publishing dispenser state, %s', state.guid); cb(state as Message); } }, 1000); @@ -63,18 +77,11 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. } } - // TODO remove the skip validation when the type problem of available_floors is solved - if (topic !== RomiCore.liftStates && topic.topic === 'lift_states') { - const liftStates = fakeLiftStates(); - setInterval(() => { - for (const state of Object.values(liftStates)) { - cb(state as Message); - } - }, 1000); - } - return { - unsubscribe: () => { }, + unsubscribe: () => { + clearInterval(timer); + debug('unsubscribed %s', topic.topic); + }, }; } @@ -94,7 +101,9 @@ export class FakeTransport extends RomiCore.TransportEvents implements RomiCore. throw new Error('not implemented'); } - destroy(): void { } + destroy(): void {} + + private _doorStateFactory: DoorStateFactory; } export default FakeTransport; diff --git a/src/settings.ts b/src/settings.ts index 9f030931b..83e8fe71c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,3 @@ -import React from 'react'; - export enum TrajectoryAnimation { None, Fill, @@ -36,5 +34,3 @@ export function defaultSettings(): Settings { trajectoryAnimationSpeed: AnimationSpeed.Normal, }; } - -export const SettingsContext = React.createContext(defaultSettings()); diff --git a/src/stories/baseComponents/dispenser-panel.tsx b/src/stories/baseComponents/dispenser-panel.tsx deleted file mode 100644 index fba257145..000000000 --- a/src/stories/baseComponents/dispenser-panel.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import { DispenserItem } from '../../components/dispenser-item'; -import { DispenserPanelProps } from '../../components/dispensers-panel'; - -export default function DispenserButton(props: DispenserPanelProps) { - const { dispenserStates } = props; - const dispenserRefs = React.useRef>({}); - - return ( - - {Object.keys(dispenserStates).map(guid => { - const state = dispenserStates[guid]; - return ( - (dispenserRefs.current[state.guid] = ref)} - dispenserState={state} - /> - ); - })} - - ); -} diff --git a/src/stories/baseComponents/door-panel.tsx b/src/stories/baseComponents/door-panel.tsx deleted file mode 100644 index ebd541505..000000000 --- a/src/stories/baseComponents/door-panel.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import DoorItem from '../../components/door-item'; -import { DoorsPanelProps } from '../../components/doors-panel'; - -export default function DoorButton(props: DoorsPanelProps) { - const { doors, doorStates } = props; - const doorRefs = React.useRef>({}); - - return ( - - {doors.map(door => { - const doorState = doorStates[door.name]; - return ( - (doorRefs.current[door.name] = ref)} - door={door} - doorState={doorState} - /> - ); - })} - - ); -} diff --git a/src/stories/baseComponents/lift-panel.tsx b/src/stories/baseComponents/lift-panel.tsx deleted file mode 100644 index 444e397b0..000000000 --- a/src/stories/baseComponents/lift-panel.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { LiftItem } from '../../components/lift-item/lift-item'; -import { LiftsPanelProps } from '../../components/lift-item/lifts-panel'; - -export default function LiftButton(props: LiftsPanelProps) { - const { lifts, liftStates } = props; - const liftRefs = React.useRef>({}); - - return ( - - {lifts.map(lift => { - const liftState = liftStates[lift.name]; - return ( - (liftRefs.current[lift.name] = ref)} - liftState={liftState} - /> - ); - })} - - ); -} diff --git a/src/stories/baseComponents/robot-gallery.tsx b/src/stories/baseComponents/robot-gallery.tsx index 5bb378e93..e750a838b 100644 --- a/src/stories/baseComponents/robot-gallery.tsx +++ b/src/stories/baseComponents/robot-gallery.tsx @@ -3,7 +3,7 @@ import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; import React from 'react'; -import { ResourcesContext } from '../../app-contexts'; +import { ResourcesContext } from '../../components/app-contexts'; import ColorManager from '../../components/schedule-visualizer/colors'; import Robot, { RobotProps } from '../../components/schedule-visualizer/robot'; import { ResourceConfigurationsType } from '../../resource-manager'; diff --git a/src/stories/baseComponents/robot-panel.tsx b/src/stories/baseComponents/robot-panel.tsx deleted file mode 100644 index 201ec30ec..000000000 --- a/src/stories/baseComponents/robot-panel.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -import { RobotItem } from '../../components/robot-item'; -import { RobotsPanelProps } from '../../components/robots-panel'; - -export default function RobotButton(props: RobotsPanelProps) { - const { fleets } = props; - const robotRefs = React.useRef>({}); - - return ( - - {fleets.flatMap(fleet => { - return fleet.robots.map(robot => { - return ( - (robotRefs.current[robot.name] = ref)} - fleetName={fleet.name} - robot={robot} - /> - ); - }); - })} - - ); -} diff --git a/src/stories/design-decision.stories.tsx b/src/stories/design-decision.stories.tsx index 0e2cd5440..3335eb347 100644 --- a/src/stories/design-decision.stories.tsx +++ b/src/stories/design-decision.stories.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { Divider, Typography } from '@material-ui/core'; - -import { StyleTyping, defaultStyles, dispenserStates } from './baseComponents/utils'; -import DispenserButton from './baseComponents/dispenser-panel'; +import React from 'react'; +import DispenserPanel from '../components/dispensers-panel'; +import { defaultStyles, dispenserStates, StyleTyping } from './baseComponents/utils'; export default { title: 'Design Decisions', @@ -27,7 +26,7 @@ export const handleLongName = () => (
- +
); @@ -43,7 +42,7 @@ export const handleUnknown = () => (
- +
); diff --git a/src/stories/dispenser.stories.tsx b/src/stories/dispenser.stories.tsx index 22e135bf7..0afbb8131 100644 --- a/src/stories/dispenser.stories.tsx +++ b/src/stories/dispenser.stories.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Typography } from '@material-ui/core'; -import DispenserButton from './baseComponents/dispenser-panel'; import { dispenserStates, defaultStyles, StyleTyping } from './baseComponents/utils'; +import DispenserPanel from '../components/dispensers-panel'; export default { title: 'Dispenser', @@ -23,6 +23,6 @@ export const dispenserPanel = () => ( Dispenser State Button color and representation - + ); diff --git a/src/stories/door.stories.tsx b/src/stories/door.stories.tsx index caf6e26e2..5f35e1518 100644 --- a/src/stories/door.stories.tsx +++ b/src/stories/door.stories.tsx @@ -1,15 +1,15 @@ -import React from 'react'; import { Divider, Typography } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; - +import React from 'react'; +import DoorsPanel from '../components/doors-panel'; import DoorComponent from './baseComponents/door-component'; -import DoorButton from './baseComponents/door-panel'; + import { + componentDisplayStyle, + defaultStyles, door, doors, doorStates, - componentDisplayStyle, - defaultStyles, StyleTyping, } from './baseComponents/utils'; @@ -126,6 +126,6 @@ export const doorPanel = () => ( Door State Button color and representation - + ); diff --git a/src/stories/lift.stories.tsx b/src/stories/lift.stories.tsx index 84e85c059..744e5fc93 100644 --- a/src/stories/lift.stories.tsx +++ b/src/stories/lift.stories.tsx @@ -1,15 +1,14 @@ -import React from 'react'; import { Divider, Typography } from '@material-ui/core'; import * as RomiCore from '@osrf/romi-js-core-interfaces'; - +import React from 'react'; +import LiftsPanel from '../components/lift-item/lifts-panel'; import LiftComponent from './baseComponents/lift-component'; -import LiftButton from './baseComponents/lift-panel'; import { + componentDisplayStyle, + defaultStyles, lift, lifts, liftStates, - componentDisplayStyle, - defaultStyles, StyleTyping, } from './baseComponents/utils'; @@ -171,6 +170,6 @@ export const liftPanel = () => ( Lift State Button color and representation - + ); diff --git a/src/stories/robot.stories.tsx b/src/stories/robot.stories.tsx index 37d458575..f678fa5f1 100644 --- a/src/stories/robot.stories.tsx +++ b/src/stories/robot.stories.tsx @@ -10,8 +10,8 @@ import { defaultStyles, StyleTyping, } from './baseComponents/utils'; -import RobotButton from './baseComponents/robot-panel'; import RobotGallery from './baseComponents/robot-gallery'; +import RobotsPanel from '../components/robots-panel'; export default { title: 'Robot', @@ -90,7 +90,7 @@ export const robotPanel = () => ( Robot State Button color and representation - + ); diff --git a/src/util/react-helpers.ts b/src/util/react-helpers.ts new file mode 100644 index 000000000..86d8ab151 --- /dev/null +++ b/src/util/react-helpers.ts @@ -0,0 +1,60 @@ +/** + * Helper function to create a callback that returns a map of callbacks from a Record. Meant to be + * used as the callback for `React.useMemo` to create callbacks that match equality test at each + * render. + * + * Given this render + * ```tsx + * {items.map((i) => } + * ``` + * This looks fine at first but this causes problems when using `React.memo` because at each render + * new functions are created which doesn't equal to the previous (functions in js are only equal + * to itself, identical functions are never equal). + * + * The naive approach would be to use `React.useCallback` but that hits another problem, you can't + * use hooks in a callback and the `ListItem` is generated in the `map` callback. + * + * ```tsx + * /// doesn't work! + * {items.map((i) => doSomething(i.value))} />} + * ``` + * + * So then how about using `React.useCallback` outside the `map` function? Then you lost your + * reference to `i`. + * + * ```tsx + * // `i` not defined! + * React.useCallback(() => doSomething(i.value)); + * ``` + * + * So then the solution is to create an array of callbacks, one for each item and wrap them in + * `React.useMemo`, this way the same callback is used each render and each callback can receive + * a reference to the `i` object. + * + * ```tsx + * // ok! + * React.useMemo(() => items.map((i) => callback(i, ...onClickArgs))); + * ``` + * + * @example + * ```ts + * const onClick = React.useMemo( + * makeCallbackMapCallback['onClick']>( + * doorsMap, + * (_, door) => onDoorClick && onDoorClick(door), + * ), + * [doorsMap, onDoorClick], + * ); + * ``` + * + * @param arr + * @param callback + */ +export function makeCallbackArrayCallback< + U extends (...args: any[]) => any = (...args: any[]) => any, + T = any, + Args extends any[] = U extends (...args: infer Args) => any ? Args : never, + R = U extends (...args: any[]) => infer R ? R : never +>(arr: T[], callback: (item: T, ...args: Args) => R): () => ((...args: Args) => R)[] { + return () => arr.map(item => (...args: Args) => callback(item, ...args)); +}