GNU-Awk w służbie człowieka (część 1: podział plików GPX)
Na temat gawk popełniono już wiele tekstu więc i ja dodam coś od siebie Przyczynkiem do popełnienia kilku nowych skryptów była w moim przypadku konieczność dzielenia, łączenia i wyszukiwania różnic w plikach XML zawierających współrzędne geograficzne (GPX). Format ten jest popularnym sposobem (standardem?) przechowywania danych z GPS - przynajmniej jeżeli chodzi o urządzenia firmy Garmin z serii eTrex (HCx - taki mam). Pominę tutaj wyjaśnienia dlaczego akurat ten format jest lepszy od miliona pozostałych - równie "standardowych" co GPX.
Ogólnie mówiąc problem polega na tym, chcę trzymać zdjęcia i współrzędne w podziale na poszczególne dni. Ze zdjęciami nie ma problemu. Główne ich źródło to dłuższe lub krótsze wycieczki rowerowe - najczęściej jednodniowe. Zresztą zawsze można sięgnąć do metadanych EXIF, wyciągnąć z nich datę utworzenia a samo zdjęcie przenieść do odpowiedniego katalogu. Ogranicza się to, w zasadzie, do kilku poleceń (Linux z zainstalowanym pakietem ImageMagick):
FILES=$(find . -name *.JPG) for FILE in $FILES; do DATE=$(identify -format "%[EXIF:DateTime]" $FILE | cut -c 1-10 | sed {s/:/-/g}) mkdir -p $DATE mv -p $FILE ${DATE}/ done
No dobrze. Ale co ze współrzędnymi (tzw. waypointami)?
Tu już nie jest tak fajnie bo najpierw trzeba je "wyciągnąć" z GPS i zapisać w formacie GPX (polecam program G7ToWin - dzięki pakietowi Wine zadziała także pod Linuxem). Plik GPX to w zasadzie odpowiednio sformatowany plik XML. Trzeba tylko właściwie go przetworzyć, posortować po dacie utworzenia waypointów, podzielić na dni z uwzględnieniem strefy czasowej (wszystkie daty w GPS są według czasu UTC) i zapisać wynik. Bułka z masłem
W praktyce jest kilka problemów do rozwiązania:
- Plik XML wymaga odpowiedniego przetworzenia i sprawdzenia kompletności danych. W sumie można przyjąć, że jak coś zapisało dane w takim formacie to zrobiło to poprawnie ale pewności nie ma nigdy. Trzeba sprawdzać.
- Daty w pliku GPX są zapisane w standardzie ISO 8601 i do tego w UTC (Universal Coordinated Time). Nie da się ich sortować, odejmować czy dodawać. Wymagają konwersji.
- Oprócz danych poprawnych, w GPS (a więc i w pliku GPX) mogą trafić się ręcznie dodane współrzędne z błędnymi parametrami (wysokość, nazwa, data utworzenia). Poza tym mogą pojawiać się zdublowane dane.
- Wiele urządzeń (np. eTrex Legend HCx) nie zapisuje daty i czasu utworzenia waypointa. Na szczęście informacje te są przechowywane jako komentarz w definicji waypointa. Starsze modele (np. "zwykły" eTrex Legend) w ogóle nie zapisują daty utworzenia… Na szczęście i na to jest rada ale jak już napisałem wcześniej: mam nowszy model
Jest jednak coś, co jest w stanie pomóc ogarnąć problem: gawk.
gawk jest dobry w dwóch rzeczach: wyszukiwaniu i przetwarzaniu wzorców w plikach lub strumieniach danych.
Pre-rekwizyty
Mamy więc interpreter gawk (dostępny w każdej dystrybucji Linux, pod Windows za pomocą pakietu Cygwin), plik GPX i kilka pomysłów. Przede wszystkim trzeba przyjrzeć się pacjentowi. Na nasze szczęście program G7ToWin zapisuje pliki w taki sposób, że definicja każdego waypointa jest oddzielona od poprzedniej pustą linią. Wykorzystamy to do automatycznego oddzielania rekordów danych, za pomocą zmiennej RS (Record Separator).
Idea jest taka:
- Dzielimy plik GPX na rekordy (separatorem jest pusta linia czyli "podwójny enter").
- W każdym rekordzie wyszukujemy datę utworzenia waypointa.
- Sortujemy dane.
- Grupujemy po dacie z dokładnością do dnia i zapisujemy.
O dzieleniu danych na rekordy było przed chwilą. Teraz kilka słów o "dacie utworzenia waypointa", która czasem jest wiarygodna, a czasem nie. Za przechowywanie daty w pliku GPX odpowiada znacznik <time> ale jak już wiemy - nie każde urządzenie datę taką określa (wtedy <time> zawiera datę odczytania danych z GPS przez komputer). Szczęśliwie dla nas część urządzeń zapisuje lokalną datę utworzenia waypointa w formacie "DD-MMM-YY HH:MI:SS" (np. 10-PAZ-11 17:41:12) jako komentarz. Wykorzystamy ten fakt.
Daty w formacie ISO 8601 czy też "DD-MMM-YY" niestety nie nadają się do sortowania (przynajmniej nie za pomocą gawk). Trzeba je poddać konwersji na coś prostszego… czyli liczbę całkowitą. Zabieg jest prosty i pozwala zamieć datę i czas na liczbę sekund, które upłynęły od "linuxowej epoki", czyli od 1 stycznia 1970 roku. Liczby takie mogą być sortowane, porównywane, odejmowane, dodawane czy też konwertowane do innej postaci za pomocą funkcji strftime(). Trzeba jeszcze pamiętać, że jest różnica między czasem UTC i "czasem lokalnym" (w Polsce zmieniającym się dwa razy w roku: tzw. czas letni i zimowy) oraz, że w pliku GPX daty są zapisane jako UTC (znacznik <time>) lub jako czas lokalny (znacznik <cmt>).
Jako kolejny pewnik przyjmujemy, że w pliku GPX mogą pojawić się rekordy z identyczną datą utworzenia oraz takie, które zostały dodane ręcznie. Te ostatnie można zidentyfikować po tym, że najczęściej nie zawierają znacznika <cmt> (a więc naszej daty utworzenia), definicji wysokości (znacznik <ele>) bądź też wysokość wynosi 0 lub -777. Nazwami waypointów nie przejmujemy się.
Skrypt
Po przydługim wstępie przechodzimy do konkretów.
Po pierwsze definiujemy separatory rekordów i pól (wierszy) za pomocą zmiennych RS (Record Separator) i FS (Field Separator), które mogą zawierać wyrażenia regularne. Jest to pomocne ponieważ plik GPX może być zapisany pod Windows (koniec linii "\r\n") lub Linux (koniec linii "\n").
RS="[\r]?\n[ ]+?[\r]?\n" FS="[\r]?\n"
Z danych wejściowych odrzucamy rekordy nie mające nic wspólnego ze współrzędnymi (w tym i tzw. Proximity Points - punkty ostrzegawcze, które dublują się z właściwymi waypointami).
# odsiej zbedny xml i punkty ostrzegawcze (sa zdublowane z waypointami) if ($0 !~ /wpt/) next if ($0 ~ /gpxx:Proximity/) next
Za wyznaczanie daty utworzenia waypointa (jako liczby sekund) odpowiada funkcja getwptstamp(), która działa według algorytmu:
- Jeżeli jest poprawna data w znaczniku <cmt> to jej użyj.
- Jeżeli wysokość waypointa jest poprawna a nie ma <cmt> to użyj znacznika <time>.
- Jeżeli wysokość nie jest poprawna lub nie ma <time> to za datę utworzenia przyjmij 0 (1970-01-01 00:00:01)
- Jeżeli jest błędna data to zwróć (-1).
Poszczególne waypointy są przechowywane w tablicy asocjacyjnej wpt[], której indeksem jest data utworzenia (liczba sekund) oraz krotność (liczba waypointów o tej samej dacie utworzenia). Pomocnicza tablica cnt[] zawiera unikalne daty utworzenia. Skrypt zadba także o to by uspójnić zawartość znaczników <cmt> i <time>.
# unikalne daty utworzenia waypointow (ind) oraz krotnosc (cnt[ind]) cnt[ind]+=1 # tablica waypointow ma indeks zlozony z daty utworzenia i licznika duplikatow # jesli stamp==0 (brak <cmt> w rekordzie) to zostawiamy oryginalny # znacznik <time>; w przeciwnym razie podmieniamy na czas UTC zgodny z <cmt> wpt[ind,cnt[ind]]=(stamp==0)? $0: gensub(/<time>.?*<\/time>/,strftime("<time>%Y-%m-%dT%H:%M:%SZ</time>",stamp,1),$0)
Sortowanie i grupowanie jest przeprowadzane w pętli, która wyznacza nowy indeks (PREFIX) i łączy pod nim wszystkie "pasujące" waypointy (tablica waypoints[]). Jako bonus przeprowadzane jest usuwanie punktów o takiej samej dacie utworzenia i tych samych współrzędnych. Jeżeli takie rekordy się trafią - brany pod uwagę jest tylko pierwszy z nich.
numwpts=asorti(cnt,key) # grupuj wpt po dacie utworzenia (PREFIX) # w przypadku dubli daty: cnt[key[pos]] > 1 for (pos=1;pos<=numwpts;pos++) { ind=key[pos] ymd=strftime(PREFIX,int(ind)) duble=cnt[ind] for (i=1;i<=duble;i++) { # zachowaj wspolrzedne lat i lng waypointow z danego dnia # do wyznacznia obszaru <bounds> w naglowku gpx if (match(wpt[ind,i],/lat=\"([0-9,.-]+)\"[ ]+lon=\"([0-9.,-]+)\"/,latlng)) { lat[ymd]=(lat[ymd])?(lat[ymd] SUBSEP latlng[1]):latlng[1] lng[ymd]=(lng[ymd])?(lng[ymd] SUBSEP latlng[2]):latlng[2] # pomin powielone waypointy # (taka sama data i wspolrzedne) point=(ind SUBSEP sprintf("%2.6f",latlng[1]) SUBSEP sprintf("%2.6f",latlng[2])) loc[point]+=1 if (loc[point]==1) waypoints[ymd]=(waypoints[ymd] LF wpt[ind,i] LF) } else print "rekord bez współrzędnych",LF,wpt[ind,i],LF > "/dev/stderr" } }
Na koniec zawartość tablicy waypoints[] trafia na dysk, pod nazwą określoną przez połączenie wartości zmiennych PREFIX i SUFFIX (domyślnie jest to YYYY-MM-DD.gpx, np.: 2011-10-10.gpx). By uniknąć pomyłek, wszystkie wynikowe pliki są zapisywane w katalogu o nazwie tworzonej dynamicznie.
Voilá. Prosto, szybko i przyjemnie
Poniżej cały skrypt, który uruchamia się za pomocą polecenia: ./wptsplit <plik.gpx>
#!/usr/bin/gawk -f # (c) 2011 pijoter # skrypt do dzielenia pliku waypoints/gpx na poszczegolne dni # definicje waypointow musza byc rozdzielone znakiem nowej linii BEGIN { IGNORECASE=1 RS="[\r]?\n[ ]+?[\r]?\n" FS="[\r]?\n" VERBOSE=1 DEBUG=0 INVALID_ELE=-777 Y2K=2000 LF="\n" # katalog docelowy if (DIR=="") DIR=strftime("SPLIT_%Y%m%d%H%M%S") DIR=(DIR "/"); sub(/[\/]+$/,"/",DIR) system(sprintf("mkdir -p %s",DIR)) # decyduje o nazwie plikow wynikowych PREFIX="%Y-%m-%d" SUFFIX=".gpx" CREATOR="pijoter" LINK_URL="http://www.rowery.olsztyn.pl" LINK_NAME="Olsztynska Strona Rowerowa" i18n["STY"]=i18n["JAN"]="01" i18n["LUT"]=i18n["FEB"]="01" i18n["MAR"]=i18n["MAR"]="03" i18n["KWI"]=i18n["APR"]="04" i18n["MAJ"]=i18n["MAY"]="05" i18n["CZE"]=i18n["JUN"]="06" i18n["LIP"]=i18n["JUL"]="07" i18n["SIE"]=i18n["AUG"]="08" i18n["WRZ"]=i18n["SEP"]="09" i18n["PAZ"]=i18n["OCT"]="10" i18n["LIS"]=i18n["NOV"]="11" i18n["GRU"]=i18n["DEC"]="12" gpx="<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:gpxx=\"http://www.garmin.com/xmlschemas/GpxExtensions/v3\" xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v1\" creator=\"%s\" version=\"1.1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd\">\n <metadata>\n <link href=\"%s\">\n <text>%s</text>\n </link>\n <name>%s</name>\n <time>%s</time>\n <bounds minlat=\"%f\" minlon=\"%f\" maxlat=\"%f\" maxlon=\"%f\"/>\n </metadata>\n%s\n</gpx>\n" } function getwptstamp(rec, spec,cmt,datetime,date,time,utc,isotime,zone,ele) { # data utworzenia waypointa (czas LOKALNY - liczba sekund epoch): # data z <cmt></cmt> (domyslnie) # data z <time></time> (gdy nie ma <cmt> i jest <ele>) # 0 (w przypadku niejasnosci) # -1 (blad) spec=0 if (match(rec,/<cmt>(.+)<\/cmt>/,cmt)) { split(cmt[1],datetime," ") split(datetime[1],date,"-") split(datetime[2],time,":") spec=mktime(sprintf("%4d %02d %02d %02d %02d %02d", Y2K+date[3],i18n[toupper(date[2])],date[1], time[1],time[2],time[3])) } if (spec<=0) { # nie ma znacznika <cmt> lub bledny? # korzystamy z <time> ale tylko gdy jest poprawny # znacznik <ele> (wysokosc) valid=(match(rec,/<ele>([0-9,.-]+)<\/ele>/,ele))? sprintf("%.0f",ele[1])!=INVALID_ELE: (0) if (valid && match(rec,/<time>(.+)<\/time>/,isotime)) { # data i czas w znaczniku <time> jest w UTC # trzeba zamienic go na czas lokalny gsub(/[-:TZ]/," ",isotime[1]) utc=mktime(isotime[1]) zone=mktime(strftime("%Y %m %d %H %M %S",utc,0))-mktime(strftime("%Y %m %d %H %M %S",utc,1)) spec=utc+zone } else spec=0 } return spec } function base(name, part,n) { n=split(name,part,"/") return part[n] } # MAIN { # opcjonalnie, usun znaki "\r" gsub("\r","",$0) # odsiej zbedny xml i punkty ostrzegawcze (sa zdublowane z waypointami) if ($0 !~ /wpt/) next if ($0 ~ /gpxx:Proximity/) next # odsiej bledne waypointy stamp=getwptstamp($0) if (stamp<0) { print "błędny rekord",LF,$0,LF > "/dev/stderr" next } # proteza! asorti() porownuje indeksy jako napisy (a nie liczby) ind=sprintf("%010d",stamp) # unikalne daty utworzenia waypointow (ind) oraz krotnosc (cnt[ind]) cnt[ind]+=1 # tablica waypointow ma indeks zlozony z daty utworzenia i licznika duplikatow # jesli stamp==0 (brak <cmt> w rekordzie) to zostawiamy oryginalny # znacznik <time>; w przeciwnym razie podmieniamy na czas UTC zgodny z <cmt> wpt[ind,cnt[ind]]=(stamp==0)? $0: gensub(/<time>.?*<\/time>/, strftime("<time>%Y-%m-%dT%H:%M:%SZ</time>",stamp,1),$0) } END { # w pliku wynikowym waypointy beda posortowane narastajaco, # po dacie utworzenia numwpts=asorti(cnt,key) # grupuj wpt po dacie utworzenia (PREFIX) # w przypadku dubli daty: cnt[key[pos]] > 1 for (pos=1;pos<=numwpts;pos++) { ind=key[pos] ymd=strftime(PREFIX,int(ind)) duble=cnt[ind] for (i=1;i<=duble;i++) { # zachowaj wspolrzedne lat i lng waypointow z danego dnia # do wyznacznia obszaru <bounds> w naglowku gpx if (match(wpt[ind,i],/lat=\"([0-9,.-]+)\"[ ]+lon=\"([0-9.,-]+)\"/,latlng)) { lat[ymd]=(lat[ymd])?(lat[ymd] SUBSEP latlng[1]):latlng[1] lng[ymd]=(lng[ymd])?(lng[ymd] SUBSEP latlng[2]):latlng[2] # pomin powielone waypointy # (taka sama data i wspolrzedne) point=(ind SUBSEP sprintf("%2.6f",latlng[1]) SUBSEP sprintf("%2.6f",latlng[2])) #if (DEBUG) printf "wpt[%d,%d]\n%s\n\n",ind,i,wpt[ind,i] > "/dev/stderr" loc[point]+=1 if (loc[point]==1) waypoints[ymd]=(waypoints[ymd] LF wpt[ind,i] LF) } else print "rekord bez współrzędnych",LF,wpt[ind,i],LF > "/dev/stderr" } } # uzupelnij naglowek GPX # zapisz waypointy do pliku z danego dnia for (ymd in waypoints) { # wyznacz obszar <bounds> waypointow z danego dnia split(lat[ymd],minmaxlat,SUBSEP) split(lng[ymd],minmaxlng,SUBSEP) num=asort(minmaxlat); latmin=minmaxlat[1]; latmax=minmaxlat[num] num=asort(minmaxlng); lngmin=minmaxlng[1]; lngmax=minmaxlng[num] # if (DEBUG) printf "%s bounds =>\n[latmin=%s]\n[latmax=%s]\n[lngmin=%s]\n[lngmax=%s]\n\n",ymd,latmin,latmax,lngmin,lngmax > "/dev/stderr" # daty w pliku sa w UTC (konwersja w funkcji strftime) printf(gpx, CREATOR, LINK_URL, LINK_NAME, ymd, strftime("%Y-%m-%dT%H:%M:%SZ",systime(),1), latmin,lngmin,latmax,lngmax, waypoints[ymd]) > (DIR ymd SUFFIX) } # statystyki if (VERBOSE) printf("waypointow=%d, unikalnych=%d, plikow=%d (od %s do %s)\n", length(wpt), # waypointy razem z duplikatami length(loc), # unikalne waypointy length(waypoints), # waypointy pogrupowane do dacie YMD strftime("%Y-%m-%d",key[1]), strftime("%Y-%m-%d",key[numwpts])) }
Zobacz także
Na skróty
Ostatnie zmiany
- Le Mans grób ppor. Józefa Franka
- Saint-Mandé utworzono
- Pantin utworzono
- Ivry-sur-Seine utworzono
- Talence utworzono
- I Wojna Światowa (1914-18r.) [Francja]
- Nowiny [Mogiła żołnierska]
- Kowale Oleckie [Pomnik poległych i cmentarz]
- Giżycko (Lec) [Spis poległych]
- Koszyce (Košice) [Pomnik Żołnierzy Armii Czerwonej]
- Rogojny [2024-10-26]
- Krysk Nekrologi, Ltn. Max Paluka
- Kłajpeda [Cmentarz wojenny]
- Kłajpeda (Klaipėda) [Cmentarz wojenny]
- Bolków [2015-08-17]