Szybkie bo za pomocą google apps script (GAS).
Nie miałem do wczoraj świadomości że można automatyzować różne
rzeczy korzystając z narzędzia pn
Google Apps Script.
Konkretnie zaczynamy tu. A nawet
konkretniej https://script.google.com/home/my
. Oczywiście trzeba być
zalogowanym żeby link zadziałał.
No więc kliknąłem nowy projekt a potem zmodyfikowałem przykład z samouczka bo zawierał wszystko co potrzeba:
// Create a new form, then add a checkbox question, a multiple choice question, // a page break, then a date question and a grid of questions. function convertToForm(){ var form = FormApp.create('New Form') form .setTitle('Badanie wizerunku i marki) .setDescription('Badamy wizerunek i markę') form.addPageBreakItem() .setTitle('Część 1/3'); form.addCheckboxItem() .setTitle('What condiments would you like on your hot dog?') .setChoiceValues(['Ketchup', 'Mustard', 'Relish']) form.addMultipleChoiceItem() .setTitle('Do you prefer cats or dogs?') .setChoiceValues(['Cats','Dogs']) .showOtherOption(true); form.addPageBreakItem() .setTitle('Część 2/3'); form.addDateItem() .setTitle('When were you born?'); form.addGridItem() .setTitle('Rate your interests') .setRows(['Cars', 'Computers', 'Celebrities']) .setColumns(['Boring', 'So-so', 'Interesting']); form.addScaleItem() .setTitle('Scale item') .setLabels('zły', 'dobry') form.addTextItem() .setTitle('Uwagi') .setRequired(true) form.addMultipleChoiceItem() .setTitle('Płeć') .setChoiceValues(['K','M']) .setRequired(true); form.addTextItem() .setTitle('Wiek') .setRequired(true) Logger.log('Published URL: ' + form.getPublishedUrl()); Logger.log('Editor URL: ' + form.getEditUrl()); }
Następnie Save. Uruchamiamy przez Run/Run function. Jeżeli
nie będzie błędów pod adresem https://docs.google.com/forms/u/0/
pojawi się formularz. Jak coś nie pasuje, to można resztę doklikać.
Długie formularze w ułamku tego czasu który jest potrzebny do ręcznego
wklikania da się zrobić...
Google has launched a new website that uses anonymous location data collected from users of Google products and services to show the level of social distancing taking place in various locations. The COVID-19 Community Mobility Reports web site will show population data trends of six categories: Retail and recreation, grocery and pharmacy, parks, transit stations, workplaces, and residential. The data will track changes over the course of several weeks, and as recent as 48-to-72 hours prior, and will initially cover 131 countries as well as individual counties within certain states. (cf. www.google.com/covid19/mobility/.)
The raports contains charts and comments in the form:
NN% compared to baseline (in six above mentioned categories) where NN is a number.
It is assumed the number is a percent change at the last date
depicted (which accidentaly is a part of a filename). So for example a filename
2020-03-29_PL_Mobility_Report_en.pdf
contains a sentence `Retail & recreation -78% compared to baseline` which (probably)
means that (somehow) registered traffic at R&R facilities was 22% of the baseline.
Anyway those six numbers was extracted for OECD countries (and some other countries)
and converted to CSV file.
The conversion was as follows: first PDF files was downloaded with simple Perl script:
#!/usr/bin/perl # https://www.google.com/covid19/mobility/ use LWP::UserAgent; use POSIX 'strftime'; my $sleepTime = 11; %OECD = ('Australia' => 'AU', 'New Zealand' => 'NZ', 'Austria' => 'AT', 'Norway' => 'NO', 'Belgium' => 'BE', 'Poland' => 'PL', 'Canada' => 'CA', 'Portugal' => 'PT', 'Chile' => 'CL', 'Slovak Republic' => 'SK', ## etc ... ); @oecd = values %OECD; my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0', cookie_jar =>{}); my $date = "2020-03-29"; foreach $c (sort @oecd) { $PP="https://www.gstatic.com/covid19/mobility/${date}_${c}_Mobility_Report_en.pdf"; my $req = HTTP::Request->new(GET => $PP); my $res = $ua->request($req, "${date}_${c}_Mobility_Report_en.pdf"); if ($res->is_success) { print $res->as_string; } else { print "Failed: ", $res->status_line, "\n"; } }
Next PDF files was converted to .txt
with pdftotext
. The relevant
fragments of .txt
files looks like:
Retail & recreation +80% -78% compared to baseline
So it looks easy to extract the relevant numbers: scan line-by-line looking for a line with appropriate content (Retail & recreation for example). If found start searching for 'compared to baseline'. If found retrieve previous line:
#!/usr/bin/perl $file = $ARGV[0]; while (<>) { chomp(); if (/Retail \& recreation/ ) { $rr = scan2base(); } if (/Grocery \& pharmacy/ ) { $gp = scan2base(); } if (/Parks/ ) { $parks = scan2base(); } if (/Transit stations/ ) { $ts = scan2base(); } if (/Workplaces/ ) { $wps = scan2base(); } if (/Residential/ ) { $res = scan2base(); print "$file;$rr;$gp;$parks;$ts;$wps;$res\n"; last; } } sub scan2base { while (<>) { chomp(); if (/compared to baseline/) { return ($prevline); } $prevline = $_; } }
Extracted data can be found here.
W zeszłym roku wziąłem udział w imprezie kolarsko-rekreacyjnej pn. Żuławy w Koło, a teraz zapisałem się na Kociewie Kołem, która ma się odbyć 9 września. Ta sama firma organizuje jak się łatwo domyśleć.
Żeby nie jechać w ciemno pobrałem stosowne dane ze strony organizatora, zamieniłem je na plik CSV i policzyłem różne statystyki. W 2016 średnia prędkość na najdłuższym dystancie (170 km) wyniosła na przykład 27,05 km/h. Rok później (dystans 155 km) było to 26,69 km/h. Czyli sporo, bo na płaskiej i krótszej trasie Żuławy w Koło było dla przykładu w 2016 roku 25,47 km/h, a w 2017 26,23 km/h. Więcej szczegółów na wykresach pudełkowych obok.
Ściągnąłem też w środę listę uczestników, których okazało się jest 719, w tym z Gdańska 332, z Gdyni 107, a tak w ogóle to ze 120 różnych miejscowości. Za pomocą Google Fusion Tables można pokazać listę na mapie. Żeby kropki z tej samej miejscowości się nie nakładały na siebie zastosowałem losowe `drganie' (jitter) wg. algorytmu:
### Jitter w kole o średnicy $r $factorJ = 0.00001; ## ustalone heurystycznie $sd = sqrt($factorJ * $N); # $N liczba kropek dla miejscowosci, tj dla GDA 332 $r = $sd * sqrt(rand()); $theta = rand() * 2 * $pi; $rand_lat = $lat + $r * cos($theta); $rand_lon = $lon + $r * sin($theta); ### Jitter w prostokącie o boku $r $rand_lat = $lat + rand($sd); $rand_lon = $lon + rand($sd);
Rezultat jak na obrazku poniżej, albo tutaj.
Lewy obrazek to mapa bez `jittera' a prawy z zastosowanym `jitterem'.
Bo zmieniła się licencja na korzystanie (z Google Maps API):
Google has made significant changes to the way they allow the use of their Google Maps Platform. Google maintains its Google Map API will still be free to a certain limit and it provides free $200 monthly recurring credit to every user. Google requires all user to have a billing account. They must be willing to submit credit card details, so excess is charged pro rata.
Więcej jest tutaj.
W rezultacie tych zmian na mapach Google, które wykorzystują poprzez API (konkretnie korzystają z Maps JS API) pojawiło się okienko z komunikatem Ta strona nie może poprawnie wczytać Map Google. Sama wyświetlona mapa jest zaś na szaro i ma powtórzony znak wodny For development purposes only.
No kicha niewątpliwie. Na razie nie wiem co robić, bo rejestrować kartę do moich hobbystycznych projektów to mi się nie chce. Rozważam wykorzystanie czegoś w zamian.
Oprócz Google Maps JS API korzystałem też z Google Geocoding API. No i ta usługa też przestała działać: OVER_QUERY_LIMIT dostaję w odpowiedzi. Tutaj też rozważam wykorzystanie czegoś w zamian, a nawet rozważyłem i spróbowałem: darmowy serwis MapQuest. BTW nie wiemy czy to jest najlepszy zamiennik ale spróbować trzeba.
Piszą (cf. https://developer.mapquest.com/documentation/open/geocoding-api/
), że:
The Open Geocoding API is similar
to our MapQuest Geocoding API, but instead relies solely
on data contributed to OpenStreetMap.
Zapytania mogą mieć postać metod GET
lub POST
.
Jest jeszcze coś co się nazywa Batch Geocode
, które to coś
pozwala na geokodowania do 100 adresów na raz.
Szczegółowa dokumentacja jest
tutaj.
Posługując się metodą GET korzysta się bardzo prosto:
http://open.mapquestapi.com/geocoding/v1/address?key=APIKEY&location=ADDRESS
Użyteczne parametry:
boundingBox=UppLLat,UppLLon,LowRLat,LowRLon
(górny lewy/dolny prawy),
maxResults
,
outFormat
(json,xml albo csv).
Przykłady:
http://open.mapquestapi.com/geocoding/v1/address?key=KLUCZ&location=Sopot,PL&maxResults=10 ## BB województwa pomorskiego 54.83572969999 19.6489837 53.49067179999999 16.699129 http://open.mapquestapi.com/geocoding/v1/address?key=KLUCZ&location=Sopot,PL&\ boundingBox=54.835729,19.648983,53.490671,16.699129
Żeby było łatwiej powyższe zaimplementowałem w prostym skrypcie Perlowym:
#!/usr/bin/perl # use strict; use LWP::Simple; # from CPAN use JSON qw( decode_json ); # from CPAN use Getopt::Long; #use Encode; use utf8; binmode(STDOUT, ":utf8"); my $format = "json"; # xml | csv (not implemented) my $myAPIkey = 'APIKEY'; ## Klucz od MapQuesta (trzeba się zarejestrować) my $geocodeapi = "http://open.mapquestapi.com/geocoding/v1/address?key=$myAPIkey&location="; my $addrLine = ''; my $logFile = ''; my $boundingBox = ''; my $country = 'PL'; my $maxResults = 1; ## podaj tylko pierwszy my $USAGE="USAGE: $0 [-a ADDRESS] [-log FILENAME] [-country CODE] [-bb SWlat,SWlng|NElat,NElng] \n"; GetOptions( "a=s" => \$addrLine, "log=s" => \$logFile, "country=s" => \$country, "bb=s" => \$boundingBox, "max=i" => \$maxResults ) ; if ($logFile) { open LOG, ">>$logFile" || die "Cannot log to $logFile\n" ; print STDERR "*** Logging JSON to $logFile ***\n" ; } else { print STDERR "*** Logging JSON OFF ***\n"; } unless ($addrLine ) { print STDERR "$USAGE"; exit 0; } my ($lat, $lng, $quality, $lname) = getLatLong($addrLine, $boundingBox); printf "Lat/Lng/Name/Quality: %s %s %s %s\n", $lat, $lng, $lname, $quality; if ($logFile) { close (LOG) ; } ### ### ### ### ### ### ### ### sub getLatLong($){ my $address = shift; my $bb = shift; my $url = $geocodeapi . "$address,$country" ; # if ($boundingBox ) { $bb =~ s/[\|,]/,/g; $url .= "&boundingBox=$bb" ; } if ($maxResults ) { $url .= "&maxResults=$maxResults" ; } print STDERR "\n*** [URL: $url] ***\n"; my $json = get($url); if ($logFile) { print LOG "##_comment_:\n##_comment_:$url\n$json\n"; } my $d_json = decode_json( $json ); #if ( $d_json->{statuscode} eq '0' ) { my $loc_name = $d_json->{results}->[0]->{providedLocation}->{location}; my $lat = $d_json->{results}->[0]->{locations}->[0]->{latLng}->{lat}; my $lng = $d_json->{results}->[0]->{locations}->[0]->{latLng}->{lng}; my $quality = $d_json->{results}->[0]->{locations}->[0]->{geocodeQuality}; return ($lat, $lng, $quality, $loc_name ); #} else { return ( $d_json->{statuscode} ) } } ## ///
Teraz wystarczy na przykład:
geocodeCoder1.pl -a Sopot -bb 54.835729,19.648983,53.490671,16.6991
W pewnym hobbystycznym projekcie geokoduję nazwy miejscowości używając do tego celu usługi Google. Potencjalnym źródłem błędnych wyników mogą być miejscowości, które mają identyczne nazwy. Jaka jest skala zjawiska?
Według obwieszczenie Ministra Administracji i Cyfryzacji z dnia 4 sierpnia 2015 r. w wykazie urzędowych nazw miejscowości i ich części (Dziennik Ustaw 19 10. 2015 r., poz. 1636) znajduje się: 103086 nazw, w tym 915 nazw miast, 6710 nazw części miast, 43068 nazw wsi, 36263 nazwy części wsi, 5132 nazwy osad. Z czego wynika, że wsi + miast jest 43068 + 915 = 43,983 nazw. (Na podstawie KSNG/Urzędowe nazwy miejscowości)
Po ściągnięciu arkusza z powyższej strony, zamieniam go na format CSV używając jako separatora pola średnika (bo lubię). Następnie za pomocą poniższego skryptu agregują dane:
#!/usr/bin/perl # zamiana listy nazw na listę: # nazwa;województwo;liczba-wystąpień-w-województwie while (<>) { chomp(); @tmp = split /;/, $_; if ($tmp[1] eq 'wieś' || $tmp[1] eq 'miasto' ) { $NW{"$tmp[4]"}{"$tmp[0]"}++; } } for $w (sort keys %NW ) { for $m (sort { $NW{$w}{$b} <=> $NW{$w}{$a} } keys %{$NW{$w} }) { print "$m;$w;$NW{$w}{$m}\n"; } }
Teraz z kolei używając R grupuję dane w podziale na następujące cztery klasy: 1 wystąpienie, 2 wystąpienia 3--4 oraz 5 i więcej wystąpień. Rezultaty zestawiono w poniższej tabeli (kolumny 2--5 to liczebności a 6--10 udziały; przykładowo zapis [1,2) oznacza, że klasa zawiera 1 a nie zawiera 2):
Województwo [1,2) [2,3) [3,5) [5,30) [1,2) [2,3) [3,5) [5,30) --------------------------------------------------------------------- dolnośląskie 2082 117 37 3 92,99 5,23 1,65 0,13 kuj.-pomorskie 2257 183 52 10 90,20 7,30 2,10 0,40 lubelskie 2698 154 65 26 91,68 5,23 2,21 0,88 lubuskie 965 57 5 0 93,96 5,55 0,49 0,00 łódzkie 3276 275 99 45 88,70 7,40 2,70 1,20 małopolskie 1594 105 21 4 92,46 6,09 1,22 0,23 mazowieckie 5780 466 177 79 88,90 7,20 2,70 1,20 opolskie 941 39 9 1 95,05 3,94 0,91 0,10 podkarpackie 1429 49 17 2 95,46 3,27 1,14 0,13 podlaskie 2852 136 42 9 93,80 4,50 1,40 0,30 pomorskie 1579 58 13 2 95,58 3,51 0,79 0,12 śląskie 1033 48 7 2 94,77 4,40 0,64 0,18 świętokrzyskie 1844 111 51 12 91,38 5,50 2,53 0,59 war.-mazurskie 2060 128 34 3 92,58 5,75 1,53 0,13 wielkopolskie 3378 310 99 19 88,80 8,10 2,60 0,50 zachodniopomorskie 1504 117 19 1 91,65 7,13 1,15 0,06
Lista najczęściej powtarzających się (więcej niż 4 powtórzenia) nazw w poszczególnych województwach:
dolnośląskie: Księginice (5) Nowy Dwór (5) Piotrowice (5)
kujawsko-pomorskie: Nowa Wieś (14) Dąbrówka (9) Nowy Dwór (8) Janowo (7) Zalesie (7) Ostrowo (6) Zakrzewo (6) Józefowo (6) Białe Błota (5) Brzeźno (5)
lubelskie: Marysin (9) Brzeziny (8) Zalesie (8) Dąbrowa (7) Stara Wieś (7) Władysławów (6) Antoniówka (6) Nowiny (6) Józefów (6) Ludwinów (6) Dębina (6) Natalin (5) Zastawie (5) Nowosiółki (5) Stanisławów (5) Ruda (5) Kąty (5) Michałówka (5) Janówka (5) Ostrów (5) Zabłocie (5) Huta (5) Wysokie (5) Nowa Wieś (5) Wojciechów (5) Siedliska (5)
mazowieckie: Nowa Wieś (28) Zalesie (23) Dąbrowa (21) Helenów (16) Władysławów (16) Józefów (15) Aleksandrów (15) Dąbrówka (14) Zawady (14) Stanisławów (13) Ruda (11) Marianów (11) Janów (11) Ludwików (10) Kamionka (10) Górki (10) Borki (9) Ostrówek (9) Lipiny (9) Grabina (9) Janówek (9) Marysin (8) Józefowo (8) Stara Wieś (8) Majdan (8) Łazy (8) Michałów (8) Wygoda (7) Bronisławów (7) Julianów (7) Sewerynów (7) Adamowo (7) Grądy (7) Trzcianka (7) Osiny (7) Adamów (7) Kąty (7) Emilianów (6) Żuków (6) Wymysłów (6) Pieńki (6) Lipniki (6) Grabowo (6) Franciszków (6) Mościska (6) Dębówka (6) Mała Wieś (6) Natolin (6) Piaski (6) Anielin (6) Bieliny (6) Lipa (6) Wincentów (6) Kazimierzów (6) Pogorzel (5) Aleksandrówka (5) Józefin (5) Laski (5) Huta (5) Kałęczyn (5) Łaziska (5) Marianka (5) Pawłowice (5) Karolew (5) Osiek (5) Zakrzew (5) Zamość (5) Chmielewo (5) Ostrów (5) Rososz (5) Białobrzegi (5) Romanów (5) Pawłowo (5) Wyczółki (5) Kozłów (5) Podgórze (5) Dąbrówki (5) Ignaców (5) Jakubów (5)
małopolskie: Przybysławice (6) Zawadka (5) Siedliska (5) Biskupice (5)
opolskie: Jakubowice (5)
podkarpackie: Nowa Wieś (7) Zalesie (5)
podlaskie: Zalesie (9) Olszanka (8) Ogrodniki (8) Wólka (7) Janowo (5) Rybaki (5) Nowinka (5) Nowosady (5) Jałówka (5)
pomorskie: Dąbrówka (8) Ostrowite (5)
warmińsko-mazurskie: Zalesie (7) Nowa Wieś (5) Olszewo (5)
wielkopolskie: Dąbrowa (15) Nowa Wieś (13) Bielawy (9) Zalesie (8) Biskupice (7) Zakrzewo (7) Marianowo (6) Józefowo (6) Józefów (6) Ruda (5) Zawady (5) Piaski (5) Brzezie (5) Chrustowo (5) Góra (5) Drzewce (5) Kamień (5) Nowy Dwór (5) Kamionka (5)
zachodniopomorskie: Grabowo (5)
łódzkie: Józefów (20) Dąbrowa (17) Janów (17) Dąbrówka (14) Stanisławów (14) Nowa Wieś (11) Zalesie (11) Piaski (11) Adamów (11) Zawady (10) Marianów (10) Ostrów (9) Władysławów (9) Stefanów (9) Helenów (8) Dębina (8) Aleksandrów (8) Julianów (7) Bronisławów (7) Marianka (7) Borki (7) Stara Wieś (7) Brzeziny (7) Gaj (7) Michałów (7) Ludwików (6) Witów (6) Wygoda (6) Kazimierzów (6) Kuźnica (6) Anielin (6) Karolew (6) Ruda (6) Antoniew (5) Jeziorko (5) Konstantynów (5) Zakrzew (5) Emilianów (5) Osiny (5) Poręby (5) Feliksów (5) Borowa (5) Zagórze (5) Teodorów (5) Wymysłów (5)
śląskie: Zawada (6) Nowa Wieś (6)
świętokrzyskie: Wolica (9) Wymysłów (8) Podlesie (8) Hucisko (6) Zagórze (5) Nowa Wieś (5) Brzeście (5) Zakrzów (5) Janów (5) Bugaj (5) Ludwinów (5) Górki (5)
W skali całego kraju jest 5080 miejscowości, których nazwa nie jest unikatowa, w tym, w przypadku 868 miejscowości nazwy powtarzają się 5 i więcej razy. Pierwsza dziesiątka wygląda następująco: Nowa Wieś (113 powtórzeń), Dąbrowa (92), Zalesie (86), Dąbrówka (65), Józefów (49), Kamionka (41), Ruda (39), Janów (39), Zawady (39) Stanisławów (38).
W pierwszym akapicie napisałem, że problem może być potencjalny ponieważ wydaje się, że geokoder Google działa całkiem sprytnie. Przykładowo
Nowa Wieś -> 07-416 Nowa Wieś (powiat oświęcim) Nowa Wieś, ul. Dworcowa -> 64-234 Nowa Wieś (Powiat wolsztyński)
Po wpisaniu po prostu Nowa Wieś
geocoder, z jakiś
powodów, decyduje się na
Nową Wieś w powiecie oświęcimskim, ale już
podanie dodatkowo ul. Dworcowa
zmienia wynik na Nową Wieś
w powiecie wolsztyńskim; faktycznie w tej wsi jest ul. Dworcowa
(pytanie czy tylko w tej). Można uściślić o co nam chodzi podając też:
Nowa Wieś, powiat wolsztyński Nowa Wieś, województwo wielkopolskie
Podanie powiatu powinno rozwiązać problem duplikatów, zaś drugi sposób wydaje jednak mniej pewny biorąc pod uwagę liczbę powtórzeń nazw na poziomie województwa...
Skrypty i dane są tutaj.
Wkurzające są ciągłe udoskonalenia usług Google, których obocznym skutkiem jest niekompatybilność i/lub konieczność zmiany narzędzi, które działały kiedyś i nagle przestały działać. Taka przykrość spotkała mnie wczoraj i dotyczyła importu plików KML do myMaps/Mojemapy google.
Otóż kiedyś działało coś takiego (opisującego miejsce zrobienia zdjęcia):
<Placemark><name>epl316_2135207.jpg</name> <description><![CDATA[ <a href='https://www.flickr.com/photos/tprzechlewski/24967425626/' target='_blank'> <img src='https://farm2.staticflickr.com/1508/24967425626_2166aceb85_m.jpg' />epl316_2135207.jpg </a>]]> </description> <Point><coordinates>9.20398056,45.48436667</coordinates> </Point> </Placemark>
dziś przestało. Po kliknięciu w pinezkę, otwiera się okienko z informacją o zdjęciu, ale miniaturka zdjęcia nie jest wyświetlana.
Za pomocą reverse-engineering ustaliłem (być może nie jest to rozwiązanie optymalne), że działa poniższe:
... jak poprzednio ... </description> <ExtendedData><Data name='gx_media_links'> <value>https://farm2.staticflickr.com/1508/24967425626_2166aceb85_m.jpg</value> </Data></ExtendedData> <Point> ...itd...
tzn po elemencie description
trzeba wstawić ExtendedData
,
z zawartością jak wyżej.
Por także: Importowanie i wyświetlanie plików KML w Google Maps oraz Keyhole Markup Language/Adding Custom Data.
W promocji (w ramce po prawej) mapka wycieczki do Mediolanu/Bergamo zaimportowana w opisany wyżej sposób.
GoogleCL przestało działać, bo Google przestało obsługiwać wersję OAuth 1.0. Ponadto, wygląda na to, że dostosowanie tego użytecznego narzędzia do wersji OAuth 2.0 bynajmniej nie jest trywialne na co wskazują liczne (ale do tej pory bezskuteczne) prośby i wołania o aktualizację GoogleCL, które można znaleźć w Internecie.
Ponieważ poszukiwania
w miarę podobnego zamiennika zakończyły
się niepowodzeniem, nie pozostało nic innego zmajstrować coś samodzielnie.
Autoryzację OAuth 2.0 mam już opanową -- obsługuje ją Pythonowy skrypt oauth2picasa.py
.
(Skrypt jest (zapożyczonym) fragmentem z projektu
picasawebsync
).
Wystarczyło
dorobić następujący prosty skrypt Perlowy (por. także:
Publishing
a blog post):
#!/usr/bin/perl # *** Wyslanie posta na blogger.com *** use strict; use LWP::UserAgent; use XML::LibXML; use Getopt::Long; my $profileID="default"; my $blogID = '1928418645181504144'; # Identyfikator bloga my $blog_entry ; ## Na wypadek gdy ktoś ma kilka blogów moża podać na któr ## ma być wysłany post używając opcji -blog GetOptions( "blog=s" => \$blogID, "post=s" => \$blog_entry) ; if ( $blog_entry eq '' ) { print STDERR "*** USAGE: $0 -b blog -p message (-b is optional) ***\n" } ## sprawdź czy post jest well formed: my $parser = XML::LibXML->new(); eval {my $res_ = $parser->parse_string($blog_entry) }; if ($@) { die "*** Error parsing post message! \n"; } my $ACCESS_TOKEN=`oauth2blogger.py`; # pobierz ACCESS_TOKEN print STDERR "*** AccessToken: $ACCESS_TOKEN ***\n"; my $req = HTTP::Request->new( POST => "https://www.blogger.com/feeds/$blogID/posts/default"); $req->header( 'Content-Type' => 'application/atom+xml' ); $req->header( 'Authorization' => "Bearer $ACCESS_TOKEN" ); $req->header( 'GData-Version' => '2' ); $req->content($blog_entry); my $ua = LWP::UserAgent->new; my $res = $ua->request($req); # Jeżeli coś jest nie tak poniższe drukuje verbatim: # http://www.perlmonks.org/bare/?node_id=464442 # $ua->prepare_request($req); print($req->as_string); exit ; if ($res->is_success) { my $decoded_response = $res->decoded_content; print STDERR "*** OK *** $decoded_response\n"; } else { die $res->status_line; }
Wykorzystanie:
perl blogger_upload.pl -p 'treść-posta'
Treść posta musi być oczywiście w formacie xHTML i zawierać
się wewnątrz elementu content
, który z kolei
jest wewnątrz elementu entry
.
Element entry
zawiera także title
określający tytuł posta, oraz elementy category
zawierające
tagi. Przykładem może być coś takiego:
<entry xmlns='http://www.w3.org/2005/Atom'> <title type='text'>Marriage!</title> <content type='xhtml'> <div xmlns="http://www.w3.org/1999/xhtml"> <p>Mr. Darcy has proposed marriage to me!</p> <p>He is the last man on earth I would ever desire to marry.</p> <p>Whatever shall I do?</p> </div> </content> <category scheme="http://www.blogger.com/atom/ns#" term="marriage" /> <category scheme="http://www.blogger.com/atom/ns#" term="Mr. Darcy" /> </entry>
Opisany skrypt jest tutaj:
blogger_upload.pl
.
Google wyświetla coś takiego kiedy oglądam mapę z ostatniego wyjazdu:
Jedna z funkcji Map Google używanych na tej stronie wkrótce się zmieni. Konieczne będzie przeniesienie zawartości niestandardowej mapy.
Klikam w Dowiedz się więcej:
Po lutym 2015 roku nie będzie już można wyświetlać niestandardowej zawartości plików KML w klasycznej wersji Map Google. KML to format pliku używany przez Google Earth do wymiany informacji geograficznych.
Dalej nie bardzo wiadomo w czym będzie problem, ale przynajmniej powiedzieli, że to już niedługo, jak przestanie działać. Jeszcze jeden klik i w końcu wiadomo o co chodzi (ale po angielsku):
From February 2015, maps created in the classic Google Maps --
https://maps.google.com/
-- will no longer load KML/KMZ files from
external websites. However, we know that KML files are a really useful
way to work with geographic data, so we've added KML to Google My
Maps, and continue to support this format with other Google Maps
APIs. We hope that one of these options will meet your needs.
A więc
google zmienił zdanie.
Kiedyś twierdził, że cyt:
KML upload and rendering has always
been problematic with Maps i że lepiej (jeżeli już), to nie importować KML tylko
wyświetlić go podając URL do
dokumentu KML jako wartość parametru q
, przykładowo:
https://mapy.google.pl/maps?q=http://pinkaccordions.homelinux.org/PLIK.kml
A teraz że wręcz przeciwnie: parametr q
w ogóle nie zadziała.
Wraz ze zmianą zdania trzeba przyznać poprawiono wsparcie dla KML w Google Maps -- teraz już nie jest problematic (sprawdziłem jest OK).
Google wreszcie ustalił, że można korzystać z Google Maps bez zakładania konta Google i podjął stosowne działania? Nieładnie.
Zatem trzeba będzie zmienić sposób w jakim są wyświetlane pliki KML na Google Maps (teraz to się nazywa zdaje się Moje Mapy/My Maps). Na szczęście nie jest tego w moim przypadku za dużo.
UWAGA: Ten tekst nie jest o polityce ale o [elementarnej] statystyce.
Media informowały, że posłowie PiS Adam Hofman, Mariusz A. Kamiński i Adam Rogacki wzięli na podróż do Madrytu na posiedzenie komisji Zgromadzenia Parlamentarnego Rady Europy po kilkanaście tysięcy złotych zaliczki, zgłaszając wyjazd samochodem; w rzeczywistości polecieli tanimi liniami lotniczymi. Ponieważ kontrola wydatków posłów jest iluzoryczna różnica pomiędzy kosztem podróży samochodem a samolotem [za dużo mniejsze pieniądze] miała stanowić dodatkowy przychód wyżej wymienionych. Według prokuratury, która wszczęła śledztwo, zachodzi podejrzenie popełnienia oszustwa.
Łapiąc wiatr w żagle [sprawa się upubliczniła tuż przed ostatnimi wyborami samorządowymi] koalicja rządząca w osobie Marszałka Sejmu RP Sikorskiego zarządziła audyt, którego efektem było udostępnienie m.in. dokumentu pn. Wyjazdy zagraniczne posłów VII kadencja (kopia jest tutaj).
Jak przystało na kraj, w którym od lat działa Ministerstwo cyfryzacji zestawienie jest w formacie PDF, zatem pierwszym ruchem była zamiana na coś przetwarzalnego. Wpisanie w google PDF+Excel+conversion skutkuje ogromną listą potencjalnych konwerterów. Bagatelizując skalę problemu spróbowałem dokonać konwersji narzędziami dostępnymi on-line, ale z marnym rezultatem (za duży dokument przykładowo; serwis za free jest tylko dla PDFów mniejszych niż 50 stron). W przypadku Convert PDF to EXCEL online & free coś tam skonwertował, nawet wyglądało toto na pierwszy rzut oka OK ale na drugi już nie: dokument niekompletny oraz nieprawidłowo zamienione niektóre liczby (przykładowo zamiast 837,50 zł w arkuszu jest 83750 -- 100 razy więcej!).
Ostatecznie skończyło się na ściągnięciu 30 dniowej wersji Adobe Acrobata Pro XI, który faktycznie sprawdził się w roli konwertera PDF→XLSX. Do konwersji wykorzystałem służbowego laptopa Elki wyposażonego w legalny Office 2010, na którym zainstalowałem ww. AA Pro XI. OOffice niby czyta XLSX, ale z koszmarnymi błędami, więc żeby dalej móc obrabiać arkusz w Linuksie wczytałem wynikowy XLSX do Excela 2010 po czym zapisałem go w (starszym) formacie XLS. Ten plik wyświetlił się w OO Calcu bez problemu.
Arkusz jest tak sformatowany, że 4 pierwsze komórki oraz są często wielowierszowe i scalone, zawierają bowiem liczbę porządkową, datę, miejsce i cel wyjazdu delegacji posłów. Po zamianie na plik CSV zawartość komórek scalonych pojawi się w pierwszym wierszu, a pozostałe będą puste. Prostym skryptem Perlowym mogę wypełnić puste komórki wg. algorytmu: jeżeli cztery pierwsze pola są puste, to skopiuj wartości ostatnich niepustych:
if ($tmp[0] eq '' && $tmp[1] eq '' && $tmp[2] eq '' && $tmp[3] eq '' ) { ... }
Pierwszy problem: wielowierszowe komórki z kolumn 1--4 nie zawsze są scalone. Czasem tekst jest podzielony na wiersze co psuje konwersję. Ręcznie scalam niescalone komórki (trochę to trwa). Przed scaleniem usuwam z kolumn 1--4 końce wiersza.
Drugi problem: część liczb nie jest liczbami z uwagi na użycie separatora tysięcy, który się zamienił w PDFie na odstęp (spację). Zatem zaznaczam kolumny zawierające różne pozycje kosztów po czym:
Po uporządkowaniu arkusza, zapisuję go w formacie CSV. Następnie prostym skryptem Perlowym zamieniam na taki plik CSV, w którym puste komórki są wypełniane zawartością z poprzednich wierszy. Kolumna Państwo - miasto jest kopiowana. Kopia jest zmieniana na jednoznaczne: Państwo, miasto (pierwszy-kraj, przecinek, pierwsze miasto z listy celów podróży -- żeby geokoderowi było łatwiej.)
Innym skryptem Perlowym dodaję do pliku CSV 3 kolumny, które zawierają:
współrzędne celu podróży (w tym celu zamieniam adres Państwo, miasto na współrzędne geograficzne korzystając z geokodera Google);
odległość w kilometrach pomiędzy punktem o współrzędnych
21.028075/52.225208 (W-wa, Wiejska 1) a celem podróży (obliczoną przy wykorzystaniu pakietu
GIS::Distance
);
linię zdefiniowana w formacie KML o końcach 21.028075/52.225208--współrzędne-celu-podróży (do ewentualnego wykorzystania z Google Fusion Tables).
#!/usr/bin/perl # use Storable; use Google::GeoCoder::Smart; use GIS::Distance; $geo = Google::GeoCoder::Smart->new(); my $gis = GIS::Distance->new(); my $GeoCodeCacheName = 'geocode.cache'; my $NewCoordinatesFetched=0; # global flag my $SLEEP_TIME = 2 ; my $coords_okr = "21.028075,52.225208"; # Warszawa = środek świata my %GeoCodeCache = %{ retrieve("$GeoCodeCacheName") } if ( -f "$GeoCodeCacheName" ) ; my ($wwa_lng, $wwa_lat) = split (",", $coords_okr); my $linesNo = 0 ; my $GCtotaluse = 1; # laczna liczba wywolan geocodera while (<>) { $linesNo++; chomp(); $_ =~ s/[ \t]+;[ \t]+/;/g; ## usuń ew. niepotrzebne spacje @line = split ";", $_; print STDERR "**$linesNo = $line[3] ... "; # geokodowanie (uwaga na limit) # Poprawki dla miejsc, których nie zna Google: $line[3] =~ s/Erewań/Erywań/; ## $line[3] =~ s/Sowayma/Madaba/; ## najbliższe miasto $line[3] =~ s/Bołszowce/Iwano-Frankiwsk/; ## najbliższe miasto my $coords = addr2coords( $line[3] ); ($tmp_lat, $tmp_lng, $gcuse) = split " ", $coords; if ($gcuse > 0) {$GCtotaluse++ ; } $distance = $gis->distance($tmp_lat,$tmp_lng => $wwa_lat,$wwa_lng ); $distance_t = sprintf ("%.1f", $distance); my $kml_line = "<LineString><coordinates>$tmp_lng,$tmp_lat $coords_okr</coordinates></LineString>"; print "$_;\"$coords\";$distance_t;\"$kml_line\"\n"; print STDERR "\n"; if ($GCtotaluse % 100 == 0 ) {# store every 100 geocoder calls store(\%GeoCodeCache, "$GeoCodeCacheName"); print STDERR "\n... Cache stored. ***\n"; } } ## store(\%GeoCodeCache, "$GeoCodeCacheName"); ## ## ## #### sub addr2coords { my $a = shift ; my $r = shift || 'n'; my ($lat, $lng) ; my $GCuse = 0; ##consult cache first if (exists $GeoCodeCache{"$a"} ) { print STDERR "Coordinates catched ... $a "; ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { print STDERR "Geocoding ... $a "; my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $GCuse = 1; sleep $SLEEP_TIME; ## make short pause $resultnum--; $resultNo=$resultnum ; if (resultNo > 0) { print STDERR "** Location $a occured more than once! **" } if ($error eq 'OK') { $NewCoordinatesFetched=1; for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache } else { print STDERR "** Location $a not found! due to $error **" } } if ($r eq 'r' ) { return "$lng,$lat,$GCuse"; } # w formacie KML else { return "$lat $lng $GCuse"; } }
Gotowy plik CSV zawierający zestawienie podróży jest dostępny tutaj.
Na podstawie zestawienia i z użyciem pakietu ggplot2 generują się takie oto śliczne wykresy.
Wszystkie podróże z zestawienie (N=1874; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Tylko podróże dla których koszt transportu był niezerowy (N=1423; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Poniższy skrypt R sumuje i drukuje wszystkie podróże każdego posła:
require(plyr) d <- read.csv("W7RR_podroze_by_podroz1.csv", sep = ';', dec = ",", header=T, na.string="NA"); # Dodaj kolumnę której wartości to konkatenacja: "Poseł|Klub" d[,"PosKlub"] <- do.call(paste, c(d[c("Posel", "Klub")], sep = "|")); # Usuń wszystko za wyjątkiem tego co potrzeba: d <- d[ c("PosKlub", "Klacznie", "Ktransp", "Dist") ]; # Sumowanie po PosKlub PSums <- as.data.frame ( ddply(d, .(PosKlub), numcolwise(sum)) ); # Z powrotem rozdziel kolumnę "Poseł|Klub" na dwie PSums <- as.data.frame ( within(PSums, PosKlub <-data.frame( do.call('rbind', strsplit(as.character(PosKlub), '|', fixed=TRUE)))) ) # Drukuj PSums;
Z pliku .Rout
kopiuję zestawienie łącznych wydatków
posłów oraz łącznej pokonanej przez nich odległości:
PosKlub.X1 PosKlub.X2 KlacznieT KtranspT DistT 1 Adam Abramowicz PiS 4.02599 2.64595 1.3153 2 Adam Hofman PiS 119.55271 59.53315 26.1716 3 Adam Kępiński SLD 10.15754 7.93882 3.8069 4 Adam Kępiński TR 12.63098 8.02327 2.2107 ...
Uwaga: kilkanaście nazwisk się powtarza ponieważ posłowie zmienili przynależność klubową w czasie trwania kadencji [Aby uwzględnić takich posłów sumowanie odbywało się po wartościach zmiennej zawierającej połączone napisy Poseł|Klub.]
Na podstawie takiego z kolei zestawienia i znowu z użyciem ggplot2 generują inne śliczne wykresy.
Uwaga: sumowane tylko podróże, dla których koszt transportu był niezerowy (N=1423; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Link do tabeli zawierającej zestawienie podróży w formacie Google Fusion Tables jest tutaj.
Dane + skrypty dostępne są także w: github.com/hrpunio/Data.
The problem: upload photos to Picasa (trivial); 2) scale them if neccessary before upload; 3) if photos contains some EXIF tags (geotags in particular) copy these tags to Picasa as well.
To achieve the above I use: googleCL
(upload),
convert
(from ImageMagick bundle for scaling)
exiftool
(for metadata extraction/manipulation).
Using convert
the script below
(jpgresize.sh
) scale picture to 2048 pixels along longest
side:
#!/bin/bash # Scale pictures to 2048 pixels along longest side for upload to Picasa # Photos below 2048 x 2048 pixels do not count towards storage limit # (cf. https://support.google.com/picasa/answer/6558?hl=en ) # PICASA_FREE_LIMIT=2048 while test $# -gt 0; do case "$1" in -o) shift; OUT_FILE="$1";; -o*) OUT_FILE="`echo :$1 | sed 's/^:-o//'`";; *) FILE="$1";; esac shift done if [ -z "$OUT_FILE" ] ; then my_pic="${FILE%.*}_s.${FILE#*.}" else my_pic="$OUT_FILE"; fi if [ -f "$FILE" ] ; then echo "** converting $FILE to $my_pic ***" SIZE="2048x" my_pic_width=`exiftool -ImageWidth "$FILE" | awk '{print $NF}'` my_pic_height=`exiftool -ImageHeight "$FILE" | awk '{print $NF}'` if [[ ( -z "$my_pic_width" ) || ( -z "$my_pic_height" ) ]] ; then echo "*** $FILE has 0 width and/or height ***"; exit ; fi ## http://www.imagemagick.org/Usage/resize/#resize if [[ ( "$my_pic_width" -gt "$PICASA_FREE_LIMIT" ) || \ ( "$my_pic_height" -gt "$PICASA_FREE_LIMIT" ) ]] ; then if [ "$my_pic_width" -gt "$my_pic_height" ] ; then SIZE="${PICASA_FREE_LIMIT}x>" echo "*** $FILE width: $my_pic_width ; converting to $SIZE" convert "$FILE" -geometry $SIZE "$my_pic" else SIZE="x$PICASA_FREE_LIMIT>" echo "*** $FILE height: $my_pic_height ; converting to $SIZE" convert "$FILE" -geometry $SIZE "$my_pic" fi else ## File is too small copy the original: echo "*** $FILE has $my_pic_width in width; COPYING" cp "$FILE" "$my_pic" fi else echo "*** FILE $FILE not found! ***" fi
Upload one picture to picasa with 1photo2picasa.sh
#!/bin/bash # # Upload photo to Picasa with googleCL # It is assumed the photo contains UserComment GPSLatitude GPSLongitude GPSAltitude # Exif tags which are copied to Picasa (see below for more details) # # Default album title: ALBUMTITLE="???" echo "$0 -a AlbumTitle FILE-2-UPLOAD" while test $# -gt 0; do case "$1" in -a) shift; ALBUMTITLE="$1";; -a*) ALBUMTITLE="`echo :$1 | sed 's/^:-a//'`";; *) FILE="$1";; esac shift done AUTHOR=`exiftool -S -Artist $FILE` if [ -z "$AUTHOR" ] ; then # It there is no Artist tag it is assumed photo was not tagged properly echo "*** ERROR: $FILE lacks Artist EXIF tag" exit; else ## Some tags are edited: TAGS=`exiftool -S -UserComment $FILE | awk '{ $1=""; for (i=1;i<=NF;i++) { if ($i ~ /http/) { $i=""}}; \ gsub (/, +/, ",", $0); gsub (/ +,/, ",", $0); gsub (/^ +| +$/, "", $0); print $0}'` GPSLat=`exiftool -S -c '%+.6f' -GPSLatitude $FILE | awk '{ print $2}'` GPSLon=`exiftool -S -c '%+.6f' -GPSLongitude $FILE | awk '{ print $2}'` GPSAlt=`exiftool -GPSAltitude $FILE -S -c "%.1f" | awk '{ if ($0 ~ /Below/) { print -$2} else {print $2}}'` PICASA_TAGS="" ## Concatenate all tags if [ -n "$TAGS" ] ; then PICASA_TAGS="$TAGS"; fi if [ -n "$GPSLat" ] ; then PICASA_TAGS="$PICASA_TAGS,geo:lat=$GPSLat"; fi if [ -n "$GPSLon" ] ; then PICASA_TAGS="$PICASA_TAGS,geo:lon=$GPSLon"; fi if [ -n "$GPSAlt" ] ; then PICASA_TAGS="$PICASA_TAGS,geo:alt=$GPSAlt"; fi # Upload to picasa: google picasa post --title "$ALBUMTITLE" --src="$FILE" --photo="$FILE" --tags="$PICASA_TAGS" fi
Finally simple bash script upload2picassa.sh
uses
jpgresize.sh
and 1photo2picasa.sh
to upload all .jpg
files from the
current directory to picasa:
#!/bin/bash # Upload all .jpg files (scaled tp 2048) to picasa # echo "Uploading all .jpg files to album: $albumtitle" albumtitle=$1 if [ -z "$albumtitle" ] ; then echo "Podaj ID albumu!"; exit 1 fi for file in *.jpg; do echo "Uploading $file to $albumtitle album..." outfile="${file%.*}_s.${file#*.}" jpgresize.sh -p -o $outfile $file && 1photo2picasa.sh -a $albumtitle $outfile done
Ubocznym skutkiem dodania współrzędnych geograficznych do zdjęć za pomocą skryptu gpsPhoto.pl jest plik w formacie KML. Każde zdjęcie w takim pliku jest oznakowane jak na przykładzie:
<Placemark> <name>s300013_08275298.jpg</name> <description><![CDATA[<a href="/home/tomek/Obrazy/s300013_08275298.jpg"> <img src="/home/tomek/Obrazy/s300013_08275298.jpg" width="200" /></a><br> <a href="/home/tomek/Obrazy/s300013_08275298.jpg">full size</a>]]> </description> <Point> <altitudeMode>clampToGround</altitudeMode> <coordinates>16.496808361,53.466461571,122</coordinates> </Point>
Dawno temu zrobiłem skrypt, który podmienia adres w lokalnym systemie plików (taki jak
/home/tomek/Obrazy/s300013_08275298.jpg
) na tenże plik, ale umieszczony
na moim koncie w serwisie flickr.com
:
<Placemark> <name>s300013_08275298.jpg</name> <description><![CDATA[<a href="http://www.flickr.com/tprzechlewski/9615076609/"> <img src="http://static.flickr.com/7411/9615076609_a243eed0f1_m.jpg" width="200" /></a><br> <a href="http://www.flickr.com/tprzechlewski/9615076609/">full size</a>]]> </description> ... itd ...
Taki plik za pomocą kilku klików importowałem do mapy Google (Mapy→Moje Miejsca → utwórz w klasycznej wersji Moich map) i działało -- do zeszłego piątku, kiedy to plik KML z wycieczki do Nadarzyc uparcie nie chciał się poprawnie wyświetlić (#1 -- obcięty ślad w okolicach Nadarzyc).
Konsultując problem via Google dowiedziałem się, że cyt KML upload and rendering has always
been problematic with Maps i że lepiej (jeżeli już) to nie importować KML tylko
wyświetlić go podając URL do
dokumentu KML jako wartość parametru q
.
Przykład:
https://mapy.google.pl/maps?q=http://pinkaccordions.homelinux.org/Geo/kml/20130828__MP.kml
Działa (rys. #2).
Ponieważ serwer pinkaccordions.homelinux.org jest rachityczny pomyślałem, że dobrze by
było plik KML umieścić w jakimś lepszym miejscu, np. na witrynie sites.google.com
.
Jest nawet dedykowany -- i niestary bo z 2010 roku -- dokument w temacie
pn. Using Google Sites to Host Your KML
Niestety
plik KML pobierany z http://sites.google.com/site/
nie wyświetla się poprawnie. Po kliknięciu
w ikonę niektórych zdjęć otwiera się okno ale jest
ono puste (#3) i/lub nie wskazuje na stosowne miejsce na mapie (por. #4). Kombinowałem jak toto poprawić
ale nie doszedłem do żadnych pozytywnych wniosków: połączenie GMaps + KML + sites.google.com nie działa.
PS: przypomniałem sobie, że mój Internet Provider daje w pakiecie konto na serwerze WWW z limitem 200Mb (z którego do tej pory mało korzystałem)... Sprawdziłem -- działa. Będę zatem tam trzymał pliki KML/KMZ (Rys. #5.)
Uwaga: w tym wpisie przykłady ilustrujące działanie Lightboksa NIE DZIAŁAJĄ, bo
na blogu pinkaccordions.homelinux.org
nie ma zainstalowanego Lightboksa.
(Działają tutaj).
Kliknięcie zdjęcia lub obrazu zamieszczonego na blogu powoduje wyświetlenie go w nakładce na stronie. Jest to tzw. widok Lightbox. (cf. Widok Lightbox w Bloggerze):
<div> <span> <a href="https://lh4.googleusercontent.com/-cKBvbosMsII/Ua4zYPs99kI/AAAAAAAAB90/NLTG1cMbI88/epl313_6040342.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="https://lh4.googleusercontent.com/-cKBvbosMsII/Ua4zYPs99kI/AAAAAAAAB90/NLTG1cMbI88/s128/epl313_6040342.jpg" /></a> </span> <span> <a href="http://lh3.ggpht.com/-3POvL9Uv478/UhXICIpp_3I/AAAAAAAAB-w/K1rRrCU2D9c/epl313_6040338.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="http://lh3.ggpht.com/-3POvL9Uv478/UhXICIpp_3I/AAAAAAAAB-w/K1rRrCU2D9c/s128/epl313_6040338.jpg" /></a> </span> <span> <a href="http://lh6.ggpht.com/-nmAnV2_TD8U/UhXIIoYKraI/AAAAAAAAB-4/mWAJWpsOUZU/epl313_6040339.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="http://lh6.ggpht.com/-nmAnV2_TD8U/UhXIIoYKraI/AAAAAAAAB-4/mWAJWpsOUZU/s128/epl313_6040339.jpg" /></a> </span> <span> <a href="http://lh5.ggpht.com/-wO5_E5_jK14/UhXG7Rd-bSI/AAAAAAAAB-c/x76Ee7I9tgU/epl313_6040332.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="http://lh5.ggpht.com/-wO5_E5_jK14/UhXG7Rd-bSI/AAAAAAAAB-c/x76Ee7I9tgU/s128/epl313_6040332.jpg" /></a> </span> </div>
Wynik jest następujący (kliknij w zdjęcie aby przejść do widoku Lightboksa):
W powyższym przykładzie zawartość atrybutów href
i src
jest prawie identyczna, bo różni się wyłącznie
fragmentem s128/
.
Jest to przykład zastosowania sprytnego URLa, który spowoduje automagiczne przeskalowanie
zdjęcia, tak aby dłuższy wymiar miał maksymalnie 128 pikseli. Jeżeli zamiast s128/
wstawimy
s128-c/
albo s128-p/
, to
spowoduje to automagiczne wycięcie kwadratu o wymiarach maksymalnych 128 pikseli (crop).
Przykład:
<div> <span> <a href="http://lh3.ggpht.com/.../epl313_6040338.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="http://lh3.ggpht.com/.../s128-c/epl313_6040338.jpg" /></a> </span> <span> <a href="http://lh3.ggpht.com/.../epl313_6040338.jpg" imageanchor="1" style="margin-bottom: 1em; margin-right: .1em;"><img border="0" src="http://lh3.ggpht.com/.../s128-p/epl313_6040338.jpg" /></a> </span>
Symbolem ...
oznaczono pominiętą część URLa (żeby nie wystawał na margines).
Wynik (kliknięcie w zdjęcie nie powoduje przejścia do widoku Lightboksa):
BTW: Liczba 128 nie jest magiczna, można przeskalować obrazek na inny wymiar.
Jeżeli zdjęcia są różnych wymiarów (portret/pejzaż),
to s128-c/
(albo s128-p/
) jest lepsze niż zwykłe s128
,
bo minaturki są jednakowej wielkości,
tyle że Lightbox takie zdjęcia ignoruje. Czemu -- nie wiem... Nie wiem też jak to zmienić
Jako obejście problemu można przeskalować miniaturkę do jednakowej wysokości. Przykład:
<div> <span> <a href="https://lh4.googleusercontent.com/.../epl313_6040342.jpg" imageanchor="1" style="margin-bottom: 1em;"><img border="0" height='85' src="https://lh4.googleusercontent.com/.../s128/epl313_6040342.jpg" /></a> </span> <span> <a href="http://lh3.ggpht.com/.../epl313_6040338.jpg" imageanchor="1" style="margin-bottom: 1em;"><img border="0" height='85' src="http://lh3.ggpht.com/.../s128/epl313_6040338.jpg" /></a> </span> <span> <a href="http://lh6.ggpht.com/.../epl313_6040339.jpg" imageanchor="1" style="margin-bottom: 1em;"><img border="0" height='85' src="http://lh6.ggpht.com/.../s128/epl313_6040339.jpg" /></a> </span> <span> <a href="http://lh5.ggpht.com/.../epl313_6040332.jpg" imageanchor="1" style="margin-bottom: 1em;"><img border="0" height='85' src="http://lh5.ggpht.com/.../s128/epl313_6040332.jpg" /></a> </span> </div>
Symbolem ...
oznaczono pominiętą część URLa (żeby nie wystawał na margines).
Wynik jest następujący:
Kliknięcie w zdjęcie powoduje przejście do widoku Lightboksa.
Być może za czas jakiś problem zniknie. Wyświetlanie dla tego wpisu 10 zamiast 8 obrazków w widoku Lightboksa będzie tego dowodem:-)
The idea is to add an event to Google calendar with some short start time from now (say 15 minutes) and SMS reminder feature.
#!/bin/bash # reminder via google with 15 minutes from now start time # MESSAGE="Neptune ALARM: something nasty has happened!" # In case of service failure try 3 times: for i in 1 2 3; do # Compute 15min from now time with date: NOW=`date "+%s"` TIME_SHF1=$(($NOW + 900 )) FWD_TIME1=`date +"%d/%m/%Y %H:%M" -d"@$TIME_SHF1"`; # Check for optional script argument if [ -n "$1" ] ; then MESSAGE="Neptune ALARM: $1" ; fi # use GoogleCL to add to calendar with 15m from now/1m reminder google calendar add "$MESSAGE $FWD_TIME1" --reminder="1m" RC=$? if [ $RC -ne 0 ] ; then sleep 30; else break ; fi done
The script uses GoogleCL (a python-based command-line utility for accessing Google). Installing GoogleCL is easy:
sudo apt-get install googlecl # or (in case there is no ready-to install package): wget http://googlecl.googlecode.com/files/googlecl-0.9.13.tar.gz tar -zxvf googlecl-0.9.13.tar.gz cd googlecl-0.9.13 sudo python setup.py install
Note: googleCL is launched via google
not googleCL
command.
The first time one uses googleCL, ie:
google calendar add "test from neptune 22/03/2013 10:30:22"
it will prompt for one's Google username. One has to type it in and hit enter. Next the user is asked to grant access permissions.
On non-gui systems (text-based) w3m browser usually will be launched.
When connected to google one has to press q y
and the text silimlar to the following will be displayed:
Please log in and/or grant access via your browser at https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=\ 4%xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&hd=default then hit enter.
One has to copy the above link to another machine (with gui browser).
Next one has to grant access and then go back to the terminal
and hit enter
to complete the authorization.
BTW the access token is stored in:
~/.local/share/googlecl/access_tok_USERNAME
If access is granted from some machine it will probably suffice to copy the access token file to another machine to grant access for it as well (not tested).
The configuration file for googleCL is in:
~/.config/googlecl
If access is grated successfully the message similar to the following will be displayed:
Event created: http://www.google.com/calendar/event?eid=anFsdGFucXFwdmZnbW1sMmcwc2t1NDI3cm8gbG9vc2VoZWFkcHJvcDFAbQ
The nuisance of GoogleMaps is that it does not know GPX and only KML/GeoRSS files can be uploaded. Fortunately it is easy to convert GPX to KML with gpsbabel:
gpsbabel -i gpx -f file.gpx -x simplify,count=333 -o kml -F file.kml
If there are several files they have to be convert one-by-one and then upload to Google. It would be more comfortable to merge them first and upload it in one go rather than uploading each individually.
As I could not find how to merge serveral GPX files into one using gpsbabel (merge option in gpsbabel puts all track points from all tracks into a single track and sorts them by time stamp. Points with identical time stamps will be dropped) I worked out the following simple Perl script:
#!/usr/bin/perl # # Combine GPX files into one # usage: gpxmerge file1 file2 file3 .... # use XML::DOM; binmode(STDOUT, ":utf8"); my $parser = new XML::DOM::Parser; for my $file2parse (@ARGV) { my $doc = $parser->parsefile ($file2parse); for my $w ( $doc->getElementsByTagName ("wpt") ) { $waypoints .= $w->toString() . "\n"; } for my $r ( $doc->getElementsByTagName ("rte") ) { $routes .= $r->toString() . "\n"; } for my $t ( $doc->getElementsByTagName ("trk") ) { $tracks .= $t->toString() . "\n"; } } print "<?xml version='1.0' encoding='UTF-8' ?> <gpx version='1.1' creator='GPXmerger' xmlns='http://www.topografix.com/GPX/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'>\n <author>tomasz przechlewski </author> <email>tprzechlewski[at]acm.org</email> <url>http://pinkaccordions.homelinux.org/Geo/gpx/</url>"; print "$waypoints\n$routes\n$tracks\n"; print "</gpx>\n";
The resulting file structure is as follows: first all waypoints, then all routes and finally all tracks. This is perfectly legal GPX file.
Now I convert GPX to KML using gpsbabel:
gpsbabel -i gpx -f file.gpx -x simplify,count=333 -o kml -F file.kml
Since gpsbabel generates pretty verbose KML files I simplify them using XSLT stylesheet (perhaps this step is superfluous):
xsltproc -o simplified.kml kml2kml.xsl file.kml
and the stylesheet looks like:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:kml="http://www.opengis.net/kml/2.2" > <xsl:output method="xml" indent='yes' /> <xsl:template match="/"> <!-- ;; http://www.jonmiles.co.uk/2007/07/using-xpaths-text-function/ ;; --> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"> <Document> <Folder> <name>Waypoints</name> <xsl:for-each select="//kml:Folder[kml:name/text()='Waypoints']/kml:Placemark//kml:Point"> <Placemark> <!-- <name><xsl:value-of select='../kml:name'/></name> --><!-- niepotrzebne --> <Point> <coordinates> <xsl:value-of select="kml:coordinates"/> </coordinates> </Point> </Placemark> </xsl:for-each> </Folder> <Folder> <name>Tracks</name> <xsl:for-each select="//kml:Placemark//kml:LineString"> <Placemark> <name>Path</name> <LineString> <tessellate>1</tessellate> <coordinates> <xsl:value-of select="kml:coordinates"/> </coordinates> </LineString> </Placemark> </xsl:for-each> </Folder> </Document> </kml> </xsl:template> </xsl:stylesheet>
Now KML file is ready to be imported to Google maps via Maps → My Places → Create map.
The result of example conversion can be be found here.
Google fusion tables another excercise.
Two data sets describe football players who plays in Polish t-Mobile ekstraklasa (1st division) and Pierwsza Liga (2nd division) in 2011/2012 (autumn).
To show from where the player came a straight line is drawn from a player's birthplace to club's stadium, the player plays for.
Figure 1. 1st division.
Figure 2. 2nd division.
Players from 2nd division seems to be born closer to the clubs they play for:-)
Warning: in considerable number of cases the geocoding as performed by Google maybe wrong due to poor data quality--have no time to check/correct.
Yet another excercise, which tests Google Fusion Tables.
The data set contains--among other things--birth place for 600+ rugby players who last year take part in Rugby World Cup in New Zealand. The raw data is available here and here.
On the following diagram (cf. Figure 1) the players are mapped by column containing birthplace coordinates
Figure 1. World's concentration of top Rugby Union players.
The map do not show which player plays for which country. To show that a straight line is drawn from each player's birthplace to the country's capital, the player plays for (cf. Figure 2).
Figure 2. The origin of top Rugby Union players by federation.
Some lines look strange and the problem is particularly evident around New-Zealand-Samoa-Tonga-Fiji. For example it seems that many Samoan players were born at high ocean (cf. Figure 3).
Figure 3. The origin of top Rugby Union Samoan players.
BTW mapping Samoans one-by-one is OK (try it). The problem is when all rows are mapped together by Visualize→Map function.
Accidentaly around 600 miles to the East of New Zealand lies antipodal meridian ie. a meridian which is diametrically opposite the Greenwich meridian (a pime one). The longitude of points lying on antipodal meridian can be referenced (at least in GoogleMaps) both as -180° or 180° (ie. -36,-180° and -36,+180° refers to the same place). Perhaps it is the cause of observed errors...
Wprawdzie Sejm RP szybkimi krokami zmierza w stronę wybranego w 1936 r. Reichstagu określanego jako ,,najlepiej opłacany męski chór ma świecie'' (The best paid male chorus in the world -- określenie użyte podobno w amerykańskim czasopiśmie The Literary Digest) i być może już w kadencji ósmej ten stan ideału zostanie osiągnięty. Ale zanim to nastąpi zbierzmy dane dotyczące posłów wybranych do Sejmu 7 kadencji.
Zaczynam od ściągnięcia listy stron bibliograficznych posłów:
## Najpierw zestawienie posłów: wget -U XX http://www.sejm.gov.pl/sejm7.nsf/poslowie.xsp?type=A -O lista-poslow.html ## Potem dla każdego posła z zestawiena: cat lista-poslow.html | perl -e ' undef $/; $_ = <>; s/\n/ /g; while ($_ =~ m@href="/sejm7.nsf/posel.xsp\?id=([0-9]+)\&type=A@g ) { $id = $1; ## id posła $pos = "http://www.sejm.gov.pl//sejm7.nsf/posel.xsp?id=$id&type=A"; print STDERR $pos, "\n"; system ('wget', '-U', 'XXX', "-O", "7_$id.html", $pos); sleep 3; ## --let's be a little polite-- }
Powyższe pobiera 460 stron, każda zawierająca biografię jakiegoś posła.
Z kolei poniższy skrypt wydłubuje co trzeba (data scraping) ze strony zawierających biografię pojedynczego posła (np. tego).
#!/usr/bin/perl # use Storable; use Google::GeoCoder::Smart; $geo = Google::GeoCoder::Smart->new(); # Domyślny kraj my $GeoCodeCacheName = 'geocode.cache'; my $NewCoordinatesFetched=0; # global flag my $kraj = 'Polska'; my $SLEEP_TIME = 3 ; # Rok wyborów my $baseyr = 2011; my ($data_day, $data_mc, $data_yr); undef $/; $_ = <>; s/\n/ /g; # Retrieve geocode cash (if exists) # http://curiousprogrammer.wordpress.com/2011/05/11/faking-image-based-programming-in-perl/ my %hash = %{ retrieve("$GeoCodeCacheName") } if ( -f "$GeoCodeCacheName" ) ; if (m@<h2>([^<>]+)</h2>@) { $posel = recode($1); } # zgadnij płeć patrząc na imię (jeżeli kończy się na a->kobieta): my @tmp_posel = split " ", $posel; if ( $tmp_posel[0] =~ m/a$/ ) { $sex='K' } else { $sex='M' } # id posła: if (m@http://www.sejm.gov.pl/sejm7.nsf/posel.xsp\?id=([0-9]+)@) {$id = $1; } # liczba oddanych na posła głosów: if (m@<p class="left">Liczba g\łos\ów:</p>[ \n\t]*<p class="right">([^<>]+)</p>@) { $glosy = $1 ; } # Data i miejsce urodzenia:</p><p class="right">29-06-1960<span>, </span>Gdańsk</p> if (m@Data i miejsce urodzenia:[ \n\t]*</p>[ \n\t]*<p class="right">([0-9\-]+)<span>,[ \n\t]*\ [ \n\t]*</span>([^<>]+)</p>@ ) { $data = recode ($1); ($data_day, $data_mc, $data_yr) = split "-", $data; ##print STDERR "R/M/D: $data_day, $data_mc, $data_yr\n"; $mce = recode ($2); } # Zawód: if (m@<p class="left">Zaw\ód:</p>[ \nt]*<p class="right">([^<>]+)</p>@ ) { $zawod = recode($1) ; } if (m@klub.xsp\?klub=([^"<>]+)@ ) { $klub = recode($1); } # Klub poselski: if (m@Okr\ęg wyborczy:</p>[ \t\n]*<p class="right">([^<>]+)</p>@) { $okr = recode($1); ## Pierwszy wyraz to numer okręgu, reszta nazwa (może być wielowyrazowa) ($okrnr, $okrnz) = $okr =~ m/([^ \n\t]+)[ \n\t]+(.+)/; ## -- sprawdzić czy działa -- } # Poprawienie błędów if ($mce =~ /Ostrowiec Świetokrzyski/) {$mce = 'Ostrowiec Świętokrzyski' } elsif ($mce =~ /Stargard Szaczeciński/) {$mce = 'Stargard Szczeciński' ; } elsif ($mce =~ /Białograd/ ) {$mce = 'Białogard'; } elsif ($mce=~ /Szwajcaria/) {$mce = 'Szwajcaria,Suwałki' } elsif ($mce=~ /Kocierz Rydzwałdzki/) { $mce= "Kocierz Rychwałdzki" } elsif ($mce=~ /Stąporów/) { $mce= "Stąporków" } elsif ($mce=~ /Szmotuły/) { $mce= "Szamotuły" } # Wyjątki: if ($posel=~ /Arkady Fiedler/ ) {$kraj = 'Wielka Brytania' } elsif ($posel=~ /Vincent-Rostowski/) {$kraj = 'Wielka Brytania' } elsif ($mce=~ /Umuahia/) {$kraj = 'Nigeria' } elsif ($posel=~ /Munyama/) {$kraj ='Zambia'; } ## Imię kończy się na `A' ale to facet: elsif ($posel=~ /Kosma Złotowski/ ) {$sex = "M"} # Sprawdź czy data jest zawsze w formacie dd-mm-yyyy: if ($data_yr < 1900) { print STDERR "*** $data_yr: $data for $posel ($id)\n"; } # geokodowanie (uwaga na limit) my $coords = addr2coords($mce); my $wiek = $baseyr - $data_yr; my $coords_okr = addr2coords($okrnz, 'r'); ($tmp_lat, $tmp_lng) = split " ", $coords; my $kml_line = "<LineString><coordinates>$tmp_lng,$tmp_lat $coords_okr</coordinates></LineString>"; print STDERR "$id : $sex : $posel : $wiek\n"; print "$id;$sex;$posel;$data;$wiek;$mce,$kraj;$coords;$klub;$glosy;$zawod;$okrnr;$okrnz;$kml_line\n"; ## Retrieve geocode cash if ($NewCoordinatesFetched) { store(\%hash, "$GeoCodeCacheName"); } ## ## ## ## ## ## sub recode { my $s = shift; $s =~ s/\ / /g; $s =~ s/\ł/ł/g; $s =~ s/\ó/ó/g; $s =~ s/\ń/ń/g; $s =~ s/\Ł/Ł/g; $s =~ s/\ź/ź/g; $s =~ s/\ż/ż/g; $s =~ s/\Ś/Ś/g; $s =~ s/\Ż/Ż/g; $s =~ s/\ę/ę/g; $s =~ s/\ą/ą/g; $s =~ s/\ś/ś/g; $s =~ s/\ć/ć/g; $s =~ s/[ \t\n]+/ /g; return $s; } ## ## ## ## ## ## sub addr2coords { my $a = shift ; my $r = shift || 'n'; my ($lat, $lng) ; ##consult cache first if (exists $GeoCodeCache{"$a"} ) { ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $resultnum--; $resultNo=$resultnum ; if (resultNo > 0) { print STDERR "** Location $a occured more than once! **" } if ($error eq 'OK') { $NewCoordinatesFetched=1; for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } } else { print STDERR "** Location $a not found! due to $error **" } } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache sleep $SLEEP_TIME; if ($r eq 'r' ) { return "$lng,$lat"; } # w formacie KML else { return "$lat $lng"; } }
Skrypt wydłubuje id posła (id), imię i nazwisko (imnz), datę urodzenia (data_ur),
miejsce urodzenia (mce_ur), skrót nazwy partii do której należy (partia),
liczbę oddanych na niego głosów (log), zawód (zawod),
numer okręgu wyborczego (nrokregu) i nazwę okręgu (nzokregu). Do tego
zgadywana jest płeć posłanki/posła oraz obliczany jest wiek w chwili wyboru jako $baseyr - $data_yr
, gdzie
$data_yr
to rok urodzenia.
Miejsce urodzenia oraz nazwa okręgu są geokodowane. Współrzędne miejsca urodzenia są zapisywane jako zmienna wsp. Jako zmienna odleglosc zapisywana jest linia definiowana (w formacie KML) jako: współrzędne miejsce urodzenia--współrzędne okręgu wyborczego.
<LineString><coordinates>lng1,lat1 lng2,lat2</coordinates></LineString>
Zamiana kupy śmiecia (tj. wszystkich 460 plików HTML) na plik CSV zawierający wyżej opisane dane sprowadza się teraz do wykonania:
for i in 7_*html ; do perl extract.pl $i >> lista_poslow_7_kadencji.csv ; done
Jako ciekawostkę można zauważyć, że strony sejmowe zawierają 6 oczywistych błędów: Ostrowiec Świetokrzyski, Stargard Szaczeciński, Białograd, Kocierz Rydzwałdzki, Stąporów oraz Szmotuły. Ponadto wieś Szwajcaria k. Suwałk wymagała doprecyzowania. Czterech posłów urodziło się za granicą a pan Kosma jest mężczyzną i prosta reguła: jeżeli pierwsze imię kończy się na ,,a'' to poseł jest kobietą zawiodła.
Ostatecznie wynik konwersji wygląda jakoś tak (cały plik jest tutaj):
id;plec;imnz;dat_ur;wiek;mce_ur;wsp;partia;log;zawod;nrokregu;nzokregu;odleglosc 001;M;Adam Abramowicz;10-03-1961;50;Biała Podlaska,Polska;52.0324265 23.1164689;PiS;\ 12708;przedsiębiorca;7;Chełm;<LineString><coordinates>23.1164689,52.0324265 23.4711986,51.1431232</coordinates></LineString> ...
Teraz importuję plik jako arkusz kalkulacyjny do GoogleDocs, a następnie, na podstawie tego arkusza tworzę dokument typu FusionTable. Po wizualizacji na mapie widać parę problemów -- ktoś się urodził niespodziewanie w USA a ktoś inny za Moskwą. Na szczęście takich omyłek jest tylko kilka...
Geocoder nie poradził sobie z miejscowościami: Model/prem. Pawlak, Jarosław/posłowie: Kulesza/Golba/Kasprzak, Orla/Eugeniusz Czykwin, Koło/Roman Kotliński, Lipnica/J. Borkowski, Tarnów/A. Grad. We wszystkich przypadkach odnajdywane były miejsca poza Polską -- przykładowo Orly/k Paryża zamiast Orli. Ponadto pani poseł Józefa Hrynkiewicz została prawidłowo odnaleziona w Daniuszewie ale się okazało, że ta miejscowość leży na Białorusi a nie w Polsce. Też ręcznie poprawiłem...
Ewidentnie moduł Google::GeoCoder::Smart
ustawia
geocoder w trybie partial match, w którym to
trybie Google
intelligently handles incomplete input aka stara się być mądrzejszym od pytającego.
Może w trybie full match byłoby lepiej, ale to by wymagało doczytania.
Na dziś, po prostu poprawiłem ręcznie te kilka ewidentnie błędnych przypadków.
Po poprawkach wszystko -- przynajmniej na pierwszy rzut -- oka wygląda OK
Link do tabeli jest zaś tutaj.
Nawiasem mówiąc kodowanie
dokumentów na stronach www.sejm.gov.pl
jest co najmniej dziwaczne.
Geokodowanie to zamiana adresu lub nazwy miejsca na parę współrzędnych.
Perlowy moduł Google::GeoCoder::Smart
wykorzystany
w poniższym skrypcie używa geolokalizatora Google'a:
#!/usr/bin/perl use Google::GeoCoder::Smart; $geo = Google::GeoCoder::Smart->new(); $location = $ARGV[0]; my $coords = addr2coords( $location ); ## ## ## ## ## ## sub addr2coords { my $a = shift ; ## address for example "Sopot,Polska" my $r = shift || 'n'; ## flag--order of coordinates lat/lng or lng/lat my ($lat, $lng) ; ## ## consult cache first ; $GeoCodeCache is a global hash ## ## if (exists $GeoCodeCache{"$a"} ) { ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $resultnum--; if ($resultnum > 0) { print STDERR "** Location $a occured more than once! **" } if ( $error eq 'OK' ) { for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } } else { print STDERR "** Location $a not found! due to $error **" } } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache ##sleep $SLEEP_TIME; if ($r eq 'r' ) { return "$lng,$lat"; } # KML order lng/lat else { return "$lat $lng"; ## GPX order lat/lng } }
Jest limit 2500 żądań/dzień (24 godziny, przy czym nie jest dokładnie opisane kiedy następuje `reset' licznika, tj. rozpoczyna się następna doba). Jeżeli się przekroczy limit to:
perl ./coordinates.pl Wrocław ** Location Wrocław not found! due to OVER_QUERY_LIMIT ****
Ponieważ w bibliotekach Perla jest wszystko są także moduły Geo::Coder::Many i Geo::Coder::Multiple, który potrafią korzystać z wielu Geokoderów na raz (Google, Yahoo, Bing), zwiększając w ten sposób dzienny limit. Nie używałem...
Dopisane 29 stycznia 2012: W sieci via Google można znaleźć informacje, że reset ma miejsce ,,at midnight 12:00 PST.'' Ale w tym przypadku coś nie bardzo się zgadza, bo exact midnight PST byłoby o 9:00 rano (8:00 GMT), a blokę na mój IP zdjęli około 16.00. (A kwota wyczerpała się o jakieś 18--19 dnia poprzedniego--dokładnie nie pamiętam.)
Klasyczny zbiór danych pn. niemieckie łodzie podwodne z 2WWŚ wg. typu, liczby patroli, zatopionych statków i okrętów oraz -- dodane dziś -- miejsca zatopienia (źródło: www.uboat.net). W zbiorze są 1152 okręty (pomięto 14, które były eksploatowane przez Kriegsmarine ale nie były konstrukcjami niemieckimi), z tego 778 ma określoną pozycję, na której zostały zatopione (z czego kilkadziesiąt w ramach operacji DeadLight). Można zatem powiedzieć, że dane dotyczące miejsca zatopienia/zniszczenia są w miarę kompletne, bo np. Kemp (1997) podaje liczbę 784 U-bootów zniszczonych w czasie 2WWŚ (dane są różne w zależności od źródła.)
Jeżeli U-boot zaginął/przepadł bez wieści/został zniszczony, ale nie wiadomo dokładnie gdzie, to nie występuje na mapie. Operacja DeadLight to ta wielka kupa kropek ,,nad'' Irlandią....
Link do danych/mapy na serwerze Google jest tutaj.
Dane były tylko zgrubie weryfikowane, więc mogą zawierać błędy (ale ,,na oko'' jest ich niewiele)...
Kemp Paul, U-Boats Destroyed: German Submarine Losses in World Wars, US Naval Institute Press, 1997 (isbn: 1557508593)
Pod tym adresem umieściłem moje ślady GPS (głównie rowerowe) z lat 2010--2012. W formacie KML bo to jedyny -- z tego co mi się wydaje -- obsługiwany przez Google Fusion Tables (GFT).
Moja procedura skopiowania danych z urządzenia GPS (zwykle jest to Garmin Legend) do GFT jest następująca:
Skryptem obsługującym program
gpsbabel
pobieram dane z urządzenia GPS do pliku w formacie GPX.
Skryptem kml2kml.sh
zamieniam plik GPX otrzymany w pierwszym kroku
na plik w formacie KML. Ponieważ plik ten jest ogromy (przykładowo z 60 kilowego pliku GPX
gpsbabel wygenerował plik o wielkości ponad 500 kb) zostaje on za pomocą
trywialnego arkusza XSLT zamieniony na znacznie mniejszą wersję uproszczoną:
#!/bin/bash # KMLSTYLE=~/share/xml/kml2kml.xsl # # Domyslna liczba punktow na sladzie COUNT=99 ## Domyslna nazwa Placemarka NAME=`date +%Y%m%d_%H`; while test $# -gt 0; do case "$1" in -name) shift; NAME="$1";; -name*) NAME="`echo :$1 | sed 's/^:-name//'`";; -max) shift; COUNT="$1";; -max*) COUNT="`echo :$1 | sed 's/^:-max//'`";; *) FILE="$1";; esac shift done ## Usun rozszerzenie z nazwy pliku wejsciowego OUT_FILE="${FILE%.*}" TMP_FILE=/tmp/${OUT_FILE}.kml.tmp echo "Converting $FILE to ${TMP_FILE}..." if [ -f $FILE ] ; then ## Konwersja GPX->KML gpsbabel -i gpx -f $FILE -x simplify,count=$COUNT -o kml -F $TMP_FILE else echo "*** ERROR *** File $FILE not found.... ***" ; exit fi ## Konwersja do uproszczonego KMLa (KML->KML) echo "Converting $TMP_FILE to ${OUT_FILE}.kml with maximum $COUNT points..." echo "KML' Placemark name is: $NAME..." xsltproc --stringparam FileName "$NAME" -o ${OUT_FILE}.kml $KMLSTYLE $TMP_FILE echo "Done..."
Zawartość arkusza XSLT zamieszczono na końcu tej notki.
Do interakcji z Fusion Tables wykorzystam zmodyfikowany skrypt (nazwany ftquery.sh
)
znaleziony na stronach
code.google.com:
#!/bin/bash # # Copyright 2011 Google Inc. All Rights Reserved. # Author: arl@google.com (Anno Langen) # http://code.google.com/intl/pl/apis/fusiontables/docs/samples/curl.html # -- Modified by TP -- MY_QUERY=$* function ClientLogin() { password='?1234567890?' email='looseheadprop1@gmail.com' local service=$1 curl -s -d Email=$email -d Passwd=$password -d \ service=$service https://www.google.com/accounts/ClientLogin | tr ' ' \n | grep Auth= | sed -e 's/Auth=//' } function FusionTableQuery() { local sql=$1 curl -L -s -H "Authorization: GoogleLogin auth=$(ClientLogin fusiontables)" \ --data-urlencode sql="$sql" https://www.google.com/fusiontables/api/query } FusionTableQuery "$MY_QUERY"
Teraz tworzę nową tabelę (wyklikowując co trzeba w przeglądarce) MyTracks
o następującej strukturze:
## Korzystamy z skryptu ftquery.sh ./ftquery.sh SHOW TABLES table id,name 2590817,MyTracks ## Tabela MyTracks ma zatem id=2590817 ./ftquery.sh DESCRIBE 2590817 column id,name,type col4,Date,string col0,Start,string col1,Stop,string col2,Location,location col3,Description,string
Kolumny zawierają odpowiednio: datę (Date
), czas
pierwszego
wpisu na śladzie GPX (Start
), czas ostatniego wpisu na
śladzie GPX (Stop
), ślad (Location
) oraz opis (Description
).
Kolumny Tabeli w Google Fusin Tables mogą być albo napisami (STRING
),
liczbami (NUMBER
), zawierać dane przestrzenne (LOCATION
) albo czas (DATETIME
).
Wstępne eksperymenty
z typem DATETIME
wskazują,
że jest z nim jakiś problem.
Primo
format czasu jest dość dziwaczny (np. gpsbabelowy zapis: 2012-01-07T09:19:47Z
nie jest rozpoznawany).
Także późniejsze filtrowanie w oparciu
o kolumnę DATETIME
też jakoś nie wychodzi, nawet jak zapisałem
datę w formacie YYY.MM.DD
,
który wg. dokumentacji
powinien być rozpoznawany.
Nie badając sprawy dogłębnie, zmieniłem po prostu
typ kolumn Date
, Start
, Stop
na STRING
.
Cytując za dokumentacją GFT: In a column of type LOCATION, the value can be a string containing an address, city name, country name, or latitude/longitude pair. The string can also use KML code to specify a point, line, or polygon... (cf. SQL API: Reference ). Zatem żeby wstawić linię śladu wystarczy przesłać napis zawierający współrzędne opakowane w następujący sposób:
<lineString><coordinates>lng,lat[,alt] lng,lat[,alt]...<coordinates></lineString>
Do tego służy następujący skrypt:
#!/bin/bash # STYLE=~/share/xml/gpxtimestamp.xsl MYTRACKS_TABLE_ID="2590817" GPX_FILE=$1 KML_FILE="${GPX_FILE%.*}".kml ## Ustal czas od--do sladu STARTTIME=`xsltproc --param Position '"First"' $STYLE $GPX_FILE` STOPTIME=`xsltproc --param Position '"Last"' $STYLE $GPX_FILE` DATE=`xsltproc --param Position '"First"' --param Mode '"Date"' $STYLE $GPX_FILE` if [ ! -f $GPX_FILE ] ; then echo "*** Error *** No GPX file: $GPX_FILE" ; exit ; fi if [ ! -f $KML_FILE ] ; then echo "*** Error *** No KML file: $KML_FILE" ; exit ; fi ## ## Wycinamy nastepujacym skryptem z uproszczonego pliku KML: TRACK=`perl -e 'undef $/; $t = <>; $t =~ m/(<coordinates>.*<\/coordinates>)/s; \ $t = $1; $t =~ s/[ \t\n]+/ /gm; print $t;' $KML_FILE` TRACK="<LineString>$TRACK</LineString>" ftquery.sh "INSERT INTO $MYTRACKS_TABLE_ID (Date, Start, Stop, Location, Description) VALUES ('$DATE', '$STARTTIME', '$STOPTIME', '$TRACK', '') "
I działa, tj. przesyła co trzeba na moje konto Google.
Google się chwali, że GFT radzi sobie z ogromnymi zbiorami danych. Mój ma -- na tem chwilem -- ponad 150 wierszy, a w każdym 99 punktów na śladzie, co daje łącznie approx 15,000 punktów do wykreślenia. Firefox/Opera coś tam wyświetla ale niewiele widać. Chrome radzi sobie najlepiej -- faktycznie da się to obejrzeć, przesuwać, powiększać... Wprawdzie nic z tych danych nie wynika, ale to już nie jest pytanie do Google.
Także filtr w połączeniu z mapą działa w Firefoksie i Operze tak sobie.
Wybieram zbiór tras,
np. za pomocą warunku Date < 20100901
(na tem chwilem jest to 7 wierszy)
następnie klikam Visualise Map i guzik -- nic nie widać.
Ale znowu w Chrome jest znacznie lepiej.
Dowód w postaci zrzutu ekranu obok.
Także wklejony poniżej link do mapy (pobrany
przez kliknięcie w guzik Get embeddable link) daje pozytywny wynik:
Przynajmniej w mojej wersji FF (8.0).
Zamiana pliku KML na uproszczony plik KML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:kml="http://www.opengis.net/kml/2.2" > <xsl:output method="xml"/> <xsl:param name='FileName' select="'??????'"/> <xsl:template match="/"> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"> <Document> <Placemark> <name><xsl:value-of select="$FileName"/></name> <MultiGeometry><LineString> <tessellate>1</tessellate> <coordinates> <xsl:for-each select="//kml:Placemark//kml:LineString"> <xsl:value-of select="kml:coordinates"/> </xsl:for-each> </coordinates></LineString></MultiGeometry></Placemark> </Document></kml> </xsl:template> </xsl:stylesheet>
Wydrukowanie daty/czasu z pliku w formacie GPX:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gpx="http://www.topografix.com/GPX/1/0"> <xsl:output method="text"/> <xsl:param name='Position' select="'First'"/> <xsl:param name='Mode' select="'Time'"/> <xsl:template match="/" > <xsl:for-each select='//*[local-name()="trkpt"]'> <xsl:choose> <xsl:when test="position()=1 and $Position='First' and $Mode='Time'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=1 and $Position='First' and $Mode='Date'"> <xsl:value-of select='translate(substring(*[local-name()="time"], 1, 10), "-", "")'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=last() and $Position='Last'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> </xsl:choose> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Cannot start Google Chrome:
$ google-chrome /opt/google/chrome/chrome: error while loading shared libraries: cannot restore segment prot after reloc: Permission denied
SE Linux is supposed to cause the problem. The following solution is suggested here and works:
# ## execute as root the following two magic commands: semanage fcontext -a -s system_u -t usr_t /opt/google/chrome/chrome-sandbox restorecon -v /opt/google/chrome/chrome-sandbox