Ein XPath-basierter GPX-Filter

So komfortabel manche Geocaching Applikationen auch sind, manchmal stoßen sie an ihre Grenzen. Der Open Cache Manager (OCM) bietet nur begrenzte Möglichkeiten, die Caches meinen Wünschen entsprechend aufs Garmin zu spielen. Es gibt jedoch eine Lösung für Linux, die ich hier Schritt für Schritt vorstellen möchte.

Die Aufgabe

Zunächst noch einmal kurz zu der Problemstellung, die ich hier beschrieben habe: An einer GPX-Datei sollen die folgenden Änderungen durchgeführt werden:

  1. Ungelöste Mysterys, die keine Bonus-Caches und keine Challenge-Caches sind, sollen entfernt werden.
  2. Die originalen Wegepunkte von gelösten Mysterys sollen gelöscht werden.
  3. Caches, die nicht verfügbar sind, sollen durch ein vorangestelltes "(-)" im Cachenamen gekennzeichnet werden.

Der Ansatz

Es kam mir die Idee, die GPX-Datei mit einem perl-Script zu bearbeiten. Dazu muss man wissen, dass Garmins GPX-Format nichts anderes ist als reines XML.

Doch wozu das Rad neu erfinden? Bei meinen Recherchen nach Tools, mit denen man XML-Dateien manipulieren kann, bin ich auf xmlstarlet gestoßen. Dieses Command-Line-Tool bietet alles, was ich brauche. Meine Gesamtlösung lässt sich so zusammen fassen: Die GPX-Datei wird mittels einer Pipe an ein Shell-Script übergeben. Dieses Script führt die Manipulationen aus und speichert die Ergebnisdatei schließlich auf dem Garmin ab.

Schritt für Schritt Anleitung

Die folgende Beschreibung gilt für die Linux-Distribution Ubuntu, sollte sich aber problemlos auch auf andere Distributionen übertragen lassen.

Als erstes wird ein Terminal geöffnet und das Paket xmlstarlet installiert:

sudo apt-get install xmlstarlet

Jetzt muss ein Script-File erstellt werden. Bei mir liegen Scripts unter meinem User im Verzeichnis "bin". Verzeichnis anlegen, falls nicht vorhanden:

mkdir -p ~/bin

In diesem Verzeichnis wird jetzt folgendes Script unter dem Namen "filter_gpx.sh" abgelegt:

#!/bin/bash

gpx_name=$1
pipe_dir=~/pipe
pipe_file=$pipe_dir/$gpx_name
garmin_dir=/media/$USER/GARMIN/Garmin/GPX
garmin_file=$garmin_dir/$gpx_name
tmp_file=/tmp/$gpx_name.$$
pid_file=/tmp/$gpx_name.pid

if [ -e $pid_file ]; then
    pid=`cat $pid_file`
    kill `pgrep -P $pid`
    kill $pid
fi;
echo $$ > $pid_file

mkdir -p $pipe_dir
[ -e $pipe_file ] || mkfifo $pipe_file

while true ; do
    cat < $pipe_file > $tmp_file
    if [ -e $garmin_dir ]; then 
        url=`grep -m 1 -e 'xmlns:groundspeak=".*"' $tmp_file | grep -o 'http[^"]*'`

        xmlstarlet ed -N g=$url -N a=http://www.topografix.com/GPX/1/0 \
          -d "//g:type[text()='Unknown Cache' and not ( \
            contains(../g:name,'BONUS') or contains(../g:name,'bonus') or \
            contains(../g:name,'Bonus') or contains(../g:name,'CHALLENGE') or \
            contains(../g:name,'challenge') or contains(../g:name,'Challenge') or \
            starts-with(../g:name,'(*)'))]/../.." \
          -d "//a:wpt/a:name[contains(.,'-ORIG')]/.." \
          -u "//g:cache[contains(./@available,'False')]/g:name" -x "concat('(-) ',.)" \
          $tmp_file > $garmin_file
    fi;
    rm $tmp_file
done

Dieses Script muss jetzt noch ausführbar gemacht werden:

chmod +x ~/bin/filter_gpx.sh

Aufgerufen wird das Script z.B. so:

~/bin/filter_gpx.sh HomeZone.gpx &

Die Erklärung

Dem Script wird als Parameter der Name der Datei übergeben, die nachher auf dem Garmin erscheinen soll, in dem Beispiel wäre das "HomeZone.gpx".

Das Script speichert seine Prozess-ID in einer Datei ab (Zeile 9 + Zeile 16). Das ist nützlich, um eventuell noch laufende Prozesse zu beenden. Ist diese Datei vorhanden (Zeile 11), so wird sie ausgelesen, und per kill werden die Altlasten entsorgt (Zeile 12-14).

Als nächstes wird das Verzeichnis (Zeile 4 + 18) sowie die Pipe selbst angelegt (Zeile 5 + 19), falls beides noch nicht existiert. Eine erfolgreich angelegte Pipe sieht fast wie eine normale Datei aus, und das ist auch der große Vorteil.

ls -l ~/pipe/HomeZone.gpx
prw-rw-r-- 1 jens jens 0 Dez 27 22:14 /home/jens/pipe/HomeZone.gpx

Später werden wir eine *.gpx-Datei einfach unter dem Namen der Pipe abspeichern.

Nach diesen "Vorarbeiten" wird das Script in eine Endlosschleife geschickt. Jedes mal, wenn unter dem Namen der Pipe eine *.gpx-Datei abgespeichert wird, wird diese vom Script erst einmal in einer temporären Datei gespeichert (Zeile 8 + 22). Dies ist nötig, denn aus der Datei muss der verwendete XML-Namespace ausgelesen werden. OCM verwendet beim Generieren von *.gpx-Dateien den Namespace "http://www.groundspeak.com/cache/1/0", während geocaching.com "http://www.groundspeak.com/cache/1/0/1" verwendet, also noch "/1" anhängt. Das Tool xmlstarlet will den Namespace jedenfalls ganz genau mitgeteilt bekommen. Daher wird in Zeile 24 in die grep-Trickkiste gegriffen. Dadurch landet letztlich irgendwie der richtige Wert in der Variablen $url.

Kommen wir nun zum Herzstück des Scripts, die Zeilen 26-34. Das Tool xmlstarlet wird aufgerufen, und drei Filterkriterien werden definiert.

Zunächst muss man wissen, dass eine *.gpx-Datei letztlich eine Liste von "Waypoints" beinhaltet. Diese Waypoints können Caches sein, Parkplätze, oder alles andere, was Koordinaten hat.

Fangen wir in Zeile 32 an. Hier werden alle Waypoints gelöscht, bei denen im <name>-Tag die Zeichenkette "-ORIG" enthalten ist. In diesem Fall handelt es sich nämlich um die Originalkoordinaten von gelösten Mystery-Caches.

In einer *.gpx-Datei sieht das z.B. so aus:

<wpt lat="48.9259185791016" lon="9.18518257141113">
    <time>2013-12-29T17:06:33.8832890+01:00</time>
    <name>GC3MTRN-ORIG</name>
    <desc>Ursprüngliche Position - Kostbar</desc>
    <cmt>Ursprüngliche Position - Kostbar</cmt>
    <url>http://www.geocaching.com/</url>
    <sym>Reference Point</sym>
    <type>Waypoint|Reference Point</type>
</wpt>

Damit ist die zweite Forderung schon einmal erfüllt.

Kommen wir nun zu Zeile 33. Hier wird ein XML-Knoten nicht gelöscht, sondern modifiziert ("-u"). Selektiert werden alle <name>-Tags, die ein Kindelement eines <cache>-Knotens sind, bei dem das Attribut "available" den Wert "False" hat. Mit

-x "concat('(-) ',.)

wird der Inhalt dieser Knoten geändert, und zwar wird hier die Zeichenkette "(-) " vorangestellt.

Vorher sieht das z.B. so aus:

