Skip to content

Instantly share code, notes, and snippets.

@cou929
Created April 29, 2010 15:50
Show Gist options
  • Save cou929/383806 to your computer and use it in GitHub Desktop.
Save cou929/383806 to your computer and use it in GitHub Desktop.
// use closure to avoid global nemespace pollution
var opentimetable = (function() {
// load libraries as pseudo global constants
const LIB = {};
Components.utils.import("resource://opentimetable/Preferences.js", LIB);
Components.utils.import("resource://opentimetable/StringBundle.js", LIB);
Components.utils.import("resource://opentimetable/Console.js", LIB);
const prefs = new LIB.Preferences("extensions.opentimetable.");
const strings = new LIB.StringBundle("chrome://opentimetable/locale/opentimetable.properties");
var loglevel = prefs.get("loglevel");
loglevel = loglevel ? loglevel : "off";
const console = new LIB.Console({level: loglevel, prefix: "次電+ (tsugiden plus):\n"});
const debug = LIB.debug;
const UTILS = {};
Components.utils.import("resource://opentimetable/utils.jsm", UTILS);
const bindFunction = UTILS.bindFunction;
const httpGet = UTILS.httpGet;
const unique = UTILS.unique;
const intersect = UTILS.intersect;
const replaceTemplate = UTILS.replaceTemplate;
const pad0 = UTILS.pad0;
const EXTENSION = Application.extensions.get("opentimetable@extdev.org");
// pseudo global constants
const FIRST_HOUR = 4;
const LAST_HOUR = 25;
const RE_ROUTE = /(?:線|[((][^))]+[))])$/;
const RE_PLACE = /(?:駅|バス停|[((][^))]+[))])$/;
const RE_BOUND = /(?:行き|方面|[((][^))]+[))])$/;
const DAYS_OF_TYPE = { // [Sun,Mon,...,Sat, Holiday]
"毎日": [true,true,true,true,true,true,true, false],
"平日": [false,true,true,true,true,true,false, false],
"土日": [true,false,false,false,false,false,true, false],
"休日": [true,false,false,false,false,false,true, true],
"日曜": [true,false,false,false,false,false,false, false],
"月曜": [false,true,false,false,false,false,false, false],
"火曜": [false,false,true,false,false,false,false, false],
"水曜": [false,false,false,true,false,false,false, false],
"木曜": [false,false,false,false,true,false,false, false],
"金曜": [false,false,false,false,false,true,false, false],
"土曜": [false,false,false,false,false,false,true, false],
"祝日": [false,false,false,false,false,false,false, true]
};
const TIMETABLE_LIST_API_URL = "http://opentimetable.jp/dev/api/getTimetables.php";
// http://opentimetable.jp/dev/timetable.php?r=有楽町線&p=麹町&b=新木場&t=平日
const TIMETABLE_URL_TEMPLATE = "http://opentimetable.jp/dev/timetable.php?r={route}&p={place}&b={bound}&t={type}";
// http://opentimetable.jp/dev/api/getTimetable.php?r=有楽町線&p=麹町&b=新木場&t=平日
// http://opentimetable.jp/dev/api/getTimetableWithLabels.php?id=2
// http://opentimetable.jp/dev/api/getTimetableWithLabels.php?r=有楽町線&p=麹町&b=新木場&t=平日
const TIMETABLE_API_URL_TEMPLATE = "http://opentimetable.jp/dev/api/getTimetableWithLabels.php?r={route}&p={place}&b={bound}&t={type}";
const HOLIDAY_CALENDER_API_URL_TEMPLATE = "http://www.google.com/calendar/feeds/japanese@holiday.calendar.google.com/public/full?alt=json&orderby=starttime&start-min={STARTDAY}&max-results={NUM}&sortorder=ascending&fields=entry(gd:when(@startTime))";
// main object which will be returned and set to real global object
var global = {
/* -------------------- Main Part -------------------- */
// element getters
get panel() {
delete this.panel;
return this.panel = document.getElementById("opentimetable-panel");
},
get statusbarpanel() {
delete this.statusbarpanel;
return this.statusbarpanel = document.getElementById("opentimetable-statusbarpanel");
},
get statusbarpanelLabel() {
delete this.statusbarpanelLabel;
return this.statusbarpanelLabel = document.getElementById("opentimetable-statusbarpanel-label");
},
get typePanel() {
delete this.typePanel;
return this.typePanel = document.getElementById("opentimetable-type-panel");
},
get typeBox() {
delete this.typeBox;
return this.typeBox = document.getElementById("opentimetable-type-box");
},
get messageBox() {
delete this.messageBox;
return this.messageBox = document.getElementById("opentimetable-message-box");
},
get routeMessageBox() {
delete this.routeMessageBox;
return this.routeMessageBox = document.getElementById("opentimetable-route-message-box");
},
get placeMessageBox() {
delete this.placeMessageBox;
return this.placeMessageBox = document.getElementById("opentimetable-place-message-box");
},
get boundMessageBox() {
delete this.boundMessageBox;
return this.boundMessageBox = document.getElementById("opentimetable-bound-message-box");
},
get routeTextbox() {
delete this.routeTextbox;
return this.routeTextbox = document.getElementById("opentimetable-route-textbox");
},
get placeTextbox() {
delete this.placeTextbox;
return this.placeTextbox = document.getElementById("opentimetable-place-textbox");
},
get boundTextbox() {
delete this.boundTextbox;
return this.boundTextbox = document.getElementById("opentimetable-bound-textbox");
},
get margintimeMenulist() {
delete this.margintimeMenulist;
return this.margintimeMenulist = document.getElementById("opentimetable-margintime-menulist");
},
get routeSuggestPanel() {
delete this.routeSuggestPanel;
return this.routeSuggestPanel = document.getElementById("opentimetable-route-suggest-panel");
},
get placeSuggestPanel() {
delete this.placeSuggestPanel;
return this.placeSuggestPanel = document.getElementById("opentimetable-place-suggest-panel");
},
get boundSuggestPanel() {
delete this.boundSuggestPanel;
return this.boundSuggestPanel = document.getElementById("opentimetable-bound-suggest-panel");
},
get routeSuggestBox() {
delete this.routeSuggestBox;
return this.routeSuggestBox = document.getElementById("opentimetable-route-suggest-box");
},
get placeSuggestBox() {
delete this.placeSuggestBox;
return this.placeSuggestBox = document.getElementById("opentimetable-place-suggest-box");
},
get boundSuggestBox() {
delete this.boundSuggestBox;
return this.boundSuggestBox = document.getElementById("opentimetable-bound-suggest-box");
},
// interface getter
get formHistory() {
delete this.formHistory;
return this.formHistory = Components.classes["@mozilla.org/satchel/form-history;1"].getService(Components.interfaces.nsIFormHistory2 || Components.interfaces.nsIFormHistory);
},
// initialize and finalize
init: function(){
if (this._inited_) { return; }
this.loadPref();
httpGet(TIMETABLE_LIST_API_URL, function(data, status, req) {
this.TimetableList.init(JSON.parse(data), bindFunction(function() {
this.setTimetable();
console.log("次電+ inited!");
this._inited_ = true;
}, this));
}, this);
},
finalize: function() {
},
loadPref:function(){
[this.routeTextbox.value, this.placeTextbox.value, this.boundTextbox.value]
= prefs.get(["route", "place", "bound"]);
this.margintimeMenulist.label = prefs.get("margintime");
this.statusbarLabelTemplate = prefs.get("statusbarlabel.template");
},
savePref: function() {
prefs.set({
route: this.routeTextbox.value.trim(),
place: this.placeTextbox.value.trim(),
bound: this.boundTextbox.value.trim()
});
var margintime = parseInt(this.margintimeMenulist.label.trim());
prefs.set("margintime", margintime >= 0 ? margintime : 10);
},
_showPanelFirst: true,
showPanel: function() {
this.loadPref();
global.TimetableList.resetSelection();
this.hideTypePanel();
this.hideMessage();
this.panel.openPopup(this.statusbarpanel,"before_end", -1,-1,false);
if (EXTENSION.firstRun && this._showPanelFirst) {
this.showFirstrunPanel();
}
this._showPanelFirst = false;
},
hidePanel: function() {
this.panel.hidePopup();
},
togglePanel: function() {
var state = this.panel.state;
if (state=="hidden"||state=="closed") {
this.showPanel();
}
else {
this.hidePanel();
}
},
showFirstrunPanel: function() {
document.getElementById("opentimetable-firstrun-panel").openPopup(this.statusbarpanel,"before_end", -1,-1,false);
},
hideFirstrunPanel: function() {
document.getElementById("opentimetable-firstrun-panel").hidePopup();
},
showMessage: function(messageList, box) {
box = box ? box : this.messageBox;
while (box.firstChild) {
box.removeChild(box.firstChild);
}
if (!(messageList instanceof Array))
messageList = [messageList];
for each (var message in messageList) {
var desc = document.createElement("description");
desc.setAttribute("value", message);
box.appendChild(desc);
}
box.collapsed = false;
},
hideMessage: function(boxList) {
boxList = boxList ? boxList : [this.messageBox, this.routeMessageBox, this.placeMessageBox, this.boundMessageBox];
if (!(boxList instanceof Array))
boxList = [boxList];
for each (var box in boxList) {
box.collapsed = true;
}
},
checkInput: function(route, place, bound, ignoreerror) {
var status = !!(route && place && bound);
if (!status && !ignoreerror) {
this.showMessageRequiredInput(route, place, bound);
}
return this.checkInputMatch(route, place, bound, ignoreerror) && status;
},
showMessageRequiredInput: function(route, place, bound) {
if (!route)
this.showMessage(strings.get("requiredInputError", [strings.get("route")]), this.routeMessageBox);
if (!place)
this.showMessage(strings.get("requiredInputError", [strings.get("place")]), this.placeMessageBox);
if (!bound)
this.showMessage(strings.get("requiredInputError", [strings.get("bound")]), this.boundMessageBox);
},
checkInputMatch: function(route, place, bound, ignoreerror) {
var matchedRoute = this.TimetableList.getMatchedRoute(route);
var matchedPlace = this.TimetableList.getMatchedPlace(place);
var matchedBound = this.TimetableList.getMatchedBound(bound);
var status = !!(matchedRoute && matchedPlace && matchedBound);
if (!status && !ignoreerror) {
this.showMessageNotFound(matchedRoute, matchedPlace, matchedBound);
}
return status;
},
showMessageNotFound: function(matchedRoute, matchedPlace, matchedBound) {
if (!matchedRoute)
this.showMessage(strings.get("notRegisteredError", [strings.get("route")]), this.routeMessageBox);
if (!matchedPlace)
this.showMessage(strings.get("notRegisteredError", [strings.get("place")]), this.placeMessageBox);
if (!matchedBound)
this.showMessage(strings.get("notRegisteredError", [strings.get("bound")]), this.boundMessageBox);
},
/* -------------------- Suggest Part -------------------- */
showSuggestPanel: function(panel) {
switch (panel) {
case this.routeSuggestPanel:
var target = this.routeTextbox;
break;
case this.placeSuggestPanel:
var target = this.placeTextbox;
break;
case this.boundSuggestPanel:
var target = this.boundTextbox;
break;
default:
console.error("invalid suggest panel");
return null;
}
this.hideOtherSuggestPanels(panel);
panel.openPopup(target,"before_end", -1,-1,true);
},
hideSuggestPanel: function(panel) {
panel.hidePopup();
},
hideOtherSuggestPanels: function(panel) {
[this.routeSuggestPanel, this.placeSuggestPanel, this.boundSuggestPanel].filter(function(p) p!=panel).forEach(function(p) this.hideSuggestPanel(p), this);
},
suggestRoute: function(route) {
route = route.trim();
this.TimetableList.selectRoute();
this.suggestUpdate({
axis: "route",
val: route,
panel: this.routeSuggestPanel,
box: this.routeSuggestBox,
list: this.TimetableList.getRouteSuggest(route)
});
},
suggestPlace: function(place) {
place = place.trim();
this.TimetableList.selectPlace();
this.suggestUpdate({
axis: "place",
val: place,
panel: this.placeSuggestPanel,
box: this.placeSuggestBox,
list: this.TimetableList.getPlaceSuggest(place)
});
},
suggestBound: function(bound) {
bound = bound.trim();
this.TimetableList.selectBound();
this.suggestUpdate({
axis: "bound",
val: bound,
panel: this.boundSuggestPanel,
box: this.boundSuggestBox,
list: this.TimetableList.getBoundSuggest(bound)
});
},
suggestUpdate: function(param /* axis, val, panel, box, list */) {
if (param.list.length==0 || (param.list.length==1 && param.list[0]==param.val)) {
this.hideSuggestPanel(param.panel);
return;
}
while (param.box.firstChild) { param.box.removeChild(param.box.firstChild); }
var maxitem = prefs.get("maxsuggestitem");
for each (var item in param.list) {
var desc = document.createElement("description");
desc.setAttribute("value", item);
desc.setAttribute("class", "text-link opentimetable-suggest-item");
desc.addEventListener("click", (function(item) function() { global.suggestSelect(param.axis,item); })(item), false); // hack for closure within for loop
param.box.appendChild(desc);
if (--maxitem === 0) { break; }
}
this.showSuggestPanel(param.panel)
},
suggestSelect: function(axis, value) {
switch (axis) {
case "route":
this.routeTextbox.value = value;
this.TimetableList.selectRoute(value);
this.hideSuggestPanel(this.routeSuggestPanel);
break;
case "place":
this.placeTextbox.value = value;
this.TimetableList.selectPlace(value);
this.hideSuggestPanel(this.placeSuggestPanel);
break;
case "bound":
this.boundTextbox.value = value;
this.TimetableList.selectBound(value);
this.hideSuggestPanel(this.boundSuggestPanel);
break;
}
},
/* -------------------- Type Selector Part -------------------- */
showTypePanel: function() {
this.typePanel.openPopup(this.statusbarpanel,"before_end", -1,-1,false);
},
hideTypePanel: function() {
this.typePanel.hidePopup();
},
selectFallbackType: function(param) {
this._fallbacktype = param.fallbacktype;
prefs.set("fallbacktype", param.fallbacktype);
this.TimetableList.getTimetable(param, bindFunction(function(timetable) {
if (this._timer) {
clearTimeout(this._timer);
this._timer=null;
}
this.currentTable = timetable;
this.updateStatusbar({});
}, this));
global.hideTypePanel();
},
/* -------------------- Timetable Handler Part -------------------- */
_setTimetableFirst: true,
setTimetable: function() {
this.savePref();
if (this._timer) {
clearTimeout(this._timer);
this._timer=null;
}
this._fallbacktype = "";
var route = this.routeTextbox.value.trim();
var place = this.placeTextbox.value.trim();
var bound = this.boundTextbox.value.trim();
if (route && place && bound) {
if (!this.checkInputMatch(route, place, bound)) { return; }
// 入力駅の履歴を保存しておく (将来履歴に応じた Suggest なども...)
this.formHistory.addEntry("opentimetable-route-history", route);
this.formHistory.addEntry("opentimetable-place-history", place);
this.formHistory.addEntry("opentimetable-bound-history", bound);
try {
var param = {
route: route,
place: place,
bound: bound,
requirealldays: true
};
if (this._setTimetableFirst) {
param.fallbacktype = prefs.get("fallbacktype");
if (param.fallbacktype) {
this._fallbacktype = param.fallbacktype;
param.requirealldays = false;
}
this._setTimetableFirst = false;
}
this.TimetableList.getTimetable(param, bindFunction(function(timetable) {
this.currentTable = timetable;
this.updateStatusbar();
}, this));
}
catch(e) {
console.trace();
if (e.name === "OnlyPartialDaysError") {
this.disableStatusbar();
// 時刻表定義が足りないがどうするかとユーザに確認する
var box = global.typeBox;
while (box.firstChild) { box.removeChild(box.firstChild); }
for each (var type in e.typeList) {
// use block scope to define separate param for each eventlistener
let param = {route: route, place: place, bound: bound, fallbacktype: type};
var desc = document.createElement("description");
desc.setAttribute("value", type);
desc.setAttribute("class", "text-link opentimetable-suggest-item");
desc.addEventListener("click", function() global.selectFallbackType(param), false);
box.appendChild(desc);
}
prefs.reset("fallbacktype");
global.showTypePanel();
}
else {
console.exception(e);
}
}
}
else {
// route/place/bound を設定しなかったときは無効化する
this.disableStatusbar();
}
this.hidePanel();
},
disableStatusbar: function() {
this.statusbarpanelLabel.value = "";
this.statusbarpanel.className = "disabled";
if (this._timer) {
clearTimeout(this._timer);
this._timer=null;
}
},
updateStatusbar: function() {
if (this.currentTable === null) {
console.log("Current timetable is not valid and disable statusbar...");
return this.disableStatusbar();
}
var margintime = prefs.get("margintime");
var next = this.currentTable.getNextService(new Date(Date.now() + margintime*1000*60));
if (next instanceof global.Service) { // 次のサービスが無事取得できた場合
this.statusbarpanelLabel.value = next.text;
this.statusbarpanel.className = "enabled";
var timediff = next.date.getTime() - margintime*1000*60 - Date.now();
this._timer = setTimeout(bindFunction(function() {
this.updateStatusbar();
}, this), timediff);
}
else { // 本日分のサービスがすべて終了している場合は明日の時刻表を取得して最初の電車を選択
console.warn("No more services today and getting tomorrow timetable...");
var param = {
route: this.currentTable.route,
place: this.currentTable.place,
bound: this.currentTable.bound,
fallbacktype: this._fallbacktype,
date: new Date(Date.now() + 1000*60*60*24)
}
this.TimetableList.getTimetable(param, bindFunction(function(timetable) {
this.currentTable = timetable;
next = this.currentTable.getFirstService();
if (next) {
this.statusbarpanelLabel.value = next.textTomorrow;
this.statusbarpanel.className = "enabled";
var timediff = next.date.getTime() + 1000*60*(60*24-margintime) - Date.now();
this._timer = setTimeout(bindFunction(this, function() {
this.updateStatusbar();
}), timediff);
}
else {
console.info("Tomorrow's timetable has no service.");
this.statusbarpanelLabel.value = prefs.get("statusbarlabel.noservice");
this.statusbarpanel.className = "enabled";
var timediff = (Date()).setHours(FIRST_HOUR) + 1000*60*(60*24) - Date.now();
this._timer = setTimeout(bindFunction(this, function() {
this.updateStatusbar();
}), timediff);
}
}, this));
}
},
// https://developer.mozilla.org/ja/Code_snippets/Tabbed_browser
openTab: function(url) {
gBrowser.selectedTab = gBrowser.addTab(url);
},
showTimetable: function() {
this.hideMessage();
var route = this.routeTextbox.value.trim();
var place = this.placeTextbox.value.trim();
var bound = this.boundTextbox.value.trim();
if (!this.checkInput(route, place, bound)) { return; }
var param = {
route: global.TimetableList.getMatchedRoute(route),
place: global.TimetableList.getMatchedPlace(place),
bound: global.TimetableList.getMatchedBound(bound)
}
try {
this.TimetableList.getTimetable(param, bindFunction(function(timetable) {
if (timetable) {
this.openTab(timetable.getUrl());
this.hidePanel();
}
else {
this.showMessage(strings.get("notRegisteredTimetableError"));
}
}, this));
}
catch(e) { // 全曜日分の type がなくとも今日の分または該当曜日数の多い type を表示する
if (e.name === "OnlyPartialDaysError") {
param.type = e.typeList[e.typeList.length-1];
console.warn("本日分の時刻表が登録されていないため "+param.type+" の時刻表を表示します");
this.TimetableList.getTimetable(param, bindFunction(function(timetable) {
if (timetable) {
this.openTab(timetable.getUrl());
this.hidePanel();
}
else {
this.showMessage(strings.get("notRegisteredTimetableError"));
}
}, this));
}
else {
console.exception(e);
}
}
}
};
/* -------------------- TimetableList Objects -------------------- */
global.TimetableList = {
getHolidayApiUrl: function(t, n) {
t = t instanceof Date ? t : new Date();
return replaceTemplate(HOLIDAY_CALENDER_API_URL_TEMPLATE, {
STARTDAY: t.getFullYear()+"-"+pad0(t.getMonth()+1,2)+"-"+pad0(t.getDate(),2),
NUM: n > 0 ? n : 10
});
},
_comingHolidays: [],
isHoliday: function(t) {
t = t instanceof Date ? t : new Date();
return this._comingHolidays.indexOf(t.getFullYear()+"-"+pad0(t.getMonth()+1,2)+"-"+pad0(t.getDate(),2)) != -1;
},
init: function(data, callback) {
// all data
this._all = data;
this._allKeys = [i for (i in this._all)];
unique(Array);
unique(new Array(1,2,3));
this._allRoutes = unique(data.map(function(v)v.route));
this._allPlaces = unique(data.map(function(v)v.place));
this._allBounds = unique(data.map(function(v)v.bound));
// keys of current subset (for filtering cache)
// if these will not modified at all, need not clone...
this._keysByRoute = this._allKeys.concat();
this._keysByPlace = this._allKeys.concat();
this._keysByBound = this._allKeys.concat();
this._keys = this._allKeys.concat();
// unique value list of current subset (for suggest)
this._routeList = this._allRoutes.concat();
this._placeList = this._allPlaces.concat();
this._boundList = this._allBounds.concat();
//this._typeList = this._allBounds.concat();
this.resetSelection();
httpGet(this.getHolidayApiUrl(), function(data, status ,req) {
this._comingHolidays = JSON.parse(data).feed.entry.map(function(e) e["gd$when"][0].startTime);
if (typeof callback === "function") {
callback(this);
}
}, this);
},
// check route/place/bound with postfix normalization
normalize: function(s,re) s.replace(re,''),
normalizeRoute: function(s) s.replace(RE_ROUTE,''),
normalizePlace: function(s) s.replace(RE_PLACE,''),
normalizeBound: function(s) s.replace(RE_BOUND,''),
match: function(a,b,re) this.normalize(a,re)===this.normalize(b,re),
matchRoute: function(a,b) this.normalizeRoute(a)===this.normalizeRoute(b),
matchPlace: function(a,b) this.normalizePlace(a)===this.normalizePlace(b),
matchBound: function(a,b) this.normalizeBound(a)===this.normalizeBound(b),
getMatched: function(s,list,re) {
if (!s) { return; }
for each (var e in list) {
if (this.match(s,e,re))
return e;
}
},
getMatchedRoute: function(s) this.getMatched(s, this._allRoutes, RE_ROUTE),
getMatchedPlace: function(s) this.getMatched(s, this._allPlaces, RE_PLACE),
getMatchedBound: function(s) this.getMatched(s, this._allBounds, RE_BOUND),
// return sub list of routes/places/bounds filtered with s
// 本当は 線/駅/方面 などの postfix がある場合は部分一致じゃなくて後方一致にすべきだが...
getRouteSuggest: function(s) {
var t = this.normalizeRoute(s);
return s ? this._routeList.filter(function(e) e.indexOf(t)!==-1) : this._routeList;
},
getPlaceSuggest: function(s) {
var t = this.normalizePlace(s);
return s ? this._placeList.filter(function(e) e.indexOf(t)!==-1) : this._placeList;
},
getBoundSuggest: function(s) {
var t = this.normalizeBound(s);
return s ? this._boundList.filter(function(e) e.indexOf(t)!==-1) : this._boundList;
},
_updateSuggestLists: function() {
this._routeList = unique(this._keys.map(function(k) this._all[k].route, this));
this._placeList = unique(this._keys.map(function(k) this._all[k].place, this));
this._boundList = unique(this._keys.map(function(k) this._all[k].bound, this));
//this._typeList = unique(this._keys.map(function(k) this._all[k].type));
},
// route/place/bound を選択状態にし、suggest list に反映させる
selectRoute: function(route) {
var matchedRoute = this.getMatchedRoute(route);
if (matchedRoute) {
this._keysByRoute = this._allKeys.filter(function(k) this.matchRoute(this._all[k].route, route), this);
this._keys = intersect(this._keysByRoute, this._keysByPlace, this._keysByBound);
}
else {
this._keysByRoute = this._allKeys.concat();
this._keys = intersect(this._keysByPlace, this._keysByBound);
}
this._updateSuggestLists();
return matchedRoute;
},
// NOTE 湖遊館新「駅駅」 には対応できていない
selectPlace: function(place) {
var matchedPlace = this.getMatchedPlace(place);
if (matchedPlace) {
this._keysByPlace = this._allKeys.filter(function(k) this.matchPlace(this._all[k].place, place), this);
this._keys = intersect(this._keysByRoute, this._keysByPlace, this._keysByBound);
}
else {
this._keysByPlace = this._allKeys.concat();
this._keys = intersect(this._keysByRoute, this._keysByBound);
}
this._updateSuggestLists();
return matchedPlace;
},
selectBound: function(bound) {
var matchedBound = this.getMatchedBound(bound)
if (matchedBound) {
this._keysByBound = this._allKeys.filter(function(k) this.matchBound(this._all[k].bound, bound), this);
this._keys = intersect(this._keysByRoute, this._keysByPlace, this._keysByBound);
}
else {
this._keysByBound = this._allKeys.concat();
this._keys = intersect(this._keysByRoute, this._keysByPlace);
}
this._updateSuggestLists();
return matchedBound;
},
resetSelection: function() {
this.selectRoute(prefs.get("route"));
this.selectPlace(prefs.get("place"));
this.selectBound(prefs.get("bound"));
},
getTimetable: function(param, callback) {
//console.dir(param);
console.assert(param && param.route && param.place && param.bound, "Not enough info to create timetable!\n route: "+route+"\n place: "+place+"\n bound: "+bound);
var route = param.route.trim();
var place = param.place.trim();
var bound = param.bound.trim();
var date = param.date || new Date();
if (param.type) { // 曜日が明示指定されているとき
return new global.Timetable(route, place, bound, param.type, callback);
}
// type は通常 route/place/bound がマッチする時刻表の type のリストから自動選択
var keys = this._allKeys
.filter(function(k) this.matchRoute(this._all[k].route, route), this)
.filter(function(k) this.matchPlace(this._all[k].place, place), this)
.filter(function(k) this.matchBound(this._all[k].bound, bound), this);
console.assert(keys, "No timetable batch to the parameters!\n route: "+route+"\n place: "+place+"\n bound: "+bound);
this._typeList = unique(keys.map(function(k) this._all[k].type, this));
// 該当曜日が少ないものが先(平日などより月曜の方が先)に来るように並び替え
this._typeList.sort(function(t1,t2)
DAYS_OF_TYPE[t1].filter(function(a) a).length >=
DAYS_OF_TYPE[t2].filter(function(a) a).length);
var day = date.getDay();
var hour = date.getHours();
day = hour>LAST_HOUR-24 ? day : (day===0 ? 6 : day-1); // 2時まで前日扱い
if (this.isHoliday() && this._typeList.some(function(e) e[7])) {
var type = this._typeList.filter(function(e) e[7])[0];
}
else {
for each (t in this._typeList) {
if (DAYS_OF_TYPE[t][day]) {
var type = t;
break;
}
}
}
// 全曜日分の時刻表が定義されているか
var haveAllDays = this._typeList.length > 0 && this._typeList
.map(function(type) DAYS_OF_TYPE[type]||[] ,this)
.reduce(function(days1,days2) [days1[i]||days2[i] for (i in days1)])
.slice(0,7) // 祝日フラグは無視
.every(function(day)day===true);
if (haveAllDays) { // 全曜日の定義あり
return new global.Timetable(route, place, bound, type, callback);
}
else if (!param.requirealldays && (type || param.fallbacktype)) { // 定義不足だが該当曜日または Fallback 指定有り
type = type ? type : param.fallbacktype;
console.warn("Only partial days of timetable is defined and using timetable with\n route: "+route+"\n place: "+place+"\n bound: "+bound+"\n type: "+type+"\n defined types: "+this._typeList.join(", "));
return new global.Timetable(route, place, bound, type, callback);
}
else { // 定義不足で該当曜日も Fallback も無い
var error = new Error("timetable is defined but not for all days");
error.name = "OnlyPartialDaysError";
error.typeList = this._typeList;
throw error;
}
}
}
/* -------------------- Timetable Class -------------------- */
global.Timetable = function(route, place, bound, type, callback) {
if ([route,place,bound,type].some(function(x) typeof x !== "string" || x == "")) {
console.trace();
return null;
}
this.route = route.trim();
this.place = place.trim();
this.bound = bound.trim();
this.type = type.trim();
httpGet(this.getApiUrl(), function(data, status, req) {
//console.log(this.getApiUrl()+":\n"+data);
var parsedData = JSON.parse(data);
if (parsedData.error) { // 未登録時刻表を要求した場合 "no timetable"
console.error("server responce to get timetable: \n"+parsedData.error);
}
var labels = parsedData.labels;
this._allLabels = [];
for (var i=0; i < labels.length; i++) {
this._allLabels[labels[i].id] = labels[i];
}
var timetable = parsedData.timetable;
this._allServices = []; // 時刻表に登録されている全サービス
this._serviceIndexByHour = []; // X時以降のサービスを検索するときに検索開始すべき番号を記録
for (var h=0; h < 36; h++) {
this._serviceIndexByHour[h] = this._allServices.length;
if (timetable[h]) {
for each (var item in timetable[h]) {
this._allServices.push(new global.Service(h, item.minute, this._allLabels[item.label_id].remark, this._allLabels[item.label_id].color));
}
}
}
this.serviceNum = this._allServices.length;
if (typeof callback === "function") {
callback(this);
}
}, this);
}
global.Timetable.prototype = {
getUrl: function() replaceTemplate(TIMETABLE_URL_TEMPLATE, this),
getApiUrl: function() replaceTemplate(TIMETABLE_API_URL_TEMPLATE, this),
getFirstService: function() this._allServices[0],
getLastService: function() this._allServices[this._allServices.length-1],
getNextService: function(date) {
date = date instanceof Date ? date : new Date();
var hour = date.getHours();
var min = date.getMinutes();
if (hour < FIRST_HOUR) {
hour += 24;
}
var startIndex = this._serviceIndexByHour[hour];
for (var i = this._serviceIndexByHour[hour]; i < this._allServices.length; i++) {
if (this._allServices[i].hour > hour || this._allServices[i].min > min) {
return this._allServices[i];
}
}
return null;
}
};
global.Service = function(hour, min, remark, color) {
// まとめて大量生成されるので初期化は最小限に
this._hour = parseInt(hour);
this._min = parseInt(min);
this._remark = remark;
this._color = color;
}
global.Service.prototype = {
get hour() this._hour,
get min() this._min,
get date() {
var d = new Date();
d.setHours(this._hour < 24 ? this._hour : this._hour-24);
d.setMinutes(this._min);
d.setSeconds(0);
return this._hour < 24 ? d : new Date(d.getTime() + 1000*60*60*24)
},
get remark() this._remark,
get color() this._color,
get textHour() pad0(this._hour,2),
get textMin() pad0(this._min,2),
get textTime() this.textHour+":"+this.textMin,
_textTemplate: prefs.get("statusbarlabel.template"),
get text() replaceTemplate(this._textTemplate, {
STARTTIME: this.textTime,
REMARK: this.remark
}),
_textTomorrowTemplate: prefs.get("statusbarlabel.tommorow.template"),
get textTomorrow() replaceTemplate(this._textTomorrowTemplate, {
STARTTIME: this.textTime,
REMARK: this.remark
})
}
return global;
})();
window.addEventListener('load', function() { opentimetable.init(); }, false);
window.addEventListener("unload", function() { opentimetable.finalize(); }, false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment