場合によっては、ユーザーの位置のマッピングや地理データの分析など、アプリケーション内で地理空間機能を実行する必要に迫られることがあります。これらのタスクには、GDAL、Shapely、Python 用の Geopandas など、言語固有のライブラリが多数用意されています。
あるいは、地理空間機能はデータベースを通じて実装することもできます。たとえば、PostgreSQL などのリレーショナル データベースで PostGIS 拡張機能を使用したり、Azure CosmosDB などの分散データベースで空間データ型のネイティブ サポートを活用したりすることができます。
ただし、Redis や Google Spanner などのバックエンド ストレージが空間クエリをネイティブにサポートしておらず、大規模な地理空間クエリを処理する必要がある場合は、この記事が役立ちます。
空間データを処理する別のマイクロサービスをいつでも構築できますが、このオプションでは、追加のアプリケーションを維持するオーバーヘッドが発生することがよくあります。別のアプローチは、S2 や H3 などの地理インデックス ライブラリを使用することです。Google が開発した S2 はヒルベルト曲線に基づいており、Uber が開発した H3 は測地線離散グローバル グリッド システムに基づいています。S2 と H3 には多くの類似点があります。どちらも特定の領域をセルに分割し、64 ビットの整数を使用してこれらのセルをインデックスします。
ただし、主な違いはセルの形状にあります。S2 は正方形のセルを使用するのに対し、H3 は六角形のセルを使用します。一部のアプリケーションでは、H3 の方がパフォーマンスが優れている場合があります。ただし、全体的にはどちらのライブラリでも十分です。この記事では S2 を使用しますが、H3 を使用しても同様の機能を実行できます。
セル レベル: 階層により、大きな領域から小さな正確な領域まで、さまざまなレベルの詳細が可能になります。各レベルは異なる解像度を表します。
レベル 0: 地球の表面の大部分を覆う最大のセル。
上位レベル: セルは段階的に小さな象限に分割されます。たとえば、レベル 1 のセルはそれぞれ 4 つのレベル 2 のセルに分割されます。
解像度と領域: レベルが高いほど、解像度が高くなり、セル領域が小さくなります。この階層により、さまざまな詳細レベルで正確なインデックス作成とクエリが可能になります。
以下の表は、さまざまなセル レベルとそれに対応する領域を示しています。
レベル | 最小面積 | 最大面積 | 平均面積 | ユニット | セルの数 |
---|---|---|---|---|---|
00 | 85011012.19 | 85011012.19 | 85011012.19 | 平方キロメートル | 6 |
01 | 21252753.05 | 21252753.05 | 21252753.05 | 平方キロメートル | 24 |
02 | 4919708.23 | 6026521.16 | 5313188.26 | 平方キロメートル | 96 |
03 | 1055377.48 | 1646455.50 | 1328297.07 | 平方キロメートル | 384 |
04 | 231564.06 | 413918.15 | 332074.27 | 平方キロメートル | 1536 |
05 | 53798.67 | 104297.91 | 83018.57 | 平方キロメートル | 6K |
06 | 12948.81 | 26113.30 | 20754.64 | 平方キロメートル | 24K |
07 | 3175.44 | 6529.09 | 5188.66 | 平方キロメートル | 98K |
08 | 786.20 | 1632.45 | 1297.17 | 平方キロメートル | 393K |
09 | 195.59 | 408.12 | 324.29 | 平方キロメートル | 1573K |
10 | 48.78 | 102.03 | 81.07 | 平方キロメートル | 6M |
11 | 12.18 | 25.51 | 20.27 | 平方キロメートル | 2500万 |
12 | 3.04 | 6.38 | 5.07 | 平方キロメートル | 1億 |
13 | 0.76 | 1.59 | 1.27 | 平方キロメートル | 402M |
14 | 0.19 | 0.40 | 0.32 | 平方キロメートル | 1610M |
15 | 47520.30 | 99638.93 | 79172.67 | 平方メートル | 6B |
16 | 11880.08 | 24909.73 | 19793.17 | 平方メートル | 25B |
17 | 2970.02 | 6227.43 | 4948.29 | 平方メートル | 103B |
18 | 742.50 | 1556.86 | 1237.07 | 平方メートル | 412B |
19 | 185.63 | 389.21 | 309.27 | 平方メートル | 1649B |
20 | 46.41 | 97.30 | 77.32 | 平方メートル | 7T |
21 | 11.60 | 24.33 | 19.33 | 平方メートル | 26T |
22 | 2.90 | 6.08 | 4.83 | 平方メートル | 105T |
23 | 0.73 | 1.52 | 1.21 | 平方メートル | 422T |
24 | 0.18 | 0.38 | 0.30 | 平方メートル | 1689T |
25 | 453.19 | 950.23 | 755.05 | 平方センチメートル | 7e15 |
26 | 113.30 | 237.56 | 188.76 | 平方センチメートル | 27e15 |
27 | 28.32 | 59.39 | 47.19 | 平方センチメートル | 108e15 |
28 | 7.08 | 14.85 | 11.80 | 平方センチメートル | 432e15 |
29 | 1.77 | 3.71 | 2.95 | 平方センチメートル | 1729e15 |
30 | 0.44 | 0.93 | 0.74 | 平方センチメートル | 7e18 |
提供された表から、S2 を使用すると 0.44 cm^2 までのマッピング精度を実現できることは明らかです。S2 セルの各四角形内には、同じ親を共有する子セルが存在し、階層構造を示しています。セルのレベルは静的な値 (すべてのセルに同じレベルを適用) にすることも、S2 が最適な解像度を決定する動的な値にすることもできます。
まずは例から始めましょう。シアトル地域に近接サービスのような機能を提供するアプリケーションを作成するとします。指定された範囲内のコーヒーショップのリストを返します。これらの操作を実行するには、このタスクを 4 つのサブタスクに分割します。
Google マップを読み込むには、gmplot ライブラリを使用します。このライブラリを読み込むには、Google マップ API キーが必要です。API キーを生成するには、ここ の手順に従ってください。
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')
上記のコードは、以下に示すような map.html ファイルを生成します。
マップができたので、マップ用の S2 セルをいくつか描画してみましょう。
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
上記のコードでは、まず Google マップ プロッターをシアトル周辺を中心に配置します。 S2RegionCoverer
では、リージョン カバラーを初期化して、最小レベル 8 から最大レベル 15 までの動的レベルを設定します。これにより、S2 はすべてのセルを特定のセル サイズに動的に適合させ、最適なフィットを実現できます。 GetCovering
メソッドは、シアトル周辺の四角形のカバリングを返します。
次に、各セルを反復処理して、セルの頂点を計算し、マップ上にプロットします。生成されるセルの数は約 273 個に抑えます。最後に、入力された四角形を赤でプロットします。このコードは、以下に示すように、シアトル マップの/tmp/map.html
に S2 セルをプロットします。
コーヒーショップのデータベースを S2 セル識別子とともに生成してみましょう。これらのセルは任意のデータベースに保存できます。このチュートリアルでは、SQLite データ データベースを使用します。以下のコード サンプルでは、SQLite データベースに接続して、 Id
、 name
、 cell_id
の 3 つのフィールドを持つCoffeeShops
テーブルを作成します。
前の例と同様に、 S2RegionCoverer
を使用してセルを計算しますが、今回はポイントをプロットするための固定レベルを使用します。最後に、計算された ID が文字列に変換され、データベースに保存されます。
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()
この時点で、セル レベルで選択された解像度によって決定されるセル ID とともにコーヒー ショップを保存するデータベースができました。
最後に、大学地区エリアのコーヒーショップを検索してみましょう。
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
以下はセルの視覚的表現です。簡潔さを保つために、以下の視覚化コードは追加されていません。
すべての子セルと親セルはプレフィックスを共有しているので、最小値と最大値の間のセル範囲をクエリして、その2つの値の間にあるすべてのセルを取得できます。例では、同じ原理を使用してコーヒーショップをクエリします。
この記事では、地理インデックスを使用して、地理空間クエリをサポートしていないデータベースに地理空間データを保存およびクエリする方法を説明しました。これは、2 点間のルートを計算したり、最も近い近傍を取得したりするなど、複数のユース ケースにさらに拡張できます。
通常、地理インデックス付きデータベースのクエリでは、データに対して追加の後処理を実行する必要があります。ノードに過負荷をかけないように、クエリと後処理のロジックを慎重に検討する必要があります。