Русский
Русский
English
Статистика
Реклама

Как мы нарисовали на карте несколько тысяч интерактивных объектов без вреда для перформанса

Привет, меня зовут Дарья, и я Frontend-разработчик юнита Гео вАвито. Хочу поделиться опытом того, как мы сделали навебе новый поиск покарте, заменив кластеры более удобным решением и сняв ограничение наколичество отображаемых объектов.


В статье я расскажу, какая перед нами стояла задача и как мы справлялись спроблемами впроцессе реализации.



Чем нас не устраивал старый поиск покарте


Раньше впоиске накарте объявления отображались кластерами, а при приближении или на небольшой выборке пинами. Кластеры это когда близкие объекты накарте собраны водну большую группу, обозначенную кружком сцифрой. Пин это объект собъявлениями вдоме.


Решение скластерами было неудобным понескольким причинам. Во-первых, пользователям нужно было сделать оттрёх досеми кликов поним, прежде чем появлялась возможность кликнуть наконкретный пин и посмотреть объявления изнего всписке слева. Во-вторых, кластеры плохо считывались нужно было сравнивать цифры внутри них или сопоставлять визуально размеры кластеров. Витоге пользователю приходилось прилагать много усилий длятого, чтобы найти интересующие его объявления.


Выглядело это так:



Что мы решили изменить


Мы исследовали описанные выше проблемы, обращая внимание нато, как их решают конкуренты, и увидели, что многие ушли откластеров впользу отдельных точек. Точки хорошо подсвечивают, вкаком месте карты объявлений больше там выше плотность точек.


Мы пришли квыводу, что нам подойдёт решение сточками вместо кластеров. Пины же останутся наближних уровнях зума и наборе фильтров, которые дают небольшую выдачу, когда реально нарисовать все объявления вобласти и не закрыть ими всю карту.


Поскольку мы решили рисовать пины или точки взависимости отплотности объектов наэкране и полноты выборки, нам понадобилась формула, которая бы говорила, когда рисовать какой тип объектов. Переменные этой формулы размер карты и размер пина впикселях. Константы вней подбирали наоснове наших ощущений того, какая плотность пинов кажется нам уместной, и здесь всё индивидуально. Мы выбрали вариант, прикотором каждый девятый элемент сетки состоронами, равными диаметру пина, может быть занят. Получилось так:


$limit = viewPort.width viewPort.height / (pinDiameter^2 9)$


Если количество найденных объявлений меньше этого лимита, то показываем пины, а иначе точки.


Мы хотели сделать консистентное поведение длявсех объектов накарте, чтобы пользователю было удобно пользоваться поиском. Поэтому нужно было применить дляточек те же требования, которыми обладают пины. Витоге получился такой список:


  • точки должны быть кликабельными навсех уровнях зума (сокращаем трудозатраты пользователя);
  • точки должны реагировать нанаведение (подсказываем пользователю, что они кликабельны);
  • точки должны становиться просмотренными, если поним кликали, и сохранять просмотренность надлительное время (чтобы пользователю было легче ориентироваться накарте).

Приведу примеры целевого состояния поиска покарте дляразных случаев:



Много объявлений безфильтров показываем точки. Голубой цвет точек обозначает просмотренность, красный ховер



Выбраны фильтры, которые дают небольшую выдачу показываем пины



Приблизились на уровень, когда виды дома, и можем отобразить все объявления показываем пины


Далее я перечислю вопросы, скоторыми мы столкнулись впроцессе реализации, и расскажу, как их решали.


Как нарисовали на карте несколько тысяч точек


Когда унас были кластеры и пины, мы рисовали накарте меньше 1000объектов. Вслучае сточками нужно уметь рисовать много объектов, чтобы подсвечивать пользователям, где объявлений больше. Например, так:



Поиск двухкомнатных квартир повсей России


Мы используем Яндекс-карты, API которых предоставляет разные способы отрисовки объектов. Например, кластеры мы рисовали черезинструмент ObjectManager, и он отлично подходит дляслучаев, когда количество объектов накарте не превышает 1000. Если попробовать нарисовать сего помощью, например, 3000объектов, карта начинает подтормаживать привзаимодействии сней.


Мы понимали, что может появиться необходимость отрисовать несколько тысяч объектов безвреда дляпроизводительности. Поэтому посмотрели всторону ещё одного API Яндекс-карт картиночного слоя, длякоторого используется класс Layer.


