Компания
447,23
рейтинг
19 ноября 2014 в 17:34

Разработка → ObjectManager в API Яндекс.Карт. Как быстро отрисовать 10 000 меток на карте и не затормозить всё вокруг

Перед разработчикам, которые используют API Яндекс.Карт, довольно часто встаёт задача отобразить много объектов на карте. Действительно много — порядка 10 000. Причем эта задача актуальна и для нас самих — попробуйте поискать аптеки на Яндексе. На первый взгляд кажется: «А в чем собственно проблема? Бери да показывай». Но пока не начнешь этим заниматься, не поймешь, что проблем на самом деле целый вагон.



Вопросы по большому количеству меток с завидной регулярностью поступают в наш клуб и техподдержку. Кто все эти люди? Кому может быть интересно показать на карте больше 10 меток? В этом посте я подробно рассмотрю весь вагон проблем и расскажу, как в API появились инструменты, помогающие разработчикам оптимально показать большое количество объектов на карте.

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



Также от большого количества данных страдают ресурсы, посвященные недвижимости. Яркий пример – Cian.ru.



Мы сами внутри Яндекса до недавнего времени советовали смежным командам различные «хаки» и приемы для показа множества точек через API. Яркие примеры – Яндекс.Недвижимость и Яндекс.Такси.



Пункт 1. В чем собственно проблема?


Чтобы прочувствовать на себе всю тяжесть поставленной задачи, нужно попробовать ее решить. Для начала давайте поймем, как показать карту на странице вашего сервиса. Рассмотрим простую схему:



Клиент (например, Safari на iPhone) запрашивает с сервера страницу index.html. Страница представляет собой документ вот с таким кодом:

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script src="//api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>
    <script type="text/javascript">
        ymaps.ready(init);

        function init () {
            var myMap = new ymaps.Map('map', {
                center: [55.76, 37.64], 
                zoom: 10
            });
        }
    </script>
</head>
<body>
    <div id="map"></div>
</body>


Теперь усложняем задачу. У нас есть база данных, в которой хранятся адреса болельщиков «Зенита». И мы хотим показать на карте адреса этих болельщиков.

Решение задачи «в лоб»:
  1. Делаем выборку из базы данных, получаем 1 млрд адресов.
  2. Дописываем в файл index.html массив, содержащий весь миллиард адресов.
  3. Передаем этот файл на клиент.
  4. На клиенте перебираем данные массива и рисуем для каждого элемента метку на карте.


Если вы менеджер проекта, и ваш разработчик демонстрирует такое решение, скорее всего, вы поседеете. Вы выскажете ему свое оценочное суждение. Если убрать нецензурную брань, можно будет выделить следующие тезисы:
  • Вес файла index.html увеличится до нескольких Мб и у пользователя страница будет открываться по несколько секунд.
  • Зачем передавать на клиент ВСЮ базу, если нужно показать только метки для Москвы?
  • Зачем рисовать на карте ВСЕ метки, если человек увидит только десятую их часть?
  • Если на карте нарисовать около 100-200 меток обычным способом, карта будет тормозить.
  • Можно загружать метки постепенно, пачками, чтобы канал не забивался и браузер успевал эти метки отрисовывать?


Мысль получает два направления:
  1. Нужно уметь определять, какие данные видит пользователь и запрашивать только нужное.
  2. Когда это нужное пришло, его надо оптимально отрисовать.

В API Яндекс.Карт сто лет назад был сделан инструмент для решения этих задач – технология активных областей. Кому интересно подробно, почитайте руководство разработчика.

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

С помощью хотспотов, например, рисуются пробки на maps.yandex.ru. На этой же технологии сделан сайт bankomator.ru.



У этой технологии есть несколько существенных минусов
1. Очень сложная серверная часть. Попробуйте на досуге написать модуль, который генерирует вот такие картинки и их геометрические описания, и вы все поймете.



2. Абсолютная негибкость. Невозможно «приподнять» метку при наведении на нее курсора. Невозможно быстро поменять на клиенте внешний вид меток. Короче – на любой чих надо просить сервер перегенерировать картинку.

Поэтому пользователи крутились, как могли, без хотспотов – передавали наборы единичных объектов на клиент пачками, через таймаут. При этом на клиенте их снова ждали проблемы. Если вы передали на клиент 1000 точек, как их отрисовать?

Из каждой точки нужно было сгенерировать объект ymaps.Placemark и добавить его на карту. Можно было добавить метки в кластеризатор (ymaps.Clusterer) и добавить откластеризованные метки на карту. Тут надо обратить внимание, что при кластеризации 10 000 точек нужно сначала эти 10 000 точек инстанцировать, а потом передать в кластеризатор. То есть метка может не показаться на карте, так как войдет в кластер, но мы все равно потратим время на ее инициализацию.



Подытожив все эти дела, мы решили написать модуль, который бы позволил:
  1. Быстро и легко отрисовать на клиенте большое количество точек.
  2. Избежать лишних инициализаций при работе с точками на клиенте.
  3. Загружать данные на клиент строго по требованию.

И мы это сделали. Мы котики.

Пункт 2. Рисуем метки быстро


Чтобы научиться рисовать метки быстро, надо было понять, какие проблемы кроются в текущем, уже существующем решении. Давайте посмотрим, что может делать объект ymaps.Placemark:
  1. Он умеет рисоваться на карте.
  2. У него есть свой менеджер балуна placemark.balloon.
  3. У него есть свой менеджер хинта placemark.hint.
  4. У него есть редактор, который позволяет перетаскивать метку и фиксировать ее координаты placemark.editor.

Кроме того, метка динамически реагирует на любое изменение внешней среды – изменение опций, данных, проекции карты, смена масштаба карты, смена центра карты и многое, многое другое. Такие вот у нас могучие плейсмарки.



А нужна ли вся эта программная мощь для случая, когда разработчику просто нужно показать много однотипных меток на карте? Правильно, не нужна.



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



Второе озарение пришло, когда мы думали над проблемой лишних программных инициализаций. Вспоминаем рассказ выше, где-то в районе вот такой картинки.



Нам захотелось избавиться от лишних программных инициализаций, и мы придумали гениальное. Садитесь поудобнее, сейчас будет откровение: если вам мешают лишние программные инициализации – не делайте их.
Мы решили, что будем хранить пользовательские данные об объектах (по факту в JSON), а программные сущности для объектов будут создаваться только тогда, когда какой-либо объект нужно будет отрисовать на карте.



После комбинации этих идей и некоторой разработки родился новый модуль API для отображения большого количества точечных объектов – ymaps.ObjectManager.

На вход этого менеджера скармливается JSON-описание объектов.
Менеджер анализирует, какие метки попадают в видимую область карты и либо рисует метки, либо кластеризует эти метки и показывает результат на карте.

Для отрисовки меток и кластеров на карте мы взяли только часть объекта ymaps.Placemark (а именно ymaps.overlay.*), которая отвечала только за отображение метки на карте. Всю инфраструктуру типа балунов и хинтов мы вынесли в единый общий компонент.

Эти приемы позволили нам неплохо продвинуться в вопросе отрисовки большого числа меток на клиенте. Вот какие мы получили приросты по скорости:


График 1. Скорость создания и добавления объектов на карту с последующей асинхронной отрисовкой их видимой части

  1. Создание 1000 меток и добавление их на карту, все метки видны.
  2. Создание 1000 меток и добавление их на карту с кластеризацией, все метки видны.
  3. Создание 10000 меток и добавление их на карту с кластеризацией, все метки видны.
  4. Создание 50 000 меток и добавление их на карту с кластеризацией, все метки видны.
  5. Создание 50 000 меток и добавление их на карту с кластеризацией, видны 500 объектов.
  6. Создание 50 000 меток и добавление их на карту без кластеризации, видны 10 000.



График 2. Скорость создания и добавления объектов на карту с последующей синхронной отрисовкой их видимой части

  1. Создание 1000 меток и добавление их на карту, все метки видны.
  2. Создание 1000 меток и добавление их на карту с кластеризацией, все метки видны.
  3. Создание 10000 меток и добавление их на карту с кластеризацией, все метки видны.
  4. Создание 50 000 меток и добавление их на карту с кластеризацией, все метки видны.
  5. Создание 50 000 меток и добавление их на карту с кластеризацией, видны 500 объектов.
  6. Создание 10 000 меток и добавление их на карту без кластеризации, видны 2000.
  7. Создание 5000 меток и добавление их на карту без кластеризации, видны 1000.


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

У нас получилось ускорить непосредственно создание и отрисовку объектов, вдобавок к этому мы максимально оптимизировали инициализацию программных сущностей. Теперь вы можете, например, откластеризовать на клиенте 50 000 точек, и работать с картой будет комфортно.

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

Итак, мы научились быстро-быстро рисовать и кластеризовать точки на клиенте. Что дальше?

Пункт 3. Оптимально подгружаем данные


Помните пример про болельщиков «Зенита»? Мы решили проблему отрисовки данных на клиенте, но никак не решили проблему, связанную с оптимальной подгрузкой этих данных. Мы начали собирать типовые задачи пользователей API. По итогам исследований мы получили два типовых кейса:
  1. У человека на сервере много данных, он хочет показывать их на клиенте, но подгружать данные по мере надобности.
  2. Разработчик подготавливает данные на сервере (например, реализует серверную кластеризацию) и хочет показывать на клиенте результаты этой обработки.


Для решения этих кейсов были написаны модули LoadingObjectManager и RemoteObjectManager соответственно.Оба модуля основаны по сути на реализации ObjectManager, но имеют ряд различий в алгоритме загрузки и кеширования загруженных данных.



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

Данные хранятся на клиенте в pr-дереве, поэтому выборки даже для большого количества данных делаются довольно шустро.

Теперь обсудим вариант номер два – отображение на клиенте результатов серверной кластеризации. Допустим, вы написали серверную кластеризацию меток. Также вы написали скрипт, который по запросу от клиента умеет отдавать кластеры и единичные метки, не вошедшие в состав кластера.

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

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

Если с сервера передается описание метки-кластера, то на клиенте эти метки подцепят всю инфраструктуру из API – для кластеров нарисуются специальные значки, для них будут работать все стандартные поведения и так далее и тому подобное.

Пункт 4. Размышления на тему серверной реализации


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

1. Хранение информации об объектах на сервере в статических файлах


Клиентский код оперирует данными исключительно потайлово. Тайл – это некоторая нумерованная область на карте. Подробнее про нумерацию тайлов можно прочитать в нашей документации.



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

У клиентского модуля есть настройки, которые заставляют отправлять запросы за каждым новым тайлом по отдельности. Чем это ценно? Да тем, что мы получаем конечное число вариантов запроса клиента на сервер.

zoom=0, tile=[0, 0]
zoom=1, tile=[0, 0]
zoom=1, tile=[0, 1]
zoom=1, tile=[1, 0]
zoom=1, tile=[1, 1]
zoom=2, tile=[0, 0]


Поскольку запросы известны заранее, ответы на запросы тоже можно сгенерировать заранее. Организуем на сервере какую-то такую файловую структуру.



В файлах будет храниться примерно такой код:

myCallback_x_1_y_2_z_5({
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": 0,
      "geometry": {
        "type": "Point",
        "coordinates": [55.831903, 37.411961]
      },
      "properties": {
        "balloonContent": "Содержимое балуна",
        "clusterCaption": "Метка 1",
        "hintContent": "Текст подсказки"
      }
    },
    ...
  ]
}


При загрузке такого файла на клиенте будет вызван JSONP-callback, прописанный в файле. Данные попадут в недры LoadingObjectManager, закешируются и отрисуются в нужном виде.

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

2. Динамическое формирование ответа из статических файлов


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

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

3. Динамическое формирование ответа с использованием базы данных


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

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

В этом репозитории живет пример реализации серверной части с серверной grid-кластеризацией, написанный на node.js + mongo.db. может кому-то пригодится (Демо).

Заключение


Сравнительная таблица новых модулей.

Модуль Преимущества Недостатки
ObjectManager Позволяет кластеризовать объекты на клиенте.

Отрисовка производится только тех объектов, которые попадают в видимую область карты.

Позволяет фильтровать объекты при их отображении.

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

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

Кластеризация объектов производится на стороне клиента.
LoadingObjectManager Позволяет кластеризовать объекты на клиенте.
Загружает данные только для видимой области карты.

Сохраняет загруженные данные. Для каждого объекта данные загружаются только один раз.

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

Кластеризация объектов производится на стороне клиента.

Необходимо реализовать серверную часть.
RemoteObjectManager Использует серверную кластеризацию данных.

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

При изменении коэффициента масштабирования данные загружаются заново (даже для тех объектов, для которых данные уже были загружены).

Необходимо реализовывать собственную кластеризацию.

Необходимо реализовать серверную часть.


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

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

Полезные ссылки:
  1. Подробнейшее руководство разработчика с картинками – tech.yandex.ru/maps/doc/jsapi/2.1/dg/concepts/many-objects-docpage.
  2. Примеры в песочнице – tech.yandex.ru/maps/jsbox/2.1/object_manager
  3. Проект на гитхабе с примером реализации серверной части для RemoteObjectManager – github.com/dimik/geohosting-server.
  4. Клуб разработчиков API Яндекс.Карт, куда нужно приходить с вопросами – clubs.ya.ru/mapsapi.
Автор: @Kukabarra
Яндекс
рейтинг 447,23

