A Dockerized application that fetches real-time bus positions from the Porto Digital FIWARE NGSIv2 broker every minute, stores both latest and historical data in Postgres/PostGIS, and displays vehicle positions on an interactive Leaflet map. Designed to grow into a full analytics dashboard (e.g., route travel times by hour/day).
- Automatic ingestion every minute from FIWARE NGSIv2 endpoint
- Two storage layers
bus.vehicle_latest: fast access for “current positions”bus.vehicle_observation: append-only history for analytics
- Map view by route and direction
- Filter by
route(e.g., 704) - Filter by
direction(sentido 0/1)
- Filter by
- Current vs previous position on the map
- Current position green
- Previous position red
- Optional line from previous → current
- REST API for frontend consumption
- Backend API: FastAPI + Uvicorn
- Worker: Python (requests + psycopg3)
- Database: Postgres 16 + PostGIS
- Frontend: React + Vite + TypeScript + Leaflet / React-Leaflet
- Containerization: Docker + Docker Compose
- Docker + Docker Compose installed
docker compose up --buildOr run in background:
docker compose up --build -d
docker compose ps- Frontend:
http://localhost:5173 - API health check:
http://localhost:8000/api/health - Swagger docs:
http://localhost:8000/docs
Check worker logs:
docker compose logs -f --tail=200 backend_workerCheck DB counts:
docker exec -it bus_db psql -U app -d busdb -c "SELECT count(*) FROM bus.vehicle_latest;"
docker exec -it bus_db psql -U app -d busdb -c "SELECT count(*) FROM bus.vehicle_observation;"List available routes currently in the DB:
docker exec -it bus_db psql -U app -d busdb -c \
"SELECT route_id, direction, count(*) FROM bus.vehicle_latest GROUP BY 1,2 ORDER BY count(*) DESC NULLS LAST LIMIT 30;"All latest vehicles:
curl "http://localhost:8000/api/latest"Latest vehicles for route 704, direction 1:
curl "http://localhost:8000/api/latest?route=704&direction=1"Latest for a specific vehicle by fleet id:
curl "http://localhost:8000/api/vehicle/3527"Stop:
docker compose downReset DB (deletes all stored data):
chmod +x scripts/reset-db.sh
./scripts/reset-db.shbus-tracker/
├─ docker-compose.yml
├─ .env.example
├─ README.md
├─ scripts/
│ ├─ init-dev.sh
│ └─ reset-db.sh
│
├─ database/
│ ├─ migrations/
│ │ ├─ 001_init.sql
│ │ └─ 002_indexes.sql
│ ├─ seed/
│ └─ README.md
│
├─ backend/
│ ├─ Dockerfile
│ ├─ pyproject.toml
│ └─ src/
│ ├─ app/ # FastAPI app (read-only services)
│ │ ├─ main.py
│ │ ├─ api/ # routes
│ │ ├─ services/ # DB access logic
│ │ ├─ db/ # DB session + queries
│ │ └─ models/ # Pydantic response models
│ └─ worker/ # ingest pipeline (writes to DB)
│ ├─ ingest.py
│ └─ parse.py
│
├─ frontend/
│ ├─ Dockerfile
│ ├─ package.json
│ ├─ index.html
│ ├─ vite.config.ts
│ ├─ tsconfig.json
│ └─ src/
│ ├─ App.tsx
│ ├─ main.tsx
│ ├─ api/
│ ├─ components/
│ ├─ pages/
│ └─ styles/
│
└─ docs/
├─ architecture.md
└─ api.md
- Trip/run reconstruction
- Derive
trip_runfrom observation history (by route, direction, trip_id, gaps)
- Derive
- Travel time statistics
- Rollups by route × day-of-week × hour-of-day
- p50/p90 travel time estimates
- Improved “previous position”
- Option to compute previous within same route+direction only
- Better realtime UX
- WebSocket / Server-Sent Events instead of polling
- Partitioning / TimescaleDB
- Scale long-term history storage and queries
- Route metadata
- Integrate GTFS or an official route/stop dataset
Contributions are welcome. A typical workflow:
- Fork the repo and create a branch:
git checkout -b feature/my-change
- Make changes and run the stack locally:
docker compose up --build
- Add/adjust docs if needed (
docs/architecture.md,docs/api.md). - Open a PR describing:
- what you changed
- how to test it
- any tradeoffs or known limitations
If you’re making a larger change (e.g., stats pipeline), consider opening an issue first to align on design.