Основной принцип этого API заключается втом, что вся карта делится натайлы (изображения вpng или svg формате) фиксированного размера, которые маркируются через номера X, Y и зум Z. Эти тайлы накладываются поверх самой карты, и витоге каждая область представляется каким-то количеством изображений взависимости отразмера области и разрешения экрана. Собственно, API берёт насебя всю фронтовую часть, запрашивая нужные тайлы привзаимодействии скартой (изменении уровня зума и сдвиге), а бэкенд-часть нужно писать самостоятельно.



Разбиение натайлы. Длянаглядности их границы выделены черными линиями. Каждый квадрат отдельное изображение


Этот механизм подходит длярешения нашей задачи, ведь мы можем отрисовать набэкенде любое количество точек и отдать нафронт готовые изображения. Приэтом количество изображений не будет зависеть отданных.


Чтобы реализовать бэкенд, мы взяли формулы дляприведения координат точек ксистеме тайлов впроекции Меркатора:


func (*Tile) Deg2num(t *Tile) (x int, y int) {    x = int(math.Floor((t.Long + 180.0) / 360.0 * (math.Exp2(float64(t.Z)))))    y = int(math.Floor((1.0 - math.Log(math.Tan(t.Lat*math.Pi/180.0)+1.0/math.Cos(t.Lat*math.Pi/180.0))/math.Pi) / 2.0 * (math.Exp2(float64(t.Z)))))    return}func (*Tile) Num2deg(t *Tile) (lat float64, long float64) {    n := math.Pi - 2.0*math.Pi*float64(t.Y)/math.Exp2(float64(t.Z))    lat = 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n)))    long = float64(t.X)/math.Exp2(float64(t.Z))*360.0 - 180.0    return lat, long}

Приведённый код написан наGo, формулы длядругих языков впроекции Меркатора можно найти поссылке.


Нарисовав svg поданным, мы получили подобные изображения тайлов:



Поскольку нам необходимо учитывать разные фильтры впоиске покарте, мы добавили значения выбранных фильтров вGET-запросы затайлами. Реализация нафронте свелась кнескольким строкам кода:


