Eckpunkte der Explorer Tiles als GPX-Datei

Wie im Artikel zu den VeloViewer Explorer Tiles beschrieben, versuche ich immer mehr Gegenden zu einem möglichst großen Cluster zu verbinden. Auf Stats Hunters kann man sich dann die Karte anschauen:

Dort sieht man auch, wo noch etwas fehlt. Hier fehlt mir zum Beispiel noch ein Teil von Rheinbach. Jetzt kann ich mit Bike Router eine Route planen, die dort durch führt. Dort kann ich nämlich auch das gleiche Gitter einblenden lassen:

Die so erstellte Route kann ich dann auf mein Handy kopieren und mich per OsmAnd routen lassen. Das klappt ganz gut, aber ich habe unterwegs dann keine Möglichkeit in OsmAnd zu sehen, welche Kacheln ich jetzt eigentlich schon eingesammelt habe.

Explorer Helper App

Es gibt noch eine weitere App dafür, die Explorer helper for VeloViewer. Die scheint die Kacheln aus der VeloViewer App zu synchronisieren. Auch ohne Konto bei VeloViewer kann man sich dann aber auch die Kacheln anzeigen lassen, die man während der aktuellen Fahrt aufgenommen hat. Das ist schon einmal hilfreich, jedoch muss man sich dann merken, welche Kacheln man noch braucht, und welche man schon hat. Beim Start der App sieht das dann so aus:

Während der Fahrt werden die Kacheln dann grau. Die Karte von Mapbox ist leider für die Orientierung nicht so gut geeignet wie die Open Street Map, und zudem kann ich mich auch nicht in dieser App navigieren.

Schön wäre, wenn man das dieses Liniengitter noch in OsmAnd importieren könnte. Und dann noch die schon erledigten Kacheln markiert wären. Mal schauen, vielleicht kann man da ja etwas basteln. Schließlich gibt es die Möglichkeiten Wegpunkte und Linien per GPX zu importieren. Vielleicht kann man über andere Formate wie KML/KMZ oder GeoJSON auch noch Polygone importieren. Dann brauche ich nur noch ein Skript, das diese Dateien erzeugt.

Ich brauche hier ein paar Dinge:

  1. Zuerst muss ich die Definition der Kacheln finden. Dann kann ich von dort aus die Eckpunkte aller Kacheln rechnerisch erzeugen.
  2. Aus einem Export meiner Aktivitäten von Strava rechne ich aus, welche Kacheln ich eigentlich schon habe. Somit kann ich die in der Datei dann entsprechend markieren.
  3. Um die Kacheln, die ich schon habe, erzeuge ich einen Rand. Und dann noch ein paar Ebenen weiter, damit sich das ganze auch lohnt.
  4. Ich schaue, ob ich nur die Eckpunkte als GPX erzeugen kann, oder ob ich auch irgendwie schicker die Flächen erzeugen kann.

Definition der Kacheln

Eine Definition der Kacheln findet sich im Open Street Map Wiki. Dort wird auf den Wikipedia-Artikel zur Web-Mercator-Projektion verwiesen. Und die funktioniert so, gegeben geografische Breite $\phi$ und Länge $\lambda$. Zuerst transformiert man sie in planare Koordinaten: $$ x := \lambda\,, \qquad y := \sinh^{-1}(\tan(\phi)) \,. $$

Dann werden die Koordinaten noch normiert und zentriert: $$ x := \frac{1 + \frac x\pi}2\,, \qquad y := \frac{1 − \frac y \pi}2 \,. $$

Die Anzahl der Kacheln pro Zoomstufe $z$ sind $n := 2^z$. Die Kacheln bei VeloViewer Explroer Tiles sind bei $z = 14$ definiert. Somit haben wir dort $2^{14} = 16\,384$ Kacheln in der Höhe und Breite.

Man muss die Koordinaten $x$ und $y$ nun mit diesem $n$ multiplizieren und dann abrunden. Dann bekommt man die Kachelnummer.

Daraus ergibt sich dann dieser Test mit einer Kachel aus Rheinbach:

def test_rheinbach() -> None:
    lat, lon = 50.6202, 6.9504
    assert compute_explorer_tile(lat, lon) == (8508, 5512)

Diesen Test können wir mit der Implementierung der Rechenvorschrift erfüllen:

def compute_explorer_tile(lat: float, lon: float) -> Tuple[int, int]:
    x = np.radians(lon)
    y = np.arcsinh(np.tan(np.radians(lat)))
    x = (1 + x/np.pi)/2
    y = (1 - y/np.pi)/2
    n = 2**14
    return int(x * n), int(y * n)

Die Rückrichtung ist etwas schwerer, weil man da den atan hat. Glücklicherweise gibt es schon Beispielcode dafür, den ich dann übernommen habe.

Nun haben wir Code, mit dem Koordinaten (in Grad) in Kachelnummern umgerechnet werden können.

Erzeugen eines Punktegitters

Nun ist es eigentlich relativ einfach, ich muss nur noch das Gitter durchgehen. Ich kann auf die Karte bei Stats Hunters schauen und sehe dort, dass ich alles von Kachel 8499-5488 bis 8540-5530 gerne nehmen möchte. Dann kann ich das durchgehen und einfach eine GPX-Datei damit erzeugen.

Diese ist auch schnell in OsmAnd importiert und schon habe ich die Eckpunkte dort:

Wenn man allerdings weiter aus der Karte geht, wird es schnell richtig unübersichtlich:

Vielleicht wäre es cleverer, wenn man da anstelle von Punkten lieber kleine quadratische Tracks macht. Und damit sieht es dann viel besser aus, wenn man die Start- und Zielmarkierungen deaktiviert:

