O co chodzi w tej aplikacji
Torowisko to mała aplikacja webowa pokazująca pociągi przejeżdżające w czasie rzeczywistym
obok punktu 52°15'23.8"N 21°04'14.9"E (52.25661, 21.07081) - czyli wschodniego wlotu do
Warszawy Wschodniej, ~1.4 km od peronów, w okolicach rogu ul. Iławskiej i Podskarbińskiej.
Powód powstania: właściciel kupuje mieszkanie 100 m od torów i chciał wiedzieć
"co konkretnie tam jeździ, jak często i jak głośno?". Z czasem do tego doszły inne pytania:
które tory są które (linia 7? linia 2?), jakie EZTy używane (głośne EN57 czy ciche Impulsy?),
kiedy są szczyty ruchu.
Cały projekt powstał w jednej sesji wieczornej w Claude Code
(~4-5 godzin pracy z przerwami). To nerdowski hobby projekt, ale pokazuje całkiem dobrze pełen cykl:
eksploracja źródeł danych → MVP → researche → dodatkowe feature'y → dokumentacja.
Architektura - flow danych
flowchart LR
classDef src fill:#f5ecd9,color:#191919,stroke:#95702a
classDef build fill:#ece6f0,color:#191919,stroke:#6b5a7d
classDef serve fill:#e8efe2,color:#191919,stroke:#5d7a52
classDef ui fill:#cc785c,color:#fff,stroke:#191919
GTFS["mkuran.pl/gtfs
polish_trains.zip
(34 MB)"]:::src
OSM["OSM Overpass API
railway=rail tags"]:::src
WIKI["Wikipedia PL
linie 7/448/452"]:::src
BAZA["bazakolejowa.pl
+ Wikipedia tabor"]:::src
BUILD["build_index.py
parsuje GTFS,
klasyfikuje linie,
oblicza noise_class"]:::build
PICKLE["data/index.pkl
(0.8 MB,
2064 rekordy)"]:::build
APP["app.py (Flask)
/api/passing
/api/heatmap
/api/meta"]:::serve
HTML["templates/index.html
3 zakladki:
Teraz / Ruch / Info"]:::ui
USER["Twoja przegladarka
localhost:5544"]:::ui
GTFS -->|"raz przy buildzie"| BUILD
BUILD --> PICKLE
PICKLE -->|"loadowane do RAM
przy starcie Flask"| APP
OSM -.->|"research #1"| RESEARCH["research/01-tory.md"]:::build
WIKI -.->|"research #1 + #2"| RESEARCH
BAZA -.->|"research #2"| RESEARCH2["research/02-sklady.md"]:::build
APP -->|"JSON co 20s"| HTML
HTML --> USER
Kluczowe decyzje architektoniczne:
- Stateless app. Flask wczytuje pickle przy starcie, trzyma w RAM. Brak bazy. Każdy request liczy z pamięci.
- Pre-compute przy buildzie. 42 MB CSV (stop_times.txt) parsowane raz, do pickle. Startup serwera = 0.5 sek zamiast 30.
- Brak frameworka frontendowego. Vanilla JS + Leaflet (mapa) + Chart.js (heatmap). Wszystko z CDN. HTML serwowany przez Flask Jinja.
- Auto-refresh co 20s. Tab "Teraz" odpytuje
/api/passing. Tab "Ruch" jest statyczny (dane GTFS nie zmieniają się w czasie rzeczywistym).
Skąd dane
Dane rozkładowe pociągów - GTFS od Mikołaja Kurana
mkuran.pl/gtfs to projekt prywatny Mikołaja Kurana który
agreguje rozkłady jazdy wszystkich polskich przewoźników kolejowych do jednego pliku w formacie
GTFS (Google Transit Feed Specification - standardowy format wymiany danych transportowych).
Plik polish_trains.zip ma ~34 MB i zawiera:
- 14 operatorów: PKP IC, KM, PR, SKM Warszawa, KD, KW, KS, KML, ŁKA, Arriva, Leo Express, RegioJet, ODEG, SKM Trójmiasto
- ~35 tys. tripsów w jednym sezonie (grudzień 2025 - sierpień 2026)
- ~3.2 tys. stacji w całej Polsce
- Pola PLK: kategoria pociągu, numer pociągu, numer toru, peron na stacji - kluczowe dla naszego mappingu fizycznych torów
Dlaczego nie API live? Sprawdziliśmy Portal Pasażera PKP (ma proof-of-work challenge na endpointach AJAX
- trzeba by łamać puzzles kryptograficzne) i Koleo (zmienne wersjonowanie API, niezgrabne). GTFS to deterministyczny,
offline, kompletny snapshot - good enough dla MVP.
Dane o torach fizycznych - OpenStreetMap (Overpass API)
OpenStreetMap to mapa świata pisana przez ludzi.
Polski węzeł kolejowy w Warszawie jest zmapowany przez społeczność polskich kolejarzy w detalu
do pojedynczych torów stacyjnych, z numerami linii PKP (ref=7) i numerami torów (railway:track_ref=2W).
Do researchu #1 wyciągnęliśmy te dane przez Overpass API
(queryowy interfejs do OSM), bounding box wokół punktu P. Z 149 wayów torowych wyfiltrowaliśmy 9 najbliższych
do okna Igora. Tak dowiedzieliśmy się dokładnie: 4 najbliższe tory to linia 7 (tory 1W i 2W) plus linia 448 (tory 3M i 4M).
Szczegóły w research/01-tory.md.
Dane o operatorach i taborze - Wikipedia + bazakolejowa.pl
Żeby ocenić jakim taborem (głośnym czy cichym) jeżdżą konkretne pociągi przez punkt P, sięgnęliśmy do:
- Wikipedia PL - profile linii kolejowych (7, 2, 448, 449, 452), profile serii EZT (EN57, EN76 Impuls, ED161 Dart, ED250 Pendolino)
- bazakolejowa.pl - społecznościowa baza zdjęć i historii pojazdów
- rynek-kolejowy.pl - branżowy newsfeed (np. "KM żegna ostatnie EN57 w 2026")
Wynik researchu #2: nie ma publicznego API "numer pociągu → konkretny skład", ale heurystyka
(operator + kategoria + linia) daje ~80% trafień. Szczegóły w research/02-sklady.md.
Skąd "magiczne" liczby na wykresach
Wszystkie wartości pokazywane w aplikacji wywodzą się z konkretnych źródeł, ale po drodze są pewne
uproszczenia i estymaty. Tutaj transparentnie:
Czas dojazdu Wschodnia ↔ punkt P = 120 sekund
Stała w kodzie SECONDS_STATION_TO_P = 120. Wyliczone z geometrii:
odległość peron Wschodnia → punkt P ≈ 1.4 km tor, średnia prędkość przez rozjazdy stacyjne ≈ 50 km/h,
więc czas ≈ 100 sek (zaokrąglone do 120 dla bezpieczeństwa). To znaczy że pociąg odjeżdżający z Wschodniej
o 14:32 mija punkt P o 14:34.
Uproszczenie: faktyczna prędkość zależy od kategorii pociągu (SKM przyspiesza inaczej niż IC),
natężenia ruchu, sygnalizacji. Realny czas może być ±30 sek. To wystarczająca dokładność dla appki która ma
odpowiedzieć "co teraz jedzie obok mojego okna?"
Granica "wschód" = długość geograficzna 21.058°E
Stała EAST_LON_THRESHOLD = 21.058 w build_index.py. Wschodnia ma lon 21.052, więc
wszystko z lon > 21.058 (czyli ~400 m na wschód od peronu) traktujemy jako "wschód". Tak filtrowaliśmy z 5037
wszystkich postojów pociągów na Wschodniej te 2064 które jadą na wschód (mijając nasz punkt P).
Klasyfikacja na linie 7 / 448-2 / 448-449-6
Funkcja classify_line() w build_index.py patrzy na nazwę następnej (lub poprzedniej) stacji
na trasie pociągu i mapuje na linię PKP:
jeśli next_stop ∈ {Warszawa Grochów, Olszynka Grochowska, Wawer, Otwock, Pilawa, Dęblin, Lublin} → linia 7
jeśli next_stop ∈ {Warszawa Rembertów, Sulejówek, Mińsk Mazowiecki, Siedlce, Terespol} → linia 448/2
jeśli next_stop ∈ {Zielonka, Wołomin, Tłuszcz, Białystok} → linia 448/449/6
Wszystko inne to ? (nie powinno się zdarzyć dla pociągów jadących na wschód, w praktyce 0 rekordów).
Klasa hałasu 1-5
Funkcja estimate_noise() w build_index.py - heurystyka per operator + kategoria + linia.
Bazuje na researchu #2 (Wikipedia + dziennikarstwo branżowe). Konkretnie:
| Klasa | Szac. dB (z 30 m) | Typowy tabor | % ruchu przez P |
| 1 | 72-78 dB | Pendolino, Dart, EIC Flirt | ~14% |
| 2 | 72-80 dB | SKM Impuls, KM EN76/ER75/ER160 | ~73% |
| 3 | 80-90 dB | PR EN57/EN71 | ~1% |
| 4 | 85-90 dB | IC wagonowy do Lublina | ~11% |
| 5 | 88-95 dB | TLK, lokomotywy spalinowe | ~1% |
Średnia "ile pociągów dziennie" w heatmapie
GTFS od mkurana ma nieintuicyjną strukturę: każdy dzień ma unikalne service_id. Tylko ~32 dni
z 259 w sezonie mają "pełny" rozkład (>100 aktywnych callsów na Wschodniej) - to dni typowe. Reszta to dni świąteczne, modyfikowane, początkowe.
Heatmap uśrednia tylko po tych 32 "pełnych" dniach (≈4-5 per dzień tygodnia), żeby pokazać realne wzorce.
Stała MIN_CALLS_PER_DAY = 100 w app.py.
Chronologia sesji w Claude Code
Cały projekt to ~4-5h pracy w jednej wieczornej sesji. Pokazuję tu kluczowe momenty
- łącznie z błędami i ślepymi uliczkami, bo właśnie to jest najciekawsze edukacyjnie.
Faza 0 - pierwszy prompt
"Zrób mi szybko appką pokazującą jaki pociąg jedzie po torach na wysokości 52°15'23.8"N..."
Igor rzucił surowy prompt - współrzędne + "skorzystaj z ogólnodostępnych rozkładów". Bez tech stacka,
bez specyfikacji UI. Claude Code (tu: ja) musiał sam zdecydować jak to zrobić. Od tego momentu wszystko
było decyzjami "po drodze".
Faza 1 - eksploracja źródeł danych 3 ślepe uliczki
Próby Portalu Pasażera, Koleo, mapy pociągów
Próba 1: Portal Pasażera PKP - publiczna strona z rozkładem, ale endpoint AJAX wraca HTTP 500.
Po analizie JS bundle (1.8 MB) okazało się że ma proof-of-work challenge przed każdym requestem.
Łamanie tego = niewspółmierna praca. odrzucone
Próba 2: Koleo API - znalazłem ich JSON pod /api/v2/main/stations, ale wymaga
specjalnego nagłówka X-KOLEO-Version i często zmienia wersje. Endpoint connections też kapryśny.
działa ale nietrwałe
Próba 3: mapa pociągów PLK - statyczna, brak live data. nieprzydatne
Rozwiązanie: GTFS od mkurana.pl - statyczny rozkład, deterministyczny, kompletny, offline.
34 MB, parsowane raz przy buildzie. to działa
Faza 2 - MVP
build_index.py + app.py + templates/index.html (4 tasks, ~1h)
Struktura: GTFS → pickle → Flask → HTML z mapą Leaflet + lista pociągów. Heurystyka kierunku: pociąg z next_stop
o lon > 21.058 jedzie na wschód, mija P po ~2 minutach od odjazdu z Wschodniej. Działało od razu, bez większych
poprawek. 2064 rekordy zaklasyfikowane jako "wschodnie", rendering listy "teraz / za chwilę / niedawno".
Faza 3 - researche o torach i taborze
Igor: "zrób mi dwa researche" + screenshot Google z 4 strzałkami
Pytanie 1: które tory są dla mnie najbliższe i kto po nich jeździ?
Pytanie 2: czy są publiczne bazy "numer pociągu → konkretny skład"?
Tu były problemy. Trzy podagenty Explore zostały odpalone równolegle - dwa zinterpretowały
sygnały jako "plan mode" i nic nie zrobiły, jeden padł na context-thrashing. Musiałem wszystko zrobić sam,
ostrożnie batchując WebFetch żeby nie przeładować kontekstu.
Pomocniczo Igor wrzucił screenshot Google Maps z czerwoną ramką "tu mieszkam" i 4 strzałkami na tory -
to dało dużo lepszą lokalizację (ul. Iławska/Podskarbińska 47) niż surowe współrzędne.
Faza 4 - precyzyjne mappowanie torów
Screenshot OpenRailwayMap + Overpass API + GTFS = pełna mapa torów
Drugi screenshot Igora - tym razem z
OpenRailwayMap
zoom 17 na obszar Podskarbińska/Iławska. Widać każdy tor z numerem linii (żółte tagi) i numerem toru (niebieskie).
Na początku źle odczytałem "127" jako linia 127 (nie istnieje na tym odcinku jako pasażerska). Po pobraniu
danych z Overpass API zorientowałem się: tory najbliższe to
linia 7 (1W/2W) +
linia 448 (3M/4M).
Plus odkryliśmy 2 nieelektryfikowane bocznice towarowe bezpośrednio nad blokiem -
największe pytanie do weryfikacji on-site.
Faza 5 - API PLK i maszynista.gov.pl częściowo
Igor dostarczył klucz PLK API i link do maszynista.gov.pl
PLK API (zarządca infrastruktury PKP, ma real-time pozycje pociągów) - klucz Igora w stanie
PendingActivation, czeka na aktywację mailową (2-3 dni). odłożone na Phase 4
maszynista.gov.pl - okazało się że to API UTK dla rejestru maszynistów (świadectwa, badania medyczne,
egzaminy). Brak endpointów o pociągach. odrzucone
Faza 6 - heatmapa ruchu
Tab "Ruch w dobie" z 2 widokami: 7×24 i 24h liniowy
Dodano endpoint /api/heatmap liczący agregaty z GTFS na żywo (3 ms na request - 2k rekordów to nic).
Tu był subtelny bug. Pierwsze testy pokazywały 4.5 pociągów/h max w godzinie szczytu - oczywiście
za mało. Po debug okazało się że GTFS ma dziwną strukturę: większość 259 dni w sezonie ma tylko 1-3 aktywne
service_id (czyli ~0 callsów). Tylko 32 dni mają "pełny" rozkład. Fix: filtruj tylko dni z >100 callsami.
Po fix: 33 pociągów/h w szczycie, jak być powinno.
Faza 7 - deploy guide
docs/deploy-guide.md (5500 słów) - "jak zhostować to LIVE"
Igor poprosił o materiał dla kursantów: jak wziąć appkę z Claude Code do produkcji?
Powstał pełny przewodnik - hosting (VPS vs PaaS vs serverless), baza danych (kiedy potrzebna),
secrets (env vars), domena, HTTPS, monitoring, koszty. Konkretny przepis dla Render.com.
Faza 8 - to co czytasz teraz
Tab "O projekcie" - meta-opis dla kursantów
Pomysł żeby pokazać kursantom proces a nie tylko wynik. Stąd ta zakładka z chronologią, ślepymi uliczkami,
decyzjami. Bo najciekawsze w hobby projektach to nie sama appka, tylko ścieżka jak się do niej doszło.
Co zostało do zrobienia:
(1) Aktywacja klucza PLK API → integracja real-time z opóźnieniami,
(2) Weryfikacja on-site bocznic 3a/123a (czy aktywne lokomotywy spalinowe),
(3) Opcjonalnie deploy na Render.com (przepis gotowy w docs/deploy-guide.md),
(4) Opcjonalnie: kolumna "noise_class" w widoku listy pociągów (kod gotowy w pickle, brakuje UI).
Stack techniczny - czego użyto
Backend
Python 3.9+, Flask 3.x, picklowany indeks GTFS w RAM
Frontend
Vanilla JS (~300 linii), Leaflet (mapa OSM), Chart.js (heatmap day view), Mermaid (diagram flow)
Dane statyczne
GTFS polish_trains.zip od mkuran.pl (34 MB), parsowane do pickle 0.8 MB
Dane do researchu
OpenStreetMap (Overpass API), Wikipedia PL, bazakolejowa.pl, rynek-kolejowy.pl
Style / design
Anthropic-light palette (krem #faf9f5, terracotta #cc785c, ciepłe pastele) - własna konwencja Igora
Środowisko pracy
Claude Code + Chrome MCP do testów wizualnych + macOS
Struktura plików
torowisko/
├── app.py # Flask, 3 endpointy API, ~280 linii
├── build_index.py # Parser GTFS + klasyfikacja, ~250 linii
├── start.sh, stop.sh # Skrypty do detached-mode lokalnego
├── data/
│ ├── polish_trains.zip # GTFS źródłowy, 34 MB
│ └── index.pkl # Pickle (cache), 0.8 MB
├── templates/
│ └── index.html # SPA z 3 zakładkami, ~1000 linii
├── research/
│ ├── 01-tory.md # Mappowanie 4 najbliższych torów
│ └── 02-sklady.md # Bazy taboru + ranking głośności
└── docs/
└── deploy-guide.md # Jak zhostować na produkcję (dla kursantów)
Lekcje dla kursantów
-
Audyt źródeł danych przed kodem. Tu zmarnowałem ~30 min na Portal Pasażera i Koleo zanim
poszedłem do GTFS od mkurana - który okazał się najprostszy i najmocniejszy. Czasami "nudna" opcja statyczna
bije "fajne" API live.
-
MVP w 1h, polish na N dni. Sama appka "co jedzie obok punktu P" powstała w godzinę.
Reszta sesji to researche, refactor, dokumentacja, edge cases. Tak wygląda realna praca.
-
Multimodalność Claude jest gamechangerem. Screenshoty Google Maps i OpenRailwayMap od Igora
dały precyzję której nie byłbym w stanie wyciągnąć z samego API. Wklejaj zdjęcia / mapki / mockupy do Claude Code.
-
Heurystyki zamiast perfekcji. Nie ma publicznego API "numer pociągu → konkretny tabor".
Ale heurystyka
(operator + kategoria + linia) daje 80% trafień - wystarczy do oceny "głośno/cicho".
Zamiast szukać 100%, zaakceptuj 80% z disclaimer.
-
Pokazuj ślepe uliczki. Tutaj transparentnie wszystkie 3 podejścia które nie wyszły
(Portal Pasażera, Koleo, maszynista.gov.pl). Dla kursantów to ważniejsze niż wypolerowany "tutorial" pokazujący
tylko ścieżkę szczęścia.
-
Plan mode kiedy stawka rośnie. Jak Igor poprosił o "2 researche + nową funkcję" - przeszedłem
w plan mode, ustaliliśmy zakres, zatwierdził, dopiero potem kodowanie. Bez tego ryzyko poszło bym w złym kierunku
i marnowali kontekst.
-
Dokumentuj decyzje, nie tylko kod. Pliki
research/01-tory.md i
02-sklady.md są ważniejsze niż sam Flask. Za 3 miesiące to one będą wartościowe (a kod się odbuduje).
Linki do plików projektu
research/01-tory.md - mappowanie 4 najbliższych torów Iławskiej (linia 7 + 448 + bocznice)
research/02-sklady.md - bazy taboru + ranking głośności EZTów + heurystyka noise_class
docs/deploy-guide.md - krok-po-kroku jak zhostować appką na produkcji (~5500 słów dla kursantów)
build_index.py - parser GTFS, klasyfikacja linii, estymata hałasu (~250 linii Pythona)
app.py - Flask app, 3 endpointy API: /api/passing, /api/heatmap, /api/meta
Pliki znajdziesz w katalogu ~/misc-projects/torowisko/ na komputerze Igora.