Комментарии (88)

  • +3
    Спасибо! Как раз заканчиваю делать GPS трекер и этот момент для меня очень актуален.
    Раньше маршрут отображался больно задумчиво.
    • +1
      У трекеров свои заморочки, я недавно делал отображение трека, довольно не тривиальная задача оказалась, но в итоге удалось с хорошей производительностью отрисовать трек из 60000 точек на WP. Очень сильно осложнялось, тем что пришлось все рисовать в 3D, так как в WP 8 нет поддержки 2D графики :)

      Получилось вроде не плохо cdn.marketplaceimages.windowsphone.com/v8/images/46a9e67d-df20-4a8d-b641-1ce2db649c09?imageType=ws_screenshot_large&rotation=0
      • 0
        Симплификация и немного другие алгоритмы «раскраски» могли бы спасти.
        • 0
          Симплификация используется по полной, более того 60000 это количество маршрутных точек, прошедших симплификацию на сервере.
          А что с алгоритмами раскраски?
          • 0
            На картинке не более 2000 точек, а по хорошему можно и в 200 нарисовать — измените параметры симплификатора :P
            А «алгоритмы раскраски» в общем случае зависят от рендера.
            Если 3D — то заливаем в точки еще значение «расстояние от начала кривой», и по этому значению через lookup в 1d текстуре(прийдется сгенерить) красим. Можно хитрее. Но в любом случае — один вызов рендера.
            • 0
              На картинке 600 точек :)
              Это скриншот из магазина приложений, но ситуация когда надо будет рисовать все точки, вполне реальна.
              А там и так не много вызовов рендера, обычно 6 дроколов на проход, больше только для треков с огромным количеством поворотов.
              Раскраска не из текстуры, а просто математика в шейдерах, получается быстрее терстур и главное стабильно качество на различных резрешениях и размерах экранов.
              На самом деле большую проблему для меня представляла тесселяция и отрисовка стрелок направления.
              Ну и математика для пересчета гео координат на GPU тоже заняла ощутимое время.
              • 0
                И стрелки и гео координаты — решаемые проблемы. Так есть хитрые варианты «ступенчатых» оптимизаций, которые у нас в АПИ называются «renderFlow».
                Надо будет как-нибудь о них рассказать.
                • 0
                  Да они решаемы и решены. После своих изысканий, думал написать статью, но мне запретили :)

                  Еще один интересный «прикол», что бы использовать кеширование хотел делать расчеты на процессоре, а потом просто отрисовывать. Не тут то было, начали появляться мизерные но отклонения, которые были хорошо видны на маленьких масштабах.
                  • 0
                    Расчеты чего? И отклонения в какую сторону?
                    Быть может проблема идет из 16-32-48-64 битных переменных?
                    • 0
                      Расчеты положения стрелок направления, я передаю вектор направления и расстояние от начала участка. Если считать на GPU то все координаты совпадают. Если считать на процессоре, результат скачет вокруг нужной точки. Расчеты везде проводил в 32 разрядных вещественных, потому, что видеопроцессор другого не умеет, а вот процессор вероятно считает в 64 бита, а потом приводит к 32. В общем проблема скорее всего в округлении. Добраться до расчетов шейдеров я не смог, поэтому просто принял за данность.
  • 0
    «Впервые» Ремот был презентован на Яндекс.Субботнике в Минске — events.yandex.ru/lib/talks/2217/
    Еще тогда в презе мелькала ссылка на серверный кластеризатор, но в те времена он работал не очень шустро.
    Буквально пару дней назад его автор (dimik) обновил библиотеку и она теперь работает с теми самыми «пространственными ключами», конкретнее с Z-curve. И работает очень быстро.
    Опен сорс, монго, map-reduce, агрегация — github.com/dimik/geohosting-server/blob/master/storage/model/feature.js#L66
  • 0
    Хорошая реализация, но всё лишь для меток. А будет ли что-то в этом роде для полигонов (особенно актуально решение из коробки для кластеризации полигонов, кастом при увеличении более 13 не хочется постоянно использовать)?
    • 0
      Для полигонов решение будет. Правда с кластеризацией неточечных объектов на клиенте картинка пока туманная.
      • 0
        Быть может сделать дополнительную опцию какую-либо, по верхней точке кластеризировать? В кастоме сейчас использую именно так, а при зуме > 13 уже отрисовываются сами полигоны.

        P.S. Задача вывести более 1 500 полигонов на старте проекта.
        • 0
          А точно нужно вывести 1500 полигонов?
          Если нужно — зачем прятать. Если склеивать то во что?
          Стандартные кейсы (n.maps.yandex.ru,wikimapia.org) отображения полигонов на карте обычно реализуют два механизма:
          1. «Не показ» невидимых полигонов(не считая что оба сайта работают на «активных областях», те тайлах). В АПИ карт для графики это выполняется автоматически.
          2. «Не показ» «мелких» обьектов на текущем зуме — именно это, а не кластиризатор, дает «разрежение» данных. Оно же — различные наборы данных для различных зумом.

          А насчет кластеризации по одной точке могу рассказать о случае esosedi:
          Там, не смотря на то, что многие обьекты площадные система работает только с точками. В случае полигона это левый верхний угол.
          Далее работают два момента — при запросе с сервера приходят только те полигоны, которые имеют угол в запрашиваемом тайле. С учетом маргина с сервера на клиент не передаются обьекты больше чем 512 пикселей на текущем масштабе.
          Потом работают просто сортировки — если вы знаете что не можете показать больше чем 500 обьектов — отсортируйте те что есть, и покажите только 500 из них.
          Например те, чьи линейные размеры меньше 512х512, далее order by size, и не более чем можно. Или не более чем нужно.
          При этом обьекты, линейные размеры которых на текущем зуме меньше некого значения (размер дива карты/64) считаются точечными и кластеризуются как точки.
          А если нужно показать все — ну судьба такова.

          К чему я это — лучше вас никто ваши данные отобразить не сможет. Написаться свой ObjectManager не так чтобы сложно. Только это (почти) никто не делает.
  • 0
    Только что заметил, что на я-картах рисуются какие-то иероглифы при поиске.
    Оказалось это цифры меток загрузились до самих меток и показываются друг над другом.
    Потом закорючки тоже появились, но выглядело забавно.
  • 0
    полагаю, более красивый вариант со статическими JSONP-файлами выглядел бы так, как описано в blog.arty.name/JSONP,-статика-и-CDN/ или в webreflection.blogspot.de/2011/08/how-to-jsonp-static-file.html
    • 0
      ага, хабр не очень хорошо парсит адреса
      вот так должно быть лучше
    • 0
      Ровно так было сделано в первой версии API: tech.yandex.ru/maps/doc/jsapi/1.x/ref/reference/hotspots.loader-docpage/
      Однако, что же в нём «более красивого» — большой вопрос. Такая схема подразумевает обязательное наличие в глобальной области видимости статического объекта (StaticJSONP), а это на один глобальный объект больше, чем хотелось бы. Статические же имена callback-ов на клиенте можно создавать и удалять динамически, как и в обычном jsonp.
      • 0
        ну вот выходит либо ноль глобальных имён (при расширении JSON), либо одно глобальное имя (при использовании StaticJSONP) против десятков глобальных имён, пусть и короткоживущих

        впрочем, в этом году уже очень мало смысла обсуждать JSONP :)
        • 0
          … хотя в «этом году» только pastvu использует WebSockets для транспорта данных карты…
          • 0
            для самых модных — WebSockets, для более простых — CORS
            • 0
              И гнать все в формате csv как экспорт из базы данных.
              Зачем весь этот синтаксический мусор JSON?
              • 0
                потому для него в браузере есть парсер на C
                • 0
                  Как и другие строковые функции :P
                  При этом трафика создает сильно больше, даже после gzip
                  Имхо — для однотипных табличных данных, а это самый частый кейс для ObjectManager, «табличный» экспорт — лучшее решение.
                  JSON как красивый и «глубокий» формат полезен когда можно что-то сильно по разному расписывать. Но тут можно воспользоваться серверной подгрузкой, а не качать все эти данные на каждый запрос.
                  • 0
                    я бы посмотрел сравнение парсера на JS и встроенного парсера

                    а ещё на степень сжатия JSON
                    • +1
                      Года три назад тесты проводились. Не в пользу JSON. Тем более не в пользу генерации JSON.
                      Попробую поднять данные, а вообще один знакомый(Alexey Perchenok) почти на эту тему диплом защищал(или докторскую?) и вроде успешно.
                      • 0
                        ну как, получилось поднять данные?

                        а я сегодня ещё и про JSON Homogeneous Collections вспомнил, у них ни с размером (как у JSON), ни со скоростью парсинга (как у CSV) не должно быть проблем
                        • 0
                          Нормальных цифер, которые можно было бы на графике показать — нет. Пну кого надо сегодня.
                          Но JSONH чтука странная (вложенности то не поддерживает), но каких мутантов только не бывает.
                          • 0
                            CSV тоже не поддерживает, а именно с ним JSONH соревнуется
                            • 0
                              Не соревнуется, а просто ничем не отличается.
                              Более того — в случае равномерных данных можно было бы заголовки передать один раз, а не в каждой строке.
                              По сути JSONH просто создает строчку где в начале идут ключи, потом значения.
                              Я думаю, в 99% случаях, ключи и так известны с обеих сторон.
                              • 0
                                да, очень похож, но я бы не стал говорить, что совсем ничем не отличается
        • 0
          Расширять чужой JSON (мы же API, работаем в контексте другого сайта) мы не имеем права. По той же причине не можем заводить в глобальной области переменные с неспецифичными именами типа StaticJSONP — мало ли, вдруг у вебмастера есть своя переменная StaticJSONP.
          • 0
            ну можно и в своё пространство имён положить
            хотя ещё раз скажу, сейчас тема уже потеряла актуальность
            • 0
              Тогда этой системой сможем пользоваться только мы, вебмастер не сможет.
              Короче, проходили мы всё это.
              • 0
                Но вот что мешало в последнем этапе, по сути «транспорте» оставить дырку для user реализации? До сих пор понять не могу :(
  • 0
    Еще можно не изобретать велосипед и использовать стандартизированные технологии. протокол OGC WFS вполне позволяет тянуть данные (полигоны, линии, точки) с сервера (сервера — в смысле «готового серверного приложения»), передавая серверу только область покрытия (bounding box). При этом решать вопрос перегруженности области можно сразу и на стороне клиента (используя кластеризацию, которую позволяет делать тот фреймворк, который выводит собственно карту) и на стороне сервера (используя для хранения объектов БД с пространственными расширениями и адаптируя запрос к ней в соответствии с величиной bounding box.

    Простор для оптимизаций под свои нужды — огромный, например, можно закэшировать центроиды полигональных объектов (а не считать их каждый раз), чтобы показывать точечные маркеры вместо полигонов для тех масштабов, где величина каждого конкретного полигона меньше величины точечного маркера. Интервалы этих масштабов — тоже закэшировать.
    • 0
      Все, что вы описали, прекрасно работает в связке с клиентскими модулями, описанными в статье
      • 0
        Это был, скорее, ответ на некий оттенок изложения в статье, из которого можно легко сделать ошибочный вывод, что ваша реализация — чуть ли не единственное спасение от тех проблем, которые вами описаны.
        • 0
          Ну я действительно не знаю про клиентские библиотеки, работающие в связке с популярными карточными апи, который управляют оптимальной загрузкой данных. Буду благодарна, если приведете ссылки на такие решения
          • 0
            Если под загрузкой вы понимаете загрузку с сервера, а не показ, то вопрос чаще всего решается именно на стороне сервера, опираясь на bbox и запрошенный набор слоев. А клиент занимается уже только показом, стилизацией, кластеризацией (для того же Leaflet есть прекрасные варианты ее реализации) и подгрузкой свойств (WFS getfeatureinfo) вслед за действиями пользователя.
            • 0
              Я имею в виду вот что. Вот у вас есть карта, у нее есть видимая область. Вы берете эту область и делаете запрос на сервер за данными. Данные пришли, их нарисовали — все ок.
              Дальше пользователь меняет зум карты. Клиент должен понять, какие данные у него уже для новой видимой области карты загружены, а какие нет. И сходить строго за недостающими данными. Ну и аналогично при сдвиге карты.
              • 0
                OpenLayers поддерживает Tiled-режим классического WFS уже много лет. Чуть менее классический WMTS (который может содержать как растровые, так и векторные данные) тоже дофига где работает, включая OL и Leaflet. ESRI также имеет для своих feature layer-ов в ArcGIS Web API такую же штуку. И ниже я уже писал вашему коллеге, что разбиение на тайлы — это всего лишь один из видов оптимизации, который еще и не всегда полезен.
              • 0
                Э-э-э… А никак нельзя отправить запрос типа «сессия та же, зум стал меньше вдвое, дай-ка мне, сервер, дифф»?

                Зачем клиенту что-то понимать?
                • 0
                  Насколько я понял — именно так работает pastvu (WebSockets), но это на самом деле сложно сделать. В том числе из-за большого latency.
                  Обычно получается как-то так
                  events.yandex.ru/lib/talks/2217/ — в самом начале презентации разбираются варианты загрузки и обьясняется несколько стандартных косяков.
                  Если лень смотреть немного расскажу:
                  1. Кроме зума есть текущее «окно» выборки — активная область на карте.
                  2. Окно может быть любым
                  3. Одно и тоже окно в гео координатах может указывать на различные окна в пиксельных, но на разных зумах.
                  4. И вы не можете передать все данные с сервера на клиент.

                  Результат — прыгает и скачет, неравномерное покрытие карты и тд и тп. И чем «умнее» сервис — тем хуже(по этим параметрам) в итоге работает. Как не странно.

                  PS: Только тайлы, только стабильная выборка. А если хотите одним запросом — ничего не мешает послать один большой запрос с указанием загружаемых квадрантом и получить один ответ с кучей данных. Флаг splitRequest.
                  • +1
                    Стоп-стоп-стоп.

                    Пока я не смотрел видео, задам вопрос из незамутненного избыточным знанием мозга. Прозой: «что такого клиент не может передать на сервер, что создает ситуацию „клиент знает, а сервер нет“?».

                    Пьесой:
                    ————
                    Начало сессии.

                    Клиент (серверу): дай данных для lat/long/zoom/whatever.
                    Сервер: ннна!
                    Клиент (рисует).
                    Юзер: тап-тап-тап.
                    Клиент: у меня тут юзер обнаглел, хочет теперь lat/long/zoom/whatever2 и, пока мы тут кофе пили, он еще успел экран сменить на ретину.
                    Сервер (хранилищу): дай всё для lat/long/zoom/whatever2.
                    Сервер (делает дифф).
                    Сервер (клиенту): ннна!

                    • 0
                      Не помню когда я про это рассказывал, но вы явно бородатый и родом из gamedev.
                      Ибо описали типичную ошибку. Или не ошибку, а «привычку» что клиент и сервер это что-то цельное, что-то рядом.
                      Хотя нет. Любой «приставочник», у которого носитель — CD, а не SSD имеет ровно туже проблему — можно иметь хорошую скорость канала, но латенси…
                      А в вэбе между вами километры проводов и этот ужасный http. И, конечно же, клиентов у вас СИЛЬНО больше чем серверов.
                      Без организации относительно полноценных мозгов как на сервере, так и на клиенте — ваши сервера взорвуться!
                      Му-ха-ха-ха-ха!
                      Ну или можно покурить теории и практики сетевых предсказаний еще кваки первой. С тех пор суть проблемы мало изменилась.
                      www.gamedev.ru/code/articles/?id=4258
                      en.wikipedia.org/wiki/Client-side_prediction
                      • +1
                        Я к геймеву никогда даже близко не подходил, но дело не в этом.

                        Мне очень хорошо понятно, что бесплатный ботнет это круто, и этим необходимо пользоваться. Но в данном конкретном случае: а) провода — вообще не в кассу, если подумаете — сразу поймете, почему (вне зависимости от того, кто придумал структуру диффа, передать дифф придется); б) эффективнее эту задачу решать на сервере, даже когда он один у разраба под столом (у сервера есть куча дополнительных способов снизить нагрузку, да и при правильной архитектуре построить дифф не должно быть ощутимо затратнее, чем просто сплюнуть данные); в) prediction — это хорошо и нужно, только вы все путаете: prediction в данном случае относится к «уловили движение глаз юзера влево и запросили этот кусок не дожидаясь скролла».

                        И, наконец, г) я понимаю и принимаю аргумент «мы хотим разгрузить наши сервера», правда я пока не понимаю, удалось ли решить эту задачу в данном конкретном случае. Пока складывается впечатление, что нет.
                        • 0
                          Все плохо :(
                          Представим что я вот
                          1. открыл вашу карту.
                          2. передал viewport
                          3. сервер сделал выборку из бд
                          4. передал мне 100 маркеров и запомнил какие
                          — прошло, кстати, 50-300 мсек.
                          5. я передал новый viewport (он может отличаться на 10 пикселей)
                          6. сервер сделал выборку из бд (о, 2д индекс это круто)
                          7. сервер начал искать диф между 100 старыми и 100 новыми. В худшем случае у меня теперь 200 точек
                          — будем считать, что 100-500 мсек

                          100. сервер сделал выборку из бд
                          101. диф между 100500 тем что уже отправил, и новыми 100.

                          На самом деле умрет на выборках из бд — потому что их нельзя закешировать. В случае загрузки по дискретным границам — можно.
                          Конечно никто не запрешает держать «дискретные» границы на сервере, но дифать сложно будет.
                          Плюс дифы — просто зло. Выборка должна быть _стабильна_ вне зависимости от внешний условий.

                          PS: Мы, конечно же говорим о двух случаях:
                          1. У вас много пользователей и много данных
                          2. У вас мало пользователей, средне данных, но и сервера на виртуалке…
                          • 0
                            > 6. сервер сделал выборку из бд (о, 2д индекс это круто)
                            > 7. сервер начал искать диф
                            А выборка не может быть сразу диффом? Я же не знаю архитектуру, может быть, и не может, но вроде бы никто не в силах помешать поднять рядом инстанс, из которого можно сразу забирать дифф. Ему же даже репликация мгновенная не нужна, маркеры это не пробки, ежесекундно не должны меняться.

                            > 101. диф между 100500 тем что уже отправил, и новыми 100.
                            Зачем это? Обычно диффы делают супротив предыдущей версии, а не момента перед «И создал бог свет».

                            > Плюс дифы — просто зло.
                            Мы точно в одной и той же ветке? :)
                            Мой первый комментарий («а почему не сервер?») был реакцией на «дифф должен определить клиент».

                            Давайте я еще раз скажу: я понимаю, почему в вакууме лучше все, что можно, — считать на клиенте. Я даже теорию для чайников прочитал.
                            Но в данном конкретном случае я не вижу выгоды. Все еще не вижу (возможно, это мои проблемы, не спорю).

                            Я понимаю, что серверу не помешает сразу отдавать все восемь квадратов вокруг, а клиенту предупредительно ходить за «еще» по факту скролла/зума. Это как раз то, что вы назвали «prediction». Но зачем клиенту вычислять, какие ему данные нужны — убейте, не представляю. Не говоря о том, что клиент вообще-то не обладает полнотой информации на данном этапе (например, он может не знать, что плотность покрытия на соседнем участке гораздо выше и с этим надо что-то сделать, или: соседний участок пуст, поэтому вот тебе следующий, чтобы пустые пакеты не гонять).
                            • 0
                              В том и проблема, что никто не видит проблемы.
                              Тормознутый js, ширина канала, latency и RPS — всего три параметра, которые усложняют жизнь.
    • 0
      И нельзя просто взять и загружать данные по bbox — нужна сетка.
      • +1
        Разбиение на тайлы — всего лишь одна из форм оптимизации. В общем случае оно не является обязательным.
      • 0
        Даже, скорее, так: польза от разбиения данных на тайлы зависит от характера распределения данных в пространстве, относительной нагрузки на сервер и наличия функций выборки объектов. Когда клиент имеет возможность выбрать, условно, любые 49 слоев из 135 возможных (нормальная ситуация для ГИС, но редкая — для «плоских» сервисов), а распределение объектов не является сколько-нибудь равномерным, разбиение на тайлы становится откровенно вредным, увеличивая число запросов ради «полупустых» тайлов.
        • 0
          Для таких случаев существует понятие «пирамиды тайлов», про которое немного знает LoadingObjectManager.
          А вообще про «загрузка по квадродереву», когда для адресации «тайла» используются Z-коды лично я рассказывал года 3-4 назад (на CodeFest). А по факту — Wikimapia использует такой механизм уже лет 6, если не больше.

          Как бы не было идеи придумать что-то новое — ничего прорывного в представленной «технологии» нет. Была идея предоставить полезный инструмент тем людям, которые за прошедшие годы до этого сами не дошли.
          • 0
            Я тут возражал немного по другому поводу (по поводу того, что сетка не всегда нужна и не всегда вообще полезна), возможно вы интерпретировали «слои» как «масштабные слои», но я этого не имел в виду.
            У Викимапии или условного «сервиса с банкоматами» — один слой данных, возможно — разные стили внутри него.

            А в общем случае (ГИС, а не картографический сервис) тематических слоев может быть сто. Резать каждый на тайлы и отдавать пользователю столько, сколько он хочет, из этой сотни на каждую ячейку сетки — расточительство, особенно если неизвестно, есть ли сколько-нибудь значимое число данных в каждом тематическом слое. В этом случае отдавать данные целиком на bbox вполне может оказаться экономнее уже из-за снижения накладных расходов на передачу.
            • 0
              Покурите gdeetotdom.ru/map на момент загрузки слоев данных (их там ~30). Так, например на сервер идет запрос вида:
              www.gdeetotdom.ru/map/tiles/14-42-39-48-38l032/103/002/2/
              14-42-39-48-38 — это номер «слоев» которые надо отдать из тайла 032/103…
              Никакого оверхеда, и со 100 слоями тоже будет работать хорошо.

              У викимапии, кстати, слоев — очень много. Можно увидеть если сделать поиск и/или выбрать какую либо категорию.
    • 0
      Разбирать XML на клиенте? Спасибо, но нет.
      Не говоря уже о том, что там гигантский оверхед по функциональности (10+ разных методов), но все равно не покрыты базовые кейсы типа задания стилей меткам.
  • 0
    Недавно пригодилось, хорошая вещь (использовал LoadingObjectManager), но местами волосы начинали шевелиться в разных местах тела. Например от таких вещей:

    Нужно было динамически загружать данные в balloon метки, т.е. для метки из менеджера (из коллекции loadinObjectManager.object) по которой был клик, нужно было установить свойства, тут начинается интересное: вот тут описан метод setObjectOptions, но не описан метод setObjectProperties, а на самом деле он есть, нашёл через консоль хрома.

    На документации приключения не закончились: кликаем на метку, открывается пустой бален, после получения ответа на ajax-запрос делаем

    objectManager.objects.setObjectProperties(obj.id, {
    	balloonContent: data
    });
    

    и балун остаётся пустым, но стоит сдвинуть карту хоть на пиксель — контент появляется.

    Описанные ситуации имели место в версии 2.1.16, на 2.1.17 не проверял.

    И кстати, здесь пример jsonp ответа с синтаксической ошибкой. Это не помешало ни сколько в разработке, но удивило.

    Не судите строго за, может быть, негативный оттенок комментария, просто накипело.
    • 0
      Метод не включили в документацию, так как не хотим, чтобы его использовали. Для загрузки содержимого балуна по требованию мы рекомендуем другой способ и описали его в примере в песочнице.
      Можете уточнить, в чем ошибка? Мы в ближайшее время поправим.
      • 0
        Просто метод вполне себе публичный, в противном случае я бы его бы даже и не заметил.
        Способ из песочницы мне почему-то не подошёл(уже не вспомню по какой причине), что меня и побудило к раскопкам.

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

        Ошибка проявлялась примерно так
        objectManager.events.add('balloonopen', function(e) {
        	var d = e.get('target').getData();
        
        //	открывается балун с прелоудером
        	loadingObjectManager.objects.setObjectProperties(d.id, {
        		balloonContent: 'Загружаю…'
        	});
        
        
        //	getBalloonData посылает запрос для получения данных и возвращает Promise
        	getBalloonData(d.id).then(function (data) {
        		objectManager.objects.setObjectProperties(d.id, {
        			balloonContent: data
        		});
        
        //		ответ пришёл, метод setObjectProperties отработал, но по прежнему видим балун с надписью «Загрузаю…», если сместить немного карту мышкой или даже методом map.panTo новый контент балуна появляется мгновенно
        
        	});
        });
        


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

        А есть ли какие-нибудь примерные сроки релиза поддержки objectManager`ами полигонов?
        Когда objectManager`ы будут поддерживать все типы геообъектов, цены им не будет!
        • 0
          Вот корректный способ решить вашу задачу jsfiddle.net/hn2fnxku/.
          Я посмотрела — у нас не подтянулся в документации соответствующий пример к методу, поправим.

          Что за синтаксическая ошибка в примере с json?

          Поддержка полигонов уже в процессе разработки, но по срокам сейчас не могу ничего сказать.
          • 0
            Вот здесь после таблицы с параметрами 2 примера, во второй
            jsonp_callback({
                // Ответ содержит поля error и data. В случае ошибки в поле error
                // пишется код ошибки или ее описание.
                error: null,
                data: {
                    {
                        type: 'FeatureCollection',
                        features: [
                             {
                                 type: 'Feature',
                                 geometry: {
                                     type: 'Point',
                                     coordinates: [55, 35]
                                 },
                                 id: 23,
                                 properties: {
                                     balloonContent: 'Содержимое балуна метки',
                                     iconContent: 'Содержимое метки'
                                 },
                                 options: {
                                     preset: 'islands#yellowIcon'
                                 }
                             }
                        ]
                    }
                }
            });
            


            data лишний раз обёрнута в {}
            • +1
              Спасибо, поправим
              • 0
                Но воз и ныне там
                Написал не для того, чтобы лишний раз тыкнуть "ах вы такие", а для чтобы как-то ускорить этот процесс, я лично отправлял много фидбеков про ошибки в документации к АПИ, но они не правятся — «Спасибо, ваше обращение очень важно». Все.
                • 0
                  В документации на релиз-кандидат ошибку уже поправили. Что вы так горячитесь? Минорная же правка)
                  • 0
                    Ну тогда и здесь исправьте код ответа )
                    • 0
                      ок)
  • 0
    Была месяцев 8 назад задача отобразить 32к точек, в итоге единственным адекватным решением в плане сложности реализации/скорости оказалось:
    1) Предварительная кластеризация для малых масштабов
    2) Для крупных масштабов предварительно получать прямоугольник отображаемой области, вручную отрезать точки, которые не попадают в прямоугольник, и отображать только их, используя кластеризатор.
    • +1
      Я тоже как-то делал подобное. Нашел библиотеку от хорошего человека.
      Скорость можно оценить на этом сайте, там сейчас как раз 32к точек отображается:
      facebiz.info/search/
      Выберите слева плитку «Торги высвобождаемого имущества»
      и в открывшемся окне нажмите «Найти»
      • 0
        <Зануда мод он>У вас на карте при поиске не видно копирайтов, что нарушает пользовательское соглашение</Зануда мод офф>. А по делу да — шустро работает.
  • 0
    А существует какие-нибудь открытые CMS на эту тему (можно в виде плагина к Wordpress)? С одной стороны — форма добавления точек в базу, а с другой — карта со всеми этими крутыми оптимизациями.

    Иногда хочется сделать что-то вроде локального сервиса помощи или карты интересных событий в ближайшей округе, но количество тонкостей, которые надо реализовать для приличного результата — пугает.
  • 0
    А подскажите, для яндекс.пробок слоя есть возможность получить информацию не графическим слоем единым, а набором данных пригодным для анализа? Не просто отразить графически загрузку по маршруту для визуальной оценки, а для чтобы выполнить автоматическую оценку загруженность? Не совсем по теме статьи, но просто по случаю, раз специалисты по яндекс.картам на хабре.
    • 0
      Публичного api для решения вашей задачи сейчас нет
  • 0
    В общем, как говорится: «хорошая мысля — приходит опосля».
    Скрытый текст
    Несколько лет назад, потестировали несколько кластеризаторов (в том числе и от яндекса), и в итоге остановились на своём (конечно, он не так хорош, как ObjectManager). Но в нём не происходит инициализация меток для карты, пока метка не оказывается рядом с границей видимой части карты, группировка меток привязана не к каждому значению зума. Работа по группировке проводится не с объектами-метками, а с исходными данными. Сейчас эти хорошие мысли описаны в этой статье. На тот момент показал неплохое быстродействие среди прочих кластеризаторов, а общие принципы позволили достаточно легко подключать этот кластеризатор к другим сервисам карт. В итоге и сейчас продолжает работать. Теперь будем тестировать новый ObjectManager
  • 0
    Хочу задать вопрос здесь, может кто-то из Яндекса ответит т.к. в клубе разработчиков карт ответы есть только на базовые вопросы.

    Суть в том, что мне нужно чтобы LoadingObjectManager не кешировал результаты, а при каждом действии обращался к серверу за данными, это делает RemoteObjectManager, но он не кластеризирует сам, а кластеризация нужна.

    Как отключить кеш в LoadingObjectManager?
    • 0
      Такие вопросы лучше задавать в клубе, потому что тогда другие пользователи смогу найти ответы на их вопросы, не создавая новых тем. А отвечают и тут и там все равно одни и те же люди.
      Сейчас такой возможности нет, но мы подумаем о добавлении такой функциональности и будущих релизах.
      • 0
        Скажу так: это очень важный функционал. RemoteObjectManager — это уже хардкор уровень, который не часто может понадобится, а LoadingObjectManager является как раз нужной в большинстве случаев серединой.

        Ещё вопрос: как заставить LoadingObjectManager перезагрузить текущее состояние меток програмно? Т.е. взять и без изменения масштаба заставить его очистить кеш и получить данные снова? (немного другой вопрос чем был ранее)

        • 0
          Сейчас это возможно сделать только удалив-добавив менеджер с карты. В будущем добавим метод для этого кейса
          • 0
            Надеюсь таки будет программный reload для LoadingObjectManager
            • 0
              Да, выйдет в паблик через релиз
              • +1
                Ещё один баг про LoadingObjectManager:

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

                (Это нужно и в топик и в документацию добавить) что LoadingObjectManager при отключенном splitRequests (по-умолчанию) при изменении масштаба посылает на сервер несколько запросов за данными по прямоугольным областям вокруг ранее видимой, а не один запрос как описывалось (или в топике или в документации). При включенной splitRequests запросы будут на каждый тайл, при выключенном — splitRequests на целые блоки тайлов вокруг области, которая уже видна.

                Это важно чтобы понять описание бага: т.е. на изменение масштаба в 1 пункт на сервер отправится 4-5 запросов на области вокруг, данные ведь кешируются и запрос на видимую область карты не посылается. Это отличие от RemoteObjectManager, который пошлет реально один запрос на всю новую область карты (это то, что я и просил выше: добавить в LoadingObjectManager отключения кеша и посылку одного запроса на всю область)

                Баг: что происходит

                Если резко уменьшить масштаб колесом мышки на 2 порядка, то на сервер будут отправлены 2 группы запросов (по 4-5 внутри) для каждой области вокруг ранее показанной. И тут самое интересное: ответы на эти запросы могут приходить назад не в том порядке в каком они были посланы к серверу.

                И если ответы второй группы запросов пришли раньше первой (что вполне может быть — загружена бд и просто нагрузка на сайт была), то метки той первой группы, ответ на которую придет после второй не будут показаны.

                Это очень важный баг, я о нем писал, постарайтесь командой его рассмотреть внимательнее.

                Я даже макет сделал, уж очень хочется закрыть этот баг:
                • 0
                  Большое спасибо за подробное описание, обязательно починим

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка