import React, { useState, useRef, useEffect, useMemo } from "react";
|
import { useTranslate, useRefresh, useSidebarState } from "react-admin";
|
import {
|
Select,
|
MenuItem,
|
Button,
|
Box,
|
useTheme,
|
Fab,
|
} from '@mui/material';
|
import { MAP_MODE, MAP_DEFAULT_ROTATION } from "./constants";
|
import Player from './player';
|
import * as Tool from './tool';
|
import { NotificationProvider, useNotification } from './Notification';
|
import Insight from "./insight";
|
import Device from "./Device";
|
import Settings from "./settings";
|
import Batch from "./batch";
|
import * as Http from './http';
|
import WebSocketClient from './websocket'
|
import ConfirmButton from "../page/components/ConfirmButton";
|
import { FitScreen, RotateRight } from '@mui/icons-material';
|
import MapSearch from "./header/MapSearch";
|
import { startupOrShutdown } from "./http";
|
import PulseSignal from "../page/components/PulseSignal";
|
import FakeFab from "./header/FakeFab";
|
import RouteFab from "./header/RouteFab";
|
|
let player;
|
let websocket;
|
|
const Map = () => {
|
const notify = useNotification();
|
const [sidebarOpen] = useSidebarState();
|
const translate = useTranslate();
|
const theme = useTheme();
|
const themeMode = theme.palette.mode;
|
|
const mapRef = useRef();
|
const contentRef = useRef();
|
const [app, setApp] = useState(null);
|
const [mapContainer, setMapContainer] = useState(null);
|
|
const [mode, setMode] = useState(MAP_MODE.OBSERVER_MODE);
|
const modeRef = useRef(mode);
|
const [dataFetched, setDataFetched] = useState(false);
|
const [insightVisible, setInsightVisible] = useState(false);
|
const [deviceVisible, setDeviceVisible] = useState(false);
|
const [settingsVisible, setSettingsVisible] = useState(false);
|
const [batchSelectionVisible, setBatchSelectionVisible] = useState(false);
|
|
const [curSprite, setCurSprite] = useState(null);
|
const [batchSprites, setBatchSprites] = useState([]);
|
|
const [rcsStatus, setRcsStatus] = useState(null);
|
const [showRoutes, setShowRoutes] = useState(false);
|
const [curZone, setCurZone] = useState(() => {
|
const storedValue = localStorage.getItem('curZone');
|
return storedValue !== null ? JSON.parse(storedValue) : null;
|
});
|
|
const handleResize = () => {
|
if (!contentRef) { return; }
|
const width = contentRef.current.offsetWidth;
|
const height = contentRef.current.offsetHeight;
|
player.resize(width, height);
|
};
|
|
useEffect(() => {
|
Tool.patchRaLayout('0px');
|
const initialize = async () => {
|
player = new Player(mapRef.current, themeMode);
|
setApp(player.app);
|
setMapContainer(player.mapContainer);
|
Tool.setApp(player.app);
|
Tool.setMapContainer(player.mapContainer);
|
Tool.setThemeMode(themeMode);
|
Http.setNotify(notify);
|
Http.setMapContainer(player.mapContainer);
|
websocket = new WebSocketClient('/ws/map/websocket');
|
|
await Http.fetchMapData(curZone, setRcsStatus, setCurSprite);
|
websocket.connect();
|
websocket.onMessage = (wsMsg) => {
|
if (modeRef.current === MAP_MODE.OBSERVER_MODE) {
|
Tool.generateDynamicGraphic(curZone, JSON.parse(wsMsg), setCurSprite);
|
}
|
}
|
|
player.rotateMap(localStorage.getItem('mapRotation') || Tool.rotationParseNum(MAP_DEFAULT_ROTATION));
|
setTimeout(() => {
|
notify.info(translate('page.map.welcome'));
|
player.adaptScreen();
|
setDataFetched(true);
|
}, 200)
|
}
|
initialize();
|
|
// resize
|
handleResize();
|
window.addEventListener('resize', handleResize);
|
|
return () => {
|
if (websocket) {
|
websocket.onMessage = () => { }
|
websocket.close();
|
}
|
player.destroy();
|
window.removeEventListener('resize', handleResize);
|
Tool.patchRaLayout('');
|
};
|
}, [])
|
|
useEffect(() => {
|
setTimeout(handleResize, 300)
|
}, [sidebarOpen]);
|
|
useEffect(() => {
|
player.setTheme(themeMode);
|
Tool.setThemeMode(themeMode);
|
}, [themeMode])
|
|
const switchMode = (mode) => {
|
modeRef.current = mode;
|
|
Tool.removeSelectedEffect();
|
player.hideGridLines();
|
|
setInsightVisible(false);
|
setDeviceVisible(false);
|
setSettingsVisible(false);
|
setBatchSelectionVisible(false);
|
|
setCurSprite(null);
|
setBatchSprites([]);
|
|
switch (mode) {
|
case MAP_MODE.OBSERVER_MODE:
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
Tool.multipleSelectEnhancer(selectedSprites, setCurSprite, setBatchSprites);
|
});
|
|
mapContainer.children.forEach(child => {
|
Tool.beInsight(child, setCurSprite);
|
})
|
break
|
case MAP_MODE.MOVABLE_MODE:
|
player.showGridLines();
|
Tool.hideRoutes(curZone, setShowRoutes);
|
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
Tool.spriteListBeMovable(selectedSprites, () => {
|
restartFn();
|
});
|
});
|
|
mapContainer.children.forEach(child => {
|
Tool.beMovable(child);
|
})
|
break
|
case MAP_MODE.SETTINGS_MODE:
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
Tool.multipleSelectEnhancer(selectedSprites, setCurSprite, setBatchSprites);
|
});
|
|
mapContainer.children.forEach(child => {
|
Tool.beSettings(child, setCurSprite);
|
})
|
break
|
default:
|
break
|
}
|
}
|
|
useEffect(() => {
|
if (!mapContainer) {
|
return
|
}
|
switchMode(mode);
|
}, [mode, mapContainer]);
|
|
const onDrop = (sprite, type, x, y) => {
|
const { mapX, mapY } = Tool.getRealPosition(x, y);
|
sprite.x = mapX;
|
sprite.y = mapY;
|
|
// sprite.scale.set(mapContainer.scale.x);
|
sprite.rotation = -mapContainer.rotation;
|
|
Tool.initSprite(sprite, type);
|
mapContainer.addChild(sprite);
|
Tool.beMovable(sprite);
|
};
|
|
// watch curSprite
|
React.useEffect(() => {
|
if (!mapContainer) {
|
return;
|
}
|
Tool.removeSelectedEffect();
|
if (curSprite) {
|
if (mode === MAP_MODE.OBSERVER_MODE) {
|
Tool.showSelectedEffect(curSprite);
|
setInsightVisible(true);
|
}
|
if (mode === MAP_MODE.SETTINGS_MODE) {
|
Tool.showSelectedEffect(curSprite);
|
setSettingsVisible(true);
|
}
|
} else {
|
Tool.removeSelectedEffect();
|
setInsightVisible(false);
|
setSettingsVisible(false);
|
}
|
}, [curSprite]);
|
|
// watch batchSprites
|
React.useEffect(() => {
|
if (!mapContainer) {
|
return;
|
}
|
if (batchSprites?.length > 0) {
|
setBatchSelectionVisible(true)
|
} else {
|
player.clearSelectedSprites();
|
setBatchSelectionVisible(false)
|
}
|
}, [batchSprites])
|
|
return (
|
<Box
|
sx={{
|
width: '100%',
|
height: '100%',
|
display: 'flex',
|
flexDirection: 'column',
|
}}
|
>
|
{/* header */}
|
<Box
|
sx={{
|
display: 'flex',
|
alignItems: 'center',
|
backgroundColor: theme.palette.background.default,
|
color: theme.palette.text.primary,
|
padding: '0 16px',
|
height: '64px',
|
flexShrink: 0, // keep height
|
zIndex: 200,
|
boxShadow: theme.shadows[1],
|
}}
|
>
|
<MapSearch
|
mode={mode}
|
setMode={setMode}
|
dataFetched={dataFetched}
|
curZone={curZone}
|
curSprite={curSprite}
|
setCurSprite={setCurSprite}
|
/>
|
<Box sx={{ flexGrow: 1 }} />
|
|
{mode === MAP_MODE.OBSERVER_MODE && (
|
<>
|
<Box sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>
|
<PulseSignal
|
negative
|
negativeColor='#a4b0be'
|
flag={rcsStatus}
|
width={12}
|
/>
|
</Box>
|
<Button
|
variant="contained"
|
color={rcsStatus ? 'inherit' : 'primary'}
|
sx={{ mr: 2 }}
|
onClick={() => {
|
startupOrShutdown(() => {
|
setRcsStatus(!rcsStatus);
|
});
|
}}
|
>
|
{rcsStatus ? translate('page.map.action.shutdown') : translate('page.map.action.startup')}
|
</Button>
|
<Button variant="contained" color="primary">
|
{translate('page.map.action.monitor')}
|
</Button>
|
</>
|
)}
|
|
{mode === MAP_MODE.MOVABLE_MODE && (
|
<>
|
<Button
|
variant="outlined"
|
sx={{ mr: 2 }}
|
onClick={() => {
|
Tool.clearMapData();
|
}}
|
>
|
{translate('page.map.action.clear')}
|
</Button>
|
<ConfirmButton
|
label="page.map.action.save"
|
variant="contained"
|
sx={{ mr: 2 }}
|
onConfirm={() => {
|
Http.saveMapData(curZone);
|
}}
|
/>
|
<Button
|
variant="contained"
|
color="primary"
|
onClick={() => setDeviceVisible(!deviceVisible)}
|
>
|
{translate('page.map.devices.title')}
|
</Button>
|
</>
|
)}
|
|
{mode === MAP_MODE.SETTINGS_MODE && (
|
<>
|
<Button
|
variant="outlined"
|
sx={{ mr: 2 }}
|
onClick={() => {
|
Tool.clearMapData();
|
}}
|
>
|
{translate('page.map.action.clear')}
|
</Button>
|
<ConfirmButton
|
label="page.map.action.save"
|
variant="contained"
|
onConfirm={() => {
|
Http.saveMapData(curZone);
|
}}
|
/>
|
</>
|
)}
|
|
<Select
|
value={mode ?? ''}
|
onChange={(event) => {
|
setMode(event.target.value);
|
}}
|
variant="outlined"
|
size="small"
|
sx={{
|
ml: 2,
|
backgroundColor: theme.palette.background.paper,
|
color: theme.palette.text.primary,
|
borderRadius: 1,
|
}}
|
>
|
<MenuItem value={MAP_MODE.OBSERVER_MODE}>{translate('page.map.mode.observer')}</MenuItem>
|
<MenuItem value={MAP_MODE.MOVABLE_MODE}>{translate('page.map.mode.movable')}</MenuItem>
|
<MenuItem value={MAP_MODE.SETTINGS_MODE}>{translate('page.map.mode.settings')}</MenuItem>
|
</Select>
|
</Box>
|
{/* content */}
|
<Box
|
sx={{
|
flexGrow: 1, // fill remaining of map space
|
position: 'relative',
|
backgroundColor: '#fff',
|
...(mode === MAP_MODE.SETTINGS_MODE && {
|
animation: 'settingsPulse 1.5s infinite',
|
'@keyframes settingsPulse': {
|
'0%': {
|
boxShadow: `0 0 3px 1px ${theme.palette.primary.main.replace('rgb', 'rgba').replace(')', `, 0.1)`)}`,
|
},
|
'50%': {
|
boxShadow: `0 0 8px 3px ${theme.palette.primary.main.replace('rgb', 'rgba').replace(')', `, 0.5)`)}`,
|
},
|
'100%': {
|
boxShadow: `0 0 3px 1px ${theme.palette.primary.main.replace('rgb', 'rgba').replace(')', `, 0.1)`)}`,
|
},
|
},
|
})
|
}}
|
>
|
<Box
|
ref={contentRef}
|
sx={{
|
position: 'relative',
|
width: '100%',
|
height: '100%',
|
backgroundColor: '#e0e0e0',
|
overflowY: 'hidden',
|
}}
|
>
|
<div ref={mapRef} style={{
|
position: 'absolute',
|
top: 0,
|
left: 0,
|
width: '100%',
|
height: '100%',
|
}} />
|
</Box>
|
|
<Box
|
sx={{
|
position: 'absolute',
|
left: 35,
|
bottom: 30,
|
display: 'flex',
|
flexDirection: 'column',
|
gap: 2
|
}}
|
>
|
{mode !== MAP_MODE.MOVABLE_MODE && (
|
<>
|
<RouteFab
|
curZone={curZone}
|
showRoutes={showRoutes}
|
setShowRoutes={setShowRoutes}
|
notify={notify}
|
/>
|
<FakeFab />
|
</>
|
)}
|
<Fab
|
variant="extended"
|
color="primary"
|
size="small"
|
onClick={() => {
|
player.rotateMap(Math.PI / 2);
|
}}
|
>
|
<RotateRight />
|
{translate('page.map.action.rotate')}
|
</Fab>
|
<Fab
|
variant="extended"
|
color="primary"
|
size="small"
|
onClick={() => {
|
player.adaptScreen();
|
}}
|
>
|
<FitScreen />
|
{translate('page.map.action.adapt')}
|
</Fab>
|
</Box>
|
</Box>
|
|
<Insight
|
open={insightVisible}
|
onCancel={() => {
|
setCurSprite(null);
|
}}
|
sprite={curSprite}
|
width={570}
|
/>
|
|
<Device
|
open={deviceVisible}
|
onCancel={() => {
|
setDeviceVisible(false);
|
}}
|
onDrop={onDrop}
|
width={378}
|
/>
|
|
<Settings
|
open={settingsVisible}
|
onCancel={() => {
|
setCurSprite(null);
|
}}
|
sprite={curSprite}
|
setSpriteSettings={setCurSprite}
|
width={570}
|
/>
|
|
<Batch
|
open={batchSelectionVisible}
|
onCancel={() => {
|
setBatchSprites([]);
|
}}
|
batchSprites={batchSprites}
|
mode={mode}
|
width={570}
|
/>
|
|
</Box>
|
);
|
}
|
|
const MapPage = () => {
|
return (
|
<NotificationProvider>
|
<Map />
|
</NotificationProvider>
|
)
|
}
|
|
export default MapPage;
|