Fakultät für Elektrotechnik und Informatik Institut für Praktische Informatik Fachgebiet Datenbanken und Informationssysteme Routenberechnung mit partitionierten Straßendaten Bachelorarbeit im Studiengang Informatik David Bormann Matrikelnummer: 2739920 Prüfer: Prof. Dr. Udo Lipeck Zweitprüfer: Dr. Hans Hermann Brüggemann Betreuer: M. Sc. Hendrik Warneke Juli 2012 Inhaltsverzeichnis 1 Einleitung 4 1.1 Motivation und Aufgabenstellung . . . . . . . . . . . . . . . . . . . . . . 4 1.2 Gliederung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2 Grundlagen 6 2.1 Die Open-Streetmap-Daten . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.2 Das Simple-Feature-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2.1 Geometrieschema . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2.2 Topologische Prädikate und Geometriefunktionen . . . . . . . . . 9 Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3.1 Graphentheorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3.2 Der abstrakte Datentyp Graph . . . . . . . . . . . . . . . . . . . 11 Kürzeste-Wege-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.4.1 Der Bellman-Ford-Algorithmus . . . . . . . . . . . . . . . . . . . 13 2.4.2 Der Dijkstra-Algorithmus . . . . . . . . . . . . . . . . . . . . . . 14 2.4.3 Der A*-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . 16 Schlussfolgerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.3 2.4 2.5 3 Konzepte 19 3.1 Partitionierungsarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.2 Ablauf der partitionierten Routenberechnung . . . . . . . . . . . . . . . . 21 3.3 Routensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.3.1 Heuristiken zur Findung von Zielknoten . . . . . . . . . . . . . . 23 3.3.2 Metriken zur Gewichtung der Kanten . . . . . . . . . . . . . . . . 24 2 INHALTSVERZEICHNIS 3.3.3 3.4 3 Vorgehen beim Misserfolg . . . . . . . . . . . . . . . . . . . . . . 24 Komposition der Routen . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4 Implementierung 27 4.1 Anforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.2 OpenJUMP und seine Schnittstelle . . . . . . . . . . . . . . . . . . . . . 27 4.3 Beschreibung der Benutzerschnittstelle . . . . . . . . . . . . . . . . . . . 29 4.4 Finden von Koordinaten über Adressangabe . . . . . . . . . . . . . . . . 31 4.5 Umgang mit den WGS84-Koordinaten . . . . . . . . . . . . . . . . . . . 31 4.6 Aufbau des Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 4.6.1 Ermittlung von Kanten aus den OSM-Daten . . . . . . . . . . . . 32 4.6.2 Clipping der Kanten in einer Partition . . . . . . . . . . . . . . . 33 4.7 Klassenbeschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 4.8 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 5 Experimente 48 5.1 Die Experimentierumgebung . . . . . . . . . . . . . . . . . . . . . . . . . 48 5.2 Das Vorgehen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 5.3 A* versus Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 5.4 Große versus kleine Partitionen . . . . . . . . . . . . . . . . . . . . . . . 53 5.5 Unterschiede der Metriken . . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.6 Unterschiede der Heuristiken . . . . . . . . . . . . . . . . . . . . . . . . . 56 5.7 Abhängige versus unabhängige Partitionierung . . . . . . . . . . . . . . . 59 5.8 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 6 Ausblick 64 A Das simple Osmosis-PostGIS-Schema 65 B PL/pgSQL-Funktionen 66 B.1 Funktionen zum Erstellen der Edges-Tabelle . . . . . . . . . . . . . . . . 66 B.2 Funktionen zum Finden von Start- und Zielknoten . . . . . . . . . . . . . 69 B.3 Funktionen zum Graphaufbau . . . . . . . . . . . . . . . . . . . . . . . . 72 Kapitel 1 Einleitung Dieses Kapitel dient dazu, einen Überblick über das Thema dieser Arbeit zu geben. Es wird die Aufgabenstellung beschrieben und eine Gliederung der weiteren Kapitel angegeben. 1.1 Motivation und Aufgabenstellung Die für Fahrzeug- oder Fußgängernavigation benötigten Wegenetze werden üblicherweise aus geographischen Daten gewonnen. Diese werden in einen gewichteten Graphen transformiert, in dem Kanten Wege darstellen und Knoten Kreuzungen dieser. Mit Hilfe verschiedener Algorithmen kann in diesem Graphen der kürzeste Pfad zwischen zwei Knoten ermittelt werden. Dies basiert auf einer Gewichtung der Kanten, beispielsweise aus der Entfernung zwischen zwei Knoten. Nun können diese Wegenetze allerdings sehr groß werden und den kompletten Graph zu speichern, könnte die Arbeitsspeicherkapazitäten eines Rechners sprengen, zum Beispiel die von mobilen Kommunikationsgeräten oder mobilen Servicerobotern. In diesem Fall kann der Graph, beziehungsweise dessen Ausgangsmaterial aus geographischen Daten, in kleinere Stücke zerlegt werden, um diese dann vollständig im Arbeitsspeicher abzulegen. In dieser Arbeit geht es um die Routenplanung mit Straßendaten, die vorab partitioniert worden sind. Für die Berechnung einer Route über mehrere Partitionen, werden die einzelnen Ergebnisse einer Wegfindung auf einer Partition zusammengesetzt. Durch Experimente soll bestimmt werden, inwiefern ein solches Ergebnis von der optimalen Route abweicht. Dabei sollen verschiedene Algorithmen, sowie Partitionierungsgrade und -arten getestet werden. Die erforderlichen geographischen Daten stammen aus dem Open-Streetmap-Projekt und werden in einer PostGIS-Datenbank abgelegt. 4 KAPITEL 1. EINLEITUNG 1.2 5 Gliederung In Kapitel 2 werden zuerst Open-Streetmap und die Struktur der von ihr bereitgestellten Daten skizziert. Es werden graphentheoretische Begriffe diskutiert und verschiedene Algorithmen zur Wegfindung vorgestellt. In Kapitel 3 wird der Ablauf der partitionierten Routenberechnung erläutert. Dazu gehört das Laden von Graphpartitionen, dann wird auf diesen die Wegesuche ausgeführt und anschließend werden die Routen der einzelnen Partitionen zusammengeführt. Das Kapitel 4 beschäftigt sich mit der Implementierung des Graphaufbaus, verschiedenen weiteren Implementierungsdetails und einer Beschreibung der Benutzerschnittstelle des Programms. Das nachfolgende Kapitel 5 beschreibt die ausgeführten Experimente und ihre Ergebnisse. Das Kapitel 6 bietet einen Ausblick, was man bei der partitionierten Routensuche noch ändern oder verbessern könnte. Im Anhang A ist das verwendete Datenbankschema zur Verwendung der Open-StreetMap-Daten dargestellt. Im Anhang B finden sich entwickelte PL/pgSQL-Funktionen. Kapitel 2 Grundlagen In diesem Kapitel wird ein Überblick über das Open-Streetmap-Projekt gegeben, sowie über den Aufbau der Daten, die von Open-Streetmap zur Verfügung gestellt werden. Es folgen notwendige Begriffserklärungen der Graphentheorie und der Aufbau eines Abstrakten Datentyps eines Graphen. Anschließend werden mehrere Algorithmen vorgestellt, die kürzeste Wege von einem Startknoten aus finden. 2.1 Die Open-Streetmap-Daten Open-Streetmap (im Folgendem mit OSM abgekürzt) ist eine freie Weltkarte, an der jeder mitarbeiten kann, sie zu komplettieren.1 Sie wurde 2004 in Großbritannien ins Leben gerufen. Mitglieder der OSM-Community zeichnen ihre Bewegung mit einem GPSTracker auf und fügen den geographischen Daten anschließend weitere Informationen hinzu. Dies können Straßenart und -namen und angrenzende Points of Interests, beispielsweise Gebäude oder Parkplätze, aber auch Briefkästen und Bushaltestellen sein. Eine andere Möglichkeit OSM-Karten zu erstellen, ist das Abzeichnen von Luftbildern. Weitere Information zu OSM kann man in [RT09] finden. Das Datenmodell von OSM enthält zwei für das Routing wichtige Typen Ways und Nodes. Des Weiteren gibt es noch Relations, die aus Ways und Nodes bestehen und so komplexere Zusammenhänge darstellen können, zum Beispiel den Grundriss eines Gebäudes, Straßen sind allerdings keine Relationen. Ein Node besteht aus seiner geographischen Länge und Breite und, wie die anderen Objekte auch, aus beliebig vielen Attributen, sogenannte Tags. Das sind Schlüssel-Wert-Paare, die dabei helfen, Objekte genauer zu klassifizieren und so zum Beispiel eine Autobahn und einen Fußweg auf einer Karte verschieden darzustellen. Alle Straßen erhalten den Schlüssel highway, ein Fußweg 1 http://www.openstreetmap.org, steht unter der CC-BY-SA 2.0-Lizenz 6 KAPITEL 2. GRUNDLAGEN 7 erhält dann den Tag highway=footway, eine Autobahn highway=motorway. Es gibt aber auch Tags wie oneway=yes, an denen man Einbahnstraßen erkennen kann. Ein Way besteht zusätzlich noch aus mehreren Nodes, wobei es auf die Reihenfolge ankommt, in der die Nodes abgelegt sind. Seit 2009 darf ein Way- oder Nodeobjekt keine zwei Tags mit dem selben Schlüssel besitzen. Abbildung 2.1: EER-Diagramm zu Node, Way und deren Tags In Abbildung 2.1 sind diese Zusammenhänge als EER-Diagramm dargestellt. Für das Routing irrelevante Attribute sind nicht aufgenommen. Zum Beispiel welcher User eine Kante oder einen Knoten zu welchem Zeitpunkt hinzugefügt oder verändert hat. Die Attribute lat und lon bestimmen die geographische Lage eines Nodes, die geographische Breite lat und Länge lon. Die Koordinaten sind in der Form des WGS84 (World Geodetic System 1984) gespeichert. Die OSM-Daten von Deutschland, Stand 06.06.2012, wurden mit Hilfe von Osmosis2 , einem Bearbeitungswerkzeug für OSM-Daten, in eine PostGIS-Datenbank mit dem simplen OSM-PostGIS -Schema, siehe Anhang A überführt. Wichtig für den Aufbau des Graphen sind dabei folgende Relationen und Attribute: NODES NODE TAGS WAYS WAY TAGS WAY NODES 2 (Id, Geom) (Node Id → NODES, K, V) (Id, Linestring) (Way Id → WAYS, K, V) (Way Id → WAYS, Sequence Id, Node Id → NODES) http://wiki.openstreetmap.org/wiki/DE:Osmosis, Version 0.40.1 KAPITEL 2. GRUNDLAGEN 8 Die geographische Lage wird zusammengefasst in dem geographischen Attribut Geom Das Attribut Linestring stellt einen Linienzug über eine geordnete Menge von Koordinaten dar. Es gilt die Integritätsbedingung, dass die sequence id von 0 beginnend, fortlaufend für jeden Way ist. Mit Hilfe der sequence id lässt sich ein Linienzug über die Nodes eines Ways konstruieren. Die Relation bilden aus dem EER-Diagram wird durch WAY NODES umgesetzt. Dass die Karten vornehmlich von Hobbykartographen erstellt werden, bedeutet allerdings auch, dass ein OSM-Datensatz nicht unbedingt vollständig sein muss, beziehungsweise Details fehlen können, die für ein Routing benötigt würden. Dies kann der Fall sein, wenn man eine Route von einem Startpunkt, von dem nur die Adresse bekannt ist, aus sucht, diese Adresse aber nicht in den OSM-Daten verzeichnet ist. Im Normalfall sollte allerdings die Postleitzahl und der Straßenname ausreichen, um einen Start- oder Endpunkt fest zu legen. 2.2 2.2.1 Das Simple-Feature-Modell Geometrieschema Das Modell, in dem die OSM-Daten vorliegen, ist das Simple-Feature-Modell 3 . In diesem werden Datentypen und räumliche Operationen für zweidimensionale Geometrien definiert[Bri08]. Die Geometrien sind deswegen simpel, da ihre Stützpunkte nur gradlinig verbunden sind. Es gibt vier Geometrieformen des Typs Geometry in dem Modell, nämlich Punkte, Linien, Flächen und Geometriesammlungen. Punkte werden durch ein Point-Objekt beschrieben, dem eine Koordinate zugeordnet ist, auf deren Werte man mittels getX(), beziehungsweisse getY() zugreifen kann. Ein OSM-Node wird so dargestellt. Linien sind Streckenzüge, die durch eine Folge von Stützpunkten definiert sind. Man kann auf den Anfangs- und Endpunkt, die Länge und den n-ten Stützpunkt zugreifen. Durch diese LineStrings werden OSM-Ways und somit unter anderem Straßen in OSM dargestellt. Flächen werden durch Polygone beschrieben. Ein Polygon besteht aus einem geschlossenen äußeren Streckenzug und beliebig vielen inneren, die Löcher beschreiben. In OSM werden Polygone durch Relationen dargestellt, zum Beispiel durch das Relationentag area und mit Verweis auf eine Menge von OSM-Ways, die den Tag outer besitzen, wird der äußere Ring eines Polygons beschrieben. 3 ISO 19125 KAPITEL 2. GRUNDLAGEN 9 Eine Geometriesammlung ist eine Sammlung verschiedener Geometrien. Man kann bei einer GeometryCollection auf die verschiedenen Geometrien zugreifen und falls alle Geometrien vom gleichen Typ sind, kann man eine der Unterklassen MultiPoint, MultiLineString oder MultiPolygon verwenden. Abgespeichert werden die Geometrien als well-known Binary (WKB) und können für Menschen lesbar gemacht werden als well-known Text (WKT). Beispiele für WKT sind: POINT(6.2 19.90) oder LINESTRING(1 3, 3 7, 4 2). 2.2.2 Topologische Prädikate und Geometriefunktionen Topologische Eigenschaften beschreiben die räumliche Beziehung von Geometry-Objekten zueinander. Ob eine Geometrie vollständig oder nur zum Teil in einer anderen enthalten ist oder diese berührt wird, wird mit Hilfe von topologischen Prädikaten ausgedückt. • contains(Geometry other) - ist true, wenn die Geometrie other komplett in dieser Geometrie enthalten ist • crosses(Geometry other) - ist true, wenn die beiden Geometrien eine Schnittmenge haben, aber eine Geometrie nicht komplett in der anderen enthalten ist • touches(Geometry other) - ist true, wenn die beiden Geometrien sich nur an ihren Rändern schneiden Weitere geometrische Funktionen einer Geometrie sind: • Envelope(): Geometry - Rückgabe ist das minimal umgebende Rechteck der Geometrie • Boundary(): Geometry - zurück gegeben wird der Rand der Geometrie • Distance(Geometry other): double - es wird der geringste Abstand zwischen zwei Punkten der Geometrie und other berechnet und zurück gegeben • Intersection(Geometry other): Geometry - gibt eine Geometrie zurück, die die Schnittmenge der Geometrie und other beschreibt • Difference(Geometry other): Geometry - gibt eine Geometrie zurück, die die Geometrie ohne die Schnittmenge der Geometrie und other beschreibt KAPITEL 2. GRUNDLAGEN 2.3 2.3.1 10 Graphen Graphentheorie Ein Graph besteht aus Knoten, die über Kanten verbunden sein können. Oder formal ausgedrückt, ein Graph G ist ein Tupel (V,E), welches aus zwei endlichen Mengen V 6= ∅ (Vertices für die Knoten) und E (Edges für die Kanten) besteht. Die Kanten in einem gerichteten Graphen sind zweistellige Relationen der Knoten, E ⊆ V × V , zum Beispiel die Kante e=(a, b) verbindet die beiden Knoten a und b. Diese sind die Endpunkte von e. Die Knoten a und b sind damit adjazent und diese Knoten mit e inzident. Der Grad eines Knoten v ist die Anzahl der mit v inzidenten Kanten. Eine Schleife ist eine Kante, die einen Knoten mit sich selbst verbindet. In Abbildung 2.2 ist ein gerichteter Graph dargestellt. Ein Graph heißt dünn besetzt, wenn |E| = Θ(|V|).[Jun94] d e b a c Abbildung 2.2: Ein gerichteter Graph Bei einem gewichteten Graphen werden alle Kanten mit einer Zahl, der Länge dieser Kante, gewichtet. Das muss nicht unbedingt der geographische Abstand zwischen zwei Knoten sein, sondern kann auch als Kosten, Zeiten oder Kapazitäten genutzt werden und darf auch negativ sein. Ein Weg besteht aus einer Menge von Knoten, die jeweils adjazent sind und ein Pfad ist ein spezieller Weg, bei dem kein Knoten doppelt auftreten darf. Ein Zyklus in einem Graph ist ein Weg, in dem ein Knoten Start- und Zielknoten zugleich darstellt. Die Länge eines Weges ist dann die Summe der Gewichte der Kanten des Weges. Der Abstand d(a,b) der Knoten a und b, oder der kürzeste Weg von a nach b, ist das Minimum der Längen aller Pfade mit Startpunkt a und Endpunkt b, b ist dann von a aus erreichbar. Falls es keinen Pfad von a nach b gibt, wird d(a,b) angenommen als ∞. Ein Weg, der nur einen Knoten enthält, hat die triviale Länge 0. In Abbildung 2.3 ist der kürzeste Weg vom Knoten e zum Knoten d markiert. Falls es in dem Graphen einen Zyklus gibt, dessen Länge negativ ist, kann es keinen kürzesten Weg geben. Zumindest nicht, wenn man einen kürzesten Weg von a nach b sucht und man einen solchen Zyklus von a aus erreichen kann. In den folgenden Betrachtungen seien negative Kantengewichte ausgeschlossen. KAPITEL 2. GRUNDLAGEN 1 11 d b 0.5 3 e 1 1 2 a c 4 Abbildung 2.3: Ein kürzester Weg zwischen zwei Knoten 2.3.2 Der abstrakte Datentyp Graph Der abstrakte Datentyp, kurz ADT, Graph benutzt die ADTs Vertex und Edge. Diese dienen lediglich dazu die Knoten und Kanten darzustellen und stellen, außer einem Konstruktor, nur Anfrage- und Änderungsmethoden bereit, die für die Bestimmung des kürzesten Weges hilfreich sind. Der ADT Edge stellt folgende Methode bereit: • getId(): String - liefert die OSM-Id dieser Kante • getWeight(): double - liefert das Gewicht einer Kante • getLineString(): LineString - gibt den Streckenzug der Kante als Geometrie zurück Im ADT Vertex wird vermerkt, welchen Abstand ein Knoten zu einem Startknoten hat, beziehungsweise welcher Abstand bisher geschätzt wurde und welcher Knoten in einem kürzesten Weg sein Vorgänger ist: • getId(): String - liefert die OSM-Id dieses Knotens • setPred(Vertex v) - setzt den Vorgänger von diesem Knoten auf v • getPred(): Vertex - gibt den Vorgänger dieses Knotens zurück • setD(double distance) - setzt den Abstand von dem Startknoten zu diesem auf distance • getD(): double - gibt den Abstand zum Startknoten zurück • getPoint(): Point - gibt die geometrische Lage diese Knotens als Geometrie zurück • markAsTarget(): - markiert den Knoten als Ziel einer Wegesuche • isTarget(): boolean - gibt true zurück, falls der Knoten ein Zielknoten ist KAPITEL 2. GRUNDLAGEN 12 Der ADT Graph besitzt folgende Methoden. Eine Sequence ist eine verkettete Liste mit Indexzugriff. Ein Großteil der Methoden entstammt [Lip12]; • size(): int - liefert die Anzahl der Knoten dieses Graphen • vertices(): Sequence - liefert die Knoten dieses Graphen • insertVertex(Vertex v) - fügt den Knoten v in den Graphen ein • removeVertex(Vertex v) - entfernt den Knoten v und seine inzidenten Kanten aus dem Graph • edges(): Sequence - liefert die Kanten dieses Graphen • insertEdge(Vertex v, Vertex w, Edge e) - fügt die gerichtete Kante e zwischen den Knoten v und w ein • removeEdge(Edge e) - entfernt die Kante e aus dem Graphen • inDegree(Vertex v): int - liefert die Anzahl der eingehenden Kanten von v • inIncidentEdges(Vertex v): Sequence - liefert die eingehenden Kanten vom Knoten v • inAdjacentVertices(Vertex v): Sequence - liefert die Knoten, die über eine eingehende Kante zum Knoten v adjazent sind • areAdjacent(Vertex v, Vertex w): boolean - gibt true zurück, falls v über eine Kante mit w verbunden ist • origin(Edge e): Vertex - liefert den Knoten, von dem die Kante e ausgeht • destination(Edge e): Vertex - liefert den Knoten, wohin die Kante e führt • findEdge(Vertex v, Vertex w): Edge - liefert die Kante, über die die Knoten v und w verbunden sind • getVertexWithId(String id): Vertex - liefert die Knoten mit der übergebenen id, sofern vorhanden • containsVertexWithId(String id): boolean - liefert true, falls ein Knoten mit der dieser id im Graphen existiert Die Methoden outDegree(), outIncidentEdges() und outAdjacentVertices() werden analog definiert. Die Methode findEdge() hat für beliebige Graphen eine Laufzeit von O(|E|). Da ein Straßennetz in der Regel dünn besetzt ist, Knoten höchstens den Grad 4 haben und ein- und ausgehende Kanten eines Knoten mit Indizenzlisten für jeden Knoten festgehalten werden können, beträgt die Laufzeit in diesem Fall O(1). KAPITEL 2. GRUNDLAGEN 2.4 13 Kürzeste-Wege-Algorithmen Man unterscheidet zwischen einer kürzesten Wegfindung von einem Startknoten aus und zwischen allen Knoten. Manche Algorithmen kann man abbrechen, falls man einen kürzesten Weg zu einem gesuchten Zielknoten gefunden hat. Die Initalisierung ist bei allen Algorithmen gleich. Man benötigt einen Graph G und einen Startknoten, Vertex s, siehe Algorithmus 1. Dieser ist aus [CLRS10] entnommen, wie auch die Algorithmen 2 und 3. Die Distanz zum Startknoten wird für alle Knoten auf Algorithm 1 1: procedure Init-Single-Source(Graph G, Vertex s) 2: for all Vertex v ∈ G.vertices() do 3: v.setD(∞); 4: v.setPred(NULL); 5: end for 6: s.setD(0); 7: end procedure den maximal zulässigen Wert gesetzt, bis auf die triviale Distanz vom Startknoten s zu s auf 0. Da jeder Knoten so initalisiert wird, hat die Methode die Laufzeit O(|V|). Wird die Routensuche nur einmal auf einen Graphen angewandt, kann schon bei der Initalisierung der Knoten die Distanz auf ∞ gesetzt werden, wodurch die Laufzeit konstant wäre. Die folgenden Algorithmen berechnen alle kürzesten Wege von einem Knoten zu prinzipiell allen anderen. 2.4.1 Der Bellman-Ford-Algorithmus Der Bellman-Ford-Algorithmus würde auch ein Ergebnis bei negativen Kanten liefern, hat aber eine Laufzeit von O(|E||V |). Er wird im Algorithmus 2 beschrieben. Da es nur positve Kanten gibt, wird auf die Darstellung des Programmcodes, der prüft, ob der Graph einen Zyklus mit negativer Länge enthält, verzichtet. Die Zeilen 6-9 sind bei allen Algorithmen ähnlich und beschreiben die Relaxation einer Kante. Dabei wird geprüft, ob sich der geschätzte Abstand vom Startpunkt zum Endknoten einer Kante verringert, falls man diese Kante in den kürzesten Weg mit aufnehmen würde. Wenn dies der Fall ist, wird der neue Schätzwert vermerkt und der Startknoten der Kante wird zu dem Vorgänger des Endknotens in diesem geschätzten kürzesten Weg. Dies wird für alle Kanten |V|-1 mal gemacht Der Algorithmus arbeitet korrekt, falls der Graph keinen Zyklus mit negativer Länge enthält. Denn dann kann ein kürzester Weg höchstens aus |V|-1 Kanten bestehen, da KAPITEL 2. GRUNDLAGEN 14 Algorithm 2 1: procedure Bellman-Ford(Graph G, Vertex s) 2: Init-Single-Source(G,s); 3: int newDist=0; 4: for i=1 to G.size()-1 do 5: for all Edge e ∈ G.edges() do 6: newDist=G.origin(e).getD()+e.getWeight(); 7: if G.destination(e).getD()>newDist then 8: G.destination(e).setD(newDist); 9: G.destination(e).setPred(G.origin(e)); 10: end if 11: end for 12: end for 13: end procedure ein Zyklus mit positiver Länge einen Weg nur verlängert. Wenn es einen kürzesten Weg von s zu einem Knoten gibt, dann kann man diesen erhalten, indem man die Kanten in der Reihenfolge dieses Weges sukzessiv relaxiert. Pro äußeren Schleifendurchlauf, werden alle Kanten relaxiert. Da dies |V|-1 mal passiert, relaxiert man unter anderem auch die Kanten eines kürzesten Weges mit |V|-1 Kanten. Daher die Laufzeit O(|E||V|). 2.4.2 Der Dijkstra-Algorithmus Djikstras Algorithmus, der Algorithmus 3, kann nur auf einen Graphen mit positiven Kantengewichten ausgeführt werden, läuft dafür aber auch schneller, bei geeigneter Implementierung der PriorityQueue Q, in O((|V|+|E|)log|V|). Die Idee hinter Djkstras Algorithmus ist, dass in einer Menge von Knoten, der cloud, vermerkt wird zu welchen Knoten kürzeste Wege schon bekannt sind. Dies wird erreicht, in dem der Knoten mit der kürzesten Länge zur cloud in eben diese aufgenommen wird, siehe die Zeilen 8 und 9. Dann werden alle Kanten, die von diesem Knoten ausgehen, relaxiert. Das funktioniert, da es nur positive Kantengewichte gibt und so ein aktuelles Minimum einer Distanz zur cloud nicht nachträglich unterschritten werden kann. Insgesammt wird jede Kante nur genau einmal relaxiert. In Q werden die noch nicht in die cloud aufgenommenen Knoten nach ihrer bisher berechneten Länge des kürzesten Weges aufsteigend geordnet. Dabei muss ein Knoten nach einer erfolgreichen Relaxation einer eingehenden Kante eventuell neu positioniert werden, siehe Zeile 15 replaceKeyAt(). Q.poll() gibt den Knoten mit kleinster Distanz zurück und löscht diesen aus Q. Die Implementierung dieser Methoden, beeinflusst die Laufzeit des Dijkstra-Algorithmus. Wird die Priorityqueue mit einem Min-Heap realisiert, erhält man eine Laufzeit von O(|E|log|V|). Ein Knoten mit der geringsten Distanz zur cloud, KAPITEL 2. GRUNDLAGEN 15 Algorithm 3 1: procedure Dijkstra(Graph G, Vertex s) 2: Init-Single-Source(G,s); 3: int newDist=0; 4: Vertex u=NULL; 5: Set cloud=∅; 6: PriorityQueue Q=G.vertices(); . Der Schlüssel ist getD() 7: while Q 6= ∅ do 8: u=Q.poll(); 9: cloud.add(u); 10: for all Vertex v ∈ G.outAdjacentVertices(u) do 11: newDist=u.getD()+G.findEdge(u,v).getWeight(); 12: if v.getD()>newDist then 13: v.setD(newDist); 14: v.setPred(u); 15: Q.replaceKeyAt(v, newDist); 16: end if 17: end for 18: end while 19: end procedure also der Aufruf Q.poll(), wird maximal |V|-mal aus Q benötigt. Die Umstrukturierung nach dem Entfernen des Minimums läuft, bei maximal |V| Knoten im Heap, in einer Laufzeit von O(log|V|), also insgesammt O(|V|log|V|). Die nach der Relaxation nötige Umstrukturierung des Heaps, kann in O(log|V|) geschehen, durch schrittweises Vertauschen des betreffenden Heapknotens mit seinen Elternknoten, bis die Heapeigenschaft wieder erfüllt ist4 . Da dies höchstens |E|-mal passieren kann ergibt sich eine Laufzeit von O(|E|log|V|). Die Laufzeiten von replaceKeyAt() und Q.poll() ergeben also insgesammt O((|V|+|E|)log|V|). Falls man die Route zu einem bestimmten Zielknoten sucht, kann der Algorithmus abgebrochen werden, wenn der Zielknoten in die cloud aufgenommen wurde oder man nicht alle Knoten erreichen kann und sich in Q nur noch Knoten mit einer unendlichen Distanz zum Startknoten befinden. 4 Da die Java-Implementierung der PriorityQueue die replaceKeyAt()-Methde nicht anbietet, wird der Knoten aus dem Heap entfernt und neu einsortiert. Durch das Entfernen eines Nicht-Wurzel-Knotens verschlechtert sich die Laufzeit auf O(|V|) KAPITEL 2. GRUNDLAGEN 2.4.3 16 Der A*-Algorithmus Die bisher vorgestellten Algorithmen bedienen sich nur Eigenschaften eines abstrakten Graphen. Sind zusätzlich noch andere Informationen bekannt, wie zum Beispiel die geographische Lage der Knoten, lässt sich eine Heuristik bestimmen, mit deren Hilfe man gezielter nach einem kürzesten Weg suchen lassen kann. Im Falle des A*-Algorithmus, siehe Algorithmus 4, wird es vermieden, Kanten zu relaxieren, die wahrscheinlich nicht zu dem kürzesten Weg gehören. Dazu müssen aber auch eine Menge von Zielknoten T bekannt sein. Um zu entscheiden, welche Kante als nächstes relaxiert wird, wird eine Schätzfunktion fˆ benutzt,[HNR68]. Die Länge des kürzesten Weges von einem Start- zu einem Zielpunkt über einen Knoten n wird beschrieben durch f (n) = g(n) + h(n). g(n) ist die Länge des kürzesten Weges zum Knoten n, während h(n) die Länge des kürzesten Weges von n zum Zielpunkt bestimmt, beziehungsweise zu einem Zielpunkt t ∈ T , zum Beispiel zum Weitentferntesten vom Startpunkt aus. Die Schätzfunktion für die Länge eines kürzesten Weges ist dann fˆ(n) = ĝ(n) + ĥ(n). ĝ(n) ist eine Schätzung der Länge des Weges zum Knoten n, ĥ(n) dementsprechend analog. Für ĝ(n) kann also die Länge des bisher berechneten kürzesten Weges genutzt werden. Wird ĥ für alle Knoten auf 0 gesetzt, erhält man dadurch Dijkstra’s Algorithmus und es gilt sogar fˆ(n) = f (n), da durch die Aufnahme des Knotens mit dem kürzesten Abstand zur cloud, ĝ(n) = g(n) gilt. ĥ(n) darf die wirkliche Länge nicht überschätzen, dann bestimmt der Algorithmus auch den kürzesten Weg. Eine mögliche Schätzung für die Länge des Restwegs ist der Luftlinienabstand, da kein Weg kürzer sein kann, als der direkte. Die Schätzfunktion ĥ ist konsistent, genau dann wenn für jeden Knoten n und jeden seiner Nachfolger m die Dreiecksungleichung h(n, m) + ĥ(m) ≥ ĥ(n) gilt, wobei h(n, m) die wirkliche Länge zwischen n und m ist. Die Luftlinie ist eine konsistente Schätzfunktion, da die Dreiecksungleichung der euklidischen Distanz in der Ebene gilt. Die Berechnung der Schätzfunktion unter Verwendung des euklidischen Abstandes für einen Knoten v sähe folgendermaßen aus, wobei t ein Point sei, der die geographische Lage des Zielpunktes besitzt und tx für die X-Koordinate von t steht. q ˆ f (v) = v.getD() + (vx − tx )2 + (vy − ty )2 Der Algorithmus 4 zeigt den allgemeinen Fall, in dem ĥ(n) nicht konsistent ist. Es werden wie bei Dijkstra zwei Mengen von Knoten benutzt, die zwar disjunkt sind, aber vereinigt nicht die Menge aller Knoten ergeben müssen. Es wird eine PriorityQueue, hier open, benutzt, um zu ermitteln welcher Knoten als nächstes in die Liste closed kommt. Der Schlüssel, nach dem die Priorityqueue sortiert ist, ist der Wert von fˆ für den jeweiligen Knoten. Sobald ein Weg zum Startknoten bekannt ist, wird der Knoten inopen aufgenommen, Zeile 14 f. Wenn der geschätzte kürzeste Weg zu einem Knoten bekannt ist, wird er in closed aufgenommen. Falls der Knoten zu T gehört, kann der Algorithmus abgebrochen werden. Wenn ein Knoten in closed aufgenommen wird, werden alle aus- KAPITEL 2. GRUNDLAGEN 17 Algorithm 4 1: procedure A*(Graph G, Vertex s) 2: Init-Single-Source(G, s); 3: int newDist=0; 4: Vertex u=NULL; 5: Sequence closed=∅; . Hier ist der Schlüssel fˆ(n), Pendant zu Dijkstra’s cloud 6: PriorityQueue open.add(s); . enhält Knoten, zu denen ein Weg bekannt ist 7: while open 6= ∅ do 8: u=open.poll(); 9: closed.add(u); 10: if u.isTarget() then 11: break 12: end if 13: for all Vertex v ∈ G.outAdjacentVertices(u) do 14: if v ∈ / closed ∧ v ∈ / open then 15: open.add(v); 16: end if 17: newDist=u.getD()+G.findEdge(u,v).getWeight(); 18: if v.getD()>newDist then 19: v.setD(newDist); 20: v.setPred(u); 21: if v ∈ closed then 22: closed.remove(v); 23: open.add(v) 24: else 25: open.replaceKeyAt(v, fˆ(v)); 26: end if 27: end if 28: end for 29: end while 30: end procedure KAPITEL 2. GRUNDLAGEN 18 gehenden Kanten relaxiert. Ist ĥ konsistent, dann entspricht die Laufzeit im worst-case der des Dijkstra Algorithmus. Denn dann wird kein Knoten mehr nachträglich aus closed entfernt, beziehungsweise muss die Schätzung für die Länge des kürzesten Weges nicht mehr nachträglich korrgiert werden. Ist ĥ(n) konsistent, kann Dijkstras Algorithmus übernommen werden, nur der Schlüssel, der zum Einordnen in die PriorityQueue benutzt wird, muss fˆ(n) sein.[Bri08] Aber unter Anwendung der gleichen Heuristik, relaxiert kein Algorithmus weniger Kanten als der A*-Algorithmus, bis er mit einer geeigneten Heuristik einen Zielknoten gefunden hat. 2.5 Schlussfolgerung Die OSM-Daten sind schon als Graph organisiert, weshalb es nicht schwer ist, aus diesen einen Graphen aufzubauen. Die OSM-Nodes werden zu Knoten in dem Graphen und die Kanten entstehen aus den Teilabschnitten der Ways, die zwei Nodes verbinden. Da es Einbahnstraßen gibt, bietet sich ein gerichteter Graph an. Gewichtete Kanten werden vorausgesetzt, bleibt also nur noch die Frage offen, wie man die Kanten gewichtet. Zusätzlich zu der Länge des Weges, den eine Kante darstellt, könnten auch andere Faktoren, wie die Art der Straße, das Gewicht einer Kante bestimmen. Die vorgestellten Algorithmen bestimmen kürzeste Wege von einem Knoten zu allen anderen. Das wird nicht nötig sein und der Dijkstra- und A*Algorithmus können abgebrochen werden, wenn ein kürzester Weg zum Zielknoten gefunden oder ein Zielknoten aus einer Menge von Knoten erreicht wurde. Dies ist beim Bellman-Ford-Algorithmus nicht der Fall, daher wird auf die Implementierung verzichtet, da er die gleiche kürzeste Route wie Dijkstra liefert. Der A*-Algorithmus ist der einzige Algorithmus, der die geographische Lage der Knoten beachtet, dadurch wird er durch das zielgerichtete Suchen wahrscheinlich am Schnellsten ein Ergebnis liefern. Kapitel 3 Konzepte In diesem Kapitel wird der Ablauf der partitionierten Routenberechnung erläutert. Dazu gehört zunächst das Laden von Graphpartitionen, dann wird auf diesen die Wegesuche ausgeführt und anschließend werden die Routen der einzelnen Partitionen zusammengeführt. 3.1 Partitionierungsarten Bevor die Routensuche beginnen kann, müssen die geographischen Daten partitioniert werden. Es werden nun zwei Arten der Partitionierung vorgestellt. Die Straßendaten werden partitioniert, indem sie in Gitterzellen zerlegt werden. Diese Gitterzellen sind alle gleich breit und gleich lang und sind lückenlos aneinander gereiht. Die geometrischen Daten in Punktform sind so disjunkt aufgeteilt. Bei Linienzügen wird durch sogenanntes Clipping der Teil, der außerhalb einer Gitterzelle liegt, abgeschnitten, so dass auch die Straßen disjunkt aufgeteilt sind. Einzig die Knoten auf dem Gitter sind in den jeweiligen Graphen mehrmals vorhanden. Im Folgenden unterscheiden wir zwischen Partitionierungsarten, die abhänig oder unabhängig von der gesuchten Route sind. Eine unabhängige Partitionierung kann für verschiedene Routensuchen genutzt werden, nur die Wahl der relevanten Partitionen wäre unterschiedlich. Ein Beispiel für eine unabhängige Partitionierung ist in Abbildung 3.1(a) dargestellt. Die Gitterlinien sind horizontal und vertikal angeordnet. Alle geographischen Daten liegen innerhalb der schwarzen Umrandung. Zusätzlich ist ein Weg von einem Punkt s zu einem Punkt t eingezeichnet. In 3.1(b) ist eine abhängige Partitionierung dargestellt. Die Gitterlinien sind parallel und orthogonal zur Luftlinie zwischen Start- und Endpunkt ausgerichtet. Der Start- und Endpunkt befinden sich im Zentrum einer Partition. 19 KAPITEL 3. KONZEPTE 20 t s t s (a) Eine unabhängige Partitionierung (b) Eine abhängige Partitionierung Abbildung 3.1: Zwei Partitionierungsarten Dadurch soll verhindert werden, dass Straßen nicht vollends berücksichtigt werden können, falls bei der unabhängigen Partitionierung der Start- oder Endpunkt der gesuchten Route am Rand zu einer nicht weiter betrachteten Partition liegt. Das soll die Wahrscheinlichkeit erhöhen, dass Straßen, die zu dem optimalen kürzesten Weg gehören, nicht an einem Rand abgeschnitten werden und nicht weiter untersucht werden können. Durch die Ausrichtung entlang der Luftlinie, kann sich die Anzahl der relevanten Partitionen verringern, vergleiche Abbildung 3.1. Wir nehmen an, dass eine Vorgabe existiert, wie breit und wie lang eine Gitterzelle sein soll. Das abhängige Gitter soll also in zwei Kriterien von der gesuchten Route abghängig sein. • Start- und Zielpunkt sollen mittig in ihrer Partition liegen • Die Gitterlinien sollen parallel, beziehungsweise orthogonal zur Luftlinie ausgerichtet sein. Anders als bei der unabhängien Partitionierung muss die vorgegebene Breite oder Höhe, die eine Partitionsgitterzelle besitzen soll, wahrscheinlich noch angepasst werden, damit die Partitionierung dem ersten Kriterium gerecht wird. Die Vorgaben der Länge und Höhe werden dabei als Mindestlänge und -höhe interpretiert und werden gegebenenfalls noch angepasst. Um das zweite Kriterium zu erfüllen, müssen nun die Partitionen in Richtung der Luftlinie vom Startpunkt aus rotiert werden. KAPITEL 3. KONZEPTE 21 Beispiel zur Erstellung einer abhängigen Partitionierung Gegeben sei der Startpunkt s bei (0,0) und ein Endpunkt t bei (2.5,5). Die Höhe und Breite einer Gitterpartition soll 1 betragen. Eine unabhängige Partitionierung sähe wie im Bild von Abbildung 3.2(a) aus. Um aus ihr eine abhängige Partitionierung zu machen, wird zuerst beispielsweise die Höhe einer Partition neu angepasst. Der Abstand zwischen s und t beträgt 5,59. Das bedeutet, dass bei der Höhe von 1 nach sechs Partitionen der Zielpunkt erreicht wird. Damit beide Punkte jeweils mittig liegen, wird der Nachkommateil der Differenz auf die Partitionshöhe von fünf Partitionen aufgeteilt. Das macht 1+(0,59/5) also etwa 1,18. Nachdem man das Gitter um die Hälfte der neuen Höhe und der Breite verschoben hat, erhält man eine Partitionierung, wie sie im Bild 3.2(b) dargestellt ist. Nun muss noch das gesammte Gitter um den Winkel, der zwischen der Y-Achse und der Luftlinie liegt, im Startpunkt gedreht werden, in diesem Beispiel sind das 26,565◦ . In diesem Beispiel wurde die Y-Achse genommen, da der Startpunkt im Ursprung liegt und so um den kleinsten Winkel gedreht werden konnte. Die fertige abhängige Partitionierung ist in Bild 3.2(c) zu sehen. t s (a) Erster Schritt t s (b) Zweiter Schritt t s (c) Dritter Schritt Abbildung 3.2: Beispiel zur Erstellung einer abhängigen Partitionierung 3.2 Ablauf der partitionierten Routenberechnung Ziel einer partitionierten Routenberechnung ist es, den kürzesten Weg von einem Startpunkt in einer Partition zu einem Endpunkt in einer anderen Partition über beliebig viele Partitionen zu finden. Es ergibt sich grob der folgende Programmablauf aus Algorithmus 5. KAPITEL 3. KONZEPTE 22 Algorithm 5 1: procedure PartitionStreetRouting() 2: Erstelle Partitionen und 3: Bestimme Start- und Zielpartition . benötigt Start- und Zielpunkt 4: Beginne in der Startpartition 5: while aktuelle Partition ist nicht die Zielpartition do 6: Baue den Graph auf . benötigt Partitionspolygon 7: Ermittle den Rand, zu dem geroutet werden soll 8: Markiere den oder die Zielknoten nach gewählter Heuristik 9: Suche einen Weg vom aktuellen Startknoten aus 10: if Kein weiterer Rand erreichbar then . siehe Algorithmus 7 11: Breche Routensuche komplett ab 12: end if 13: Setze neuen Startknoten auf Zielknoten dieser Route 14: Verarbeite das Routingergebnis 15: Verwerfe den Graphen 16: Wechsel in die nächste Partition 17: end while 18: Baue Graph auf und markiere Zielknoten 19: Finde Route zum Endpunkt 20: Gib das Ergebnis der Gesamroute zurück 21: end procedure KAPITEL 3. KONZEPTE 23 Ein Graph wird dabei für eine Partition höchstens ein Mal aufgebaut, man kann also nicht zweimal die gleiche Partition untersuchen. In den nachfolgenden Abschnitten werden ein Großteil der Zeilen erläutert. In Abschnitt 3.2 werden zwei Partitionierungsarten vorgestellt. Die while-Schleife und was genau mit Heuristik gemeint ist, wird in Abschnitt 3.3 geklärt. 3.3 Routensuche Nachdem die Art der Partitionierung gewählt wurde, kann man mit der Routensuche in der Partition mit dem Startpunkt beginnen.Die nachfolgenden Abschnitten beziehen sich auf die Schleife in Algorithmus 6. Der Zwischenzielpunkt der Routensuche in einer Partition ist ein Randpunkt zu einer anderen Partition. Dieser Rand wird vor jeder Routensuche durch die geringste Distanz eines Randes zum Zielpunkt bestimmt. 3.3.1 Heuristiken zur Findung von Zielknoten Zu Beginn der Routensuche in einer Partition hat man genau einen Startknoten. Das ist entweder der Startpunkt der gesuchten Route in der ersten Partition oder ein Knoten, der die gleiche geographische Lage besitzt, wie der Zielknoten, der in der vorherigen Partition berechnet wurde. Den Zielknoten auf dem Zielrand einer Partition kann man über folgende Heuristiken bestimmen. a) Kürzeste Distanz zum Start: Der Randknoten mit dem berechneten kürzesten Weg zum Startpunkt wird ausgewählt b) Kürzeste Distanz zum Ziel: Der Randknoten mit dem geschätztesten geringsten Abstand zum Zielpunkt wird ausgewählt c) Gewichtung der Straßenart: Ein Randknoten einer Autobahn wird dem einer Landstraße vorgezogen Wird die Heuristik a) gewählt, hat man eine Menge von Zielknoten, nämlich alle die auf dem Rand zur nächsten relevanten Partition liegen. Verwendet man b), kann man vor dem Starten der Routensuche den Knoten auf dem Zielrand berechnen, der den geringsten Abstand zum Zielknoten der gesamten Route besitzt, und nur diesen als Ziel markieren. Man sollte jedoch sicher stellen, dass dieser Knoten auch erreichbar ist und man nicht die Spur einer Autobahn wählt, die in die KAPITEL 3. KONZEPTE 24 Partition hineinführt. Denn dann kann dieser Zielknoten nicht erreicht werden und es werden solange Kanten relaxiert, bis zu allen erreichbaren Knoten ein kürzester Weg bestimmt worden ist. Jedoch kann man dies nicht nur über das Ausschließen von bestimmten Einbahnstraßen lösen und diese Gefahr besteht immer bei einer ungeschickten Partitionierung. Die Heuristik c), die die Art der Straße berücksichtigt, muss entweder mit a) oder b) kombiniert werden. Im Falle von a) werden zum Beispiel nur Autobahnknoten und von b) nur der Autobahnknoten mit dem geringsten Abstand zum Ziel markiert. 3.3.2 Metriken zur Gewichtung der Kanten Zur Gewichtung der Kanten haben wir zwei verschiedene Metriken gewählt. i) Die kürzeste Route: Das Maß ist die Länge des LineStrings einer Kante ii) Die schnellste Route: Das Maß ist die Länge des LineStrings ÷ Durchschnittsgeschwindigkeit, bestimmt durch die Art der Straße In der Tabelle 3.1 sind die highwaytags, ihre Bedeutung und die Wahl der Durchschnittsgeschwindigkeit dieser Straßenart in der Reihenfolge notiert, in der sie von der Heuristik c) bevorzugt werden. Die Geschwindigkeiten entsprechen denen von OpenRouteService1 , ein Webservice, das eine Routensuche mit OSM-Daten anbietet. 3.3.3 Vorgehen beim Misserfolg Stellt man während des Routings fest, dass man in einer Partition nicht den Zielknoten oder Zielgrenzknoten erreichen kann, muss die Auswahl des Zielrandes korrigiert werden. Wir gehen aber davon aus, dass man die Partitionen so groß wählt und das Straßennetz von Deutschland so ausgebaut ist, dass man von einem Rand einen Weg zu jedem anderen Rand finden kann. Erreicht man trotzdem keine anderen Randknoten, weil man zum Beispiel durch eine ungeschickte Partitionierung auf einer Halbinsel angekommen ist, wird die Routensuche ohne Ergebnis abgebrochen. Auch falls man bereits in der Endpartition angelangt ist und man den Zielpunkt nicht erreichen kann, wird abgebrochen, da kein Partitionsgraph mehrfach berechnet werden soll. Dieses Vorgehen ist im Algorithmus 6 dargestellt. Das Rand erreichbar aus Zeile 7 soll einerseits ausdrücken, dass es einen Knoten auf diesem Rand gibt, der bei der Routensuche erreicht wurde, als auch dass man die Partition 1 http://www.openrouteservice.org KAPITEL 3. KONZEPTE 25 Highway-Tag Bedeutung Ø Geschwindigkeit (km/h) motorway Autobahn 110 motorway link Autobahnauffahrt 90 trunk Kraftfahrstraße 90 trunk link Kraftfahrstraßenauffahrt 70 primary Bundesstraße 70 primary link Bundesstraßenauffahrt 60 secondary Landesstraße 60 tertiary Kreisstraße 55 unclassified Nebenstraße 50 residential Straße in einem Wohngebiet 40 service Zugangsweg 30 living street Wohn- oder Spielstraße 10 track Feld- oder Waldweg 10 Tabelle 3.1: Die Straßenarten und ihre Gewichtungen noch untersuchen darf. Wurde der Zielrand nicht erreicht gibt es somit nur noch zwei mögliche Ränder, da man auch nicht mehr die Partition untersuchen kann, aus der man in diese Partition gelangt ist. KAPITEL 3. KONZEPTE 26 Algorithm 6 1: function Kein weiterer Rand erreichbar() 2: if mindestens ein Knoten auf dem Zielrand erreichbar then 3: Bestimme Zielknoten 4: return false; 5: else 6: for all Ränder dieser Partition do . sortiert nach Entfernung zum Ziel 7: if Rand erreichbar then 8: Bestimme Zielknoten 9: return false; 10: end if 11: end for 12: Bestimme Teilergebnis der Routensuche 13: return true; . Suche muss endgültig abgebrochen werden 14: end if 15: end function 3.4 Komposition der Routen Die Ergebnisse der Routensuche in einer Partition bestehen aus dem berechneten Weg und dem Gewicht dieses Kantenzugs. Um Speicherplatz zu sparen wird lediglich der LineString gespeichert, der aus den Kanten des kürzesten Weges bestimmt wird, sowie das Gewicht. Dadurch gehen zwar Informationen verloren, die in dieser Arbeit aber auch keine Rolle spielen, wie eine detailierte Beschreibung des Weges, beispielsweise ”nach zwanzig Kilometern auf der A2, rechts abbiegen“. Die Gewichte der einzelnen Partitionen auf dem Gesamtweg werden aufsummiert und die Linestring-Geometrien vereinigt. Kapitel 4 Implementierung Dieses Kapitel beschäftigt sich mit der Implementierung des Graphaufbaus, verschiedenen Implementierungsdetails, einer Beschreibung der Klassen und einer Beschreibung der Benutzerschnittstelle. 4.1 Anforderungen Das Programm soll einen Weg durch verschiedene Partitionen berechnen können. Dabei sollen die Faktoren Partitionsgröße, Metrik zur Gewichtung der Kanten, Heuristik zur Auswahl der Randknoten und Wahl des Kürzeste-Wege-Algorithmus, entweder Dijkstra oder A*, beliebig kombinierbar sein. Da das alleinige Gesamtgewicht der Route wenig Aussagekraft hat, soll die Route noch visuell dargestellt werden können. Dies geschieht durch die Umsetzung als PlugIn für das Open-Source-Programm OpenJUMP 1 . Der Benutzer soll Ziel- und Endpunkt entweder als WGS84-Koordinaten oder als Adressen angeben können. Da die Straßendaten partitioniert werden sollen, um den Arbeitsspeicher zu entlasten, sollte der Speicherplatz des Graphen, der in einer Partition berechnet wurde, wieder freigegeben werden, wenn man eine andere Partition betrachtet. 4.2 OpenJUMP und seine Schnittstelle OpenJUMP ist ein Open-Source-Programm, das in Java geschrieben ist und unter der GNU-Lizenz steht. Es ist ein geographisches Informationssystem und bietet die Anzeige und Bearbeitung von Geometrien. Die Geometrien können zum Beispiel aus einer 1 http://www.openjump.org/ 27 KAPITEL 4. IMPLEMENTIERUNG 28 PostGIS-Datenbank geladen und in Features, die aus einer Geometrie und beliebig vielen anderen Attributen bestehen, überführt werden. Die Geometrien werden mit Datentypen von der Java Topology Suite, kurz JTS, dargestellt, ein Framework, das das Simple Feature Format unterstützt. Diese Features können in der Workbench dargestellt werden, dem Arbeitsbereich in OpenJUMP. Die Features befinden sich in Layern, die in ihrer Darstellung in Farbe, Transparenz oder Linienbreite angepasst werden können. Außer Geometrien, können auch Bilder dargestellt werden und auch Karten, zum Beispiel mittels einem sogenannten Web-Map-Service, kurz WMS. In Abbildung 4.1 ist die Oberfläche mit einem Ausschnitt eines WMS-Layers von Deutschland dargestellt. Abbildung 4.1: Die OpenJump-Oberfläche OpenJUMP unterstützt das PlugIn Framework. Damit OpenJUMP ein PlugIn benutzen kann, muss es sich entweder im Extension-Ordner, der von OpenJUMP spezifiziert wird, befinden oder man teilt OpenJUMP über eine workbench-property.xml -Datei mit, an welchem Ort im Dateisystem sich ein zu ladenes PlugIn befindet. Ein PlugIn bekommt dann ein PluginContext übergeben, über das es auf die Workbench zugreifen und neue Layer oder Menüpunkte anlegen kann. Das Programm wird also als PlugIn für OpenJUMP realisiert. KAPITEL 4. IMPLEMENTIERUNG 29 Abbildung 4.2: Das Einstellungsfenster 4.3 Beschreibung der Benutzerschnittstelle Wurde das PlugIn in OpenJUMP geladen, erscheint ein Menue-Punkt, der PRS lautet. Betätigt man diesen kann man das Partitionierte Routensuche-PlugIn auswählen. Es erscheint ein Fenster in dem man verschiedene Einstellungen bezüglich der Routensuche treffen kann. In Abbildung 4.2 ist dieses Fenster zu sehen. Man kann zwischen folgenden Einstellungen wählen. Start/Ziel Es kann gewählt werden zwischen der Eingabe des Start- und Zielpunktes als Adresse, bestehend aus Postleitzahl und Straßenname oder als Koordinaten im dezimalen Gradsystem. In das jeweils linke Textfeld wird die Postleitzahl oder der Breitengrad angegeben. Routingalgorithmus Man kann zwischen Dijkstra und A* als Algorithmus für die Routensuche wählen. Heuristik Es kann zwischen den vier Heuristiken eine ausgewählt werden, die der kürzesten Distanz zum Startknoten, zum Zielknoten oder jeweils unter Berücksichti- KAPITEL 4. IMPLEMENTIERUNG 30 gung der Straßenart. Metrik Hier muss man die Entscheidung treffen, ob man die kürzeste oder die schnellste Route suchen möchte. Partitionierung In diesem Bereich müssen die Höhe und die Breite einer Gitterzelle gesetzt werden. Wird als Einteilung ausgewählt, wird die Fläche Deutschlands in ein Gitter der Form Breite mal Höhe aufgeteilt. Wird in km gewählt werden die Angaben im Textfeld als Breite- und Längeangaben einer Partition in km interpretiert. Zudem kann zwischen einer abhängigen und einer unabhängigen Partitionierung gewählt werden. Damit die Verbindung zu einer PostGIS-Datenbank hergestellt werden kann, muss sich im OpenJUMP-Ordner einer Datei namens db.con befinden. In ihr müssen folgende Verbindungseinstellungen befinden, wobei es nicht auf die Reihenfolge ankommt: Host, Port, Datenbank, Benutzer, Passwort. host:<Host> port:<Port> database:<Datenbank> user:<Benutzer> password:<Passwort> Bestätigt man die Eingabe, wierden die Partitionen aufgebaut. Liegen bei der abhängigen Partitionierung die eingegebenen Punkte zu nah beieinander, sodass nicht beide mittig in einer Partition liegen können, wird der Benutzer aufgefordert die Partitionsgrößen anzupassen. Ansonsten beginnt die Routensuche und es werden erst die Startund Zielpunkte angezeigt. Für jede Partition erscheint das Partitionspolygon und nach dem Aufbau des Graphen und der Routensuche wird der Ergebnis-LineString dargestellt. Dahinter wird ein WMS-Layer aus OSM-Daten2 angezeigt. Am Ende erscheint ein Fenster, in dem die Länge des kürzesten Weges oder die Information, dass abgebrochen werden musste, steht. Nach der Berechnung einer Route kann man das PlugIn erneut starten und die neue Route wird gezeichnet, während die bisherige Route in der Workbench verbleibt. Man kann die Berechnung des PlugIns auch abbrechen, allerdings sollte dann vor einem erneuten Durchlauf, OpenJUMP neu gestartet werden. 2 http://ows.terrestris.de/dienste.html#openstreetmap-deutschland KAPITEL 4. IMPLEMENTIERUNG 4.4 31 Finden von Koordinaten über Adressangabe Da sowieso nicht alle Straßennummern in den OSM-Daten verzeichnet sind, werden Koordinaten nur über eine Postleitzahl und eine Straße gesucht. Es gibt zwei Modellierungsmöglichkeiten für Postleitzahlen. Entweder werden OSMNodes mit Schlüssel-Wert-Paaren der Form addr:postcode und addr:street angelegt oder es gibt Polygone, die über eine Relation mit einer Postleitzahl verknüpft sind. Diese Polygone sind weit verbreitet, decken aber nicht ganz Deutschland ab. Es wird zuerst geprüft, ob es ein passendes Polygon zu der gegebenen Postleitzahl gibt. Falls kein Polygon existiert werden alle Edges und Nodes aus den OSM-Daten überprüft, ob sie einen Tag-key besitzten mit addr:postcode und der value gleich der gesuchten Postleitzahl ist. Um diese Geometrien wird eine BoundingBox gelegt und als Polygonersatz genutzt. Wurde ein Polygon oder eine BoundingBox gefunden, wird in dieser Geometrie nach der Straße mit dem angegebenen Straßennamen gesucht und die Node Id und PointGeometrie, eines Knotens zurückgegeben. Dies führt die Funktion getNodeIdForAdress(text,text) aus, die im Anhang B.2 zu finden ist. 4.5 Umgang mit den WGS84-Koordinaten Die OSM-Daten liegen in zweidimensionalen Koordinaten bezogen auf den WGS84Referenzellipsoiden in dezimaler Schreibweise vor. Das bedeutet unter anderem, dass die Entfernung zwischen zwei beliebigen Koordinaten nicht über den euklidischen Abstand der Ebene bestimmt werden kann. Zumindest nicht, wenn man den realen Abstand zweier Punkte auf der Erde in beispielsweise Kilometern wissen möchte. Dazu müsste man den kürzesten Weg auf einer Kugel oder besser noch auf dem WGS84-Referenzellipsoiden bestimmen. PostGIS hat für geographische Daten und Geometrien mit geographischen Koordinaten ab der Version 1.5 einen eigenen Typen, Geography, der bestimmte Geometrieberechnungen auf dem WGS84-Referenzellipsoiden anbietet. So kann die Distanz zwischen zwei Koordinaten mit ST Distance(Geography,Geography) bestimmt werden und auch die Länge eines LineStrings kann mit ST Length(Geography) in Metern ausgedrückt werden. Die Linestrings liegen zwar als Geometry vor, können jedoch automatisch zu Geography gecastet werden, wenn eindeutig ist, dass eine Geography benötigt wird. Den Abstand zwischen zwei Koordinaten kann man mit dem Verfahren nach Andoyer aus [Mee91], Seite 81, bestimmen. Alternativ, da sowieso nur Distanzen innerhalb Deutschlands bestimmt werden sollen, könnte auf trigonometrische, spärischen Berechnungen verzichtet und angenommen werden, dass die Krümmung der Erde keine Rolle spielt. Es KAPITEL 4. IMPLEMENTIERUNG 32 wird die Abstandsberechnung der Ebene modifiziert und um die durchschnittliche Distanz in Kilometern zwischen zwei WGS84-Koordinaten mit dem Abstand einer Längenund einer Breiteneinheit in dem Weltkoordinatensystem, erweitert. Der 1◦ -BreitenkreisAbstand variert jedoch stark innerhalb Deutschlands, so dass es nicht sehr genau ist, die Abweichung beträgt zum Beispiel sechs Kilometer bei der Distanz zwischen Hamburg und Dresden. Der Durchschnittsabstand von 70,5 km für eine Längeneinheit wird dennoch benutzt, um eine gegebene Partitionslänge von km in Gradabständen umzurechnen. Für die Bestimmung der Distanz zum Startpunkt, sei es für die Heuristik b) zum Finden eines Randzielknotens oder die Schätzfunktion beim A*-Algorithmus, benutzen wir das Verfahren nach Andoyer. Die Länge eines LineStrings wird mit PostGIS bei der Erstellung, beziehungsweise während des Clippings berechnet. 4.6 4.6.1 Aufbau des Graphen Ermittlung von Kanten aus den OSM-Daten Nach dem Import der OSM-Daten in die PostGIS-Datenbank legen wir eine Tabelle an, in der die Straßen in Kanten aufgeteilt sind. EDGES( Edge Id, Linestring, Length, Node Id1 → NODES, Node Id2 → NODES, Oneway, Highway) Die Funktion, die diese Tabelle erstellt, buildEdges() ist im Anhang B.1 zu finden. Zu der LineString-Geometrie wird ein räumlicher Index angelgt. Dabei werden nur Kanten übernommen, die zu Straßen gehören, auf denen ein vierrädiges Fahrzeug fahren kann, siehe Abschnitt 3.3.2, für eine Beschreibung der hierfür notwendigen Tags. Sackgassen werden nicht in den Graph mit aufgenommen oder Teile einer Straße, die nur einen Wendekreis bilden - unter der Annahme, dass die Straßen ‘‘vernünftig’’ modelliert wurden, das heißt, dass Straßen nicht unnötig durch viele kleine LineStrings modelliert werden, wenn auch einer genügen würde und dass kreuzende Straßen sich auch in dem selben OSM-Node treffen und es nicht verschiedene OSM-Nodes mit gleichen Koordinaten sind. Die Straßenart wird im Attribut Highway vermerkt und ob die Straße eine Einbahnstraße ist, im Attribut Oneway. Um eine Straße als Einbahnstraße zu klassifizieren, benötigt man den Tagschlüssel oneway, nur nicht bei Autobahnen, die auch ohne Tag als Einbahnstraßen betrachtet werden. Der Tagschlüssel ist dann yes, true, 1 oder -1. Die Einbahnstraße verläuft dann in die Richtung, in die der LineString erstellt wurde. Es sei denn, der Tagwert ist -1, dann ist die Richtung umgekehrt zu der des LineStrings. KAPITEL 4. IMPLEMENTIERUNG 33 In Abbildung 4.3 ist dargestellt, wie die OSM-Ways und -Nodes in den Graphen übernommen werden. Es werden nur Kreuzungen übernommen, nicht aber die OSM-Nodes, durch die schwarzen Punkte dargestellt, die nur für die Darstellung des Linienzuges benötigt werden. Die magenta-farbene Straße wird nur bis zum Knoten c übernommen, die Sackgasse jedoch nicht. a b c Abbildung 4.3: Transformation des OSM-Graphen 4.6.2 Clipping der Kanten in einer Partition Um zu entscheiden welche Geometrien in welcher Partition liegen, wird jeder Partition ein Polygon zugeordnet, das die Partitionsfläche beschreibt. Die LineStrings von Kanten in der Datenbank, die in einer fraglichen Partition beginnen und auch enden, werden komplett in den Graphen übernommen. Die anderen LineStrings, die das Polygon nur kreuzen, also nur zum Teil innerhalb des Polygons liegen, werden, sofern sie aus der Partition heraus und nicht wieder zurück führen, am Rand der Partition abgeschnitten (geclippt). Beginnt eine Kante in der Partition, aber endet dort nicht, dann wird sie am ersten Schnittpunkt mit dem Rand abgeschnitten. Endet sie nur in einer Partition, wird der LineString bis auf den Teil vom Start- zum ersten Schnittpunkt übernommen. Dadurch werden Kanten, die sich um den Rand zweier Partitionen schlängeln, disjunkt und eindeutig aufgeteilt. Das ist in Abbildung 4.4 graphisch dargestellt, wobei die Pfeile die Richtung andeuten, in die der LineString modelliert ist. Der blaue Teil eines LineStrings wird in der unteren Partition aufgenommen, die roten nicht. Führt eine Kante durch drei Partitionen, was selten passiert, aber wenn, dann an einer Ecke vorkommt, muss sie in der mittleren Partition so gekürzt werden, dass die Vereinigung der drei Teil-LineStrings wieder den Gesammten ergibt. Um dies zu erreichen müssen die vier Kombinatinen von Startpunkt liegt/liegt nicht und Endpunkt liegt/liegt nicht in der gegebenen Partition untersucht werden. Das wird von den zwei PL/pgSQL-Funktionen getEdges(Geometry) und getBorderEdges(Geometry) erledigt, die ebenfalls im Anhang B.3 zu finden sind. KAPITEL 4. IMPLEMENTIERUNG Partitionsgrenze c a b 34 d e f g h Abbildung 4.4: Clipping an der Partitionsgrenze 4.7 Klassenbeschreibung Im Folgenden werden die implementierten Klassen kurz erläutert und wie sie zusammenhängen. Die Architektur wurde an das MVC-Muster angelehnt. Allerdings gibt es wenig Interaktion mit dem Benutzer, wodurch es es viele Models, aber wenige Views und Controller gibt. Alle Klassen, die mit den Knoten des Graphen arbeiten, haben eine tidyUp()-Methode, die dafür sorgt, dass nicht mehr auf diese Knoten referenziert wird, wodurch sie von der GarbageCollection von Java gelöscht werden können. Reine Getund Set-Methoden werden nicht weiter erklärt, es werden nur die Attribute erläutert. Welche Get- und Set-Methoden eine Klasse anbietet, kann man den Klassendiagrammen entnehmen. Das Package Graph In diesem Package befinden sich die drei Klassen Vertex, Edge und Graph, die zusammen einen Graph darstellen. Sie implementieren die Methoden, die in Kapitel 2 vorgestellt worden sind, sowie Methoden, die zur Verwaltung der Relationen zwischen Knoten und Kanten benötigt werden, aber nur Package-weit sichtbar sind. In Abbildung 4.5 sind die Klassen diese Packages als Klassendiagramm dargestellt. Graph Die Klasse Graph wurde als Singleton implementiert. Das bedeutet, dass es nur eine Instanz dieser Klasse gibt. Zusätzlich zu den Knoten- und Kanten-Mengen wurde eine Hashtabelle erstellt, die eine rasche Zuordnung von einer Vertex-Id zu ihrem Vertex ermöglicht. • getInstance():Graph - Liefert die Instanz des Graphen KAPITEL 4. IMPLEMENTIERUNG 35 • tidyUp() - leert die Knoten- und Kanten-Menge und sorgt dafür, dass auch zwischen den Knoten und Kanten untereinander keine Referenzen mehr bestehen Edge Jede Edge, besitzt eine Referenz auf die beiden Vertices, start und end, die sie verbindet. Die Methoden zum Setzen dieser Vertices ist nicht öffentlich, sondern nur Package-weit zugänglich. Straßen, die keine Einbahnstraßen sind, werden nur durch eine Kante dargestellt, daher wird in einem boolean oneway gespeichert, ob es sich um eine Einbahnstraße handelt. Die Art der Straße wird über ein Enum Highway vermerkt, das im Abschnitt über die Model -Klassen erläutert wird. Noch nicht genannte Methoden sind folgende. • Edge(String id, LineString line, boolean oneway) - Erstellt eine neue Kante, die, falls oneway true ist, eine Einbahnstraße darstellt, mit der übergebenen id und dem PostGIS-LineString line • Edge(String id, LineString line) - Erstellt eine Kante, die keine Einbahnstraße ist Vertex Jeder Vertex hat eine Liste von ein- (in) und ausgehenden (out) Edges. Die Methoden, die diese Listen verändern können nicht von außerhalb dieses Packages aufgerufen werden. Die Klasse Vertex implementiert zusätzlich das Interface Comparable<Vertex>, wodurch eine natürliche Ordnung der Knoten festgelegt wird, nämlich die Länge des kürzesten Weges zu einem Startknoten. • Vertex(String id, Point p) - erstellt einen neuen Knoten mit der übergebenen id und dem PostGIS-Point p Die Geometrien der Knoten- und Kanten werden jeweils als PostGIS-Geometrie abgelegt. Um die Geometrien in OpenJUMP darstellen zu können, müssen sie erst noch JTSGeometrien transformiert werden. Da dies aber nicht für alle Kanten notwendig ist, wird dies nur bei den Kanten auf dem kürzesten berechneten Weg vorgenommen. Das Package Partition Die Klassen in diesem Package dienen der Darstellung einer Partition. Die Klassen sind auch in Abbildung 4.6 dargestellt. KAPITEL 4. IMPLEMENTIERUNG Abbildung 4.5: Die Klassen des Graph-Packages 36 KAPITEL 4. IMPLEMENTIERUNG 37 Partition Diese Klasse besitzt ein JTS-Polygon, das ihre Fläche beschreibt, eine Liste von PostGISLineStrings, die den Weg in dieser Partition wiedergeben und einen double-Wert für de Länge des kürzesten Weges. Desweiteren zwei boolsche Werte, die anzeigen, ob es sich um die Partition mit dem Endpunkt der Route handelt, target und ob sie schon untersucht wurde, used. Es wird ein JTS-Polygon verwendet, da die PostGIS-Geometrieklassen keine Geometrie-Methoden anbieten. Mit JTS ist es leichter eine Partition zu drehen oder zu prüfen, ob eine Koordinate auf dem Rand liegt. • Partition(Polygon p) - erstellt eine neue Partition mit dem übergebenen Polygon p • buildResultLineString(Vertex v) - Baut die Route vom Startpunkt der Route, bis zum Knoten v auf Border Über das Enum Border, das die Werte RIGHT, LEFT, ABOVE und BELOW annehmen kann, ist eine Bezeichnung der Ränder möglich, sofern es sich um ein Gitter handelt. GridPartition Eine Spezialisierung der Partition-Klasse ist die GridPartition, in ihr wird vermerkt in welcher Spalte i und Zeile j sich die Partition im Gitter befindet. Sie hat folgende Methoden. • GridPartition(int i, int j, Polygon p) - erstellt eine neue Gitterpartition mit dem übergebenen Polygon p und der Postion (i,j ) im Gitter • getBorders():Collection<Border> - gibt die Ränder, sortiert nach der geringsten Distanz zum Startpunkt, zurück • getBorder(Gridpartition other):Border - gibt den Rand zurück, an den die übergebene GridPartition other grenzt Das Package Routingalgorithm In diesem Package befindet sich ein Interface IRoutingAlgorithm, das von zwei Klassen implementiert wird , Dijkstra und AStar. Die Klassen sind zusätzlich in Abbildung 4.7 KAPITEL 4. IMPLEMENTIERUNG 38 GridPartition Partition Partition Object double getResultWeigth() void buildResultLineString(Vertex) Polygon getPolygon() String toString() void setPolygon(Polygon) boolean isTargetPartition() void setTargetPartition(boolean) boolean isUsed() void setUsed(boolean) void setPolygon(Polygon) Collection getBorders() int getJ() int getI() Border getBorder(GridPartition) boolean equals(Object) String toString() Border Enum Border[] values() Border valueOf(String) Abbildung 4.6: Die Klassen des Partition-Packages dargestellt. IRoutingAlgorithm verlangt folgende Methoden. • routes(String startVertexId) - Startet die Routensuche mit dem Vertex, der die startVertexId aufweist • getreachedVertices():Collection<Vertex> - gibt die Knoten zurück, die erreichbar waren, bis die Routensuche abgebrochen wurde • tidyUp() - Leert die Container, in denen Vertices gespeichert waren Im Gegensatz zur Dijkstra-Klasse implementiert AStar noch eine weitere Methode: • setTargetPoint(Point target)- Setzt den Zielpunkt, zu dem die Distanz geschätzt werden soll auf den PostGIS-Point target Das Package View In diesem Package befinden sich zwei Klassen. Sie sind in Abbildung 4.8(a) als Klassendiagramm dargestellt. Die eine ist ein spezialisiertes JPanel, die SettingsView und wird benutzt um die Routing-Einstellungen, die zu Beginn vom PlugIn abgefragt werden, an zu zeigen, siehe Abbildung 4.2. Sie implementiert einen KeyListener, um einen Teil von fehlerhaften Eingaben nicht zu zu lassen. KAPITEL 4. IMPLEMENTIERUNG 39 Abbildung 4.7: Die Klassen des RoutingAlgorithm-Package OpenJumpView Die andere Klasse ist die OpenJumpView, die als Singleton implementiert ist. Diese Klasse dient dem Anzeigen des Start- und Zielpunktes, der Partitionen und den LineStrings der errechneten Route. • getInstance():OpenJumpView - gibt die Instanz der OpenJumpView zurück • init(PlugInContext context, TaskMonitor monitor) - nimmt den context und monitor entgegen • addResultLineString(Collection<LineString> linestrings) - wandelt die linestrings in eine JTS-Geometrie um und stellt sie in der Workbench dar • addPolygon(Polygon p) - stellt das JTS-Polygon p in der Workbench dar • setPoints(Point start, Point end) - stellt start und end in der Workbench dar • setMonitorText(String text) - setzt die Anzeige des Monitors auf text Das Package Controller Die einzige Klasse in diesem Package dient dazu, die Angaben, die im SettingsPanel getroffen wurden, an die verschiedenen Models weiterzugeben. Der SettingsController ist KAPITEL 4. IMPLEMENTIERUNG (a) View 40 (b) Controller Abbildung 4.8: Die Klassen des Controller- und View-Packages in Abbildung 4.8(b) dargestellt un nutzt dafür folgende Methoden. • SettingsController(SettingsView settings) - erstellt einen neuen SettingsController mit den Komponenten der übergebenen SettingsView settings • store():boolean - übergibt den verschiedenene Models die Einstellungen, die getroffen wurden und gibt true zurück, falls keine fehlerhaften Eingaben getroffen wurden. Das Package Connectors In diesem Package befinden sich Klassen, die in irgendeiner Art und Weise Verbindung zu anderen Systemen als unsere Programm aufbauen oder selbst von anderen Systemen benutzt werden. Ihre Klassen sind auch in Abbildung 4.9 dargestellt. PSR OJ Plugin Diese Klasse ist das PSR OJ Plugin, das ThreadedPlugIn implementiert, ein Interface, das von OpenJUMP bereitgestellt wird und benutzt werden kann, falls nicht absehbar ist, wie lange eine Aufgabe dauert. Bei einem normalen PlugIn friert die Workbench ein, während ein PlugIn beim ThreadedPlugin die Workbench verändern kann, allerdings kann der Benutzer nicht an der Workbench arbeiten, sondern höchstens das Plugin abbrechen. Außerdem wird ein kleines Fenster, ein TaskMonitor, geöffnet, in dem kurze Textnachrichten erscheinen können. • initalisize(PlugInContext context) - Fügt dem Menue einen neuen Punkt hinzu, über das das PlugIn gestartet werden kann • execute(PlugInContext context) - Öffnet ein Fenster, in dem die SettingsView angezeigt wird und beendet das Plugin, falls die Eingaben fehlerhaft sind KAPITEL 4. IMPLEMENTIERUNG 41 • run(TaskMonitor monitor, PlugInContext context) - Gibt den context und den monitor an die OpenJumpView weiter und startet die Routensuche PSR Extension Die Klasse PRS Extension, sorgt lediglich dafür, dass das Plugin geladen wird, falls es im Extension-Ordner von OpenJump liegt. Dazu muss es von Extension, welches ebenfalls von OpenJUMP bereitgestellt wird, erben und folgende Methode überschreiben. • configure(PlugInContext context) - erstellt ein neues PSR OJ Plugin-Objekt und initalisiert es mit dem übergebenen context DataBase Die dritte Klasse in diesem Package ist die DataBase-Klasse. Sie ist als Singleton implementiert und verarbeitet die Anfragen an die PostGIS-Datenbank. Es werden zwei Arten von Anfragen gestellt, die zum Finden von Start und Endpunkt am Anfang und die zum Aufbau des Graphen, die mehrmals pro Routensuche benötigt wird. • getInstance():DataBase - gibt die DataBase-Instanz zurück • buildEdges(Polygon p) -stellt die Anfrage an die PostGIS-Datenbank und gibt die Ergebnisse dieser Anfrage weiter, siehe die Beschreibung zum GraphModel • findVertexWithAddress(String postcode, String street, boolean startPoint) - startet die Anfrage getPointForAddress(postcode, street) und falls startpoint true ist, wird das Ergebnis als Startpunkt gespeichert, sonst als Endpunkt der Routensuche • findVertexWithPoint(String lat, String lon, boolean startPoint) - startet die Anfrage getPointForPoint(Point) mit einem PostGIS-Point, der aus lat und lon erstellt wurde Das Package Model In diesem Package befinden sich eine Reihe von Klassen, die das Haupt-Model, das RoutingModel, unterstützen, in dem in ihnen Funktionen ausgelagert sind. Es sind alles Singleton-Klassen, mit Ausnahme eines Enums. Sie sind in Abbildung 4.10 zusehen. Durch die Assoziationspfeile ist angedeutet, dass jediglich das RoutingModel alle anderen Models kennt. Nur das TargetModel kennt ebenfalls ein anderes Model. KAPITEL 4. IMPLEMENTIERUNG 42 Abbildung 4.9: Die Klassen des Connectors-Packages Highway Das Highway-Enum stellt die verschiedenen Straßenarten aus Tabelle 3.1 dar. Sie besitzt nur eine nicht-Enum-spezifische Methode. • getWeight(Highway street):int - gibt die Durchschnittsgeschwindigkeit der Straßenart street zurück RoutingModel Dieser Klasse kümmert sich um die Hauptaufgabe des PlugIns, eine Route zu finden. Sie setzt den Ablauf um, der im Algorithmus 5 zusammen mit der Funktion aus 3.3.3 beschrieben ist. Also solange in Partitionen die Routensuche durch zu führen, bis die Endpartition und in ihr der Zielpunkt der Route erreicht wurde. Im RoutingModel werden über startPoint und endPoint die Koordinaten des Start- und Zielpunktes als PostGISPoint gehalten. Das resultWeight bezeichnet das bisher errechnete Gesamtgewicht der Routenberechnung. Es besitzt als einziges Model eine Referenz auf einen IRoutingAlgorithm und es wird vermerkt, welche Heuristik gewählt wurde, ohne die Gewichtung der Straßen mit ein zu beziehen, daher reicht ein Boolean targetHeuristic aus. Es bietet folgende Methoden an. • getInstance():RoutingModel - gibt die RoutingModel-Instanz zurück • route():boolean - startet die partitionierte Routensuche und gibt true zurück, falls der Endpunkt gefunden wurde KAPITEL 4. IMPLEMENTIERUNG 43 • setTargetPointForAStar(Point p)- verändert den Punkt, zu dem geroutet werden soll auf p, wird nur benötigt, wenn der A*-Algorithmus mit der Heuristik der niedrigsten Distanz zum Zielpunkt gewählt wird PartitionModel Das PartitionModel sorgt für die Einteilung in Gitterzellen und verwaltet diese. Bei der Erstellung vermerkt sie die Partition, in der die Route beginnt in der GridPartition startPartition. • getInstance():PartitionModel - gibt die PartitionModel-Instanz zurück • init(Point start, Point end) - nimmt den Startpunkt start und Zielpunkt end entgegen • buildGrid(double width, double height, boolean absolut, boolean dependent) - erstellt das Gitter, wenn dependent true ist ein abhängiges, ansonsten eine unabhängiges; ist absolut true dann werden width und height als Gitterhöhe und -breite in km interpretiert, sonst wird ein Gitter der Form width mal height erstellt • getNextPartition(GridPartition partition, Border next):GridPartition - gibt die GridPartition zurück die an den Rand next von partition angrenzt GraphModel Das GraphModel ist für den Aufbau des Graphen zuständig. Es wird auch vermerkt welche Knoten in der aktuellen Partition auf welchem Rand liegen. Diese Knoten werden jeweils in einer Periphery-Liste für jeden Rand gehalten. Um zu entscheiden, ob ein Randknoten prinzipiell erreichbar ist, muss es eine Referenz auf den Rand mit dem Startpunkt geben, startBorder. Der Border targetBorder stellt den Zielrand der aktuellen Routensuche dar. Das GraphModel verwaltet die Ids des Start- und Endknotens, startVertexId und endVertexId und welche Metrik gewählt wurde in der boolschen Variable velocityMetric. Ist diese true, wird die schnellste Route gesucht. Es beinhaltet folgende Methoden. • getInstance():GraphModel - gibt die GraphModel-Instanz zurück • buildGraph(Polygon p) - veranlasst die DataBase-Instanz die Anfrage nach den Kanten in dem JTS-Polygon p vorzunehmen die Ergebnisse werden an die folgenden Methoden übermittelt KAPITEL 4. IMPLEMENTIERUNG 44 • addEdge(String id, LineString line, String vertexId1, String vertexId2, String oneway, String highway, double weight) - Fügt dem Graph eine Kante mit den Übergebenen Eigenschaften zwischen den Vertices mit den Ids vertexId1 und vertexId2 • addBorderEdge(String id, LineString line, String vertexId1, String vertexId2, String oneway, String highway, double weight) - siehe addEdge(...), nur dass zusätzlich die Randknoten auf die Ränder aufgeteilt werden • isIrrelevantBorderVertex(Vertex v):boolean - gibt true zurück, falls der Knoten zu einer Kante gehört, die zu einem anderen Rand führt als der auf dem v liegt, und dieser nicht der Startrand ist; der Knoten kann folglich nicht erreicht werden • getTargetPeripheryVertices():Collection<Vertex> - gibt die Knoten zurück, die auf dem Zielrand liegen • getPeripheryVertices(Border b):Collection<Vertex> - gibt die Knoten zurück, die auf dem Rand b liegen • tidyUp(): - leert die Collections mit den Rand-Vertices Das TargetModel Das TargetModel ist für das Markieren der Zielknoten zuständig und wählt auch nach der Routensuche in einer Partition den Knoten aus, der der Startpunkt in der nächsten Partition sein soll. Dafür benötigt es eine Menge von Randknoten oder den Knoten, der den Zielpunkt der Routensuche beschreibt, falls in der Endpartition geroutet werden soll. Diese werden ebenfals in einer Periphery-Sammlung von Knoten gehalten. Einzig das TargetModel weiß über eine boolsche Variable withType, ob die Art der Straße in die Bestimmung der Zielknoten mit ein bezogen werden soll. Es muss wissen, ob der A*-Algorithmus verwendet wird, um nach dem Markieren des einen Zielknotens dessen Punkt als neues Ziel fest zu legen, siehe die Klasse RoutingModel, Methode setPointForAStar(). Das wird über das Boolean aStar gelöst. Die Methoden des TargetModels sind: • getInstance():TargetModel - gibt die TargetModel-Instanz zurück • markTarget() - wird benutzt wenn mit der Heuristik b, oder c in Kombination mit b, nur ein Knoten als Ziel markiert wird • markAllAsTargets(): - markiert alle Knoten auf dem Zielrand, Heuristik a, oder nur die mit der höchsten Gewichtung, also c plus a • getNextStartVertex():Vertex - gibt den Knoten zurück, der auf dem Zielrand erreicht wurde oder einen default-Vertex mit der Id deadend, wenn kein Knoten auf dem Rand erreicht wurde KAPITEL 4. IMPLEMENTIERUNG 45 GraphModel Object RoutingModel Object GraphModel getInstance() void buildGraph(Polygon) void addEdge(String,LineString,String,String,String,String,double) void addBorderEdge(String,LineString,String,String,String,String,double) boolean isIrrelevantBorderEdge(Vertex) Collection getTargetPeripheryVertices() Collection getPeripheryVertices(Border) void tidyUp() void setVelocityMetric (boolean) void setTargetBorder(Border) void setStartBorder(Border) void setStartVertexId(String) void setEndVertexId(String) String getStartVertexId() String getEndVertexId() boolean isVelocityMetric () RoutingModel getInstance() boolean route() void setTargetPointForAStar(Point) double getResultWeight() void addResultWeight(double) Point getEndPoint() void setEndPoint(Point) Point getStartPoint() void setStartPoint (Point) void setRoutingAlgorithm(IRoutingAlgorithm) void setHeuristic (boolean) PartitionModel Object Highway Enum int getWeight(Highway) Highway[] values() Highway valueOf(String) PartitionModel getInstance() void init(Point,Point) GridPartition getNextPartition(GridPartition,Border) GridPartition getStartPartition() void buildGrid(double,double,boolean,boolean) TargetModel Object TargetModel getInstance() void markTarget() void markAllAsTargets() Vertex getNextStartVertex () void tidyUp() void setPeriphery(Collection) void setPeriphery(Vertex) void setWithType(boolean) void setTargetHeuristic(boolean) void setAStar (boolean) Abbildung 4.10: Die Klassen des Model-Packages • tidyUp(): - leert die Collections mit den Rand-Vertices Interaktion zwischen den Models Im folgenden Sequenzdiagramm, Abbildung 4.11, ist der allgemeine Ablauf als Interaktion zwischen den Model-Klassen und einem IRoutingAlgorithm dargestellt, die Korrektur des Weges ist der Übersicht halber nicht mit aufgenommen wurde. Dies ist nur eine Schleife in der geprüft wird, ob man in die angrenzenden Partitionen gelangen kann, falls sie noch nicht besucht worden sind. Falls in eine Partition gewechselt werden darf, wählt das TargetModel den nächsten Startknoten aus. Es ist stark vereinfacht und stellt nur die wichtigsten Methoden der Model-Klassen dar, um zu zeigen, dass vom RoutingModel die Aktionen ausgehen und die anderen Models es nur unterstützen. 4.8 Speicherverwaltung In Java hat man nur einen indirekten Zugriff auf die Speicherverwaltung. Die GarbageCollection prüft in gewissen Abständen oder wenn der Speicher knapp wird, welche KAPITEL 4. IMPLEMENTIERUNG 46 Objekte nicht mehr referenziert werden und gibt deren Speicherplatz frei. Das muss aber nicht sofort geschehen. Das hat zur Folge, dass eine Partition, die ein Viertel des Gebiets Deutschlands beinhaltet und dadurch 2 bis 2,5GB groß ist, noch Speicher verbraucht, obwohl es keine Referenzen auf die Knoten und Kanten des Graphen gibt und der Speicher ausgeht, beim Versuch die nächste Partition zu laden. Bei etwas kleineren Partitionen ab circa 1,8GB kann es zu Verzögerungen im Graphaufbau kommen, aber die GarbageCollection schafft es meistens rechtzeitig genug Speicherplatz zu schaffen. :PartitionModel :GraphModel bis die Endpartition erreicht ist Vertex :TargetModel Abbildung 4.11: Der Ablauf des Routings als Sequenzdiagramm (vereinfacht) Partition getNextPartition() getNextStartVertex() routes(StartVertex) markTarget() buildGraph( Partition) findTargetBorder() Loop Partition getStartPartition() :RoutingModel route() :PRS OJ Plugin :IRoutingAlgorithm KAPITEL 4. IMPLEMENTIERUNG 47 Kapitel 5 Experimente Das folgende Kapitel beschreibt die ausgeführten Experimente und ihre Ergebnisse. Am Ende werden diese in einem Fazit zusammengefasst. 5.1 Die Experimentierumgebung Die Experimente wurden an einem fachgebietseigenen Rechner vorgenommen. Die benutzte PostgreSQL-Datenbank ist von der Version 9.1 und wurde unter dem LinuxBetriebssystem openSUSE kompiliert. Instaliert ist auch die PostGIS-Erweiterung in der Version 1.5. Der Rechner ist mit 4 GB Arbeitsspeicher ausgestattet und hat einen Quad-Core mit jeweils 3 Ghz. Der virtuellen Maschine wurden 2,5 GB Arbeitsspeicher zugestanden. 5.2 Das Vorgehen Es wurden drei Routen gewählt, an deren Beispiel die Laufzeit und die Güte der partitionierten Routenberechnung getestet werden soll. Diese Routen gehen von Hamburg nach Dresden, von Dresden nach München und von Hannover nach München. Um die Güte der errechneten Routen abzuschätzen, wurde zuerst die optimale Route berechnet. Die Heuristiken aus Kapitel 3 werden wie folgt abgekürzt. a) Kürzeste Distanz zum Start b) Kürzeste Distanz zum Ziel 48 KAPITEL 5. EXPERIMENTE 49 Route kürzeste in km schnellste in h Dresden-München 435.73 4.22 Hamburg-Dresden 438.06 4.58 Hannover-München 549.32 5.82 Tabelle 5.1: Die optimalen kürzesten Routen c.a) Kürzeste Distanz zum Start unter Berücksichtigung der Gewichtung der Straßenart c.b) Kürzeste Distanz zum Ziel unter Berücksichtigung der Gewichtung der Straßenart Als Abweichung der berechneten Route zur optimalen, wurde die Differenz zwischen der Länge der kürzesten Wege von berechneter und optimaler Route ins Verhältnis zur Länge der optimalen Route gesetzt. Beträgt die Länge des optimalen Weges 400 km und wurde ein Weg der Länge 600 km errechnet, beträgt die Abweichung 50%. Wenn in einer Tabelle die Anzahl der Knoten vermerkt sind, so ist dies die durchschnittliche Knotenanzahl der Partitionen, durch die die Route berechnet wurde. Spielt die Anzahl der untersuchten Knoten, also die Größe der cloud oder der closed-List, eine Rolle, so wurde wieder der Durchschnitt über den Partitionsweg ermittelt. Um den Schnitt nicht nach unten zu ziehen, wurden dabei nur die Partitionen betrachtet, in denen nach mehr als fünfhundert untersuchten Knoten ein kürzester Weg gefunden wurde. Tritt der Text k.E. in einer Tabelle auf, so heißt das, dass keine Route zum Zielpunkt gefunden werden konnte. Wurde kein Grund dafür angegeben, so wurde in ein Teil einer Partition geroutet, von der aus man keinen anderen Partitionsrand mehr erreichen konnte. In den folgenden Experimenten soll geklärt werden, welche Unterschiede es zwischen Dijkstra und A* gibt, wie die Güte und die Laufzeit von den Partitionsgrößen abhängt, ob die abhängige Partitionierung bessere Ergebnisse erzeugt, als die unabhängige, welche Unterschiede es zwischen den Metriken der schnellsten und der kürzesten Route gibt und ob eine Heuristik den anderen vielleicht überlegen ist. 5.3 A* versus Dijkstra Das erste Experiment befasst sich mit der Fragestellung, um wieviel ‘‘schneller’’ der A*-Algorithmus im Vergleich zu Dijkstra ist und ob beide Algorithmen in den selben Partitionen die gleiche Route finden. Dafür wurde die schnellste Route von Dresden nach München und die kürzeste von Hamburg nach Dresden mit verschiedenen Partitionsgrößen gesucht. Im ersten Experiment wurde die kürzeste Route zwischen Dresden und München mit KAPITEL 5. EXPERIMENTE 50 einer abhängigen Partitionierung und der Heuristik c.b) gesucht. Es wird also genau ein Zielknoten gesucht und deswegen errechnen Dijkstra und A* die gleichen optimalen Routen für eine Partition und diesen Zielrandpunkt. Sie unterscheiden sich nur in der Anzahl der untersuchten Knoten. In der folgenden Tabelle 5.2 ist der Zusammenhang zwischen Knoten in einer Partition und untersuchten Knoten dargestellt. Dabei wurden nur Partitionen betrachtet, in denen der Zielrandknoten auch erreicht wurde Die Anzahl der Knoten der Route ist der Durchschnitt über die Partitionen auf der berechneten Route. P.Größe in km2 3 080 3 300 4 265 4 620 10 265 # Knoten # unters. Kn. (D) # unters. Kn. (A*) #Knoten der Route 50 162 34 249 8 571 259 54 909 39 954 9 155 242 72 546 49 298 16 188 312 60 028 40 835 17 316 299 172 812 109 396 25 650 321 Tabelle 5.2: Dresden-München, Vergleich Knotenanzahl Dijkstra und A* Es gibt in der Tabelle eine Auffälligkeit. So verringert sich die durchschnittliche Anzahl der Knoten bei dem Wechsel der Partitionsgröße von 4 265km2 auf 4 620km2 um fast 12 000, da verschiedene Wege eingeschlagen werden und die Route mit den größeren Partitionen durch zwei ländlichere Gebiete führt, was die Anahl der Knoten insgesamt verringert. Dadurch werden auch weniger Knoten von Dijkstra untersucht, während sich der Größenunterschied beim A*-Algorithmus bemerkbar macht. Wird die Partition vergrößert, werden auch mehr Knoten untersucht. Wie man in der Tabelle 5.2 und im Diagramm 5.1 sehen kann, untersucht Dijkstra zwei bis vier Mal so viele Knoten wie Dijkstra. Wobei man bei der größten Partition am besten sehen kann, wie viele kürzeste Wege zu nicht relevanten Knoten ermittelt werden. Nur 0,2% der Knoten sind Bestandteil des kürzesten Weges, während es beim A*-Algorithmus immerhin gut 1,5% sind. Das ist zwar auch nicht viel, aber dafür wurden auch nur 15% der Gesamtknoten untersucht, statt 63% wie bei Dijkstra. Aber was sagt das über die Geschwindigkeit und die Güte der beiden Algorithmen aus? In Tabelle 5.3 wird gezeigt, wie lange die Routenberechnung ohne Graphaufbau gedauert hat. Es ist zudem die Anzahl der untersuchten Knoten der beiden Algorithmen dargestellt und die Größe der Partitionen. Dabei fällt auf, dass der A*-Algorithmus in den meisten Fällen weniger als 1% der Zeit benötigt, die Dijkstra rechnet. Im zweiten Experimente wurde die kürzeste Route gesucht, aber diesmal von Hamburg nach Dresden und die Heuristik c.a) wurde benutzt, es wurden also mehrere Randknoten KAPITEL 5. EXPERIMENTE 51 untersuchte Knoten ·105 1 Dijkstra A* 0.8 0.6 0.4 0.2 0 0.4 0.6 0.8 1 1.2 1.4 1.6 1.8 Anzahl Knoten ·105 Abbildung 5.1: A* und Dijkstra, Vergleich untersuchter Knoten P.Größe in km #unt. Kn. (D) Rout.Ber.(D) in s 3 080 34 249 31,23 3 300 39 954 40,07 4 265 49 298 68,89 4 620 40 835 53,61 10 265 109 396 638,01 #unt. Kn. (A*) Rout.Ber. (A*) in s 8 571 0,32 9 155 0,31 16 188 0,43 17 316 0,57 25 650 0,97 Tabelle 5.3: Dresden-München, Vergleich Routingdauer zwischen Dijkstra und A* als Ziel markiert. Es wurde eine abhängige Partitionierung genutzt. Die Metriken und die Partitionierung wurden verändert, um beispielhaft zu zeigen, dass diese keinen Einfluss auf die Laufzeit und die Güte der berechneten Routen der beiden Algorithmen haben. In der Tabelle 5.4 ist die Dauer der Routenberechnung dargestellt. Die Tabelle ist genauso wie die Tabelle 5.3 aufgebaut. Man kann die gleiche Beobachtung wie in der Tabelle 5.3 machen, nämlich dass der Dijkstra-Algorithmus bei jeder Partitionsgröße mehr Zeit benötigt. Besonders auffällig ist die Dauer bei großen Partitionen, wie der in der letzten Zeile. Insgesamt vier Stunden und vierzig Minuten benötigt Dijkstra um durch zwei Partitionen von Hamburg nach Dresden zu gelangen, während der A*-Algorithmus es in 73 Sekunden schafft. Das liegt daran, dass zum Beispiel in der Zielpartition der A*-Algorithmus nur 45 000 Knoten untersucht und dafür 3-8 Sekunden benötigt, während Dijkstra in der Zielpartition fast 300 000 Knoten untersuchen muss und dafür fast 110 Minuten braucht. Dadurch dass mehrere Knoten als Zielknoten innerhalb einer Partition markiert werden, können sich die kürzesten Wege unterscheiden, die von den beiden Algorithmen errechnet KAPITEL 5. EXPERIMENTE P.Größe in km #unt. Kn. (D) Rout.Ber.(D) in s 2 432 23 896 14,3 3 242 25 318 24,75 4 539 22 910 42,25 6 808 42 775 98,1 11 347 66 576 250,75 22 693 88 167 685,25 68 074 373 919 7 918,2 52 #unt. Kn. (A*) Rout.Ber. (A*) in s 10 354 0,675 14 758 0,89 17 728 1,13 22 517 2,26 38 898 3,85 36 446 4,35 150 492 36,5 Tabelle 5.4: Hamburg-Dresden, Vergleich Routingdauer zwischen Dijkstra und A* werden. Dijkstra bestimmt wirklich den kürzesten Weg zu einem Randpunkt, während der A*-Algorithmus den Randknoten zu finden versucht, der einen kurzen Weg aus vom Start aufweist und am nächsten am Zielpunkt liegt. Tatsächlich ermittelt Dijkstra in dieser Testreihe nur einmal einen schnelleren Weg als A* bei gleichen Partitionsgrößen, siehe Tabelle 5.5. In ihr ist dargestellt, welche Abweichungen die Routen der beiden Algorithmen von der optimalen Route aufweisen und wieviele Partitionen auf dem berechneten Weg liegen. P.Größe in km Abweichung (D) % Abweichung (A*) in % # Part. (D) # Part. (A*) 2 432 45,6 34,9 10 8 3 242 58,5 49,3 7 8 4 539 19,9 42,8 10 7 6 808 40,8 39,1 6 6 11 347 43,2 40,6 4 4 22 693 5,4 4,6 5 5 68 074 19,4 6,7 2 2 Tabelle 5.5: Hamburg-Dresden, Vergleich Güte zwischen Dijkstra und A* Dass die Abweichungen so groß sind, meistens zwischen 40 und 60%, liegt daran, dass der schnellste Weg zwischen Hamburg und Dresden über eine Autobahn durch Brandenburg führt und nicht wie die Luftlinie durch Sachsen-Anhalt. Im Falle des besseren Ergebnisses von Dijkstra, routet der A*-Algorithmus schon in der ersten Partition zu einem Randknoten, der nicht auf der optimalen Route liegt, während Dijkstra einen Umweg von drei Partition nimmt und ein doppelt so gutes Ergebnis errechnet. Siehe Bild 5.2. Die Route, die von Dijkstra gefunden wird ist in rot dargestellt, während der blaue Linienzug die berechnete Route vom A*-Algorithmus darstellt. Man kann erkennen, dass Dijkstras Weg in der Startpartition echt kürzer ist als der vom A*-Algorithmus, allerdings ist dies auch die einzige Partitionierung, in der der vorausberechnete Rand nicht erreicht wird, weshalb es zu diesem Umweg kommt, der trotzdem ein besseres Ergebnis liefert. KAPITEL 5. EXPERIMENTE 53 Abbildung 5.2: Vorteil von Dijkstra gegenüber A* An diesen zwei Beispielen kann man die Vorteile bezüglich der Laufzeit des A*-Algorithmus im Vergleich zu dem von Dijkstra erkennen. In den nachfolgenden Vergleichen wird daher auf Dijkstra verzichtet und alle Berechnungen mit dem A*-Algorithmus gemacht. 5.4 Große versus kleine Partitionen Die folgenden Experimente sollen zeigen, in wie fern die Größe der Partition die Güte der Routensuche bestimmt. Man sollte annehmen, dass große Parttionen ein besseres Ergebnis als kleine liefern, da man über mehr Informationen in einer Partition verfügt und man insgesamt weniger Partitionen untersuchen muss. Als erste Route wurde die schnellste Route von Hannover nach München mit der Heuristik c.b) mit einer unabhängigen Partitionierung gesucht. Dabei wurde die Fläche Deutschlands in Quadrate unterteilt. Die erste Tabelle 5.6, zeigt die Unterschiede verschiedener Partitionsgrößen in Gesamtrechendauer und Abweichung von der optimalen Route. Es sind zudem die Größe der Partitionen, die Dauer, in der der Graph aufgebaut wurde, die Anzahl der Knoten pro Partition und die Anzahl von Partitionen auf der Route angegeben. Man kann in dieser Tabelle erkennen, dass die Güte mit jeder Vergrößerung einer Partition besser wird. Die Zeit für die Erstellung des Graphen wächst scheinbar proportional mit der Anzahl der Knoten. Wie erwartet verringert sich auch die Anzahl der Partitionen mit der Vergrößerung der Partitionen. Das kann man auch an der Tabelle 5.7 ablesen. KAPITEL 5. EXPERIMENTE 54 Aufteilung Größe (km2 ) # Knoten # Part. Aufbau (s) Dauer (s) Abweichung (%) 10x10 6 052 115 120 10 24 318 34,19 8x8 9 392 141 260 7 28 287 31,44 6x6 17 011 311 755 5 56 496 9,62 4x4 37 989 608 616 4 105 734 9,11 3x3 69 023 1 101 969 2 206 677 5,15 Tabelle 5.6: Hannover-München, Vergleich Partitionsgrößen Sie zeigt die Ergebnisse der kürzesten Route von Hamburg nach Dresden, die mit der Heuristik c.a und einer abhängigen Partitionierung gesucht wurde. Aufteilung Größe (km2 ) # Knoten # Part. Aufbau (s) Dauer (s) Abweichung (%) 10x10 6 267 57 221 8 17 138 29,43 8x8 9 813 99 225 6 20 153 26,85 6x6 17 506 162 327 5 31 163 24,31 4x4 39 661 338 600 5 60 312 18,94 3x3 69 023 669 354 4 119 332 15,43 2x2 161 831 1 037 545 3 180 555 9,12 Tabelle 5.7: Hamburg-Dresden, Vergleich Partitionsgrößen Die Tabelle 5.8 zeigt jedoch, dass eine Partitionierung mit großen Partition nicht automatisch besser ist als eine mit kleineren Partitionen. In ihr sind die Ergebnisse einer Routensuche des schnellsten Weges von Dresden nach München mit einer unabhängigen Partitionierung und der Heuristik c.b) in dem gleichen Format wie die vorherigen zwei Tabellen dargestellt. In dieser Tabelle kann man einen Nachteil großer Parttionen erkennen, nämlich wenn eine Partition Straßen beinhaltet, die zwar zum kürzesten oder schnellsten Weg gehören, aber nicht vom Startpunkt in dieser Partition aus erreichbar sind. Da man keine Partition zweimal untersuchen kann, sind diese Straßen nicht mehr zu erreichen und folglich kann die optimale Route nicht mehr gefunden werden. Dieses Verhalten ist bei der Route von Dresden nach München bei einer Aufteilung von drei mal drei Gitterzellen zu sehen. Dafür ergibt die Aufteilung von vier mal vier Gittern eine Route, die der optimalen Route nahe kommt. Aufteilung Größe (km2 ) # Knoten # Part. Aufbau (s) Dauer (s) Abweichung (%) 8x8 9 392 130 265 6 26 177 17,77 6x6 17 011 191 572 4 46 257 3,79 4x4 37 989 405 843 3 71 320 1,18 3x3 69 023 888 601 3 148 686 7,82 Tabelle 5.8: Dresden-München, Vergleich Partitionsgrößen Die Ergebnisse zeigen, dass die Größe der Partitionen eine Rolle spielen, es aber auch KAPITEL 5. EXPERIMENTE 55 immer darauf an kommt, ob die Partitionierung glücklich gewählt ist. Wurde eine Route gesucht, deren Luftlinie nicht das Bundesgebiet verlässt, wurde durch eine Vergrößerung der Partitionen ein besseres Ergebnis erzielt. Die Route von Dresden nach München besitzt noch weitere Besonderheiten, wie die folgenden Abschnitte zeigen werden. 5.5 Unterschiede der Metriken Dieser Abschnitt soll die Unterschiede der beiden Metriken bei der partitionierten Routensuche deutlich machen. Dazu wurde mit der Heuristik c.b) und einer unabhängigen Partitionierung und verschiedenen Partitionsgrößen der schnellste und kürzeste Weg auf den den drei Teststrecken gesucht. In den Tabellen sind die Abweichungen der kürzesten und schnellsten berechneten Route zu den verschiedenen Aufteilungen dargestellt. In der Tabelle 5.9 sind die Ergebnisse der Route in Form der Abweichung von der optimalen Route in % von Hannover nach München vermerkt. In den meisten Fällen liegt die Güte der kürzesten Routen unterhalb der der schnellsten oder sie sind etwa gleich gut.. Aufteilung schnellste 10x10 34,36 8x8 31,44 6x6 9,62 4x4 9,11 3x3 5,15 kürzeste 30,25 12,44 8,04 9,50 3,57 Tabelle 5.9: Hannover-München, Vergleich Güte bezüglich der Metriken In der Tabelle 5.11 sind die Ergebnisse der Route von Dresden nach München dargestellt. Bei einer Aufteilung von zehn mal zehn Gitterzellen wird keine Route gefunden, siehe auch den Abschnitt über Heuristiken 5.6. Hier schneidet die schnellste Route etwas besser ab, als die schnellste Route zwischen Hannover und München. Die Aufteilung in drei mal drei Gitterzellen liefert ein ungenaueres Ergebnis als die Aufteilung von vier mal vier, da ein Teil des kürzesten Weges in einer Partition nicht erreichbar ist und so dieser Teil nicht mehr genutzt werden kann. In Tabelle 5.11 sind die Ergebnisse der Route Hamburg-Dresden enthalten. Hier liegt die Güte der kürzesten Route wieder für jede Aufteilung unterhalb der der schnellsten. Wie schon bemerkt sind die Abweichungen der schnellsten Route sehr groß, da die optimale Route nicht mit der Luftlinie übereinstimmt. Die Abweichungen sind zum Teil noch größer, wenn man eine abhängige Partitionierung verwendet, siehe Tabelle 5.5. Die Methode, den Zielrand über die geringste Entfernung zum Zielpunkt der Route zu ermitteln, ist also nicht immer optimal für das Finden der schnellsten Route. KAPITEL 5. EXPERIMENTE Aufteilung schnellste 10x10 k.E. 8x8 41,71 6x6 8,77 4x4 1,18 3x3 7,82 56 kürzeste k.E. 18,40 10,17 0,39 10,16 Tabelle 5.10: Dresden-München, Vergleich Güte bezüglich der Metriken Aufteilung schnellste 10x10 51,09 8x8 18,56 6x6 k.E. 4x4 15,50 kürzeste 20,28 9,20 4,15 1,19 Tabelle 5.11: Hamburg-Dresden, Vergleich Güte bezüglich der Metriken Insgesamt hat sich gezeigt, dass das Finden einer kürzesten Route mit einer geringen Abweichung zur optimalen meistens eher gelingt als mit der schnellsten Route. 5.6 Unterschiede der Heuristiken In diesem Abschnitt geht es um die Unterschiede zwischen den Heuristiken a), c.a), b) und c.b). Es wurden die drei bekannten Strecken bei fester Partitionsgröße einer Aufteilung von zehn mal zehn Gitterzellen untersucht. Es wurde eine abhängige Partitionierungen gewählt und diese mit den beiden Metriken kombiniert. In der Tabelle 5.12 sind die Abweichungen in Prozent der Route Hamburg-Dresden dargestellt. Metrik schnellste kürzeste Heuristik a Heuristik c.a Heuristik b Heuristik c.b 46,72 40,39 94,98 46,72 1,42 2,85 17,99 10,84 Tabelle 5.12: Hamburg-Dresden, Güte der verschiedenen Heuristiken Es tritt das wieder das Problem auf, dass die Abweichung sehr groß wird, wenn die schnellste Route nicht ungefähr mit der Luftlinie übereinstmmt. Trotzdem ist es schon überraschend, dass die Heuristik b) eine fast doppelt so langsame Strecke ermittelt. Die kürzesten Strecken sind gemessen an der geringen Partitionsgröße bei den Heuristiken a und c.a überraschend nah an der optimalen Route mit jeweils weniger als 3% Abweichung. Die Tabelle 5.13 liegt im gleichen Format vor und beschreibt die Abweichungen der KAPITEL 5. EXPERIMENTE Metrik schnellste kürzeste 57 Heuristik a Heuristik c.a Heuristik b Heuristik c.b 17,30 10,66 k.E. 13,51 13,45 7,86 k.E. 9,25 Tabelle 5.13: Dresden-München, Güte der verschiedenen Heuristiken Strecke Dresden-München. Bei dieser Route schneidet die Heuristik c.a) am besten ab. Die Heuristik b) gerät mit ihrem gewählten Zielknoten in eine Sackgasse. Im Vergleich zu der schnellsten Strecke von Hamburg nach Dresden sind die Abweichungen bei der schnellsten Route von Dresden nach München geringer, obwohl auch hier die schnellste Route nicht mit der Luftlinie übereinstimmt. In tabelle 5.14 sind die Abweichungen zur optimalen Route bezüglich der Route von Hannover nach München dargestellt. Auch hier sind die Ergebnise für diese kleine Partitionsgröße mit den Heuristiken a) und c.a) ziemlich gut mit weniger als 2%. Die Abweichung bei der Heuristik b) hingegen ist wieder sehr groß, auch auf dieser Strecke stimmt die schnellste Route nur zum Teil mmit der Luftlinie überein. Metrik schnellste kürzeste Heuristik a Heuristik c.a Heuristik b Heuristik c.b 6,36 6,01 89,35 5,50 1,19 1,70 10,06 5,97 Tabelle 5.14: Hannover-München, Güte der verschiedenen Heuristiken Insgesamt liegt die Heuristik b, was die Güte betrifft zum Teil deutlich - 60% Unterschied und mehr in der Abweichung - hinter den anderen zurück. Die Routen, die ermittelt werden sehen auch nicht natürlich aus, sie sind sehr zackig, siehe Abbildung 5.3. Es ist die schnellste Route von Hannover nach München mit der Heuristik b) dargestellt. Es ist eine Aufteilung in zehn mal zehn Gitterzellen genutzt worden mit einer unabhängigen Partitionierung. Bei der unabhängigen Partitionierung ist noch deutlicher das Verhalten dieser Heuristik zu erkennen und liefert auch ein noch schlechteres Ergebnis, nämlich eine Abweichung von 95%. Die kürzeste Route in Kombination mit der Heuristik a) liefern meistens die besten Ergebnisse. Es kommt allerdings auf eine glückliche Partitionierung an, siehe die kürzeste Strecke von Dresden nach München, in der die Heuristik c.a das beste Ergebnis liefert. Wird die schnellste Route gesucht, schneiden die Heuristiken c.a) und c.b) am Besten ab. Siehe die drei Tabellen dieses Abschnittes und zum Beispiel Tabelle 5.8 in der mit der Heuristik c.b) eine Abweichung von nur 1,18% erreicht wird. In der Abbildung 5.4 ist allgemein die Probleme der Heuristiken zu sehen. Dargestellt ist die kürzeste optimale Route in grün von dem oberen Punkt zum unteren. Die untere rote Linie symbolisiert die Route, die mit der Heuristik a bestimmt wurde, während die obere Route mit der Heuristik b bestimmt wurde. Die schwarze Linie ist ein Partitionsrand. Die KAPITEL 5. EXPERIMENTE Abbildung 5.3: Probleme der Heuristiken Abbildung 5.4: Probleme der Heuristiken 58 KAPITEL 5. EXPERIMENTE 59 Heuristiken c.a und c.b sind nicht dargestellt, da sie eine Autobahn als Zielrandknoten bestimmt, der sehr weit von dem Zielpunkt entfernt liegt. Die Heuristiken sind eben nur Heuristiken und bestimmen nicht zwangsläufig den optimalen Zielrandknoten. Rein optisch wurde von der Heuristik a) der kürzeste Weg in der Startpartition bestimmt, nur ist dieser gewählte Randknoten nicht Teil der optimalen Route. Den Zielpunkt, den die Heuristik b) gewählt hat, liegt zwar am nächsten zum Zielpunkt, aber über diesen Randknoten führt der optimale kürzeste Weg nicht. Dieses Beispiel ist zwar konstruiert, aber es verdeutlicht das Problem, dass je mehr Partitionen genutzt werden, die Wahrscheinlichkeit steigt, dass wie in der Abbildung nicht die optimalen Randknoten gewählt werden. Die Ergebnisse zeigen, dass die Heuristik b) in den meisten Fällen ungeeignet ist den kürzesten Weg zu finden. Die Heuristen c.a) und c.b) können der optimalen schnellsten Route nahe kommen, sofern sie mit der Luftlinie übereinstimmt. Die Heuristik a) liefert meistens gute Ergebnisse auf der Suche nach der kürzesten Route. 5.7 Abhängige versus unabhängige Partitionierung Im letzen Abschnitt befassen wir uns mit der Frage, welchen Vorteil eine abhängige Partitionierung bringt oder ob sie die Ergebnisse der Routensuche überhaupt verbessern kann. In den folgenden Tabellen sind die Ergebnisse des letzten Abschnitts noch um die Abweichungen der unabhängigen Partitionierung erweitert. Es ist also wieder die Abweichung der verschiedenen Heuristiken bei einer Partitionierungsaufteilung von zehn mal zehn Gitterzellen dargestellt. Die Tabelle 5.15 zeigt diese für die Route Hamburg-Dresden. Für beide Metriken und alle Heuristiken liefert die abhängige Partitionierung ein besseres Ergebnis. Bei der unabhängigen Partitionierung in Kombination mit der Heuristik b wird eine Route gefunden, die in eine Sackgasse auf halber Strecke führt. Auch kann man hier wieder sehen, dass die Auswahl des Zielrandes über die kürzeste Distanz zum Zielpunkt, nicht geeignet ist zur Findung der schnellsten Route. Partitionierung unabhängig abhängig Metrik schnellste kürzeste schnellste kürzestee Heuristik a Heuristik c.a Heuristik b Heuristik c.b 52,40 51,09 k.E. 47,82 16,40 20,28 k.E. 17,60 46,72 40,39 94,98 46,72 1,42 2,85 17,99 10,84 Tabelle 5.15: Hamburg-Dresden, Güte bzgl. der Partitionierung und Heuristiken KAPITEL 5. EXPERIMENTE 60 Abbildung 5.5: Nachteil der unabhängige Partitionierung Die Tabelle 5.16, in der die Ergebnisse der Routensuche von Hannover nach München dargestellt ist, bestätigt den Eindruck, dass die abhängige Partitionierung geeigneter zur Routensuche ist. Wie bei der Route Hamburg-Dresden sind alle Ergebnisse mit einer abhängigen Partitionierung genauer. Bei der Heuristik c.a und der kürzesten Route sogar um 50%. Partitionierung unabhängig abhängig Metrik schnellste kürzeste schnellste kürzeste Heuristik a Heuristik c.a Heuristik b Heuristik c.b 8,93 30,07 95,70 34,36 k.E. 54,51 26,09 31,07 6,36 6,01 89,35 5,50 1,19 1,70 10,06 5,97 Tabelle 5.16: Hannover-München, Güte bzgl. der Partitionierung und Heuristiken In Abbildung 5.5 kann man den Nachteil der unabhängigen Partitionierung sehen, wenn der Startpunkt nahe der Partitionsgrenze liegt. Dargestellt sind die Routen die bei einer Aufteilung von 8x8 Gitterzellen entstehen (blau) und bei einer Aufteilung von 6x6 (rot). Der Zielrand der ersten Partition bei der Aufteilung 8x8 ist schwarz hervorgehoben. Es stehen bei der Partitionierung mit den kleineren Partitionen nicht genug Informationen in der Startpartition zur Verfügung, wodurch eine komplett andere Route gefunden wird und ein Unterschied von 20% in der Abweichung entsteht. Die Tabelle 5.17 zeigt die Ergebnisse der Route Dresden-München. Auffällig ist, das bei der unabhängigen Partitionierung einzig in Kombination mit der Heuristik b eine Route gefunden wird. Das liegt daran, dass bei den anderen Heuristiken zu früh eine Partition KAPITEL 5. EXPERIMENTE Partitionierung unabhängig abhängig Metrik schnellste kürzeste schnellste kürzeste 61 Heuristik a Heuristik c.a Heuristik b Heuristik c.b k.E. k.E. 128,20 k.E. k.E. k.E. 11,86 k.E. 17,30 10,66 k.E. 13,51 13,45 7,86 k.E. 9,25 Tabelle 5.17: Dresden-München, Güte bzgl. der Partitionierung und Heuristiken verlassen wird und in einen Teil einer Partition gerät, den man nicht wieder verlassen kann. Gleichzeitig findet man mit der Heuristik b) keine Route bei einer abhängigen Partitionierung, dies liegt allerdings an einem Modellierungsfehler in den OSM-Daten, sodass die Route in eine Sackgasse führt. Bei der Suche nach der schnellsten oder kürzesten Route von Dresden nach München schneidet die Heuristik a), abgesehen von der Aufteilung von zehn mal zehn Gitterzellen, immer schlechter ab als die anderen Heuristiken. Das liegt daran, dass sowohl die kürzeste als auch die schnellste Route nicht entlang der Luftlinie führen kann, da diese das Bundesgebiet zwischenzeitlich verlässt. Die Tabelle 5.18 ist eine Erweiterung der Tabelle 5.11, in ihr ist zusätzlich die Abweichungen einer abhängigen Partitionierung dargestellt. Benutzt wurde also die Heuristik c.b). Bis auf eine Ausnahme sind mit einer abhängigen Partitionierung bessere kürzeste Routen als mit der unabhängigen errechnet worden. Durch die Heuristik c.b) werden die Ergebnisse aber auch ungenauer mit größeren Partitionen, bis auf die Aufteilung vier mal vier. Aufteilung 10x10 8x8 6x6 4x4 abhängig unabhängig schnellste kürzeste schnellste kürzeste 40,39 2,85 51,09 20,28 43,45 8,94 18,56 9,20 48,47 12,27 k.E 4,15 19,00 1,12 15,50 1,19 Tabelle 5.18: Hamburg-Dresden, Vergleich Güte bzgl. der Partitionierung und den Metriken Die Tabelle 5.19 zeigt im gleichen Format die Abweichungen der Strecke Dresden-München mit der Heuristik c.b). Auf dieser Strecke wurde mit einer abhängigen Partitionierung die schnellste Route gemessen an der Abweichung besser errechnet als die kürzeste. Das hängt mit der Heuristik zusammen, die besser schnellere Routen bestimmen kann. Zudem wird die Routensuche ohnehin schwieriger, da die sie nicht nicht nur der Luftlinie folgen kann. Wurde die kürzeste Route bei der unabhängigen Partitionierung gefunden, wurde KAPITEL 5. EXPERIMENTE 62 bis auf eine Ausnahme eine bessere kürzeste Route berechnet als mit der abhängigen Partitionierung. Die Tabelle zeigt aber vor allem eins, nämlich dass eine optimale Route in partitionierten Straßendaten gefunden werden kann, allerdings wurden hierfür auch nur zwei Partitionen untersucht. Aufteilung 10x10 8x8 6x6 4x4 3x3 abhängig unabhängig schnellste kürzeste schnellste kürzeste 10,66 29,43 k.E. k.E. 20,85 26,85 41,71 18,40 21,09 24,31 8,77 10,17 0,00 18,94 1,18 0,39 0,00 9,12 7,82 10,16 Tabelle 5.19: Dresden-München, Vergleich bzgl. der Partitionierung und den Metriken Die Ergebnisse zeigen, dass eine abhängige Partitionierung in Verbindung mit der Heuristik a) meist bessere Ergebnisse liefert, als die abhängige Partitionierung. Liegt die schnellste Route nicht auf der Luftlinie, tragen beide Partitionierungen selten zu einer guten schnellsten Route bei. 5.8 Fazit Die Ergebnisse der Experimente haben gezeigt, dass es schwierig ist die Partitionierung und Heuristik im Vorfeld zu bestimmen, die zu einer optimalen kürzesten Route führt. Siehe zum Beispiel das Beispiel der besseren Routenberechnung von Dijkstra, die nur entsteht, da der Zielrand nicht erreicht werden kann. So kann ein Teil des optimalen schnellsten Weges genutzt werden, der sonst nicht in Betracht gezogen wird, da er nicht zum Zielrand führt. Der A*-Algorithmus nützt die Zusatzinformation der geographischen Lage der Knoten effizient aus, was sich in der Anzahl der untersuchten Knoten und damit in der Laufzeit im Vergleich zu Dijkstra bemerkbar macht. Eine Vergrößerung der Partitionen ist solange ein Vorteil, bis ein Zielrand nicht erreicht wird. Dann muss die Partition gewechselt werden und Strecken der optimalen kürzesten Route können nicht mehr erreicht werden. Die Auswahl des Zielrandes über die kürzeste Entfernung zum Zielpunkt der Route ist für die Findung der schnellsten Route nicht immer optimal, wenn die Partitionen nicht groß genug gewählt wurde. Das man die optimale Route finden kann, zeigt die Tabelle 5.19. KAPITEL 5. EXPERIMENTE 63 Die Heuristik a) liefert zum Teil auch bei kleinen Partitionen ein annehmbares Ergebnis, solange der die kürzeste Route auch über die Luftlinie führt. Die Heuristik b) liefert in der Regel die Ergebnisse mit der höchsten Abweichung. Das Problem des nicht erreichten Zielknotens kann auch mit den Heuristiken b) und c.b) auftreten und macht dadurch vor allem die Heuristik b) noch unattraktiver zur Bestimmung des kürzesten Weges. Die abhängige Partitionierung kann die Ergebnisse der Routensuche verbessern, hat aber die gleichen Probleme, wie die abhängige, wenn der Zielrand nicht erreicht wird oder die gesuchte Route nicht mit der Luftlinie übereinstimmt. Kapitel 6 Ausblick Insgesamt hat sich gezeigt, dass es nicht zwangsläufig eine perfekte Partitionierung zur optimalen Routenbestimmung gibt. Es können noch alternative Routing-Möglichkeiten umgesetzt werden, zum Beispiel das hierarchische Kürzeste-Wege-Suchen. Man könnte die Routensuche zusätzlich auch vom Endpunkt zum Startpunkt durchführen und sie mit der normalen Route vergleichen. Der A*-Algorithmus verletzt unter gewissen Umständen die Heuristik a), das könnte vermieden werden, in dem alle markierten Randknoten gefunden werden und von diesen den mit dem kürzesten Weg auszuwählen. Allerdings würde dann, falls ein einziger dieser Randknoten nicht erreicht würde, wieder alle Kanten zu erreichbaren Knoten relaxiert werden, was der A*-Algorithmus ja gerade vermeiden soll. Man könnte auch die Routensuche, die bisher nur von einem Start- zu einem Zielpunkt auf mehrere Start- und Zielpunkte erweitern. Die vorhandenen Daten aus OpenStreetMap könnten noch besser für das Routing genutzt werden. So existieren noch mehr Tags, die man beachten könnte, wie ein maxspeed -Tag oder ein Tag für Ampeln, den man benutzen könnte, um zum Beispiel eine Wartezeit pro Ampel auf den schnellsten Weg addieren könnte. Die Art der Partitionen könnten auf beliebige Polygone erweitert werden, wie Polygone, die die Bundesländer beschreiben. Falls man eine genauere Beschreibung der Route wünscht, müsste diese Beschreibung ebenfalls bei der Komposition der Routen zusamengeführt werden, damit man nicht durch die Partitionierung die Anweisungen “zehn Kilometer auf der A2” und “nach fünf Kilometern rechts abbiegen” erhält, sondern “nach fünfzehn Kilometern auf der A2 rechts abbiegen”. 64 Anhang A Das simple Osmosis-PostGIS-Schema Osmosis ist ein Kommandozeilen-Werkzeug zur Verarbeitung von OSM-Daten. Um die OSM-Daten, die zum Beispiel im .osm-Format vorliegen, in eine PostGIS-Datenbank zu laden, muss diese Tabellen gemäß dem Osmosis PostGIS simple Schema vorliegen. Im Osmosis-Software-Paket befindet sich ein Ordner script, in dem Erstellungsskripte für dieses Schema vorliegen. Benötigt werden für unsere Anwendung die Skripte pgsimple schema 0.6.sql und pgsimple schema 0.6 linestring.sql, das in der WAYS-Tabelle eine LineString-Geometrie hinzufügt. Insgesamt werden folgende Tabellen erstellt: SCHEMA INFO USERS NODES NODE TAGS WAYS WAY NODES WAY TAGS RELATIONS RELATION MEMBERS RELATION TAGS (Version) (Id, Name) (Id, Version,User ID → USERS, Tstamp, Changeset Id, Geom) (Node Id → NODES, K, V) (Id, Version,User ID → USERS, Tstamp, Changeset Id, Linestring) (Way Id → WAYS, Node Id → NODES, Sequence Id) (Way Id → WAYS, K, VNULL ) (Id, Version,User ID → USERS, Tstamp, Changeset Id) (Relation Id → RELATIONS, Member Id, Member Type, Member Role, Sequence Id) (Relation Id → RELATIONS, K, V) Wenn nicht anders angegeben, wird NOT NULL für die Attribute angenommen. Zusätzlich werden räumliche Indexe für die Geometrien Geom und Linestring angelegt, sowie Indexe für die Tag-Relationen für den jeweiligen Typen (Node Id, Relation Id und Way Id ) und bei der WAY NODES-Relation für die Way Id. 65 Anhang B PL/pgSQL-Funktionen B.1 Funktionen zum Erstellen der Edges-Tabelle Die folgende Funktion nimmt einen Linestring entgegen und gibt den Teil des Linestrings zurück, der zwischen dem first-ten und last-en Knoten besteht. CREATE OR REPLACE FUNCTION makeSubLineString(geometry, integer, integer) RETURNS geometry AS $BODY$ DECLARE line ALIAS FOR $1; first ALIAS FOR $2; last ALIAS FOR $3; result geometry; BEGIN --ST_PointN beginnt bei 1, die Sequence_id bei 0 result:=ST_MakeLine(ST_PointN(line,first+1),ST_PointN(line,first+2)); FOR i IN first+2..last LOOP result:=ST_AddPoint(result, ST_PointN(line,i+1)); END LOOP; return result; END; $BODY$ 66 ANHANG B. PL/PGSQL-FUNKTIONEN 67 Die folgenden Sichten edge view und node view dienen nur zur Abkürzung der Abfragen in der nächsten Funktion. CREATE OR REPLACE VIEW edge_view AS SELECT DISTINCT ways.id, ways.linestring, wt2.v AS highway, CASE wt1.v WHEN ’1’ THEN ’yes’ WHEN ’-1’ THEN ’vice_versa’ WHEN ’true’ THEN ’yes’ WHEN ’yes’ THEN ’yes’ ELSE ’no’ END AS oneway FROM ways LEFT JOIN way_tags wt1 ON ways.id = wt1.way_id AND wt1.k = ’oneway’ JOIN way_tags wt2 ON ways.id = wt2.way_id WHERE wt2.k = ’highway’ AND (w2.v = ANY (ARRAY[’motorway’, ’motorway_link’, ’trunk’, ’trunk_link’, ’primary’, ’primary_link’, ’secondary’, ’tertiary’, ’residential’, ’living_street’, ’unclassified’, ’service’, ’track’])); Die Highway-Tags sind in Abschnitt 3.3.2 beschrieben und die genaue Auflistung ist bei der Node View weggelassen worden. Alternativ kann man auch zwei Edge Views joinen, aber in denen wäre jeweils ein unnötiger Join für die LineString-Geometrie. CREATE OR REPLACE VIEW node_view AS SELECT DISTINCT wn1.way_id, wn1.node_id, nodes.geom, wn1.sequence_id FROM way_nodes wn1 JOIN way_nodes wn2 ON wn1.node_id = wn2.node_id AND wn1.way_id <> wn2.way_id JOIN nodes ON nodes.id = wn1.node_id JOIN way_tags wt1 ON wn1.way_id = wt1.way_id JOIN way_tags wt2 ON wn2.way_id = wt1.way_id WHERE wt1.k = ’highway’ ... AND wt2.k = ’highway’ ... --siehe oben ORDER BY wn1.way_id; Die folgende Funktion legt die Tabelle EDGES neu an. CREATE OR REPLACE FUNCTION buildEdgeTable() RETURNS void AS $BODY$ BEGIN --Die Tabelle edges neu anlegen DROP TABLE IF EXISTS edges; ANHANG B. PL/PGSQL-FUNKTIONEN 68 CREATE TABLE edges ( edge_id text, linestring geometry, oneway text, highway text, length double precision, node_id1 text, node_id2 text, ); CREATE INDEX index_linestring ON edges USING gist (linestring); END; $BODY$ LANGUAGE plpgsql Die Funktion buildEdges() erstellt die Kanten für den Graphen. Dabei werden für alle Straßen, die jenigen OSM-Nodes identifiziert, die Kreuzungen auf dieser Straße darstellen und an diesen wird der Linestring des OSM-Ways aufgespalten. CREATE OR REPLACE FUNCTION buildedges() RETURNS void AS $BODY$ DECLARE --Die richtigen Automobilstraßen herausfinden way_cursor Cursor IS SELECT * FROM edge_view; --Kreuzungen ermitteln edge_cursor Cursor (id bigint) IS SELECT * FROM node_view WHERE way_id=id; i integer; edge_record Record; old_seqence_id integer; old_node_id bigint; new_edge geometry; line geometry; BEGIN SELECT buildEdgeTable(); --Jede Straße an den Kreuzungen in Kanten umwandeln FOR way_record IN way_cursor LOOP i:=0; Open edge_cursor(way_record.id); FETCH edge_cursor INTO edge_record; ANHANG B. PL/PGSQL-FUNKTIONEN 69 old_seqence_id:=edge_record.sequence_id; old_node_id:=edge_record.node_id; FETCH edge_cursor INTO edge_record; --den LineString zwischen den sequence_id’s aufspalten WHILE FOUND LOOP line:=makeSubLineString(way_record.linestring, old_seqence_id, edge_record.sequence_id); INSERT INTO edges VALUES (way_record.id||’_’||i, line, way_record.oneway,way_record.highway, ST_Length(line,true), old_node_id,edge_record.node_id); i:=i+1; old_seqence_id:=edge_record.sequence_id; old_node_id:=edge_record.node_id; FETCH edge_cursor INTO edge_record; END LOOP; CLOSE edge_cursor; END LOOP; END; $BODY$ LANGUAGE plpgsql B.2 Funktionen zum Finden von Start- und Zielknoten Die Funktion liefert den Knoten, der innerhalb eines Radius von 500m, am nächsten zum übergebenen Punkt liegt. CREATE OR REPLACE FUNCTION getnodeidforPoint(IN geometry, OUT nodeid text, OUT resultPoint geometry) AS $BODY$ DECLARE point ALIAS FOR $1; edge edges%ROWTYPE; BEGIN --Die Kante ermittelt die am nächsten zum übergebenen Punkt liegt SELECT * FROM edges WHERE ST_DWithin(point,linestring,500,TRUE) ORDER BY ST_Distance(linestring,point,TRUE) LIMIT 1 INTO edge; IF ST_Distance(ST_Startpoint(edge.linestring),point)<= ST_Distance(ST_Endpoint(edge.linestring),point) THEN nodeId:=edge.node_id1; ANHANG B. PL/PGSQL-FUNKTIONEN 70 resultPoint:=st_startpoint(edge.linestring); ELSE nodeId:=edge.node_id2; resultPoint:=st_endpoint(edge.linestring); END IF; RETURN; END; $BODY$ LANGUAGE plpgsql Die Funktion liefert ein Plygon zurück, in dem nach der Straße gesucht werden kann. CREATE OR REPLACE FUNCTION getPostcodePolygon(IN postcode text, IN street text, OUT postcodepolygon geometry) RETURNS geometry AS $BODY$ BEGIN SELECT ST_Buildarea(ST_Collect(linestring)) INTO postcodepolygon FROM relation_members r1 JOIN relation_tags r2 USING (relation_id) JOIN ways ON r1.member_id=id WHERE (member_role=’outer’ OR member_role=’’) AND r2.k=’postal_code’ AND v=postcode; --wenn kein Polygon gefunden wurde, dann selbst eines "basteln"... IF postcodepolygon IS NULL THEN SELECT ST_ENVELOPE(ST_COLLECT(ARRAY(( (SELECT geom FROM node_tags JOIN nodes n ON n.id=node_id WHERE v=postcode AND k IN (’postal_code’,’addr:postcode’)) --...indem um alle Knoten und Kanten mit dieser Postleitzahl --eine Boundingbox gelegt wird UNION (SELECT linestring FROM way_tags JOIN ways w ON w.id=way_id WHERE v=postcode AND k IN (’postal_code’,’addr:postcode’)))))) INTO postcodepolygon; END IF; END; $BODY$ LANGUAGE plpgsql ANHANG B. PL/PGSQL-FUNKTIONEN 71 Die Funktion ermittelt einen Knoten, der zu einer Straße mit dem übergebenen Namen und in dem Postleitzahlgebiet liegt. CREATE OR REPLACE FUNCTION getnodeidforaddress(IN text, IN text, OUT nodeid text, OUT point geometry) AS $BODY$ DECLARE postcode ALIAS FOR $1; street ALIAS FOR $2; postcodepolygon geometry; findEdge CURSOR (line geometry) IS SELECT node_id1,linestring,node_id2 FROM edges WHERE ST_Intersects(postcodepolygon,linestring) ORDER BY ST_Distance(linestring,line) LIMIT 1; findWay CURSOR (geom geometry) IS SELECT linestring FROM ways JOIN way_tags ON id=way_id WHERE ST_Intersects(geom,linestring) AND k=’name’ AND v=street LIMIT 1; BEGIN SELECT * FROM getPostCodePolygon(postcode,street) INTO postcodepolygon; FOR way IN findWay(postcodepolygon) LOOP FOR edge IN findEdge(way.linestring) LOOP IF ST_Distance(ST_Startpoint(edge.linestring),way.linestring)<= ST_Distance(ST_Endpoint(edge.linestring),way.linestring) THEN nodeId:=edge.node_id1; point:=ST_Startpoint(edge.linestring); ELSE nodeId:=edge.node_id2; point:=ST_Endpoint(edge.linestring); END IF; END LOOP; END LOOP; RETURN; END; $BODY$ LANGUAGE plpgsql ANHANG B. PL/PGSQL-FUNKTIONEN B.3 72 Funktionen zum Graphaufbau Liefert alle Kanten, die nicht geclippt werden müssen. CREATE OR REPLACE FUNCTION getEdges(polygon geometry) RETURNS SETOF edges AS $BODY$ BEGIN --einfach nur die Kanten zurückgeben, die im Polygon beginnen und auch enden RETURN QUERY SELECT * FROM edges WHERE ST_Contains(polygon,ST_Startpoint(linestring)) AND ST_Contains(polygon, ST_Endpoint(linestring)); END; $BODY$ LANGUAGE plpgsql Liefert alle Kanten, die in der gegebenen Partition beginnen und clippt sie vorher CREATE OR REPLACE FUNCTION getBeginningBorderedges(polygon geometry) RETURNS SETOF edges AS $BODY$ DECLARE count integer; temp geometry; result edges%rowtype; --Die Kanten die in dieser Partition beginnen, können gleich mit "Intersection()" --abgeschnitten werden, es wird ja nur dieser Teil des LineStrings benötigt beginning CURSOR FOR SELECT edge_id,ST_Intersection(polygon,linestring) linestring, oneway, highway,length,node_id1,node_id2 FROM edges WHERE ST_Crosses(polygon, linestring) AND ST_Contains(polygon, ST_Startpoint(linestring)) AND NOT ST_Contains(polygon, ST_Endpoint(linestring)); BEGIN --Die abgeschnittenen Kanten können so zurückgegeben werden, es sei denn es --gab mehrere Ergebnisse bei der Intersection(), dann wird nur die --erste Geometrie übernommen FOR edge IN beginning LOOP result:=edge; result.node_id2:=edge.edge_id||’_e’; IF(ST_Geometrytype(edge.linestring)=’ST_MultiLineString’) THEN result.linestring:=ST_Geometryn(edge.linestring,1); END IF; result.length:=ST_Length(result.linestring,TRUE); ANHANG B. PL/PGSQL-FUNKTIONEN 73 RETURN NEXT result; END LOOP; RETURN; END; $BODY$ LANGUAGE plpgsql Liefert alle geclippten Kanten, die in der Partition enden. CREATE OR REPLACE FUNCTION getEndingBorderEdges(polygon geometry) RETURNS SETOF edges AS $BODY$ DECLARE count integer; temp geometry; result edges%rowtype; --Für die Kanten, die in dieser Partition enden, muss noch geprüft werden, --welche wie geclippt werden muss ending CURSOR FOR SELECT * FROM edges WHERE ST_Crosses(polygon, linestring) AND NOT ST_Contains(polygon, ST_Startpoint(linestring)) AND ST_Contains(polygon, ST_Endpoint(linestring)); BEGIN FOR edge IN ending LOOP result:=edge; result.node_id1:=edge.edge_id||’_b’; --Es wird der Schnittpunkt des Polygonrandes mit dem --Kanten-LineString ermittelt temp:=ST_Intersection(edge.linestring,ST_Boundary(polygon)); --wenn die Kante den Polygonrand nur einmal schneidet, ist alles --in Ordnung, wenn nicht... IF ST_Geometrytype(temp)=’ST_MultiPoint’ THEN --muss hier geprüft werden, ob der erste oder letzte dieser Punkte näher --zum Startpunkt liegt und dann dieser als Schnittpunkt festgelegt werden IF ST_Distance(ST_Startpoint(edge.linestring),ST_Geometryn(temp,1)) < ST_Distance(ST_Startpoint(edge.linestring), ST_Geometryn(temp,ST_Numgeometries(temp))) THEN temp:=ST_Geometryn(temp,1); ELSE temp:=ST_Geometryn(temp,ST_Numgeometries(temp)); END IF; END IF; --ST_Line_Substring(a,b) liefert den linestring von a nach b, aber es sind --keine Koordinaten, sondern relative Werte z.B. 0.5, diesen ANHANG B. PL/PGSQL-FUNKTIONEN 74 --berechnet ST_Locate_Point result.linestring:=ST_Line_Substring(edge.linestring, ST_Line_Locate_Point(edge.linestring,temp),1); result.length:=ST_Length(result.linestring,TRUE); RETURN NEXT result; END LOOP; RETURN; END; $BODY$ LANGUAGE plpgsql Gibt die Kanten zurück, die in der Partition enden oder beginnen. CREATE OR REPLACE FUNCTION getSimpleBorderEdges(polygon geometry) RETURNS SETOF edges AS $BODY$ BEGIN --einfach nur die Kanten dieser Funktionen zurückgeben RETURN QUERY SELECT * FROM getBeginningBorderEdges(polygon) UNION SELECT * FROM getEndingBorderEdges(polygon); END; $BODY$ LANGUAGE plpgsql In den folgenden Funktion tauchen immer wieder die gleichen Parameter auf. Border1 bezeichnet die Geometrie, die entsteht, wenn man einen Rand, der den LineString der Kante schneidet, border2 analog. Wenn firstBorder wahr ist, heißt das, dass border1 aus der Sicht des LineStrings zuerst geschnitten wurde. Prüft, ob der linestring das polygon an mindestens zwei Rändern schneidet und gibt die Schnittmenge der Ränder mit dem Linienzug zurück, sowie welcher Rand zuerst geschnitten wurde. CREATE OR REPLACE FUNCTION realCrossing(IN polygon geometry, IN linestring geometry, OUT firstborder boolean, OUT count integer, OUT border1 geometry, OUT border2 geometry) AS $BODY$ DECLARE temp geometry; BEGIN firstborder:=FALSE; ANHANG B. PL/PGSQL-FUNKTIONEN 75 count:=0; border1:=NULL; --die Geometrien, die entstehen, wenn border2:=NULL; --man den Polygonrand mit dem LineString schneidet --zählen, wie viele Polygonränder geschnitten wurden FOR i IN 0..3 LOOP temp:=makeSubLinestring(ST_Boundary(polygon),i,i+1); IF ST_Intersects(temp,linestring) THEN IF count=0 THEN border1:=ST_Intersection(temp,linestring); count:=count+1; ELSE border2:=ST_Intersection(temp,linestring); --firstBorder zeigt an, dass der erste Rand der gefunden wurde näher --am Startpunkt des Linestrings liegt IF ST_Distance(border1,ST_Startpoint(linestring))<= ST_Distance(border2,ST_Startpoint(linestring)) THEN firstborder:=TRUE; END IF; count:=count+1; END IF; END IF; END LOOP; END; $BODY$ LANGUAGE plpgsql Ermittelt, falls der linestring den Polygonrand mehrfach geschnitten hat, welcher der Punkt zum clippen ist. CREATE OR REPLACE FUNCTION getRightPoint(IN linestring geometry, IN multipoint geometry, IN toEnd boolean, OUT point geometry) AS $BODY$ DECLARE BEGIN --toEnd bedeutet, dass der LineString "hinten" abgeschnitten werden soll IF toEnd THEN --prüfen, ob der erste oder letzte Punkt von --multipoint näher zum Ende liegt IF ST_Distance(ST_Endpoint(linestring),ST_Geometryn(multipoint,1)) < ST_Distance(ST_Endpoint(linestring), ST_Geometryn(multipoint,ST_Numgeometries(multipoint))) THEN point:=ST_Geometryn(multipoint,1); ANHANG B. PL/PGSQL-FUNKTIONEN 76 ELSE point:=ST_Geometryn(multipoint,ST_Numgeometries(multipoint)); END IF; ELSE --analog, nur mit Startpunkt IF ST_Distance(ST_Startpoint(linestring),ST_Geometryn(multipoint,1)) < ST_Distance(ST_Startpoint(linestring), ST_Geometryn(multipoint,ST_Numgeometries(multipoint))) THEN point:=ST_Geometryn(multipoint,1); ELSE point:=ST_Geometryn(multipoint,ST_Numgeometries(multipoint)); END IF; END IF; END; $BODY$ LANGUAGE plpgsql Bastelt den LineString, der von beiden Seiten geclippt wurde CREATE OR REPLACE FUNCTION buildSubLineString(IN linestring geometry, IN border1 geometry, IN border2 geometry, OUT line geometry) AS $BODY$ DECLARE BEGIN line:=linestring; --gegebenenfalls den richtigen Point aus den Multipoints auswählen IF ST_Geometrytype(border1)=’ST_MultiPoint’ THEN SELECT point INTO border1 FROM getRightPoint(linestring, border1, FALSE); END IF; IF ST_Geometrytype(border2)=’ST_MultiPoint’ THEN SELECT point INTO border2 FROM getRightPoint(linestring, border1, TRUE); END IF; --nur einen neuen LineString erstellen, wenn der erste Randpunkt --vor dem zweiten auf dem LineString liegt IF ST_Line_Locate_Point(linestring,border2)> ST_Line_Locate_Point(linestring,border1) THEN line:=ST_Line_Substring(edge.linestring, ST_Line_Locate_Point(linestring,border1), ST_Line_Locate_Point(linestring,border2)); END IF; ANHANG B. PL/PGSQL-FUNKTIONEN 77 END; $BODY$ LANGUAGE plpgsql Gibt die Kanten zurück, die von beiden Seiten geclippt wurden. CREATE OR REPLACE FUNCTION getComplicatedBorderEdges(polygon geometry) RETURNS SETOF edges AS $BODY$ DECLARE firstborder boolean; count integer; border1 geometry; --border1 und border2 sind Geometrien, die man erhält, border2 geometry; --wenn man die Kante mit dem Polygonrand schneidet result edges%rowtype; middle CURSOR FOR SELECT * FROM edges WHERE ST_Crosses(polygon, linestring) AND NOT ST_Contains(polygon, ST_Startpoint(linestring)) AND NOT ST_Contains(polygon, ST_Endpoint(linestring)); BEGIN FOR edge IN middle LOOP result:=edge; SELECT * INTO firstborder,count,border1,border2 FROM realCrossing(polygon,result.linestring); --wenn count>1, dann schneidet die Kante zwei Ränder dieses Polygons IF count>1 THEN result.node_id1:=edge.edge_id||’_b’; result.node_id2:=edge.edge_id||’_e’; --firstBorder zeigt an, an welchem Rand die Kante --das Polygon zuerst schneidet IF firstborder THEN result.linestring:=buildSubLineString( edge.linestring, border1, border2); ELSE result.linestring:=buildSubLineString( edge.linestring, border2, border1); END IF; result.length:=st_length(result.linestring,TRUE); RETURN NEXT result; END IF; END LOOP; RETURN; END; $BODY$ LANGUAGE plpgsql ANHANG B. PL/PGSQL-FUNKTIONEN Gibt alle Rankkanten zurück, die in dem übergebenen Polygon liegen. CREATE OR REPLACE FUNCTION getBorderEdges(polygon geometry) RETURNS SETOF edges AS $BODY$ BEGIN RETURN QUERY SELECT * FROM getSimpleBorderEdges(polygon) UNION SELECT * FROM getComplicatedBorderEdges(polygon); END; $BODY$ LANGUAGE plpgsql 78 Abbildungsverzeichnis 2.1 EER-Diagramm zu Node, Way und deren Tags . . . . . . . . . . . . . . . 7 2.2 Ein gerichteter Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3 Ein kürzester Weg zwischen zwei Knoten . . . . . . . . . . . . . . . . . . 11 3.1 Zwei Partitionierungsarten . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.2 Beispiel zur Erstellung einer abhängigen Partitionierung . . . . . . . . . 21 4.1 Die OpenJump-Oberfläche . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4.2 Das Einstellungsfenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4.3 Transformation des OSM-Graphen . . . . . . . . . . . . . . . . . . . . . 33 4.4 Clipping an der Partitionsgrenze . . . . . . . . . . . . . . . . . . . . . . . 34 4.5 Die Klassen des Graph-Packages . . . . . . . . . . . . . . . . . . . . . . . 36 4.6 Die Klassen des Partition-Packages . . . . . . . . . . . . . . . . . . . . . 38 4.7 Die Klassen des RoutingAlgorithm-Package . . . . . . . . . . . . . . . . . 39 4.8 Die Klassen des Controller- und View-Packages . . . . . . . . . . . . . . 40 4.9 Die Klassen des Connectors-Packages . . . . . . . . . . . . . . . . . . . . 42 4.10 Die Klassen des Model-Packages . . . . . . . . . . . . . . . . . . . . . . . 45 4.11 Der Ablauf des Routings als Sequenzdiagramm (vereinfacht) . . . . . . . 47 5.1 A* und Dijkstra, Vergleich untersuchter Knoten . . . . . . . . . . . . . . 51 5.2 Vorteil von Dijkstra gegenüber A* . . . . . . . . . . . . . . . . . . . . . . 53 5.3 Probleme der Heuristiken . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 5.4 Probleme der Heuristiken . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 5.5 Nachteil der unabhängige Partitionierung . . . . . . . . . . . . . . . . . . 60 79 Tabellenverzeichnis 3.1 Die Straßenarten und ihre Gewichtungen . . . . . . . . . . . . . . . . . . 25 5.1 Die optimalen kürzesten Routen . . . . . . . . . . . . . . . . . . . . . . . 49 5.2 Dresden-München, Vergleich Knotenanzahl Dijkstra und A* . . . . . . . 50 5.3 Dresden-München, Vergleich Routingdauer zwischen Dijkstra und A* . . 51 5.4 Hamburg-Dresden, Vergleich Routingdauer zwischen Dijkstra und A* . . 52 5.5 Hamburg-Dresden, Vergleich Güte zwischen Dijkstra und A* . . . . . . . 52 5.6 Hannover-München, Vergleich Partitionsgrößen . . . . . . . . . . . . . . . 54 5.7 Hamburg-Dresden, Vergleich Partitionsgrößen . . . . . . . . . . . . . . . 54 5.8 Dresden-München, Vergleich Partitionsgrößen . . . . . . . . . . . . . . . 54 5.9 Hannover-München, Vergleich Güte bezüglich der Metriken . . . . . . . . 55 5.10 Dresden-München, Vergleich Güte bezüglich der Metriken . . . . . . . . . 56 5.11 Hamburg-Dresden, Vergleich Güte bezüglich der Metriken . . . . . . . . 56 5.12 Hamburg-Dresden, Güte der verschiedenen Heuristiken . . . . . . . . . . 56 5.13 Dresden-München, Güte der verschiedenen Heuristiken . . . . . . . . . . 57 5.14 Hannover-München, Güte der verschiedenen Heuristiken . . . . . . . . . 57 5.15 Hamburg-Dresden, Güte bzgl. der Partitionierung und Heuristiken . . . . 59 5.16 Hannover-München, Güte bzgl. der Partitionierung und Heuristiken . . . 60 5.17 Dresden-München, Güte bzgl. der Partitionierung und Heuristiken . . . . 61 5.18 Hamburg-Dresden, Vergleich Güte bzgl. der Partitionierung und den Metriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 5.19 Dresden-München, Vergleich bzgl. der Partitionierung und den Metriken 62 80 Literaturverzeichnis [Bri08] T. Brinkhoff. Geodatenbanksysteme in Theorie und Praxis. Herbert Wichmann Verlag, 2. Aufl., 2008. ISBN 978-3-87907-472-3. [CLRS10] T. H. Cormen, C. E. Leiserson, R. Rivest, C. Stein. Algorithmen - Eine Einführung. Oldenbourg Verlag München, 3. Aufl., 2010. ISBN 978-3-48659002-9. [HNR68] P. E. Hart, N. J. Nilsson, B. Raphael. A formal basis for the heuristic determination of minimum cost paths. IEEE Transactions on Systems, Science, and Cybernetics, SSC-4(2), 1968, 100–107. [Jun94] D. Jungnickel. Graphen, Netzwerke und Algorithmen. BI-Wissenschaftsverlag, 3. Aufl., 1994. ISBN 3-411-14263-4. [Lip12] U. Lipeck. Vorlesung Datenstrukturen und Algorithmen, WiSe 2011/12. URL http://www.dbs.uni-hannover.de/dsalg1112.html. [Mee91] J. H. Meeus. Astronomical Algorithms. Willmann-Bell, Incorporated, 1991. ISBN 0943396352. [Pap80] U. Pape. Algorithm 562: Shortest Path Lengths [H]. ACM Trans. Math. Softw., 6(3), 1980, 450–455. ISSN 0098-3500. URL http://doi.acm.org/ 10.1145/355900.355919. [RT09] F. Ramm, J. Topf. OpenStreetMap - Die freie Weltkarte nutzen und mitgestalten. Lehmanns Media, Berlin, 2. Aufl., 2009. ISBN 978-3-86541-320-8. [Sch00] W. Schmid. Berechnung kürzester Wege in Straßennetzen mit Wegeverboten. Dissertation, Universität Stuttgart, Holzgartenstr. 16, 70174 Stuttgart, 2000. URL http://elib.uni-stuttgart.de/opus/volltexte/2002/1190. [WL12] H. Warneke, U. W. Lipeck. Ein Partitionierungsdienst für Geographische Daten in Räumlichen Datenbanken. Proceedings of the 24th GI-Workshop ”Grundlagen von Datenbanken 2012”, 2012, 35–40. URL http://ceur-ws. org/Vol-850/paper_warneke.pdf. 81 Erklärung Hiermit versichere ich, dass ich die vorliegende Arbeit und die zugehörige Implementierung selbstständig verfasst und dabei nur die angegebenen Quellen und Hilfsmittel verwendet habe. Hannover, 15. Juli 2012 David Bormann 82