11. Podglądamy trolejbusy, czyli uczymy się pracować z otwartymi danymi

Dzisiaj zaczniemy kolejny ambitny projekt, mianowicie spróbujemy napisać program do sprawdzania rozkładu jazdy komunikacji miejskiej w Gdyni.

API resztowe

W jaki sposób można dowiedzieć się o aktualnym rozkładzie autobusów? Naturalnie najlepiej pójść na przystanek i sprawdzić na tablicy odjazdów. Można też zajrzeć na stronę Internetową odpowiedniego wydziału transportu miejskiego. Jak jednak sobie z tym problemem radzą programiści? Istnieje wiele aplikacji służących do planowania tras. Wydaje się wątpliwe, by autorzy w nich ręcznie przepisywali rozkłady jazdy i aktualizowali swoje dzieła ilekroć gdzieś wprowadzone zostaną zmiany. No i rzeczywiście intuicja tu nie zawodzi, programiści też są leniwi przecież!

Tak zaczął się rodzić koncept API resztowego.

Samo określenie API (Application Programming Interface) to ogólna nazwa na wszystkie zbiory funkcji, metod czy poleceń, do jakich dostęp ma, w różny sposób, programista. Przykładowo w Eltenie funkcje takie jak “alert”, “input_text” czy “play” są właśnie elementami API o nazwie “Elten API”. Nieco większe zamieszanie sprawia drugi człon nazwy, skąd ta reszta? Pochodzi ona od angielskiego skrótu REST (Representational state transfer). Tłumaczenie “API resztowe” jest ze wszech miar błędne, ale… no cóż, tak się już przyjęło.

API takie pozwalają różnym programom na zdalne pobieranie, zapisywanie, tworzenie czy edytowanie różnego rodzaju informacji. Rozwiązanie takie wykorzystuje na przykład Elten, pozwala ono programom (tu klientowi) na pisanie na forum czy czytanie tego wpisu. Inne API służą do przeglądania danych, na przykład NASA udostępnia taką drogą zainteresowanym programistom informacje o pogodzie na Marsie. W taki sam sposób wiele miast udostępnia informacje między innymi o aktualnych rozkładach jazdy komunikacji miejskiej.

Kodowanie danych

Kluczem prostego przekazywania informacji jest ujmowanie jej w taki sposób, by każdy program, niezależnie od systemu, języka programowania czy architektury sprzętowej, mógł ją odczytać i przetworzyć. Powstawało wiele standardów przekazywania informacji, a tym, który oficjalnie przyjął się kilkadziesiąt lat temu, był XML (Extended Markup Language). Obecnie jednak XML to przeżytek, choć wciąż spotykany, a proponowanym i najczęściej stosowanym kodowaniem informacji między różnymi aplikacjami jest stworzony w roku 2001, a oficjalnie przyjęty w 2014 JSON (JavaScript Object Notation). Wbrew nazwie ten sposób kodowania nie ma nic wspólnego z językiem JavaScript, co najwyżej tyle, że z niego się wywodzi.

Kilka przykładów

Ruby oczywiście ma pełną obsługę kodowania JSON, żeby daleko nie szukać to metoda wykorzystywana w komunikacji także przez Eltena. Możemy więc pomaszerować do konsoli i zobaczyć, jak to wygląda.

JSON.generate("Ala ma kota")
:   #=> "\"Ala ma kota\""
JSON.generate(["Ala", "ma", 3, "koty"])
:   #=> "[\"Ala\",\"ma\",3,\"koty\"]"
JSON.generate({'koty'=>3, 'psy'=>0, 'chomiki'=>1, 'papugi'=>2})
:   #=> "{\"psy\":0,\"papugi\":2,\"koty\":3,\"chomiki\":1}"

JSON pozwala kodować napisy i liczby, wartości logiczne, ale także co bardzo ważne tablice i tablice haszowe. Dzięki temu możemy przekazywać między różnymi aplikacjami praktycznie dowolne dane. Do kodowania do JSON służy funkcja “generate” z modułu “JSON”, analogicznie do ich dekodowania służy funkcja “load”.

x=JSON.generate(["Ala", "ma", 3, "koty"])
:   #=> "[\"Ala\",\"ma\",3,\"koty\"]"
y=JSON.load(x)
:   #=> ["Ala", "ma", 3, "koty"]

Gdzie tego wszystkiego szukać?

Wiemy, że dane są udostępnione, wiemy, że zakodowane. No i pięknie ładnie, ale skąd je wziąć? Nie istnieje coś takiego, jak biblioteka obsługi wszystkich istniejących API – tak samo jak nie ma jednego odpowiedzialnego za Internet serwera. Kiedy chcemy sprawdzić, czy dane API istnieje i zapoznać się z jego dokumentacją, najczęściej musimy zajrzeć na stronę odpowiedzialnego za nie urzędu. W wypadku rozkładu jazdy autobusów w Gdyni będzie to oczywiście Urząd Miasta Gdyni.

Tak znajdujemy adres otwartedane.gdynia.pl, gdzie czeka na nas spis dostępnych danych, w tym między innymi “Pomiary liczników rowerowych”, “Informacje o rozkładach jazdy i lokalizacji przystanków”, “Prędkości dla wybranego segmentu”, “Natężenia ruchu dla wybranego segmentu”, “Aktualne dostępne wolne miejsca parkingowe”, “Zebrane dane pogodowe dla wybranej stacji pogodowej”, “Jakość powietrza w Gdyni”, “Ilości odebranych odpadów z podziałem na sektory”, “Hałas”, “Stężenie pyłu zawieszonego (PM 10)”, “Odsetek kobiet zatrudnionych w administracji miejskiej”, “Szczepienia w Gdyni” czy “Zachorowania i zgony COVID-19 w Gdyni”. Do każdego z tych interfejsów możemy sięgnąć, by wykorzystać je na żywo w naszych aplikacjach. Nas oczywiście interesuje komunikacja miejska.

I wreszcie, spróbujmy coś sprawdzić

Wczytując się w dokumentację interfejsu informacji o komunikacji miejskiej w Gdyni, dowiadujemy się, że na przykład informacje o wszystkich kursujących liniach znajdziemy pod adresem URL http://api.zdiz.gdynia.pl/pt/routes Spróbujmy więc zajrzeć na niego z użyciem naszej przeglądarki Internetowej.

Konkretne zachowanie zależy od naszego oprogramowania. Mój Firefox automatycznie rozpoznał, że otrzymał dane w formacie JSON, wyświetlając je w ładnym drzewku. Otwarcie karty “Nieprzetworzone dane” pozwala mi zobaczyć dokładną treść otrzymanej od serwera informacji.

Możemy zauważyć, że serwer zwrócił nam tablicę z różnymi trasami. Spójrzmy na przykładowy element tej tablicy. Przykładowe treści jednego z elementów prezentują się następująco:

{
"routeId": 10133,
"agencyId": 8,
"routeShortName": "133",
"routeLongName": "Dworzec Morski - Płyta Redłowska",
"routeDesc": null,
"routeType": 700,
"routeURL": null,
"routeColor": null,
"routeTextColor": null,
"routeSortOrder": null
}

Dostaliśmy sporo informacji, z których tak naprawdę potrzebujemy trzech. Od razu sięgając do dokumentacji dowiemy się, że pola “routeColor”, “routeTextColor”, “routeSortOrder”, “routeURL” oraz “routeDesc” nie są używane, a definiuje się je tylko dla zgodności ze standardem GTFS. “agencyId”, czyli informacja o przewoźniku, a także “routeType”, czyli typ pojazdu (autobus / trolejbus / tramwaj) także nam nie są teraz potrzebne. Na koniec pozostają tylko trzy istotne dla nas pola: “routeId”, czyli unikalny identyfikator trasy, “routeShortName”, czyli numer linii oraz “routeLongName”, czyli jej nazwa (tu trasa).

