Иногда вы можете столкнуться с необходимостью выполнения геопространственных функций в вашем приложении, таких как картографирование местоположений пользователей или анализ географических данных. Для этих задач доступно множество специфичных для языка библиотек, таких как GDAL, Shapely и Geopandas для Python.
В качестве альтернативы геопространственные функции могут быть реализованы через базы данных; например, вы можете использовать расширение PostGIS с реляционной базой данных, такой как PostgreSQL, или использовать встроенную поддержку пространственных типов данных в распределенной базе данных, такой как Azure CosmosDB.
Однако если ваше серверное хранилище, такое как Redis или Google Spanner, не поддерживает пространственные запросы изначально и вам необходимо обрабатывать крупномасштабные геопространственные запросы, эта статья предназначена для вас.
Вы всегда можете создать еще один микросервис для обработки пространственных данных, но этот вариант часто влечет за собой затраты на поддержку дополнительного приложения. Другой подход — использовать библиотеки геоиндексации, такие как S2 и H3. S2, разработанный Google, основан на кривой Гильберта, а H3, разработанный Uber, основан на геодезической дискретной глобальной сетке. S2 и H3 имеют много общего: оба делят данную область на ячейки и используют 64-битные целые числа для индексации этих ячеек.
Однако основное отличие заключается в форме ячеек; В S2 используются ячейки квадратной формы, тогда как в H3 используются ячейки шестиугольной формы. Для некоторых приложений H3 может обеспечить более высокую производительность. Однако в целом любой библиотеки должно быть достаточно. В этой статье мы будем использовать S2, но аналогичные функции можно выполнять и с помощью H3.
Уровни ячеек. Иерархия допускает различные уровни детализации: от больших регионов до небольших точных областей. Каждый уровень представляет собой разное разрешение:
Уровень 0: Самые крупные клетки, покрывающие значительную часть поверхности Земли.
Более высокие уровни: клетки постепенно подразделяются на более мелкие квадранты. Например, каждая ячейка уровня 1 делится на четыре ячейки уровня 2 и так далее.
Разрешение и площадь: более высокие уровни соответствуют более высокому разрешению и меньшим площадям ячеек. Эта иерархия обеспечивает точную индексацию и запросы на различных уровнях детализации.
В таблице ниже показаны различные уровни ячеек и соответствующие им области.
уровень | минимальная площадь | максимальная площадь | средняя площадь | единицы | Количество ячеек |
---|---|---|---|---|---|
00 | 85011012.19 | 85011012.19 | 85011012.19 | км2 | 6 |
01 | 21252753.05 | 21252753.05 | 21252753.05 | км2 | 24 |
02 | 4919708.23 | 6026521.16 | 5313188.26 | км2 | 96 |
03 | 1055377.48 | 1646455,50 | 1328297.07 | км2 | 384 |
04 | 231564.06 | 413918.15 | 332074.27 | км2 | 1536 |
05 | 53798,67 | 104297.91 | 83018.57 | км2 | 6К |
06 | 12948,81 | 26113.30 | 20754.64 | км2 | 24К |
07 | 3175,44 | 6529.09 | 5188,66 | км2 | 98К |
08 | 786,20 | 1632,45 | 1297,17 | км2 | 393 тыс. |
09 | 195,59 | 408,12 | 324,29 | км2 | 1573К |
10 | 48,78 | 102.03 | 81.07 | км2 | 6М |
11 | 12.18 | 25.51 | 20.27 | км2 | 25М |
12 | 3.04 | 6.38 | 5.07 | км2 | 100М |
13 | 0,76 | 1,59 | 1,27 | км2 | 402М |
14 | 0,19 | 0,40 | 0,32 | км2 | 1610М |
15 | 47520.30 | 99638.93 | 79172,67 | м2 | 6Б |
16 | 11880.08 | 24909,73 | 19793,17 | м2 | 25Б |
17 | 2970.02 | 6227,43 | 4948,29 | м2 | 103Б |
18 | 742,50 | 1556,86 | 1237.07 | м2 | 412Б |
19 | 185,63 | 389,21 | 309,27 | м2 | 1649Б |
20 | 46,41 | 97.30 | 77,32 | м2 | 7Т |
21 | 11.60 | 24.33 | 19.33 | м2 | 26Т |
22 | 2,90 | 6.08 | 4,83 | м2 | 105Т |
23 | 0,73 | 1,52 | 1.21 | м2 | 422Т |
24 | 0,18 | 0,38 | 0,30 | м2 | 1689Т |
25 | 453,19 | 950,23 | 755.05 | см2 | 7e15 |
26 | 113.30 | 237,56 | 188,76 | см2 | 27e15 |
27 | 28.32 | 59,39 | 47,19 | см2 | 108e15 |
28 | 7.08 | 14.85 | 11.80 | см2 | 432e15 |
29 | 1,77 | 3,71 | 2,95 | см2 | 1729e15 |
30 | 0,44 | 0,93 | 0,74 | см2 | 7e18 |
Из представленной таблицы видно, что с помощью S2 можно добиться точности отображения до 0,44 см^2. Внутри каждого квадрата ячейки S2 существует дочерняя ячейка, имеющая одного и того же родителя, что указывает на иерархическую структуру. Уровень ячейки может быть статическим значением (один и тот же уровень применяется ко всем ячейкам) или может быть динамическим, когда S2 решает, какое разрешение работает лучше всего.
Начнем с примера. Предположим, мы пишем приложение, которое предоставляет функции, подобные сервису близости, для района Сиэтла. Мы хотим вернуть список кофеен в заданном районе. Для выполнения этих операций разделим эту задачу на 4 подзадачи:
Чтобы загрузить карту Google, мы будем использовать библиотеку gmplot. Для загрузки этой библиотеки требуется ключ API Google Maps. Чтобы сгенерировать ключ 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 Map вокруг района Сиэтла. В S2RegionCoverer
мы инициализируем средство покрытия региона, чтобы оно имело динамические уровни от минимального уровня 8 до максимального уровня 15. Это позволяет S2 динамически подгонять все ячейки под определенные размеры ячеек для наилучшего соответствия. Метод GetCovering
возвращает покрытие прямоугольника вокруг области Сиэтла.
Затем мы перебираем каждую ячейку, вычисляя вершины ячеек и нанося их на карту. Мы сохраняем количество сгенерированных ячеек примерно 273. Наконец, мы рисуем входной прямоугольник красным. Этот код будет отображать ячейки S2 на карте Сиэтла в /tmp/map.html
, как показано ниже:
Давайте сгенерируем базу данных кофеен вместе с их идентификаторами ячеек S2. Вы можете хранить эти ячейки в любой базе данных по вашему выбору. В этом уроке мы будем использовать базу данных данных SQLite. В приведенном ниже примере кода мы подключаемся к базе данных SQLite, чтобы создать таблицу CoffeeShops
с тремя полями Id
, name
и cell_id
.
Как и в предыдущем примере, мы используем 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()
На данный момент у нас есть база данных, в которой хранятся кофейни вместе с их идентификаторами ячеек, определяемыми выбранным разрешением для уровня ячейки.
Наконец, давайте запросим кафе в районе Университетского округа.
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
Ниже приведено визуальное представление ячеек. Для краткости приведенный ниже код визуализации не добавляется.
Поскольку все дочерние и родительские ячейки имеют общий префикс, мы можем запросить диапазоны ячеек между минимальным и максимальным значениями, чтобы получить все ячейки между этими двумя значениями. В нашем примере мы используем тот же принцип для запроса кафе.
В этой статье мы продемонстрировали, как использовать геоиндексацию для хранения и запроса геопространственных данных в базах данных, которые не поддерживают геопространственные запросы. Это можно распространить на несколько случаев использования, таких как расчет маршрута между двумя точками или получение ближайших соседей.
Обычно для запросов к базе данных с геоиндексацией вам придется выполнить некоторую дополнительную постобработку данных. Чтобы гарантировать, что мы не перегружаем узел, необходимо тщательно продумать логику запросов и постобработки.