Pull to refresh

Ubiquity: just another map plug-in

Reading time10 min
Views1.6K
Ubiquity Logo

На Хабре уже была статья про чудесный плагин для Firefox — Ubiquity. Очень экспериментальный плагин. Советую как минимум посмотреть на него.

Так вот, мне он очень понравился и я написал своё расширение — поиск адреса на картах mapia.ua. О чём и написал статейку.


Превью


Что же примечательного в этом Ubiquity?
  • Во-первых этот плагин суть сборище веб-сервисов в одной командной строке (!): Google [Calculator, Mail, Maps, Translate, ...], Twitter, delicious, Wiki, Digg, tinyurl, Yahoo, Flickr и много чего ещё. Список участников постоянно пополняется. И дело тут даже не в количестве и качестве предоставляемых веб-сервисов. Дело в удобстве использования самого Ubiquity: теперь необязательно обвешивать бедный Firefox сотней чудесных тулбаров и плагинов, всё — в одном месте. И это действительно удобно.
  • Jef RaskinAza RaskinВо-вторых, разрабатывает его не кто-то там, а сам Аза Раскин, сын великого (не побоюсь этого слова) эксперта в юзабилити Джефа Раскина (земля ему пухом), автора великолепной книги «The Human Interface», которую просто обязан прочесть каждый, кто имеет дело с юзер-интерфейсом. Аза Раскин сейчас — ведущий спец по юзабилити в Mozilla Labs. Респект ему и уважуха :-)

Вот ознакомительное видео от автора плагина.

Всё действо происходит в командной строке, прямо в браузере. Достаточно нажать Ctrl+Space. Как говорится, всем любителям POSIX посвящается. Но всё настолько просто и ясно, что и мартышка освоит: команды вводить полностью не надо (suggestions), параметров раз-два и обчёлся. По умолчанию первым аргументом в команду передаётся то, что выделено мышкой на странице. Собственно в этом огромный плюс, скажем, при переводе статей с немецкого (по нажатию Enter, кстати, выделенный кусок текста переведётся прямо на странице). Или чтоб скинуть текст другу на мыло. Вообщем, Аза потрудился на славу.

Велосипед


Чтобы создать свой чудо-сервис, нам понадобится, собственно, Firefox с установленным Ubiquity. Есть? Приступим.

Карты, карты, как мне вас не хватает


Mapia Logo

В Ubiquity уже есть предустановленные карты Google Maps. Но я, как житель славного города Киева, привык пользоваться другими — http://mapia.ua Да и в качестве примера — сойдёт :-)
Да, специально для тех, кто не любит многабукаф — вот ссылка на уже готовый плагин.

Чтобы начать редактировать, нажмите «Редактировать». Спасибо, Кэп!


Итак, нажимаем Ctrl+Space, набираем help, переходим на страничку настроек Ubiquity, находим и нажимаем «Hack Ubiquity». Мы в редакторе. Если не очень удобно кодить в textarea, то можно подключить любой внешний редактор и работать в нём (я предпочитаю notepad++, к примеру).

Первые шаги


CmdUtils.CreateCommand({
    names: ["mapia", "mpi"],
    icon: "http://mapia.ua/favicon.ico",
    description: _("Kiev map powered by mapia.ua"),
    author: {name: "Kottenator", email: "kottenator@gmail.com"},
    license: "MIT"
});


* This source code was highlighted with Source Code Highlighter.

Тут всё очень просто:
  • names, icon, description, author, license — очевидно :-)
  • _(...) — метод для i18n

И мы уже можем запустить и посмотреть нашу команду:
Ctrl+Space -> type «mapia» or «mpi» -> увидим такое:

Screenshot 1

Прямо гордость берёт за содеянное! :) Но продолжим:
CmdUtils.CreateCommand({
    names: ["mapia", "mpi"],
    icon: "http://mapia.ua/favicon.ico",
    description: _("Kiev map powered by mapia.ua"),
    author: {name: "Kottenator", email: "kottenator@gmail.com"},
    license: "MIT",
    
    arguments: [
        {role: 'object', nountype: noun_arb_text, label: _('address')}
    ],
    
    preview: function preview(pblock, args) {
        var msg = "You'd just typed \"${text}\"";
        pblock.innerHTML = _(msg, { text: args.object.text });
    },
    
    execute: function execute(args) {
        var msg = "You choose address: \"${text}\"";
        displayMessage(_(msg, { text: args.object.text }));
    }
});


* This source code was highlighted with Source Code Highlighter.

Что же мы наделали?
  • arguments: [arg1, arg2, ...] — описание аргументов нашей команды (спасибо, Кэп!).
    К примеру, {role: 'object', nountype: noun_arb_text, label: _('address')}:
    role: 'object' означает, что это первый аргумент, ожидаемый сразу же после имени команды; nountype означает что-то вроде типа этой переменной; label — то, что мы увидим в описании команды (н-р «mpi (address)»). Подробнее об аргументах команд можно почитать тут
  • preview: function preview(pblock, args) {
        var msg = "You'd just typed \"${text}\"";
        pblock.innerHTML = _(msg, { text: args.object.text });
    }
    Это метод для отображения превью в окошке Ubiquity. pblock — контейнер превьюхи, args — текущие аргументы комманды; args.object.text — тот самый аргумент с role: 'object'.
    Как видим, метод _(...) умеет делать не только i18n, но и подставляет параметры в шаблон.
  • execute: function execute(args) {
        var msg = "You choose address: \"${text}\"";
        displayMessage(_(msg, { text: args.object.text }));
    }
    А этот метод вызывается, когда мы нажмём Enter.
    displayMessage(...) выводит небольшое slide-up уведомление.

Сохранили, поигрались, красота:

Screenshot 2

Собственно, а где же карты? Начнём с Mapia Static API — это просто картинка участка карты. Продолжим наш код:
arguments: [
    {role: 'object', nountype: noun_arb_text, label: _('address')},
    {role: 'goal', nountype: noun_arb_text, label: _('zoom')},
    {role: 'source', nountype: noun_arb_text, label: _('map size')}
],

previewDelay: 800,

getStaticURL: function(address, zoom, size) {
    var mapUrl = "http://mapia.ua/static?";
    
    address = address || '';
    zoom = parseInt(zoom, 10) || 15;
    zoom = Math.min(18, Math.max(zoom, 6));
    size = size || '400x300';
    
    var params = {
        address: address,
        zoom: zoom,
        size: size
    };
    
    return mapUrl + jQuery.param(params);
},

preview: function preview(pblock, args) {
    var img = '<img src="${url}" />';
    var url = this.getStaticURL(args.object.text, args.goal.text, args.source.text);
    pblock.innerHTML = _(img, { url: url });
},

* This source code was highlighted with Source Code Highlighter.

  • Во-первых, мы добавили ещё 2 аргумента:
    {role: 'goal', nountype: noun_arb_text, label: _('zoom')},
    {role: 'source', nountype: noun_arb_text, label: _('map size')}


    * This source code was highlighted with Source Code Highlighter.

    Первый будет отвечать за zoom карты, второй — за размер рисунка.
    role: 'goal' будет ожидать, что мы напишем что-то вроде «mapia Адресс to 16». Вот это «to 16» будет означать, что args.goal.text == '16' и это будет наш zoom. Возможные значения — от 6 до 18.
    Аналогично «mapia Адресс from 300x200» будет означать, что args.source.text == '300x200' и это будет размер рисунка.
  • Метод getStaticURL(address, zoom, size) возвращает строку адреса нашей карты.
  • Метод preview(...) теперь делает что-то дельное :) — показывает карту
  • previewDelay: 800 — чтобы не убивать сервера mapia.ua сумасшедшей нагрузкой (по запросу на каждое нажатие клавиши), введём задержку перед запросом в 800 миллисекунд

Вот результат:

Screenshot 3

Отлично, на этом можно было бы и закончить, но я лично хотел создать действительно удобную штуку. Статический рисунок — маловато для моих потребностей. И вот что я сделал…

Вторые шаги


Собственно, предисловие. Mapia не предоставляет открытого веб-сервиса для поиска по адресу улицы. Но ведь это работает на их сайте! И я подумал — ведь нет ничего преступного в том, что я воспользуюсь их поиском через XSS? В конце концов это не уязвимость, и вреда я не наношу, ничего не ворую, а лишь создаю дополнительную рекламу этим ребятам ;-)

Добавим такой метод:
loadDetails: function(pblock, args, page_num) {
    var baseUrl = "http://mapia.ua/search";
    var params = {city: "%u041A%u0438%u0435%u0432", "search[query]": args.object.text};
    var self = this;
    
    jQuery.ajax({
        type: "GET",
        url: baseUrl,
        data: params,
        dataType:'html',
        error: function() { },
        success: function(html) {
            $('#content', pblock).html(html);
        }
    });
},

preview: function preview(pblock, args) {
    var img = '<img src="${url}" /><div id="content"></div>';
    var url = this.getStaticURL(args.object.text, args.goal.text, args.source.text);
    pblock.innerHTML = _(img, { url: url });
    
    this.loadDetails(pblock, args);
}


* This source code was highlighted with Source Code Highlighter.

Как видим, Ubiquity поддерживает jQuery. Это чудесно!
И вот тот самый поиск по mapia — «mapia.ua/search?city=Киев&search[query]=Введённый адресс»
Что делает метод loadDetails(...)? Он просто подгружает в наше превью результаты поиска. Думаю, что код ясен и объяснению не подлежит.

Screenshot 4

Перед нами результаты поиска. Но почему-то мы не можем никуда перейти. Ссылки не работают, всё плохо. На самом деле это нормально :) Как я сказал выше, этот механизм предназначен вовсе не для нас, а лишь для самого сайта mapia.ua. Но Ubiquity позволяет делать крос-доменные запросы, что мы и сделали.

Поэтому придётся поработать напильником (RegExp pre-fix + jQuery post-fix):
mapiaURL: "http://mapia.ua/#popup_class=${cat}&popup_id=${sid}",

clearSearchResultsCode: function(html) {
    html = html.replace(/<div id="search_bar_title">((?:.|\s)*?)<\/div>/i, '<h3 class="search_bar_title">$1</h3>')
        .replace(/<p>.*?<\/p>/i, '<p><small>(результаты поиска предоставлены сайтом mapia.ua)</small></p>')
        .replace(/(<div class="buildings_info")[^>]*>/gi, '$1>')
        .replace(/<a href="\/center\/street\/(\d+)[^>]*>/i, '<a href="' + _(this.mapiaURL, {cat: 'Street', sid: "$1"}) + '">')
        .replace(/<a[^<>]+onclick="\$\('street_\d+_buildings'\).*?<\/a>/i, '')
        .replace(/<a href="\/center\/address\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Address', sid: "$1"}) + '">')
        .replace(/<a href="\/center\/feature\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Feature', sid: "$1"}) + '">')
        .replace(/<a href="\/center\/station\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Station', sid: "$1"}) + '">')
        .replace(/<a href="\/center\/city\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'City', sid: "$1"}) + '">')
        .replace(/(<div id="search_bar_title"(?:.|\s)*?)<span(?:.|\s)*?<\/span>/i, '$1')
        .replace(/(<div id="search_bar_title"(?:.|\s)*?)<span(?:.|\s)*?<\/span>/i, '$1') /* not a duplicate */
        .replace(/href="\/search?[^"]*pages_feature=(\d+)[^"]*"/gi, 'page_num="$1"')
        .replace(/<p><a[^<>]*onclick="load_content(?:.|\s)*?<\/p>/i, '')
        .replace(/<script>(?:.|\s)*?<\/script>/i, '');
    return html;
},

loadDetails: function(pblock, args, page_num) {
    var baseUrl = "http://mapia.ua/search";
    var params = {city: "%u041A%u0438%u0435%u0432", "search[query]": args.object.text};
    if (page_num)
        params.pages_feature = page_num;
    var self = this;
    
    jQuery.ajax({
        type: "GET",
        url: baseUrl,
        data: params,
        dataType:'html',
        error: function() {},
        success: function(html) {
            // Pre-fixes via RegExp
            html = self.clearSearchResultsCode(html);
            
            $('#content', pblock).html(html);
            
            // After-fixes via jQuery
            $('div.pagination a', pblock).click(function(e){
                self.loadDetails(pblock, args, $(this).attr('page_num'));
                e.preventDefault();
                return false;
            })
        }
    });
}

* This source code was highlighted with Source Code Highlighter.

Тут долго объяснять, как оно работает :) Скажу лишь, что оно чистит html от лишнего и делает ссылки кликабельными.

Вот как это должно выглядеть:

Screenshot 5

Готово!


Ну вот и всё, пациент здоров и может выписываться. Рад, что кто-то дочитал до сих строк :)

Здесь можно скачать собранный воедино код.
Tags:
Hubs:
Total votes 61: ↑52 and ↓9+43
Comments21

Articles