Leaflet — API карт от Cloudmade. Рецензия

Interfaces
To Mourner — бойся своих желаний, они могут исполниться. Шутка.

Начнём с начала



На главной Leaflet API нас встречает quickstart-пример. С него и начнём.

// create a CloudMade tile layer
var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/YOUR-API-KEY/997/256/{z}/{x}/{y}.png',
    cloudmadeAttribution = 'Map data © 2011 OpenStreetMap contributors, Imagery © 2011 CloudMade',
    cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18, attribution: cloudmadeAttribution});

// initialize the map on the "map" div
var map = new L.Map('map');

// set the map view to a given center and zoom and add the CloudMade layer
map.setView(new L.LatLng(51.505, -0.09), 13).addLayer(cloudmade);

// create a marker in the given location and add it to the map
var marker = new L.Marker(new L.LatLng(51.5, -0.09));
map.addLayer(marker);

// attach a given HTML content to the marker and immediately open it
marker.bindPopup("A pretty CSS3 popup.<br />Easily customizable.").openPopup();


Пример начинается с создания слоя с тайлами от cloudmade. Само API вроде как тоже «by cloudmade». Внимание, вопрос: а что, для родительского/дружественного проекта нельзя сделать удобный способ добавления слоя тайлов? Типа такого:

var cloudmade = new L.CloudMade.TileLayer(YOUR-API-KEY);
?
Или даже такого:
map.addLayer('cloudmade', { apiKey: YOUR-API-KEY });


Не знаю, какие отношения связывают Leaflet и Cloudmade, но уж сделать удобно клиенту Cloudmade — точно не последняя задача Leaflet API. Заставлять пользователя самостоятельно добавлять копирайт Cloudmade — это какое-то насилие над здравым смыслом.

Чайнинг





В отличие от Google Maps API v2/OpenLayers, Leaflet представляет jQuery-подобную парадигму (большинство действий чайнятся). Ну так что же стесняться-то?

var marker,
      map = (new L.Map('map'))
        .setView(new L.LatLng(51.505, -0.09), 13)
        .addLayer('cloudmade', { apiKey: YOUR-API-KEY })
        .addLayer((marker = new L.Marker(new L.LatLng(51.5, -0.09)))
            .bindPopup("A pretty CSS3 popup.<br />Easily customizable.")
        );

marker.openPopup();


Кстати, из приведённого примера хорошо заметно неудобство смешивания объектного подхода и чайнинга — (new X()).y() не самая красивая конструкция в JS.

Кстати, почему маркеры добавляются через addLayer? L.Layer в Leaflet — вполне понятная отдельная сущность — слой. Почему через addLayer добавляются и другие сущности — маркеры, геометрии?

И потом, если разницы между addLayer и addMarker нет (т.е. вся логика добавления зашита в самом объекте), разве не логично сдублировать этот метод в сам объект?

var map = (new L.Map('map'))
        .setView(new L.LatLng(51.505, -0.09), 13)
        .addLayer('cloudmade', { apiKey: YOUR-API-KEY }),
      marker = (new L.Marker(new L.LatLng(51.5, -0.09)))
        .appendTo(map)
        .bindPopup("A pretty CSS3 popup.<br />Easily customizable.")
        .openPopup();


Теперь, если заменить констукторы фабриками, получится совсем красиво:

var map = L.map('map')
             .setView(L.latLng(51.505, -0.09), 13))
             .addLayer('cloudmade', { apiKey: YOUR-API-KEY }),

      marker = L.marker(L.latLng(51.5, -0.09))
            .appendTo(map)
            .bindPopup("A pretty CSS3 popup.<br />Easily customizable.")
            .openPopup();


Сравните с исходным примером.

Кстати, забегая вперёд, setView и addLayers можно делать прямо в конструкторе — почему бы не вынести эту возможность прямо в quickstart?

Поковырямся немытыми руками



ОК, убедили, я хочу попользоваться вашим АПИ. Смотрю в код и… недоумеваю. Как его подключить-то? Ладно, я понимаю — зачем перегружать пример всякими html-тэгами и т.п., но как подключать АПИ надо же показать. Поставили галочку, идём в пример.

Before writing any code for the map, you need to do the following preparation steps on your page:
Include Leaflet CSS files in the head section of your document:
<link rel="stylesheet" href="leaflet/leaflet.css" />
<!--[if lte IE 8]><link rel="stylesheet" href="leaflet/leaflet.ie.css" /><![endif]-->

Include Leaflet JavaScript file somewhere on the page (preferably before body close tag):
<script src="leaflet/leaflet.js"></script>

Put a div element with a certain id where you want your map to be and make sure it has defined width and height:
<div id="map" style="height: 200px"></div> <!-- width equals available horizontal space by default -->

Now you're ready to initialize the map and do some stuff with it.


До этого момента (я уже прочел доку, прежде чем лезть в примеры), Leaflet оставлял о себе исключительно приятное впечатление. Но вот этот небольшой кусок примера вызывает больше вопросов, чем вся документация вместе взятая.

Во-первых, откуда ж на моей странице возьмётся папка leaflet с кодом leaflet-а? Кажется, что вот как раз получение кода библиотеки и нужно описывать в разделе «Preparing your page». Если вы думаете, что вебмастера легко заметят, что ссылки никуда не ведут, залезут на гитхаб и скачают код — вы очень глубоко ошибаетесь. Я уверен, что ваш саппорт завален вопросами «я скопировал код — ниче не работает — что делать???»

Далее, зачем заставлять вебмастера самого размещать код подключения css? Почему не оставить эту работу js-скрипту? (Кстати, graceful degradation под IE — моё почтение.)

А вот этот коммент «width equals available horizontal space by default» — на кого он рассчитан? На тех, кто не имеет никакого представления об HTML? Ну так этот коммент их только запутает. Им бы вот как раз пояснить, (а) откуда на их странице возьмётся leaflet/leaflet.js и как с гитхаба код качать, (б) что это за магические комментарии в подключении CSS и почему их нельзя просто так удалять.

Make sure this code is below both the map div and leaflet.js inclusion, or in a window.load or document.ready event handler.


Вы поясняете вебмастерам, что дивы тянутся по умолчанию на 100%, но при этом не считаете нужным сообщить, что это за зверь window.load и как в его обработчик добавить какой-то код?

Поймите, веб-мастера от вас хотят ровно одного: скопировать кусок кода, и чтобы он работал. Работал везде — хоть в head его ставь, хоть в body, хоть до window.onload, хоть после. И как бы я их вполне понимаю — если мне вдруг надо использовать стороннюю библиотеку, то последнее, чем я хочу заниматься — это ковырять референсный пример, выясняя, почему он не работает в моём окружении.

Ладно, я отвлёкся. Смотрим в firebug. Ребята, да вы читеры! JS API в 25 Кб — это колдовство какое-то. Тут — твёрдая и безоговорчная пятёрка. Кстати, у вас функция _leaflet_resize3 светится в глобальный неймспейс (v0.3), где-то var забыли.

Удивляет другое — почему не хостите эту библиотеку сами и заставляете подключать с домена пользователя? Не ахти ж какая нагрузка, да и договориться с партнером каким-нибудь можно. Зато у ваших пользователей не будет проблем с обновлением версий (и с критическими багами в старых версиях, которые рано или поздно появятся) + с распространением библиотеки она очень скоро окажется у большинства пользователей в кэше.

Десерт



Ладно, заканчиваем с примерами, переходим ко вкусному — к документации. И сразу вопрос — почему некоторые ссылки серые и никуда не ведут? Документация не готова? Сломалось что-то? Roadmap? Кстати, а к какой версии эта документация — к стабильной 0.2 или dev 0.3?

Начинаем читать.

// initialize the map on the "map" div with a given center and zoom 
var map = new L.Map('map', {
    center: new L.LatLng(51.505, -0.09), 
    zoom: 13
});

// create a CloudMade tile layer
var cloudmadeUrl = 'http://{s}.tile.cloudmade.com/YOUR-API-KEY/997/256/{z}/{x}/{y}.png',
    cloudmade = new L.TileLayer(cloudmadeUrl, {maxZoom: 18});

// add the CloudMade layer to the map
map.addLayer(cloudmade);


Оказывается, центр и зум карте можно задать в конструкторе. А можно не задавать. А что будет, если только центр задать? Или только зум? А главное, кому нужна неинициализированная карта? Какая у неё есть функциональность?

Опять создание cloudmade-овского слоя вынесено в самое начало — да прикрутите уже нормальный способ это сделать! Кстати, теперь копирайты уже можно не указывать?

Начинаем смотреть в параметры карты.
layers ILayer[] [] Layers that will be added to the map initially.

Оказывается, слои можно задать прямо в конструкторе. Так а что не задаёте-то в своих примерах?
minZoom Number 0 Minimum zoom level of the map. Overrides any minZoom set on map layers.
maxZoom Number 18 Maximum zoom level of the map. This overrides any maxZoom set on map layers.

