Leaflet как оболочка для «Яндекс.Карт» — отображаем 100 тысяч маркеров на карте

    Я очень люблю Leaflet. С его помощью можно очень быстро строить свои интерактивные карты. Однако, практически все доступные поставщики тайлов (слоёв для карт) предоставляют свои услуги за весьма внушительные деньги. Существуют такие OpenSource-проекты, как OSM, но не всегда их тайлы удовлетворяют своим внешним видом.

    Цель


    Цель заключалась в том, чтобы слепить своего полностью бесплатного кентавра. Мне всегда нравились Yandex-карты, но не их API. Поэтому я заинтересовался вопросом внедрения Яндекс-карты, как слоя для Leaflet.

    Пример готового приложения. В репозитории 48 Мбайт дамп базы.

    Рабочий пример. Может не пережить Хабраэффект.

    Беглое исследование


    Проинспектировав запросы легальной Яндекс-карты, я вычислил сервер тайлов с которым идет общение.

    'http://vec{s}.maps.yandex.net/tiles?l=map&v=4.55.2&z={z}&x={x}&y={y}&scale=2&lang=ru_RU'
    
    {s} - поддомен (subdomain), необходим для того, чтобы не попасть в лимит браузера по запросам к одному и  тому же домену. Эмпирическим путем удалось вычислить, что это 01, 02, 03, 04
    {z} - масштаб слоя (zoom)
    {x - широта (latitude)
    {y} - долгота (longitude)
    

    Это все данные, которые нам необходимы, чтобы использовать тайлы Яндекс-карты внутри Leaflet.

    Реализация


    Для бэкенде я буду использовать Ruby On Rails, чтобы слегка развеять миф о том, что рельсы медленные. Ведь выводить на карту мы будем 100 тысяч маркеров!

    Первым делом создадим модель Marker:
    rails g model marker
    

    Содержимое миграции
    class CreateMarkers < ActiveRecord::Migration
      def change
        create_table :markers do |t|
          t.float :lat
          t.float :lng
          t.string :name
          t.string :avatar
          t.string :website
          t.string :email
          t.string :city
          t.string :address
          t.string :phone
          t.text :about
    
          t.timestamps null: false
        end
      end
    end
    



    rake db:create
    rake db:migrate
    


    Я написал небольшую фабрику, генерирующую 100000 маркеров с заполненными Фейкером полями. Я использую PostgreSQL. Дамп базы можно найти в db/db.dump.

    Фабрика
    # test/factories/markers.rb
    FactoryGirl.define do
      factory :marker do
        lat {Faker::Address.latitude}
        lng {Faker::Address.longitude}
        avatar {Faker::Avatar.image}
        name {Faker::Name.name}
        website {Faker::Internet.url}
        email {Faker::Internet.email}
        city {Faker::Address.city}
        address {Faker::Address.street_address}
        about {Faker::Hipster.paragraph}
        phone {Faker::PhoneNumber.cell_phone}
      end
    end
    
    # db/seeds.rb
    100000.times do |num|
      FactoryGirl.create(:marker)
      ap "#{num}"
    end
    



    Для управления моделью Marker сгенерируем контроллер markers:

    rails g controller markers
    


    Код контроллера
    class MarkersController < ApplicationController
      before_action :set_marker, only: [:show]
    
      def index
        respond_to do |format|
          format.html
          format.json {
            pluck_fields = Marker.pluck(:id, :lat, :lng)
            render json: Oj.dump(pluck_fields)
          }
        end
      end
    
      def show
        render "show", layout: false
      end
    
      private
        def set_marker
          @marker = Marker.find(params[:id])
        end
    end
    
    



    Чтобы не терять время на построении AR-объекта, я вызываю метод pluck, который выполняет SELECT-запрос только к нужным мне полям. Это дает значительный прирост в производительности. Результат представляет из себя массив массивов:

    [
      [1,68.324,-168.542],
      [2,55.522,59.454],
      [3,-19.245,-79.233]
    ]
    


    Так же я использую гем Oj для быстрой генерации json. Потери на view не превышают 2мс для 100000 объектов.

    Не забываем указать новый ресурс в routes.rb:

    Rails.application.routes.draw do
      root to: "markers#index"
      resources :markers, only: [:index, :show]
    end
    


    Приступаем к самой карте.

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

    Подключаем все необходимые библиотеки:

    application.css
    /*
     *= normalize
     *= require leaflet
     *= require prune_cluster
     *= require_tree .
     *= require_self
     */
    


    application.js
    //= require jquery
    //= require leaflet
    //= require prune_cluster
    //= require_self
    //= require_tree .
    



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

    markers/index.html.slim
      #map
    


    application.css
    #map {
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
    }
    



    Теперь мы можем нарисовать leaflet-карту:
    var map = L.map('map').setView([54.762,37.375], 8), // Карта внутри блока #map
          leafletView = new PruneClusterForLeaflet(); // Кластер, в который мы будем складывать маркеры
    


    Так как карта не имеет ни одного слоя, мы увидим только серый фон. Добавить слой на карту очень просто:
    L.tileLayer(
      'http://vec{s}.maps.yandex.net/tiles?l=map&v=4.55.2&z={z}&x={x}&y={y}&scale=2&lang=ru_RU', {
        subdomains: ['01', '02', '03', '04'],
        attribution: '<a http="yandex.ru" target="_blank">Яндекс</a>',
        reuseTiles: true,
        updateWhenIdle: false
      }
    ).addTo(map);
    


    Теперь внутри контейнера #map отображается привычная нам Яндекс-карта. Однако, нам необходимо переопределить проекцию карты со сферического меркатора на эллиптический, иначе будет заметный сдвиг по координатам. Заодно укажем, откуда leaflet должен забирать дефолтные иконки для маркеров.

    map.options.crs = L.CRS.EPSG3395;
    L.Icon.Default.imagePath = "/leaflet";
    


    Осталось запросить все маркеры и отрисовать их на карте:

    jQuery.getJSON("/markers.json", {}, function(res){
      res.forEach(function (item) {
        leafletView.RegisterMarker(new PruneCluster.Marker(item[1], item[2], {id: item[0]}));
      });
      map.addLayer(leafletView);
    })
    


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

    leafletView.PrepareLeafletMarker = function (marker, data) {
      marker.on('click', function () {
        jQuery.ajax({
          url: "/markers/"+data.id
        }).done(function (res) {
          if (marker.getPopup()) {
            marker.setPopupContent(res)
          } else {
            marker.bindPopup(res);
            marker.openPopup();
          }
        })
      })
    }
    


    Создадим соответствующую разметку для Popup:
    markers/show.html.slim
    h1
      | #{@marker.name}
    .popup__address
      | #{@marker.city}, #{@marker.address}
    
    .nowrap
      .popup__avatar
        img src="#{@marker.avatar}" width="120" height="120"
      .popup__contacts
        .popup__contact
          b Телефон:
          div
            | #{@marker.phone}
        .popup__contact
          b Эл. почта:
          div
            a href="mailto:#{@marker.email}"
              | #{@marker.email}
        .popup__contact
          b Вебсайт:
          div
            a href=" #{@marker.website}" target="_blank"
              | #{@marker.website}
    p
      | #{@marker.about}
    



    Итог


    Мы интегрировали Leaflet c Яндекс-картами, а значит нам стали доступны все плагины для leaflet-карт. Написанное приложение не только выдерживает нагрузку в 100000 маркеров, но еще при этом обладает достаточно полезной функциональностью.

    Пример готового приложения. В репозитории 48 Мбайт дамп базы.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 30

      0
      Как сам Яндекс смотрит на такой вариант использования их тайлов?
        +1
        Мне тоже интересен ответ на этот вопрос. Возможно, кто-то из команды Яндекс-карт отпишется здесь. Копирайты соблюдены. API тайлов без ключа.
        +7
        Тайлы Яндекс.Карт можно использовать только в рамках их API. Об этом неоднократно писали разработчики:
        https://yandex.ru/blog/mapsapi/51030
        https://yandex.ru/blog/mapsapi/60531
          +1
          Спасибо! В таком случае статья будет представлять чисто академический интерес.
            +1
            Вообще В Леафлет можно подключить нормальные «полноценные» Яндекс.Карты и ничего не нарушать. Да и различные кластеризаторы у нас самих тоже есть…
              0
              Вы про https://github.com/shramov/leaflet-plugins? Не получилось нормально завести. Про драге карты происходит непонятное смещение и мерцание.
                +1
                Кластеризаторы (во множественном числе)? Это интересно. Где можно о них почитать?
                  +2
                  ObjectManager, RemoteObjectManager, LoadingObjectManager и просто Clusterer.
                  https://tech.yandex.ru/maps/doc/jsapi/2.1/dg/concepts/many-objects-docpage/
                    0
                    К сожалению, ObjectManager работает очень медленно на 4000 точек уже пользоваться не возможно
                      0
                      Мы активно работаем над этим.
            +1
            Помимо уже упомянутых проблем с лицензией, возникает вопрос — а где же онлайн-демо?
            Насколько мне известно, узкое место для 100k маркеров — это вовсе не Рельсы или что-то подобное, а дом в браузере.
              0
              Нет возможности пережить на хаброэффект :) Любой желающий может запустить пример на своей машине.
                +3
                Я любой желающий, но я понятия не имею, как мне на Винде развернуть Руби :)
                  +1
                  Держите. Сервер очень слабенький. Возможно прийдется подождать
                  http://45.55.238.107:8080/
                    0
                    Спасибо. PruneCluster, конечно, сразу узнается.
                    0
                    уж рельсы на винду ставятся сразу в отличие от питона с бесконечными неработающими коннекторами к mysql
                    0
                    Опишите всё Vagrant-ом. Одна команда в консоли и полностью готовый сервер в виртуалке запущен.
                      0
                      пробую, почти все завелось, но как подключить PruneCluster ??

                        0
                        Что-то пропустили. В статье все описано. В описании prunecluster на гитхабе есть вся информация, необходимая для того, чтобы грамотно с ним работать.

                        Я положил leaflet.js и prunecluster.js в vendor/assets/javascripts
                          0

                          да, конечно, все так и сделал, но нет классов
                          div id="map" /div


                          должно быть так
                          div tabindex="0" class="leaflet-container leaflet-fade-anim" id="map".../div


                          когда именно он их навешивает на #map

                            0
                            На гитхабе есть примеры по работе с prunecluster.
                            На сколько помню, блок к карте привязывается в момент создания карты.Кластер привязывается к слою и добавляется на карту.
                      0
                      По поводу лицензии – это у нас ее нет. У других она есть. Как пример https://all.culture.ru
                      +2
                      Огромное спасибо! Не знал про эту либу.
                        +1
                        Вот пример на 1000000 статических маркеров с использованием PruneCluster.
                        Пример на 50000 тысяч статических объектов с использованием marker-clustering
                        В чем смысл использования PruneCluster для отображения статичных данных, если можно использовать рекомендованный leaflet marker-clustering, который гораздо лучше вливается в работу с markers?
                        PruneCluster наиболее актуален для динамических маркеров (пример на 10000 устройств в динамике).
                        В целом, для leaflet с кластером без проблем сожрет и больше устройств чем 100 000 динамических устройств, при адекватной частоте перерисовки.
                          0
                          Спасибо за пример!
                          Я выбрал PruneCluster, потому что думаю, что это будет цикл статей. Дальше будет внедрение полнотекстового поиска и фильтрации по карте.
                          0
                          И EPSG:3857 (Pseudo Mercator) — плоская цилиндрическая равноугольная проекция на сферу, и EPSG:3395 — плоская цилиндрическая равноугольная проекция на эллипсоид WGS-84 одновременно являются «меркатором», проекциями и координатными.
                          А разница в том, что у первого проекция идёт на сферу и формулы пересчёта проще, а у второго на эллипсоид вращения.
                            0
                            много читал ваши статьи на GisLab.info
                            Знаю, что и вы делали какие то разработки связанные с JS.
                            Почему бы вам тоже не поделиться.
                              0
                              так там же, на gis-lab.info, все ссылки и есть. ссылки на git-репозитории, скриншоты и мои обычные призывы присоединятся к совместной разработке :)
                              но они слишком специфичные, «узкие» что ли. поэтому я и публикую их там
                                0
                                Только ради этого буду туда заглядывать!) Последний раз был там пару лет назад. Вас хорошо запомнил почему то, статьи качественные были очень.
                                  0
                                  Хабру очень не хватает глубоких «специфичных» материалов.

                            Only users with full accounts can post comments. Log in, please.