Aus Linux-Magazin 07/2006

GPS-Daten mit Perl auswerten und online in Karten einblenden

© The Vorck Project

Der naturverbundene Perl-Hacker erforscht die Bergwelt selbstverständlich mit einem Navigationssystem. Dass er anschließend seine erbrachte Wanderleistung grafisch auswertet, ist Ehrensache.

Mit einem tragbaren Navigationssystem wandern macht gleich viel mehr Spaß. Der GPS-Empfänger teilt dem Wanderer nicht nur ständig die aktuelle Position mit, sondern auch seine Höhe über dem Meeresspiegel oder seine Entfernung zu einprogrammierten Objekten. Auch von der Wandergeschwindigkeit, der bisher zurückgelegten Wegstrecke und der wahrscheinlich noch benötigten Zeit bis zum Ziel weiß das System stets zu berichten. Nach abgeschlossener Wanderung lässt sich das Gerät an den heimischen PC anschließen, um die unterwegs gesammelten GPS-Daten herunterzuladen und auszuwerten.

Der GPS-Empfänger Etrex von Garmin [7] ist nur ein Einsteigermodell, eignet sich aber hervorragend für den sporadischen Wanderer. Er ist ab etwa 120 Euro zu haben, handlich, wasserfest und so robust, dass er auch leichte Stürze überlebt (Abbildung 1). Um den Empfänger nach der Wanderung an den heimischen PC anzuschließen, ist ein Spezialkabel erforderlich, das den Etrex mit der seriellen Schnittstelle des Rechners verbindet. Das Original ist überteuert (ab 25 Euro), weswegen es unter [3] ein Selbstbau-Projekt gibt. Mein Lötkolben blieb dieses Mal kalt, ich habe das Teil einfach gekauft (Abbildung 2),

Listing 1:
»tracks2yml«

01 #!/usr/bin/perl -w
02 use strict;
03 use Sysadm::Install qw(:all);
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 my @points = ();
15 $twig->parsefile("tracks.xml");
16 DumpFile("tracks.yml", @points);
17
18 ###########################################
19 sub handler {
20 ###########################################
21     my($t, $trkpt)= @_;
22     my $lat     = $trkpt->att('lat');
23     my $lon     = $trkpt->att('lon');
24     my $ele     = $trkpt->first_child(
25                             'ele')->text();
26     my $isotime = $trkpt->first_child(
27                            'time')->text();
28     my $time    = str2time($isotime);
29     push @points, {
30         lat => $lat, lon => $lon,
31         ele => $ele, time => $time,
32         isotime => $isotime,
33     };
34 }
Abbildung 1: Der mobile GPS-Empfänger Etrex von Garmin, ein preiswertes Gerät der Einsteigerklasse.

Abbildung 1: Der mobile GPS-Empfänger Etrex von Garmin, ein preiswertes Gerät der Einsteigerklasse.

Abbildung 2: Ein Spezialkabel verbindet den Etrex mit der seriellen Schnittstelle des Rechners.

Abbildung 2: Ein Spezialkabel verbindet den Etrex mit der seriellen Schnittstelle des Rechners.

Babylonische Verwirrung

GPS-Empfänger arbeiten leider mit unterschiedlichen Koordinaten-Formaten. Diese Sprachverwirrung babylonischen Ausmaßes bekämpft das Freeware-Programm »gpsbabel« [4]. Es beherrscht nicht nur Dutzende Datenformate, sondern liest unter Linux auch die Daten von einem am seriellen Port hängenden Garmin Etrex aus. Im Speicher des GPS-Empfängers liegen gesammelte Waypoints (auf Knopfdruck festgehaltene Ortspunkte), Routes (Wegpunkte geplanter Routen) und Tracks (Spuren, automatisch alle paar Sekunden aufgezeichnete Ortspunkte).

Wer vor dem Start seinen Trackspeicher löscht und am Ziel auf den Rechner herunterlädt, erhält eine digitale Aufzeichnung der Wanderstrecke. Nun kann er die Positionsdaten kreativ auswerten. Hängt der Garmin etwa an der zweiten seriellen Schnittstelle, lädt »gpsbabel -t -i garmin -f /dev/ttyS1 -o gpx -F tracks.txt« die Trackdaten (»-t«) im Garmin-Format (»-i garmin«) von der Schnittstelle »/dev/ttyS1« und speichert sie im GPX-Format (»-o gpx«) in »tracks.txt« (»-F tracks.txt«).

Damit der Prozess, der den Garmin ansteuert, nicht mit Root-Rechten laufen muss, macht der folgende Befehl den Device-Eintrag für die serielle Schnittstelle einfach für alle beschreibbar:

> chmod a+rw /dev/ttyS1
> ls -l /dev/ttyS1
crw-rw-rw- 1 root uucp 4, 65 Feb 10 22:47 /dev/ttyS1

