Handgeräte für die Navigation weisen den Weg und schreiben bei Wanderungen laufend die aktuelle Position mit. Mit einigen Skripten unter Linux bereitet Extrembergsteiger Michael Schilli die Daten seiner waghalsigen Erstbesteigungen optisch ansprechend auf.
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2016/03/plus
Jedes Smartphone bietet einen GPS-Empfänger und Apps zuhauf, die den Wanderer auf eingeblendeten Landkarten über Berg und Tal leiten. Allerdings geht es in der Wildnis oft etwas rustikal zu, und da kommen robustere und spritzwasserfeste Geräte mit dicken Batterien zum Einsatz. Vor einiger Zeit hatte ich im Sonderangebot ein Garmin 62s gekauft, das mittlerweile etwas in die Jahre gekommen ist, aber aussieht, als könnte es einem darüberfahrenden Panzer standhalten.
Wer allerdings von seinem Smartphone intuitive On-Screen-Bedienung gewöhnt ist, reibt sich verwundert die Augen, dass es tatsächlich noch LCD-Displays mit bizarr gestalteten Menüs gibt, über die der User einen Cursor mit einem Dutzend Plastikknöpfen auf der Vorderseite des Geräts bugsieren muss (Abbildung 1).
UI für Steam-Punks
Ich wurde aus diesen Gründen schon beim Eingeben eines einzigen Wegpunktes zur Markierung des Trail-Eingangs fast wahnsinnig. So hätte wohl die Zukunft des mobilen Telefonierens ausgesehen, wenn Bill Gates mit seiner aggressiven Monopolpolitik gewonnen hätte, gar nicht auszudenken! Wäre ich Produktdesigner bei Garmin, brächte ich sofort eine an den neuen Mad-Max-Film “Fury Road” angelehnte Produktlinie im Steam-Punk-Look heraus.
Auch die Software zum Auslesen und Beschreiben des Geräts läuft nur auf Windows, die Version für den Mac erkannte das eingestöpselte Gerät gar nicht. Aber da mein Ubuntu-System sofort darauf ansprang, als ich den USB-Stecker einschob (Syslog in Abbildung 2) und die Datenbereiche des Geräts auf zwei gemounteten Platten offenlegte, beruhigte ich mich sofort wieder und beschloss, das Gerät auf Wanderungen einfach eingeschaltet mitzuführen.
Auch ohne Zutun des Users schreibt es alle paar Sekunden einen Eintrag in eine so genannte Tracks-Datei, die festhält, zu welchem Zeitpunkt sich das Gerät an welcher Position befindet. Es notiert neben dem Zeitstempel die geografischen Längen- und Breitengrade sowie die aktuelle Höhe über dem Meeresspiegel.
Open Source statt Windows
Diese Werte legen Garmin-Geräte in einem XML-Dialekt namens GPX ab, der sich einfach mit Open-Source-Tools erforschen lässt. Abbildung 3 zeigt die auf dem Gerät liegenden Dateien. Die Tracks-Datei »Track_2015-12-31 130304.gpx« enthält die gesuchten, über die Zeit festgehaltenen Aufenthaltsorte.
Das XML zu parsen ist nicht weiter schwierig und ein CPAN-Autor hat sich schon die Mühe gemacht, alles sauber in das Modul Geo::Gpx zu verschnüren, sodass Listing 1 nach dem Herunterladen des Moduls nur noch wenige Zeilen benötigt, um das Ganze ins Yaml-Format zu überführen, das sich sowohl einfach visuell inspizieren als auch maschinell weiterverarbeiten lässt.
Listing 1
gpx2yaml
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Geo::Gpx;
04 use YAML qw( Dump );
05
06 my( $file ) = @ARGV;
07 die "usage: $0 file" if !defined $file;
08
09 open my $fh, "<$file" or die $!;
10 my $gpx = Geo::Gpx->new( input => $fh );
11
12 print Dump $gpx->tracks->[ 0 ]->
13 { segments }->[ 0 ]->{ points };
Die Ausgabe in Abbildung 4 zeigt zu jedem Zeitpunkt (»time« ) in Unix-Sekunden die geografische Länge (»lon« für Longitude), die Breite (»lat« für Latitude) und die Höhe über dem Meeresspiegel in Fuß (»ele« für Elevation) an. Falls es gewünscht ist, lässt sich das Gerät in den Settings auch auf metrische Maßeinheiten umstellen.

