Loty podczas burzy
Stworzenie tej wizualizacji zajęło mi znacznie więcej czasu niż sądziłem. Pomysł pojawił się 29. czerwca gdy siedziałem w biurze Microsoft przy lotnisku w Warszawie. Burza tego dnia była ogromna. Podczas sprawdzania ruchu lotniczego na Flightradar24, zacząłem zastanawiać się jak wyglądałyby loty na jednym wykresie z mapą i chmurami. Po 3(!) miesiącach pracy, oto i on:
Poniżej znajdziecie szczegółowy opis, jak został stworzony.
1. Dane lotów
Po bardzo dokładnych poszukiwaniach stwierdziłem, że na danych z Flightradar24 można polegać najbardziej, więc wziąłem kartę do ręki i rozpocząłem okres próbny. Na stronie wyfiltrowałem interesujące mnie loty i pobrałem pliki csv z kolumnami: Timestamp, UTC, Callsign, Position, Altitude ,Speed, Direction. Wszystkie pliki są dostępne tutaj.
Głównym problemem z danymi był fakt, że różnice czasu między obserwacjami (wierszami) nie były stałe. Przykładowo podczas startu i lądowanie w danych znajdowało się około 10 obserwacji na minutę, a na wysokości przelotowej 1 lub 2. Z tego powodu musiałem interpolować “brakujące” obserwacji zakładając, że zmiana między dwoma obserwacjami była liniowa.
Kod, użyty do przygotowania danych jest dostępny poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import re from numpy import genfromtxt import numpy as np import os.path from distribution import uniform_distribution #the function I have written #Get list of all the CSV path = '.' r = re.compile(".*csv") csv_list = list(filter(r.match,os.listdir(path))) csv_count = len(csv_list) def data_extract(file1): #Rename files and write them to a separate folder file2 = file1.replace('Flight_','')[:5] + '.csv' fin = open(file1) data = fin.read().splitlines(True) fout = open('source files/'+ file2,"w+") for line in data[1:]: line = line.replace('"','') fout.write(line) fin.close() fout.close() #Create arrays from the data and filter it by time data = genfromtxt('source files/'+ file2, delimiter=",", dtype = None, names="timestamp, utc, callsigb, pos1, pos2, att, speed, direction") data = data[::-1] data = data[data['timestamp']>1498731036] #Interpolate the data coords = uniform_distribution (data) return coords |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#Interpolate the coords from numpy import genfromtxt import numpy as np import math import bisect def uniform_distribution (data): #Get min and max time from the data max_t = max(data['timestamp']) min_t = min(data['timestamp']) #Divide it by 12 seconds, because this is my aim diff = (max_t-min_t)//12 rounded = math.ceil(diff) #Create new array with time distributed by 12 seconds new_data = np.arange(min_t, max_t, 12) pos1 = np.empty(len(new_data)) pos2 = np.empty(len(new_data)) for i,timestamp in enumerate(new_data): #Find next timestamp bigger than current index = bisect.bisect(data['timestamp'], timestamp) #Smaller smaller = data['timestamp'][index-1] smaller_pos1 = data['pos1'][index-1] smaller_pos2 = data['pos2'][index-1] #Bigger bigger = data['timestamp'][index] bigger_pos1 = data['pos1'][index] bigger_pos2 = data['pos2'][index] #Linear interpolation pos1[i]=smaller_pos1 + ((bigger_pos1-smaller_pos1)/(bigger - smaller))*(timestamp-smaller) pos2[i]=smaller_pos2 + ((bigger_pos2-smaller_pos2)/(bigger - smaller))*(timestamp-smaller) #Merge columns and return them new_coords = np.column_stack((pos1, pos2)) return new_coords |
2.Mapa i chmury
Dane chmur zdobyłem od portalu radar-opadow, którego ekipa dostała od IMGW. Pliki sa dostępne tutaj. Pliki te musiały zostać przetworzone używając proj4. Cały proces jest opisany na tej stronie. Ta część projektu była najwolniejsza i ostatecznie zapisywałem wycinku ekranu z plików HTML, które stworzyłem. Tym sposobem otrzymałem kilkanaście plików, które miały posłużyć jako tło dla wykresu. Przykładowy html jest do pobrania pod tym linkiem (kliknij zapisz jako).
3. Tworzenie wykresu
Początkowo planowałem stworzyć animację korzystając z Matplotlib i zapisać ją jako gif/mp4. Niestety nie byłem w stanie znaleźć sposobu na zmianę tła razem z kolejnymi klatkami. Aby rozwiązać ten problem napisałem pętlę, która tworzyła poszczególne klatki. Cały kod jest poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import re from numpy import genfromtxt import numpy as np import os.path from distribution import uniform_distribution #Find all the backgrounds path1 = '.' r1 = re.compile(".*png") png_list = list(filter(r1.match,os.listdir(path1))) png_count = len(png_list) path = '.' r = re.compile(".*csv") csv_list = list(filter(r.match,os.listdir(path))) csv_count = len(csv_list) fig = plt.figure(figsize = (50,80)) lines = [plt.plot([], [], 'r-', markersize=10,linewidth=5)[0] for _ in range(csv_count)] extracted_data = [] for i in range(csv_count): extracted_data.append(data_extract(csv_list[i])) if __name__ == '__main__': for line in lines: line.set_data([], []) #Loop all the frames for i in range(0,475): for index, line in enumerate(lines): line.set_data(((extracted_data[index][1+i:10+i,1]-13)*(600/13)-45)*(1326/531), (392-(extracted_data[index][1+i:10+i,0]-49)*(430/6)+40)*(983/392)) l = i//50 img = plt.imread(png_list[l]) plt.imshow(img, zorder = 1,interpolation='nearest') #extent=[14, 27, 49, 55]) plt.axis('off') plt.savefig('final_gif/' + str(i) + "_ready.jpg", bbox_inches='tight', dpi=40) print('Frame {}'.format(i)) |
Za pomocą tego skryptu wygenerowałem 476 klatek, które później zostały zapisane w MP4. W pierwszym podejściu chciałem użyć pythona do stworzenia animacji, ale po kilku eksperymentach zawsze kończyłem z plikiem o rozmiarze 500+. Z tego powodu poprosiłem o pomoc kolegę, który jest mistrzem Photoshopa. On dał radę stworzyć znacznie mniejszy plik.
Czekam na feedback, to są moje pierwsze kroki w Pythonie i pewnie dałoby się zrobić to lepiej. Każda sugestia jest mile widziana 🙂
Dzięki!