Nach geraumer Weile (serielle Schnittstellen stammen aus dem vergangenen Jahrhundert) kehrt das »gpsbabel«-Kommando zurück und in »tracks.txt« liegen die Daten im XML-Format (Abbildung 3). Damit die folgenden Auswertungen diese XML-Daten nicht parsen müssen, transformiert sie das Skript »tracks2yml« in das augenfreundliche YAML-Format, das sich zudem in einem Rutsch in eine Perl-Datenstruktur verwandeln lässt. Abbildung 4 zeigt die YAML-Daten.

Abbildung 3: Dieses nicht leicht lesbare Format haben die frisch vom Garmin heruntergeladenen GPX-Streckendaten vor der Konvertierung.

Abbildung 3: Dieses nicht leicht lesbare Format haben die frisch vom Garmin heruntergeladenen GPX-Streckendaten vor der Konvertierung.

Abbildung 4: Die gleichen Track-Daten wie in Abbildung 3 jetzt im YAML-Format nach der Umwandlung durch »track2yml«.

Abbildung 4: Die gleichen Track-Daten wie in Abbildung 3 jetzt im YAML-Format nach der Umwandlung durch »track2yml«.

Koordinaten-Objekt

Das Skript »tracks2yml« in Listing 1 nutzt das Modul »XML::Twig«, das – wie in [5] schon 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 Unter-Tags.

Das Attribut »lat« (Latitude, Breite) gibt die geographische Breite des Wegpunkts in Dezimalschreibweise an. Nördliche Breitengrade sind positiv, südliche negativ. Das Attribut »lon« (Longitude, Länge) benennt die geographische Länge, wobei westliche Längengrade negativ, östliche hingegen positiv sind. Das Unterelement »ele« (Elevation, Höhe) 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 Zeitzonen-unabhängige Unix-Zeit in Sekunden für spätere Berechnungen. »handler()« ab Zeile 19 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«. Von dort wird der Array in den folgenden Skripten wieder mit einem einfachen »LoadFile()« eingelesen.

Bergauf, bergab

Meine bisher letzte satellitengestützte Wanderung führte auf die nördlich der Golden Gate Bridge gelegenen Wanderwege “Coastal Trail” und “Rodeo Trail”. Auf und ab ging es durch die Marin Headlands, eine malerische Hügellandschaft am Pazifischen Ozean.

Wenn ich aus den Daten, die das GPS-System über drei Stunden lang gesammelt hat, die Höhenangaben extrahiere und auf der Zeitachse auftrage, ergibt sich Abbildung 6. Kurz nach 13:00 Uhr ging die Wanderung etwa 200 Meter über dem Meerespiegel los, um nach etwa eineinhalb Stunden bei leichtem Auf und Ab bis auf Meereshöhe abzufallen. Zum Schluss folgt noch ein steiler einstündiger 200-Meter-Anstieg zurück zum Eingang des Rundwegs.

Listing 2 plottet den Graphen mit Hilfe des Moduls »RRDTool::OO«, das unter der Haube die Round-Robin-Datenbank »rrdtool« verwendet. Rrdtool kam vor allem wegen der eleganten (sprich automatischen) Datumsanzeige auf der x-Achse zum Einsatz. Zeile 7 liest die YAML-Daten ein, der darauf folgende Konstruktor »new()« erzeugt eine neue RRD-Datenbank mit einer temporären Datei als Speicher, denn die Daten werden später nicht mehr gebraucht.

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 );

Die Funktion »tempfile()« liefert zwei Argumente zurück, von denen »new()« nur den Namen der Temp-Datei überreicht bekommt. 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 10000 gerundete Minuten-Messpunkte, das dürfte auch für die längsten Gewaltmärsche ausreichen.

Die »for«-Schleife ab Zeile 19 iteriert über alle Trackpunkte und verfüttert sie samt der zugehörigen Uhrzeit mit Hilfe der Methode »update()« an die Datenbank. Da »rrdtool« die Eigenheit hat, gleich auszurasten, wenn zum Beispiel derselbe Zeitstempel zweimal vorliegt, umrahmt ein »eval«-Block den Update-Befehl, der auf diese Weise 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 ansprechend gestaltete Diagramm im PNG-Format in der Datei »elevation.png«.

Um die Zahl der abgespulten Kilometer auszurechnen, muss Listing 3 durch alle Trackpunkte iterieren, jeweils die Distanz zwischen ihnen ausrechnen und die Einzelstrecken addieren. 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 » liegt während jeder Schleifeniteration (außer der ersten) der Trackpunkt des vorherigen Durchgangs. Die Strecke zwischen zwei Messpunkten bestimen ist nicht ganz trivial, da es sich um die Entfernung auf der Oberfläche eines Ellipsoids handelt. Das Modul Geo::Distance vom CPAN führt die Berechnung mit trigonometrischen Funktionen aus und bietet dafür die einfach zu bedienende Methode »distance()« an. Parameter sind die gewünschte Maßeinheit (Kilometer oder Miles) und zwei Messpunkte als Längen- und Breitenwert, zurück kommt die errechnete Strecke:

