Easy-R5 · notatka architektoniczna
Jak QGIS rozmawia z R5
R5 to biblioteka Javy — bez CLI do macierzy czasów przejazdu i bez trybu serwerowego, który by je liczył. Każde istniejące wiązanie ładuje więc JVM do wnętrza własnego procesu i woła klasy R5 bezpośrednio: r5r przez rJava, r5py przez JPype. Easy-R5 nie może pójść tą drogą — r5r wymaga R, r5py ciągnie 16 pakietów pip. Zamiast tego wtyczka traktuje R5 jak każdy inny podproces Processing: uruchamia oficjalny jar przez jeden mały plik Javy, przekazuje zadanie w JSON-ie i czyta wynik z CSV oraz protokołu na stdout.
- 1
- plik
.java— cała powierzchnia Javy w projekcie - 0
- pakietów
pipweasy_r5/ - 3
- komendy runnera:
build·matrix·info - 1:43
- macierz 1389 × 956 (Gdańsk, P50) — zweryfikowane vs R5 7.6
Przepływ danych
Dwa procesy, granica i kontrakt
Python przygotowuje wszystko, czego R5 nie policzy sam: reprojektuje
warstwy punktowe do CSV, składa job.json, rozwiązuje ścieżki
do JDK i jara, dobiera -Xmx do RAM-u. runner.py
uruchamia proces potomny i od tej chwili tylko czyta jego stdout —
linia PROGRESS d/t napędza pasek postępu,
feedback.isCanceled() między liniami wywołuje
kill(pid), DONE kończy przebieg. Java robi
wyłącznie routing. Dostępność skumulowana, kontury
izochron, statystyka strefowa, klasyfikacja i style liczą się
z powrotem w Pythonie/QGIS.
EasyR5Runner.java woła klasy R5 i pisze CSV. Wynik wraca tym
samym korytarzem (stdout + plik) i płynie dolnym torem do warstwy QGIS.
Poniżej granicy nie ma ani linijki Pythona; powyżej — ani jednej JVM.
job_spec.pywaliduje parametry (percentyle ≤ 5 rosnąco, tryby, zakres dat, wyprowadzamax_walk_time) →job.jsonw katalogu tymczasowym.points.pyprzerzuca warstwy QGIS doorigins.csv/destinations.csvw EPSG:4326, ze stabilnymiid.network_cache.pyznajdujenetwork.datpo kluczusha256(osm) + sha256(gtfs…) + wersja R5; niezgodna wersja = przebudowa.java_env.pybierze ścieżki JDK i jara z QSettings (weryfikacja SHA-256), wykrywa RAM, dobiera-Xmx = min(0,6 · RAM, 12 GB).runner.pyskłada komendę, uruchamia proces potomny, czyta stdout linia po linii:PROGRESS→ pasek,ERROR <kod>→ komunikat PL/EN,DONE→ koniec.EasyR5Runner.javaczytajob.json, budujeRegionalTask, w pętli poorigin_rangewołaTravelTimeComputer, pisze wiersze CSV, streamuje postęp.matrix.pyscala CSV z batchy →QgsVectorLayerz kompletem pól metody (r5_version,run_date,departure_time,percentile,modes).
RESULT transit_used_pairs=<n> —
liczbę par OD szybszych niż dojście pieszo. Zero oznacza, że R5 policzył
same trasy piesze (najczęściej: data bez aktywnych kursów GTFS,
którą R5 przyjmuje bez błędu). Python przerywa wtedy z komunikatem. Ten
dokładny błąd raz trafił na produkcję w tools/
(GZM, sierpień 2026) — stąd dwa niezależne mechanizmy: twarda
walidacja daty przed startem i ten detektor po.
Nazewnictwo
Adapter? Wrapper? Jak to nazwać
To nie jest „wrapper” ani „binding” w sensie bibliotecznym. r5r i r5py owijają API R5 klasa po klasie i wystawiają je w innym języku, w tym samym procesie. Tu nie ma owijania API — jest własny, wąski kontrakt zadaniowy (trzy komendy), a R5 żyje po drugiej stronie granicy procesu i nigdy nie dotyka interpretera QGIS-a. Rozbicie na warstwy i precyzyjne słowa:
- out-of-process bindingwiązanie przez proces potomny
- Cały mechanizm. W opozycji do in-process (r5r = rJava, r5py = JPype), gdzie JVM ładuje się do wnętrza hosta. Tu JVM to osobny PID.
- cienki adapter CLIEasyR5Runner.java
- R5 nie ma CLI do macierzy ani dostępności — ten jeden plik jest
tym CLI. Adaptuje bibliotekę Javy do interfejsu linii komend
o stabilnym kontrakcie. Zakres:
build,matrix,info(późniejitinerary). - sterownik procesueasy_r5/core/runner.py
- Buduje linię komend, uruchamia dziecko, parsuje protokół stdout, robi
pasek postępu i anulowanie (
kill(pid)), mapuje kodyERRORna komunikaty, sprząta wfinally. - runnernazwa w kodzie i w rozmowie
- Tak jak w easy-OTP. Konkretniej: job runner, nie serwer — w przeciwieństwie do OTP, które easy-OTP odpala jako serwer HTTP. Nie „adapter”, nie „wrapper”, nie „bridge”.
W skrócie
Easy-R5 nie „owija” R5 jak r5r czy r5py — uruchamia oficjalny jar jako proces potomny, sterowany jednym małym plikiem Javy, który dostaje zadanie w JSON-ie i oddaje wynik w CSV plus protokół postępu na stdout.
To ten sam wzorzec, którym QGIS Processing woła GDAL (ogr2ogr,
gdal_contour), SAGA i GRASS — podproces z wejściem
i wyjściem plikowym, nie biblioteka in-process. Nie jest egzotyczny;
jest natywny dla QGIS-a.
Alternatywy
Dlaczego nie r5r i nie r5py
Punkt wyjścia to twarde ograniczenie odziedziczone po easy-OTP: wtyczka
musi działać na czystej instalacji QGIS — żadnego
pip install, R, condy, Dockera. Pobieranie binariów
przy setupie (JDK, jar) jest dozwolone — precedens DownloadJre.
Instalowanie pakietów Pythona do interpretera QGIS-a — nie.
tools/ naprawdę padał na 12 GB
sterty przy Warszawie. Proces potomny izoluje awarię, heap i anulowanie.
| Kryterium | r5r | r5py | JPype-only | jar + runner (nasz) |
|---|---|---|---|---|
| Działa na czystym QGIS | ✗ wymaga R | ✗ 16 pakietów pip | ~ 1 dep kompilowany | ✓ |
| Java do utrzymania | brak | brak | brak | ~1 plik |
| Crash JVM kładzie QGIS | — | — | ✗ tak | ✓ nie |
| Heap per run | — | — | ✗ | ✓ |
| Anulowanie = kill procesu | — | — | ✗ | ✓ |
| Potrzebny JDK (nie JRE) | JDK 21 | JDK 21 | JDK/JRE 21 | JDK 21 (lub prekompilacja) |
| Zmiana API R5 psuje… | upstream | upstream | nas | nas |
r5r — odpada natychmiast
Wymaga R + rJava. CLAUDE.md: „ZERO R,
ZERO GRASS”. R5 żyje w R przez in-process JVM przez własny jar
r5r_core (~250 KB Javy w kształcie ramek danych R:
RDataFrame, build przeciw JRI.jar) — nieużywalny
bez R nawet przy kompilacji. Zostaje jako referencja
zachowania: wszystkie skrypty w tools/ go
używają, więc definiuje, jaki wynik ma wychodzić (M4 odtwarza
gdansk_service_accessibility.csv co do wiersza).
r5py — odpada jako zależność runtime
16 pakietów pip, w tym kompilowane:
jpype1, rasterio, simplification,
scikit-learn, plus geopandas. To drzewo
w kształcie condy, nie „jeden wąski wyjątek” jak openpyxl.
API r5py opiera się też mocno na semantyce GeoPandas, której wtyczka QGIS
nie potrzebuje. Zostaje jako najlepszy wzorzec „jak wołać klasy
R5” — jego src/r5py/r5/*.py to faktyczna
dokumentacja kolejności wywołań. Czytać, nie shippować.
JPype-only — plan B, udokumentowany, nie wybrany
pip install jpype1 jako jedyny wyjątek, potem wołać
com.conveyal.r5.* z Pythona samemu. Zero Javy do
napisania, jedna zależność. Odrzucone jako domyślne, bo
jpype1 to kompilowany wheel, który musi trafić w dokładne
ABI Pythona QGIS-a (3.9 w 3.22 LTR, 3.12 w nowszych)
i platformę — dużo bardziej kruche niż pure-Pythonowy trik
z openpyxl. I bo JVM w procesie QGIS-a znaczy:
OOM kładzie QGIS, heap zamrożony na sesję, brak czystego anulowania.
Serwer debug R5 — odpada
PointToPointRouterServer to jedyny „gotowy” serwer w jarze,
ale robi routing punkt-do-punktu do debugowania — bez endpointu macierzy
i dostępności. Oddaje dokładnie tę zdolność, po którą bierzemy R5.
Kontrakt runnera
Co przechodzi przez granicę
Jedna linia = jedna wiadomość, UTF-8, \n. Python mapuje kody
ERROR na komunikaty dla użytkownika — żaden stack trace Javy
nie trafia do GUI.
EasyR5Runner.java musi zostać jednym plikiem —
single-file source launcher (JEP 330) w Javie 21 kompiluje jedną
jednostkę kompilacji; multi-file source programs to dopiero Java 22+.
Klasy pakietowo-prywatne w tym samym pliku są OK. Mapowanie na
RegionalTask jest sprawdzone w spike'u i nie zmienia
się bez powodu.
Koszty
Co za to płacimy i warunek odwrócenia
-
Utrzymujemy trochę Javy przeciw API, które upstream
jawnie odmawia stabilizować. Mitygacja: pin wersji R5 (ADR-0002),
minimalna powierzchnia runnera,
command=infojako smoke test po każdym podbiciu. - Potrzebny JDK (~180 MB), nie JRE (~45 MB) — chyba że shippujemy skompilowaną klasę, co przywraca krok buildu.
-
Jeden plik = jedna jednostka kompilacji do Javy 21.
Runner musi zostać jednym plikiem; jak przekroczy — osobne repo MIT
z jarem, jak
easy-GTFS-RT. -
Bierzemy easy-OTP-owy wzorzec, nie kod semantyki OTP.
Zarządzanie procesem, pasek postępu, anulowanie,
DownloadJre→ skopiowane. Semantyka OTP — nie.
easy_r5/core/ nie zależy od tego, którego z dwóch użyto.