11
Ноя

Как сделать гаджет для Google Wave

Написал Максим Крентовский в Исследования

wavelogoGoogle Wave — новый инновационный сервис Google, призванный заменить одновременно электронную почту, чат и средство групповых обсуждений. В основе лежат волны (wave) и вейвлеты (wavelet), представляющие собой ветви дискуссии, посвященные какой-либо теме. Сообщения, или всплески (blip), могут создаваться и редактироваться в рамках волн как участниками обсуждения, так и роботами, следящими за дискуссией и дополняющие ее данными (например, автоматическим переводом фраз между языками). Помимо текста во всплески можно добавлять гаджеты — объекты, реализующие дополнительную функциональность, например, систему голосования, интерактивную карту и т.п. Изготовлением гаджетов и займемся в нашей статье.

Для начала рекомендую посетить описание Google Wave API — это полезное чтение, особенно если вы решили, что возможностей гаджетов недостаточно и нужно написать робота. В отличие от последних, гаджеты пишутся на традиционной для клиентской части вэба связке HTML + CSS + JS и представляют собой некий контейнер с данными, помещенный во всплеск через iframe.

Вид гаджета Яндекс.Карт в Google Wave

Для начала уточним, что такое гаджет. Гаджет — это некий код, работающий целиком на клиентской стороне. Гаджеты в сервисах Google можно использовать не только в волне, но мы рассматриваем в данный момент вырожденный случай :) , поэтому не будем заострять внимание на собственно гаджетостроении.

Итак, гаджет представляет собой XML-файл, содержащий конфигурационные параметры и собственно код. Так, в нашем случае код гаджета будет выглядеть следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<Module>
    <ModulePrefs title="YaMaps" height="400">
        <Require feature="setprefs" />
    </ModulePrefs>
<Content type="html"><![CDATA[
<head>
    <link type="text/css" href="http://devimpress.com/p/wave/yamaps/css/s.css" rel="stylesheet" />
   
    <script src="https://wave-api.appspot.com/public/wave.js" type="text/javascript"></script>
    <script src="http://api-maps.yandex.ru/1.1/index.xml?key=ANke9EoBAAAAzj5mEgIAKU9tx9_axPZq-JwSadOBHKTPpwgAAAAAAAAAAABP2mxEp_R1RnRiMrMjUTVotl9fgQ==" type="text/javascript"></script>
   
    <script src="http://www.geoplugin.net/javascript.gp" type="text/javascript"></script>
       
    <script src="http://devimpress.com/p/wave/yamaps/js/jquery.js" type="text/javascript"></script>
    <script src="http://devimpress.com/p/wave/yamaps/js/yamaps.js" type="text/javascript"></script>
</head>
<body onload="$.ym();">
 <div id="map"></div>  
</body>
</html>
]]></Content>
</Module>

Что здесь нужно отметить? Ну, во-первых, что описание содержит две основных секции — ModulePrefs (конфигурационную секцию, которая задает, в частности высоту нашего гаджета) и Content (секцию, содержащую собственно код). Вид последней не сильно далеко ушел от html-файла — так же секция head, где перечислены вспомогательные файлы, и так же body, которая при загрузке документа создает объект $.ym (о нем чуть позднее), и содержит всего-навсего один элемент, куда мы будем прятать нашу карту. Стоит отметить, что аналогичный гаджет с Google Maps гораздо изощреннее, впрочем, сие не есть наша цель.

Пройдемся по подключаемым файлам. Поскольку наш гаджет может быть встроен абсолютно куда угодно, необходимо давать абсолютные ссылки. CSS-файл не представляет ничего сверхординарного и описывает стиль нашего единственного контейнера:

1
2
3
4
5
#map {
    width: 99%;
    height: 400px;
    background: #eee;
}

Разумеется, такое простое описание можно было бы включить в код гаджета, но это не столь принципиально — мы пока пробуем и экспериментируем.

Далее, wave.js — это собственно и есть Wave API. О нем поподробнее позже. Следующим идет API Яндекс.Карт. Тут надо отметить любопытный момент — ключ на пользование API привязан к домену. Но поскольку гаджет будет встроен как iframe, а в качестве исходного домена будет мой же devimpress.com, то для того, чтобы ключ работал без проблем, вполне достаточно сгенерить его для домена размещения — и он будет без проблем встраиваться в волну.

javascript.gp — скрипт, выдернутый из GoogleMaps-гаджета, содержит результаты определения географического положения по IP с представлением их в виде переменных JavaScript. Думаю, достаточно один раз в браузере посмотреть на этот скрипт, чтобы понять, что именно он содержит.

Далее, идет подключение библиотеки jQuery (ее очень удобно использовать для написания приложений на JavaScript и Google, слава богу, это не ограничивает) и собственно yamaps.js, который и содержит код, реализующий функционал гаджета. Но предварительно стоит сделать лирическое отступление по поводу взаимодействия в волне.

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

К слову говоря, в момент чтения кодов у меня зародилась мысль, что есть еще и состояние администратора-владельца, поместившего гаджет в всплеск, но я этот вопрос не уточнял, потому как спешил написать эту заметку. Поэтому чтение документации в данном случае будет самым правильным. :)

Помимо состояний по отношению к пользователю, гаджет имеет возможность получить список всех пользователей на волне, а при помощи делегата — отслеживать их изменение. Получая список пользователей, можно добраться и до аватаров, и до имен пользователей, что, в целом несложно, но в данном примере это никак не используется.

Далее. Когда гаджет находится в режиме редактирования, предполагается, что пользователь делает над ним всякие нехорошие штуки, как, например, перемещает карту, увеличивает/уменьшает, сменяет тип отображения карты на вид со спутника или гибридный. То есть изменяет состояние самого гаджета, которое, согласно идеологии волны, должно быть а) запомнено для истории, б) передано на сторону других пользователей, дабы те наблюдали за редактированием в прямом эфире, в) сохранено с целью восстановления в случае последующих входов на волну этого и других пользователей. И тут в Google сделали просто и изящно — создателю гаджета необходимо прописать две процедуры: первая будет отслеживать изменение состояния гаджета, сериализовать его в JSON и отправлять в волну, вторая — служить делегатом в случае получения состояния, разворачивать данные о нем из JSON и применять их к гаджету. Соответственно, фиксируя на сервере смену состояний, они получают историю, пересылая изменения состояния между гаджетами на пользовательских компьютерах — синхронизацию и интерактив, а по последнему сообщению можно восстановить последнее состояние гаджета для тех, кто только присоединился к волне или отходил почитать другие волны. Просто и очевидно. Правда, по названию функции можно предположить, что все состояние объекта пересылать не нужно, достаточно дельту изменений, но, повторюсь, у нас вопрос познавательский, а структура, описывающая состояние гаджета, незначительная — координаты центра, уровень детализации и тип отображения карты — поэтому будем передавать состояние целиком, без рассчета дельт.

Собственно, код с комментариями, думаю, будет лучшей демонстрацией.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
(function($) { 
    // Конструктор нашего объекта. будет вызван в первую очередь после загрузки гаджета
    $.ym = function() {
        // Создаем карту и центрируем ее с ориентиром на текущего пользователя
        $.ym.i.map = new YMaps.Map($("#map")[0]);
        $.ym.i.map.setCenter(new YMaps.GeoPoint(geoplugin_longitude(), geoplugin_latitude()), $.ym.i.zoom);

        // создаем элементы управления для редактирования
        $.ym.i.controls[0] = new YMaps.Zoom();
        $.ym.i.controls[1] = new YMaps.ScaleLine();
        $.ym.i.controls[2] = new YMaps.TypeControl();
        $.ym.i.controls[3] = new YMaps.ToolBar();
        $.ym.i.controls[4] = new YMaps.SearchControl();

        if (wave && wave.isInWaveContainer()) {
            // мы же на волне?
            $.ym.i.isWave = true;
            $.ym.i.state = "UNKNOWN";
           
            // Установим обработчики событий        
            // ловящих изменение состояния карты
            YMaps.Events.observe($.ym.i.map, $.ym.i.map.Events.MoveEnd, $.ym.sendState);
            YMaps.Events.observe($.ym.i.map, $.ym.i.map.Events.SmoothZoomEnd, $.ym.sendState);
            YMaps.Events.observe($.ym.i.map, $.ym.i.map.Events.TypeChange, $.ym.sendState);
           
            // ... и волны
            wave.setStateCallback($.ym.reciveState, this);
            wave.setParticipantCallback($.ym.reciveParticipant, this);
            wave.setModeCallback($.ym.reciveMode, this);           
        }
    };
   
    // наши данные
   
    $.ym.i = {
        map: null, 
        controls: [],
        zoom: 10,
        isWave: false,
        state: null
    }
   
    // функция обертывания состояния и отправки его в волну
   
    $.ym.sendState = function() {
        if($.ym.i.state == "EDIT") {
            // отправляем только в случае, когда находимся в режиме редактирования
            var state = wave.getState();       
           
            // сериализуем данные о типе карты
            var type = "";
            switch($.ym.i.map.getType()) {
                case (YMaps.MapType.HYBRID): type = "H"; break;
                case (YMaps.MapType.MAP): type = "M"; break;
                case (YMaps.MapType.SATELLITE): type = "S"; break;
            }
           
            // ... о центре карты
            var yaCenter = $.ym.i.map.getCenter();
            var x = JSON.stringify(yaCenter.getX());
            var y = JSON.stringify(yaCenter.getY());
           
            // ... и о текущем масштабе
            var zoom = JSON.stringify($.ym.i.map.getZoom());
           
            // а теперь засылаем все это в волну.
            state.submitDelta({ 'type': type, 'center_x': x, 'center_y': y, 'zoom': zoom });
        }      
    }
   
    // Фунция реакции на смену состояния гаджета, пришедшую извне
   
    $.ym.reciveState = function() {
        // получаем состояние
        var state = wave.getState();
       
        if($.ym.i.state != "EDIT") {
            // применяем состояние только если мы не в режиме редактирования.
            // иначе начнутся гонки
           
            // десериализуем и тут же применяем тип карты
            switch(state.get('type')) {
                case ("H"): type = $.ym.i.map.setType(YMaps.MapType.HYBRID); break;
                case ("M"): type = $.ym.i.map.setType(YMaps.MapType.MAP); break;
                case ("S"): type = $.ym.i.map.setType(YMaps.MapType.SATELLITE); break;
            }
       
            // ...устанавливаем центр
            var p = new YMaps.GeoPoint(JSON.parse(state.get('center_x')),JSON.parse(state.get('center_y')));
            $.ym.i.map.panTo(p);
       
            // ... и масштаб
            var zoom = JSON.parse(state.get('zoom'));
            $.ym.i.map.setZoom(zoom);        
       
            // обновим объект карты, на всякий случай
            $.ym.i.map.update();
        }
    }
   
    $.ym.reciveParticipant = function(participants) {
        // на изменения собеседников нам плевать, поэтому оставим этот делегат пустым
    }      
   
    $.ym.reciveMode = function(mode) {
        // смена режима работы гаджета. По умолчанию мы не знаем, в каком режиме находимся.
        var modeStr = "UNKNOWN";
         
        switch(mode) {
            case wave.Mode.PLAYBACK: modeStr = "PLAYBACK"; break;
            case wave.Mode.EDIT: modeStr = "EDIT"; break;
            case wave.Mode.VIEW: modeStr = "VIEW"; break;
        }
       
        // теперь, когда с режимом определились, неплохо было бы поколдовать на этот счет
        if(modeStr != $.ym.i.state) {
            if( modeStr == "EDIT") {                   
                // если мы переходим в режим редактирования
               
                // включаем элементы управления
                jQuery.each($.ym.i.controls, function(i, val) { $.ym.i.map.addControl(val); });
               
                // разрешаем управлять картой
                $.ym.i.map.enableDblClickZoom();
                $.ym.i.map.enableDragging();
                $.ym.i.map.enableHotKeys();
                $.ym.i.map.enableScrollZoom();

            } else {
                if($.ym.i.state == "EDIT") {
                    // если мы, наоборот, выходим из режима редактирования
                   
                    // отключаем элементы управления
                    jQuery.each($.ym.i.controls, function(i, val) { $.ym.i.map.removeControl(val); });
                    // на всякий случай пошлем состояние еще раз
                    $.ym.sendState();
                }
               
                // запрещаяем как можно больше возможностей управлять картой
                $.ym.i.map.disableDblClickZoom();
                $.ym.i.map.disableDragging();
                $.ym.i.map.disableHotKeys();
                $.ym.i.map.disableScrollZoom();
            }
            // ... и меняем режим внутри нашего объекта
            $.ym.i.state = modeStr;
        }
    }
})(jQuery);

Счастливые обладатели учетной записи Google Wave (мои номинации кончились, если что) могут попробовать проинтегрировать гаджет в свои волны — http://devimpress.com/p/wave/yamaps/yamaps.xml. Всем читателям статьи рекомендую еще раз обратится к руководству по API Google Wave, дабы перепроверить мои сведения, основанные большей частью на анализе кода примеров. Буду много благодарен, если укажите на ошибки и неточности. :)

Комментирование недоступно.
Максим Крентовский
системный архитектор
E-mail / GTalk: mkrentovskiy@gmail.com
Skype: mkrentovskiy