いよいよ今回で最終回です。
最終回の仕上げとして、
そして最後に、
Google Calendarの予定情報の取り込み
Google CalendarのWeb API
Google Calendarは、
参照用のURLですが、
(1) Google Calendarを開く(2) 参照したいカレンダのメニューを開き、「カレンダ設定」 を開く (3) カレンダ設定画面で「このカレンダーを共有」 タブを開く ※指定画像がありません※
(4) 「すべてのユーザと共有」の項目を 「このカレンダーのすべての情報を一般に公開 (検討の対象となる)」に設定 (5) 再びカレンダ設定画面を開き、「カレンダーのアドレス」 の項目のXMLボタンをクリック (6) 表示されたURLをコピーコピーしたURL例 http://
www. google. com/ calendar/ feeds/ gordon. timothy. nathanson%40gmail. com/ public/ basic (7) 末尾を変換末尾の /basic を /full?alt=json に置き換えます。
置き換えたあとのURL例 http://
www. google. com/ calendar/ feeds/ gordon. timothy. nathanson%40gmail. com/ public/ full?alt=json
これで各予定の開始・
なお、
また、
実装方針検討
JSON形式で予定情報を取得できる準備は整いました。あとは以下の手順で処理をすればカレンダアプリケーションに取り込むことができるでしょう。
(1) Google Calendar data APIにアクセスし、JSON形式で予定情報を取得する (2) 取得したJSON文字列をevalし、予定情報を持つオブジェクトにする (3) 予定情報オブジェクトから予定情報を取得し、スケジューラオブジェクトが管理する予定情報として取り込む (4) スケジューラオブジェクトが外部から取り込んだ予定情報は編集/削除はできないようにする
(1)
ここで、
- method
HTTPのメソッドを文字列で指定します。必須項目。GET、
POST、 PUT、 DELETEなど、 HTTPのメソッドは何でも使えます。 - url
アクセス先URLを文字列で指定します。必須項目。
- headers
HTTPリクエストヘッダを、
名前をフィールドとし、 値をそのフィールドの文字列値として持つオブジェクトとして指定します。オプション項目。 例:ブラウザ上でフォームをPOSTで送信する動作をシミュレートする場合に必要なリクエストヘッダの指定 headers:{"Content-type":"application/
x-www-form-urlencoded"} - data
HTTPリクエスト本体を文字列で指定します。オプション項目。
例:ブラウザ上でフォームをPOSTで送信する動作をシミュレートする場合 data:"q=" + encodeURIComponent("Greasemonkey アプリケーション gotin")
- onload
リクエストが正常に終了した場合に呼ばれる関数を指定します。オプション項目。
- onerror
リクエストの処理中にエラーが発生した場合に呼ばれる関数を指定します。オプション項目。
- onreadystatechange
リクエストの処理中状態が変化したタイミングで呼ばれる関数を指定します。オプション項目。
onload、
- status
HTTPレスポンスのHTTPステータスコードを表す整数値。200なら正常終了、
404ならNot Found、 など。 - statusText
サーバが返すステータステキストを表す文字列。ステータステキストはサーバにより返す文字列は変わる。
- responseHeaders
HTTPレスポンスに含まれるHTTPヘッダーを表す文字列。リクエスト用ヘッダのフィールドheadersはオブジェクト形式だが、
レスポンス用ヘッダはオブジェクトではなく文字列で返される。 - responseText
HTTPレスポンス本体を表す文字列。
- readyState
HTTPリクエストの段階を示す整数値。
- 1:リクエスト準備中
- 2:リクエスト準備完了
- 3:リクエスト送信完了、
レスポンス待機中 - 4:レスポンス完了
利用例: GM_
xmlhttpRequest({ method:"GET", url:"http:// www. google. com/ calendar/ feeds/ gordon. timothy. nathanson%40gmail. com/ public/ full?alt=json", onload:function(x){alert(x. responseText)} });
これで予定情報のJSON文字列をalertで出力することができます。図1はその主要部分の一部を示したものです。
{
"version":"1.0",
"encoding":"UTF-8",
"feed":{"xmlns":"http://www.w3.org/2005/Atom",
//中略
"entry":[{
"title":{"type":"text","$t":"第3回記事公開"},
"content":{"type":"text"},
"gd$when": [{
"startTime":"2007-08-20",
"endTime":"2007-08-21"}],
"gd$where":[{}]},
:
:
}
(2)
GM_xmlhttpRequest({
method:"GET",
url:"http://www.google.com/calendar/feeds/gordon.timothy.nathanson%40gmail.com/public/full?alt=json",
onload:function(x){
alert(eval("(" + x.responseText + ")").feed.entry[0].title.$t);
}
});
これで最初の予定情報のタイトルをalertで出力することができます。図1の場合であれば"第3回記事公開"が表示されるはずです。
この要領で予定情報を取得し、
実装すべきことは確認できましたので、
しかし、
(3) 予定情報オブジェクトから予定情報を取得し、スケジューラオブジェクトが管理する予定情報として取り込む (4) スケジューラオブジェクトが外部から取り込んだ予定情報は編集/削除はできないようにする
の処理を実装するには既存の関数も修正する必要があります。
そこで、
既存コードの修正点
既存コードの変更箇所は以下の通りです
(1) 変更点
- カレンダオブジェクト:
- makeAddClassNameToDateCell
(Date型の引数によって指定された日付に該当する日付セルにクラス名を追加する関数の生成関数) 生成される関数の引数を追加し、
クラス名の追加時に該当する日付を選択する処理をするか否かを指定できるようにしました。外部から予定情報が追加されるタイミングはいつになるか分からない (Google Calendarサーバがレスポンスを返し、 解析/ 追加処理がなされたタイミングになるため、 カレンダの操作中に追加される可能性がある) ので、 その都度日付選択処理がなされてしまうとユーザがカレンダを操作している際に勝手に選択操作がなされてしまい混乱の元になってしまいます。そこで、 外部からの予定情報追加の処理の際にその動作を抑制できるようにするため、 この修正を加えました。 - スケジューラオブジェクト:
setButtonActions
(編集ボタンと追加ボタンの処理を設定する関数) 予定情報を表すオブジェクトに新たにeditable属性を追加し、
その真偽値によって、 その予定情報が編集/ 削除可能か否かを示すようにしました。外部から追加された予定情報であればその値をfalseにすることにより編集/ 削除ができないようにする狙いです。そのため、 ここでは追加ボタンによる予定情報の追加時にeditable属性の値をtrueに設定するようにしました。 - makeEditButton / makeDeleteButton
(編集ボタンの生成処理、削除ボタンの生成処理関数) editable属性の値がfalseのときはボタンを生成しないようにしました。
- addEvent
(予定情報の追加処理関数) 予定情報を保持する変数を外部から追加されたもののために別に用意することにしました。そこで、
追加先をeditable属性の値で振り分けるようにし、 外部からのものとそれ以外のもので別管理するようにしました。 - getEvents
(指定された日付に予定情報を返す関数) 外部から追加された予定情報とカレンダアプリケーション内で生成した予定情報とをマージして返すようにしました。
- deleteEvent
(予定情報の削除関数) 削除対象の予定の日時に登録されている別の予定情報が、
外部から追加された予定情報にも存在しない場合に、 当該日付に登録されている予定情報がなくなった旨を通知 (deleteEventObserverのnotify関数呼び出し) するようにしました。 - setStyle
(スタイルシートの設定処理関数) 外部から追加された予定情報と、
カレンダアプリケーション内で生成した予定情報とでカレンダの日付セルの見栄えを返るため、 外部から追加された予定情報に対するクラスを追加しました。
(2) 追加した処理
- スケジューラオブジェクト:
importEvents 予定情報を表すオブジェクトの配列を引数で受け、
外部から追加された予定情報として追加します。各要素のeventオブジェクトのeditable属性の値をfalseにした上で、 外部予定情報管理用変数にセットするようにしました。 - addAddOtherEventListener
外部から予定情報が追加されたときに呼び出されるリスナ関数を登録する関数です。外部から追加された予定情報の場合は見栄えを変えて表示するため、
カレンダアプリケーション内で生成された予定情報の追加時のものとは別に用意しました。 - hasOtherEvents
指定された日付に対する外部から追加された予定情報の有無を返す関数です。こちらも上記と同様の理由で用意しました。
以上で、
Google Calendarの予定情報を追加する処理の実装
図2にユーザスクリプトのGoogle Calendarの予定情報を解析する処理部分を示します。
function googleCalendarEvents(url, callback){
if(typeof callback != "function") return;
GM_xmlhttpRequest({
url:url,
method:"GET",
onload:function(xhr){
callback(parse(xhr.responseText));
}
});
/**
* Google Calendarのカレンダ情報json文字列をオブジェクト化し、
* スケジューラオブジェクトで管理する予定情報の形式のオブジェクトを要素とする配列として取り出す
*/
function parse(json){
var feed = eval("("+json+")").feed;
var entries = feed.entry;
var results = [];
entries.forEach(function(entry){
results.push(convertData(entry));
});
return results;
function convertData(entry){
var id = entry.id.$t;
var title = entry.title.$t || "no title";
var content = entry.content.$t || "";
var where = entry.gd$where[0].valueString || "";
var startDate = parseDate(entry.gd$when[0].startTime);
var endDate = parseDate(entry.gd$when[0].endTime);
var timeSpan = makeTimeSpan(entry);
var description = title + (where ? "@" + where : "") + "<br />" + (timeSpan ? timeSpan + "<br />" : "") + content;
return ({id:id,
year:startDate.getFullYear() +"",
month:(startDate.getMonth() + 1) +"",
date:startDate.getDate() +"",
description:description});
// 予定の開始日時、終了日時を予定情報の内容として設定するため、
// その文字列表現を生成する
function makeTimeSpan(entry){
var startTimeS = entry.gd$when[0].startTime || "";
var endTimeS = entry.gd$when[0].endTime || "";
var startDate = parseDate(startTimeS);
var endDate = parseDate(endTimeS);
var startTime = "";
var endTime = "";
if(startTimeS.match(/T/)){
startTime = getTime(startDate);
}
if(endTimeS.match(/T/)){
endTime = getTime(endDate);
}
var timeSpan = "";
if(startTime){
timeSpan = startTime;
}
if(!startTime && !endTime &&
endDate.getTime() - startDate.getTime() == 1000* 60 * 60 * 24){
// nothing to do
} else if(startDate.getTime() != endDate.getTime()){
var formattedEndDate = "";
if(startDate.getDate() != endDate.getDate() ||
startDate.getMonth() != endDate.getMonth() ||
startDate.getYear() != endDate.getYear()){
formattedEndDate = dateFormat(endDate) + " ";
}
timeSpan += "~" + formattedEndDate + endTime;
}
return timeSpan;
}
function parseDate(s){
if(!s) return null;
s = s.replace(/\-/g, "/").replace(/T/, " ").replace(/\.000/," GMT");
return new Date(s);
}
function getTime(date){
function format(n){ return n < 10 ? "0" + n : n;}
return format(date.getHours()) + ":" + format(date.getMinutes());
}
function dateFormat(date){
return date.getFullYear()
+ "/" + (date.getMonth() + 1)
+ "/" + date.getDate();
}
}
}
}
googleCalendarEvents関数はGoogle Calendarの予定情報を解析する関数で、
callback関数として、
関数内部ではJSON文字列をオブジェクト化し、
Google Calendarでは予定情報として、
そこで、
その処理をするのがconvertData関数で、タイトル@場所<br />
開始時刻~終了日時<br />
内容
var gPanel = null;
function toggleCalendar(){
setPanelIfNeed();
with(gPanel.style){
display = (display != "block") ? "block" : "none";
}
function setPanelIfNeed(){
if(gPanel) return;
setGoogleCalendar();
connectCalendarAndScheduler();
gPanel =
$add($table({id:"_gpanel",
cellSpacing:0,
cellPadding:1}),
$add($tr(),
$add($td({id:css("frame")}), calendar.makeTable())),
$add($tr(),
$add($td({id:css("sche_frame")}), scheduler.makeController())));
$add(document.body, gPanel);
setStyle();
calendar.goToday();
function setGoogleCalendar(){
var GOOGLE_CALENDAR_FEEDS = "http://www.google.com/calendar/feeds/";
var PUBLIC_FULL = "/public/full?alt=json";
var accounts = [// gotinの公開予定情報
"gordon.timothy.nathanson%40gmail.com",
// amazonの、プログラミング関連の本の発売日情報
"ujsbklv8riuihs10lq43ig2jptfjs5v7%40import.calendar.google.com",
// amazonの、猫関連の本の発売日情報
"8s7v5t9jnhd418rvlnrlmskg346dsjlm%40import.calendar.google.com"];
accounts.forEach(function(account){
var url = GOOGLE_CALENDAR_FEEDS + account + PUBLIC_FULL;
googleCalendarEvents(url, scheduler.importEvents);
});
}
function connectCalendarAndScheduler(){
var HAS_EVENTS_CLASS_NAME = css("has_events");
var HAS_EX_EVENTS_CLASS_NAME = css("has_ex_events");
calendar.addSetClassNameListener(function(date){
return scheduler.hasEvents(date) ? HAS_EVENTS_CLASS_NAME // カレンダ内予定情報があれば黄色
:scheduler.hasOtherEvents(date) ? HAS_EX_EVENTS_CLASS_NAME // 外部予定情報があれば緑色
: "" ;
});
calendar.addSelectDateListener(scheduler.selectDate);
scheduler.addAddEventListener(calendar.makeAddClassNameToDateCell(HAS_EVENTS_CLASS_NAME));
scheduler.addDeleteEventListener(calendar.makeDeleteClassNameFromDateCell(HAS_EVENTS_CLASS_NAME));
// 外部予定情報が追加されたら背景色を緑色に設定するクラスを追加する
scheduler.addAddOtherEventListener(calendar.makeAddClassNameToDateCell(HAS_EX_EVENTS_CLASS_NAME));
}
}
function setStyle(){
var style =
<><![CDATA[
// 中略
#_gcal_ td._gcal_has_ex_events{
background-color:#99FF99;
}
// 中略
]]></>;
GM_addStyle(style);
}
}
カレンダの表示/
(1) カレンダオブジェクトとスケジューラオブジェクトの接続処理カレンダオブジェクトとスケジューラオブジェクトの接続処理では、
外部からの予定情報はカレンダアプリケーションで生成した予定情報と区別して表示できるようにするために新たに追加した外部インターフェース用関数を使った接続処理を行っています。 日付セルの色づけにおいて、
カレンダアプリケーションで生成した予定情報があるときは黄色にし、 カレンダアプリケーションで生成した予定情報がなく、 外部から追加した予定情報があるときは緑色になるようにしています。 (2) googleCalendarEvents関数を使ったGoogle Calendarの取り込み処理setGoogleCalendar関数でGoogle Calendarの取り込み処理を行っています。
処理は単純で、
三つのGoogle Calendar data APIのURLを定義し、 一つずつgoogleCalendarEvents関数を呼び出し、 スケジューラオブジェクトのimportEventsで追加処理させるようにしています。
なお、
Google Calendarの予定情報の取り込みに関するまとめ
今回のポイントはGM_
手前味噌ですが、
各々のユーザスクリプトの詳細についてはリンク先のページをご覧ください。LingrもTwitterもいずれもJSON形式をサポートしているWeb APIが公開されており、
さて、
カレンダアプリケーションの拡張アイデア
Google Calendarの取り込みまでできるようになったカレンダアプリケーションですが、
(1) 日付入力のvalidation ★現状は何も入力値のチェック処理をしていないので、
ありえないほど大きな数値や数字以外の文字列などの入力されると正常に動作する保証がありません。入力値をチェックし、 利用不可能な値が入力されている場合は更新処理を進めず、 警告を出したり入力不可能になるようにするとユーザビリティが高まると思います。 (2) クリックでも日付選択 ★現状はカーソルキーで日付を移動
(選択) できるようにしています。マウスクリックで選択できるようにすると、 予定情報の追加、 修正時に便利になると思います。 (3) 予定情報の追加、修正、 削除の処理にもキーバインドを割り当てる ★ 現状は追加/
修正/ 削除の操作をするためにマウスクリックが必要になっています。これらの操作にもキーバインドを割り当てるとより便利になりそうです。 (4) 全予定表示 ★現状は予定の内容は日付を選択しないと表示されません。全予定内容表示ボタンをつけて、
それを押したら全部表示できるようにしてみましょう。 (5) RSS Reader ★★実は本記事の執筆内容の検討当初はGoogle Calendarではなく、
ブログなどのRSSの情報をカレンダに表示できるようにしようと考えていました。しかし、 毎日エントリのあるRSSだとカレンダが全部埋め尽くされてしまい予定情報ありなしがよくわからなくなってしまうことを懸念し、 今回の方向性に決めました。表示方法を工夫すればよいかもしれません。 表示中のページがRSSを配信している場合はそれを取り込む操作をするためのボタンをカレンダ画面に追加する、
などの実装も可能です。 なお、
かつてLivedoor Readerをまねた (まねしようとした) GreasemonkeyによるRSSリーダーを作ってみたこともありました。 (6) 設定機能 ★★現状、
googleカレンダのURLがスクリプトに直接埋め込まれた形になっています。アプリ画面からURLを設定できるほうが便利です。そのための設定画面を作成してみましょう。変数accountsの値としてURLの配列がソースに埋め込んであるわけですが、 GM_ setValue/ getValueでその値を設定/ 取得できるようにすればよいわけです。 なお、
こうした設定機能をつけると、 設定した直後にすぐに反映されるようにしたいところです。そうすると追加だけでなく削除も反映したくなるので、 いったん全イベントを削除して、 リロードする処理が必要になります。 (7) インポートボタン ★★(5) のRSSリーダーのアイデアと同様ですが、 Google Calendarの 「カレンダの設定」 画面内にそのカレンダをインポートするボタンがあると便利になりそうです。XMLボタンにURLがありますから、 それを少し加工すればインポート用のURLになります。 (8) Google Calendarの更新もできるようにする。 ★★★Google Calendar data APIは、
認証処理も可能になっており、 認証を通せば更新もできるようになっています。予定情報をすべてGoogle Caelndar上に配置してしまえば、 異なるコンピュータ上のFirefoxでも予定情報を同期して扱えるようになります [1]。 (9) 画面いっぱいにカレンダ表示する ★★★現状、
常にカレンダを小さく表示することにこだわったので、 予定情報をカレンダ内で一望する、 ということができなくなっています。そこでGoogle Calendarと同じように、 カレンダを画面いっぱいに表示し、 日付セルの中に複数の予定情報が並ぶよう表示し、 さらにドラッグ&ドロップによる予定情報の移動などにも対応するようにするとよいでしょう [1]。 (10) 表示中のページ内に記載されている予定情報を取り込む ★★表示中のページ内に予定情報が記載されている場合はそれを自動的に認識してカレンダに取り込めるようになっていると非常に便利です。自然言語処理を実装する必要があるわけですが、
microformatsのhCalendar形式で記載されている予定情報であれば比較的簡単に取り込み処理を実装することはできると思います。
最後に
Greasemonkeyは様々なアイデアを手軽に実現するのによい環境だと思いますので、
なお、