Created
December 9, 2019 07:04
-
-
Save FallOutChonny/f2b1a5328c9854c0c7329c74cd3f7e6f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react' | |
import { DrawingManager, Rectangle } from '@react-google-maps/api' | |
import { useHistory } from 'react-router-dom' | |
import { isNil, length } from 'ramda' | |
import theme from 'shared/theme' | |
import { OverlayType } from 'shared/constants/types' | |
import { isEmpty } from 'shared/utils/validation' | |
import message from 'shared/utils/message' | |
import { useStreetLights, StreetLight } from 'shared/graphql/streetLight' | |
import { | |
useDistrictClusters, | |
Marker, | |
DistrictInfo, | |
useAllMarkers, | |
} from 'shared/graphql/dashboard' | |
import { Alarm, AlarmModes } from 'shared/graphql/alarm' | |
import useModalVisible from 'shared/hooks/useModalVisible' | |
import useSubscribeAlarm from 'shared/hooks/useSubscribeAlarm' | |
import GoogleMap from 'shared/components/GoogleMap' | |
import MapMarker from 'shared/components/MapMarker' | |
import Box from 'shared/components/Box' | |
import { useAlarmDrawer } from 'src/hooks/useAlarmDrawer' | |
import useGoogleMap from 'src/hooks/useGoogleMap' | |
import { STREETLIGHT_MANAGEMENT } from 'src/constants/routes' | |
import SearchDrawer from './SearchDrawer' | |
import AlarmDrawer from './AlarmDrawer' | |
import IconSearch from './IconSearch' | |
import DrawingControl from './DrawingControl' | |
import LightDrawer from './LightDrawer' | |
import DistrictMarker from './DistrictMarker' | |
// const randomMarkers = Array.from({ length: 500 }).map((_, idx) => ({ | |
// id: idx, | |
// deviceId: idx, | |
// ...getRandomMarkers(24.96251557085913, 121.2618106456357, 1000), | |
// isLight: true, | |
// })) | |
// console.log(randomMarkers) | |
function Dashboard() { | |
const { | |
alarMode, | |
isClusterMode, | |
setIsClusterMode, | |
handleToggleAlarmDrawer, | |
handleCloseAlarmDrawer, | |
handleOpenAlarmDrawer, | |
} = useAlarmDrawer() | |
const history = useHistory() | |
const { clusterOfDists, loading: loadingCluster } = useDistrictClusters() | |
const { dataSource, loading, refetch } = useAllMarkers() | |
// const [queryMarker, { querying }] = useQueryMarker() | |
const [toggleMarker, setToggleMarker] = useState<Marker | Alarm | null>(null) | |
const [drawingMode, setDrawingMode] = useState<OverlayType>() | |
const [center, setMapCenter] = useState<google.maps.LatLngLiteral | null>( | |
null, | |
) | |
const [rectangle, setRectangle] = useState<google.maps.Rectangle | null>() | |
const [markersInRect, setMarkersInRect] = React.useState< | |
Array<Marker | StreetLight> | |
>([]) | |
// const { dataSource, loading, refetch } = useStreetLights({ | |
// pageSize: 100, | |
// }) | |
const [drawerVisible, handleDrawerVisible] = useModalVisible() | |
const { handleMapLoad, map } = useGoogleMap({ | |
onMapLoad: (map: google.maps.Map) => { | |
// map.addListener('', () => {}) | |
}, | |
}) | |
const handleZoomChange = () => { | |
console.log(map?.getZoom()) | |
} | |
const handleMapClick = (evt: google.maps.MouseEvent) => { | |
console.log(evt.latLng.lat()) | |
console.log(evt.latLng.lng()) | |
} | |
const handleToLightManagement = (id: number) => { | |
history.push(`${STREETLIGHT_MANAGEMENT}?id=${id}`) | |
} | |
const handleRectangleComplete = (rectangle: google.maps.Rectangle) => { | |
const markersInRect = dataSource.content | |
.filter(x => !isNil(x.lat) && !isNil(x.lon)) | |
.filter(x => rectangle.getBounds().contains({ lat: x.lat, lng: x.lon })) | |
const count = length(markersInRect) | |
const isTooMany = count > 100 | |
const isZero = count === 0 | |
if (isTooMany) { | |
message({ content: '框選路燈數需低於100盞' }) | |
} | |
if (!isTooMany && !isZero) { | |
setRectangle(rectangle) | |
setDrawingMode(undefined) | |
setMarkersInRect(markersInRect) | |
} | |
rectangle.setMap(null) | |
} | |
const handleRectModeClick = () => { | |
if (!drawingMode) { | |
setRectangle(null) | |
setIsClusterMode(false) | |
setDrawingMode(OverlayType.RECTANGLE) | |
handleCloseAlarmDrawer() | |
} else { | |
setDrawingMode(undefined) | |
if (alarMode !== AlarmModes.NORMAL) { | |
handleOpenAlarmDrawer() | |
} | |
} | |
} | |
const handleDistMarkerClick = (data: DistrictInfo) => { | |
refetch({ | |
variables: { | |
params: { | |
sessionIds: data.id, | |
}, | |
}, | |
}) | |
handleToggleAlarmDrawer(data.type) | |
setMapCenter({ lat: data.lat, lng: data.lng }) | |
if (map) { | |
map.setZoom(15) | |
} | |
} | |
const handleAlarmClick = (item: Alarm) => { | |
if (!item.lat || !item.lon) { | |
message({ content: '此警報內容的路燈尚未設定地理座標!' }) | |
return | |
} | |
setMapCenter({ lat: item.lat, lng: item.lon }) | |
setToggleMarker(item) | |
if (isClusterMode) { | |
setIsClusterMode(false) | |
} | |
if (map) { | |
map.setZoom(15) | |
} | |
} | |
const handleSearch = (values: any) => { | |
refetch(values) | |
handleDrawerVisible(false) | |
} | |
useSubscribeAlarm((alarm: Alarm) => { | |
const handleClick = (evt: React.MouseEvent<HTMLElement>) => { | |
hide() | |
handleAlarmClick(alarm) | |
} | |
const hide = message({ | |
closable: true, | |
content: ( | |
<Box | |
display="flex" | |
justifyContent="space-between" | |
flex="1" | |
className="mr-30"> | |
<Box className="text-darkGrey"> | |
<span>{alarm.remark}</span> | |
<span className="ml-12">{alarm.address}</span> | |
<span className="ml-20 text-sm">{alarm.createTimeStr}</span> | |
</Box> | |
<Box | |
className="text-info900 text-underline cursor--pointer" | |
onClick={handleClick}> | |
查看詳情 | |
</Box> | |
</Box> | |
), | |
type: 'error', | |
duration: 15, | |
}) | |
}) | |
return ( | |
<> | |
<GoogleMap | |
zoom={12} | |
{...(center ? { center } : {})} | |
loading={loading || loadingCluster} | |
onMapLoad={handleMapLoad} | |
onZoomChanged={handleZoomChange} | |
onClick={handleMapClick}> | |
<Box left={0} top={0} position="absolute"> | |
<IconSearch | |
onClick={handleDrawerVisible} | |
{...(drawerVisible ? { style: { left: 276 } } : {})} | |
/> | |
<DrawingControl | |
onClick={handleRectModeClick} | |
drawingMode={!!drawingMode} | |
/> | |
</Box> | |
<DrawingManager | |
drawingMode={drawingMode} | |
options={drawingManagerOptions} | |
onRectangleComplete={handleRectangleComplete} | |
/> | |
{rectangle && ( | |
<LightDrawer | |
visible | |
// loading={querying} | |
dataSource={markersInRect as StreetLight[]} | |
/> | |
)} | |
{rectangle && ( | |
<Rectangle | |
bounds={rectangle.getBounds()} | |
options={rectangleOptions} | |
/> | |
)} | |
{isClusterMode && | |
clusterOfDists.map((x: DistrictInfo) => ( | |
<DistrictMarker | |
key={x.id} | |
position={{ lat: x.lat, lng: x.lng }} | |
data={{ ...x, type: alarMode }} | |
onClick={handleDistMarkerClick} | |
/> | |
))} | |
{!isClusterMode && | |
dataSource.content | |
.filter(x => !isEmpty(x.lat) && !isEmpty(x.lon)) | |
.map(item => ( | |
<MapMarker | |
isLight | |
key={item.deviceId} | |
item={item} | |
position={{ lat: item.lat, lng: item.lon }} | |
toggle={toggleMarker?.deviceId === item.deviceId} | |
alarMode={alarMode} | |
onToLightManagement={handleToLightManagement} | |
visible={!isClusterMode} | |
map={map as NonNullable<google.maps.Map>} | |
/> | |
))} | |
<SearchDrawer | |
onClose={handleDrawerVisible} | |
visible={drawerVisible} | |
onSearch={handleSearch} | |
/> | |
<AlarmDrawer onClick={handleAlarmClick} /> | |
</GoogleMap> | |
</> | |
) | |
} | |
const rectangleOptions = { | |
fillColor: theme.info200, | |
strokeColor: theme.info, | |
strokeWeight: 1, | |
} | |
const drawingManagerOptions = { | |
drawingControl: false, | |
rectangleOptions, | |
} | |
export default React.memo(Dashboard) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { merge, isNil, pathOr } from 'ramda' | |
import { Spin } from 'antd' | |
import cx from 'classnames' | |
import styled from 'styled-components' | |
import { | |
Marker as MarkerComponent, | |
InfoBox, | |
MarkerProps, | |
} from '@react-google-maps/api' | |
import { imageUrlPrefix } from '../env' | |
import { StreetLight } from '../graphql/streetLight' | |
import { AlarmModes } from '../graphql/alarm' | |
import useModalVisible from '../hooks/useModalVisible' | |
import usePrevious from '../hooks/usePrevious' | |
// import useGoogleMap from '../hooks/useGoogleMap' | |
import { getDeviceType } from '../constants/types' | |
import Slider from './Slider' | |
import Button from './Button' | |
import Icon from './Icon' | |
import Box from './Box' | |
import { useQueryMarker } from 'shared/graphql/dashboard' | |
type MapMarkerProps<T> = MarkerProps & { | |
item?: T | any | |
toggle?: boolean | |
alarMode?: AlarmModes | |
isLight?: boolean | |
isBuilding?: boolean | |
map?: google.maps.Map | |
onInfoBoxClick?: () => any | |
onToLightManagement?: (id: number) => any | |
} | |
// NOTE: Debug時設成false | |
const options = { optimized: true } | |
let openedInfoBox: InfoBox[] = [] | |
export default function MapMarker<T = StreetLight>({ | |
toggle = false, | |
item, | |
position, | |
alarMode, | |
map, | |
onInfoBoxClick, | |
onToLightManagement, | |
isLight, | |
isBuilding, | |
...props | |
}: MapMarkerProps<T>) { | |
const [infoBoxVisible, handleInfoBoxVisible, _handleClose] = useModalVisible() | |
const prev = usePrevious(toggle) | |
let ref = React.useRef<InfoBox | null>(null) | |
React.useEffect(() => { | |
if (!!prev !== !!toggle) { | |
const _toggle = !!toggle | |
if (_toggle) { | |
handleInfoBoxVisible() | |
} | |
if (!_toggle) { | |
_handleClose() | |
} | |
} | |
}, [toggle, prev]) | |
const [queryMarker, { loading, data }] = useQueryMarker() | |
const { icon } = getDeviceType(item) | |
const handleToggle = () => { | |
// 先關掉當前打開中的 InfoBox | |
openedInfoBox.forEach((x: any) => x.close()) | |
if (!infoBoxVisible) { | |
console.log(item) | |
queryMarker({ variables: { params: { deviceIds: item.deviceId } } }) | |
} | |
handleInfoBoxVisible() | |
} | |
const handleClose = (evt?: React.MouseEvent<HTMLElement>) => { | |
handleInfoBoxVisible() | |
} | |
const handleConfirm = () => { | |
handleClose() | |
if (onInfoBoxClick) { | |
onInfoBoxClick() | |
} | |
} | |
const handleInfoBoxLoad = (instance: any) => { | |
openedInfoBox.push(instance) | |
} | |
const handleDomReady = () => { | |
if (ref.current) { | |
ref.current.containerElement?.addEventListener('mouseup', evt => { | |
map?.setOptions({ gestureHandling: 'auto' }) | |
}) | |
ref.current.containerElement?.addEventListener('mousedown', evt => { | |
map?.setOptions({ gestureHandling: 'none' }) | |
}) | |
} | |
} | |
const handleToLightManagement = (evt: React.MouseEvent<HTMLElement>) => { | |
if (onToLightManagement) { | |
onToLightManagement(item.id) | |
} | |
} | |
let pixelOffset = React.useMemo(() => new google.maps.Size(20, -200), [item]) | |
let className = React.useMemo( | |
() => | |
cx({ | |
'pb-8': item.isSwitchBox, | |
}), | |
[item], | |
) | |
let infoboxStyle = merge( | |
{}, | |
{ | |
...(item.isBuilding | |
? { | |
width: 324, | |
background: item.isBuilding ? '#fffcf1' : '#fff', | |
padding: '8px 16px 56px 24px', | |
} | |
: {}), | |
}, | |
) | |
const status = React.useMemo(() => { | |
const isAlarm = alarMode === AlarmModes.ALARM | |
const isAbnormal = alarMode === AlarmModes.ABNORMAL | |
const isNormal = isNil(alarMode) || alarMode === AlarmModes.NORMAL | |
return { | |
isAlarm, | |
isAbnormal, | |
isNormal, | |
message: isAlarm ? '警報狀態' : isAbnormal ? '維修中' : '正常 (光度 xx%)', | |
} | |
}, [alarMode]) | |
return ( | |
<> | |
<MarkerComponent | |
position={position} | |
icon={`${imageUrlPrefix}/icon-${icon}.svg`} | |
options={options} | |
onClick={handleToggle} | |
{...props}> | |
{infoBoxVisible && ( | |
<InfoBox | |
ref={ref as any} | |
onLoad={handleInfoBoxLoad} | |
position={position} | |
onDomReady={handleDomReady} | |
options={{ | |
pixelOffset, | |
visible: true, | |
alignBottom: false, | |
disableAutoPan: false, | |
enableEventPropagation: true, | |
}}> | |
<Spin spinning={loading}> | |
<InfoBoxWrapper style={infoboxStyle} className={className}> | |
<CloseButton type="close" onClick={handleClose} /> | |
{isLight && ( | |
<> | |
<Box display="flex"> | |
<Block className="mr-4 w144 text-white bg-secondary"> | |
{/* 路燈編號 {item.lightId} */} | |
路燈編號 {pathOr('', ['deviceName'], data)} | |
</Block> | |
<Block | |
className={cx('w144 text-white', { | |
'bg-crimson': status.isAlarm, | |
'bg-grey': status.isAbnormal, | |
'bg-warning': status.isNormal, | |
})}> | |
{status.message} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="w292 pt-14 pb-13"> | |
<Box | |
display="flex" | |
alignItems="center" | |
justifyContent="cneter"> | |
<Box | |
display="inline-block" | |
className="text-info text-500"> | |
調光 | |
</Box> | |
<Box display="inline-block"> | |
<Slider | |
min={0} | |
max={130} | |
style={{ width: 130 }} | |
className="is--small" | |
defaultValue={pathOr(0, ['brightness'], data)} | |
/> | |
</Box> | |
<Box className="text-info text-500">100 W/m</Box> | |
</Box> | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="mr-4 w144 bg--light"> | |
{/* 燈具編號: {item.lightId} */} | |
燈具編號 {pathOr('', ['lightNo'], data)} | |
</Block> | |
<Block className="w144 bg--light"> | |
{/* 燈桿編號 {item.lightId} */} | |
燈具編號 {pathOr('', ['lightNo'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="w292 bg--light"> | |
地址: {pathOr('', ['address'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="w292 bg--warning"> | |
位置: {pathOr('', ['lat'], data)}, | |
{pathOr('', ['lon'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="mr-4 w144 bg--light"> | |
燈具種類: {pathOr('', ['lampType'], data)} | |
</Block> | |
<Block className="w144 bg--light"> | |
燈泡瓦特數: {pathOr('', ['lampWatt'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="mr-4 w144 bg--warning"> | |
方位: {item.direction} | |
</Block> | |
<Block className="w144 bg--warning"> | |
耗電瓦特數: {item.watt} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="mr-4 w144 bg--light"> | |
電壓: {pathOr('', ['voltageOut'], data)} | |
</Block> | |
<Block className="w144 bg--light"> | |
電流: {pathOr('', ['currentOut'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="w292 bg--warning"> | |
控制器編號: {pathOr('', ['controllerId'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="mr-4 w144 bg--light"> | |
燈桿類型:{pathOr('', ['poleType'], data)} | |
</Block> | |
<Block className="w144 bg--light"> | |
開關箱: {pathOr('', ['switchBoxId'], data)} | |
</Block> | |
</Box> | |
<Box display="flex"> | |
<Block className="w292 bg--warning"> | |
累計點燈時間: {pathOr('', ['lampOnByMonth'], data)} | |
</Block> | |
</Box> | |
<Box | |
className="text-sm text-primary cursor--pointer mt-12 ml-6" | |
onClick={handleToLightManagement}> | |
看更多 >> | |
</Box> | |
</> | |
)} | |
{isBuilding && ( | |
<div> | |
<div> | |
<img | |
src={`${imageUrlPrefix}/icon-building.svg`} | |
alt="building-icon" | |
/> | |
<div className="mb-8 ml-8 text-larger d-inline-block"> | |
{item.address} | |
</div> | |
</div> | |
<div className="mb-26 ml-26 text-sm"> | |
{(position as google.maps.LatLng).lat()} /{' '} | |
{(position as google.maps.LatLng).lng()} | |
</div> | |
<Button | |
color="primary" | |
className="pull-right" | |
style={{ minWidth: 80 }} | |
onClick={handleConfirm}> | |
確認 | |
</Button> | |
</div> | |
)} | |
</InfoBoxWrapper> | |
</Spin> | |
</InfoBox> | |
)} | |
</MarkerComponent> | |
</> | |
) | |
} | |
const InfoBoxWrapper = styled.div` | |
width: 310px; | |
padding: 0 8px 40px 8px; | |
background: #fff; | |
border-radius: 4px; | |
border: 1px solid ${p => p.theme.primary}; | |
font-size: 12px; | |
font-weight: 500; | |
color: ${p => p.theme.darkGrey}; | |
&.is--large { | |
width: 480px; | |
} | |
.bg--warning { | |
background: ${p => p.theme.blue300}; | |
} | |
.bg--light { | |
background: ${p => p.theme.blue200}; | |
} | |
` | |
const Block = styled.div` | |
padding: 5px 8px; | |
` | |
const CloseButton = styled(Icon)` | |
display: flex; | |
justify-content: flex-end; | |
margin-top: 9px; | |
margin-bottom: 9px; | |
font-size: 18px; | |
color: ${p => p.theme.info}; | |
cursor: pointer; | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment