aboutsummaryrefslogtreecommitdiff
path: root/maps-frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'maps-frontend/src')
-rw-r--r--maps-frontend/src/App.js88
-rw-r--r--maps-frontend/src/App.test.js8
-rw-r--r--maps-frontend/src/components/Canvas.js571
-rw-r--r--maps-frontend/src/components/CheckinList.js129
-rw-r--r--maps-frontend/src/components/CoordSelector.js17
-rw-r--r--maps-frontend/src/components/Loading.js20
-rw-r--r--maps-frontend/src/components/Route.js44
-rw-r--r--maps-frontend/src/components/UserCheckin.js33
-rw-r--r--maps-frontend/src/css/App.css76
-rw-r--r--maps-frontend/src/css/Canvas.css6
-rw-r--r--maps-frontend/src/css/CoordSelector.css51
-rw-r--r--maps-frontend/src/css/Route.css56
-rw-r--r--maps-frontend/src/css/UserCheckin.css94
-rw-r--r--maps-frontend/src/index.css14
-rw-r--r--maps-frontend/src/index.js17
-rw-r--r--maps-frontend/src/logo.svg1
-rw-r--r--maps-frontend/src/reportWebVitals.js13
-rw-r--r--maps-frontend/src/setupTests.js5
18 files changed, 1243 insertions, 0 deletions
diff --git a/maps-frontend/src/App.js b/maps-frontend/src/App.js
new file mode 100644
index 0000000..0e25f39
--- /dev/null
+++ b/maps-frontend/src/App.js
@@ -0,0 +1,88 @@
+// React/component imports
+import React, {useEffect, useState} from 'react';
+import TimeSelector from './components/TimeSelector.js';
+import Canvas from './components/Canvas.js';
+import DataWidget from './components/DataWidget.js';
+import Loading from './components/Loading.js';
+
+// CSS import
+import './css/App.css';
+
+/**
+ * Main component of the app. Holds the main layout of the big components.
+ * @returns {import('react').HtmlHTMLAttributes} A div of the body of the page.
+ */
+function App() {
+ // State to open app when loaded
+ const [hasLoaded, setHasLoaded] = useState(false);
+ // State indicate if canvas is redrawing
+ const [isChanging, setIsChanging] = useState(false);
+ // State to hold dates -> two weeks apart on initialization.
+ const [dates, setDates] = useState({
+ start: new Date(),
+ end: new Date(Date.now() - 12096e5)
+ });
+ // State for visualization data
+ const [graphData, setGraphData] = useState({});
+ // States to control cursor.
+ const [cursor, setCursor] = useState('grab');
+ // State for routing.
+ const [route, setRoute] = useState([]);
+
+ const toEpochMilli = date => Date.parse(date);
+
+ const getGraphData = () => {
+ fetch("http://localhost:4567/data", {
+ method: "POST",
+ body: JSON.stringify({
+ start: toEpochMilli(dates.start),
+ end: toEpochMilli(dates.end)
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "same-origin"
+ })
+ .then(res => res.json())
+ .then(data => {
+ setGraphData(data);
+ setHasLoaded(true);
+ })
+ .catch(err => console.log(err));
+}
+
+ /**
+ * Sets the coordinate for routing.
+ * @param {Object} coord The coordinate to replace.
+ */
+ const setCoord = coord => {
+ setCursor('grab');
+ }
+
+
+ // Hooks to update data on init and switching of data
+ useEffect(() => getGraphData(), []);
+ useEffect(() => {
+ setIsChanging(true);
+ getGraphData();
+ return () => setIsChanging(false);
+ }, [dates]);
+
+
+ return (
+ <div className="App" style={{ cursor: cursor }}>
+ <header className="App-header">Welcome to WatchDogs!</header>
+ <div className="Canvas-filler Canvas-filler-1"></div>
+ <div className="Canvas-filler Canvas-filler-2"></div>
+ <div className="Canvas-filler Canvas-filler-3"></div>
+ <DataWidget setHasLoaded={setHasLoaded}></DataWidget>
+ <Canvas setCoord={setCoord} route={route} selector={currentSelector} startLat={startLat}
+ startLon={startLon} endLat={endLat} endLon={endLon} setCursor={setCursor}
+ hasLoaded={hasLoaded} setHasLoaded={setHasLoaded}></Canvas>
+ {(!hasLoaded) ? <Loading></Loading> :
+ <TimeSelector isChanging={isChanging} dates={dates} setDates={setDates}></TimeSelector>}
+ </div>
+ );
+}
+
+export default App;
diff --git a/maps-frontend/src/App.test.js b/maps-frontend/src/App.test.js
new file mode 100644
index 0000000..1f03afe
--- /dev/null
+++ b/maps-frontend/src/App.test.js
@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+ render(<App />);
+ const linkElement = screen.getByText(/learn react/i);
+ expect(linkElement).toBeInTheDocument();
+});
diff --git a/maps-frontend/src/components/Canvas.js b/maps-frontend/src/components/Canvas.js
new file mode 100644
index 0000000..166967e
--- /dev/null
+++ b/maps-frontend/src/components/Canvas.js
@@ -0,0 +1,571 @@
+// JS module imports
+import { useEffect, useRef, useState } from "react";
+import axios from 'axios';
+
+// CSS imports
+import '../css/Canvas.css';
+
+/**
+ * This function renders and mantains thhe canvas.
+ * @param {Object} props The props for the canvas.
+ * @returns {import("react").HtmlHTMLAttributes} The canvas to be retured.
+ */
+function Canvas(props) {
+ // instance variables
+ const CANVAS_WIDTH = window.innerWidth;
+ const CANVAS_HEIGHT = window.innerHeight;
+
+ const SCALE_RATIO = .035/.015;
+ const START_LON_SCALE = .005;
+ const MIN_LON_SCALE = .001;
+ const MAX_LON_SCALE = 1;
+ const SCALE_INTERVAL = .0007;
+ const TILE_SIZE = .1;
+
+ // Ways processing. Long object...
+ const A_TIER_MAX_SCALE = .175;
+ const B_TIER_MAX_SCALE = .08;
+ const WAYS_INFO = {
+ primary: {
+ color: 'darkorange',
+ weight: 2,
+ maxScale: MAX_LON_SCALE
+ },
+ motorway: {
+ color: 'darkorange',
+ weight: 1.85,
+ maxScale: MAX_LON_SCALE
+ },
+ secondary: {
+ color: 'orange',
+ weight: 1.75,
+ maxScale: MAX_LON_SCALE
+ },
+ tertiary: {
+ color: 'orange',
+ weight: 1.75,
+ maxScale: MAX_LON_SCALE
+ },
+ residential: {
+ color: 'white',
+ weight: 1.75,
+ maxScale: MAX_LON_SCALE
+ },
+
+ primary_link: {
+ color: 'yellow',
+ weight: 1.6,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ motorway_link: {
+ color: 'yellow',
+ weight: 1.5,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ secondary_link: {
+ color: 'yellow',
+ weight: 1.5,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ tertiary_link: {
+ color: 'yellow',
+ weight: 1.3,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ footway: {
+ color: 'mediumorchid',
+ weight: 1.25,
+ maxScale: .25
+ },
+ cycleway: {
+ color: 'blue',
+ weight: 1.35,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ trunk: {
+ color: 'amber',
+ weight: 1.25,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ trunk_link: {
+ color: 'amber',
+ weight: 1,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ road: {
+ color: 'white',
+ weight: 1.25,
+ maxScale: A_TIER_MAX_SCALE
+ },
+ living_street: {
+ color: 'white',
+ weight: 1.25,
+ maxScale: A_TIER_MAX_SCALE
+ },
+
+ service: {
+ color: 'red',
+ weight: 1,
+ maxScale: B_TIER_MAX_SCALE
+ },
+ construction: {
+ color: 'red',
+ weight: 1.25,
+ maxScale: B_TIER_MAX_SCALE
+ },
+ pedestrian: {
+ color: 'purple',
+ weight: 1,
+ maxScale: B_TIER_MAX_SCALE
+ },
+ track: {
+ color: 'pink',
+ weight: 1.2,
+ maxScale: B_TIER_MAX_SCALE
+ },
+ steps: {
+ color: 'purple',
+ weight: 1.1,
+ maxScale: B_TIER_MAX_SCALE
+ },
+ path: {
+ color: 'purple',
+ weight: 1.1,
+ maxScale: B_TIER_MAX_SCALE
+ }
+ }
+
+ const START_COORD = {
+ lat: 41.825,
+ lon: -71.406
+ }
+
+ // React ref to canvas
+ const ctxRef = useRef();
+
+ //Reach states
+ const [canvasWidth, setCanvasWidth] = useState(window.innerWidth);
+ const [canvasHeight, setCanvasHeight] = useState(window.innerHeight);
+ const [dragStart, setDragStart] = useState({});
+ const [scale, setScale] = useState(START_LON_SCALE);
+ const [referencePoint, setReferencePoint] = useState(START_COORD);
+ const [tiles, setTiles] = useState({});
+ const [requests, setRequests] = useState(new Set());
+ const [mouseDown, setMouseDown] = useState(false);
+ const [canvasImg, setCanvasImg] = useState();
+
+ // The following four methods map the latitude, longitude to the x,y of the grid.
+
+ /**
+ * The method mapping x to lon.
+ * @param {Number} x The x pixel difference to be converted.
+ * @returns {Number} The the corresponding longitude .
+ */
+ const xToLon = x => {
+ return x*(scale/canvasWidth);
+ }
+
+ /**
+ * The method mapping y to lat.
+ * @param {Number} y The y pixel difference to be converted.
+ * @returns {Number} The the corresponding latitude.
+ * It's reversed due to origin being at top left (not bottom left).
+ */
+ const yToLat = y => {
+ const latScale = scale/SCALE_RATIO;
+ return -y*(latScale/canvasHeight);
+ }
+
+ /**
+ * The method mapping lon to x.
+ * @param {Number} lon The longitude to be coverted to x pixel difference.
+ * @returns {Number} The the corresponding x pixel difference.
+ */
+ const lonToX = lon => {
+ return lon/(scale/canvasWidth);
+ }
+
+
+ /**
+ * @param {Number} lat The latitude to be converted to y pixel difference.
+ * @returns {Number} The the corresponding y value.
+ * It's reversed due to the difference origins.
+ */
+ const latToY = lat => {
+ const latScale = scale/SCALE_RATIO;
+ return -lat/(latScale/canvasHeight);
+ }
+
+ /**
+ * Calculated the current grid's corners to use in the ways command.
+ * @returns {Object} The top left and bottom right coordinates in terms of the lat and lon.
+ */
+ const getLatLonBounds = () => {
+ const lat1 = referencePoint.lat + TILE_SIZE;
+ const long1 = referencePoint.lon - TILE_SIZE;
+ const lat2 = referencePoint.lat + yToLat(canvasHeight) - TILE_SIZE;
+ const long2 = referencePoint.lon + xToLon(canvasWidth) + TILE_SIZE;
+ const topLeft = {
+ lat1: +(Math.round((lat1 * 1000)/10)/100).toFixed(1),
+ long1: +(Math.round((long1 * 1000)/10)/100).toFixed(1)
+ }
+ const botRight = {
+ lat2: +(Math.round((lat2 * 1000)/10)/100).toFixed(1),
+ long2: +(Math.round((long2 * 1000)/10)/100).toFixed(1)
+ }
+
+ return {...topLeft, ...botRight};
+ }
+
+ /**
+ * Method the gets a tile and puts it into the cache.
+ * @param {Object} topLeft The topleft coordinate of the tile.
+ * @returns The ways within the tile.
+ */
+ const getTile = async topLeft => {
+ // TODO: implement cache
+
+ const toSend = {
+ lat1: topLeft[0],
+ long1: topLeft[1],
+ lat2: +(topLeft[0] - TILE_SIZE).toFixed(1),
+ long2: +(topLeft[1] + TILE_SIZE).toFixed(1)
+ }
+ //console.log(toSend);
+
+ let config = {
+ headers: {
+ "Content-Type": "application/json",
+ 'Access-Control-Allow-Origin': '*',
+ }
+ };
+
+ //Install and import this!
+ //TODO: Fill in 1) location for request 2) your data 3) configuration
+ const res = await axios.post(
+ "http://localhost:4567/maps/ways",
+ JSON.stringify(toSend),
+ config
+ );
+
+ setTiles(prevState => ({
+ ...prevState,
+ [`lat${topLeft[0]}lon${topLeft[1]}`] : res.data["ways"]
+ }));
+
+ return res.data["ways"];
+ };
+
+ /**
+ * Method that draws a single way onto the canvas.
+ * @param {import("react").HtmlHTMLAttributes} ctx The context of the canvas.
+ * @param {Object} wayInfo The information of the way as an object.
+ */
+ const drawWay = (ctx, wayInfo) => {
+ // TODO: implement cache
+ ctx.beginPath();
+
+ const startY = latToY(wayInfo.startLat - referencePoint.lat);
+ const startX = lonToX(wayInfo.startLong - referencePoint.lon);
+ ctx.moveTo(startX, startY);
+
+ const endY = latToY(wayInfo.destLat - referencePoint.lat);
+ const endX = lonToX(wayInfo.destLong - referencePoint.lon);
+ ctx.lineTo(endX, endY);
+
+ if (wayInfo.type && WAYS_INFO[wayInfo.type]) {
+ if (WAYS_INFO[wayInfo.type].maxScale >= scale) {
+ ctx.strokeStyle = WAYS_INFO[wayInfo.type].color;
+ ctx.lineWidth = WAYS_INFO[wayInfo.type].weight;
+ ctx.stroke();
+ }
+ } else {
+ if (B_TIER_MAX_SCALE >= scale) {
+ ctx.strokeStyle = 'grey';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ }
+ }
+
+ if (props.route.includes(wayInfo.id)) {
+ ctx.strokeStyle = 'lightgreen';
+ ctx.lineWidth = 10;
+ ctx.stroke();
+ }
+ }
+
+ /**
+ * Handles the timeout from multiple quick requests to the api to ease the server.
+ * @param {CallableFunction} func The pending function call.
+ * @param {Number} delay The time in milliseconds to delay.
+ * @returns {CallableFunction} The function that runs the code after the delay.
+ */
+ const debounce = (func, delay) => {
+ let debounceTimer
+ return () => {
+ const context = this
+ const args = arguments
+ clearTimeout(debounceTimer)
+ debounceTimer = setTimeout(() => func.apply(context, args), delay)
+ }
+ }
+
+ /**
+ * Determines whether the a tile is loaded on the canvas.
+ * @param {Object} topLeft The topLeft point of the tile.
+ * @returns True if any section of it is loaded on the canvas.
+ */
+ const tileOnScreen = topLeft => {
+ const tileLatBounds = [+(topLeft[0] - TILE_SIZE).toFixed(1), topLeft[0]];
+ const tileLongBounds = [topLeft[1], +(topLeft[1] + TILE_SIZE).toFixed(1)];
+ const screenLatBounds = [referencePoint.lat, referencePoint.lat + yToLat(canvasHeight)]
+ const screenLongBounds = [referencePoint.lon, referencePoint.lon + xToLon(canvasWidth)]
+ //by checking if the following for things are true, we can determine if the rectangles intersect
+ const tileLeftScreen = tileLatBounds[1] < screenLatBounds[1];
+ const tileRightScreen = tileLatBounds[0] > screenLatBounds[0];
+ const tileAboveScreen = tileLongBounds[0] > screenLongBounds[1];
+ const tileBelowScreen = tileLongBounds[1] < screenLongBounds[0];
+ return !( tileLeftScreen || tileRightScreen || tileAboveScreen || tileBelowScreen );
+ }
+
+ /**
+ * Method used to update the ways on the canvas.
+ * It draws in the new tiles and translates the old.
+ */
+ const updateWaysOnCanvas = debounce(() => {
+ const ctx = ctxRef.current.getContext("2d");
+ const bounds = getLatLonBounds();
+ ctx.canvas.width = canvasWidth;
+ ctx.fillStyle = "#121212";
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+ for (let i = +(bounds.lat2 + TILE_SIZE).toFixed(1); i <= bounds.lat1; i = +(i + TILE_SIZE).toFixed(1)) {
+ for (let j = bounds.long1; j < bounds.long2; j = +(j + TILE_SIZE).toFixed(1)) {
+ const topLeft = [i, j];
+ const pointId = `lat${i}lon${j}`;
+ if (tiles[pointId] && tileOnScreen(topLeft)) {
+ //('accessing cache!');
+ Object.keys(tiles[pointId]).forEach(key => {
+ drawWay(ctx, tiles[pointId][key]);
+ });
+ } else {
+ if (!requests.has(pointId)) {
+ setRequests(requests.add(pointId));
+ getTile(topLeft).then((data) => {
+ if (tileOnScreen(topLeft)) {
+ Object.keys(data).forEach(key => {
+ drawWay(ctx, data[key]);
+ });
+ if (!props.hasLoaded) {
+ props.setHasLoaded(true);
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ // draws the routing points if they have been set
+ drawPoints();
+ }, 0);
+
+ /**
+ * Stores the starting point of the drah.
+ * @param {Object} e The event from the mouseDown callback.
+ */
+ const storeDragStart = e => {
+ setMouseDown(true);
+ const canvas = ctxRef.current;
+ setCanvasImg(canvas);
+
+ setDragStart({
+ x: e.pageX - canvas.offsetLeft,
+ y: e.pageY - canvas.offsetTop
+ });
+
+ props.setCursor('grabbing');
+ }
+
+ /**
+ * Makes a call to the server to determine the nearest neighbor.
+ * @param {Number} x The x pixel value of the point inputted.
+ * @param {Number} y The y pixel value of the point inputted.
+ * @returns {Object} The lat and lon coordinates of the nearest.
+ */
+ const getNearest = async (x, y) => {
+ const toSend = {
+ lat: yToLat(y) + referencePoint.lat,
+ long: xToLon(x) + referencePoint.lon
+ };
+
+ //console.log(toSend);
+
+ let config = {
+ headers: {
+ "Content-Type": "application/json",
+ 'Access-Control-Allow-Origin': '*',
+ }
+ };
+
+ //Install and import this!
+ //TODO: Fill in 1) location for request 2) your data 3) configuration
+ const res = await axios.post(
+ "http://localhost:4567/maps/nearest",
+ JSON.stringify(toSend),
+ config
+ )
+
+ return res.data;
+ }
+
+ /**
+ * This method draws the start and end points for the routing.
+ */
+ const drawPoints = () => {
+ const ctx = ctxRef.current.getContext('2d');
+
+ const startY = latToY(props.startLat - referencePoint.lat);
+ const startX = lonToX(props.startLon - referencePoint.lon);
+
+ const endY = latToY(props.endLat - referencePoint.lat);
+ const endX = lonToX(props.endLon - referencePoint.lon);
+
+ const drawStart = () => {
+ ctx.beginPath();
+ ctx.arc(startX, startY, 15, 0, Math.PI * 2, true);
+
+ ctx.strokeStyle = 'pink';
+ ctx.lineWidth = 10;
+ ctx.stroke();
+ }
+
+ const drawEnd = () => {
+ ctx.beginPath();
+ ctx.arc(endX, endY, 15, 0, Math.PI * 2, true);
+
+ ctx.strokeStyle = 'lightblue';
+ ctx.lineWidth = 10;
+ ctx.stroke();
+ }
+
+ const startNotChosen = props.startLat === 0 && props.startLon === 0;
+ const endNotChosen = props.endLat === 0 && props.endLon === 0;
+
+ if (!startNotChosen && !endNotChosen) {
+ drawStart();
+ drawEnd();
+ } else if (!startNotChosen) {
+ drawStart();
+ } else if (!endNotChosen) {
+ drawEnd();
+ }
+ }
+
+ /**
+ * Drags the canvas and also responds to a solo click for inputting the route coordinate.
+ * @param {Object} e The event from the callback click handler.
+ */
+ const dragCanvas = e => {
+ const canvas = ctxRef.current;
+
+ const endX = e.pageX - canvas.offsetLeft;
+ const endY = e.pageY - canvas.offsetTop;
+
+ const distX = endX - dragStart.x;
+ const distY = endY - dragStart.y;
+
+ setDragStart({
+ x: e.pageX - canvas.offsetLeft,
+ y: e.pageY - canvas.offsetTop
+ });
+
+ if ((distX === 0 || distY === 0) && props.selector !== '') {
+ getNearest(endX, endY).then((data) => {
+ props.setCoord(data);
+ });
+ } else {
+ setReferencePoint((refPoint) => {
+ return {
+ lat: refPoint.lat - yToLat(distY),
+ lon: refPoint.lon - xToLon(distX)
+ }
+ });
+ }
+ }
+
+ /**
+ * Makes dragging smmother by calling the drag command frame by frame.
+ * @param {Object} e The event from the callback mouseMove hanlder.
+ */
+ const smoothDrag = e => {
+ if (mouseDown) {
+ requestAnimationFrame(() => dragCanvas(e));
+ props.setCursor('grabbing');
+ }
+ }
+
+ /**
+ * Releases the drag by setting the cursor back and setting the state.
+ * @param {Object} e The event from the callback mouseDown handler.
+ */
+ const releaseDrag = e => {
+ props.setCursor('grab');
+ setMouseDown(false);
+ }
+
+ // The following are the react hooks that act as handlers and rerender on change.
+
+ useEffect(() => {
+ updateWaysOnCanvas();
+ }, [referencePoint, props.route]);
+
+ useEffect(() => {
+ updateWaysOnCanvas();
+ }, [props.selector]);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ updateWaysOnCanvas();
+ props.setCursor('grab');
+ }, 750);
+ return () => clearTimeout(timer);
+ }, [scale]);
+
+ useEffect(() => {
+ const canvas = ctxRef.current;
+
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ const zoomCanvas = e => {
+ e.preventDefault();
+
+ let direction = (e.deltaY > 0) ? 1 : -1;
+ props.setCursor(direction === 1 ? 'zoom-out' : 'zoom-in');
+ setScale((s) => {
+ const newScale = s + direction*SCALE_INTERVAL;
+ return Math.min(MAX_LON_SCALE, Math.max(newScale, MIN_LON_SCALE));
+ });
+ }
+ canvas.addEventListener('wheel', zoomCanvas, false);
+
+ window.addEventListener('resize', () => {
+ setCanvasWidth(window.innerWidth);
+ setCanvasHeight(window.innerHeight);
+ });
+ }, []);
+
+ useEffect(() => {
+ const canvas = ctxRef.current;
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+ updateWaysOnCanvas();
+ }, [canvasWidth, canvasHeight])
+
+
+ return <canvas ref={ctxRef} onMouseDown={(e) => storeDragStart(e)} onClick={(e) => dragCanvas(e)}
+ onMouseMove={(e) => smoothDrag(e)} onMouseUp={() => releaseDrag()} className="Map-canvas"></canvas>;
+}
+
+export default Canvas;
+
diff --git a/maps-frontend/src/components/CheckinList.js b/maps-frontend/src/components/CheckinList.js
new file mode 100644
index 0000000..189ed8b
--- /dev/null
+++ b/maps-frontend/src/components/CheckinList.js
@@ -0,0 +1,129 @@
+// React and component imports
+import axios from 'axios';
+import { useEffect, useState } from "react";
+import UserCheckin from './UserCheckin.js';
+
+// CSS import
+import '../css/UserCheckin.css';
+
+/**
+ * Component that build the checkin list and displays checkin info.
+ * @returns {import('react').HtmlHTMLAttributes} A div with the checkins
+ * in a vertical layout.
+ */
+function CheckinList() {
+ // States...
+ const [checkins, setCheckins] = useState({});
+ const [checkinItems, setCheckinItems] = useState([]);
+ const [showAllCheckins, setShowAllCheckins] = useState(true);
+ const [userInfo, setUserInfo] = useState([]);
+ const [userCoords, setUserCoords] = useState([]);
+
+ /**
+ * Makes a call to the server to get the newer checkin data.
+ */
+ function getNewCheckins() {
+ let config = {
+ headers: {
+ "Content-Type": "application/json",
+ 'Access-Control-Allow-Origin': '*',
+ }
+ }
+
+ axios.get(
+ "http://localhost:4567/maps/checkins",
+ config
+ ).then((res) => {
+ setCheckins((prev) => {
+ return Object.assign(prev, res.data['checkins']);
+ });
+ });
+ updateCheckinItems();
+ }
+
+ /**
+ * Gives all the checkins for a specific user.
+ * @param {String} id The user id to get the checkins for.
+ * @param {String} name The name to get the checkins for.
+ */
+ function getUserCheckins(id, name) {
+ const toSend = {
+ userid : id,
+ };
+ let config = {
+ headers: {
+ "Content-Type": "application/json",
+ 'Access-Control-Allow-Origin': '*',
+ }
+ }
+
+ axios.post(
+ "http://localhost:4567/maps/checkins",
+ JSON.stringify(toSend),
+ config
+ )
+ .then(res => {
+ setUserCoords(res.data['checkins']);
+ })
+ setUserInfo([name, id]);
+ setShowAllCheckins(false);
+ }
+
+ /**
+ * Generates the user checkins html elements.
+ * @returns A div holding a list of all checkins for a user.
+ */
+ function getUserCheckinElements() {
+ const coords = userCoords.map((coord, index) =>
+ <li key={index}>
+ <span>{'('+coord[0].toFixed(6)}, {coord[1].toFixed(6)+')'}</span>
+ </li>
+ );
+ return (
+ <div className="Chosen-user" hidden={showAllCheckins}>
+ <h2>
+ <span onClick={() => setShowAllCheckins(true)}><img className="Img-btn" src="/round_arrow_back_white_18dp.png" alt="image"/></span>
+ {userInfo[0]} : {userInfo[1]}
+ </h2>
+ <div className='Coord-ex'>{"(lat , lon)"}</div>
+ <ol className='User-checkin-list'>
+ {coords}
+ </ol>
+ </div>
+ );
+ }
+
+ /**
+ * Loads new the checkins into the current cache/map of checkins.
+ */
+ const updateCheckinItems = () => {
+ let tempCheckinItems = [];
+ const sortedCheckinEntries = Object.entries(checkins).sort((a, b) => b[0] - a[0]);
+ for (const [key, value] of sortedCheckinEntries) {
+ tempCheckinItems.push(
+ <UserCheckin key={key} value={value} getUserCheckins={getUserCheckins}></UserCheckin>
+ );
+ }
+ setCheckinItems(tempCheckinItems);
+ }
+
+ // React hook that queries the checkin database every 5 seconds.
+ useEffect(() => {
+ const interval = setInterval(() => {
+ getNewCheckins();
+ }, 5000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+ <div className="User-checkin">
+ <div className="Checkins">
+ <h2>Checkins</h2>
+ <ul className='Checkin-list'>{checkinItems}</ul>
+ </div>
+ {getUserCheckinElements()}
+ </div>
+ );
+}
+
+export default CheckinList; \ No newline at end of file
diff --git a/maps-frontend/src/components/CoordSelector.js b/maps-frontend/src/components/CoordSelector.js
new file mode 100644
index 0000000..5d5e5f5
--- /dev/null
+++ b/maps-frontend/src/components/CoordSelector.js
@@ -0,0 +1,17 @@
+// CSS import
+import '../css/Route.css';
+import '../css/CoordSelector.css';
+
+/**
+ * The component that selects and displays a coordinate.
+ * @param {Object} props The props for the element.
+ */
+function DateSelector(props) {
+ return (
+ <div className="Flex-coord">
+ <input type="date" value={props.date} onChange={(e) => props.setStart(e.target.value)}/>
+ </div>
+ );
+}
+
+export default DateSelector; \ No newline at end of file
diff --git a/maps-frontend/src/components/Loading.js b/maps-frontend/src/components/Loading.js
new file mode 100644
index 0000000..ed95bb1
--- /dev/null
+++ b/maps-frontend/src/components/Loading.js
@@ -0,0 +1,20 @@
+// CSS import
+import '../css/App.css';
+
+/**
+ * Component that shows the program initially loading.
+ * @returns The loading widget - spinning logo :)
+ */
+function Loading() {
+ return (
+ <div className="App Loading">
+ <header className="App-header">
+ <img src={"./logo512.png"} className="App-logo" alt="logo" />
+ <h1 className="App-title">Loading Maps</h1>
+ <p className="App-title">It takes a minute to connect to the servers and database :)</p>
+ </header>
+ </div>
+ );
+}
+
+export default Loading; \ No newline at end of file
diff --git a/maps-frontend/src/components/Route.js b/maps-frontend/src/components/Route.js
new file mode 100644
index 0000000..2a26fd9
--- /dev/null
+++ b/maps-frontend/src/components/Route.js
@@ -0,0 +1,44 @@
+// React/Component imports
+import { useState } from "react";
+import DateSelector from './DateSelector.js';
+
+// CSS imports
+import '../css/Route.css';
+
+
+/**
+ * The component that hold the forms for routing.
+ * @param {Object} props
+ */
+function TimeSelector(props) {
+ const [current, setCurrent] = useState("");
+
+ const [startDate, setStartDate] = useState(props.dates.start);
+ const [endDate, setEndDate] = useState(props.dates.end);
+
+ const changeTimeframe = () => {
+ props.setDates({
+ start: startDate,
+ end: endDate
+ })
+ }
+
+ // The div with the html elements for routing.
+ return (
+ <div className="Route">
+ <div className="Coord-selectors-flex">
+ <DateSelector side={"left"} name={"Start Date"} className="Coord-select-left" clickedFunc={props.setCurrent("start")}
+ changedFunc={setStartDate} disabled={props.currentSelector==='start' || props.isChanging}></DateSelector>
+ <div>
+ <h2>Adjust Timeframe :)</h2>
+ <button className="Btn Route-btn" onClick={() => changeTimeframe()}
+ disabled={props.currentSelector !== '' || props.isChanging}>Change Timeframe</button>
+ </div>
+ <DateSelector side={"right"} name={"End Date"} className="Coord-select-right" clickedFunc={props.setCurrent("end")}
+ changedFunc={setEndDate} disabled={props.currentSelector==='end' || props.isChanging}></DateSelector>
+ </div>
+ </div>
+ );
+}
+
+export default TimeSelector; \ No newline at end of file
diff --git a/maps-frontend/src/components/UserCheckin.js b/maps-frontend/src/components/UserCheckin.js
new file mode 100644
index 0000000..f85994b
--- /dev/null
+++ b/maps-frontend/src/components/UserCheckin.js
@@ -0,0 +1,33 @@
+// React import
+import { useState } from "react";
+
+// CSS import
+import '../css/UserCheckin.css';
+
+/**
+ * Componenet for checkins. Has a toggle to show more info.
+ * @param {Object} props The props of the component.
+ * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info.
+ */
+function UserCheckin(props) {
+ // State - toggled
+ const [isToggled, setIsToggled] = useState(false);
+
+ return (
+ <li className='Checkin'>
+ <div className="Img-flex">
+ <span><span className="Clickable-name" onClick= {(e) => props.getUserCheckins(props.value.id, props.value.name)}>{props.value.name}</span> just checked in!</span>
+ <img className="Img-btn" hidden={isToggled} onClick={() => setIsToggled((toggle) => !toggle)} src="/round_expand_more_white_18dp.png" alt="image"/>
+ <img className="Img-btn" hidden={!isToggled} onClick={() => setIsToggled((toggle) => !toggle)} src="/round_expand_less_white_18dp.png" alt="image"/>
+ </div>
+ <div hidden={!isToggled}>
+ <ul>
+ <li>Time: {new Date(props.value.ts * 1000).toLocaleTimeString("en-US")}</li>
+ <li>Lat: {props.value.lat}</li>
+ <li>Lon: {props.value.lon}</li>
+ </ul>
+ </div>
+ </li>);
+}
+
+export default UserCheckin; \ No newline at end of file
diff --git a/maps-frontend/src/css/App.css b/maps-frontend/src/css/App.css
new file mode 100644
index 0000000..90e9046
--- /dev/null
+++ b/maps-frontend/src/css/App.css
@@ -0,0 +1,76 @@
+.App {
+ display: grid;
+ grid-template-areas: "head canvasFill2 canvasFill3 checkin"
+ "canvasFill1 canvasFill2 canvasFill3 checkin"
+ "route canvasFill2 canvasFill3 checkin";
+ grid-template-rows: max-content auto max-content;
+ grid-template-columns: max-content auto max-content max-content;
+ background-color: #121212;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+
+.Canvas-filler {
+ width: 100%;
+ height: 100%;
+
+ z-index: 1;
+}
+
+.Canvas-filler-1 {
+ grid-area: canvasFill1;
+}
+.Canvas-filler-2 {
+ grid-area: canvasFill2;
+}
+.Canvas-filler-3 {
+ grid-area: canvasFill3;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ grid-area: head;
+ min-height: 7vh;
+ width: max-content;
+ display: flex;
+ padding: 0 20px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+ z-index: 10;
+ background-color: #333333;
+ border-radius: 5px;
+ margin: 5px;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.Loading {
+ z-index: 100;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+} \ No newline at end of file
diff --git a/maps-frontend/src/css/Canvas.css b/maps-frontend/src/css/Canvas.css
new file mode 100644
index 0000000..1bc45b5
--- /dev/null
+++ b/maps-frontend/src/css/Canvas.css
@@ -0,0 +1,6 @@
+.Map-canvas {
+ touch-action: none;
+
+ position: absolute;
+ z-index: 5;
+} \ No newline at end of file
diff --git a/maps-frontend/src/css/CoordSelector.css b/maps-frontend/src/css/CoordSelector.css
new file mode 100644
index 0000000..696c888
--- /dev/null
+++ b/maps-frontend/src/css/CoordSelector.css
@@ -0,0 +1,51 @@
+/* CSS adapted from w3school buttons */
+.Btn-select-left > p, .Btn-select-right > p {
+ padding: 0;
+ margin: 0;
+}
+
+.Btn-select-left {
+ background-color: #424242;
+ border: 4px solid pink;
+}
+
+.Btn-select-left:hover {
+ box-shadow: 3px 3px #888888;
+ color: black;
+ background-color: pink;
+}
+
+.Btn-select-right {
+ background-color: #424242;
+ border: 4px solid lightblue;
+}
+
+.Btn-select-right:hover {
+ box-shadow: 3px 3px #888888;
+ color: black;
+ background-color: lightblue;
+}
+
+.Btn:disabled,
+.Btn[disabled]{
+ border: 1px solid #999999;
+ background-color: #cccccc;
+ color: #666666;
+ box-shadow: none;
+ cursor: default;
+}
+
+.Btn:disabled:hover,
+.Btn[disabled]:hover{
+ cursor: crosshair;
+}
+
+.Textbox {
+ width: 100px;
+}
+
+.Number-input {
+ width: 90%;
+}
+
+
diff --git a/maps-frontend/src/css/Route.css b/maps-frontend/src/css/Route.css
new file mode 100644
index 0000000..eaa69c4
--- /dev/null
+++ b/maps-frontend/src/css/Route.css
@@ -0,0 +1,56 @@
+.Route {
+ grid-area: route;
+ z-index: 10;
+ color: white;
+ border-radius: 10px;
+ background-color: #121212;
+ cursor: default;
+ /* Transparent background */
+ background: rgba(0, 0, 0, 0);
+}
+
+.Coord-selectors-flex {
+ display: flex;
+ gap: 20px;
+ padding: 8px;
+ margin: 0;
+ align-content: flex-end;
+ background-color: #333333;
+ margin: 5px;
+ border-radius: 3px;
+}
+
+/* CSS adapted from w3school buttons */
+.Btn {
+ color: white;
+ padding: 16px 32px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ margin: 4px 2px;
+ transition-duration: 0.4s;
+ cursor: pointer;
+ outline: none;
+}
+
+.Route-btn:hover {
+ box-shadow: 3px 3px #ccc;
+ color: black;
+ background-color: lightgreen;
+}
+
+.Route-btn {
+ background-color: #424242;
+ border: 2px solid lightgreen;
+ box-shadow: .5px .5px 0 2px lightgreen;
+}
+
+.Btn:disabled,
+.Btn[disabled]{
+ border: 1px solid #999999;
+ background-color: #cccccc;
+ color: #666666;
+ cursor: default;
+ box-shadow: none;
+} \ No newline at end of file
diff --git a/maps-frontend/src/css/UserCheckin.css b/maps-frontend/src/css/UserCheckin.css
new file mode 100644
index 0000000..3e16ffd
--- /dev/null
+++ b/maps-frontend/src/css/UserCheckin.css
@@ -0,0 +1,94 @@
+.User-checkin {
+ grid-area: checkin;
+ height: 100vh;
+ background-color: #121212;
+ z-index: 10;
+ color: white;
+ border-radius: 10px;
+ display: flex;
+ font-size: 18px;
+ cursor: default;
+ /* Transparent background */
+ background: rgba(0, 0, 0, 0);
+}
+
+ul {
+ list-style-type: none;
+}
+
+.User-checkin > div {
+ z-index: 10;
+ background-color: #333333;
+ border-radius: 20px;
+ margin: 5px;
+}
+
+.Coord-ex {
+ height: 1vh;
+ margin: 0;
+ padding: 0;
+ text-align: center;
+}
+
+.Chosen-user > h2, .Checkins > h2 {
+ display: flex;
+ justify-content: space-evenly;
+ height: 5vh;
+ padding: 0 10px;
+}
+
+.Checkin-list {
+ padding: 0 20px;
+ height: 86vh;
+ overflow-y: scroll;
+ cursor: default;
+}
+
+.User-checkin-list {
+ height: 80vh;
+ overflow-y: scroll;
+
+ list-style-position: inside;
+ padding: 0 20px;
+ text-align: center;
+ text-indent: -12px;
+}
+
+.User-checkin-list > li {
+ margin-bottom: 20px;
+}
+
+
+.Checkin {
+ padding-bottom: 20px;
+ border-bottom: 1px solid #e6ecf0;
+}
+
+.Checkin:last-child {
+ border-bottom: none;
+}
+
+.Img-flex {
+ margin: 5px 10px 10px 0;
+ gap: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.Img-btn {
+ background-color: #424242;
+ border-radius: 50%;
+ margin-right: 10px;
+}
+
+.Img-btn:hover {
+ box-shadow: 3px 3px #333333;
+ cursor: pointer;
+}
+
+.Clickable-name {
+ cursor: pointer;
+ text-decoration: underline;
+ color: lightgreen;
+} \ No newline at end of file
diff --git a/maps-frontend/src/index.css b/maps-frontend/src/index.css
new file mode 100644
index 0000000..72f4b2d
--- /dev/null
+++ b/maps-frontend/src/index.css
@@ -0,0 +1,14 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #121212;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/maps-frontend/src/index.js b/maps-frontend/src/index.js
new file mode 100644
index 0000000..ef2edf8
--- /dev/null
+++ b/maps-frontend/src/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+ReactDOM.render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>,
+ document.getElementById('root')
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();
diff --git a/maps-frontend/src/logo.svg b/maps-frontend/src/logo.svg
new file mode 100644
index 0000000..9dfc1c0
--- /dev/null
+++ b/maps-frontend/src/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file
diff --git a/maps-frontend/src/reportWebVitals.js b/maps-frontend/src/reportWebVitals.js
new file mode 100644
index 0000000..5253d3a
--- /dev/null
+++ b/maps-frontend/src/reportWebVitals.js
@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+ if (onPerfEntry && onPerfEntry instanceof Function) {
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry);
+ getFID(onPerfEntry);
+ getFCP(onPerfEntry);
+ getLCP(onPerfEntry);
+ getTTFB(onPerfEntry);
+ });
+ }
+};
+
+export default reportWebVitals;
diff --git a/maps-frontend/src/setupTests.js b/maps-frontend/src/setupTests.js
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/maps-frontend/src/setupTests.js
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';