Listing 1 wandelt die GPX-Daten der Tracks-Datei in das weitaus augenfreundlichere Yaml-Format um.” width=”288″ height=”300″ />
Abbildung 4: Listing 1 wandelt die GPX-Daten der Tracks-Datei in das weitaus augenfreundlichere Yaml-Format um.Listing 1 nutzt hierzu die beiden Module Geo::Gpx und YAML vom CPAN. Letzteres exportiert auf Anfrage die Methode »Dump()« , die eine verschachtelte Datenstruktur als Yaml ausgibt, in diesem Fall den von Geo::Gpx gelieferten Array von Hashes unter »tracks« , »segments« und schließlich »points« im GPX-Salat.
Die hier nachfolgend vorgestellten Skripte in der Verarbeitungskette lesen die Yaml-Ausgabe per Unix-Pipe und der Funktion »Load()« des YAML-Moduls ein und arbeiten dann mit der vereinfachten Datenstruktur weiter. Ihre Ausgabe verpacken sie dann ebenfalls ins Yaml-Format, sodass sich beliebig viele solcher Skripte in guter alter Unix-Manier hintereinanderhängen lassen und jedes sich auf eine spezielle Aufgabe konzentrieren kann.
Fauler Operator
Die nächste Verarbeitungsstufe kümmert sich darum, relevante Teile aus der Tracks-Datei auszuschneiden. Wegen einer Mischung aus Operator-Trägheit und Vergesslichkeit ging ich kein einziges Mal in das »Tracks« -Menü des Geräts, um die Tracks eines Tages entweder zu löschen oder auf den Namen einer Tour (etwa “Grand Canyon”) umzuspeichern, sondern ließ es einfach während des gesamten Urlaubs munter in die gleiche Tracks-Datei weiterschreiben.
Das Skript »tours« in Listing 2 schnappt sich deswegen die in Yaml umgemodelten GPX-Daten mittels »gpx2yaml *.GPX | tours« aus der Unix-Pipe und teilt sie in verschiedene Tagestouren auf, die es jeweils daran erkennt, dass zwischen zwei Einträgen fünf Stunden ohne jede Aktivität verstrichen sind, in denen das Gerät also höchstwahrscheinlich ausgeschaltet war.
Listing 2
tours
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Getopt::Long;
04 GetOptions( \my %opts, "tour=s" );
05 use YAML qw( Load Dump );
06
07 my @tours = ( );
08 my $tour;
09 my $tracks = Load join "", <>;
10 my $tour_split_secs = 60 * 60 * 5;
11
12 for my $point ( @$tracks ) {
13
14 # next tour?
15 if( defined $tour and
16 $point->{ "time" } >
17 $tour->{ end } + $tour_split_secs ) {
18 undef $tour;
19 }
20
21 # start new tour?
22 if( !defined $tour ) {
23 $tour = {
24 start => $point->{ "time" },
25 points => [] };
26 push @tours, $tour;
27 }
28
29 # next point in current tour
30 $tour->{ end } = $point->{ "time" };
31 push @{ $tour->{ points } }, $point;
32 }
33
34 if( $opts{ tour } ) {
35 print Dump( $tours[
36 $opts{ tour } - 1 ]->{ points } );
37
38 } else {
39 my $idx = 1;
40 for my $tour ( @tours ) {
41 printf "tour %02d: %s - %s (%d)\n",
42 $idx, map( { scalar localtime $_ }
43 ( $tour->{ start }, $tour->{ end } )
44 ), scalar @{ $tour->{ points } };
45 $idx++;
46 }
47 }
Abbildung 5 zeigt die von 01 bis 19 durchnummerierten Touren jeweils mit Start- und Endzeit, die das Skript deshalb anzeigt, weil ich es ohne Parameter aufgerufen habe. Um eine bestimmte Tagestour herauszupicken, wählt der User sie per angezeigter Tournummer aus. Mit
[...] | tours --tour=18
kommen hinten (natürlich wieder als Yaml) die Trackdaten der Tour vom 30. Dezember 2015 heraus, an dem ich samt Frau einige Meilen des vereisten “Bright Angel Trail” im Grand Canyon hinunter- und wieder heraufstiefelte (Abbildung 6). Die Ausgabe von Listing 2 sieht aus wie Abbildung 4, nur eben mit weniger Daten, da alles außer dem 30. Dezember im Tourenfilter festsitzt.
Touren extrahieren
Eine »Tour« definiert Listing 2 mittels des ersten (»start« ) und des letzten Eintrags (»end« ) sowie einer Reihe von zugehörigen Track Points (»points« ) mit Geodaten und Zeitstempel. Es wandert durch Einträge der auf der Standard-Eingabe hereinpurzelnden Yaml-Daten, weist die Daten der aktuellen Tour zu und fängt eine neue Tour an, falls vor einem Eintrag eine Pause von länger als »60 * 60 * 5« Sekunden (aus Zeile 10) lag, also insgesamt 5 Stunden.
Das If-Konstrukt in Zeile 34 prüft, ob das Skript mit der Option »–tour« samt Tournummer aufgerufen wurde, und druckt in diesem Fall die Yaml-Ausgabe dieser Tour. Falls keine Kommandozeilenoptionen vorliegen, springt das Skript in den Else-Zweig ab Zeile 38 und gibt die Metadaten aller Touren – wie in Abbildung 5 gezeigt – aus.
Nun ist allerdings eine Tour keineswegs auf Aktivitäten während einer Bergwanderung beschränkt. Falls der User vergisst, das Navigationsgerät nach Abschluss des Gipfelsturms wieder auszuschalten, registriert es auch noch die anschließenden Bewegungen auf der Autofahrt nach Hause. Diese als körperliche Aktivität auszugeben wäre natürlich grob unsportlich und außerdem stören die Daten beim späteren Eintragen in eine Landkarte, da das Auto bekanntlich weit längere Strecken in kürzerer Zeit zurücklegt als ein Wanderer zu Fuß.
Autofahrt ausblenden
Ein weiterer Filter in Listing 3 sucht deshalb aus den Trackdaten einer Tour die erste Wanderung (Hike) heraus. Mit dem CPAN-Modul Geo::Distance ermittelt es die Distanz zwischen zwei aufeinanderfolgenden Messpunkten in den Trackdaten. Es nutzt ein wenig Geometrie, um den Abstand zweier durch geografische Länge und Breite angegebenen Punkte auf der Erdoberfläche zu ermitteln, und gibt das Ergebnis wegen des Parameters »meter« in Zeile 16 in Metern aus. Falls diese Distanz größer als 1 Kilometer ist, verwirft Zeile 26 alle bisher aufgezeichneten Werte, um sich auf die folgenden Einträge zu konzentrieren.
Listing 3
hike-find
01 #!/usr/local/bin/perl -w
02 use strict;
03 use YAML qw( Load Dump );
04 use Geo::Distance;
05
06 my $tour = Load( join "", <> );
07 my @markers = ();
08
09 my $geo = Geo::Distance->new();
10 my $last_pt;
11
12 for my $point ( @{ $tour } ) {
13
14 if( $last_pt ) {
15
16 my $k = $geo->distance("meter",
17 $last_pt->{lon}, $last_pt->{lat},
18 $point->{lon}, $point->{lat} );
19
20 push @markers, $point;
21
22 my $time_diff =
23 $point->{ time } - $last_pt->{ time };
24
25 if( $k > 1_000 ) {
26 @markers = ();
27 $last_pt = $point;
28 next;
29 }
30
31 my $speed = 1.0 * $k / $time_diff;
32 if( $speed > 5 ) {
33 last;
34 }
35 }
36
37 $last_pt = $point;
38 }
39
40 print Dump( \@markers );
Zeile 31 berechnet die aktuelle Geschwindigkeit des Wanderers, indem sie die ermittelte Distanz zwischen zwei aufeinanderfolgenden Trackpunkten durch die zwischen beiden Messpunkten verstrichene Zeit in Sekunden teilt. Ist dieser Wert größer als 5 Meter pro Sekunde, hat der Wanderer die Strecke wohl kaum zu Fuß zurückgelegt und sitzt bereits im Auto auf dem Weg nach Hause.
Listing 4
map-draw
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Template;
04 use YAML qw( Load );
05
06 my $trip = Load( join "", <> );
07 my $data = join "", <DATA>;
08
09 my $mappoints = join ", ",
10 map {
11 mappoint( $_->{ lat }, $_->{ lon } )
12 } @$trip;
13
14 if( scalar @$trip <= 1 ) {
15 die "Need at least one track point";
16 }
17
18 my $first = $trip->[0];
19 my $center =
20 mappoint(
21 $first->{ lat }, $first->{ lon } );
22
23 my $tmpl = Template->new;
24 $tmpl->process( \$data,
25 { mappoints => $mappoints,
26 center => $center } ) or
27 die $tmpl->error();
28
29 sub mappoint {
30 my( $lat, $lon ) = @_;
31
32 return
33 "new google.maps.LatLng( $lat, $lon )";
34 }
35
36 __DATA__
37 <!DOCTYPE html>
38 <html>
39 <head>
40 <script src="http://maps.googleapis.com/maps/api/js">
41 </script>
42
43 <script>
44 function initialize() {
45 var mapProp = {
46 center:[% center %],
47 zoom:16,
48 mapTypeId:google.maps.MapTypeId.HYBRID
49 };
50
51 var map=new google.maps.Map(
52 document.getElementById("googleMap"),
53 mapProp);
54
55 var tracks=[ [% mappoints %] ];
56
57 var path=new google.maps.Polyline({
58 path:tracks,
59 strokeColor:"#f9290c",
60 strokeOpacity:0.7,
61 strokeWeight:4
62 });
63
64 path.setMap(map);
65 }
66
67 google.maps.event.addDomListener(window,
68 'load', initialize);
69 </script>
70 </head>
71
72 <body>
73 <div id="googleMap"
74 style="width:1000px;height:600px;"></div>
75 </body>
76 </html>
In diesem Fall bricht das »last« -Kommando in Zeile 33 ab, und am Skriptende druckt die Dump-Funktion in Zeile 40 alle bislang im Array »@markers« gesammelten Trackdaten im Yaml-Format für die nächste Verarbeitungsstufe aus.
Karten zeichnen
Um die Trackdaten zu visualisieren, erzeugen zwei weitere Skripte zwei verschiedene Darstellungen: Listing 4 nutzt das Google-Maps-API, um eine HTML-Darstellung eines Satellitenfotos farblich mit Trackpunkten anzureichern, Listing 5 zeichnet mittels Google-Charts-API [2]ein Höhenprofil der Wanderung.
Listing 5
elevation-chart
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Template;
04 use DateTime;
05 use YAML qw( Load );
06
07 my $tmpl = Template->new;
08 my $data = join "", <DATA>;
09 my $points = Load( join "", <> );
10
11 my $tracks = "";
12
13 for my $point ( @$points ) {
14 my $dt = DateTime->from_epoch(
15 epoch => $point->{ time } );
16 $dt->set_time_zone( "local" );
17
18 $tracks .= ", " if length $tracks;
19
20 $tracks .= sprintf "[[%d, %d, %d], %d]",
21 $dt->hour, $dt->minute, $dt->second,
22 int( $point->{ ele } );
23 }
24
25 $tmpl->process( \$data,
26 { tracks => $tracks } ) or
27 die $tmpl->error();
28
29 __DATA__
30 <!DOCTYPE html>
31 <html>
32 <head>
33 <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
34 <script>
35 google.charts.load('current', {packages: ['corechart', 'line']});
36 google.charts.setOnLoadCallback(drawBasic);
37
38 function drawBasic() {
39
40 var data = new google.visualization.DataTable();
41 data.addColumn('timeofday', 'Time of Day');
42 data.addColumn('number', 'Elevation');
43
44 data.addRows([ [% tracks %] ] );
45
46 var options = {
47 hAxis: {
48 title: 'Time',
49 format: 'HH:mm',
50 gridlines: {count: 10}
51 },
52 vAxis: {
53 title: 'Elevation (feet)'
54 }
55 };
56
57 var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
58
59 chart.draw(data, options);
60 }
61 </script>
62
63 </head>
64 <body>
65 <div id="chart_div"></div>
66 </body>
67 </html>
Abbildung 7 zeigt die mit Hilfe des Google-Maps-API [3] in das Satellitenfoto eingezeichneten Trackdaten des Navigationsgeräts. Insgesamt haben die Daten hierzu vier Filterskripte durchlaufen, bis das letzte schließlich die HTML-Daten erzeugt, die ein auf die erstellte Datei »map.html« gerichteter Browser ins rechte Licht rückt:
gpx2yaml *GPX | tours --tour=18 | hike-find | map-draw >map.html
Im unteren »DATA-Bereich« des Skripts »map-draw« (Listing 4) steht HTML-Text mit Javascript-Code, der mit dem Google-Server kommuniziert und auf diese Weise das Satellitenbild auf den Anfangspunkt des Trips zentriert und gleichzeitig die Trackkoordinaten einträgt.
Das so genannte Overlay »google.maps.Polyline« verbindet dann alle Koordinaten mit roten Linien in der in Zeile 59 festgelegten Farbe »#f9290c« , einem leuchtenden Rot. Dies alles läuft in der Javascript-Funktion »initialize()« ab Zeile 44 ab, der Browser stößt sie asynchron mit »addDomListener« an, sobald die Seite geladen wurde.
Das Perl-Skript flickt mittels des CPAN-Moduls Template::Toolkit dynamisch hereingereichte Koordinaten in den HTML-Javascript-Block, die Platzhalter »[% center %]« beziehungsweise »[% mappoints %]« ersetzt das Skript durch den ersten beziehungsweise den Array aller mittels »google.maps.LatLng« aufgemotzten Trackpunkt-Objekte.
Wer mit Googles Map-API nur spielen möchte und weniger als 1000 Abfragen am Tag abfeuert, braucht sich nicht zu registrieren, für mehr ist ein Account mit API-Key erforderlich. Wer Abbildung 7 studiert, sieht die kleinen Geradenstücke, die die diskreten Punkte des Trackers miteinander verbinden, sodass ein zittriger Pfad entsteht. Bei ganz genauem Hinsehen zeigt sich, dass Hin- und Rückweg auf demselben Pfad verlaufen und beide streckenweise leicht voneinander abweichen.
Auf und nieder
Wie viele Höhenmeter legten die Wanderer zurück? Der Graph in Abbildung 8 offenbart, dass es während der ersten Hälfte der Wanderung steil bergab ging und im zweiten Teil wieder hoch. Der Höhenunterschied belief sich auf etwa 400 Fuß, also 120 Meter. Die Wanderung ging um 9:30 Uhr morgens los, die tiefste Stelle war zur Halbzeit um 11:30 erreicht, und um 13:00 gelangten die Wanderer wieder am Einstieg an. Die Tatsache, dass die Alpinisten den Weg abwärts langsamer bewältigten als bergauf, deutet darauf hin, dass es selbst mit Treckingstöcken nicht ganz einfach ist, vereiste Serpentinen bergab zu gehen.
Listing 5 nutzt wie Listing 4 das Template-Verfahren, um einen statischen HTML-Block im »DATA« -Bereich mit Javascript und dynamisch eingeflickten Trackdaten zu beleben. Die Ausgabe des Skripts leitet der User einfach in eine HTML-Datei auf der Festplatte um und lädt diese dann im Browser. Dieser wiederum schickt die Daten an Google und der Server dort generiert den notwendigen SVG-Firlefanz, um den Graphen zu zeichnen.
Die Typdefinition für eingespeiste Werte und die Beschriftung der beiden Achsen in der farbigen grafischen Darstellung legen die Zeilen 41 und 42 mit »addColumn()« mit »Time of Day« und »Elevation« fest. Erstere ist vom Typ »timeofday« , einem Array mit Elementen für Stunden, Minuten und Sekunden. Die zweite ist vom Typ »number« , also ein einfacher Integerwert. Um den Datenpunkt um 09:04:33 Uhr mit einem Wert von 1993 Fuß über dem Meeresspiegel einzutüten, ruft das Skript folgenden Javascript-Code auf:
data.addRows([ [[9, 4, 33], 1993], [...]
Damit die Beschriftung der x-Achse die vollen Stundenwerte schön formatiert zeigt, setzt Zeile 49 die Format-Option auf »format: ‘HH:mm’« . Zeile 33 lädt nur die benötigten Javascript-Dateien für Line-Charts vom Google-Server.
Balken- und Kuchenformen bleiben außen vor, lassen sich aber bei Bedarf schnell nachladen. Wer das Skript »elevation-chart« ans Ende der Prozesskette hängt und die Ausgabe in eine Datei umleitet, auf die er dann den Browser richtet, bekommt mit geringem Aufwand eine optisch ansprechende grafische Darstellung, die sich vor allem für Webseiten eignet:
[...] hike-find | ./elevation-chart>ele.html
Da Googles Javascript-API für Charts keine Unix-Sekunden mag, wandelt Listing 5 die Datumsangaben des GPS-Emfpängers mit Hilfe des CPAN-Moduls »DateTime« in richtige Tagesdaten in den Einheiten Stunden, Minuten und Sekunden um. Mit dem CPAN-Werkzeugkasten am Gürtel gehen solche Anpassungen geschwind und zuverlässig von der Hand. Und mit technischer Unterstützung macht das Wandern im Gebirge gleich doppelt Spaß!
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/03/Perl
- Googles Chart-API: https://developers.google.com/chart/interactive/docs/quick_start?hl=en
- Googles Maps-API: https://developers.google.com/maps/?hl=en