Stellt man die Farbe noch um, so kann man gut sehen, durch welche Kacheln die rote Track-Aufzeichnungs-Linie verläuft. Damit kann man dann besser sehen, welche Tracks man schon hat. Man kann die Explorer Helper App dann noch parallel nutzen, aber man braucht sie nicht mehr unbedingt. Damit habe ich mir eine App gespart, und muss nicht immer zwischen Kacheln und Navigation wechseln.

Erledigte Kacheln entfernen

Jetzt wäre es natürlich noch toll, wenn man die erledigten Kacheln entfernen könnte. Da wird es dann schon deutlich mehr Aufwand zu programmieren, weil ich ja jetzt die Information brauche, wo ich schon einmal war.

Aktuell habe ich alle meine Aktivitäten auf Strava, weil ich die nicht mehr selbst verwalten mag. Das ist ein bisschen eine Feudalabhängigkeit, aber ich lade so alle sechs Wochen einmal alle meine Dateien herunter. Somit habe ich, auch ohne deren API zu nutzen, meine Daten in deren Format auf meinem Rechner liegen. Das sieht dann so aus:

In dem Ordner activities sind dann diverse GPX- und FIT-Dateien drin, je eine pro Aktivität:

In der Datei activities.csv sind auch alle Aktivitäten mit ihren Metadaten aufgelistet. Diese Datei kann ich mit Pandas einlesen, die GPX-Dateien mit gpxpy und die FIT-Dateien mit fitparse oder der Neuimplementierung fitdecode. Daraus kann ich dann Objekte bauen, wie ich sie gut gebrauchen kann. So habe ich meine Daten zwar weiterhin bei Strava und in deren Format, habe meinen Code aber dagegen isoliert. Das erscheint mir ganz praktisch. Vielleicht koppele ich es irgendwann auch gegen die API, dann habe ich die Daten sozusagen direkt live.

Um die schon erkundeten Kacheln zu erhalten, muss ich jede Aktivität einlesen. Dann jeden Punkt daraus nehmen, die zugehörige Kachel berechnen. Parallel erstelle ich eine Liste mit allen Kachel-Nummern. Am Ende habe ich dann die Liste aller erkundeten Kacheln. Das ganze hat nur den Nachteil, dass ich, sobald neue Dateien da sind, sie wieder komplett neu einlesen muss. Eigentlich hätte ich hier gerne Stream Processing, habe damit aber bisher noch nichts gemacht. Ich habe immer nur dieses Batch Processing gemacht.

Den Prozess sollte man in mehrere Schritte zerlegen:

  1. Zuerst die Dateien in normalisieren, also in ein Format meiner Wahl überführen. Ich möchte hier Pandas Dataframes haben, also eine Tabelle mit den Spalten Zeit, Länge und Breite. Mit diesen kann ich dann viele tolle Dinge tun. Wenn ich eine FIT-Datei einlese, kann es noch eine zusätzliche Spalte Herzfrequenz geben. Die brauche ich aktuell nicht, aber vielleicht später einmal.
  2. Für jede Datei rechne ich zu jedem Punkt aus, zu welcher Kachel er gehört. Und dann kann ich das reduzieren auf die eindeutigen Kacheln. Somit habe ich für jede Aktivität immer die erste Zeit, bei der eine Kachel betreten worden ist.
  3. Diese Kachelbetretungen kann ich dann zusammennehmen und habe daraus die Besuche aller Kacheln. Auch hier kann ich pro Kachel wieder auf die erste Betretung reduzieren und habe so eine globale Zeitabfolge der Kachelbetretungen.

Diese Schritte lassen sich im Konzept von map-reduce perfekt einfügen. Die ersten beide Schritte sind ein Abbildung (map), sie bilden also einen Datensatz in einen anderen Datensatz ab. Das dritte ist eine Reduktion (reduce), die viele Datensätze auf einen abbildet. Der zweite Schritt enthält auch ein bisschen Reduktion, das könnte man noch in eine Abbildung (Punkte zu Kacheln) und dann eine Reduktion (pro Kachel die erste Zeit) auftrennen. Das habe ich dann so implementiert, dass die Zwischenschritte einzelne Dateien erzeugen, somit geht das neue Durchlaufen dann ziemlich schnell.

Mit einer Liste der Kacheln kann ich jetzt schnell weiterkommen. Ich habe die Liste an Kacheln in ein NumPy Array konvertiert, das als Kantenlänge $2^{14}$ Elemente hat. Dann habe ich jene Elemente auf 1 gesetzt, die Kacheln entsprechen, die ich schon besucht habe. Da ich ja den Rand ausweiten möchte, habe ich eine Dilation in SciPy genutzt. Dann habe ich meine schon besuchten Kacheln wieder abgezogen. Es bleibt ein Halo. Und aus jeder Kachel in dem Halo kann ich jetzt wieder ein Segment erzeugen, die ich dann in einer GPX-Datei sammele. Auf OsmAnd übertragen sieht das dann so aus:

Das gefällt mir sehr gut! So habe ich eine Übersicht der Kacheln, die mir in meinem aktuellen Strava Checkout noch fehlen. Um das ganze zu aktualisieren, müsste ich immer einen neuen Checkout anfordern, das geht maximal einmal die Woche. Aber ich könnte mir zum einen einfach merken, welche Kacheln ich schon habe, das ist weniger Aufwand als immer komplett zu schauen. Und ich kann das ganze noch mit der Strava API verbinden, sodass ich das dann auch schneller aktualisiert habe.

Das sollte jedenfalls ziemlich hilfreich rein, damit ich besser noch fehlende Kacheln sammeln kann. Bei einem Praxistest werde ich da noch mehr erfahren.

Das Skript gibt es auf GitHub, ist allerdings noch nicht sehr aufgeräumt. Wenn da Interesse besteht, kann ich das gerne noch weiter angehen.