const createTilesUrl = (tileNumber, tileZoom) => {// params - выбранные фильтры в формате строки параметров, которые можно передать в GET-запросreturn `/web/1/map/tiles?${params}&z=${tileZoom}&x=${tileNumber[0]}&y=${tileNumber[1]}`;};const tilesLayer = new window.ymaps.Layer(createTilesUrl, { tileTransparent: true });ymap.layers.add(tilesLayer);

В итоге карта стайлами, подготовленными набэкенде и отрисованными черезLayer, выглядит так:



Как оптимизировали бэкенд


Используя механизм тайлов, мы будем присылать накаждую область примерно 15-30запросов отодного пользователя, и примаксимальном трафике накарте нагрузка насервис будет достигать 5000rps. Приэтом наш сервис только формирует изображения длякарты наосновании запросов сфронта, а данные дляобъектов собирает сервис поиска. Очевидно, всервис поиска не нужно ходить накаждый запрос.


Решая проблему сетевых походов вдругой сервис, мы придумали запрашивать данные дляобласти внесколько раз больше, чем реально отображаемая область. Будем называть её большой областью. Приэтом дляразных уровней зума мы установили разный коэффициент. Например, дляуровня, когда отображается вся Россия, это 4, а максимальное значение равно 25.


На изображении ниже разными цветами выделены разные большие области. Призапросе определённой области накарте, данные длянеё могут формироваться изнескольких больших областей.



Ответ побольшой области отсервиса поиска мы складываем вRedis схэш-ключом, содержащим всебе фильтры, диапазон тайлов и зум. Длякаждого запроса бэкенд проверяет, есть ли вкэше данные дляэтих фильтров и тайлов, и если есть, они отдаются изкэша. Если соответствий не нашлось, то дляэтих тайлов строится новая большая область и выполняется запрос впоиск поней.


Большая область геостационарна, то есть она строится всегда одинаково, независимо оттого, накакой тайл пришёлся первый запрос. Это избавляет отнакладок больших областей, чем обеспечивает консистентность данных и уменьшает размер кэша.


Время жизни разных запросов вRedis отличается. Дляшироких и популярных запросов, например, повсей России безфильтров, оно больше, чем длязапросов сузкими фильтрами. Кроме долгого кэша вRedis есть быстрый кэш непопулярных запросов вin-memory. Он позволяет уменьшить нагрузку насеть и освободить место вRedis.


Поскольку наш сервис горизонтально масштабирован и поднят нанескольких десятках подов, важно, чтобы параллельные запросы отодного пользователя попали наодин под. Тогда мы можем взять эти данные изin-memory cache пода, и не хранить одну и ту же большую область наразных подах. Этот механизм называется стики-сессиями. Насхеме его можно представить так:



Сейчас среднее время ответа запроса тайла на99перцентиле составляет ~140ms. То есть 99% измеряемых запросов выполняются за это или меньшее время. Длясравнения: вреализации черезкластеры запрос выполнялся ~230ms натом же перцентиле.


Работу пода упрощённо можно представить следующим образом: навход поступает запрос затайлом, строится большая область, проверяется кэш. Если вкэше есть данные, поним рисуется svg. Если данных дляэтой большой области нет, происходит запрос всервис поиска.



Итак, мы решили проблему сотрисовкой большого числа объектов накарте. Осталось разобраться синтерактивностью.


Кликабельность, ховер и просмотренность точек


Кликабельность. Самой критичной длянас была кликабельность, поэтому мы начали снеё. Врамках ресёрча мы сделали простое решение отправку запроса скоординатами набэк, бэк проверял, есть ли вкэше объявления поэтим координатам срадиусом 50метров. Если объявление находилось, рисовался пин. Если вкэше не было данных по области, вкоторой находились координаты, то есть истекло время хранения кэша, бэк запрашивал данные изсервиса поиска. Это решение оказалось нестабильным иногда пин рисовался втом месте, где не было точки как объекта накарте. Так происходило потому, что кэш набэке протухал, и появлялись новые объявления данные расходились стем, что есть накарте.


Мы поняли, что стабильнее будет реализовать кликабельность нафронте. Помимо запросов затайлами, унас всегда отправлялся один запрос запинами. Пины мы рисуем нафронте, и фронт ничего не знает прото, рисовать вданный момент времени пины или точки. Запросы запинами и тайлами уходят всегда накаждый сдвиг или изменение уровня зума. Чтобы не усложнять и рисовать всё быстрее, тайлы длянепустых выдач всегда возвращаются сточками. Если нужно рисовать пины, они рисуются поверх точек, перекрывая их. Поэтому всё, что нам оставалось длякликабельности точек, добавить вответ запроса запинами объект точек, который будет содержать координаты, id и количество объявлений вэтой точке. Этих данных достаточно для того, чтобы нарисовать поклику наточку пин.



Ховер. Идея присылать данные нафронт отлично решала также проблему ховера мы могли подписаться насобытие движения мыши покарте и проверять, попадают ли координаты курсора врадиус какой-то източек, что нам прислал бэкенд. Если совпадения найдены, рисуется красная точка накарте. Понашему опыту даже для10 тысяччерез forEach точек это выглядит нормально.



Просмотренность. Просмотренность пинов накарте уже была реализована наклиенте. Мы хранили вlocalStorage стек из1000id объявлений. Ids вытеснялись более свежими, которые были просмотрены позже других. Бэкенд ничего не знал пропросмотренные объявления, отдавал одинаковые данные всем пользователям, а клиент делал пин просмотренным наосновании данных изlocalStorage.


Для простоты мы поддержали это решение, распространив его наточки. Разница между точками и пинами втом, что пины это физические объекты карты, а точки нет. Поэтому наэтапе обогащения данных нам приходится рисовать просмотренные точки как объекты карты.


Поскольку запросы затайлами и запрос запинами и точками независимые, иногда мы можем видеть небольшой лаг вокрашивании точек вбледно-голубой. Тайлы обычно приходят раньше ответа запроса поточкам/пинам, и соответветственно, сначала мы увидим все синие точки, а потом нарисуем просмотренные точки как объекты. Сейчас длянас это не выглядит критичным моментом.


В целевом состоянии мы планируем перевести хранение просмотренных объявлений набэкенд и обогащать данные наэтапе рисования svg и формирования ответа запроса запинами. Тогда тайлы будут рисоваться сразу сбледно-голубыми точками.



Просмотренный пин выделен бледно-голубым



Просмотренная точка выделена бледно-голубым


Заключение


Качественные и количественные исследования показали, что решение сточками вместо кластеров облегчило поиск покарте дляпользователей выросло использование карты и сократились трудозатраты досовершения контакта.

Источник: habr.com
К списку статей
Опубликовано: 29.07.2020 10:12:50
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании авито

Maps api

Usability

Визуализация данных

Карты

Яндекс.карты

Performance

Geo

Search

Категории

Последние комментарии

© 2006-2021, personeltest.ru