Я очень люблю Leaflet. С его помощью можно очень быстро строить свои интерактивные карты. Однако, практически все доступные поставщики тайлов (слоёв для карт) предоставляют свои услуги за весьма внушительные деньги. Существуют такие OpenSource-проекты, как OSM, но не всегда их тайлы удовлетворяют своим внешним видом.
Цель заключалась в том, чтобы слепить своего полностью бесплатного кентавра. Мне всегда нравились Yandex-карты, но не их API. Поэтому я заинтересовался вопросом внедрения Яндекс-карты, как слоя для Leaflet.
Пример готового приложения. В репозитории 48 Мбайт дамп базы.
Рабочий пример. Может не пережить Хабраэффект.
Проинспектировав запросы легальной Яндекс-карты, я вычислил сервер тайлов с которым идет общение.
Это все данные, которые нам необходимы, чтобы использовать тайлы Яндекс-карты внутри Leaflet.
Для бэкенде я буду использовать Ruby On Rails, чтобы слегка развеять миф о том, что рельсы медленные. Ведь выводить на карту мы будем 100 тысяч маркеров!
Первым делом создадим модель Marker:
Я написал небольшую фабрику, генерирующую 100000 маркеров с заполненными Фейкером полями. Я использую PostgreSQL. Дамп базы можно найти в db/db.dump.
Для управления моделью Marker сгенерируем контроллер markers:
Чтобы не терять время на построении AR-объекта, я вызываю метод pluck, который выполняет SELECT-запрос только к нужным мне полям. Это дает значительный прирост в производительности. Результат представляет из себя массив массивов:
Так же я использую гем Oj для быстрой генерации json. Потери на view не превышают 2мс для 100000 объектов.
Не забываем указать новый ресурс в routes.rb:
Приступаем к самой карте.
Для такого большого количества маркеров необходим кластеризатор. В Leaflet есть большой выбор различных плагинов, добавляющих нужную нам функциональность. Я остановился на PruneCluster.
Подключаем все необходимые библиотеки:
Для того, чтобы отрисовать карту, необходимо сделать базовую разметку:
Теперь мы можем нарисовать leaflet-карту:
Так как карта не имеет ни одного слоя, мы увидим только серый фон. Добавить слой на карту очень просто:
Теперь внутри контейнера #map отображается привычная нам Яндекс-карта. Однако, нам необходимо переопределить проекцию карты со сферического меркатора на эллиптический, иначе будет заметный сдвиг по координатам. Заодно укажем, откуда leaflet должен забирать дефолтные иконки для маркеров.
Осталось запросить все маркеры и отрисовать их на карте:
Сейчас наша карта не несет никакого смысла, так как нельзя получить никакой информации о маркере. Добавим Popup, который будет вызываться при клике по маркеру и забирать содержимое с сервера:
Создадим соответствующую разметку для Popup:
Мы интегрировали Leaflet c Яндекс-картами, а значит нам стали доступны все плагины для leaflet-карт. Написанное приложение не только выдерживает нагрузку в 100000 маркеров, но еще при этом обладает достаточно полезной функциональностью.
Пример готового приложения. В репозитории 48 Мбайт дамп базы.
Цель
Цель заключалась в том, чтобы слепить своего полностью бесплатного кентавра. Мне всегда нравились 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 Мбайт дамп базы.
