Я очень люблю 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 Мбайт дамп базы.
Поделиться с друзьями
-->

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


  1. Lisio
    11.05.2016 17:56

    Как сам Яндекс смотрит на такой вариант использования их тайлов?


    1. storuky
      11.05.2016 18:00
      +1

      Мне тоже интересен ответ на этот вопрос. Возможно, кто-то из команды Яндекс-карт отпишется здесь. Копирайты соблюдены. API тайлов без ключа.


  1. andead
    11.05.2016 18:32
    +7

    Тайлы Яндекс.Карт можно использовать только в рамках их API. Об этом неоднократно писали разработчики:
    https://yandex.ru/blog/mapsapi/51030
    https://yandex.ru/blog/mapsapi/60531


    1. storuky
      11.05.2016 18:37
      +1

      Спасибо! В таком случае статья будет представлять чисто академический интерес.


      1. kashey
        11.05.2016 18:43
        +1

        Вообще В Леафлет можно подключить нормальные «полноценные» Яндекс.Карты и ничего не нарушать. Да и различные кластеризаторы у нас самих тоже есть…


        1. storuky
          11.05.2016 18:47

          Вы про https://github.com/shramov/leaflet-plugins? Не получилось нормально завести. Про драге карты происходит непонятное смещение и мерцание.


        1. dom1n1k
          11.05.2016 18:47
          +1

          Кластеризаторы (во множественном числе)? Это интересно. Где можно о них почитать?


          1. kashey
            11.05.2016 19:23
            +2

            ObjectManager, RemoteObjectManager, LoadingObjectManager и просто Clusterer.
            https://tech.yandex.ru/maps/doc/jsapi/2.1/dg/concepts/many-objects-docpage/


            1. dumistoklus
              12.05.2016 12:39

              К сожалению, ObjectManager работает очень медленно на 4000 точек уже пользоваться не возможно


              1. kashey
                12.05.2016 13:09

                Мы активно работаем над этим.


  1. dom1n1k
    11.05.2016 18:46
    +1

    Помимо уже упомянутых проблем с лицензией, возникает вопрос — а где же онлайн-демо?
    Насколько мне известно, узкое место для 100k маркеров — это вовсе не Рельсы или что-то подобное, а дом в браузере.


    1. storuky
      11.05.2016 18:49

      Нет возможности пережить на хаброэффект :) Любой желающий может запустить пример на своей машине.


      1. dom1n1k
        11.05.2016 18:58
        +3

        Я любой желающий, но я понятия не имею, как мне на Винде развернуть Руби :)


        1. storuky
          11.05.2016 20:08
          +1

          Держите. Сервер очень слабенький. Возможно прийдется подождать
          http://45.55.238.107:8080/


          1. dom1n1k
            11.05.2016 20:36

            Спасибо. PruneCluster, конечно, сразу узнается.


        1. Alexufo
          11.05.2016 20:08

          уж рельсы на винду ставятся сразу в отличие от питона с бесконечными неработающими коннекторами к mysql


      1. TheSteelRat
        11.05.2016 23:53

        Опишите всё Vagrant-ом. Одна команда в консоли и полностью готовый сервер в виртуалке запущен.


      1. dimarikpro
        14.05.2016 18:05

        пробую, почти все завелось, но как подключить PruneCluster ??


        1. storuky
          14.05.2016 23:51

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

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


          1. dimarikpro
            15.05.2016 09:00

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


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


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


            1. sinires
              15.05.2016 10:43

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


    1. storuky
      11.05.2016 18:52

      По поводу лицензии – это у нас ее нет. У других она есть. Как пример https://all.culture.ru


  1. Isopropil
    12.05.2016 11:02
    +2

    Огромное спасибо! Не знал про эту либу.


  1. sinires
    12.05.2016 11:07
    +1

    Вот пример на 1000000 статических маркеров с использованием PruneCluster.
    Пример на 50000 тысяч статических объектов с использованием marker-clustering
    В чем смысл использования PruneCluster для отображения статичных данных, если можно использовать рекомендованный leaflet marker-clustering, который гораздо лучше вливается в работу с markers?
    PruneCluster наиболее актуален для динамических маркеров (пример на 10000 устройств в динамике).
    В целом, для leaflet с кластером без проблем сожрет и больше устройств чем 100 000 динамических устройств, при адекватной частоте перерисовки.


    1. storuky
      12.05.2016 11:15

      Спасибо за пример!
      Я выбрал PruneCluster, потому что думаю, что это будет цикл статей. Дальше будет внедрение полнотекстового поиска и фильтрации по карте.


  1. filippov70
    12.05.2016 12:44

    И EPSG:3857 (Pseudo Mercator) — плоская цилиндрическая равноугольная проекция на сферу, и EPSG:3395 — плоская цилиндрическая равноугольная проекция на эллипсоид WGS-84 одновременно являются «меркатором», проекциями и координатными.
    А разница в том, что у первого проекция идёт на сферу и формулы пересчёта проще, а у второго на эллипсоид вращения.


    1. mgis
      12.05.2016 18:11

      много читал ваши статьи на GisLab.info
      Знаю, что и вы делали какие то разработки связанные с JS.
      Почему бы вам тоже не поделиться.


      1. filippov70
        13.05.2016 05:43

        так там же, на gis-lab.info, все ссылки и есть. ссылки на git-репозитории, скриншоты и мои обычные призывы присоединятся к совместной разработке :)
        но они слишком специфичные, «узкие» что ли. поэтому я и публикую их там


        1. mgis
          13.05.2016 09:17

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


        1. dom1n1k
          14.05.2016 16:34

          Хабру очень не хватает глубоких «специфичных» материалов.