Leaflet как оболочка для Яндекс-карт. Отображаем 100000 маркеров на карте
Я очень люблю 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.
Подключаем все необходимые библиотеки:
/*
*= normalize
*= require leaflet
*= require prune_cluster
*= require_tree .
*= require_self
*/
//= require jquery
//= require leaflet
//= require prune_cluster
//= require_self
//= require_tree .
Для того, чтобы отрисовать карту, необходимо сделать базовую разметку:
markers/index.html.slim
#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: 'Яндекс',
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) {
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="${website}" target="_blank"
| #{@marker.website}
p
| #{@marker.about}
Итог
Мы интегрировали Leaflet c Яндекс-картами, а значит нам стали доступны все плагины для leaflet-карт. Написанное приложение не только выдерживает нагрузку в 100000 маркеров, но еще при этом обладает достаточно полезным функционалом.
Пример готового приложения. В репозитории 48 Мбайт дамп базы.