<wpt lat="48.9316673278809" lon="9.131667137146">
    <time>2011-08-26T09:00:00.0000000+02:00</time>
    <name>GC330XV</name>
    <desc>Auf dem Holzweg 6 by Bungles,
 Unknown Cache (3.5/1.5)</desc>
    <cmt>Auf dem Holzweg 6 by Bungles,
 Unknown Cache (3.5/1.5)</cmt>
    <url>http://www.geocaching.com/seek/cache_details.aspx?guid=38883c2e-8844-43ef-80f0-58d2394473ff</url>
    <urlname>Auf dem Holzweg 6</urlname>
    <sym>Geocache Found</sym>
    <type>Geocache|Unknown Cache</type>
    <groundspeak:cache id="2449710" available="False" archived="False" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0">
        <groundspeak:name>Auf dem Holzweg 6</groundspeak:name>

Und danach:

<wpt lat="48.9316673278809" lon="9.131667137146">
    <time>2011-08-26T09:00:00.0000000+02:00</time>
    <name>GC330XV</name>
    <desc>Auf dem Holzweg 6 by Bungles,
 Unknown Cache (3.5/1.5)</desc>
    <cmt>Auf dem Holzweg 6 by Bungles,
 Unknown Cache (3.5/1.5)</cmt>
    <url>http://www.geocaching.com/seek/cache_details.aspx?guid=38883c2e-8844-43ef-80f0-58d2394473ff</url>
    <urlname>Auf dem Holzweg 6</urlname>
    <sym>Geocache Found</sym>
    <type>Geocache|Unknown Cache</type>
    <groundspeak:cache id="2449710" available="False" archived="False" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0">
        <groundspeak:name>(-) Auf dem Holzweg 6</groundspeak:name>

Damit wäre auch die dritte Forderung erfüllt.

Kommen wir also zur kompliziertesten Filterregel (Zeile 27-31). Da xmlstarlet die XPath2.0-Funktion "lower-case" nicht unterstützt (oder genauer: die libxml2), bläht sich das ganze etwas auf. Selektiert werden alle Mystery-Caches (g:type[text()='Unknown Cache'), die weder "BONUS" noch "bonus" oder "Bonus" im Namen enthalten. Ein entsprechender Check wird für die Challenge-Caches gemacht. Ein Mystery-Cache, dessen Name mit "(*)" beginnt (und damit ein gelöster Mystery ist), wird ebenfalls nicht selektiert. Da die Selektion auf dem Tag <groundspeak:type> basiert, wir jedoch den kompletten <wpt>-Knoten löschen wollen, müssen wir in der XML-Hirarchie noch zwei Ebenen zurück. Deswegen ist am Ende von Zeile 31 noch "/../.." zu finden, das uns schließlich zum <wpt>-Knoten bringt. Und dieser gesamte Knoten mit all seinen Kinder-Tags wird bei Bedarf gelöscht.

Beispiel:

<wpt lat="48.9241676330566" lon="9.10999965667725">
    <time>2013-04-17T09:00:00.0000000+02:00</time>
    <name>GC4A1E8</name>
    <desc>Liebespaare in Serie - Bonus by Babs&amp;Tamm,
 Unknown Cache (2/1.5)</desc>
    <cmt>Liebespaare in Serie - Bonus by Babs&amp;Tamm,
 Unknown Cache (2/1.5)</cmt>
    <url>http://www.geocaching.com/seek/cache_details.aspx?guid=d0446ea5-6e8a-4b8c-964a-5d3a9bd642a7</url>
    <urlname>Liebespaare in Serie - Bonus</urlname>
    <sym>Geocache Found</sym>
    <type>Geocache|Unknown Cache</type>
    <groundspeak:cache id="3582277" available="True" archived="False" xmlns:groundspeak="http://www.groundspeak.com/cache/1/0">
        <groundspeak:name>Liebespaare in Serie - Bonus</groundspeak:name>
        <groundspeak:placed_by>Babs&amp;Tamm</groundspeak:placed_by>
        <groundspeak:owner id="4459429">Babs&amp;Tamm</groundspeak:owner>
        <groundspeak:type>Unknown Cache</groundspeak:type>

Dieser Cache wird also nicht gelöscht, da er die Zeichenkette "Bonus" im Namen enthält.

Die so gefilterte Datei wird schließlich auf das Garmin geschrieben ($garmin_file). Bei mir wird das Garmin unter /media/jens/GARMIN gemountet, dem entsprechend ist die Zeile 6 bzw. 7 im Script aufgesetzt.

Als letztes wird in Zeile 36 aufgeräumt und die temporäre Datei gelöscht.

Zeit für einen Test

Und los geht's. Das Script läuft ja hoffentlich schon, falls nicht, hier noch einmal das Kommando:

~/bin/filter_gpx.sh HomeZone.gpx &

Von geocaching.com habe ich mir eine Pocket-Query heruntergeladen und auch schon entzippt. Die Datei liegt bei mit unter ~/Downloads/5882367_HomeZone1000.gpx.

Als nächstes wird das Gamin per USB-Kabel angeschlossen.

Jetzt nur noch...

cp ~/Downloads/5882367_HomeZone1000.gpx ~/pipe/HomeZone.gpx

...und: Fertig. Die Caches liegen jetzt gefiltert unter "HomeZone.gpx" auf dem Garmin.

Abschlussarbeiten

Noch zwei Dinge gilt es zu verbessern. Erst einmal ist es bis jetzt nur möglich, auf diese Weise eine Datei auf dem Garmin zu speichern. Ich selbst komme in den meisten Fällen mit zwei *.gpx-Dateien auf dem Garmin aus. Die HomeZone.gpx ist immer drauf, und wenn es mal weiter weg geht, kommt noch eine "AndereGegend.gpx" hinzu. Nehmen wir also einmal an, wir wollen die Möglichkeit haben, bis zu drei Dateien auf diese Weise aufs Garmin spielen: HomeZone.gpx, AndereGegend.gpx und export.gpx.

Wir könnten zwar ausführen:

~/bin/filter_gpx.sh HomeZone.gpx &
~/bin/filter_gpx.sh AndereGegend.gpx &
~/bin/filter_gpx.sh export.gpx &

Aber es wäre des Weiteren doch schön, wenn die Scripte schon direkt beim Einloggen automatisch gestartet werden.

Da ich als Desktop-Environment Xfce verwende, gibt es dafür eine Anleitung.

Als erstes muss der Konfigurations-Dialog für Xfce geöffnet werden. Bei mir geht das über "Menü -> Einstellungen".

Dann auf "Sitzungen und Startverhalten" klicken und den Reiter "Automatisch gestartete Anwendungen" wählen, dort auf "Hinzufügen" klicken.

Auf "OK" klicken, und das ganze für die anderen Filter "HomeZone.gpx" und "export.gpx" wiederholen.

Zum Testen kann man sich jetzt einmal aus- und wieder neu einloggen.

Übrigens, das ganze jetzt aus OCM heraus zu verwenden funktioniert analog (oder doch digital?).

Was gibt's Neues?

2016-10-05 21:04

Speeding up the Vigenere Solver

As the Vigenère Solver gains more and more popularity it was time for a some face lifting behind the scenes. The speed has been improved significantly.

Weiterlesen …

2016-03-25 18:02

Neues Design

Alter Inhalt, neues Design. Mit der Navigation war ich nicht richtig zufrieden, und nachdem ich etwas über "Hover Tunnels" erfahren hatte, war klar: etwas Neues muss her. Die Umsetzung hat allerdings doch einige Zeit in Anspruch genommen, dies ist hauptsächlich der Unterstützung älterer Browser geschuldet.

Weiterlesen …

2016-01-14 22:29

Responsive Web Design

Eigentlich überfällig, aber erst jetzt bin ich dazu gekommen, die Seite für Mobil-Endgeräte benutzerfreundlicher zu machen.

Weiterlesen …