Ostatnio omówiliśmy sposób na przekazywanie informacji zdalnych, a także zaprezentowaliśmy przykład wykorzystania takiej informacji, wyświetlając listę wszystkich linii komunikacji miejskiej w Gdyni. Dziś podążymy dalej tym tropem i napiszemy dość toporne, ale pełne oprogramowanie do wyświetlania rozkładu jazdy.
Zacznijmy od posortowania
Ostatnio sortowaliśmy linie, wykonując kilka porównań. Jak wtedy pisałem, jest to rozwiązanie szybkie, ale mało eleganckie. Skoro chcemy napisać profesjonalny program, zróbmy to profesjonalnie. Sortować można różnie, ale najczęściej robi się to, w pierwszej kolejności podając linie numerowe, a potem te zaczynające się od liter, w kolejności alfabetycznej, na przykład 20, 38, 141, L1, L3, N4, N19, N21.
Najprościej będzie stworzyć więc dwie metody w naszej klasie “ZKMRoute”, podające odpowiednio numer linii i, jeśli istnieje, jej oznaczenie literowe.
def number
return @line.split("").select{|s|s.to_i.to_s==s}.join("").to_i
end
def type
return @line.split("").select{|s|s.to_i.to_s!=s}.join("")
end
Wykorzystaliśmy tu metodę “split”. Służy ona do dzielenia łańcucha znaków wedle danego separatora.
"Ala ma kota".split(" ")
: #=> ["Ala", "ma", "kota"]
"Ala ma kota".split("")
: #=> ["A", "l", "a", " ", "m", "a", " ", "k", "o", "t", "a"]
Podanie jako parametru pustego łańcucha znaków to najprostszy sposób na zamianę ciągu znaków na ich tablicę. Następnie wybieramy, zależnie od funkcji, wszystkie znaki będące (lub nie) cyframi i ponownie je łączymy znaną już metodą “join”. Można tu zauważyć operator “!=”. O ile “==” oznacza równość, “!=” wskazuje na nierówność.
Użycie formy
x.to_i.to_s==x
to dość prosty sposób na sprawdzanie, czy znak “x” jest cyfrą.
Uzbrojeni w powyższe dwie metody, możemy pierwszy raz zdefiniować własny operator porównania, czyli “<=>”.
def <=>(o)
if !o.is_a?(ZKMRoute)
return 0
else
n1=number
t1=type
n2=o.number
t2=o.type
if t1==t2
return n1<=>n2
else
return t1<=>t2
end
end
end
Funkcja “<=>” przyjmuje jeden parametr. Jest to obiekt, z którym chcemy porównać. Przypomnę z jednej z poprzednich lekcji, że powinniśmy zwrócić:
- -1, jeśli nasz obiekt poprzedza przekazany,
- 0, jeśli są tożsame,
- 1, jeśli przekazany obiekt poprzedza nasz.
Metoda “is_a?” pozwala sprawdzić, czy dany obiekt jest instancją wybranej klasy. Tu sprawdzamy, czy porównujemy linię komunikacji miejskiej z inną linią. Jeśli nie, zawsze zwracamy “0”, dzięki temu w ewentualnej tablicy można łączyć sortowanie linii z innymi obiektami.
Jeśli porównujemy dwie linie, sprawdzamy, czy są tego samego typu (kod literowy). Gdy tak, wykonujemy porównanie numerów, w przeciwnym wypadku sortujemy alfabetycznie typy.
Ktoś mógłby spytać, poco zapisaliśmy zmienne “n1”, “t1”, “n2” oraz “t2”, zamiast po prostu użyć form “number”, “type”, “o.number” i “o.type”. Wyjaśnienie jest dość proste. Wywołanie metod “number” oraz “type” jest stosunkowo kosztowne, wymaga zamiany napisu na tablicę, wybrania elementów z tablicy (wykonując dwie konwersje) i połączenia wyniku. Oczywiście nasz komputer wykonuje tę operację w ułamku sekundy, ale nie oznacza to, że nie możemy tego ułamka sekundy przyoszczędzić. Zwłaszcza że, jak mówi znane powiedzenie, “Ziarnko do ziarnka i pełna miarka”.
Już nie wsiadajmy do pojazdu byle jakiego!
Do tej pory pobieraliśmy jedynie listę dostępnych linii. Aby zbudować pełną informację o rozkładzie jazdy, będziemy potrzebować także listy przystanków i kursów. W praktyce, patrząc na dokumentację API, dowiemy się, że musimy sięgnąć do czterech zasobów:
- routes, z którego uzyskaliśmy już listę linii,
- stops, który da nam listę przystanków,
- trips, który zawiera listę kursów danych linii,
- stop_times, który poda czasy odwiedzania przystanków przez dane kursy.
W tym punkcie pojawia się kilka pytań dotyczących relacji pobieranych obiektów. Skąd możemy wiedzieć, który kurs jest prowadzony w ramach której linii? Różne API rozwiązują to w różny sposób, w wypadku informacji o Gdyńskiej Komunikacji Miejskiej dostajemy listę wszystkich obiektów, a co do czego należy sprawdzamy, porównując odpowiednie ID.
Pomyśl, zanim napiszesz
Jesteśmy już praktycznie uzbrojeni w wiedzę, jak pobrać dane z serwera. Będą one podzielone na cztery kolekcje:
- Linii, zawierającą numery i nazwy dostępnych linii,
- Przystanków, gdzie uzyskamy nazwy przystanków oraz ich współrzędne geograficzne,
- Tras, która zawiera spis wszystkich prowadzonych przez daną linię kursów,
- Czasów odwiedzin przystanków, która da nam listę odwiedzanych w danym kursie przystanków, ich czas oraz informację o tym, czy przystanek jest przystankiem na żądanie.
Najprościej byłoby po prostu utworzyć klasy reprezentujące każdą z tych informacji i wykorzystywać je analogicznie do przekazanej z serwera. Takie podejście jednak jest dość toporne i nie wykorzystuje możliwości, które daje nam programowanie obiektowe. Lepiej zastanowić się od razu, co nam będzie potrzebne. Zależnie od typu aplikacji, możemy rozwiązać to na wiele sposobów:
- Możemy w każdej klasie linii umieścić listę tras, a w każdej trasie listę odwiedzanych przystanków,
- Możemy odwrotnie: pobierać listę odwiedzanych przystanków, a do niej przypisać informacje o linii i kursie,
- Możemy także zbudować graf (wersja dla ambitnych), w którym wierzchołkami będą przystanki, a krawędziami prowadzone kursy.
To tylko przykładowe rozwiązania, a sposobów organizacji naszego kodu jest o wiele więcej, a co ważne, nie ma jednego poprawniejszego od innych.
My dążymy do napisania programu wyświetlającego rozkład jazdy, tak więc wykorzystamy pierwszy sposób: do kolekcji linii przypiszemy trasy, zaś do tras odwiedzane przystanki. To skuteczny sposób wyświetlenia interesujących nas informacji, ale okazałby się całkiem niepraktyczny, gdybyśmy zamiast zwykłego wyświetlenia kursów, chcieli napisać planer trasy. – jak widać, zależnie od celu, różne sposoby mogą być słuszne.
Schemat
W programowaniu w tym punkcie często używa się schematów (na przykład języka UML), celem opisania zależności między różnymi obiektami. My nie musimy tego robić aż tak formalnie, ale zanim coś napiszemy, opowiedzmy sobie, co napiszemy.
ZKMRoute
To klasa napisana już wcześniej, choć nieco ją zmodyfikujemy. Zawiera ona informację o identyfikatorze, numerze oraz nazwie linii. Dodamy do niej również tablicę tras danej linii.
ZKMStop
Tu zapiszemy informację o przystankach, a więc ich identyfikator, nazwę, kod oraz współrzędne geograficzne. Gdybyśmy planowali napisać program do wyszukiwania połączeń, dobrym krokiem byłoby także dołączenie tu listy kursów zatrzymujących się na danym przystanku. Na razie tego nie uwzględnimy.
ZKMTrip
W tej klasie przechowamy informacje o danym kursie. Tak naprawdę to najważniejsza klasa, w niej umieścimy kolekcję informacji o przystankach na danym kursie, a także informacje o linii prowadzącej ten kurs.
ZKMStopTime
Ta klasa posłuży jako zbiór informacji o przystanku na trasie, z jednej strony wskaże nam na przystanek, z drugiej uzyska atrybuty informujące o czasie odjazdu i tym, czy przystanek jest przystankiem na żądanie.
Zależności
Do tej pory w naszych programach obiekt A zawsze wskazywał na obiekt B, a więc relacja była liniowa, zaczynała się w pewnym punkcie i kończyła w innym. Tutaj relacja jest cykliczna. Brzmi to skomplikowanie, ale, mam nadzieję, już rozjaśniam. Klasa “ZKMRoute” zawiera listę kursów danej linii, ale klasa “ZKMTrip” zawiera informację o linii prowadzącej taki kurs. Tak więc klasy wzajemnie na siebie wskazują. Podobnie klasa “ZKMTrip” zawiera listę przystanków “ZKMStopTime”, które posiadają informację o kursie. Gdybyśmy jednak uwzględnili w klasie “ZKMStop” informację o prowadzonych kursach, okazałoby się, że każda z utworzonych klas pozwala dostać się do każdej innej. Taką sytuację nazywamy “uwikłaniem”. Nie jest to nic złego, oznacza, że posiadając dowolny obiekt, możemy dotrzeć do każdego innego. Co więcej, gdybyśmy teraz zaczęli rozważać w definicjach programistycznych powstałe uwikłanie, moglibyśmy zauważyć, że niektóre obiekty będą zawierać kolekcje innych obiektów (linia listę kursów, kurs listę przystanków), natomiast inne tylko jeden (czas przystanku prowadzący go kurs, kurs dotyczącą go linie). Są to dwie najważniejsze relacje wymieniane w programowaniu: “One to one”, w której obiekt jest przyporządkowywany tylko do jednego innego obiektu, oraz “one to many”, gdzie jeden obiekt wskazuje na wiele innych. Takie wzajemne wskazywanie na siebie różnych obiektów jest sytuacją dość częstą w zaawansowanych programach, choć na początku może nieco mylić.
Jak to wygląda?
class ZKMRoute
attr_accessor :id, :line, :name
attr_reader :trips
def initialize(id=0, line=nil, name=nil)
@id, @line, @name = id, line, name
@trips=[]
end
def number
return @line.split("").select{|s|s.to_i.to_s==s}.join("").to_i
end
def type
return @line.split("").select{|s|s.to_i.to_s!=s}.join("")
end
def <=>(o)
if !o.is_a?(ZKMRoute)
return 0
else
n1=number
t1=type
n2=o.number
t2=o.type
if t1==t2
return n1<=>n2
else
return t1<=>t2
end
end
end
def add_trip(id, headsign, direction)
trip=ZKMTrip.new(id, self, headsign, direction)
@trips.push(trip)
return trip
end
def sort_trips
@trips.sort!
end
end
class ZKMTrip
attr_accessor :id, :route, :headsign, :direction
attr_reader :stop_times
def initialize(id, route, headsign, direction)
@id, @route, @headsign, @direction = id, route, headsign, direction
@stop_times=[]
end
def <=>(o)
if !o.is_a?(ZKMTrip)
return 0
else
return first_time<=>o.first_time
end
end
def add_stoptime(stop, arrival_time, departure_time, sequence, headsign, on_demand)
st = ZKMStopTime.new(stop, self, arrival_time, departure_time, sequence, headsign, on_demand)
@stop_times.push(st)
return st
end
def sort_stoptimes
@stop_times.sort!
end
def first_time
return "" if @stop_times.size==0
return @stop_times[0].arrival_time
end
end
class ZKMStop
attr_accessor :id, :code, :name, :latitude, :longitude
def initialize(id, code, name, latitude, longitude)
@id, @code, @name, @latitude, @longitude = id, code, name, latitude, longitude
end
end
class ZKMStopTime
attr_accessor :stop, :trip, :arrival_time, :departure_time, :sequence, :headsign, :on_demand
def initialize(stop, trip, arrival_time, departure_time, sequence, headsign, on_demand)
@stop, @trip, @arrival_time, @departure_time, @sequence, @headsign, @on_demand = stop, trip, arrival_time, departure_time, sequence, headsign, on_demand
end
def <=>(o)
if !o.is_a?(ZKMStopTime)
return 0
else
return @arrival_time<=>o.arrival_time
end
end
end
Cudownie! Udało nam się napisać 83 linie kodu, które jeszcze absolutnie nic nie robią.
Pobieramy konkretne dane
Za każdym razem schemat pobierania danych będzie podobny: będziemy pobierać zawartość pewnego adresu URL, deserializować go jako JSON i w pewien sposób przetwarzać. Możemy napisać więc funkcję, która nam to ułatwi.
def zkm_load(endpoint)
t=read_url("http://api.zdiz.gdynia.pl/pt/#{endpoint}")
raise(IOError, "Cannot load ZKM data") if t==nil
return JSON.load(t)
end
Warto zwrócić uwagę na funkcję “raise”. Poprzednio Grzegorz pisał o wyjątkach, w ten sposób możemy wywołać swój, notyfikując informację o jakimś problemie. “IOError” to jeden z typów wyjątków, służący do informowania o problemach z zapisem/odczytem, tu z odczytem danych z serwera.
Pobierzmy więc najważniejsze dla nas informacje:
j_routes = zkm_load("routes")
j_stops = zkm_load("stops")
j_trips = zkm_load("trips")
j_stoptimes = zkm_load("stop_times")
W pobranych tablicach nie mamy informacji zaprezentowanych w sposób zgodny z naszymi klasami, konkretne zależności są budowane przez informację o identyfikatorach. Na przykład, aby dowiedzieć się, którego przystanku dotyczy dany postój, musimy odnaleźć przystanek o identyfikatorze zgodnym z kluczem “stopId” tego postoju.
Tablice haszowe na pomoc
Moglibyśmy zapisać najpierw informację o wszystkich trasach, a następnie szukać tych tras i dodawać do nich odpowiednie kursy, i tak dalej. Niestety coś takiego nie jest najlepszym pomysłem. Choć bowiem na papierze wygląda to dobrze, w praktyce trwałoby to zbyt długo, bo oznaczałoby za każdym razem konieczność wielokrotnego przeszukiwania tablic. Tu na pomoc przychodzą nam tablice haszowe. Napiszemy więc funkcję, która utworzy wewnętrznie cztery tablice haszowe (linie, trasy, przystanki i czasy zatrzymania się na przystankach). W haszach tych kluczem będzie identyfikator, co pozwoli nam bardzo szybko przywoływać interesujące nas zasoby. Po zakończeniu wszystkiego, uzyskamy gotową tablicę tras.
def zkm_get_routes
alert("Loading routes information")
j_routes = zkm_load("routes")
h_routes={}
for r in j_routes
route=ZKMRoute.new(r['routeId'], r['routeShortName'''], r['routeLongName'])
h_routes[route.id]=route
end
alert("Loading stops information")
h_stops={}
j_stops = zkm_load("stops")
for s in j_stops
stop = ZKMStop.new(s['stopId'], s['stopCode'], s['stopName'], s['stopLat'], s['stopLon'])
h_stops[stop.id]=stop
end
alert("Loading trips information")
h_trips={}
j_trips = zkm_load("trips")
for t in j_trips
route=h_routes[t['routeId']]
trip = route.add_trip(t['tripId'], t['tripHeadsign'], t['directionId'])
h_trips[trip.id] = trip
end
alert("Loading stop times information")
h_stoptimes={}
j_stoptimes = zkm_load("stop_times")
for st in j_stoptimes
trip=h_trips[st['tripId']]
stop=h_stops[st['stopId']]
stoptime = trip.add_stoptime(stop, st['arrivalTime'], st['departureTime'], st['stopSequence'], st['stopHeadsign'], st['onDemand']==1)
h_stoptimes[stoptime.id] = stoptime
end
h_trips.values.each{|t|t.sort_stoptimes}
h_routes.values.each{|r|r.sort_trips}
return h_routes.values.sort
end
Ładujemy informacje o interesujących nas kategoriach, następnie wszystko sortujemy i zwracamy w eleganckiej tablicy, wypisując nawiasem mówiąc kolejne 36 linijek.
Na ekran!
Skoro udało nam się tym heroicznym wysiłkiem pozyskać informacje, na których nam zależało, zróbmy coś z nimi… Na przykład wyświetlmy.
Wyobraziłem sobie prosty formularz, który posiada listę linii, kursów i przystanków. Wybranie linii pokazuje dostępne na niej kursy, zaś kursu, przystanki. W jaki sposób jednak dynamicznie przeładowywać formularz?
Użyjemy tu zdarzenia “:move”, które jest wysyłane, gdy użytkownik przesunie kursor. Następnie podmienimy dostępne w listach wyboru opcje. Aby na początku formularz nie był pusty, sztucznie wywołamy odpowiednie zdarzenie poprzez funkcję “trigger”.
routes = zkm_get_routes
form = Form.new([
lst_route = ListBox.new(routes.map{|r|"#{r.line} (#{r.name})"}, "Routes"),
lst_trip = ListBox.new([], "Trips"),
lst_stop = ListBox.new([], "Stops"),
btn_close = Button.new("Close")
], 0, false, true)
lst_route.on(:move) {
route = routes[lst_route.index]
lst_trip.options = route.trips.map{|t|t.headsign+": "+t.first_time}
lst_trip.index=0
lst_trip.trigger(:move)
}
lst_trip.on(:move) {
route = routes[lst_route.index]
trip=route.trips[lst_trip.index]
lst_stop.options = trip.stop_times.map{|t|
headsign=t.stop.name
l=headsign+": "+t.arrival_time
l+=", on demand!" if t.on_demand
l
}
lst_stop.index=0
}
lst_route.trigger(:move)
btn_close.on(:press) {form.resume}
form.cancel_button=btn_close
form.wait
Podsumowanie
Napisaliśmy dzisiaj 152 linie kodu, nieco więcej, gdy opakujemy to w program. Nasza aplikacja dalej jest dość toporna, ale dzięki zbudowaniu całej struktury relacji, od tej pory urozmaicanie interfejsu będzie czymś bardzo prostym. Można oczywiście pobrać, i przetestować, gotowy program. A w następnym odcinku zajmiemy się nieco lepszym zoptymalizowaniem naszego dzieła.
Pobierz przeglądarkę Rozkładu Jazdy ZKM Gdynia
Dobra, nie myślę dzisiaj lol
Ale tu nie pisaliśmy metod sortowania, tylko porównania. Ruby używa domyślnie algorytmu quicksort.
Czy w Rubym jest coś na podobę LINQa? Chodzi o to że twoje metody sortowania i tak będą wolniejsze od tych teoretycznie wbudowanych w język