I jak to teraz “kodnąć”?

By nie operować na tablicach haszowych, co może nam bardzo utrudnić życie, gdyby definicja API z jakiegoś powodu się zmieniła, zdefiniujemy klasę, w której zapiszemy ważne dla nas informacje o danym kursie. Następnie pobierzemy z serwera dane i przypiszemy je do właściwych klas.

Warto zauważyć, że w ten sposób możemy w przyszłości rozwinąć naszą aplikację o obsługę innych miast. Każde z nich ma swoje adresy i kodowania, ale po napisaniu odpowiednich funkcji do pobrania tych informacji, możemy wykorzystywać wszędzie tę samą klasę i w ten sposób znacznie oszczędzić przyszłej roboty – tak zwane wzorce projektowe w praktyce.

class ZKMRoute
  attr_accessor :id, :line, :name
  def initialize(id=0, line=nil, name=nil)
    @id, @line, @name = id, line, name
  end
end

t=read_url("http://api.zdiz.gdynia.pl/pt/routes")
routes=[]
if t!=nil
  routes = JSON.load(t).map{|r|
    ZKMRoute.new(r['routeId'], r['routeShortName'''], r['routeLongName'])
  }
end
selector(routes.map{|r|r.line+" - "+r.name}, "Available routes", 0, 0)

Dostaliśmy listę wszystkich tras, całkowicie wymieszaną. Spróbujmy więc dodać nad instrukcją pokazania listy polecenie jej posortowania.

routes.sort!{|a,b|
  if a.line.to_i!=0 && b.line.to_i!=0
    s=a.line.to_i<=>b.line.to_i
  elsif a.line.to_i==0 && b.line.to_i==0
    s=a.line<=>b.line
  elsif a.line.to_i==0
    s=1
  elsif b.line.to_i==0
    s=-1
  end
  s
}

Warto zauważyć wykrzyknik po funkcji “sort”. Ta zgrabna zmiana nakazuje Rubiemu zmienić bieżącą tablicę, miast tworzyć nową. Ta z pozoru dość skomplikowana instrukcja pozwoli na posortowanie listy linii tak, by na górze znalazły się linie numerowane, a pod nimi literowane. Gdybyśmy po prostu wszystko posortowali przez “sort_by”, linia “20” znalazłaby się poniżej linii “199”.

Wskazówka! Sortowanie danych w taki sposób jest szybkie do napisania, ale nie najlepsze w praktyce. Założyliśmy tu, że w wypadku linii numerowanych liczy się numer, a literowanych litery. Tak więc hipotetyczna linia “n20” znajdzie się poniżej “n199”. Nie ma takich linii, ale przecież mogą się pojawić. I co wtedy? Katastrofa! Dlatego o wiele praktyczniejszym rozwiązaniem byłoby zdefiniowanie w klasie ZKMRoute funkcji sort_id, która w jakiś sposób wygeneruje unikalną liczbę pozwalającą na sortowanie elementów. Jedną ze strategii generowania takich liczb jest przemnożenie wartości liczbowej przez jakąś dużą stałą, a następnie dodanie do niej wartości wygenerowanej z liter, na przykład ich pozycji w alfabecie. Takie operacje wykraczają jednak znacznie poza zakres tej lekcji.

Podsumowanie

W lekcji tej omówiliśmy, czym jest API resztowe, do czego może się przydać i zaimplementowaliśmy przykładowe wyświetlenie listy linii komunikacji miejskiej w Gdyni. Oczywiście to tylko ułamek materiału, w ogóle nie dotknęliśmy tematu przystanków. Ale spokojnie, zajmiemy się tym… W następnym odcinku.

6 komentarzy

  1. elten2/src/eapi/network.rb
    line 140
    headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
    so we see that it doesn’t metter what content type you set. but i want urlencoded

  2. because of this markup, i suggest you to create forum about elten programming, not a blog. it will markup all posts, all links in program code string. all comments will be as headers

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *