Perl-Hacker erforschen die Bergwelt mit einem Navigationssystem, das anschließend eine grafische Auswertung der erbrachten Wanderleistung ermöglicht.
Mit einem Navigationssystem zu wandern, macht gleich viel mehr Spaß. Ein tragbarer GPS-Empfänger teilt dem Wanderer nicht nur ständig mit, in welche Himmelsrichtung die Wanderung geht, sondern auch die aktuelle Höhe über dem Meeresspiegel und die Position, absolut in Längen- und Breitengrad und relativ zu vorher einprogrammierten Objekten. Auch die aktuelle/maximale/durchschnittliche Wandergeschwindigkeit, die bis dato zurückgelegte Wegstrecke und die geschätzte Zeitdauer bis zum erreichen eines Zielobjekts sind laufend verfügbar. Und, nach abgeschlossener Wanderung lässt sich das Gerät an den heimischen PC anschließen, um die auf der Strecke gesammelten GPS-Daten herunterzuladen und auszuwerten.
Abbildung 1: Das GPS-System "eTrex" von Garmin |
Abbildung 2: Ein Spezialkabel verbindet den eTrex mit der seriellen Schnittstelle des Rechners |
Der GPS-Empfänger ``eTrex'' von Garmin ist nur ein Einsteigermodell, eignet sich aber hervorragend für den sporadischen Wanderer. Er kostet etwa $90, ist handlich, wasserfest und so robust, dass er auch leichte Stürze überlebt. Um den Empfänger nach abgeschlossener Wanderung an den heimischen PC anzuschließen, benötigt man ein Spezialkabel, das den eTrex mit der seriellen Schnittstelle des Rechners verbindet. Es ist überteuert (ungefährt $20) und deswegen gibt es unter [3] ein Projekt, das dabei hilft, es selbst herzustellen. Ich war allerdings dieses Mal faul und habe das Teil einfach gekauft.
GPS-Empfänger arbeiten intern zum Teil mit unterschiedlichen Datenformaten,
aber diese Sprachverwirrung biblischen Ausmaßes bekämpft das
Freeware-Programm gpsbabel
[4] wirkungsvoll. Es beherrscht nicht nur
dutzende verschiedener GPS-Datenformate, sondern kann unter anderem
auch unter Linux die Daten von einem am seriellen Port hängenden Garmin
eTrex auslesen. Im Speicher des GPS-Empfänger liegen gesammelte
Waypoints (auf Knopfdruck festgehaltene Ortspunkte), Routes
(Routen, die mehrere Waypoints verbinden) und Tracks (Spuren), automatisch
alle paar Sekunden generierten Ortspunkten. Wer vor dem Start seinen
Trackspeicher löscht und nach Beendigung auf den Rechner herunterlädt,
hat eine digitale Repräsentation der Wanderung vorliegen. Die alle
paar Sekunden aufgezeichneten Messwerte lassen sich auf
kreative Weise auswerten. Hängt der Garmin
dazu zum Beispiel
an der zweiten seriellen Schnittstelle des Rechners, lädt das Kommando
gpsbabel -t -i garmin -f /dev/ttyS1 -o gpx -F tracks.txt
die Trackdaten (-t) im Garmin-Format (-i garmin) von der zweiten seriellen Schnittstellt (/dev/ttyS1, die erste wäre /dev/ttyS0) und speichert sie im GPX-Format (-o gpx) in der Datei tracks.txt (-F tracks.txt) ab.
Damit der Prozess, der den Garmin ansteuert, nicht als Root laufen muss, wird der Device-Eintrag für die serielle Schnittstelle vor dem Auslesen (das Schreibrechte erfordert) einfach für alle beschreibbar gemacht:
# chmod a+rw /dev/ttyS1 # ls -l /dev/ttyS1 crw-rw-rw- 1 root uucp 4, 65 Feb 10 22:47 /dev/ttyS1
Nach einer Weile (Geduld, serielle Schnittstellen sind aus dem letzten
Jahrhundert),
kehrt das gpsbabel
-Kommando zurück, und in der Datei tracks.txt
liegen
die Trackdaten im in Abbildung 3 gezeigten XML-Format. Damit die nachfolgenden
Auswertungen nicht alle diese XML-Daten parsen müssen, transformiert
das Skript in Listing tracks2yml
sie in das augenfreundliche YAML-Format,
das sich ausserdem in einem Rutsch wieder in eine Perl-Datenstruktur
verwandeln lässt. Abbildung 4 zeigt die YAML-Daten. Lesbarer, oder?
Abbildung 3: Die vom Garmin heruntergeladenen Track-Daten im GPX-Format |
Abbildung 4: Die gleichen Track-Daten im YML-Format nach der Umwandlung durch track2yml |
01 #!/usr/bin/perl -w 02 use strict; 03 use Sysadm::Install qw(:all); 04 05 use XML::Twig; 06 use Date::Parse; 07 use YAML qw(DumpFile); 08 09 my $twig= XML::Twig->new( 10 TwigHandlers => { 11 "trkpt" => \&handler, 12 } 13 ); 14 15 my @points = (); 16 $twig->parsefile("tracks.xml"); 17 DumpFile("tracks.yml", \@points); 18 19 ########################################### 20 sub handler { 21 ########################################### 22 my($t, $trkpt)= @_; 23 24 my $lat = $trkpt->att('lat'); 25 my $lon = $trkpt->att('lon'); 26 my $ele = $trkpt->first_child( 27 'ele')->text(); 28 29 my $isotime = $trkpt->first_child( 30 'time')->text(); 31 my $time = str2time($isotime); 32 33 push @points, { 34 lat => $lat, lon => $lon, 35 ele => $ele, time => $time, 36 isotime => $isotime, 37 }; 38 }
track2yml
nutzt das Modul XML::Twig
vom CPAN, das wie in
[5] schon einmal gezeigt, einen Handler definiert, den der Twig-Tänzer
bei jedem <trkpt>
-Tag anspringt. Das dem Handler übergebene
Objekt vom Typ XML::Twig::Elt
repräsentiert das gefundene
<trkpt>
-Tag mit allen in ihm enthaltenen Unter-Tags.
Das Attribut lat
(Latitude) gibt die geographische Breite des Trackpunkts
in Dezimalschreibweise an. Nördliche Breitengrade sind positiv,
südliche negativ. Das Attribut lon
(Longitude) hingegen bestimmt
die geographische Länge, wobei westliche Längengrade negativ, östliche
hingegen positiv sind. Das Unterelement ele
(Elevation) legt die
Höhe des Trackpunkts über dem Meeresspiegel in Metern fest. Im Tag
<time>
liegt die UTC-Uhrzeit (GMT-Zeitzone) in
ISO-8601-Schreibweise.
Die Funktion str2time()
aus dem Modul Date::Parse
vom
CPAN macht daraus die zeitzonenunabhängige Unix-Zeit in Sekunden
für spätere Berechnungen. handler()
packt alle gefundenen
Daten in einen Hash und speichert eine Referenz darauf als Element
im globalen Array @points
ab. Die abschließend aufgerufene
Methode DumpFile
des Moduls YAML
speichert den gesamten Array mit
den darin enthaltenen Hashreferenzen gut
lesbar in der Datei tracks.yml
ab, von wo er in den später folgenden
Skripts wieder mit einem einfachen LoadFile()
eingelesen wird.
Die heute untersuchte Wanderung führte auf den nördlich der Golden Gate Bridge gelegenen Wanderwegen ``Coastal Trail'' und ``Rodeo Trail'' auf- und ab durch die sogenannten Marin Headlands, eine malerische Hügellandschaft am Pazifischen Ozean. Wenn man aus den über die drei Stunden vom GPS-System gesammelten Trackdaten die Höhenangaben extrahiert und über die Zeitachse aufträgt, ergibt sich ein Graph nach Abbildung 5.
Kurz nach 13:00 ging die Wanderung etwa 200 Meter über dem Meerespiegel los, um nach nach etwa einanhalb Stunden leichtem Auf und Ab bis auf Meereshöhe abzufallen. Zum Abschluss folgt noch ein steiler einstündiger 200-Meter-Anstieg, zurück zum Eingang des Rundwegs.
Abbildung 5: Höhe über dem Meeresspiegel während der Wanderung |
01 #!/usr/bin/perl -w 02 use strict; 03 use YAML qw(LoadFile); 04 use RRDTool::OO; 05 use File::Temp qw(tempfile); 06 07 my $trkpts = LoadFile("tracks.yml"); 08 09 my $rrd = RRDTool::OO->new( 10 file => (tempfile())[1]); 11 12 $rrd->create( 13 start => $trkpts->[0]->{time} - 1, 14 step => 60, 15 data_source => { name => "elevation", 16 type => "GAUGE" }, 17 archive => { rows => 10000 }); 18 19 for my $trkpt (@$trkpts) { 20 eval { # Deal with dupes 21 $rrd->update(time => $trkpt->{time}, 22 value => $trkpt->{ele}); 23 }; 24 } 25 26 $rrd->graph( 27 start => $trkpts->[0]->{time}, 28 end => $trkpts->[-1]->{time}, 29 image => "elevation.png", 30 vertical_label => 'Elevation', 31 width => 300, 32 height => 75, 33 lower_limit => 0, 34 );
Listing elerrd
plottet den Graphen mit Hilfe des Moduls
RRDTool::OO
, das unter der Haube die Round-Robin-Datenbank
rrdtool
verwendet. rrdtool
kam vor allem wegen seiner
eleganten (sprich: automatischen) Datumsanzeige auf der X-Achse zum Einsatz.
Zeile 7 liest die YAML-Daten ein und der darauffolgende
Konstruktor new()
erzeugt eine neue RRD-Datenbank mit einer temporären
Datei als Speicher, denn die Daten werden später nicht mehr gebraucht.
tmpfile()
liefert zwei Argumente zurück, von denen nur der Name
der Temp-Datei an new()
überreicht wird. Die Methode create()
definiert anschließend das Schema des Datenspeichers, der alle
60 Sekunden einen Wert erwartet. Der GPS-Empfänger liefert seine
Trackdaten zwar alle paar Sekunden, doch rrdtool
mittelt sie
einfach. Die Datenbank speichert maximal 10.000 gerundete
Minuten-Messpunkte, das dürfte auch für die längsten Wanderungen
ausreichen.
Die for
-Schleife ab Zeile 19 iteriert über alle Trackpunkte und
füttert sie mitsamt der zugehörigen Uhrzeit mittels der Methode
update()
in die Datenbank. Da rrdtool
gleich ausrastet, wenn
zum Beispiel zweimal derselbe Zeitstempel vorliegt, umrahmt
ein eval
-Block den Update-Befehl, der so kleine Fehler ungestraft
durchlässt.
Das Zeichnen des Graphen erledigt die Methode graph()
. Der erste
Trackpunkt bestimmt die Startzeit, der Zeitstempel des letzten
Messwerts das Ende, und schon liegt das ansprechende gestaltete
Diagramm
im PNG-Format in der Datei mit dem in Zeile 29 festgelegten Namen
elevation.png
.
Um die Zahl der abgespulten Kilometer auszurechnen, muss Listing
dist
durch alle Trackpunkte iterieren, jeweils die zwischen ihnen
liegende Distanz ausrechnen und die Einzelstrecken aufaddieren.
Jeder Trackpunkt ist eine Referenz auf einen Hash, der unter dem
Schlüssel lat
den Breitengrad und unter lon
den Längengrad
führt. In $last_pt
liegt während jeder (außer der ersten)
Schleifeniteration der Trackpunkt des vorherigen Durchgangs.
Die Strecke zwischen zwei Messpunkten zu bestimen,
die als Längen- und Breitengrade
vorliegen, ist allerdings nicht ganz trivial, da es sich um die
Entfernung auf der Oberfläche eines Ellipsoiden handelt. Das
Modul Geo::Distance
vom CPAN führt die Berechnung mit trigonometrischen
Funktionen aus und bietet hierfür die einfach zu bedienende Methode
distance()
an, die als Parameter die gewünsche Maßeinheit
(``kilometer'' oder ``mile'') und zwei Messpunkte als Längen- und Breitenwert
entgegennimmt. Zurück kommt die errechnete Strecke:
$ ./dist Total: 11.67km
01 #!/usr/bin/perl -w 02 use strict; 03 use YAML qw(LoadFile); 04 use Geo::Distance; 05 06 my $trkpts = LoadFile("tracks.yml"); 07 my $geo = Geo::Distance->new(); 08 09 my $total = 0; 10 my $last_pt; 11 12 for my $trkpt (@$trkpts) { 13 if($last_pt) { 14 my $k = $geo->distance("kilometer", 15 $last_pt->{lon}, $last_pt->{lat}, 16 $trkpt->{lon}, $trkpt->{lat}); 17 18 $total += $k; 19 } 20 $last_pt = $trkpt; 21 } 22 23 printf "Total: %.2fkm\n", $total;
Ganz populär sind neuerdings die sogenannten ``Mash-Ups'', bei denen Online-Landkarten mit selbstgebastelten Erweiterungen aufgebohrt werden. Neben Google bietet auch Yahoo die Möglichkeit, mit einer einfachen JavaScript-API skalierbare Karten dynamisch mit Markern zu versehen.
Um den zurückgelegten Wanderweg in die Yahoo-Map einzuzeichnen, muss
zunächst die Datenmenge reduziert werden. Alle 1800 Trackpunkte ergäben
ein undurchdringbares Tohuwabohu. Deswegen geht Listing map
in einer
for
-Schleife durch die Trackpunkte und schiebt nur diejenigen hinten auf
den Array @points
, die mindestens 0.4 Kilometer vom letzten Bezugspunkt
entfernt liegen. Geo::Distance
führt auch hier wieder die komplexen
Distanzberechnungen durch.
Auf [6] findet sich die Anleitung zum Mash-Up, Abbildung 6 zeigt den
gesamten dafür notwendigen JavaScript-Code.
Beim Nutzen der API ist zu beachten, dass man sich eine eigene
Application ID abholen sollte (Listing map
nutzt die ID YahooDemo),
mit der für
eine IP 50.000 Zugriffe pro Tag erlaubt sind. Als Einschränkung darf keine
Live-GPS-Navigation durchgeführt werden, die GPS-Daten muessen mindestens
6 Stunden alt sein.
Der JavaScript-Code ist zusammen mit
einigen HTML-Tags in der Datei map.tmpl
abgelegt, die Listing
map
einliest, mit dem Template Toolkit vom CPAN interpretiert und
die in [%...%]
versteckten Konstrukte ersetzt. Das Template Toolkit bietet
eine simple Skriptsprache, die bewusst beschränkte Kontrollmöglichkeiten
bietet, um zu verhindern, dass zuviel Programmlogik in die
Darstellungsschicht wandert. Außerdem ist der Zugriff auf Variablen
genial einfach, Hashes, Arrays und Referenzen darauf werden alle über
einen Kamm geschert, das magische Zeichen ist der Punkt (.).
Um zum Beispiel das erste Element des Arrays zu referenzieren, auf
den die Referenz $points zeigt, und dann den zum Schlüssel lat
gehörenden
Wert des so hervorgeholten Hashes zu extrahieren, genügt die
Template-Toolkit Notation points.0.lat
, statt wie in Perl
$points->[0]->{lat}
zu erfordern. Genial!
Die Ausgabe von Listing map
wird einfach in eine
HTML-Datei umgeleitet und dann in den Browser geladen. Schon
steht in einem 600x400 großen Fenster eine Karte, die sich verschieben
(Panning), Vergrößern/Verkleinern und von der Landkartendarstellung
in den Satellitenmodus schalten lässt. Auch einen 'hybriden' Modus
gibt es, der das Satellitenbild mit einigen Informationen aus der Landkarte
unterlegt. Abbildung 7 zeigt das zunächst erscheinende Browserbild,
die ausgewählten Trackerpunkte erscheinen als kleinen orangen Sprechblasen,
die von 1 bis 19 durchnumeriert sind. Für Deutschland ist die
Landkartensituation bei Yahoo noch nicht so ausgereift wie für die
USA, aber zumindest die Satellitenbilder sind (wenngleich auch noch in
geringerer Auflösung) verfügbar.
Der JavaScript-Code in Abbildung 6 führt nur die einfachsten Gimmicks der Map-API vor, zusätzlich lassen sich leicht aufklappende Sprechblasen mit Bildern und allerlei Schnickschack hinzufügen. Events wie eintreffende Mausklicks und Ziehbewegungen lassen sich abfangen, mit JavaScript-Code bearbeiten und eventuell mittels Ajax-Tricks zu einem Server zurückkommunizieren. Go nuts!
01 #!/usr/bin/perl -w 02 use strict; 03 use YAML qw(LoadFile); 04 use Geo::Distance; 05 use Template; 06 07 my $trkpts = LoadFile("tracks.yml"); 08 my $geo = Geo::Distance->new(); 09 10 my $count = 0; 11 my $min = 0.4; # Minimum marker distance 12 my @points = (); 13 my $last_pt; 14 15 for my $trkpt (@$trkpts) { 16 if($last_pt) { 17 my $k = $geo->distance("kilometer", 18 $last_pt->{lon}, $last_pt->{lat}, 19 $trkpt->{lon}, $trkpt->{lat}); 20 21 next if $k < $min; 22 } 23 $trkpt->{count} = ++$count; 24 push @points, $trkpt; 25 26 $last_pt = $trkpt; 27 } 28 29 my $template = Template->new(); 30 my $vars = { points => \@points }; 31 32 $template->process("map.tmpl", $vars) or 33 die $template->error();
Abbildung 6: Das Template mit dem JavaScript-Code, um den Mash-Up zu erzeugen. |
Abbildung 7: Der fertige Mash-Up mit den Wegpunkten des Wanderwegs nördlich von San Francisco |
Abbildung 8: Der gleiche Mash-Up, per Knopfdruck (links oben) in die hybride Satellitendarstellung geschaltet und mit dem Zoom-Meter (rechts oben) vergrößert. |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |