Цель
Цель заключалась в том, чтобы слепить своего полностью бесплатного кентавра. Мне всегда нравились 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.
Подключаем все необходимые библиотеки:
/*
*= normalize
*= require leaflet
*= require prune_cluster
*= require_tree .
*= require_self
*/
//= require jquery
//= require leaflet
//= require prune_cluster
//= require_self
//= require_tree .
Для того, чтобы отрисовать карту, необходимо сделать базовую разметку:
#map
#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:
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)
andead
11.05.2016 18:32+7Тайлы Яндекс.Карт можно использовать только в рамках их API. Об этом неоднократно писали разработчики:
https://yandex.ru/blog/mapsapi/51030
https://yandex.ru/blog/mapsapi/60531storuky
11.05.2016 18:37+1Спасибо! В таком случае статья будет представлять чисто академический интерес.
kashey
11.05.2016 18:43+1Вообще В Леафлет можно подключить нормальные «полноценные» Яндекс.Карты и ничего не нарушать. Да и различные кластеризаторы у нас самих тоже есть…
storuky
11.05.2016 18:47Вы про https://github.com/shramov/leaflet-plugins? Не получилось нормально завести. Про драге карты происходит непонятное смещение и мерцание.
dom1n1k
11.05.2016 18:47+1Кластеризаторы (во множественном числе)? Это интересно. Где можно о них почитать?
kashey
11.05.2016 19:23+2ObjectManager, RemoteObjectManager, LoadingObjectManager и просто Clusterer.
https://tech.yandex.ru/maps/doc/jsapi/2.1/dg/concepts/many-objects-docpage/dumistoklus
12.05.2016 12:39К сожалению, ObjectManager работает очень медленно на 4000 точек уже пользоваться не возможно
dom1n1k
11.05.2016 18:46+1Помимо уже упомянутых проблем с лицензией, возникает вопрос — а где же онлайн-демо?
Насколько мне известно, узкое место для 100k маркеров — это вовсе не Рельсы или что-то подобное, а дом в браузере.storuky
11.05.2016 18:49Нет возможности пережить на хаброэффект :) Любой желающий может запустить пример на своей машине.
TheSteelRat
11.05.2016 23:53Опишите всё Vagrant-ом. Одна команда в консоли и полностью готовый сервер в виртуалке запущен.
dimarikpro
14.05.2016 18:05пробую, почти все завелось, но как подключить PruneCluster ??
storuky
14.05.2016 23:51Что-то пропустили. В статье все описано. В описании prunecluster на гитхабе есть вся информация, необходимая для того, чтобы грамотно с ним работать.
Я положил leaflet.js и prunecluster.js в vendor/assets/javascriptsdimarikpro
15.05.2016 09:00да, конечно, все так и сделал, но нет классов
div id="map" /div
должно быть так
div tabindex="0" class="leaflet-container leaflet-fade-anim" id="map".../div
когда именно он их навешивает на #map
sinires
15.05.2016 10:43На гитхабе есть примеры по работе с prunecluster.
На сколько помню, блок к карте привязывается в момент создания карты.Кластер привязывается к слою и добавляется на карту.
storuky
11.05.2016 18:52По поводу лицензии – это у нас ее нет. У других она есть. Как пример https://all.culture.ru
sinires
12.05.2016 11:07+1Вот пример на 1000000 статических маркеров с использованием PruneCluster.
Пример на 50000 тысяч статических объектов с использованием marker-clustering
В чем смысл использования PruneCluster для отображения статичных данных, если можно использовать рекомендованный leaflet marker-clustering, который гораздо лучше вливается в работу с markers?
PruneCluster наиболее актуален для динамических маркеров (пример на 10000 устройств в динамике).
В целом, для leaflet с кластером без проблем сожрет и больше устройств чем 100 000 динамических устройств, при адекватной частоте перерисовки.storuky
12.05.2016 11:15Спасибо за пример!
Я выбрал PruneCluster, потому что думаю, что это будет цикл статей. Дальше будет внедрение полнотекстового поиска и фильтрации по карте.
filippov70
12.05.2016 12:44И EPSG:3857 (Pseudo Mercator) — плоская цилиндрическая равноугольная проекция на сферу, и EPSG:3395 — плоская цилиндрическая равноугольная проекция на эллипсоид WGS-84 одновременно являются «меркатором», проекциями и координатными.
А разница в том, что у первого проекция идёт на сферу и формулы пересчёта проще, а у второго на эллипсоид вращения.mgis
12.05.2016 18:11много читал ваши статьи на GisLab.info
Знаю, что и вы делали какие то разработки связанные с JS.
Почему бы вам тоже не поделиться.filippov70
13.05.2016 05:43так там же, на gis-lab.info, все ссылки и есть. ссылки на git-репозитории, скриншоты и мои обычные призывы присоединятся к совместной разработке :)
но они слишком специфичные, «узкие» что ли. поэтому я и публикую их тамmgis
13.05.2016 09:17Только ради этого буду туда заглядывать!) Последний раз был там пару лет назад. Вас хорошо запомнил почему то, статьи качественные были очень.
Lisio
Как сам Яндекс смотрит на такой вариант использования их тайлов?
storuky
Мне тоже интересен ответ на этот вопрос. Возможно, кто-то из команды Яндекс-карт отпишется здесь. Копирайты соблюдены. API тайлов без ключа.