API карт от 2ГИС: рецензия

    Недавно 2ГИС порадовал всех нас выспуском версии 1.0 собственного картографического JS API. API карт отечественного производства — штука редкая; Mail.ru и Rambler, например, так и не сподобились, хоть и обещали (пруф раз, пруф два). Посмотрим, что получилось у новосибирцев.

    Знакомимся



    Не будем откладывать в долгий ящик, открываем раздел "Быстрый старт" и копируем предложенный код. It works! Правда, заголовок показывает кракозябрами — оно, правда, и не удивительно, поскольку тэга с указанием charset-а нет. Ну да ладно, мелочи жизни. Смотрим на код.



                // Создаем объект карты, связанный с контейнером: 
                var myMap = new DG.Map('myMapId'); 
                // Устанавливаем центр карты: 
                myMap.setCenter(new DG.GeoPoint(82.927810142519,55.028936234826)); 
                // Устанавливаем коэффициент масштабирования: 
                myMap.setZoom(15); 
    

    Хм. Что-то мне это смутно напоминает. Непонятно только, почему в setCenter нельзя задать и зум сразу. Если закомментировать строку «myMap.setZoom(15);», то карта покажет 0-й масштаб. Интересно, а тайлы сразу начинают грузиться? Смотрим в сниффер — да, точно. Браузер выкачивает два комплекта тайлов, один для 0-го масштаба, второй для 15. Неаккуратненько как-то. Забегая вперед: задавать зум в setCenter таки можно, но в документации найти об этом информацию, кхм, непросто.

    Ладно. Смотрим дальше.
                // Добавляем элемент управления коэффициентом масштабирования: 
                myMap.controls.add(new DG.Controls.Zoom()); 
                // Создаем балун: 
                var myBalloon = new DG.Balloons.Common({ 
                    // Местоположение на которое указывает балун: 
                    geoPoint: new DG.GeoPoint(82.927810142519,55.028936234826), 
                    // Текст внутри балуна: 
                    contentHtml: 'Привет!<br>Вы кликнули по мне :)' 
                }); 
                // Создаем маркер: 
                var myMarker = new DG.Markers.Common({ 
                    // Местоположение на которое указывает маркер (в нашем случае, такое же, где и балун): 
                    geoPoint: new DG.GeoPoint(82.927810142519,55.028936234826), 
                    // Функция, которая будет вызвана при клике по маркеру: 
                    clickCallback: function() { 
                        // Если балун еще не был добавлен: 
                        if (! myMap.balloons.getDefaultGroup().contains(myBalloon)) { 
                            // Добавить балун на карту: 
                            myMap.balloons.add(myBalloon); 
                        } else { 
                        // Если балун уже был добавлен на карту, но потом был скрыт: 
                            // Показать балун: 
                            myBalloon.show(); 
                        } 
                    } 
                }); 
                // Добавить маркер: 
                myMap.markers.add(myMarker);
            }); 
    


    На первый взгляд вроде ничего. Есть некоторые странности, конечно (contentHtml вместо htmlContent, невозможность задать контекст для callback-ов). Но вот код определения нахождения балуна на карте меня потряс:
    if (! myMap.balloons.getDefaultGroup().contains(myBalloon))

    А попроще как-то нельзя? Метод isOpen балуну сделать, например?

    У ней внутре неонка



    Ладно, оставим quickstart. Посмотрим, что там внутри.



    Предзагрузчик 1.5 Кб — отлично. Код АПИ 200 Кб — нормально. Тайминги — так себе. CSS в 955 байт — wtf?

    Больше всего настораживают заголовки скрипта с API. Кэширования нет — странно. Дата последнего изменения 17 января — значит, правят скрипты без изменения номера версии, что, как минимум, нехорошо. Возможности зафиксировать ревизию АПИ нет — для крупных коммерческих проектов это лишний и очень неудобный напряг. (Кстати, сегодня с утра на бета-тестере 2ГИС-овского АПИ flamp.ru карта не открывалась. Осторожнее надо, всё-таки.)

    АПИ создаёт в глобальной области видимости три переменные — DG (Dolce & Gabbana? Deutsche Grammophon? не лучший выбор для нэймспейса), OpenLayers (сюрприз!) и $.

    Смотрим в код. И правда, нас встречает старый добрый OpenLayers.

    Решение странное. Во-первых, OpenLayers, мягко говоря, подотстал от жизни: в 2012 работать без использования transforms & transitions — уже как-то моветон. Во-вторых, BSD-лицензия накладывает кое-какие ограничения (http://trac.osgeo.org/openlayers/browser/license.txt), которые, вообще-то, надо соблюдать.

    $ появляется в глобальной области от prototype. Как минимум, неаккуратно, даже при том, что при подключении пользовательского jQuery в $ оказывается именно он (что тогда мешает убирать следы присутствия prototype всегда?)

    Кстати, вёрстка едет в quirks mode в IE любой версии — в API желательно всё же QM поддерживать.

    Сами тайлики небольшие, по 20 Кб. Рендер им, видимо, достался от десктопной версии.



    С одной стороны, подложка сделано достаточно современно для веб-картографии — не перегружена деталями и не слишком контрастная, в серых тонах а-ля Гугл.

    С другой стороны, работа со шрифтами и субпиксельное сглаживаание — ужасное. Кроме того, пинги до сервера очень большие.

    Порядок координат — long-lat. Интересно, какая проекция — сферическая как у гугла или эллиптическая, как у Яндекса? Документация ответа не даёт. Пробуем наложить сверху яндексовый гибрид — тайлы расходятся. Значит, всё же, проекция сферическая. В документации про пользовательские слои об этом ни слова — как же тогда пользователь должен тайлики нарезать? Наугад?

    Ладно. Закончим с препарированием, идём дальше.

    Комплект поставки



    Итак, нам дано из функционала:
    • собственно карта;
    • метки;
    • графика;
    • элементы управления (а точнее, элемент управления — zoom);
    • слои;
    • API для AJAX-запросов;
    • поиск.


    Для старта API — в принципе, нормально. Удивляет только отсутствие предустановленных контролов — не успели сделать?

    Читаем документацию. Раздел про Point-GeoPoint пропускаем, всё стандартно. Только вот над оформлением бы поработать, совершенно невозможно читать описания интерфейсов класса, всё сливается.



    Переходим к разделу «Карта» и натыкаемся на «Введение» — «Загрузка библиотеки» — «Версия билиотеки». Как-то неожиданно видеть это в разделе «Карта», не правда ли?

    Затем воспользуемся функцией DG.autoload, в которую помещаем код инициализации карты:

    [Код]

    Именно такой способ рассматривался в разделе «Быстрый старт». Но у функции DG.autoload есть определенные ограничения: внутри себя она использует обработчик события window.onload. Поэтому если на странице уже определен обработчик window.onload, то будут конфликты (чтобы обойти эти конфликты в имеющийся обработчик window.onload можно вставить код инициализации карты).


    WTF? Зачем переопределять window.onload? Написать

        window.attachEvent && window.attachEvent('onload', callback) || window.addEventListener('load', callback, false);
    


    быстрее будет, чем этот абзац документации. Зачем, к тому же, это примечание убрали из quickstart — чтобы больше пользователей наступило на эти грабли? Непродуманно как-то.

    Читаем дальше:
    Чтобы установить центр карты вызываем метод карты setCenter:
    // Устанавливаем центр карты в положение точки 
    point:myMap.setCenter(point);



    Что за «point:»? Неаккуратно.

    Удалить карту

    myMap.destroy()

    Уничтожает все объекты, которые содержит карта. Чтобы уничтожить сам объект myMap после вызова данного метода рекомендуется присвоить null объекту карты:

    // Первый способ. Создаем экземпляр карты, привязывая его к контейнеру по его ID:
    var myMap = new DG.Map("myMapId");
     
    // Второй способ. Получаем DOMElement контейнера:
    var container = document.getElementById("myMapId");
    // Создаем экземпляр карты, привязывая его к DOM объекту контейнеру
    var myMap = new DG.Map("container");
     
    // Создаем экземпляр карты, привязывая его к контейнеру по его ID
    var myMap = new DG.Map("myMapId");



    Что за странный кусок кода? Какое он отношение к destroy имеет? Ребят, ну все же документацию по базовому классу надо вычитывать.

    Добавить обработчик события

    myMap.addEventListener(objectId, eventType, callback)

    Параметры:
    objectId String Идентификатор DOM-элемента, к которому прикрепляем обработчик.


    Разрыв шаблона. Зачем при навешивании обработчика события на карту нужно обязательно указывать DOM-элемент? Если указать id отличный от map.getContainerId() — ничего не работает. Насилие над пользователем какое-то.

    Переместить карту на север

    myMap.moveN(moveStep)

    Параметры:
    moveStep Number Да Шаг, на сколько пикселей сместить карту на север.


    Опять разрыв шаблона. Пиксели на север — это как? Градусы дуги на север — понимаю, пиксели вверх — понимаю, пиксели на север — не понимаю. Ну и смысл существования этого метода от меня ускользает.

    Установить минимальный zoom

    myMap.setMinZoom(minZoom)

    Параметры:
    minZoom Number Да Минимально возможный коэффициент масштабирования(zoom). Минимальное допустимое значение: 1.


    Минимально допустимое значение — 1, контрол зума тоже не дает меньше 1-го масштаба выставить. Но вот если при инициализации карты масштаб не задать, окажешься на 0-м. Интересно, как так. Кстати, на 0-м масштабе getBounds карты отдает область [-268, 434] по долготе — давно ли у нас долготы больше 180 градусов появились?

    Установить ограничения на границы карты
    myMap.setBoundsRestrictions(bounds, isChangePosition)


    isChangePosition? Надо всё же дружить с английским.

    События



    Со странностями addEventListener мы уже познакомились. Дальше — больше: addEventListener есть, removeEventListener-а — нет. Зачем тогда использовать DOM-названия — вводить пользователя в заблуждение?

    Имена событий нужно писать вот так: «DgClick» — g маленькая. Учитывая, что во всех других местах DG всегда пишется большими буквами — раздражающе неконсистентно. В примерах, кстати, опять карта инициализируется без зума.

    Почти у каждого события есть свой тип. Смысл наличия такого большого числа классов от меня ускользает. Например, у DG.Events.Map можно спросить get(Min|Max)Zoom — зачем? Кому нужен этот функционал?

    Маркеры



    Конструктор

    Параметры:
    options.geoPoint DG.GeoPoint Географическое положение точки, на которую указывает маркер.
    options.icon DG.Icon Картинка маркера. Ниже описан класс DG.Icon более подробно. Если icon не указан, то используется картинка по умолчанию.
    options.clickCallback Function Обработчик, который вызываться в момент клика мышки по маркеру. Контекст вызова функции: объект window.


    callback на клик — это, конечно, полезно. Но тогда уж нужно идти дальше и делать callback как минимум на mouseenter/mouseleave.

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


    Группировка маркеров — это, конечно, удобно. Но почему группы нельзя вкладывать в группы? Это же ещё удобнее, и избавит от необходимости иметь группу по умолчанию.

    Замечу, что абсолютно точно такие же группы имеются для балунов, графики и слоёв. Зачем их все описывать отдельно? Введите общий базовый класс Group и GroupManager, документация сократится в два раза.

    Выполнить операцию для каждого маркера в группе
    myGroup.forEach(callback, context)


    Гхм. Ни в одном другом методе контекст исполнения задать было нельзя, а здесь — можно. Для похожести на браузерный Array.forEach? Ну тогда все остальные методы, начиная с addEventListener, нужно приводить к браузерной сигнатуре. Кстати, в JS 1.6 параметр context в forEach необязательный, в отличие от.

    Балуны



    Рассмотрим пример создания балуна:
    // Создаем балун в Новосибирске с текстом приветствия: 
    var myBalloon = new DG.Balloons.Common({ 
        // Местоположение, на которое указывает балун: 
        geoPoint: new DG.GeoPoint(82.927810142519,55.028936234826),
        // Текст внутри балуна: 
        contentHtml: 'Привет!
    
    Хорошего настроения :)'
     }); 
    // Добавим балун на карту:
    myMap.balloons.add(myBalloon);



    Пустая строка в середине кода — это <br>, который забыли заэскейпить (в отличие от квикстарта). Опять же, неаккуратно — пример-то теперь не работает.

    Параметры балуна опять удивляют вольным обращением с английским языком и здравым смыслом (isClosed — показывать ли крестик, contentSize — зафиксирован ли размер).

    Интересно, что событий у балуна нет (или не описаны?).

    Хинтов нет. А не помешали бы.

    Геометрии



    Про геометрии дурного слова не скажу — хорошая, добротная графическая библиотека с широкими возможностями настройки (опции linecap — выбор скругления линий в месте излома — я, кажется, нигде не встречал). Разве что удивляет отсутствие событий. (Впрочем, почему удивляет? Проблема известная — даже прозрачный canvas/svg контейнер закрывает собой карту. Решаемая :)). Добавить геодезические осталось.

    Элементы управления



    Позиционирование контролов — очередной пример насилия над мозгом пользователя.
    myMap.controls.add(new DG.Controls.Zoom(), null, new DG.ControlPosition(DG.ControlPosition.TOP_RIGHT, new DG.Point(20,10)))


    И это все ради того, чтобы прилепить контрол в правый угол с оффсетом 20,10.
    Я понимаю, что у Старших Братьев такая же балалайка — но это же не повод копировать неудачные решения. Почему бы вместо
    new DG.ControlPosition(DG.ControlPosition.TOP_RIGHT, new DG.Point(20,10)))

    не писать
    'topright', [20, 10]
    ?
    Или даже так:
    { top: 20, right: 10 }
    ?

    И унести заодно эту настройку в конструктор контрола, чтобы не писать null вместо имени группы в controls.add?

    Из элементов управления доступен только зум — не успели сделать остальное? Или с верстальщиками какие-то проблемы? Видимо, чтобы компенсировать отсутствие встроенных элементов, открыли интерфейс для создания пользовательских — DG.Controls.Abstract.

    Я, кстати, попробовал добавить на карту new DG.Controls.Abstract() — отработало штатно, исключений не кинуло. Так что это не abstract, а base. Привязка метода наследования классов extend к абстрактному классу не выглядит правильным решением — это же вполне универсальный метод, зачем ограничивать его применение конкретным классом.

    Сам интерфейс выглядит вполне вменяемо — разве что хелперы getStates()/setState() выглядят соврешенно бесполезными.

    Слои



    API карт позволяет накладывать пользовательские слои поверх слоя географической карты. Это дает возможность отображать на карте практически любые объекты, маршруты, области. Например, легко показать зоны покрытия города интернет-провайдерами, или же в графическом виде отобразить области доставки магазина или пиццерии с указанием времени и стоимости доставки. Так же можно указать путь проезда к вашим офисам или маршрут экскурсии по городу. Одним словом, возможности ограничены лишь фантазией.


    … а так же необходимостью угадать проекцию, как мы уже выяснили.

    options.countTileServers Number Количество субдоменов для тайлого сервера.

    У браузеров есть ограничение на количество одновременно скачиваемых файлов с одного домена. Чтобы обойти это ограничение можно загружать тайлы с нескольких поддоменов. Для оптимальной скорости загрузки карты, мы настоятельно рекомендуем устанавливать это значение равным 4.

    Параметр используется совместно с параметром options.tilePrefix. По умолчанию options.countTileServers равен нулю — тайлы загружаются с одного сервера.

    options.tilePrefix String Префикс имени субдомена тайлового сервера.

    Например, у параметр url равен httр://example.com/${z}/${x}/${y}.png. Если вы определяете options.tilePrefix как mytile — получаем URL субдоменов тайлового сервера в виде: httр://mytile1.example.com/, httр://mytile2.example.com/, httр://mytile3.example.com/ и так далее, в зависимости от параметра options.countTileServers. Поэтому параметр используется совместно с параметром options.tilePrefix.


    Эээ. Что за магия? Почему бы просто вот так не сделать: httр://mytile${n}.example.com/${z}/${x}/${y}.png?

    AJAX



    Смысл существования AJAX API не понятен совершенно. Любой ajax-api любого фреймворка удобнее и функциональнее, зачем картографическому АПИ вообще залезать на эту поляну? Кстати, у вас опечатка — failture.

    В целом



    В целом, 2ГИС API оставляет двойственное впечатление.

    С одной стороны — да, полноценное API, разработанное в сжатые сроки, с хорошей собственно функциональной составляющей (90% потребностей покроет), заделы для расширения оставлены.

    С другой:
    • непродуманность и неконсистентность многих интерфейсов;
    • пренебрежение тем, что называется look'n'feel — «вкусностью», сексуальностью дизайна карты и UI;
    • неаккуратная документация, малое число примеров;
    • отсутствие в составе АПИ инструментов для работы с собственно 2ГИС-овскими данными.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 25
    • +1
      Спасибо за обзор.
      • +1
        Пожалуйста. Хотя это и не обзор :)
        • +6
          Это какое-то вскрытие )
          • 0
            Какое совпадение, но сегодня Firebug'om смотрел api.2gis.ru
      • 0
        Теперь им осталось на примере яндекса открыть сервис подобный я.карта и на пару с юзерами усовершенствовать свой api
        • –7
          ахренеть. Похоже не на обзор а на маркетинговый шаг скорее :)
          • +1
            Вы так же близки к теории маркетинга, как к философии сравнения…
          • +2
            > АПИ создаёт в глобальной области видимости три переменные — DG (Dolce & Gabbana? Deutsche Grammophon?

            DoubleGis же :)
            • +3
              Спасибо, кэп.
              • 0
                Да, кстати. С учетом того, что компания сокращает себя как 2gis (TwoGis), выбор DG для неймспейса особенно неудачен.
            • 0
              Я прям удивлён. По сравнению с конкурентами выглядит можно сказать вменяемо.
              Большинство описанных косяков, мне думается, поправят в течение года.
              • 0
                Я бы на месте руководителя разработки апи карт от 2гис выписал вам премию за данный обзор. Или ещё какой-нибудь бонус.
                • 0
                  В 2ГИС если и попадёт эта ссылка к руководителю проекта, то скорее он перенаправит премию со счёта разработчиков на свой в качестве наказания, а не выпишет во вне.
                  • 0
                    Ну как бэ со своего счёта надо списывать, разработчики с чего должны отвечать за общие просчёты в архитектуре проекта.
                    Вот документаторы да, накосячили изрядно.
                  • 0
                    Жаль, что Вы не на его месте :)
                  • –1
                    Эх, и надо же было так OpenLayers исковеркать :(
                    • +5
                      Большое спасибо за анализ. Ваши замечания обязательно будут учтены в ближайших версиях. Похожесть интерфейсов на приведенные аналоги это осознанный выбор, облегчающий пользователям уже знакомыми с подобными API использовать наш.
                    • +1
                      Отличный обзор! Было бы здорово, если бы вы так прошлись по моему leaflet.cloudmade.com/. :)
                    • 0
                      Проекция как у Гугла. Меркатор.
                      Собственно, тайлы прекрасно грузятся через API гугломапа с тривиальным getTileUrl :)
                      Только, тссс ;)
                      • 0
                        Я че-то не видал ещё веб-карт не в меркаторвской проекции. Вопрос в том, какой референсный эллипсоид выбран.
                      • 0
                        гугло-карты едут в квирксе, ожидать поддержку квиркса от 2гис смешно )

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.