最近まわりでYahoo!ウィジェットで何か作るのがブームなので、私もそれに乗って作ってみた。id:n_shuyoやid:BigFatCat、id:ksmemoの各氏によるメモを見ればだいたい分かる感じ。関係ないけどはてなダイアリーってこういう検索結果を日時の昇順で見ることはできないのだろうか。
XMLでUI仕様を書いて、動作をJavaScriptで書いていく。デバッガがついているので、まあそれで試行錯誤していけばいいのかも。JavaScriptは仕様と違う動作が行われるとかはないけど、WindowsオブジェクトがなくてsetTimeoutとかができないので、自動に表示が変わるみたいなのをするには一手間必要。あとリファレンスが大量すぎて読み通す気力が起きなかったりチュートリアルと違うことが書いてあったりするけど、そこはがんばる。
で、せっかくのWidgetで、やっぱWidgetといえば画面常駐がキモで、ブラウザを開いてみればいいようなものはなるべく避けたい。あとインターネットとやりとりできるのを活かしたい。たとえば、書いた文書が(明示的にEnterなどを押さなくても)そのままインターネットに大公開されるようなテキストエディタだかWikiクライアントだかわからないようなものなどはぜひこのYahoo Widgetで作るべきだろう。しかし、今回は諸般の事情でPOSTできるようなものは歓迎されないので、GETに徹したものを作ることにする。
ということで、goo時刻表をスクレイピングして現在の発車時刻表をそれっぽく表示するウィジェット (拡張子を.widgetに変えてウィジェットエンジンにつっこめば動くはず) を作った。画面イメージは以下のような感じ:
駅名を入力して路線と方面を選ぶと、右側に現時点での発車時刻が表示される。表示は1分毎に自動的にリロードされる。できれば運行情報が下のほうでスクロールしていたりパタパタみたいなエフェクトをかけたかったけどそこまでできなかった。あと、左ペインがマウスホイールでスクロールしてほしかったんだけど、なぜかそれもできなかった。将来的にこれのiPhone版も作りたい。
ソース
traindeps.kon
<?xml version="1.0" encoding="UTF-8"?>
<widget minimumVersion="4.0">
<settings>
<setting name="debug" value="off" />
</settings>
<window name="main" width="180" height="30" style="background:#333;">
<title>時刻表表示機</title>
<text name="label" color="#fff" alignment="left" style="font-size:12px">
<hOffset>10</hOffset>
<vOffset>20</vOffset>
<data>駅名</data>
</text>
<textarea name="data" columns="14" lines="1" style="background:#fff;color:#999">
<hOffset>40</hOffset>
<vOffset>7</vOffset>
<data>駅名を入力してください</data>
</textarea>
<frame name="mframe" width="150" height="115" vLineSize="10" vScrollBar="mainv">
<hOffset>7</hOffset>
<vOffset>30</vOffset>
<vScrollBar>mainv</vScrollBar>
</frame>
<scrollbar name="mainv" height="115" width="15" autoHide="true" thumbcolor="#f00">
<hOffset>160</hOffset>
<vOffset>30</vOffset>
</scrollbar>
<text name="disptime" color="#fff">
<hOffset>200</hOffset>
<vOffset>20</vOffset>
<data></data>
</text>
<text name="time" color="#fff" alignment="left" style="font-size:12px">
<hOffset>200</hOffset>
<vOffset>45</vOffset>
<data>発車</data>
</text>
<text name="type" color="#fff" alignment="left" style="font-size:12px">
<hOffset>240</hOffset>
<vOffset>45</vOffset>
<data>種別</data>
</text>
<text name="deps" color="#fff" alignment="left" style="font-size:12px">
<hOffset>300</hOffset>
<vOffset>45</vOffset>
<data>行先</data>
</text>
<frame name="tframe" width="300" height="120">
<hOffset>200</hOffset>
<vOffset>55</vOffset>
<text name="status" color="#fc0">
<hOffset>0</hOffset>
<vOffset>15</vOffset>
<data></data>
</text>
</frame>
<script src="traindeps.js" charset="UTF-8" />
</window>
</widget>
traindeps.js
hogeという名前の関数オブジェクトがあったりしてひどい。
(function() {
items = {};
deps = {};
types = {};
prev = "";
done = false;
var reset = function() {
items = {};
deps = {};
types = {};
prev = "";
done = false;
main.width = 180;
}
var display = function(count) {
if(done) {
main.width = 450;
var now = new Date();
var hour = now.getHours() < 3 ? now.getHours() + 24 : now.getHours();
var minute = now.getMinutes();
var current = (hour < 10 ? "0"+hour : hour) + "" + (minute < 10 ? "0"+minute : minute);
if(current == prev) return;
disptime.data = hour+"時"+minute+"分現在";
if(tframe.hasChildNodes()) {
var children = tframe.subviews;
for(i=0; i<children.length; i++) {
tframe.removeChild(children[i]);
}
}
var i = 0;
var v = 15;
var tomorrow = false;
for(var j in items) {
if(j < current && !tomorrow){
delete items[j];
} else {
tomorrow = true;
var line_time = new Text();
line_time.hOffset = 0;
line_time.vOffset = v;
line_time.color = "#ffcc00";
line_time.data = j.substr(0,2)+":"+j.substr(2,2);
tframe.addSubview(line_time);
var type = "普通";
if(items[j].type in types) type = types[items[j].type];
var line_type = new Text();
line_type.hOffset = 40;
line_type.vOffset = v;
line_type.data = type;
if(type.indexOf('特') > -1 || type.indexOf("ラ") > -1)
line_type.color = "#ff9999";
else if(type.indexOf("準") > -1)
line_type.color = "#ccff99";
else if(type.indexOf("区") > -1)
line_type.color = "#99ff99";
else if(type.indexOf("勤") > -1)
line_type.color = "#99ffff";
else if(type.indexOf("新") > -1)
line_type.color = "#ffcccc";
else if(type.indexOf("快") > -1)
line_type.color = "#cc99ff";
else if(type.indexOf("急") > -1)
line_type.color = "#9999ff";
else line_type.color = "#ffcc00";
tframe.addSubview(line_type);
var line_deps = new Text();
line_deps.hOffset = 100;
line_deps.vOffset = v;
line_deps.data = (deps[items[j].dep] || items[j].dep);
line_deps.color = "#ffcc00";
tframe.addSubview(line_deps);
var line = j + " " + (deps[items[j].dep] || items[j].dep);
if(items[j].type in types)
line += (" " + types[items[j].type]);
//print(line);
i++;
v += 15;
}
if(i == count) break;
}
if(v == 15) {
var status = new Text();
status.color = "#ffcc00";
status.data = "本日の運行は終了しました。";
status.hOffset = 0;
status.vOffset = v;
tframe.addSubview(status);
}
prev = current;
}
}
var setTimeout = function(func, delay) {
var timer = new Timer();
timer.interval = delay / 1000;
timer.onTimerFired = function() {
if((typeof func) == "function")
func();
else eval(func);
}
timer.ticking = true;
return timer;
}
var timer3 = setTimeout("display(5)", 1000);
data.onGainFocus = function() {
this.data = "";
this.style.color = "#000";
};
var clearInterval = function(timer) {
timer.ticking = false;
}
var getTimetable = function(path) {
reset();
var url = "http://transit.goo.ne.jp" + path;
if(!done) {
var req = new XMLHttpRequest();
main.width = 450;
if(tframe.hasChildNodes()) {
var children = tframe.subviews;
for(i=0; i<children.length; i++) {
tframe.removeChild(children[i]);
}
}
status.data = "データ取得中";
status.color = "#ffcc00";
tframe.addSubview(status);
disptime.data = " 時 分現在";
req.onreadystatechange = function() {
if(this.readyState == 4) {
var text = req.responseText;
var hour;
var tags = text.match(/<li><font[\s\S]+?<\/li>|<th>\s*\d+\s*<\/th>|<div class="fs12">[\s\S]+?<\/div>/g);
if(tags) {
for(var i in tags) {
var each = tags[i].replace(/[ \t\n\r]+/g, "");
switch(each[1]) {
case 't' :
hour = each.match(/<th>\s*(\d+)\s*<\/th>/)[1];
hour = hour < 3 ? parseInt(hour) + 24 : hour;
//print(hour);
if(hour.length == 1) hour = "0"+hour;
break;
case 'l' :
var m = each.match(/<li><font(color="(.+?)")?><span>(.+?)<\/span><\/font><br><font.*>(.+?)<\/font><\/li>/);
if(m[4].length == 1) m[4] = "0"+m[4];
items[hour+m[4]] = {dep: m[3].replace(/<.+?>/,''), type: m[2]};
break;
case 'd' :
var pair;
while((pair = (/<fontcolor="(.+?)">(.+?)<\/font>/g).exec(each)) != null) {
types[pair[1]] = pair[2];
}
while((pair = (/(\S+?)=(.+?)行( |[\r\n]|<)/g).exec(each)) != null) {
deps[pair[1]] = pair[2];
}
}
}
done = true;
display(5);
} else {
status.data = "指定した時刻表データがありませんでした。";
}
}
}
req.open("GET", url, true);
req.send();
}
}
var hoge = function(e, path, i) {
e.onClick = function() {
getTimetable(path);
if(mframe.hasChildNodes()) {
var children = mframe.subviews;
for(i=0; i<children.length; i++) {
if(children[i].hOffset == 15)
children[i].color = "#CCCCFF";
}
e.color = "#FFCCCC";
}
}
}
var searchStation = function() {
reset();
main.height = 30;
if(mframe.hasChildNodes()) {
var children = mframe.subviews;
for(i=0; i<children.length; i++) {
mframe.removeChild(children[i]);
}
}
var url = "http://transit.goo.ne.jp/tconfirm.php?an=7&st_name=" + data.data;
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if(this.readyState == 4) {
var text = req.responseText;
var stations = text.match(/<dt><a href="\/timetable\/.+?">.+?<\/a><\/dt>|<span><a href="\/timetable\/.+?">\[ .+方面 \]<\/a><\/span>/g);
var v = 0;
for(i = 0; i < stations.length; i++) {
station = stations[i];
var line = new Text();
line.id = "cand"+i;
if(/^<dt>/.test(station)) {
line.hOffset = 2;
line.color = "#CCCCCC";
} else {
line.hOffset = 15;
line.color = "#CCCCFF";
var anchor = station.match(/^.+?"(.+?)".+$/)[1];
hoge(line, anchor, i);
}
line.vOffset = v = (v + 15);
line.data = stations[i].replace(/<.+?>|[\[\] ]/g,'');
mframe.addSubview(line);
}
main.height = 150;
}
};
req.open("GET", url, true);
req.send();
};
label.onClick = searchStation;
data.onKeyPress = function() {
if(system.event.keyString == "Return" || system.event.keyString == "Enter") {
searchStation();
//getTimetable("");
}
}
})();