Гм. Если опции карты всегда перекрывают опции слоя и у них есть дефолтное значение — зачем тогда нужны опции слоя? Непонятно…
dragging Boolean true Whether the map be draggable with mouse/touch or not.
touchZoom Boolean true Whether the map can be zoomed by touch-dragging with two fingers.
scrollWheelZoom Boolean true Whether the map can be zoomed by using the mouse wheel.
doubleClickZoom Boolean true Whether the map can be zoomed in by double clicking on it.

Три опции из четырех — инфинитивы, одна — герундий. Тогда уж или drag, или (touch|scrollWheel|doubleClick)Zooming.

Более интересно другое — почему ILayer-ы удостоены отдельной опции-массива layers, а IHandler-ы и IControl-ы — нет. Разве так не логичнее?

handlers IHandler[] ['drag', 'touchZoom', 'scrollWheel', 'doucleClick'] Map handlers that will be enabled initially


Ну и для контролов аналогично.

То же соображение касается и свойств карты. Почему бы не писать просто:
map.hadlers('drag').enable()


Такое решение (а) разгрузит интерфейс класса от множества лишних опций и свойств, (б) сделает добавление новых контролов и хэндлеров более формальным и удобным.

Допустим, я хочу добавить линейку на свою карту и хочу оформить её правильно в виде IHandler. Мне придётся отнаследоваться от L.Map и сделать примерно следующее:

var MyMap = function (id, options) {
    L.Map.call(this, id, options);
    this.ruler = new MyRulerHandlerClass(map);
    if (options && options.ruler) {
        this.ruler.enable();
    }
}


Хотя мне всего-то достаточно сделать что-то типа
L.handlers.register('ruler', MyRulerHandlerClass)

если завести статическое хранилище IHandler-ов карты по алиасам.

Перейдём к событиям.

click MouseEvent Fired when the user clicks (or taps) the map.
dblclick MouseEvent Fired when the user double-clicks (or double-taps) the map.
mousedown MouseEvent Fired when the user pushes the mouse button on the map.

А mouseup, contextmenu, mouseenter, mouseleave? Уж как-то совсем странно предоставлять событие mousedown и не давать слушать mouseup.

load Event Fired when the map is initialized (when its center and zoom are set for the first time).
viewreset Event Fired when the map needs to redraw its content (this usually happens on map zoom or load). Very useful for creating custom overlays.


Уберите неинициализированное состояние карты, и в этих двух событиях отпадёт нужда. Кстати, событие load, говорящее о том, что загрузились все тайлы, было бы куда как полезнее.

movestart Event Fired when the view of the map starts changing (e.g. user starts dragging the map).
move Event Fired on any movement of the map view.
moveend Event Fired when the view of the map ends changed (e.g. user stopped dragging the map).
dragstart Event Fired when the user starts dragging the map.
drag Event Fired repeatedly while the user drags the map.
dragend Event Fired when the user stops dragging the map.
zoomend Event Fired when the map zoom changes.


Чем вызвана необходимость иметь два набора move-событый — move* и drag*? Чтобы отличать пользовательское действие от программного? Так почему в zoom так же не сделано? Почему нет zoomstart, zoom, scrollzoomstart, scrollzoom, scrollzoomend? Непонятно.

layeradd LayerEvent Fired when a new layer is added to the map.
layerremove LayerEvent Fired when some layer is removed from the map.


Я так понимаю, эти же события бросаются при добавлении маркеров? Как я об этом должен узнать? Понятие layer ещё не введено, слово даже шрифтом не выделено как программная сущность, ссылки нет.

locationfound LocationEvent Fired when geolocation (using locate or locateAndSetView method) went successfully.
locationerror ErrorEvent Fired when geolocation (using locate or locateAndSetView method) failed.


А где мне увидеть эти магические методы, о которых идёт речь? После событий в описании какие-то projections идут, ссылки нет. Кстати, что делают projections в описании интерфейса map — мне совсем непонятно.

Map panes

An object literal that contains different map panes that you can use to put your custom overlays in. The difference is mostly in zIndex order that such overlays get.


Круто. Что это? Как получить к этому доступ? Это не поле, не метод и не событие — что это за зверь-то такой? Примера нет.

Переходим к методам. Разбиение методов на Methods that modify map state / Methods that get map state / Methods for layers and controls / Conversion methods / Other methods («все животные делятся на а) принадлежащих Императору, б) набальзамированных, в) прирученных, г) молочных поросят, д) сирен, е) сказочных, ж) бродячих собак, з) включённых в эту классификацию, и) бегающих как сумасшедшие, к) неисчисляемых, л) нарисованных тончайшей кистью из верблюжьей шерсти, м) и прочих, н) только что разбивших кувшин, о) похожих издали на мух...») как бы намекает на основную проблему с методами: их слишком много. И, главное, дальше будет ещё больше. Если выносить «sometimes useful» методы в интерфейс основного класса — очень скоро они станут totally unuseful из-на невозможности что-то найти. Надо что-то делать :) Например, если метод является прокси к какому-то внутреннему объекту — то не стоит ли открыть этот объект вместо проксирования его методов? Ну и многие методы просто избыточны — например, зачем нужны отдельные методы locate и locateAndSetView, если можно просто обойтись флагом setView в опциях метода locate? Зачем нужны методы zoomIn/zoomOut, когда есть setZoom?

В методах (add|remove)Layer опять нет ни единого намёка на то, что такое layer. Любопытно, что есть методы (add|remove)Control, но нет (add|remove)Handler. А что произойдёт, если я добавлю новый контрол — поле .controlName в карте появится? Судя по всему — нет.

Итого, у вас аж 4 неконсистетных способа добавлять/удалять/обращаться к сущностям карты:

1) для слоёв: массив layers в опциях, (add|remove)Layer в методах;
2) для IHandler: набор именованных опций, набор именованных свойств, как добавлять новые — неясно вообще;
3) для IControl: и набор именованных опций/свойств, и интерфейс (add|remove)Control для неименованных контролов;
4) для MapPanes — метод карты, который возвращает литерал со списком pane-ов.

Многовато будет, и документация хромает.

Маркеры



Интерфейс маркера бедноват в сравнении с буйством карты, но умудряется наследовать её родовую травму в виде .dragging. Невозможность перезадать иконку не радует, но ещё больше не радует отсутствие нативной возможности связать с маркером какие-то данные. Маркер соответствует какой-то географической сущности, и, скорее всего, у него есть какой-то идентификатор, имя, описание, адрес и пр. Вы заставляете пользователя либо наследоваться и расширять класс, либо хитрить с замыканиями, либо плевать на всё и просто писать marker.id = 'myid'. Все варианты не очень красивы и потенциально опасны.

clickable Boolean true If false, the marker will not emit mouse events and will act as a part of the underlying map.


Неправильное решение — назвать интерактивность объекта «clickable».

А что мне делать, если я по клику хочу решить — пропускать это событие или нет? Доступа к дом-ноде нет.

Как мне поступить, если я хочу, чтобы по клику открывался балун, а по драгу таскалась карта? Вполне обычный юзкейс, см., например, остановки общественного транспорта на гугломапсах.

draggable Boolean false Whether the marker is draggable with mouse/touch or not.


Я так понимаю, поле .dragging у маркера будет в любом случае, независимо от опции draggable? И что будет, если я принудительно вызову .dragging.enable()? Почему у карты эта опция называется dragging? Ведь это же точно тот же IHandler для таскания, что и у карты — почему у него другие опции?

Куда пропало событие mousedown?

Кстати, про то, что маркер — это layer я так и не узнал. Только примеры по-прежнему намекают мне на эту странную идентичность. В документации ещё используется термин «overlay», который тоже нигде не определен плюс ещё и в именах сущностей не встречается.

Popup



Первый вопрос, который возникает — если есть сущность L.Popup, то могу ли я передать инстанцию класса L.Popup в bindPopup? По документации выходит, что нет. А почему тогда метод называется bindPopup, если он привязывает никакой не popup, а только данные и опции для него?

autoPan Boolean true Set it to false if you don't want the map to do panning animation to fit the opened popup.'
closeButton Boolean true Controls the presense of a close button in the popup.

Разнородные названия опций. Если autoPan = автоматически передвинь, то closeButton = закрой кнопку, а вовсе не «наличие кнопки закрытия».

Кстати, в чём смысл задания html-контента, если к DOM-у доступа нет? Раз уж я хочу задавать rich html content балуну, то я точно хочу и интерактива добавить — а у меня нет ни доступа к html, ни события «попап открылся».

Настроек интерактивности попапа нет — а зря, здесть они нужнее, чем в маркере. Например, скролл всегда безусловно проходит на карту — значит, разместить в попапе проскроленный контент невозможно.

Слои



Про печальку с cloudmade-овскими слоями я уже упоминал. Почему-то для других сервисов (WMS) отдельный класс для удобства заведён, а для родного — нет.

Что такое minZoom и maxZoom и как их оверрайдят настройки карты — не разъяснено. Также непонятно, зачем введена настройка «размер тайла». Типичные юзкейсы — наложить слой в другой проекции (wgs84, например), растянуть тайлы последнего масштаба и добавить +1 или +2 масштаба карте — эта настройка не решает.

Про полезность события «все тайлы загрузились» я уже упоминал.

Что такое TileLayer.Canvas я просто не понял. Документация описывает у него ровно один метод, и пример никакого представления о том, зачем он нужен, не даёт.

Графика



Графические элементы наследуют ту же печать с clickable, что и маркеры. Кстати, а почему draggable к ним не прикручен? Число бросаемых объектом событий по мере углубления в документацию всё сокращается :)

Метод setStyle задающий options — это странно.

Геометрии задаются массивами точек — как-то непоследовательно. Саму точку нужно обязательно задавать через new LatLng, а в полилинию можно засовывать геометрию обычным массивом, а не спец. объектом. А что будет, если пользователь сделает этому массиву splice в обход API-шного метода? Метода update у графики нет.

Опция noClip с комментарием «Disabled polyline clipping.» поставила меня в тупик. Особенно отсутствием такой же опции у остальных геометрий.

Что я знаю о кругах?



Круг, нарисованный через linecap на линии — это прикольная идея. Проблема только в том, что «честный» круг радиусом в n метров в меркаторовской проекции — вовсем не круг и даже не эллипс, а сложная фигура. Ладно, положим «честный» круг никому не нужен. Но всё же: какой радиус выбирается для расчетов? На север, на юг, на запад, на восток?

Кстати, позицию центра круга изменить можно, а радиус — нельзя. Почему?

L.CircleMarker
A circle of a fixed size with radius specified in pixels. Extends Circe (здесь опечатка — forgotten). Use Map#addLayer to add it to the map.


В упор не вижу отличий от просто Circle. По названию «CircleMarker» я бы подумал, что это круг + маркер, но описание никаких наводок не даёт.

The default radius is 10 and can be altered by passing a «radius» member in the path options object.


Зачем это сделано? Кому мешал радиус в сигнатуре? Или это такой способ сделать доступным изменение радиуса? Зачем тогда нужен просто Circle? По-хорошему, надо не Circle -> CircleMarker расширять, а Options -> CircleOptions, раз в опциях появляется дополнительное поле.

Группы



Группа LayerGroup, в которую можно класть не только слои, но и маркеры/графику — это очень странно. Ссылка ILayer всё ещё никуда не ведёт. Метод clearLayers, который удаляет всех детей группы (а не очищает тайловые слои, как лично я бы подумал из названия) — тоже очень странный.

Но группа FeatureGroup, которая к LayerGroup добавляет пропагацию событий и попап — это уже прямо совсем странно. Что должно кому сказать название Feature? Почему этот функционал нельзя прибить к базовой группе и не делать разделения Layer/Feature?

GeoJSON



Во-первых, само решение «накопительного» GeoJSON-а выглядит как-то странно. Или два раза addGeoJSON нельзя вызвать? Документация ответа не даёт.

var geojson = new L.GeoJSON();
geojson.on('featureparse', function(e) {
    // do something with e.layer depending on e.properties
});
geojson.addGeoJSON(geojsonObj);
map.addLayer(geojson);


Самое-то интересное и скрыто за «do something». А что сделать-то можно? Группа не даёт аксессоров до дочерних объектов. Если я добавил FeatureCollection через geojson — что я сделать-то с ней могу? Или FeatureCollection нельзя добавлять, затем и накопительный addGeoJSON?

И, кстати, почему JSON везде большими, а в geojson — маленькими? И ещё у вас опечатка: в coordsToLatlng и coordsToLatlngs должны быть «Lng» с большой.

В итоге



Что мы имеем в итоге?

Недостатки функционала, положим, можно легко списать на малый размер библиотеки (25Кб — рекорд). (Только вот что тут экономить, если библиотека весит меньше, чем один стандартный тайл?)

Неконсистентные интерфейсы добавления разных сущностей на карту — полагаю, болезни роста. Я думаю, всё-таки к релизу их нужно свести в единый интерфейс.

Графически и эстетически библиотека (и клаудмэйдовская подложка) оставляет очень приятное впечатление, и за это ей многое простится. Ну а неконсистентности и нелогичности автор, надеюсь, доработает напильником, благо версия пока всего лишь 0.3, можно себе позволить отрывать обратную совместимость.
Tags:leafletcloudmadeAPI карт
Hubs: Interfaces
+60
33.3k 86
Comments 34

Popular right now

QA engineer
to 100,000 ₽CrypteriumRemote job
Senior Backend Developer (PHP/Go)
from 300,000 to 320,000 ₽SmartTel PlusВаршаваRemote job
Backend developer
from 150,000 ₽VMS SoftwareСанкт-ПетербургRemote job
Team Lead PHP разработчик (Laravel/Phalcon/Symfony)
from 3,500 to 4,000 €Digital LineМосква