aboutsummaryrefslogtreecommitdiff
path: root/modern/src/other
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/other')
-rw-r--r--modern/src/other/EventPage.js77
-rw-r--r--modern/src/other/GeofencesList.js56
-rw-r--r--modern/src/other/GeofencesPage.js87
-rw-r--r--modern/src/other/ReplayPage.js220
4 files changed, 440 insertions, 0 deletions
diff --git a/modern/src/other/EventPage.js b/modern/src/other/EventPage.js
new file mode 100644
index 00000000..46f5e67c
--- /dev/null
+++ b/modern/src/other/EventPage.js
@@ -0,0 +1,77 @@
+import React, { useState } from 'react';
+
+import {
+ makeStyles, Typography, AppBar, Toolbar, IconButton,
+} from '@material-ui/core';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+import { useHistory, useParams } from 'react-router-dom';
+import ContainerDimensions from 'react-container-dimensions';
+import { useEffectAsync } from '../reactHelper';
+import { useTranslation } from '../common/components/LocalizationProvider';
+import Map from '../map/core/Map';
+import PositionsMap from '../map/PositionsMap';
+
+const useStyles = makeStyles(() => ({
+ root: {
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ mapContainer: {
+ flexGrow: 1,
+ },
+}));
+
+const EventPage = () => {
+ const classes = useStyles();
+ const history = useHistory();
+ const t = useTranslation();
+
+ const { id } = useParams();
+
+ const [event, setEvent] = useState();
+ const [position, setPosition] = useState();
+
+ useEffectAsync(async () => {
+ if (id) {
+ const response = await fetch(`/api/events/${id}`);
+ if (response.ok) {
+ setEvent(await response.json());
+ }
+ }
+ }, [id]);
+
+ useEffectAsync(async () => {
+ if (event && event.positionId) {
+ const response = await fetch(`/api/positions?id=${event.positionId}`);
+ if (response.ok) {
+ const positions = await response.json();
+ if (positions.length > 0) {
+ setPosition(positions[0]);
+ }
+ }
+ }
+ }, [event]);
+
+ return (
+ <div className={classes.root}>
+ <AppBar color="inherit" position="static">
+ <Toolbar>
+ <IconButton color="inherit" edge="start" onClick={() => history.push('/')}>
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6">{t('positionEvent')}</Typography>
+ </Toolbar>
+ </AppBar>
+ <div className={classes.mapContainer}>
+ <ContainerDimensions>
+ <Map>
+ {position && <PositionsMap positions={[position]} />}
+ </Map>
+ </ContainerDimensions>
+ </div>
+ </div>
+ );
+};
+
+export default EventPage;
diff --git a/modern/src/other/GeofencesList.js b/modern/src/other/GeofencesList.js
new file mode 100644
index 00000000..b4fde749
--- /dev/null
+++ b/modern/src/other/GeofencesList.js
@@ -0,0 +1,56 @@
+import React, { Fragment } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { makeStyles } from '@material-ui/core/styles';
+import Divider from '@material-ui/core/Divider';
+import IconButton from '@material-ui/core/IconButton';
+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 MoreVertIcon from '@material-ui/icons/MoreVert';
+
+import { devicesActions } from '../store';
+import EditCollectionView from '../settings/components/EditCollectionView';
+
+const useStyles = makeStyles(() => ({
+ list: {
+ maxHeight: '100%',
+ overflow: 'auto',
+ },
+ icon: {
+ width: '25px',
+ height: '25px',
+ filter: 'brightness(0) invert(1)',
+ },
+}));
+
+const GeofenceView = ({ onMenuClick }) => {
+ const classes = useStyles();
+ const dispatch = useDispatch();
+
+ const items = useSelector((state) => state.geofences.items);
+
+ return (
+ <List className={classes.list}>
+ {Object.values(items).map((item, index, list) => (
+ <Fragment key={item.id}>
+ <ListItem button key={item.id} onClick={() => dispatch(devicesActions.select(item.id))}>
+ <ListItemText primary={item.name} />
+ <ListItemSecondaryAction>
+ <IconButton size="small" onClick={(event) => onMenuClick(event.currentTarget, item.id)}>
+ <MoreVertIcon />
+ </IconButton>
+ </ListItemSecondaryAction>
+ </ListItem>
+ {index < list.length - 1 ? <Divider /> : null}
+ </Fragment>
+ ))}
+ </List>
+ );
+};
+
+const GeofencesList = () => (
+ <EditCollectionView content={GeofenceView} editPath="/geofence" endpoint="geofences" disableAdd />
+);
+
+export default GeofencesList;
diff --git a/modern/src/other/GeofencesPage.js b/modern/src/other/GeofencesPage.js
new file mode 100644
index 00000000..79eed22a
--- /dev/null
+++ b/modern/src/other/GeofencesPage.js
@@ -0,0 +1,87 @@
+import React from 'react';
+import {
+ Divider, isWidthUp, makeStyles, withWidth, Typography, IconButton,
+} from '@material-ui/core';
+import Drawer from '@material-ui/core/Drawer';
+import ContainerDimensions from 'react-container-dimensions';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+import { useHistory } from 'react-router-dom';
+import Map from '../map/core/Map';
+import CurrentLocationMap from '../map/CurrentLocationMap';
+import GeofenceEditMap from '../map/GeofenceEditMap';
+import GeofencesList from './GeofencesList';
+import { useTranslation } from '../common/components/LocalizationProvider';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ content: {
+ flexGrow: 1,
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'row',
+ [theme.breakpoints.down('xs')]: {
+ flexDirection: 'column-reverse',
+ },
+ },
+ drawerPaper: {
+ position: 'relative',
+ [theme.breakpoints.up('sm')]: {
+ width: 350,
+ },
+ [theme.breakpoints.down('xs')]: {
+ height: 250,
+ },
+ },
+ drawerHeader: {
+ ...theme.mixins.toolbar,
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ },
+ mapContainer: {
+ flexGrow: 1,
+ },
+}));
+
+const GeofencesPage = ({ width }) => {
+ const classes = useStyles();
+ const history = useHistory();
+ const t = useTranslation();
+
+ return (
+ <div className={classes.root}>
+ <div className={classes.content}>
+ <Drawer
+ anchor={isWidthUp('sm', width) ? 'left' : 'bottom'}
+ variant="permanent"
+ classes={{ paper: classes.drawerPaper }}
+ >
+ <div className={classes.drawerHeader}>
+ <IconButton onClick={() => history.goBack()}>
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" noWrap>
+ {t('sharedGeofences')}
+ </Typography>
+ </div>
+ <Divider />
+ <GeofencesList />
+ </Drawer>
+ <div className={classes.mapContainer}>
+ <ContainerDimensions>
+ <Map>
+ <CurrentLocationMap />
+ <GeofenceEditMap />
+ </Map>
+ </ContainerDimensions>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default withWidth()(GeofencesPage);
diff --git a/modern/src/other/ReplayPage.js b/modern/src/other/ReplayPage.js
new file mode 100644
index 00000000..9b66853d
--- /dev/null
+++ b/modern/src/other/ReplayPage.js
@@ -0,0 +1,220 @@
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ Grid, FormControlLabel, Switch, IconButton, TextField, makeStyles, Paper, Slider, Toolbar, Tooltip, Typography,
+} from '@material-ui/core';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+import SettingsIcon from '@material-ui/icons/Settings';
+import PlayArrowIcon from '@material-ui/icons/PlayArrow';
+import PauseIcon from '@material-ui/icons/Pause';
+import FastForwardIcon from '@material-ui/icons/FastForward';
+import FastRewindIcon from '@material-ui/icons/FastRewind';
+import { useHistory } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import Map from '../map/core/Map';
+import ReplayPathMap from '../map/ReplayPathMap';
+import PositionsMap from '../map/PositionsMap';
+import { formatTime } from '../common/util/formatter';
+import ReportFilter from '../reports/components/ReportFilter';
+import { useTranslation } from '../common/components/LocalizationProvider';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ height: '100%',
+ },
+ sidebar: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ margin: theme.spacing(1.5),
+ width: theme.dimensions.drawerWidthDesktop,
+ [theme.breakpoints.down('sm')]: {
+ width: '100%',
+ margin: 0,
+ },
+ },
+ formControlLabel: {
+ height: '100%',
+ width: '100%',
+ paddingRight: theme.spacing(1),
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ reportFilterContainer: {
+ flex: 1,
+ padding: theme.spacing(2),
+ [theme.breakpoints.down('sm')]: {
+ margin: theme.spacing(1),
+ },
+ },
+ sliderContainer: {
+ padding: theme.spacing(2),
+ },
+}));
+
+const TimeLabel = ({ children, open, value }) => (
+ <Tooltip open={open} enterTouchDelay={0} placement="top" title={value}>
+ {children}
+ </Tooltip>
+);
+
+const ReplayPage = () => {
+ const t = useTranslation();
+ const classes = useStyles();
+ const history = useHistory();
+ const timerRef = useRef();
+
+ const [positions, setPositions] = useState([]);
+ const [index, setIndex] = useState(0);
+ const [selectedDeviceId, setSelectedDeviceId] = useState();
+ const [playbackSpeed, setPlaybackSpeed] = useState('');
+ const [expanded, setExpanded] = useState(true);
+ const [isPlaying, setIsPlaying] = useState(false);
+
+ const deviceName = useSelector((state) => {
+ if (selectedDeviceId) {
+ const device = state.devices.items[selectedDeviceId];
+ if (device) {
+ return device.name;
+ }
+ }
+ return null;
+ });
+
+ useEffect(() => {
+ if (isPlaying && positions.length > 0) {
+ timerRef.current = setInterval(() => {
+ setIndex((index) => index + 1);
+ }, 500);
+ } else {
+ clearInterval(timerRef.current);
+ }
+
+ return () => clearInterval(timerRef.current);
+ }, [isPlaying, positions]);
+
+ useEffect(() => {
+ if (index >= positions.length) {
+ clearInterval(timerRef.current);
+ }
+ }, [index, positions]);
+
+ const handleSubmit = async (deviceId, from, to, _, headers) => {
+ setSelectedDeviceId(deviceId);
+ const query = new URLSearchParams({ deviceId, from, to });
+ const response = await fetch(`/api/positions?${query.toString()}`, { headers });
+ if (response.ok) {
+ setIndex(0);
+ setPositions(await response.json());
+ setExpanded(false);
+ }
+ };
+
+ return (
+ <div className={classes.root}>
+ <Map>
+ <ReplayPathMap positions={positions} />
+ {index < positions.length && <PositionsMap positions={[positions[index]]} />}
+ </Map>
+ <div className={classes.sidebar}>
+ <Grid container direction="column" spacing={1}>
+ <Grid item>
+ <Paper elevation={3} square>
+ <Toolbar disableGutters>
+ <Grid container alignItems="center">
+ <Grid item>
+ <IconButton onClick={() => history.push('/')}>
+ <ArrowBackIcon />
+ </IconButton>
+ </Grid>
+ <Grid item xs>
+ <Typography variant="h6">{t('reportReplay')}</Typography>
+ </Grid>
+ {!expanded && (
+ <Grid item>
+ <IconButton onClick={() => setExpanded(true)}>
+ <SettingsIcon />
+ </IconButton>
+ </Grid>
+ )}
+ </Grid>
+ </Toolbar>
+ </Paper>
+ </Grid>
+ <Grid item>
+ {!expanded ? (
+ <Paper className={classes.sliderContainer}>
+ <Grid container direction="column" alignItems="center">
+ <Grid item>
+ {deviceName}
+ </Grid>
+ <Grid item style={{ width: '100%' }}>
+ <Slider
+ max={positions.length - 1}
+ step={null}
+ marks={positions.map((_, index) => ({ value: index }))}
+ value={index}
+ onChange={(_, index) => setIndex(index)}
+ valueLabelDisplay="auto"
+ valueLabelFormat={(i) => (i < positions.length ? formatTime(positions[i]) : '')}
+ ValueLabelComponent={TimeLabel}
+ />
+ </Grid>
+ <Grid item container justifyContent="space-between" alignItems="center">
+ <Grid item xs={2}>{`${index}/${positions.length}`}</Grid>
+ <Grid item xs={2}>
+ <IconButton onClick={() => setIndex((index) => index - 1)} disabled={isPlaying}>
+ <FastRewindIcon />
+ </IconButton>
+ </Grid>
+ <Grid item xs={2}>
+ <IconButton onClick={() => setIsPlaying(!isPlaying)}>
+ {isPlaying ? <PauseIcon /> : <PlayArrowIcon /> }
+ </IconButton>
+ </Grid>
+ <Grid item xs={2}>
+ <IconButton onClick={() => setIndex((index) => index + 1)} disabled={isPlaying}>
+ <FastForwardIcon />
+ </IconButton>
+ </Grid>
+ <Grid item xs>{formatTime(positions[index])}</Grid>
+ </Grid>
+ </Grid>
+ </Paper>
+ ) : (
+ <Paper elevation={3} className={classes.reportFilterContainer} square>
+ <ReportFilter handleSubmit={handleSubmit} fullScreen showOnly>
+ <Grid item xs={6}>
+ <TextField
+ fullWidth
+ label={t('reportPlaybackPerMinute')}
+ value={playbackSpeed}
+ onChange={(e) => setPlaybackSpeed(e.target.value)}
+ variant="filled"
+ />
+ </Grid>
+ <Grid item xs={6}>
+ <FormControlLabel
+ classes={{ root: classes.formControlLabel }}
+ control={(
+ <Switch
+ checked={isPlaying}
+ onChange={(e) => setIsPlaying(e.target.checked)}
+ color="primary"
+ edge="start"
+ />
+ )}
+ label={t('reportAutoPlay')}
+ labelPlacement="start"
+ />
+ </Grid>
+ </ReportFilter>
+ </Paper>
+ )}
+ </Grid>
+ </Grid>
+ </div>
+ </div>
+ );
+};
+
+export default ReplayPage;