From d3c7705bedebd65c94f9eea691aaf2fe03b0cafe Mon Sep 17 00:00:00 2001
From: Anton Tananaev <anton@traccar.org>
Date: Sat, 19 Aug 2023 13:58:45 -0700
Subject: Move to Vite

---
 modern/src/other/ReplayPage.jsx | 233 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 233 insertions(+)
 create mode 100644 modern/src/other/ReplayPage.jsx

(limited to 'modern/src/other/ReplayPage.jsx')

diff --git a/modern/src/other/ReplayPage.jsx b/modern/src/other/ReplayPage.jsx
new file mode 100644
index 00000000..1050b976
--- /dev/null
+++ b/modern/src/other/ReplayPage.jsx
@@ -0,0 +1,233 @@
+import React, {
+  useState, useEffect, useRef, useCallback,
+} from 'react';
+import {
+  IconButton, Paper, Slider, Toolbar, Typography,
+} from '@mui/material';
+import makeStyles from '@mui/styles/makeStyles';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import TuneIcon from '@mui/icons-material/Tune';
+import DownloadIcon from '@mui/icons-material/Download';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import PauseIcon from '@mui/icons-material/Pause';
+import FastForwardIcon from '@mui/icons-material/FastForward';
+import FastRewindIcon from '@mui/icons-material/FastRewind';
+import { useNavigate } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import MapView from '../map/core/MapView';
+import MapRoutePath from '../map/MapRoutePath';
+import MapRoutePoints from '../map/MapRoutePoints';
+import MapPositions from '../map/MapPositions';
+import { formatTime } from '../common/util/formatter';
+import ReportFilter from '../reports/components/ReportFilter';
+import { useTranslation } from '../common/components/LocalizationProvider';
+import { useCatch } from '../reactHelper';
+import MapCamera from '../map/MapCamera';
+import MapGeofence from '../map/MapGeofence';
+import StatusCard from '../common/components/StatusCard';
+import { usePreference } from '../common/util/preferences';
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    height: '100%',
+  },
+  sidebar: {
+    display: 'flex',
+    flexDirection: 'column',
+    position: 'fixed',
+    zIndex: 3,
+    left: 0,
+    top: 0,
+    margin: theme.spacing(1.5),
+    width: theme.dimensions.drawerWidthDesktop,
+    [theme.breakpoints.down('md')]: {
+      width: '100%',
+      margin: 0,
+    },
+  },
+  title: {
+    flexGrow: 1,
+  },
+  slider: {
+    width: '100%',
+  },
+  controls: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  formControlLabel: {
+    height: '100%',
+    width: '100%',
+    paddingRight: theme.spacing(1),
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  content: {
+    display: 'flex',
+    flexDirection: 'column',
+    padding: theme.spacing(2),
+    [theme.breakpoints.down('md')]: {
+      margin: theme.spacing(1),
+    },
+    [theme.breakpoints.up('md')]: {
+      marginTop: theme.spacing(1),
+    },
+  },
+}));
+
+const ReplayPage = () => {
+  const t = useTranslation();
+  const classes = useStyles();
+  const navigate = useNavigate();
+  const timerRef = useRef();
+
+  const hours12 = usePreference('twelveHourFormat');
+
+  const defaultDeviceId = useSelector((state) => state.devices.selectedId);
+
+  const [positions, setPositions] = useState([]);
+  const [index, setIndex] = useState(0);
+  const [selectedDeviceId, setSelectedDeviceId] = useState(defaultDeviceId);
+  const [showCard, setShowCard] = useState(false);
+  const [from, setFrom] = useState();
+  const [to, setTo] = useState();
+  const [expanded, setExpanded] = useState(true);
+  const [playing, setPlaying] = useState(false);
+
+  const deviceName = useSelector((state) => {
+    if (selectedDeviceId) {
+      const device = state.devices.items[selectedDeviceId];
+      if (device) {
+        return device.name;
+      }
+    }
+    return null;
+  });
+
+  useEffect(() => {
+    if (playing && positions.length > 0) {
+      timerRef.current = setInterval(() => {
+        setIndex((index) => index + 1);
+      }, 500);
+    } else {
+      clearInterval(timerRef.current);
+    }
+
+    return () => clearInterval(timerRef.current);
+  }, [playing, positions]);
+
+  useEffect(() => {
+    if (index >= positions.length - 1) {
+      clearInterval(timerRef.current);
+      setPlaying(false);
+    }
+  }, [index, positions]);
+
+  const onPointClick = useCallback((_, index) => {
+    setIndex(index);
+  }, [setIndex]);
+
+  const onMarkerClick = useCallback((positionId) => {
+    setShowCard(!!positionId);
+  }, [setShowCard]);
+
+  const handleSubmit = useCatch(async ({ deviceId, from, to }) => {
+    setSelectedDeviceId(deviceId);
+    setFrom(from);
+    setTo(to);
+    const query = new URLSearchParams({ deviceId, from, to });
+    const response = await fetch(`/api/positions?${query.toString()}`);
+    if (response.ok) {
+      setIndex(0);
+      const positions = await response.json();
+      setPositions(positions);
+      if (positions.length) {
+        setExpanded(false);
+      } else {
+        throw Error(t('sharedNoData'));
+      }
+    } else {
+      throw Error(await response.text());
+    }
+  });
+
+  const handleDownload = () => {
+    const query = new URLSearchParams({ deviceId: selectedDeviceId, from, to });
+    window.location.assign(`/api/positions/kml?${query.toString()}`);
+  };
+
+  return (
+    <div className={classes.root}>
+      <MapView>
+        <MapGeofence />
+        <MapRoutePath positions={positions} />
+        <MapRoutePoints positions={positions} onClick={onPointClick} />
+        {index < positions.length && (
+          <MapPositions positions={[positions[index]]} onClick={onMarkerClick} titleField="fixTime" />
+        )}
+      </MapView>
+      <MapCamera positions={positions} />
+      <div className={classes.sidebar}>
+        <Paper elevation={3} square>
+          <Toolbar>
+            <IconButton edge="start" sx={{ mr: 2 }} onClick={() => navigate(-1)}>
+              <ArrowBackIcon />
+            </IconButton>
+            <Typography variant="h6" className={classes.title}>{t('reportReplay')}</Typography>
+            {!expanded && (
+              <>
+                <IconButton onClick={handleDownload}>
+                  <DownloadIcon />
+                </IconButton>
+                <IconButton edge="end" onClick={() => setExpanded(true)}>
+                  <TuneIcon />
+                </IconButton>
+              </>
+            )}
+          </Toolbar>
+        </Paper>
+        <Paper className={classes.content} square>
+          {!expanded ? (
+            <>
+              <Typography variant="subtitle1" align="center">{deviceName}</Typography>
+              <Slider
+                className={classes.slider}
+                max={positions.length - 1}
+                step={null}
+                marks={positions.map((_, index) => ({ value: index }))}
+                value={index}
+                onChange={(_, index) => setIndex(index)}
+              />
+              <div className={classes.controls}>
+                {`${index + 1}/${positions.length}`}
+                <IconButton onClick={() => setIndex((index) => index - 1)} disabled={playing || index <= 0}>
+                  <FastRewindIcon />
+                </IconButton>
+                <IconButton onClick={() => setPlaying(!playing)} disabled={index >= positions.length - 1}>
+                  {playing ? <PauseIcon /> : <PlayArrowIcon /> }
+                </IconButton>
+                <IconButton onClick={() => setIndex((index) => index + 1)} disabled={playing || index >= positions.length - 1}>
+                  <FastForwardIcon />
+                </IconButton>
+                {formatTime(positions[index].fixTime, 'seconds', hours12)}
+              </div>
+            </>
+          ) : (
+            <ReportFilter handleSubmit={handleSubmit} fullScreen showOnly />
+          )}
+        </Paper>
+      </div>
+      {showCard && index < positions.length && (
+        <StatusCard
+          deviceId={selectedDeviceId}
+          position={positions[index]}
+          onClose={() => setShowCard(false)}
+          disableActions
+        />
+      )}
+    </div>
+  );
+};
+
+export default ReplayPage;
-- 
cgit v1.2.3

