diff --git a/.gitignore b/.gitignore index ff23e873..3a65a175 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ ##################### # Python ##################### +# testing +output.json +outputlines.json + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/client/package-lock.json b/client/package-lock.json index c727769e..416b5408 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,8 +12,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.8", + "leaflet": "^1.9.4", + "leaflet-realtime": "^2.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -3353,6 +3356,16 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -12401,6 +12414,16 @@ "shell-quote": "^1.8.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-realtime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/leaflet-realtime/-/leaflet-realtime-2.2.0.tgz", + "integrity": "sha512-Z7xbUNdhvwngJUf3plG9Zw3SaGHzcOzi59tVS3jD0GcS6cOWbRNNfP2biwmtHbufM5VYTIWLATrQ4vrlGI23WA==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15123,6 +15146,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", diff --git a/client/package.json b/client/package.json index 47232b24..91225e47 100644 --- a/client/package.json +++ b/client/package.json @@ -7,8 +7,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.8", + "leaflet": "^1.9.4", + "leaflet-realtime": "^2.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/client/src/App.js b/client/src/App.js index 18a0736b..05e3639a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,5 +1,7 @@ import React, { useState, useEffect } from "react"; import Axios from "axios"; +import MapComponent from './components/RealtimeMap'; +import Title from './components/Title'; function App() { const [products, setProducts] = useState([]); @@ -15,16 +17,14 @@ function App() { const products = data; setProducts(products); }; - useEffect(() => { fetchProducts(); }, []); return ( -
- {products.map((product) => ( -

{product.name}

- ))} +
+ + <MapComponent /> </div> ); } diff --git a/client/src/components/Bus.js b/client/src/components/Bus.js new file mode 100644 index 00000000..9379e7c4 --- /dev/null +++ b/client/src/components/Bus.js @@ -0,0 +1,15 @@ +import React, { useState, useEffect } from "react"; +import './css/Vehicle.css'; + +function Bus({ data }) { + return ( + <div> + <h2>{data.type}</h2> + <p>ID: {data.id}</p> + <p>Latitude: {data.position.lat}</p> + <p>Longitude: {data.position.lon}</p> + </div> + ); +} + +export default Bus; diff --git a/client/src/components/RealtimeMap.js b/client/src/components/RealtimeMap.js new file mode 100644 index 00000000..4663f844 --- /dev/null +++ b/client/src/components/RealtimeMap.js @@ -0,0 +1,63 @@ +import React, { useState, useEffect } from "react"; +import L from 'leaflet'; +import './../../node_modules/leaflet/dist/leaflet.css' +import './css/RealtimeMap.css'; +import 'leaflet-realtime'; +import Bus from './Bus'; +import Tram from './Tram'; +import Train from './Train'; +import { Marker } from 'react-leaflet'; +import { MapContainer, TileLayer, useMap } from 'react-leaflet'; + +function RealtimeMap() { + const map = useMap(); + const [data, setData] = useState([]); + + useEffect(() => { + const bounds = map.getBounds(); + const upperLeft = bounds.getNorthWest(); + const lowerRight = bounds.getSouthEast(); + fetch('https://hackhpi24.ivo-zilkenat.de/api/trafficData/', { + 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()) + .then(data => { + setData(data); + }); + }, []); + data["2"] = {"id": "2", "type": "bus", "position": {"lat": 52.3906, "lon": 13.0645}}; + + return Object.values(data).map(item => ( + <Marker position={[item.position.lat, item.position.lon]}> + {item.type === "bus" && <Bus data={item} />} + {item.type === "tram" && <Tram data={item} />} + {item.type === "train" && <Train data={item} />} + </Marker> + )); +} + +function MapComponent() { + return ( + <MapContainer center={[52.3906, 13.0645]} zoom={13} className="map"> + <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> + <RealtimeMap /> + </MapContainer> + ); +} + +export default MapComponent; diff --git a/client/src/components/Title.js b/client/src/components/Title.js new file mode 100644 index 00000000..6afa704f --- /dev/null +++ b/client/src/components/Title.js @@ -0,0 +1,13 @@ +import React from 'react' +import './css/Title.css'; + +function Title() { + return ( + <div className="Title"> + <h1>SFPOPOS</h1> + <div className="Title-Subtitle">San Franciscos Privately Owned Public Spaces</div> + </div> + ) +} + +export default Title \ No newline at end of file diff --git a/client/src/components/Train.js b/client/src/components/Train.js new file mode 100644 index 00000000..7dcd0e48 --- /dev/null +++ b/client/src/components/Train.js @@ -0,0 +1,15 @@ +import React, { useState, useEffect } from "react"; +import './css/Vehicle.css'; + +function Train({ data }) { + return ( + <div> + <h2>{data.type}</h2> + <p>ID: {data.id}</p> + <p>Latitude: {data.position.lat}</p> + <p>Longitude: {data.position.lon}</p> + </div> + ); +} + +export default Train; diff --git a/client/src/components/Tram.js b/client/src/components/Tram.js new file mode 100644 index 00000000..a4ab9078 --- /dev/null +++ b/client/src/components/Tram.js @@ -0,0 +1,15 @@ +import React, { useState, useEffect } from "react"; +import './css/Vehicle.css'; + +function Tram({ data }) { + return ( + <div> + <h2>{data.type}</h2> + <p>ID: {data.id}</p> + <p>Latitude: {data.position.lat}</p> + <p>Longitude: {data.position.lon}</p> + </div> + ); +} + +export default Tram; diff --git a/client/src/components/Vehicle.js b/client/src/components/Vehicle.js new file mode 100644 index 00000000..4f748087 --- /dev/null +++ b/client/src/components/Vehicle.js @@ -0,0 +1,15 @@ +import React, { useState, useEffect } from "react"; +import './css/Vehicle.css'; + +function Vehicle({ data }) { + return ( + <div> + <h2>{data.type}</h2> + <p>ID: {data.id}</p> + <p>Latitude: {data.position.lat}</p> + <p>Longitude: {data.position.lon}</p> + </div> + ); +} + +export default Vehicle; diff --git a/client/src/components/css/RealtimeMap.css b/client/src/components/css/RealtimeMap.css new file mode 100644 index 00000000..b53bce1c --- /dev/null +++ b/client/src/components/css/RealtimeMap.css @@ -0,0 +1,9 @@ +.map { + height: 400px; + width: 800px; + display: block; + margin-left: auto; + margin-right: auto; + border-radius: 0.5rem; + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.4); +} diff --git a/client/src/components/css/Title.css b/client/src/components/css/Title.css new file mode 100644 index 00000000..b2261088 --- /dev/null +++ b/client/src/components/css/Title.css @@ -0,0 +1,11 @@ +.Title { + box-sizing: border-box; + width: 100%; + display: flex; + justify-content: center; + align-items: baseline; + padding: 1em; + margin-bottom: 2em; + background-color: rgb(192, 45, 26); + color: #fff; + } \ No newline at end of file diff --git a/client/src/components/css/Vehicle.css b/client/src/components/css/Vehicle.css new file mode 100644 index 00000000..f6c095d8 --- /dev/null +++ b/client/src/components/css/Vehicle.css @@ -0,0 +1,3 @@ +.bus{ + +} diff --git a/client/src/index.css b/client/src/index.css index ec2585e8..7c036384 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -5,6 +5,8 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: white; + color: #000000; } code { diff --git a/install.py b/install.py index fc3de0c6..983ef50e 100644 --- a/install.py +++ b/install.py @@ -29,8 +29,6 @@ def install_backend_dependencies(): # Linux and macOS use this command run_command('. venv/bin/activate && pip install -r requirements.txt') - print("Dependencies installed successfully.") - os.chdir("..") def main(): diff --git a/server/database.py b/server/database.py new file mode 100644 index 00000000..92d65537 --- /dev/null +++ b/server/database.py @@ -0,0 +1,18 @@ +from typing import Dict, List +from models import trafficDataItem + +trafficDataDict: Dict[str, trafficDataItem.TrafficDataItem] = dict() + +# Example traffic data (you would fetch or compute this data in a real application) +trafficDataDict = { + "1": trafficDataItem.TrafficDataItem( + id="1", + type="Bus", + subType="City Bus", + position=trafficDataItem.Position(lat=40.7128, lon=-74.0060), + line="Line 1", + direction="North", + utilization=trafficDataItem.Utilization(abs=30, rel=0.6) + ), + # Add more items as needed... +} \ No newline at end of file diff --git a/server/fetch_radar.py b/server/fetch_radar.py new file mode 100644 index 00000000..15251a4a --- /dev/null +++ b/server/fetch_radar.py @@ -0,0 +1,64 @@ +import httpx +from fastapi import HTTPException +from models import trafficDataItem +import asyncio +import database + +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") + +async def fetch_radar_data_periodically(period_time: int = 1): + while True: + await asyncio.sleep(period_time) # Wait for n seconds + # print("Fetching radar data...") + radar_data = await fetch_radar_data( + north=52.4288, + west=12.96249, + south=52.35401, + east=13.16608, + results=1024, + duration=period_time * 2, + frames=2, + polylines=True, + language="en" + ) + + database.trafficDataDict = dict() + for movement in radar_data["movements"]: + + database.trafficDataDict[movement["tripId"]] = trafficDataItem.TrafficDataItem( + id=movement["tripId"], + type=movement["line"]["mode"], + subType=movement["line"]["product"], + position=trafficDataItem.Position( + lon=movement["location"]["longitude"], + lat=movement["location"]["latitude"] + ), + line=movement["line"]["name"], + direction=movement["direction"], + utilization=trafficDataItem.Utilization( + abs=None, + rel=None + ) + ) + + \ No newline at end of file diff --git a/server/main.py b/server/main.py index 46e8b20e..ccf40965 100644 --- a/server/main.py +++ b/server/main.py @@ -1,6 +1,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from routers import items, users +from routers import trafficData, updateUtilization +from fetch_radar import fetch_radar_data_periodically +import asyncio app = FastAPI() @@ -13,5 +15,12 @@ allow_headers=["*"], # Allows all headers ) -app.include_router(items.router) -app.include_router(users.router) +app.include_router(trafficData.router, prefix="/api") +app.include_router(updateUtilization.router, prefix="/api") +# app.include_router(radar.router, prefix="/api") + +@app.on_event("startup") +async def app_startup(): + task = asyncio.create_task( + fetch_radar_data_periodically() + ) \ No newline at end of file diff --git a/server/models/item.py b/server/models/item.py deleted file mode 100644 index cb2cd91e..00000000 --- a/server/models/item.py +++ /dev/null @@ -1,8 +0,0 @@ -# /models/item.py -from pydantic import BaseModel - -class Item(BaseModel): - name: str - description: str = None - price: float - tax: float = None diff --git a/server/models/trafficDataItem.py b/server/models/trafficDataItem.py new file mode 100644 index 00000000..b0044361 --- /dev/null +++ b/server/models/trafficDataItem.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field + +# Model Definitions +class Position(BaseModel): + lat: float = Field(..., example=40.7128) + lon: float = Field(..., example=-74.0060) + +class Utilization(BaseModel): + abs: int | None = Field(..., example=50) + rel: float | None = Field(..., example=0.75) + +class TrafficDataItem(BaseModel): + id: str = Field(..., example="1") + type: str = Field(..., example="Bus") + subType: str = Field(..., example="City Bus") + position: Position + line: str = Field(..., example="Line 1") + direction: str = Field(..., example="North") + utilization: Utilization + +# Request Model +class Bounds(BaseModel): + upper_left: Position = Field(..., alias="upper-left") + lower_right: Position = Field(..., alias="lower-right") + +class TrafficDataRequest(BaseModel): + bounds: Bounds \ No newline at end of file diff --git a/server/models/user.py b/server/models/user.py deleted file mode 100644 index 59545c75..00000000 --- a/server/models/user.py +++ /dev/null @@ -1,6 +0,0 @@ -# /models/user.py -from pydantic import BaseModel - -class User(BaseModel): - username: str - full_name: str = None diff --git a/server/requirements.txt b/server/requirements.txt index 9b84904b..af39a68a 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,8 +1,13 @@ annotated-types==0.6.0 anyio==4.3.0 +certifi==2024.2.2 click==8.1.7 +colorama==0.4.6 +exceptiongroup==1.2.0 fastapi==0.110.1 h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 idna==3.6 pydantic==2.6.4 pydantic_core==2.16.3 diff --git a/server/routers/items.py b/server/routers/items.py deleted file mode 100644 index 6a10f21f..00000000 --- a/server/routers/items.py +++ /dev/null @@ -1,8 +0,0 @@ -# /routers/items.py -from fastapi import APIRouter - -router = APIRouter() - -@router.get("/api/items/") -async def read_items(): - return [{"name": "Item Foo"}, {"name": "Item Bar"}] diff --git a/server/routers/radar.py b/server/routers/radar.py new file mode 100644 index 00000000..f002777f --- /dev/null +++ b/server/routers/radar.py @@ -0,0 +1,30 @@ +# 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/server/routers/trafficData.py b/server/routers/trafficData.py new file mode 100644 index 00000000..3b4fc9ac --- /dev/null +++ b/server/routers/trafficData.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter +from models import trafficDataItem +from typing import Dict + +import database + +router = APIRouter() + +@router.post("/trafficData/", response_model=Dict[str, trafficDataItem.TrafficDataItem]) +async def read_trafficData(request: trafficDataItem.TrafficDataRequest): + return database.trafficDataDict \ No newline at end of file diff --git a/server/routers/updateUtilization.py b/server/routers/updateUtilization.py new file mode 100644 index 00000000..b60290fb --- /dev/null +++ b/server/routers/updateUtilization.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter +from models import trafficDataItem + +import database + +router = APIRouter() + +@router.post("/{tripId}/updateUtilization") +async def update_bus_utilization(tripId: str, utilization: trafficDataItem.Utilization): + # Here you can update the utilization data for the specific bus + # For simplicity, this example just stores the data in the `buses_utilization` dictionary + database.trafficDataDict[tripId].utilization = utilization + return {"message": f"Updated utilization for bus {tripId}"} \ No newline at end of file diff --git a/server/routers/users.py b/server/routers/users.py deleted file mode 100644 index 37fdccf4..00000000 --- a/server/routers/users.py +++ /dev/null @@ -1,8 +0,0 @@ -# /routers/users.py -from fastapi import APIRouter - -router = APIRouter() - -@router.get("/api/users/") -async def read_users(): - return [{"username": "johndoe"}, {"username": "janedoe"}]