Серверная кластеризация гео-объектов на карте на основе Geohash
Когда слишком много гео-объектов (точек, маркеров, меток) расположены рядом на карте, они сливаются в одно большое, едва различимое пятно. При большом количестве гео-объектов координаты и другие данные занимают много памяти, а для отображения карты потребляется много ресурсов устройства, что может привести к частому зависанию приложения.
Стандартное решение данной проблемы — группировать расположенные рядом объекты в кластеры и представлять их в виде специальной иконки. Часто иконка кластера указывает количество объектов, содержащихся в нем, а чтобы увидеть отдельные объекты, необходимо приблизить кластер.
Кластеризация может значительно увеличить производительность при отображении большого числа гео-объектов.
Много JavaScript библиотек для интерактивных карт предоставляют возможность кластеризации гео-объектов на стороне клиента. При кластеризации на стороне клиента с сервера получаются отдельные точки, которые затем обрабатываются в браузере или мобильном приложении для объединения в кластер. Это делает размер ответа сервера огромным, занимая время и память на стороне клиента. Ели используется кластеризация на стороне сервера, размер ответа сервера значительно меньше — данные о нескольких кластерах вместо тысяч отдельных точек. Это быстрее и потребляет намного меньше памяти на стороне клиента.
Рассмотрим как реализовать серверную кластеризацию гео-объектов на карте на основе системы геокодирования Geohash.
Что такое Geohash
Geohash — это алфавитно-цифровая строка, представляющая географические координаты, широту и долготу. Данная система геокодирования разработана Gustavo Niemeyer, создателем geohash.org.
Например, точка с широтой 50.450101
и долготой 30.523401
представлена Geohash строкой u8vxn84mnu3q
. Короткий URL geohash.org/u8vxn84mnu3q может использоваться для определения местоположения.
Количество символов в Geohash напрямую связано с точностью координат. Если убрать символы с конца Geohash строки, чтоб уменьшить ее длину, некоторая точность координат будет потеряна. Например, Geohash u8vxn84mnu
расшифровывается в координаты 50.45010
и 30.5234
, а Geohash u8vxn8
- в 50.45
и 30.5
.
Точке на расстоянии 50 км, такой как 50.348751
и 30.90151
, соответствует Geohash u8vyrjty9r7y
. Как видите, Geohash строки имеют общий префикс u8v
.
Это позволяет нам с легкостью искать объекты, расположенные поблизости. Например, используя SQL:
SELECT * FROM GEO_POINT WHERE GEOHASH LIKE 'u8v%'
Чтобы закодировать широту и долготу координат, алгоритм Geohash делит карту на ячейки, в которых группируются расположенные поблизости объекты.
Принцип работы алгоритма
Как работает алгоритм геокодирования Geohash? Сначала весь мир делится пополам вертикально. Левой половине присваивается бинарное значение 0, а правой — значение 1. Если точка, координаты которой кодируются, попадает в правую половину, мы добавляем 1 к бинарному значению Geohash:
Далее, алгоритм геокодирования Geohash делит карту горизонтально на 2 половины и присваивает бинарное значение 1 верхней половине, а нижней — 0. Точка, координаты которой мы кодируем в Geohash, расположена в верхней половине, поэтому мы добавляем 1 к бинарному значению Geohash. На данном этапе бинарное значение Geohash нашей точки — 11:
Мы продолжаем делить ячейки на половины вертикально и горизонтально, каждый раз добавляя к бинарному значению Geohash 0 или 1. Каждый раз, когда мы добавляем бит к бинарному значению Geohash, точность повышается:
Бинарное значение Geohash необходимо перевести в строку, кодированную в Base 32. Каждые 5 бит преобразуются в символ, используя таблицу символов:
Десятичная система | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
Base 32 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | b | c | d | e | f | g | h |
Десятичная система | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Base 32 | j | k | m | n | p | q | r | s | t | u | v | w | x | y | z |
Geohash разбивает карту мира на ячейки, каждая из которых представляет один кластер. Длина префикса Geohash строки напрямую связана со степенью приближения (zoom). Для лучшей визуализации, среднее арифметическое значение всех координат в ячейке будет местом размещения иконки кластера — вместо размещения иконки в центре ячейки. Предположим, географические координаты хранятся в таблице GEO_POINT
:
CREATE TABLE GEO_POINT ( GEO_POINT_ID SERIAL PRIMARY KEY, LATITUDE_DEG FLOAT8 NOT NULL, LONGITUDE_DEG FLOAT8 NOT NULL, GEOHASH VARCHAR(12) NOT NULL, COUNTRY_CODE VARCHAR(2) ); CREATE INDEX I_GEO_POI_LAT_LON ON GEO_POINT (LATITUDE_DEG, LONGITUDE_DEG); CREATE INDEX I_GEO_POI_GEOHASH ON GEO_POINT (GEOHASH);
Чтобы сформировать кластеры из видимых в текущей области просмотра (viewport) точек, может использоваться следующий SQL запрос:
SELECT AVG(GP.LATITUDE_DEG) AS LATITUDE_DEG, AVG(GP.LONGITUDE_DEG) AS LONGITUDE_DEG, COUNT(*) AS QUANTITY, SUBSTRING(GP.GEOHASH FROM 1 FOR :precision) AS GEOHASH_PREFIX, GP.COUNTRY_CODE AS COUNTRY_CODE FROM GEO_POINT GP WHERE GP.LATITUDE_DEG BETWEEN :south_west_lat AND :north_east_lat AND GP.LONGITUDE_DEG BETWEEN :south_west_lon AND :north_east_lon GROUP BY GEOHASH_PREFIX, COUNTRY_CODE
south_west_lat/south_west_lon
— широта/долгота нижней левой вершины ограничительного прямоугольника области просмотра (viewport);
north_east_lat/north_east_lon
— широта/долгота верхней правой вершины ограничительного прямоугольника области просмотра (viewport);
precision
— так как количество символов в Geohash коде напрямую связано с размером кластера, значение данного параметра зависит от расстояния между нижней левой и верхней правой вершиной (диагональ ограничительного прямоугольника области просмотра).
Этот запрос вернет координаты размещения иконки кластера и количество объектов в каждом кластере. Гео-объекты группируются в кластеры по Geohash префиксу и стране. Гео-объекты, которые расположены рядом, но в разных странах, не будут объединены в один кластер, даже имея одинаковый Geohash префикс. Для этого колонка COUNTRY_CODE
была добавлена в GROUP BY
.
Определяем параметры
Когда мы приближаем (zoom in) и отдаляем (zoom out) карту, Geohash изменяется соответственно. Длина Geohash префикса зависит от степени приближения. Определим связь длинны Geohash префикса и степени приближения как функцию y=f(x). Это скорее экспоненциальная функция (y = ae^(bx)), а не линейная (y = kx + b):
И степень приближения, и длина Geohash префикса имеют минимальное и максимальное значение. Когда степень приближения минимальная, длина Geohash префикса тоже минимальная. То же самое и с максимальными значениями.
Решим систему уравнений, чтобы получить значения параметров a и b.
x — степень приближения (zoom),
y — длина префикса Geohash,
gmin, gmax — минимальная и максимальная длина Geohash префикса,
zmin, zmax — минимальная и максимальная степень приближения.
Когда значения параметров a и b известны, функция может быть записана. Так как длина Geohash префикса имеет нижний и верхний предел, функция y=f(x) будет выглядеть следующим образом:
Что получается в итоге
В примере я использую следующие значения: gmin = 1, gmax = 12, zmin = 0, zmax = 17:
Кластеризация подходит для любого проекта, отображающего на карте большое количество гео-объектов, которые сливаются в едва различимое пятно из-за большого количества. Серверная кластеризация на основе Geohash повышает производительность, значительно снижая размер ответа сервера, заменяя несколькими кластерами потенциально тысячи точек.
P.S. Интерактивный пример доступен тут, исходный код примера — тут.