Doprava | Pojďme programovat elektroniku | GIS

Šalinoleaks: Odposloucháváme pražské šaliny, autobusy a vlaky a kreslíme je na mapu

  • Kde jsou právě teď pražské tramvaje, autobusy a příměstské vlaky?
  • Zobrazí je oficiální mapa a mobilní aplikace
  • Věděli jste ale, že je k dispozici také API? Dnes si ho vyzkoušíme

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.

d753f538-12fc-44eb-952d-05abc8cd9a38759c5264-13ba-4ac9-b736-91c57fb8c324fe2dca91-fbae-4502-a75b-5eeb13673398
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.

04d2d043-83a9-426e-bc59-aee41ffcf9a1
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
fb82a3a6-d7fd-488a-a7e7-11b0e8c4a0e8
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
6e5216e4-c0e0-41d6-98a4-50571e7a1973
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.

a3b69b21-4a37-4d47-84c2-29c25012b43f
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.

b3d77ddf-81b5-49ff-ad47-ff55b364dd3b
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í.

8bab6524-9d25-4f38-9b33-019bb8ff59f6
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 železnici3 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.

b59edf82-2198-4c03-bd8b-affd7788624d
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!

d2da9fa0-9d3a-4791-9832-9f4ab42097c0
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.

5c9969cf-aeb5-45a1-b93d-dda5a1a94787
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.

bc4a3e5c-736f-4d01-baee-6dccd4e11cbc
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.

adea7bb3-77a8-4c73-9546-731a83716007
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>
Diskuze (48) Další článek: Nový web ProUkrainu.cz: Vydavatelství Czech News Center pomáhá lidem, kteří prchají před válkou

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , ,