paint-brush
So führen Sie GIS-Berechnungen auf Nicht-GIS-Datenbanken durchvon@joellopes
1,662 Lesungen
1,662 Lesungen

So führen Sie GIS-Berechnungen auf Nicht-GIS-Datenbanken durch

von Joel Lopes9m2024/06/15
Read on Terminal Reader

Zu lang; Lesen

Wenn Ihr Backend-Speicher räumliche Abfragen nicht nativ unterstützt, ist dieser Artikel für Sie geeignet. Sie können jederzeit einen anderen Microservice zum Verarbeiten räumlicher Daten erstellen, aber diese Option ist häufig mit dem Mehraufwand verbunden, eine zusätzliche Anwendung zu verwalten. Ein anderer Ansatz besteht darin, Geoindexierungsbibliotheken wie S2 und H3 zu verwenden. S2 unterteilt die Kugel in Zellen, jede mit einer eindeutigen 64-Bit-Kennung. Höhere Ebenen entsprechen feineren Auflösungen und kleineren Zellbereichen.
featured image - So führen Sie GIS-Berechnungen auf Nicht-GIS-Datenbanken durch
Joel Lopes HackerNoon profile picture

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 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:


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 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.

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: