Einführung: 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. Welche Möglichkeiten habe ich? 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. Grundlegende Konzepte der Google S2-Bibliothek Zellen: S2 unterteilt die Kugel in Zellen, jede mit einer eindeutigen 64-Bit-Kennung. 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. Berechnen der nächsten Nachbarn 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: Karte von Seattle wird geladen Visualisieren Sie S2-Zellen auf der Karte von Seattle Speichern Sie einige Coffee-Shop-Standorte in der Datenbank Abfrage nach nächstgelegenen Cafés Karte von Seattle wird geladen 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: Visualisieren Sie S2-Zellen auf der Karte von Seattle 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 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 gibt die Abdeckung für ein Rechteck um den Bereich Seattle zurück. S2RegionCoverer GetCovering 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 auf, wie unten gezeigt: /tmp/map.html Speichern Sie einige Coffee-Shop-Standorte in der Datenbank 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 mit den drei Feldern , und zu erstellen. CoffeeShops Id name cell_id Ähnlich wie im vorherigen Beispiel verwenden wir 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. S2RegionCoverer 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. Abfrage nach den nächstgelegenen Coffee Shops 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. Abschluss: 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. Verweise: Google-Kartenplotter - https://github.com/gmplot/gmplot/wiki/GoogleMapPlotter S2-Entwicklerhandbuch - http://s2geometry.io/devguide/ Sqlite – https://docs.python.org/3/library/sqlite3.html Georäumliche Azure Cosmos DB – https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/geospatial?tabs=javascript GDAL - https://gdal.org/index.html Shapley – https://shapely.readthedocs.io/en/stable/manual.html Geopandas - https://geopandas.org Posgis - https://postgis.net/ Google Schraubenschlüssel - https://cloud.google.com/spanner?hl=de Redis - https://redis.io/