Městská a regionální hromadná doprava v Česku patří bezesporu k těm nejlepším na světě. Není divu, na rozdíl třeba od USA u nás došlo k masivnímu a tržnímu rozpuku individuální automobilové dopravy až s pádem minulého režimu.
Vozidla jsou často vybavená přijímačem satelitní polohy, a tak bylo jen otázkou času, než dopravci vyrukují s mobilními a webovými aplikacemi, které na mapě a prakticky v reálnem čase zobrazí blížící se autobus či tramvaj.
První bylo Brno, pak přibyla i Praha, no a obě města dnes zároveň zveřejňují polohová data o svém vozovém parku skrze otevřená a bezplatná API. Využít je může každý z vás a postavit nad nimi prakticky libovolnou aplikaci:
Všechny tramvaje, autobusy a vlaky okolo Prahy
Dnes si to vyzkoušíme na co možná nejjednodušší aplikaci v Javascriptu a HTML. Výsledkem bude soubor salinoleaks.html, který po načtení v prohlížeči zobrazí pomocí knihovny Leaflet mapu OSM, načež se spojí se serverem Golemio API a stáhne z něj obrovský JSON se všemi autobusy, tramvajemi, vlaky a dalšími vozidly, které jsou součástí systému pražské integrované dopravy PID.
Sledujeme pražskou integrovanou dopravu ve své vlastní mapě se statistikou v javascriptové konzoli prohlížeče
Aby to mohlo fungovat, nejprve si na webu Golemio API vygenerujeme osobní klíč API, pomocí kterého se budeme identifikovat. Jak už jsme si řekli výše, přístup k API je sice bezplatný, takže se nebojte žádných paušálů, klíč je ale potřeba i z toho důvodu, aby mohl správce systému zablokovat třeba ty uživatele, kteří API příliš zatěžují.
Golemio API, dej mi seznam všech vozidel
Tak, máme klíč v podobě dlouhého textového řetězce a teď už se tedy pojďme zeptat serveru, kde se právě teď pohybují všechna vozidla PID vybavené satelitním přijímačem. Slouží k tomu HTTP GET dotaz /vehiclepositions.
Opravdu velmi dlouhá odpověď – strukturovaný text ve formátu JSON se stovkami vozidel pražské integrované dopravy
Na odkazu výše najdete všechny jeho parametry a když jej zavoláte v té nejjednodušší podobě https://api.golemio.cz/v2/vehiclepositions, tak…
Tak se nic nestane a server odpoví chybovým hlášením, protože nejsme přihlášení. Nepoužili jsme totiž ten klíč API, který musíme serveru odeslat ve speciální hlavičce x-access-token.
Stahujeme JSON v příkazové řádce
Na linuxových systémech by nám v tom pomohl třeba textový HTTP klient cURL a příkaz, který výsledek uloží do souboru praha.json:
curl --header "x-access-token: API klic" https://api.golemio.cz/v2/vehiclepositions > praha.json
Stažení obrovského JSON a uložení do souboru praha.json na Linuxu pomocí curl
Na Windows a v jeho PowerShellu bychom téhož dosáhli příkazem:
iwr https://api.golemio.cz/v2/vehiclepositions -Headers @{"x-access-token" = "API klic"} -OutFile praha.json
Stažení obrovského JSON a uložení do souboru praha.json v PowerShellu na Windows
Iwr je zkratka pro powershellový HTTP klient Invoke-WebRequest.
A teď si to vyzkoušíme v moderním Javascriptu
Dobrá, příkazovou řádku bychom měli, ovšem my si data stáhneme v Javascriptu. Nejprve se ale omluvím čtenářům, kteří holdují prehistorickým verzím webových prohlížečů.
Ty mají v seriálu o programování jednu velkou červenou stopku, já totiž hodlám používat jen moderní verze ECMAScriptu, ze kterého vycházejí i javascriptové implementace v prohlížečích.
Fetch API stáhne obří JSON
Stručně řečeno, pokud tento článek nečtete v Internet Exploreru, ve kterém to fakt nebude fungovat, můžete společně se mnou stáhnout data z webu Golemio pomocí vestavěného javascriptového rozhraní Fetch API pro asynchronní HTTP komunikaci.
Dotaz včetně speciální hlavičky s klíčem a přečtení výsledku ve formátu JSON by mohlo vypadat takto:
fetch("https://api.golemio.cz/v2/vehiclepositions", {method: "GET", headers: {"x-access-token": golemio_klic}})
.then(response => response.json())
.then(data => {
console.log("Stazeny a dekodovany JSON:");
console.log(data);
});
Stovky zeměpisných souřadnic
Odpověď ve formátu JSON pro dotaz /vehiclepositions obsahuje pole features, což je vlastně seznam všech evidovaných prostředků PID, které se právě teď pohybují po Praze a v jeho širokém okolí. Během dne to budou vyšší stovky vozidel.
Kód výše vypsal do konzole webového prohlížeče objekt dekódovaného JSON
Každá položka dále obsahuje objekty geometry a properties.
Uvnitř geometry najdeme pole coordinates se souřadnicemi zeměpisné délky a šířky vozidla. Na mapu bychom mohli tímto způsobem vykreslit anonymní puntíky a kochat se hustotou pražské integrované dopravy, nicméně v obřím JSONu máme ještě ten objekt properties.
Rozbalil jsem první záznam v poli features a jeho vnořené objekty geometry a coordinates, kde konečně najdete údaj o zeměpisné poloze
Properties nabízí hromadu dodatečných informací o spoji. Třeba vnořenou položku properties.last_position.bearing, která obsahuje kurz/azimut vozidla v celých stupních, nebo objekt properties.last_position.delay s dodatečnými údaji o aktuálním zpoždění.
V objektu last_position najdete azimut, detaily o zpoždění a také zastávkách
Je to tramvaj? Kam jede? A kdo ji provozuje?
Vše ostatní se skrývá v objektu properties.trip. V podobjektu propertis.trip.gtfs to bude třeba položka route_type, která číselným indexem identifikuje typ linky. Hodnota 0 představuje tramvaj, 1 patří metru, 2 železnici, 3 autobusu a tak dále.
Jejich kompletní výčet najdete v dokumentaci specifikace GTFS, což je zkratka pro General Transit Feed Specification. Používá ji především Google pro popis veřejné dopravy, no a ostatní se přidali včetně našeho pražského API.
Objekt trip nabízí detaily o dopravci a lince
Další zajímavou položkou je trip_headsign, tedy cíl vozidla (Kladno-Ostrovec), nebo trip_short_name (respektive alternativní route_short_name), která odhalí číslo linky (Os 9826). Položka trip.agency_name.scheduled konečně obsahuje jméno dopravce (ČESKÉ DRÁHY).
Knihovna Leaflet s mapovými podklady OSM
To by mohlo pro začátek stačit, ještě ale potřebujeme tu mapu. Javascriptová knihovna Leaflet funguje podobně jako třeba mapové API od Googlu, ovšem s tím rozdílem, že sama o sobě žádné mapové vrstvy nenabízí.
Může pracovat třeba s překrásnými dlaždicemi od placeného Mapboxu, poradí si ale i se základní mapou OpenStreetMap. Při splnění podmínek fair use můžete komunikovat s jejím mapovým serverem skrze Leaflet, aniž byste zaplatili jediný eurocent.
Férovým použitím je zejména to, že tímto způsobem nebude mapový server zatěžovat nějaká hypotetická velká komerční služba s hromadou návštěvníků. U té se předpokládá, že si stáhne surová vektorová data OSM a spustí si mapový server na své vlastní náklady. My kutilové se ale do mlhavých podmínek samozřejmě vejdeme.
Vytvoříme objekt mapy
Zobrazení mapy v Javascriptu je opravdu jednoduché. V HTML stačí vytvořit kontejner DIV s patřičným ID:
<div id="mapa"></div>
A v Javascriptu jej pak napojíme na objekt mapy knihovny Leaflet, přičemž rovnou určíme i souřadnice středu mapy (z.š. a z.d.) a míru přiblížení (vyšší číslo, větší přiblížení):
let mapa = L.map("mapa").setView([50.0835494, 14.4341414], 8);
Nastartovali jsme objekt mapy, ještě ji ale musíme napojit na nějakou službu poskytovatele mapových dlaždic. Jak už jsme si řekli, zvolíme projekt OSM, takže zbytek kódu by mohl vypadat takto:
let dlazdice = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution: 'Bastl: <a href="https://www.zive.cz">Živě.cz</a> | ' +
'Data: <a href="https://pid.cz/">PID</a> | ' +
'API: <a href="https://golemio.cz/projects">Golemio</a> | ' +
'Mapa: <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(mapa);
Součástí napojení vrstvy s dlaždicemi OSM je i uživatelská patička, která se zobrazí v pravém dolním rohu a obsahuje údaje o autorských právech a zdrojích. Čerpáme z OSM, takže pro splnění licenčních podmínek nesmí chybět odkaz. I kdyby to nebyla povinnost, je to přinejmenším slušnost!
A takto vypadá výsledek. Rozměry mapy lze nastavit v CSS. Viz kompletní kód na konci článku nebo na GitHubu
Značka v mapě s ikonou tramvaje
Mapa je zatím prázdná, když se ale vrátíme k JSONu s aktuálními pozicemi vozidel pražské integrované dopravy, můžeme nad mapu vynést markery – značky. A jelikož známe typ jednotlivých vozidel (ona položka route_type), mohla by mít každá značka specifickou ikonu podle toho, jestli se jedná o tramvaj, autobus, vlak a tak dále.
Značku s vlastní ikonou vytvoříme takto:
let ikona = L.icon({iconUrl:"tramvaj.png", iconSize:[24,24]});
let znacka = L.marker([50.1589, 16.4789], {icon: ikona}).addTo(mapa);
Jak vidno, jako ikonu značky použijeme obrázek tramvaj.png, který jsme si stáhli z webu flaticon.com. Obrázek má velikost 24x24 pixelů. Parametry 50.1589 a 16.47899 v objektu nové značky odpovídají zeměpisné šířce a délce a parametrem icon odkážeme na ikonu, kterou jsme si vytvořili v předchozím kroku.
Značka s ikonou tramvaje
Po klepnutí se zobrazí bublina s detaily spoje
Aby byla naše mapa interaktivní, po klepnutí na libovolnou značku v mapě se zobrazí bublina s detaily o konkrétním vozidlu, respektive lince. Použijeme údaje, které jsme vyčetli ze sekce properties u každé položky obřího pole features s jednotlivými vozidly, které právě teď eviduje API.
Jak na to? Bublinu připojíme ke značce pomocí metody bindPopup, jejímž parametrem je HTML kód, který se po klepnutí zobrazí v dialogu:
znacka.bindPopup("<h2>Linka 1234</h2>" +
"<b>Vozidlo</b>: Vlak<br>" +
"<b>Dopravce:</b> České dráhy<br />" +
"<b>Jede do:</b> Čmelákovice<br />"+
"<b>Nízkopodlažní:</b> Ano");
A to je celé. Jakmile náš Javascript projde celé pole JSON, vykreslí klidně i stovky dílčích značek nad mapou. Tu můžete libovolně přibližovat, posouvat a pro detail klikat na značky.
Připojili jsme ke značce bublinu s vlastním HTML
Kdybychom chtěli zobrazovat situaci v reálném čase, stačí se dopravního API dotazovat průběžně a obnovovat údaje nad mapou. Nicméně pozor, ten JSON je opravdu rozměrný – jsou to stovky vozidel –, takže byste v takovém případě měli zvážit frekvenci, abyste server zbytečně nezatěžovali a neporušili pravidla fair use.
Když vše složíme dohromady, získáme kompletní pohled na aktuální stav PID
To je už ale úkol pro vás a pro hlubší studium aplikačního rozhraní, které nabízí desítky dalších parametrů a funkcí.
Zdrojový kód
Na závěr nesmí chybět kompletní zdrojový kód salinoleaks.html. Stačí jej uložit do stejnojmenného souboru, do konstanty golemio_klic vložit vlastní klíč API, který jsme si vygenerovali v úvodu článku, a soubor načíst ve webovém prohlížeči.
<!DOCTYPE html>
<html lang="cs">
<head>
<title>Šalinoleaks: PRAHA</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
.leaflet-container {
width: 1024px;
height: 768px;
max-width: 100%;
max-height: 100%;
}
</style>
<script>
// Pomocny objekt s typy vozidel podle formatu GTFS a vlastnosti route_type
// Viz https://developers.google.com/transit/gtfs/reference#routestxt
// Kazde vozidlo ma svoji ikonu a pocitadlo pro statistiku
let vozidla_typy = {
"0": {"ikona": L.icon({iconUrl:"tram.png", iconSize:[24,24]}), "nazev": "Tramvaj", "pocet": 0},
"1": {"ikona": L.icon({iconUrl:"metro.png", iconSize:[24,24]}), "nazev": "Metro", "pocet": 0},
"2": {"ikona": L.icon({iconUrl:"train.png", iconSize:[24,24]}), "nazev": "Vlak", "pocet": 0},
"3": {"ikona": L.icon({iconUrl:"bus.png", iconSize:[24,24]}), "nazev": "Autobus", "pocet": 0},
"4": {"ikona": L.icon({iconUrl:"ship.png", iconSize:[24,24]}), "nazev": "Loď", "pocet": 0},
"5": {"ikona": L.icon({iconUrl:"tram.png", iconSize:[24,24]}), "nazev": "Lanová tramvaj", "pocet": 0},
"6": {"ikona": L.icon({iconUrl:"lift.png", iconSize:[24,24]}), "nazev": "Lanovka", "pocet": 0},
"7": {"ikona": L.icon({iconUrl:"lift.png", iconSize:[24,24]}), "nazev": "Pozemní lanovka", "pocet": 0},
"11": {"ikona": L.icon({iconUrl:"bus.png", iconSize:[24,24]}), "nazev": "Trolejbus", "pocet": 0},
"12": {"ikona": L.icon({iconUrl:"train.png", iconSize:[24,24]}), "nazev": "Jednokolejka", "pocet": 0}
};
// Pomocny objekt pro statistiku dopravcu
let dopravci = {};
// Pomocne pole se znackami vsech vozidel v mape
// Nevyuzivame jej, ale mohlo by pomoci s dalsim managementem
// Treba s mazanim znacek, filtrovanim atp.
let vozidla_markery = [];
// Klic do systemu Golemio, ktere pro PID zpracovava
// aplikacni rozhrani o pohybu vozidel. Je soucasti
// HTTP dotazu na server https://api.golemio.cz
// Klic si muzete bezplatne vygenerovat na webu:
// https://api.golemio.cz/api-keys/dashboard
// Dokumentaci ke Golemio API najdete na webu:
// https://golemioapi.docs.apiary.io/#reference/public-transport/realtime-vehicle-positions/get-all-vehicle-positions
// !!! BEZ RUCNIHO NASTAVENI TETO HODNOTY NEBUDE MAPA FUNGOVAT !!!
const golemio_klic = "";
// Po nacteni stranky spust tuto funkci
window.onload = () =>{
// Objekt mapy Leaflet vycentrovane na souradnice stredu Prahy a priblizeni 10 (vyssí cislo, vyssi priblizeni)
// Dokumentace knihovny Leaflet: https://leafletjs.com/SlavaUkraini/reference.html
let mapa = L.map("mapa").setView([50.0835494, 14.4341414], 8);
// Objekt poskytovatele mapovych dlazdic pro Leaflet
// Pouziji bezplartny pristup k OSM
// Pozor, dodruzjte politiku fair use a nepretezujte OSM!
// Mohl bych pouzit take placeny Mapbox a dalsi poskytovatele
// V mape nezapomenu uvest zdroje dat "attribution", ktere se zobrazi v paticce
let dlazdice = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Bastl: <a href="https://www.zive.cz">Živě.cz</a> | ' +
'Data: <a href="https://pid.cz/">PID</a> | ' +
'API: <a href="https://golemio.cz/projects">Golemio</a> | ' +
'Mapa: <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(mapa);
// Spojim se asynchronne s HTTP serverem Golemio, ktere zpracovava API pro ROPID
// a polozim dotaz /vehiclepositions, ktery vrati obrovsky JSON
// s polohami vsech vozidel prazske integrovane dopravy
console.log("Stahuji šaliny a další věci, které jezdí v Praze...");
fetch("https://api.golemio.cz/v2/vehiclepositions", {
method: "GET",
// V HTTP dotazu musi byt hlavicka x-access-token,
// ktera obsahuje osobni klic API
headers: {
"Content-Type": "application/json; charset=utf-8",
"x-access-token": golemio_klic
}
})
.then(response => response.json()) // Pokud jsem ziskal odpoved, interpretuji ji jako JSON
.then(data => { // Objekt "data" nyni obsahuje citelny JSON
// Jednotliva vozdila jsou ve vnorenem poli features,
// a tak pro kontrolu vypisu jeho delku
console.log(`Ale ne, v Praze a okolí se pohybuje ${data.features.length} prostředků MHD. To je více než v Brně!`);
// Projdu pole features s vozdily
for(const vozidlo of data.features){
let zd = vozidlo.geometry.coordinates[0]; // Zemepisna delka vozdila
let zs = vozidlo.geometry.coordinates[1]; // Zemepisna sirka vozidla
let linka = vozidlo.properties.trip; // Informace o lince a vozidlu
// Typ linky ve form8tu GTFS
// Viz https://developers.google.com/transit/gtfs/reference#routestxt
let typ = linka.gtfs.route_type;
// Navysim pocitadlo zjisteneho dopravniho prostredku kvuli statistice
vozidla_typy[typ].pocet++;
// Podle GPS polohy a typu vozidla vytvorim na mape znacku s konkretni ikonou
let marker = L.marker([zs, zd], {icon: vozidla_typy[typ].ikona}).addTo(mapa);
// Po klepnuti na znacku se zobrazi bublina s popisem linky
let nizkopodlazni = (linka.wheelchair_accessible)? "ano" : "ne";
let cislo_linky = (linka.gtfs.trip_short_name != null)? linka.gtfs.trip_short_name : linka.gtfs.route_short_name;
marker.bindPopup(`<h2>Linka ${cislo_linky}</h2>` +
`<b>Vozidlo</b>: ${vozidla_typy[typ].nazev}<br>` +
`<b>Dopravce:</b> ${linka.agency_name.scheduled}<br />` +
`<b>Jede do:</b> ${linka.gtfs.trip_headsign}<br />` +
`<b>Nízkopodlažní:</b> ${nizkopodlazni}`);
// pridam znacku do pole znacek, kdybych s nim ichtel dale hroamdne pracovat
// Treba kdybych je chtel smazat
vozidla_markery.push(marker);
// Aktualizuji pocitadlo dopravcu
if(dopravci.hasOwnProperty(linka.agency_name.scheduled)) dopravci[linka.agency_name.scheduled]++;
else dopravci[linka.agency_name.scheduled] = 1;
}
// Na zaver vypisu statisrtiku vozdel, tedy pocet pro kazdy typ
console.log("---------------------------------------------------");
console.log("Statistika vozidel:");
for (const [vozidlo, vlastnosti] of Object.entries(vozidla_typy)) {
console.log(` ${vlastnosti.nazev}: ${vlastnosti.pocet}`);
}
// A také statistiku dopravcu, tedy pocet vozdel, ktere prave vypravili
console.log("---------------------------------------------------");
console.log("Statistika dopravcu:");
for (const [operator, pocet] of Object.entries(dopravci)) {
console.log(` ${operator}: ${pocet}`);
}
// A jeste vypisu kompletni JSON, pro dalsi studium, co vsechno obsahuje
console.log("---------------------------------------------------");
console.log("Surový JSON:");
console.log(data)
})
// V pripade jakekoliv chyby zobrazim v konzoli chybove hlaseni
.catch(e => {
console.log("Ale ně, něco se pokazilo. Uklidni se u pořadu Týden Živě, a pak to zkus znovu!");
console.log(e);
});
}
</script>
</head>
<body>
<h1>Šalinoleaks: PRAHA</h1>
<div id="mapa"></div>
</body>
</html>