diff --git a/client/src/App.js b/client/src/App.js index 0dcad028..a316c69c 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,13 +1,15 @@ import React from "react"; import "./App.css" import MapComponent from './components/RealtimeMap'; -import HeaderBanner from "./components/HeaderBanner"; +import Banner from "./components/Banner"; +import InputBox from "./components/inputBox"; function App() { return (
-
+ +
); diff --git a/client/src/components/ApiCall.js b/client/src/components/ApiCall.js new file mode 100644 index 00000000..d0ca8cc1 --- /dev/null +++ b/client/src/components/ApiCall.js @@ -0,0 +1,28 @@ +export function fetchCurrentData(map, route) { + const bounds = map.getBounds(); + const upperLeft = bounds.getNorthWest(); + const lowerRight = bounds.getSouthEast(); + const hostname = window.location.hostname; + const protocol = window.location.protocol; + const port = hostname === "localhost" ? "3001" : "443"; + + return fetch(`${protocol}//${hostname}:${port}/api/${route}/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + "bounds": { + "upper-left": { + "lat": upperLeft.lat, + "lon": upperLeft.lng + }, + "lower-right": { + "lat": lowerRight.lat, + "lon": lowerRight.lng + } + } + }) + }) + .then(response => response.json()); +} diff --git a/client/src/components/Banner.js b/client/src/components/Banner.js index e69de29b..1b9b550a 100644 --- a/client/src/components/Banner.js +++ b/client/src/components/Banner.js @@ -0,0 +1,21 @@ +import React, { useEffect, useRef } from "react"; +import "./css/Banner.css"; + +function Banner() { + return ( +
+
Wischen & Mischen
+
{/* This will take up the unused space */} +
+
Home
+
Data Solutions
+
About
+
Contact
+ {/* Add more nav items as needed */} +
+
+ + ); +} + +export default Banner; \ No newline at end of file diff --git a/client/src/components/RealtimeMap.js b/client/src/components/RealtimeMap.js index 4de7f944..7c31184d 100644 --- a/client/src/components/RealtimeMap.js +++ b/client/src/components/RealtimeMap.js @@ -2,97 +2,48 @@ import React, { useState, useEffect } from "react"; import L from 'leaflet'; import './../../node_modules/leaflet/dist/leaflet.css' import './css/RealtimeMap.css'; +import './css/Vehicle.css'; import 'leaflet-realtime'; -import Vehicle from './Vehicle.js'; -import { Marker } from 'react-leaflet'; +import { Marker, Circle } from 'react-leaflet'; import { Popup } from 'react-leaflet'; import { MapContainer, TileLayer, useMap } from 'react-leaflet'; -import busIconUrl from '../resources/bus_icon.png'; -import trainIconUrl from '../resources/train_icon.png'; -import tramIconUrl from '../resources/tram_icon.png'; -import subwayIconUrl from '../resources/subway_icon.png'; -import ferryIconUrl from '../resources/ferry_icon.png'; -import expressIconUrl from '../resources/express_icon.png'; -import suburbanIconUrl from '../resources/suburban_icon.png'; +import VehicleIcons from './VehicleIcons'; +import { fetchCurrentData } from './ApiCall'; +import { getIcon, getColor } from './VehicleIcons'; -const busIcon = L.icon({ - iconUrl: busIconUrl, - iconSize: [40, 40], -}); - -const trainIcon = L.icon({ - iconUrl: trainIconUrl, - iconSize: [25, 41], -}); - -const tramIcon = L.icon({ - iconUrl: tramIconUrl, - iconSize: [25, 41], -}); - -const subwayIcon = L.icon({ - iconUrl: subwayIconUrl, - iconSize: [25, 41], -}); - -const ferryIcon = L.icon({ - iconUrl: suburbanIconUrl, - iconSize: [25, 41], -}); - -const expressIcon = L.icon({ - iconUrl: expressIconUrl, - iconSize: [25, 41], -}); - -const suburbanIcon = L.icon({ - iconUrl: ferryIconUrl, - iconSize: [25, 41], -}); +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} function RealtimeMap() { const map = useMap(); const [data, setData] = useState([]); + const [stations, setStations] = useState([]); useEffect(() => { const interval = setInterval(() => { - const bounds = map.getBounds(); - const upperLeft = bounds.getNorthWest(); - const lowerRight = bounds.getSouthEast(); - const hostname = window.location.hostname; - const protocol = window.location.protocol; - const port = hostname === "localhost" ? "3001" : "443"; - fetch(`${protocol}//${hostname}:${port}/api/trips/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - "bounds": { - "upper-left": { - "lat": upperLeft.lat, - "lon": upperLeft.lng - }, - "lower-right": { - "lat": lowerRight.lat, - "lon": lowerRight.lng - } - } - }) - }) - .then(response => response.json()) + fetchCurrentData(map, 'trips') .then(data => { setData(data); }); }, 1000); - return () => clearInterval(interval); -}, []); + return () => clearInterval(interval); + }, []); - return Object.values(data).map(item => { + useEffect(() => { + fetchCurrentData(map, 'stations') + .then(data => { + setStations(data); + }); + }, []); + + const tripMarkers = Object.values(data).map(item => { let icon; let colorClass; - if (item.utilization.rel < 0.3) { + if (item.utilization.rel == null) { + colorClass = 'gray-icon'; + } else if (item.utilization.rel < 0.3) { colorClass = 'green-icon'; } else if (item.utilization.rel >= 0.3 && item.utilization.rel <= 0.7) { colorClass = 'yellow-icon'; @@ -100,39 +51,17 @@ function RealtimeMap() { colorClass = 'red-icon'; } - switch (item.subtype) { - case "suburban": - icon = suburbanIcon; - break; - case "subway": - icon = subwayIcon; - break; - case "tram": - icon = tramIcon; - break; - case "bus": - icon = busIcon; - break; - case "ferry": - icon = ferryIcon; - break; - case "express": - icon = expressIcon; - break; - case "regional": - icon = trainIcon; - break; - default: - icon = busIcon; - break; - } + icon = getIcon(item.subType); - icon.options.className = colorClass; + icon.options.className = `${colorClass} vehicle-icon`; return (

+ Type: {capitalizeFirstLetter(item.type)}
+ Line: {item.line} | Direction: {item.direction}
+
Absolute Utilization: {item.utilization.abs}
Relative Utilization: {item.utilization.rel}

@@ -140,11 +69,28 @@ function RealtimeMap() {
); }); + + const stationMarkers = Object.values(stations).map(item => { + let color = getColor(item.products); + return ( + + +

{item.name}

+

+ Absolute Utilization: {item.utilization.abs}
+ Relative Utilization: {item.utilization.rel} +

+
+
+ ); + }); + + return [...tripMarkers, ...stationMarkers]; } function MapComponent() { return ( - + diff --git a/client/src/components/Vehicle.js b/client/src/components/Vehicle.js deleted file mode 100644 index 7dcd0e48..00000000 --- a/client/src/components/Vehicle.js +++ /dev/null @@ -1,15 +0,0 @@ -import React, { useState, useEffect } from "react"; -import './css/Vehicle.css'; - -function Train({ data }) { - return ( -
-

{data.type}

-

ID: {data.id}

-

Latitude: {data.position.lat}

-

Longitude: {data.position.lon}

-
- ); -} - -export default Train; diff --git a/client/src/components/VehicleIcons.js b/client/src/components/VehicleIcons.js new file mode 100644 index 00000000..e9d2070e --- /dev/null +++ b/client/src/components/VehicleIcons.js @@ -0,0 +1,81 @@ +import L from 'leaflet'; +import busIconUrl from '../resources/bus_icon.png'; +import trainIconUrl from '../resources/train_icon.png'; +import tramIconUrl from '../resources/tram_icon.png'; +import subwayIconUrl from '../resources/subway_icon.png'; +import ferryIconUrl from '../resources/ferry_icon.png'; +import expressIconUrl from '../resources/express_icon.png'; +import suburbanIconUrl from '../resources/suburban_icon.png'; + +const busIcon = L.icon({ + iconUrl: busIconUrl, + iconSize: [40, 40], +}); + +const trainIcon = L.icon({ + iconUrl: trainIconUrl, + iconSize: [25, 25], +}); + +const tramIcon = L.icon({ + iconUrl: tramIconUrl, + iconSize: [40, 40], +}); + +const subwayIcon = L.icon({ + iconUrl: subwayIconUrl, + iconSize: [40, 40], +}); + +const ferryIcon = L.icon({ + iconUrl: ferryIconUrl, + iconSize: [40, 40], +}); + +const expressIcon = L.icon({ + iconUrl: expressIconUrl, + iconSize: [40, 40], +}); + +const suburbanIcon = L.icon({ + iconUrl: suburbanIconUrl, + iconSize: [32, 32], +}); + + +export function getIcon(subtype) { + switch (subtype) { + case "suburban": + return suburbanIcon; + case "subway": + return subwayIcon; + case "tram": + return tramIcon; + case "bus": + return busIcon; + case "ferry": + return ferryIcon; + case "express": + return expressIcon; + case "regional": + return trainIcon; + default: + return busIcon; + } +} + +export function getColor(products) { + if (products.suburban) { + return "green"; + } else if (products.subway) { + return "red"; + } else if (products.tram) { + return "blue"; + } else if (products.bus) { + return "yellow"; + } else if (products.ferry) { + return "purple"; + } else { + return "black"; + } +} diff --git a/client/src/components/css/Banner.css b/client/src/components/css/Banner.css new file mode 100644 index 00000000..b932afb6 --- /dev/null +++ b/client/src/components/css/Banner.css @@ -0,0 +1,70 @@ +/* In Navbar.css */ +.navbar { + display: flex; + flex-direction: row; /* Default direction */ + align-items: center; + justify-content: space-between; /* Align items with space between */ + background-color: #333; + color: white; + padding: 5px 20px 5px 0px; /* Added padding to the right and left */ + width: 100%; /* Full width */ + position: fixed; + z-index: 1000; /* Ensures it's above the map */ +} + +.brand { + padding: 10px; + font-size: large; + font-weight: bold; + /* margin-left: 50px; */ /* Ensures some space between the brand and the nav items */ +} + +.nav-items { + display: flex; /* Display nav items in a row */ +} + +.nav-item { + margin: 0 10px; + padding: 5px 10px; /* Padding inside each nav item for better touch targets */ + cursor: pointer; + background-color: #3c3c3c; /* Slightly darker background for clickable items */ + border-radius: 5px; /* Rounded borders for a softer look */ + transition: background-color 0.3s; /* Smooth transition for hover effect */ +} + +.nav-item:hover { + background-color: #555; /* Slightly lighter on hover for feedback */ + text-decoration: none; /* Overrides the underline from previous styles if needed */ +} + +.nav-item a { + color: inherit; /* Makes the link color the same as the parent element */ + text-decoration: none; /* Removes the underline from links */ + cursor: default; /* Changes the cursor to the default arrow, not the pointer typically used for links */ +} + +/* Optional: Style for hover effect to make it look like normal text */ +.nav-item a:hover { + text-decoration: none; +} + +/* Media query for screens smaller than 600px */ +@media (max-width: 600px) { + .navbar { + flex-direction: column; /* Stack items vertically */ + align-items: center; /* Center items */ + } + + .nav-items { + flex-direction: column; /* Stack nav items vertically */ + } + + .nav-item { + margin: 5px 0; /* Adjust margin for vertical layout */ + } + + .spacer { + display: none; /* Hide spacer in mobile view */ + } +} + diff --git a/client/src/components/css/InputBox.css b/client/src/components/css/InputBox.css new file mode 100644 index 00000000..d0319a26 --- /dev/null +++ b/client/src/components/css/InputBox.css @@ -0,0 +1,18 @@ +.translucent-box { + background-color: rgba(255, 255, 255, 0.7); /* Translucent white background */ + border: 1px solid #ccc; + border-radius: 5px; + padding: 20px; + width: 300px; + margin: 0 auto; + z-index: 999; + } + + .input-field { + width: 100%; + padding: 10px; + margin-bottom: 10px; + z-index: 9999; + } + + \ No newline at end of file diff --git a/client/src/components/css/RealtimeMap.css b/client/src/components/css/RealtimeMap.css index d1e68db8..858ebfa3 100644 --- a/client/src/components/css/RealtimeMap.css +++ b/client/src/components/css/RealtimeMap.css @@ -1,22 +1,8 @@ .map { - height: 600px; - width: 95%; + height: 100vh; + width: 100%; display: block; margin-left: auto; margin-right: auto; - margin-top: 100px; - border-radius: 0.5rem; - box-shadow: 0 0 1rem rgba(0, 0, 0, 0.4); } -.green-icon { - filter: hue-rotate(90deg) brightness(150%); -} - -.yellow-icon { - filter: hue-rotate(45deg) brightness(150%); -} - -.red-icon { - filter: hue-rotate(0deg) brightness(150%); -} diff --git a/client/src/components/css/Vehicle.css b/client/src/components/css/Vehicle.css index f6c095d8..e178799f 100644 --- a/client/src/components/css/Vehicle.css +++ b/client/src/components/css/Vehicle.css @@ -1,3 +1,31 @@ -.bus{ +.vehicle-icon { + padding: 10px !important; + background-color: white; /* Icon background color - blue */ + border-radius: 50%; /* Circular shape */ + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); /* Subtle shadow for depth */ + color: white; /* Icon text color */ + font-size: 12px; /* Icon text size */ + font-family: Arial, sans-serif; /* Font for any text inside the icon */ + border-width: 4px; + border-style: solid; +} + +.gray-icon { + border-color: gray; +} + +.green-icon { + border-color: green; +} + +.yellow-icon { + border-color: orange; +} +.red-icon { + border-color: red; } + \ No newline at end of file diff --git a/client/src/components/inputBox.js b/client/src/components/inputBox.js new file mode 100644 index 00000000..e8e7ec8a --- /dev/null +++ b/client/src/components/inputBox.js @@ -0,0 +1,114 @@ +import React, { useState } from 'react'; + +function App() { + const [input1, setInput1] = useState(''); + const [input2, setInput2] = useState(''); + const [suggestedStations1, setSuggestedStations1] = useState([]); + const [suggestedStations2, setSuggestedStations2] = useState([]); + + const handleInputChange1 = async (event) => { + const query = event.target.value; + setInput1(query); + if (query.trim() === '') { + setSuggestedStations1([]); // Clear suggestions if input is empty + return; + } + try { + const response = await fetch(`https://v6.vbb.transport.rest/stations?query=${query}`, { + headers: { + 'Accept': 'application/x-ndjson' + } + }); + const reader = response.body.getReader(); + let decoder = new TextDecoder(); + let partialChunk = ''; + const stations = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + partialChunk += decoder.decode(value, { stream: true }); + const lines = partialChunk.split('\n'); + lines.forEach(line => { + if (line.trim() !== '') { + const station = JSON.parse(line); + stations.push(station.name); + } + }); + partialChunk = lines.pop() || ''; + } + setSuggestedStations1([...new Set(stations.slice(0, 3))]); // Update suggested stations for input1, filtering duplicates + } catch (error) { + console.error('Error fetching stations:', error); + } + }; + + const handleInputChange2 = async (event) => { + const query = event.target.value; + setInput2(query); + if (query.trim() === '') { + setSuggestedStations2([]); // Clear suggestions if input is empty + return; + } + try { + const response = await fetch(`https://v6.vbb.transport.rest/stations?query=${query}`, { + headers: { + 'Accept': 'application/x-ndjson' + } + }); + const reader = response.body.getReader(); + let decoder = new TextDecoder(); + let partialChunk = ''; + const stations = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + partialChunk += decoder.decode(value, { stream: true }); + const lines = partialChunk.split('\n'); + lines.forEach(line => { + if (line.trim() !== '') { + const station = JSON.parse(line); + stations.push(station.name); + } + }); + partialChunk = lines.pop() || ''; + } + setSuggestedStations2([...new Set(stations.slice(0, 3))]); // Update suggested stations for input2, filtering duplicates + } catch (error) { + console.error('Error fetching stations:', error); + } + }; + + return ( +
+ + + {suggestedStations1.map((station, index) => ( + +
+ + + {suggestedStations2.map((station, index) => ( + +
+ ); +} + +export default App; diff --git a/client/src/resources/logo.png b/client/src/resources/logo.png new file mode 100644 index 00000000..9edd8dbd Binary files /dev/null and b/client/src/resources/logo.png differ diff --git a/client/src/resources/tram_icon.png b/client/src/resources/tram_icon.png index 316f453c..1936032f 100644 Binary files a/client/src/resources/tram_icon.png and b/client/src/resources/tram_icon.png differ diff --git a/randomutil.py b/dev/randomutil.py similarity index 100% rename from randomutil.py rename to dev/randomutil.py diff --git a/output.txt b/output.txt new file mode 100644 index 00000000..687be7c1 --- /dev/null +++ b/output.txt @@ -0,0 +1,60 @@ +[2024-04-06 05:06:54] +[2024-04-06 05:06:55] +[2024-04-06 05:06:56] +[2024-04-06 05:06:57] +[2024-04-06 05:06:57] +[2024-04-06 05:06:58] +[2024-04-06 05:06:59] +[2024-04-06 05:07:00] +[2024-04-06 05:07:00] +[2024-04-06 05:07:01] +[2024-04-06 05:07:02] +[2024-04-06 05:07:03] +[2024-04-06 05:07:03] +[2024-04-06 05:07:04] +[2024-04-06 05:07:05] +[2024-04-06 05:07:05] +[2024-04-06 05:07:06] +[2024-04-06 05:07:07] +[2024-04-06 05:07:08] +[2024-04-06 05:07:08] +[2024-04-06 05:07:09] +[2024-04-06 05:07:10] +[2024-04-06 05:07:11] +[2024-04-06 05:07:12] +[2024-04-06 05:07:12] +[2024-04-06 05:07:13] +[2024-04-06 05:07:14] +[2024-04-06 05:07:15] +[2024-04-06 05:07:16] +[2024-04-06 05:07:16] +[2024-04-06 05:07:17] +[2024-04-06 05:07:18] +[2024-04-06 05:07:19] +[2024-04-06 05:07:19] +[2024-04-06 05:07:20] +[2024-04-06 05:07:21] +[2024-04-06 05:07:21] +[2024-04-06 05:07:23] +[2024-04-06 05:07:23] +[2024-04-06 05:07:24] +[2024-04-06 05:07:26] +[2024-04-06 05:07:26] +[2024-04-06 05:07:27] +[2024-04-06 05:07:28] +[2024-04-06 05:07:29] +[2024-04-06 05:07:29] +[2024-04-06 05:07:30] +[2024-04-06 05:07:31] +[2024-04-06 05:07:32] +[2024-04-06 05:07:32] +[2024-04-06 05:07:33] +[2024-04-06 05:07:34] +[2024-04-06 05:07:34] +[2024-04-06 05:07:35] +[2024-04-06 05:07:36] +[2024-04-06 05:07:37] +[2024-04-06 05:07:37] +[2024-04-06 05:07:38] +[2024-04-06 05:07:39] +[2024-04-06 05:07:40] diff --git a/server/fetch_stations.py b/server/fetch_stations.py index 5d7255a2..8aa836a0 100644 --- a/server/fetch_stations.py +++ b/server/fetch_stations.py @@ -38,7 +38,7 @@ async def fetch_station_data_periodically(period_time: int = 1800): # Map the external API response to your StationInfo model # This is a basic mapping, adjust according to the actual response structure and your model - database.stationDataDict[stationId] = database.station.StationDataItem( + newStationDataDict[stationId] = database.station.StationDataItem( id=stationId, name=item["name"], position=core.Position( diff --git a/server/routers/radar.py b/server/routers/radar.py deleted file mode 100644 index f002777f..00000000 --- a/server/routers/radar.py +++ /dev/null @@ -1,30 +0,0 @@ -# import httpx -# from fastapi import APIRouter, HTTPException - -# router = APIRouter() - -# async def fetch_radar_data(north: float, west: float, south: float, east: float, results: int = 256, duration: int = 30, frames: int = 3, polylines: bool = True, language: str = "en"): -# url = "https://v6.vbb.transport.rest/radar" -# params = { -# "north": north, -# "west": west, -# "south": south, -# "east": east, -# "results": results, -# "duration": duration, -# "frames": frames, -# "polylines": polylines, -# "language": language -# } - -# async with httpx.AsyncClient() as client: -# response = await client.get(url, params=params) - -# if response.status_code == 200: -# return response.json() -# else: -# raise HTTPException(status_code=response.status_code, detail="Failed to fetch radar data from VBB API") - -# @router.get("/api/radar") -# async def api_vbb_radar(north: float, west: float, south: float, east: float, results: int = 256, duration: int = 30, frames: int = 3, polylines: bool = True, language: str = "en"): -# return await fetch_radar_data(north, west, south, east, results, duration, frames, polylines, language) \ No newline at end of file diff --git a/start_all_videos.py b/stream-processing/start_all_videos.py similarity index 100% rename from start_all_videos.py rename to stream-processing/start_all_videos.py