Lenker aus dem All (Linux-Magazin, Juli 2006)

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.

Babylonische Verwirrung

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

Listing 1: tracks2yml

    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.

Bergauf, bergab

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

Listing 2: elerrd

    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.

Summieren in Sphären

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

Listing 3: dist

    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;

Landkarten aufmischen

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!

Listing 4: map

    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.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/07/Perl

[2]
``Google Maps Hacks'', Rich Gibson & Schuyler Erle, O'Reilly 2006

[3]
http://pfranc.com, Anleitung/Versand für selbstgebastelte Garmin-Stecker

[4]
http://www.gpsbabel.org, GPS-Formatkonvertierer

[5]
``Datenfischer'', Michael Schilli, http://www.linux-magazin.de/Artikel/ausgabe/2005/08/perl/perl.html

[6]
Yahoo! Maps Web Services - AJAX API Getting Started Guide, http://developer.yahoo.com/maps/ajax/

Michael Schilli

arbeitet 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.