aboutsummaryrefslogtreecommitdiff
path: root/modern/src/map
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-05-01 15:26:08 -0700
committerAnton Tananaev <anton@traccar.org>2022-05-01 15:26:08 -0700
commitaf6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5 (patch)
tree6fe30c87a48ad5a438554f8cb594028b49efcfef /modern/src/map
parent90f292b7739835202842d88eeaf55a531d29d3c3 (diff)
downloadtrackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.gz
trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.bz2
trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.zip
Migrate to a card for popup
Diffstat (limited to 'modern/src/map')
-rw-r--r--modern/src/map/PositionsMap.js47
-rw-r--r--modern/src/map/StatusCard.js136
-rw-r--r--modern/src/map/StatusView.js140
3 files changed, 142 insertions, 181 deletions
diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js
index d82347f1..033ec760 100644
--- a/modern/src/map/PositionsMap.js
+++ b/modern/src/map/PositionsMap.js
@@ -1,22 +1,15 @@
-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';
+import { useCallback, useEffect } from 'react';
+import { useDispatch, 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';
import { getStatusColor } from '../common/formatter';
-import { LocalizationProvider } from '../LocalizationProvider';
+import { devicesActions } from '../store';
const PositionsMap = ({ positions }) => {
const id = 'positions';
const clusters = `${id}-clusters`;
- const history = useHistory();
+ const dispatch = useDispatch();
const devices = useSelector((state) => state.devices.items);
const createFeature = (devices, position) => {
@@ -34,36 +27,8 @@ const PositionsMap = ({ positions }) => {
const onMarkerClick = useCallback((event) => {
const feature = event.features[0];
- const coordinates = feature.geometry.coordinates.slice();
- while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
- coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
- }
-
- const placeholder = document.createElement('div');
- ReactDOM.render(
- <Provider store={store}>
- <LocalizationProvider>
- <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>
- </LocalizationProvider>
- </Provider>,
- placeholder,
- );
-
- new maplibregl.Popup({
- offset: 25,
- anchor: 'top',
- })
- .setDOMContent(placeholder)
- .setLngLat(coordinates)
- .addTo(map);
- }, [history]);
+ dispatch(devicesActions.select(feature.properties.deviceId));
+ }, [dispatch]);
const onClusterClick = useCallback((event) => {
const features = map.queryRenderedFeatures(event.point, {
diff --git a/modern/src/map/StatusCard.js b/modern/src/map/StatusCard.js
new file mode 100644
index 00000000..47f7724d
--- /dev/null
+++ b/modern/src/map/StatusCard.js
@@ -0,0 +1,136 @@
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import {
+ makeStyles, Button, Card, CardContent, Typography, CardActions, CardHeader, IconButton, Avatar, Table, TableBody, TableRow, TableCell, TableContainer,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+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 { useTranslation } from '../LocalizationProvider';
+import { formatDistance, formatPosition, formatSpeed, formatStatus } from '../common/formatter';
+import RemoveDialog from '../RemoveDialog';
+import { useAttributePreference } from '../common/preferences';
+
+const useStyles = makeStyles((theme) => ({
+ paper: {
+ width: '300px',
+ },
+ negative: {
+ color: theme.palette.colors.negative,
+ },
+ listItemContainer: {
+ maxWidth: '240px',
+ },
+ icon: {
+ width: '25px',
+ height: '25px',
+ filter: 'brightness(0) invert(1)',
+ },
+ table: {
+ '& .MuiTableCell-sizeSmall': {
+ paddingLeft: theme.spacing(0.5),
+ paddingRight: theme.spacing(0.5),
+ },
+ },
+ cell: {
+ borderBottom: 'none',
+ },
+}));
+
+const StatusRow = ({ name, value }) => {
+ const classes = useStyles();
+
+ return (
+ <TableRow>
+ <TableCell className={classes.cell}>
+ <Typography variant="body2">{name}</Typography>
+ </TableCell>
+ <TableCell className={classes.cell}>
+ <Typography variant="body2" color="textSecondary">{value}</Typography>
+ </TableCell>
+ </TableRow>
+ );
+}
+
+const StatusCard = ({ deviceId, onClose }) => {
+ const classes = useStyles();
+ const history = useHistory();
+ const t = useTranslation();
+
+ const device = useSelector((state) => state.devices.items[deviceId]);
+ const position = useSelector((state) => state.positions.items[deviceId]);
+
+ const distanceUnit = useAttributePreference('distanceUnit');
+ const speedUnit = useAttributePreference('speedUnit');
+
+ const [removeDialogShown, setRemoveDialogShown] = useState(false);
+
+ return (
+ <>
+ {device &&
+ <Card>
+ <CardHeader
+ avatar={
+ <Avatar>
+ <img className={classes.icon} src={`images/icon/${device.category || 'default'}.svg`} alt="" />
+ </Avatar>
+ }
+ action={
+ <IconButton onClick={onClose}>
+ <CloseIcon />
+ </IconButton>
+ }
+ title={device.name}
+ subheader={formatStatus(device.status, t)}
+ />
+ {position &&
+ <CardContent>
+ <TableContainer>
+ <Table size="small" classes={{ root: classes.table }}>
+ <TableBody>
+ <StatusRow name={t('positionSpeed')} value={formatSpeed(position.speed, speedUnit, t)} />
+ <StatusRow name={t('positionBattery')} value={formatSpeed(position.speed, speedUnit, t)} />
+ {position.attributes.odometer
+ ? <StatusRow name={t('positionOdometer')} value={formatDistance(position.attributes.odometer, distanceUnit, t)} />
+ : <StatusRow name={t('deviceTotalDistance')} value={formatDistance(position.attributes.totalDistance, distanceUnit, t)} />
+ }
+ <StatusRow name={t('positionCourse')} value={formatPosition(position.course, 'course', t)} />
+ </TableBody>
+ </Table>
+ </TableContainer>
+ </CardContent>
+ }
+ <CardActions disableSpacing>
+ <Button onClick={() => history.push(`/position/${position.id}`)} disabled={!position} color="secondary">
+ {t('sharedInfoTitle')}
+ </Button>
+ <IconButton onClick={() => history.push('/replay')} disabled={!position}>
+ <ReplayIcon />
+ </IconButton>
+ <IconButton>
+ <ExitToAppIcon />
+ </IconButton>
+ <IconButton onClick={() => history.push(`/device/${deviceId}`)}>
+ <EditIcon />
+ </IconButton>
+ <IconButton onClick={() => setRemoveDialogShown(true)} className={classes.negative}>
+ <DeleteIcon />
+ </IconButton>
+ </CardActions>
+ </Card>
+ }
+ <RemoveDialog
+ open={removeDialogShown}
+ endpoint="devices"
+ itemId={deviceId}
+ onResult={() => setRemoveDialogShown(false)}
+ />
+ </>
+ );
+};
+
+export default StatusCard;
diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js
deleted file mode 100644
index 2c3a7568..00000000
--- a/modern/src/map/StatusView.js
+++ /dev/null
@@ -1,140 +0,0 @@
-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, formatStatus,
-} from '../common/formatter';
-import { useAttributePreference } from '../common/preferences';
-import RemoveDialog from '../RemoveDialog';
-import { useTranslation } from '../LocalizationProvider';
-
-const useStyles = makeStyles((theme) => ({
- paper: {
- width: '300px',
- },
- negative: {
- color: theme.palette.colors.negative,
- },
- listItemContainer: {
- maxWidth: '240px',
- },
-}));
-
-const StatusView = ({
- deviceId, onShowDetails, onShowHistory, onEditClick,
-}) => {
- const classes = useStyles();
- const t = useTranslation();
-
- const [removeDialogShown, setRemoveDialogShown] = useState(false);
- const device = useSelector((state) => state.devices.items[deviceId]);
- const position = useSelector((state) => state.positions.items[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 (
- <>
- <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)]}>{formatStatus(device.status, t)}</span>
- </ListItemSecondaryAction>
- </ListItem>
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionSpeed')} />
- <ListItemSecondaryAction>
- {formatSpeed(position.speed, speedUnit, t)}
- </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', t)}
- </span>
- </ListItemSecondaryAction>
- </ListItem>
- )}
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionDistance')} />
- <ListItemSecondaryAction>
- {formatDistance(position.attributes.totalDistance, distanceUnit, t)}
- </ListItemSecondaryAction>
- </ListItem>
- <ListItem classes={{ container: classes.listItemContainer }}>
- <ListItemText primary={t('positionCourse')} />
- <ListItemSecondaryAction>
- {formatPosition(position.course, 'course', t)}
- </ListItemSecondaryAction>
- </ListItem>
- </List>
- </Grid>
- <Grid item container>
- <Grid item>
- <Button color="secondary" onClick={handleClick}>{t('sharedInfoTitle')}</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.negative}>
- <DeleteIcon />
- </IconButton>
- </Grid>
- </Grid>
- </Grid>
- </Paper>
- <RemoveDialog open={removeDialogShown} endpoint="devices" itemId={deviceId} onResult={handleRemoveResult} />
- </>
- );
-};
-
-export default StatusView;