aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modern/public/styles.css4
-rw-r--r--modern/src/common/formatter.js22
-rw-r--r--modern/src/map/PositionsMap.js11
-rw-r--r--modern/src/map/SelectedDeviceMap.js50
-rw-r--r--modern/src/map/StatusView.js170
-rw-r--r--modern/src/theme/palette.js11
6 files changed, 210 insertions, 58 deletions
diff --git a/modern/public/styles.css b/modern/public/styles.css
index e7aee86..147c3e3 100644
--- a/modern/public/styles.css
+++ b/modern/public/styles.css
@@ -10,3 +10,7 @@ canvas {
.mapboxgl-ctrl {
margin: 10px;
}
+
+.maplibregl-popup {
+ max-width: 330px !important;
+}
diff --git a/modern/src/common/formatter.js b/modern/src/common/formatter.js
index 3c0341b..f0bb2e1 100644
--- a/modern/src/common/formatter.js
+++ b/modern/src/common/formatter.js
@@ -98,3 +98,25 @@ export const formatCoordinate = (key, value, unit) => {
return `${value.toFixed(6)}°`;
}
};
+
+export const getStatusColor = (status) => {
+ switch (status) {
+ case 'online':
+ return 'green';
+ case 'offline':
+ return 'red';
+ case 'unknown':
+ default:
+ return 'gray';
+ }
+};
+
+export const getBatteryStatus = (batteryLevel) => {
+ if (batteryLevel >= 70) {
+ return 'green';
+ }
+ if (batteryLevel > 30) {
+ return 'gray';
+ }
+ return 'red';
+};
diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js
index 2de01d2..9719b45 100644
--- a/modern/src/map/PositionsMap.js
+++ b/modern/src/map/PositionsMap.js
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import ReactDOM from 'react-dom';
+import { ThemeProvider } from '@material-ui/core/styles';
import maplibregl from 'maplibre-gl';
import { Provider, useSelector } from 'react-redux';
@@ -7,6 +8,7 @@ import { useHistory } from 'react-router-dom';
import { map } from './Map';
import store from '../store';
import StatusView from './StatusView';
+import theme from '../theme';
const PositionsMap = ({ positions }) => {
const id = 'positions';
@@ -49,7 +51,14 @@ const PositionsMap = ({ positions }) => {
const placeholder = document.createElement('div');
ReactDOM.render(
<Provider store={store}>
- <StatusView deviceId={feature.properties.deviceId} onShowDetails={(positionId) => history.push(`/position/${positionId}`)} />
+ <ThemeProvider theme={theme}>
+ <StatusView
+ deviceId={feature.properties.deviceId}
+ onShowDetails={(positionId) => history.push(`/position/${positionId}`)}
+ onShowHistory={() => history.push('/replay')}
+ onEditClick={(deviceId) => history.push(`/device/${deviceId}`)}
+ />
+ </ThemeProvider>
</Provider>,
placeholder,
);
diff --git a/modern/src/map/SelectedDeviceMap.js b/modern/src/map/SelectedDeviceMap.js
index e6c5f58..e55ee10 100644
--- a/modern/src/map/SelectedDeviceMap.js
+++ b/modern/src/map/SelectedDeviceMap.js
@@ -1,21 +1,63 @@
-import { useEffect } from 'react';
-import { useSelector } from 'react-redux';
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import { ThemeProvider } from '@material-ui/core/styles';
+import maplibregl from 'maplibre-gl';
+import { Provider, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
import { map } from './Map';
+import store from '../store';
+import StatusView from './StatusView';
+import theme from '../theme';
+
+let popup;
const SelectedDeviceMap = () => {
+ const history = useHistory();
+
const mapCenter = useSelector((state) => {
if (state.devices.selectedId) {
const position = state.positions.items[state.devices.selectedId] || null;
if (position) {
- return [position.longitude, position.latitude];
+ return { deviceId: state.devices.selectedId, position: [position.longitude, position.latitude] };
}
}
return null;
});
+ const showStatus = (deviceId, coordinates) => {
+ const placeholder = document.createElement('div');
+ ReactDOM.render(
+ <Provider store={store}>
+ <ThemeProvider theme={theme}>
+ <StatusView
+ deviceId={deviceId}
+ onShowDetails={(positionId) => history.push(`/position/${positionId}`)}
+ onShowHistory={() => history.push('/replay')}
+ onEditClick={(deviceId) => history.push(`/device/${deviceId}`)}
+ />
+ </ThemeProvider>
+ </Provider>,
+ placeholder,
+ );
+
+ if (popup) {
+ popup.remove();
+ }
+ popup = new maplibregl.Popup({
+ offset: 25,
+ anchor: 'top',
+ closeOnClick: true,
+ });
+
+ popup.setDOMContent(placeholder).setLngLat(coordinates).addTo(map);
+ };
+
useEffect(() => {
- map.easeTo({ center: mapCenter });
+ if (mapCenter) {
+ map.easeTo({ center: mapCenter.position });
+ showStatus(mapCenter.deviceId, mapCenter.position);
+ }
}, [mapCenter]);
return null;
diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js
index c0f723d..00d36c5 100644
--- a/modern/src/map/StatusView.js
+++ b/modern/src/map/StatusView.js
@@ -1,70 +1,134 @@
-import React from 'react';
+import React, { useState } from 'react';
+import {
+ makeStyles, Paper, IconButton, Grid, Button,
+} from '@material-ui/core';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
+import ListItemText from '@material-ui/core/ListItemText';
import { useSelector } from 'react-redux';
+
+import ReplayIcon from '@material-ui/icons/Replay';
+import ExitToAppIcon from '@material-ui/icons/ExitToApp';
+import EditIcon from '@material-ui/icons/Edit';
+import DeleteIcon from '@material-ui/icons/Delete';
+
+import {
+ formatPosition, getStatusColor, getBatteryStatus, formatDistance, formatSpeed,
+} from '../common/formatter';
+import { useAttributePreference } from '../common/preferences';
+import RemoveDialog from '../RemoveDialog';
import t from '../common/localization';
-import { formatPosition } from '../common/formatter';
import { getPosition } from '../common/selectors';
-const StatusView = ({ deviceId, onShowDetails }) => {
+const useStyles = makeStyles((theme) => ({
+ paper: {
+ width: '300px',
+ },
+ ...theme.palette.colors,
+ listItemContainer: {
+ maxWidth: '240px',
+ },
+}));
+
+const StatusView = ({
+ deviceId, onShowDetails, onShowHistory, onEditClick,
+}) => {
+ const classes = useStyles();
+
+ const [removeDialogShown, setRemoveDialogShown] = useState(false);
const device = useSelector((state) => state.devices.items[deviceId]);
const position = useSelector(getPosition(deviceId));
+ const distanceUnit = useAttributePreference('distanceUnit');
+ const speedUnit = useAttributePreference('speedUnit');
+
const handleClick = (e) => {
e.preventDefault();
onShowDetails(position.id);
};
+ const handleEditClick = (e) => {
+ e.preventDefault();
+ onEditClick(deviceId);
+ };
+
+ const handleRemove = () => {
+ setRemoveDialogShown(true);
+ };
+
+ const handleRemoveResult = () => {
+ setRemoveDialogShown(false);
+ };
+
return (
<>
- <b>
- {t('deviceStatus')}
- :
- </b>
- {' '}
- {formatPosition(device.status, 'status')}
- <br />
- <b>
- {t('sharedLocation')}
- :
- </b>
- {' '}
- {formatPosition(position, 'latitude')}
- {' '}
- {formatPosition(position, 'longitude')}
- <br />
- <b>
- {t('positionSpeed')}
- :
- </b>
- {' '}
- {formatPosition(position.speed, 'speed')}
- <br />
- <b>
- {t('positionCourse')}
- :
- </b>
- {' '}
- {formatPosition(position.course, 'course')}
- <br />
- <b>
- {t('positionDistance')}
- :
- </b>
- {' '}
- {formatPosition(position.attributes.totalDistance, 'distance')}
- <br />
- {position.attributes.batteryLevel
- && (
- <>
- <b>
- {t('positionBattery')}
- :
- </b>
- {' '}
- {formatPosition(position.attributes.batteryLevel, 'batteryLevel')}
- <br />
- </>
- )}
- <a href="/" onClick={handleClick}>{t('sharedShowDetails')}</a>
+ <Paper className={classes.paper} elevation={0} square>
+ <Grid container direction="column">
+ <Grid item>
+ <List>
+ <ListItem classes={{ container: classes.listItemContainer }}>
+ <ListItemText primary={t('deviceStatus')} />
+ <ListItemSecondaryAction>
+ <span className={classes[getStatusColor(device.status)]}>{device.status}</span>
+ </ListItemSecondaryAction>
+ </ListItem>
+ <ListItem classes={{ container: classes.listItemContainer }}>
+ <ListItemText primary={t('positionSpeed')} />
+ <ListItemSecondaryAction>
+ <span>{formatSpeed(position.speed, speedUnit)}</span>
+ </ListItemSecondaryAction>
+ </ListItem>
+ {position.attributes.batteryLevel && (
+ <ListItem classes={{ container: classes.listItemContainer }}>
+ <ListItemText primary={t('positionBattery')} />
+ <ListItemSecondaryAction>
+ <span className={classes[getBatteryStatus(position.attributes.batteryLevel)]}>{formatPosition(position.attributes.batteryLevel, 'batteryLevel')}</span>
+ </ListItemSecondaryAction>
+ </ListItem>
+ )}
+ <ListItem classes={{ container: classes.listItemContainer }}>
+ <ListItemText primary={t('positionDistance')} />
+ <ListItemSecondaryAction>
+ <span>{formatDistance(position.attributes.totalDistance, distanceUnit)}</span>
+ </ListItemSecondaryAction>
+ </ListItem>
+ <ListItem classes={{ container: classes.listItemContainer }}>
+ <ListItemText primary={t('positionCourse')} />
+ <ListItemSecondaryAction>
+ <span>{formatPosition(position.course, 'course')}</span>
+ </ListItemSecondaryAction>
+ </ListItem>
+ </List>
+ </Grid>
+ <Grid item container>
+ <Grid item>
+ <Button color="secondary" onClick={handleClick}>More Info</Button>
+ </Grid>
+ <Grid item>
+ <IconButton onClick={onShowHistory}>
+ <ReplayIcon />
+ </IconButton>
+ </Grid>
+ <Grid item>
+ <IconButton>
+ <ExitToAppIcon />
+ </IconButton>
+ </Grid>
+ <Grid item>
+ <IconButton onClick={handleEditClick}>
+ <EditIcon />
+ </IconButton>
+ </Grid>
+ <Grid item>
+ <IconButton onClick={handleRemove} className={classes.red}>
+ <DeleteIcon />
+ </IconButton>
+ </Grid>
+ </Grid>
+ </Grid>
+ </Paper>
+ <RemoveDialog open={removeDialogShown} endpoint="devices" itemId={deviceId} onResult={handleRemoveResult} />
</>
);
};
diff --git a/modern/src/theme/palette.js b/modern/src/theme/palette.js
index ac26e57..622e49b 100644
--- a/modern/src/theme/palette.js
+++ b/modern/src/theme/palette.js
@@ -18,4 +18,15 @@ export default {
main: traccarGreen,
contrastText: traccarWhite,
},
+ colors: {
+ red: {
+ color: traccarRed,
+ },
+ green: {
+ color: traccarGreen,
+ },
+ gray: {
+ color: traccarGray,
+ },
+ },
};