Gelegentlich müssen Sie möglicherweise georäumliche Funktionen in Ihrer Anwendung ausführen, z. B. Benutzerstandorte zuordnen oder geografische Daten analysieren. Für diese Aufgaben stehen zahlreiche sprachspezifische Bibliotheken zur Verfügung, z. B. GDAL, Shapely und Geopandas für Python.
Alternativ können georäumliche Funktionen über Datenbanken implementiert werden. Sie können beispielsweise die PostGIS-Erweiterung mit einer relationalen Datenbank wie PostgreSQL verwenden oder die native Unterstützung für räumliche Datentypen in einer verteilten Datenbank wie Azure CosmosDB nutzen.
Wenn Ihr Backend-Speicher, beispielsweise Redis oder Google Spanner, räumliche Abfragen jedoch nicht nativ unterstützt und Sie groß angelegte georäumliche Abfragen verarbeiten müssen, ist dieser Artikel auf Sie zugeschnitten.
Sie können jederzeit einen weiteren Microservice zur Verarbeitung räumlicher Daten erstellen, aber diese Option ist häufig mit dem Mehraufwand verbunden, eine zusätzliche Anwendung zu warten. Ein anderer Ansatz besteht darin, Geoindexierungsbibliotheken wie S2 und H3 zu verwenden. S2, entwickelt von Google, basiert auf der Hilbert-Kurve, während H3, entwickelt von Uber, auf einem geodätischen diskreten globalen Gittersystem basiert. S2 und H3 haben viele Gemeinsamkeiten: Beide unterteilen eine bestimmte Region in Zellen und verwenden 64-Bit-Ganzzahlen, um diese Zellen zu indizieren.
Der Hauptunterschied liegt jedoch in der Form der Zellen. S2 verwendet quadratische Zellen, während H3 sechseckige Zellen verwendet. Für einige Anwendungen bietet H3 möglicherweise eine bessere Leistung. Insgesamt sollte jedoch jede der beiden Bibliotheken ausreichen. In diesem Artikel verwenden wir S2, aber Sie können ähnliche Funktionen mit H3 ausführen.
Zellebenen: Die Hierarchie ermöglicht verschiedene Detailebenen, von großen Regionen bis hin zu kleinen, präzisen Bereichen. Jede Ebene stellt eine andere Auflösung dar:
Ebene 0: Die größten Zellen, die einen erheblichen Teil der Erdoberfläche bedecken.
Höhere Ebenen: Zellen werden schrittweise in kleinere Quadranten unterteilt. Beispielsweise werden Zellen der Ebene 1 jeweils in vier Zellen der Ebene 2 unterteilt und so weiter.
Auflösung und Fläche: Höhere Ebenen entsprechen feineren Auflösungen und kleineren Zellflächen. Diese Hierarchie ermöglicht eine präzise Indizierung und Abfrage auf unterschiedlichen Detailebenen.
In der folgenden Tabelle sind die verschiedenen Zellebenen und die dazugehörigen Bereiche aufgeführt.
Ebene | Mindestfläche | Maximale Fläche | Durchschnittliche Fläche | Einheiten | Anzahl der Zellen |
---|---|---|---|---|---|
00 | 85011012.19 | 85011012.19 | 85011012.19 | km2 | 6 |
01 | 21252753.05 | 21252753.05 | 21252753.05 | km2 | 24 |
02 | 4919708.23 | 6026521.16 | 5313188.26 | km2 | 96 |
03 | 1055377.48 | 1646455.50 | 1328297.07 | km2 | 384 |
04 | 231564.06 | 413918.15 | 332074.27 | km2 | 1536 |
05 | 53798.67 | 104297.91 | 83018.57 | km2 | 6K |
06 | 12948.81 | 26113.30 | 20754,64 | km2 | 24K |
07 | 3175,44 | 6529.09 | 5188,66 | km2 | 98.000 |
08 | 786.20 | 1632,45 | 1297.17 | km2 | 393 Tausend |
09 | 195,59 | 408.12 | 324,29 | km2 | 1573K |
10 | 48,78 | 102.03 | 81,07 | km2 | 6M |
11 | 12.18 | 25,51 | 20.27 | km2 | 25 Mio. |
12 | 3.04 | 6.38 | 5.07 | km2 | 100 Mio. |
13 | 0,76 | 1,59 | 1.27 | km2 | 402M |
14 | 0,19 | 0,40 | 0,32 | km2 | 1610M |
15 | 47520.30 | 99638.93 | 79172.67 | m2 | 6B |
16 | 11880.08 | 24909.73 | 19793.17 | m2 | 25B |
17 | 2970.02 | 6227.43 | 4948.29 | m2 | 103B |
18 | 742,50 | 1556,86 | 1237.07 | m2 | 412B |
19 | 185,63 | 389,21 | 309,27 | m2 | 1649B |
20 | 46,41 | 97,30 | 77,32 | m2 | 7T |
21 | 11,60 | 24,33 | 19.33 | m2 | 26T |
22 | 2,90 | 6.08 | 4,83 | m2 | 105T |
23 | 0,73 | 1,52 | 1.21 | m2 | 422T |
24 | 0,18 | 0,38 | 0,30 | m2 | 1689T |
25 | 453,19 | 950,23 | 755,05 | cm2 | 7e15 |
26 | 113,30 | 237,56 | 188,76 | cm2 | 27e15 |
27 | 28,32 | 59,39 | 47,19 | cm2 | 108e15 |
28 | 7.08 | 14,85 | 11,80 | cm2 | 432e15 |
29 | 1,77 | 3.71 | 2,95 | cm2 | Nr. 1729e15 |
30 | 0,44 | 0,93 | 0,74 | cm2 | 7e18 |
Aus der bereitgestellten Tabelle geht hervor, dass Sie mit S2 eine Abbildungsgenauigkeit von bis zu 0,44 cm^2 erreichen können. In jedem Quadrat einer S2-Zelle gibt es eine untergeordnete Zelle mit derselben übergeordneten Zelle, was auf eine hierarchische Struktur hinweist. Die Ebene der Zelle kann ein statischer Wert sein (dieselbe Ebene wird auf alle Zellen angewendet) oder dynamisch, wobei S2 entscheidet, welche Auflösung am besten funktioniert.
Beginnen wir mit einem Beispiel. Angenommen, wir schreiben eine Anwendung, die Funktionen ähnlich einem Proximity-Service für die Gegend um Seattle bereitstellt. Wir möchten eine Liste von Cafés in der angegebenen Umgebung zurückgeben. Um diese Vorgänge auszuführen, teilen wir diese Aufgabe in vier Unteraufgaben auf:
Um eine Google-Karte zu laden, würden wir die gmplot-Bibliothek verwenden. Zum Laden dieser Bibliothek ist ein Google Maps API-Schlüssel erforderlich. Um den API-Schlüssel zu generieren, folgen Sie den Anweisungen hier .
import gmplot import const # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) # Draw the map to an HTML file: gmap.draw('map.html')
Der obige Code generiert eine map.html-Datei wie unten gezeigt:
Nachdem wir nun die Karte haben, zeichnen wir einige S2-Zellen für Karten:
from s2 import * import gmplot # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) areatobeplotted = [ (47.64395531736767,-122.43597221319135), (47.51369277846956,-122.43597221319135), (47.51369277846956,-122.24156866779164), (47.64395531736767,-122.24156866779164), (47.64395531736767,-122.43597221319135) ] region_rect = S2LatLngRect( S2LatLng.FromDegrees(47.51369277846956,-122.43597221319135), S2LatLng.FromDegrees(47.64395531736767, -122.24156866779164)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) geoms = 0 for cellid in covering: new_cell = S2Cell(cellid) vertices = [] for i in range(0, 4): vertex = new_cell.GetVertex(i) latlng = S2LatLng(vertex) vertices.append((latlng.lat().degrees(), latlng.lng().degrees())) gmap.polygon(*zip(*vertices), face_color='pink', edge_color='cornflowerblue', edge_width=5) geoms+=1 gmap.polygon(*zip(*areatobeplotted), face_color='red', edge_color='green', edge_width=5) print(f"Total Geometries: {geoms}") gmap.draw('/tmp/map.html')
Output: Total Geometries: 273
Im obigen Code zentrieren wir zuerst den Google Map-Plotter um den Bereich Seattle. In S2RegionCoverer
initialisieren wir den Region Coverer so, dass er dynamische Ebenen zwischen einer Mindestebene von 8 und einer Höchstebene von 15 hat. Dadurch kann S2 alle Zellen dynamisch in bestimmte Zellengrößen einpassen, um die beste Anpassung zu erzielen. Die Methode GetCovering
gibt die Abdeckung für ein Rechteck um den Bereich Seattle zurück.
Dann iterieren wir über jede Zelle, berechnen die Eckpunkte für die Zellen und zeichnen sie auf der Karte auf. Wir halten die Anzahl der generierten Zellen bei etwa 273. Schließlich zeichnen wir das Eingaberechteck in Rot auf. Dieser Code zeichnet die S2-Zellen auf der Seattle-Karte unter /tmp/map.html
auf, wie unten gezeigt:
Lassen Sie uns eine Datenbank mit Coffeeshops und ihren S2-Zellkennungen erstellen. Sie können diese Zellen in einer Datenbank Ihrer Wahl speichern. Für dieses Tutorial verwenden wir eine SQLite-Datenbank. Im folgenden Codebeispiel stellen wir eine Verbindung zur SQLite-Datenbank her, um eine Tabelle CoffeeShops
mit den drei Feldern Id
, name
und cell_id
zu erstellen.
Ähnlich wie im vorherigen Beispiel verwenden wir S2RegionCoverer
zum Berechnen der Zellen, dieses Mal verwenden wir jedoch eine feste Ebene zum Plotten von Punkten. Schließlich wird die berechnete ID in eine Zeichenfolge konvertiert und in der Datenbank gespeichert.
import sqlite3 from s2 import S2CellId,S2LatLng,S2RegionCoverer # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Create a table to store cell IDs cursor.execute('''CREATE TABLE IF NOT EXISTS CoffeeShops ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, cell_id TEXT )''') coverer = S2RegionCoverer() # Function to generate S2 cell ID for a given latitude and longitude def generate_cell_id(latitude, longitude, level=16): cell=S2CellId(S2LatLng.FromDegrees(latitude, longitude)) return str(cell.parent(level)) # Function to insert cell IDs into the database def insert_cell_ids(name,lat,lng): cell_id = generate_cell_id(lat, lng) cursor.execute("INSERT INTO CoffeeShops (name, cell_id) VALUES (?, ?)", (name, cell_id)) conn.commit() # Insert cell IDs into the database insert_cell_ids("Overcast Coffee", 47.616656277302155, -122.31156460382837) insert_cell_ids("Seattle Sunshine", 47.67366852914391, -122.29051997415843) insert_cell_ids("Sip House", 47.6682364706238, -122.31328618043693) insert_cell_ids("Victoria Coffee",47.624408595334536, -122.3117362652041) # Close connection conn.close()
An diesem Punkt verfügen wir über eine Datenbank, in der Cafés zusammen mit ihren Zellen-IDs gespeichert sind, die durch die ausgewählte Auflösung für die Zellenebene bestimmt werden.
Lassen Sie uns abschließend eine Abfrage nach Cafés im Universitätsviertel durchführen.
import sqlite3 from s2 import S2RegionCoverer,S2LatLngRect, S2LatLng # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Function to query database for cells intersecting with the given polygon def query_intersecting_cells(start_x,start_y,end_x,end_y): # Create S2RegionCoverer region_rect = S2LatLngRect( S2LatLng.FromDegrees(start_x,start_y), S2LatLng.FromDegrees(end_x,end_y)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) # Query for intersecting cells intersecting_cells = set() for cell_id in covering: cursor.execute("SELECT name FROM CoffeeShops WHERE cell_id >= ? and cell_id<=?", (str(cell_id.range_min()),str(cell_id.range_max()),)) intersecting_cells.update(cursor.fetchall()) return intersecting_cells # Query for intersecting cells intersecting_cells = query_intersecting_cells(47.6527847,-122.3286438,47.6782181, -122.2797203) # Print intersecting cells print("Intersecting cells:") for cell_id in intersecting_cells: print(cell_id[0]) # Close connection conn.close()
Output: Intersecting cells: Sip House Seattle Sunshine
Unten sehen Sie eine visuelle Darstellung der Zellen. Der Kürze halber wurde der Visualisierungscode unten nicht hinzugefügt.
Da alle untergeordneten und übergeordneten Zellen ein gemeinsames Präfix haben, können wir nach Zellbereichen zwischen min und max suchen, um alle Zellen zwischen diesen beiden Werten abzurufen. In unserem Beispiel verwenden wir dasselbe Prinzip, um coffee shop abzufragen.
In diesem Artikel haben wir gezeigt, wie man Geoindizierung zum Speichern und Abfragen von Geodaten in Datenbanken verwendet, die keine Geoabfragen unterstützen. Dies kann auf mehrere Anwendungsfälle ausgeweitet werden, z. B. zum Berechnen der Route zwischen zwei Punkten oder zum Ermitteln der nächsten Nachbarn.
Normalerweise müssen Sie bei der Abfrage von geoindizierten Datenbanken eine zusätzliche Nachbearbeitung der Daten durchführen. Die Abfrage- und Nachbearbeitungslogik muss sorgfältig durchdacht werden, um sicherzustellen, dass der Knoten nicht überlastet wird.