$ ./dist
Total: 11.67km
Abbildung 5: Das Template »m.tpl« mit dem Javascript-Code, um das Mash-up zu erzeugen.

Abbildung 5: Das Template »m.tpl« mit dem Javascript-Code, um das Mash-up zu erzeugen.

Abbildung 6: Geplottete Höhe über dem Meeresspiegel während der Wanderung.

Abbildung 6: Geplottete Höhe über dem Meeresspiegel während der Wanderung.

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: %.2fkmn", $total;

Landkarten aufmischen

Populär sind gerade die so genannten Mash-ups. Sie bohren Online-Landkarten mit Erweiterungen auf. Neben Google bietet auch mein Arbeitgeber Yahoo die Möglichkeit, mit einem einfachen Javascript-API skalierbare Karten dynamisch mit Markern zu versehen.

Um den zurückgelegten Weg in die Yahoo-Map einzuzeichnen, muss zunächst die Datenmenge reduziert werden. Alle 1800 Trackpunkte ergäben ein undurchdringbares Tohuwabohu. Daher schiebt Listing 4 in einer »for«-Schleife nur jene Trackpunkte hinten auf den Array »@points«, die mindestens 0,4 Kilometer voneinander entfernt sind. Geo::Distance führt auch hier die komplexen Distanzberechnungen durch.

Abbildung 7: Die fertige Mash-up-Darstellung mit den einzelnen Wegpunkten des Wanderwegs nördlich von San Francisco.

Abbildung 7: Die fertige Mash-up-Darstellung mit den einzelnen Wegpunkten des Wanderwegs nördlich von San Francisco.

Abbildung 8: Das gleiche Mash-up, per Knopfdruck (links oben) in die hybride Satellitendarstellung geschaltet und mit dem Zoom-Meter (rechts oben) vergrößert.

Abbildung 8: Das gleiche Mash-up, per Knopfdruck (links oben) in die hybride Satellitendarstellung geschaltet und mit dem Zoom-Meter (rechts oben) vergrößert.

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();

Mash-up für Karten

Bei [6] findet sich die Anleitung für das Mash-up, Abbildung 5 zeigt den dafür notwendigen Code. Um das API zu nutzen, sollte man sich eine eigene Application-ID abholen, mit der pro IP-Adresse 50000 Zugriffe pro Tag erlaubt sind. Als Einschränkung ist jede Live-GPS-Navigation verboten, die Daten müssen mindestens sechs Stunden alt sein.

Der Javascript-Code ist zusammen mit einigen HTML-Tags in der Datei »map.tmpl« abgelegt. Listing 4 liest sie ein, interpretiert sie mit dem Template Toolkit vom CPAN und ersetzt die in »[%…%]« versteckten Konstrukte. Das Template Toolkit enthält eine einfache Skriptsprache, die bewusst beschränkte Kontrollmöglichkeiten bietet, um so zu verhindern, dass zu viel Programmlogik in die Darstellungsschicht wandert.

Außerdem ist der Zugriff auf Variablen genial einfach: Hashes, Arrays und Referenzen schert es alle über einen Kamm. Das magische Zeichen ist der Punkt. Um zum Beispiel das erste Element des Array zu referenzieren, auf den die Referenz » zeigt, und dann den zum Schlüssel »lat« gehörenden Wert des so hervorgeholten Hash zu extrahieren, genügt bereits die Template-Toolkit-Notation »points.0.lat«. Perl würde stattdessen »->[0]->{lat}« erfordern.

Tour im Satellitenfoto

Die Ausgabe von Listing »map« einfach in eine HTML-Datei umleiten und dann in den Browser laden – schon steht in einem 800 mal 600 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 Wegpunkte erscheinen als kleine orangene Sprechblasen, von 1 bis 19 durchnummeriert. Für Deutschland stehen bei Yahoo noch nicht so viele Landkarten wie für die USA bereit, aber zumindest die Satellitenbilder sind (in geringerer Auflösung) verfügbar.

Der in Abbildung 5 gezeigte Code präsentiert nur die einfachsten Gimmicks des Yahoo-API. Zusätzlich lassen sich zum Beispiel leicht aufklappende Sprechblasen mit Bildern und allerlei Schnickschnack hinzufügen oder eintreffende Events wie Mausklicks oder Ziehbewegungen abfangen, dann mit Javascript bearbeiten und anschließend eventuell mit Hilfe einiger Ajax-Tricks zu einem Server zurücksenden. (jcb)

Infos

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

[2] Rich Gibson, Schuyler Erle, “Google Maps Hacks”: O\’Reilly 2006

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

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

[5] Michael Schilli, “Datenfischer”: [https://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/]

[7] Garmin: [http://www.garmin.de/index.php]

Der Autor


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 ist: [http://perlmeister.com]

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben