Monday, June 15, 2009

Google Maps API v3, MVCObject


這算是 v3 的一個特點,使用了 MVC 架構來組織程式。其 MVC 架構的核心就是 MVCObject,不過基本上就是有系統的使用之前提到的 event system,用以通知該收到某一事件的物件。
function MVCObject() {
  this.bindValues = {};
  this.bindListeners = {}
 }
有兩個屬性,一個記錄自已正在接收哪些物件的哪些 event,另一個是接收 event 用的 listener。

最重要的就是 bindTo:
J = MVCObject["prototype"];
 // Binds a View to a Model.
 // key:string, target:MVCObject, targetKey:string, noNotify?:boolean
 // after bind, the set value or get value will call the model object. 
 J.bindTo = function(key, target, targetKey, noNotify) {
  var e = this;
  e["unbind"](key);
  (e.bindListeners[key] = {
   target : target,
   kk : targetKey
  }).Ud = event["addListener"](target, targetKey + "_changed", function() {
     e.notify(key)
    });
  this.bindValue(key, target, targetKey, noNotify)
 };
 J.bindValue = function(key, target, targetKey, noNotify) {
  this.bindValues[key] = {
   object : target,
   targetKey : targetKey
  };
  noNotify || this.notify(key)
 };

 // Notify observers of a change.
 // key:string
 J.notify = function(key) {
  var b = key + "_changed";
  this[b] ? this[b]() : this.changed(key);
  event["trigger"](this, b)
 };
表示要用本身的 key 事件來接收 target 的 targetKey 的事件,這代表,當 target 的 targetKey_changed 被觸發時,即會呼叫本身的 notify。 而呼叫 notify 通常代表的就是 key_changed functoin 被呼叫,和 key_changed event 被觸發。而除了接收 event 的觸發方式外,就是直接去設值的觸發方式了:
// Sets a value.
 // key:string, value:*
 J.set = function(key, value) {
  if (this.bindValues.hasOwnProperty(key)) {
   var c = this.bindValues[key], d = c.targetKey, e = c.object, g = "set_" + d;
   e[g] ? e[g](value) : e.set(d, value)
  } else {
   this[key] = value;
   this.notify(key)
  }
 };
set 會呼叫 set_key 的 function ,如果沒有定義的話,就會去 notify key event,而進而就會觸發在接收此 event 的物件。

有 bindTo 就會有 unbind,就是解除先前定義的 bind 關係。
// Removes a bind.
 // key:string
 J.unbind = function(key) {
  var b = this.bindListeners[key];
  if (b) {
   delete this.bindListeners[key];
   event["removeListener"](b.Ud);
   this.unbindValue(key)
  }
 };

 J.unbindValue = function(key) {
  var b = this.get(key);
  delete this.bindValues[key];
  this[key] = b
 };

雖然 MVCObject 的程式不大,但卻是 v3 非常重要的物件,大部分 v3 的物件都是繼承自 MVCObject ,而進而獲得可 bind 其他物件,收發 event 的能力。

Tuesday, June 9, 2009

Google Maps API v3, event system

此處程式是以 http://maps.gstatic.com/mapfiles/api-3/5/main.js 為對象。

Google maps api v3 的 event system 有兩種 event,一種是 Normal event,一種是 DOM event。Normal event 可以定義任何名稱的 event,而 DOM 的 event 就是 DOM 已定義的 event,而兩種 event 可以並用。範例如下:
HTML:
<span id="ts">test</span>

JavaScript:
var ts = document.getElementById("ts");
// register DOM event listener will also register normal event listener 
google.maps.event.addDomListener(ts, "click", function() {
  console.log("ts.click dom");
});
// Normal event listener will not be triggered by DOM event.
google.maps.event.addListener(ts, "click", function() {
  console.log("ts.click");
});
// can register multiple handler on same event
google.maps.event.addListener(ts, "click", function() {
  console.log("ts.click2");
});

// will trigger Normal event listener and DOM event Listener
google.maps.event.trigger(ts, "click");

執行結果為:
ts.click dom
ts.click
ts.click2
(而 mouse click 只觸發 ts.click dom 訊息)

可同時定義任意個數的 Normal event 和 DOM event。而 trigger 會同時觸發 Normal event 和 Dom event。

而 event 的 source code 有幾個相關的部分,第一是 event 本身:
var event = {};
 // Adds the given listener function to the the given event name for the given object instance.
 // Returns an identifier for this listener that can be used with eventRemoveListener().
 // 
 // instance:Object, eventName:string, handler:Function, owner?:Object
 // return EventListener
 //
 event.addListener = function(instance, eventName, handler, owner) {
  // already add listener to instance's event listener chain
  var e = new EventListener(instance, eventName, handler, 0, owner);
  getInstance(EventListenerManager).addListener(e);
  return e
 };

其中 EventListener 和 EventListenerManager 的程式如下:
// instance, eventName, handler, 0, owner
 function EventListener(instance, eventName, handler, domEventMethod, owner) {
  nc(instance);
  nc(typeof handler == "function");
  this.u = instance;
  this.Ga = eventName;
  this.mb = handler;
  this.ic = null;
  this.Hi = domEventMethod;
  this.ni = owner || null;
  this._idx = -1;
  getEventListenerChain(instance, eventName, true)["push"](this)
 }
 EventListener["prototype"].createOldStyleHandler = function() {
  var a = this;
  return this.ic = function(b) {
   if (!b)
    b = window.event;
   if (b && !b.target)
    try {
     b.target = b.srcElement
    } catch (c) {
    }
   var d = a.triggerHandler([b]);
   if (b && "click" == b["type"]) {
    var e = b.srcElement;
    if (e && "A" == e["tagName"] && "javascript:void(0)" == e.href)
     return false
   }
   return d
  }
 };
 EventListener["prototype"].remove = function() {
  if (this.u) {
   switch (this.Hi) {
    case 1 :
     this.u.removeEventListener(this.Ga, this.mb, false);
     break;
    case 4 :
     this.u.removeEventListener(this.Ga, this.mb, true);
     break;
    case 2 :
     this.u.detachEvent("on" + this.Ga, this.ic);
     break;
    case 3 :
     this.u["on" + this.Ga] = null;
     break
   }
   for (var a = getEventListenerChain(this.u, this.Ga), b = 0, c = 0; c < getLength(a); ++c)
    if (a[c] === this) {
     a.splice(c--, 1);
     b++
    }
   this.ic = this.mb = this.u = null
  }
 };
 EventListener["prototype"].isOwner = function(a) {
  return this.ni === a
 };
 EventListener["prototype"].triggerHandler = function(a) {
  if (this.u)
   return this.mb["apply"](this.u, a)
 };

 function EventListenerManager() {
  this.I = []
 }
 EventListenerManager["prototype"].removeListener = function(listener) {
  var b = listener._idx;
  if (!(b < 0)) {
   var c = this.I.pop();
   if (b < this.I["length"]) {
    this.I[b] = c;
    c._idx = b
   }
   listener._idx = -1
  }
 };
 EventListenerManager["prototype"].addListener = function(listener) {
  this.I["push"](listener);
  listener._idx = this.I["length"] - 1
 };
 EventListenerManager["prototype"].clear = function() {
  array_foreach(this.I, function(a) {
     a._idx = -1
    });
  setLength(this.I, 0)
 };
可以看得出來,Normal Event 就是在 instance 下找個地方把 handler 收集起來,以便之後 trigger 時呼叫。其中 remove 是要配合之後 DOM event 的 remove 做法。

remove 也很容易理解:
// Removes the given listener, which should have been returned by eventAddListener above.
 // listener:EventListener
 event.removeListener = function(listener) {
  listener["remove"]();
  getInstance(EventListenerManager)["removeListener"](listener)
 };

 // Removes all listeners for the given event for the given instance.
 // instance:Object, eventName:string, owner?:Object
 event.clearListeners = function(instance, eventName, owner) {
  array_foreach(cloneEventListenerChain(instance, eventName), function(d) {
     if (!owner || d.isOwner(owner)) {
      d["remove"]();
      getInstance(EventListenerManager)["removeListener"](d)
     }
    })
 };
 // Removes all listeners for all events for the given instance.
 // instance:Object, owner?:Object
 event.clearInstanceListeners = function(instance, owner) {
  array_foreach(cloneEventListenerChain(instance), function(c) {
     if (!owner || c.isOwner(owner)) {
      c["remove"]();
      getInstance(EventListenerManager)["removeListener"](c)
     }
    })
 };
 event.clearAllEventListener = function() {
  for (var a = [], b = getInstance(EventListenerManager).I, c = 0, d = getLength(b); c < d; ++c) {
   var e = b[c], g = e.u;
   if (!g.__tag__) {
    g.__tag__ = true;
    a["push"](g)
   }
   e["remove"]()
  }
  for (c = 0; c < getLength(a); ++c) {
   g = a[c];
   if (g.__tag__)
    try {
     delete g.__tag__;
     delete g.__e_
    } catch (h) {
     g.__tag__ = false;
     g.__e_ = null
    }
  }
  getInstance(EventListenerManager).clear()
 };

值得看到的是 getInstance 是用簡單的手法做 Singleton pattern。
function getInstance(a) {
  if (!a.u)
   a.u = new a;
  return a.u
 }

幾個 tool function:
// instance, eventName
 function cloneEventListenerChain(instance, eventName) {
  var c = [], d = instance.__e_;
  if (d)
   if (eventName)
    d[eventName] && pushAll(c, d[eventName]);
   else
    foreach(d, function(e, g) {
       pushAll(c, g)
      });
  return c
 }
 // instance, eventName, true
 function getEventListenerChain(instance, eventName, create) {
  var d = null, e = instance.__e_;
  if (e) {
   d = e[eventName];
   if (!d) {
    d = [];
    if (create)
     e[eventName] = d
   }
  } else {
   d = [];
   if (create) {
    instance.__e_ = {};
    instance.__e_[eventName] = d
   }
  }
  return d
 }

trigger 也很容易理解,就是找出註冊的 handler 然後一一呼叫就行了。
// Triggers the given event. All arguments after eventName are passed as arguments to the listeners.
 // instance:Object, eventName:string, var_args:*
 event.trigger = function(instance, eventName) {
  var c = dc(arguments, 2);
  array_foreach(cloneEventListenerChain(instance, eventName), function(d) {
     d.triggerHandler(c)
    })
 };

DOM event 因為要支搜多種不同 browser 的 event 系統,所以需要做一些判斷。
// Cross browser event handler registration.
 // This listener is removed by calling eventRemoveListener(handle) for the handle that is returned by this function.
 // instance:Object, eventName:string, handler:Function, owner?:Object
 // return EventListener
 event.addDomListener = function(instance, eventName, handler, owner) {
  var e;
  if (instance.addEventListener) {
   var g = false;
   if (eventName == "focusin") {
    eventName = "focus";
    g = true
   } else if (eventName == "focusout") {
    eventName = "blur";
    g = true
   }
   var h = g ? 4 : 1;
   instance.addEventListener(eventName, handler, g);
   e = new EventListener(instance, eventName, handler, h, owner)
  } else if (instance.attachEvent) {
   e = new EventListener(instance, eventName, handler, 2, owner);
   instance.attachEvent("on" + eventName, e.createOldStyleHandler())
  } else {
   instance["on" + eventName] = handler;
   e = new EventListener(instance, eventName, handler, 3, owner)
  }
  if (instance != window || eventName != "unload")
   getInstance(EventListenerManager).addListener(e);
  return e
 };

也提供一些 forward 的手法方便使用。
event.forwardDomListener = function(instance, eventName, target, handler, owner) {
  var g = xc(target, handler);
  return event["addDomListener"](instance, eventName, g, owner)
 };
 function xc(a, b) {
  nc(b);
  return function(c) {
   return b["call"](a, c, this)
  }
 }
 event.forwardListener = function(instance, eventName, target, handler, owner) {
  nc(handler);
  return event["addListener"](instance, eventName, createCaller(target, handler), owner)
 };
 event.addOnetimeListener = function(instance, eventName, handler) {
  var d = event["addListener"](instance, eventName, function() {
     handler["apply"](instance, arguments);
     event["removeListener"](d)
    });
  return d
 };
 event.forward = function(instance, eventName, target) {
  return event["addListener"](instance, eventName, yc(eventName, target))
 };
 event.forwardDom = function(instance, eventName, target, owner) {
  return event["addDomListener"](instance, eventName, yc(eventName, target, true), owner)
 };
 function yc(eventName, instance, isDom) {
  return function(d) {
   var e = [instance, eventName];
   pushAll(e, arguments);
   event["trigger"]["apply"](this, e);
   if (isDom) {
    var g = d || window.event;
    g.cancelBubble = true;
    g["stopPropagation"] && g["stopPropagation"]()
   }
  }
 }


event 系統並沒有太複雜的做法,但卻是整個系統都會使用到的工具。

Sunday, June 7, 2009

Google Maps API v3, modulize

本來想說把之前的程式搬到 Google Maps API v3 上,但很快就發現目前 v3 的完成度還不足,所以先暫時停下來,反正也不是什麼急迫的工作。而趁目前 v3 還小而且有不同的架構,所以想來看看 maps 本身的程式。

一開始看到 maps 的主程式,會發現比起之前 v2 小了非常多 (12k / 64k),但其實是 v3 將程式進行模組化切割並分離下載,所以事實上一個基本的地圖功能要載入的程式至少有 main.js, mod_common.js, mod_controls.js, mod_image.js, mod_mapview.js, mod_stats.js,如果用到 marker 和 infowindow 則還需要 mod_marker.js 和 mod_infowindow.js。原本把一個功能分成多次下載是不利於載入速度的,但對 javascript 而言,沒有完全載入是無法開始執行的,而且以一般使用 maps 的方式,是會使用 block loading 的來載入程式 (這裡有相關的說明),所以縮小初始的程式碼大小是可以加快網頁其他部分的載入,而後續其他的程式部分則使用 insert javascirpt element 的方式,通常可以同步下載而不影響其他部分的顯示。main.js 中用來載入其他部分的程式類似於
function jd(a) {
  var b;
  Kc || (Kc = document.getElementsByTagName("head")[0]);
  b = Kc;
  var c = document[x]("script");
  c[fb]("type", "text/javascript");
  c[fb]("charset", "UTF-8");
  c[fb]("src", a);
  b[q](c)
 };
 function md(a) {
  eval(a)
 }
 n.__gjsload_apilite__ = md;
 var nd = "common", od = "controls", pd = "infowindow", qd = "mapview", rd = "stats";
 function sd(a) {
  var b = a[Ma]("/main.js", "");
  return function(c) {
   return b + "/mod_" + c + ".js"
  }
 }
而其他模組的程式碼則是
__gjsload_apilite__('var Le="_xdc_";function Me(a,b){n[tb](fu.....');
所以載入後會在 maps library 的匿名 scope 中 eval 其中的程式碼。非常典型的手法。

相關的討論在最近的 google io 有好幾個 session 提到,像是 Maps APIs & MobilePerformanceTipsGeoApiMashups,Google 在很多地方也用了類似的方式來加速,像是這裡討論的 runAsync。

Friday, June 5, 2009

Please, userscript friendly.

以後寫網站的人,要注意隨時有人會拿 userscript 插你啊...

前一篇提到 Maps api 要出新版,所以就開始進行 Flickr Gmap Show 改版 (其實是改寫了),順便改變以前程式的一些問題。之前在寫 Greasemonkey userscript 時,把程式都放在 externsion 的空間中,好處不少,像是不會被外界網站的改寫所影響,而且又有使用 cross-domain xhr 的特權,但壞處很明顯,就是安全性的問題,其實像 Flickr Gmap Show 這種功能,絕大部分的功能都不需要使用特權,反而因為被隔離在 extension 空間中而有很多麻煩之處,像是使用 unsafeWindow,不能直接存取 property 等等,所以就改變做法,把 javascript "插" 到外部網頁空間中,然後就像在一般網頁的 javascirpt 一樣的方便,但也有一樣的限制,有一樣的問題。

把 Google Maps api 插進 flickr 中時, 一切都很順利,地圖也順利出現,但把 jQuery 插進 flickr 時,卻發現出現一些奇奇怪怪的問題,像是找東西找不到之類的問題,本來以為是 jQuery 的 bug,但又發現插到其他網頁卻又沒問題,然後又 trace jQuery 的程式 (要看懂別人的 javascript 真是痛苦),發現問題居然出在一個 string 的操作有不同的行為。只好把 flickr 網頁中的程式給抓出來看 (全都是被混亂過的...><),後來發現下面的程式:
String.prototype.replace_regx = String.prototype.replace;
String.prototype.replace = function(B, A) {
    return this.split(B).join(A)
};

有沒有這麼扯,把東西加到標準函式庫中的就夠大逆不道了,居然還修改行為? 我想寫網頁的人可能想說 My page, My way. 沒人會和他在同一個房間裡,但以現在的網頁環境中,有太多可能有人要和你在同一個房間中工作了,不要把環境弄得奇奇怪怪的。還好還有留下原本的 replace,所以就在我的程式中改回來就行了。

Friday, May 29, 2009

Google Maps API v3

這對我而言是個大消息,因為這意味著我不得不開始改寫之前寫的程式了...XD

Announcing Google Maps API v3
http://code.google.com/apis/maps/documentation/v3/

我覺得最大的改變就是,不用申請 Key 了 XD。所以以前做的那些狗屁倒灶的事就不用再幹了。很明顯地程式體積大幅地縮小,我想主要是不用相容之前版本的寫法,而且也還沒把所有功能都加入。因為新版本,所以對原本存在的問題可以有全面性的修正,像是以前建出 map 未設定初始座標時會出現在異度空間 XD,現在是要給初始座標才能建立,還有號稱 MVC 架構的改寫,強化對 mobile device 的支援等等,都讓人非常期待這個新的版本。不過令人心痛地,雖然 maps 載入程式的部份好像沒被混亂過,不過 main.js 主程式的部分還是繼續地混亂 XD。

不過我前一陣子剛把我的 GeoPhotoShow 做大規範的改版 (雖然在介面和功能上都看不出來 XD),看來又要再進行一次改版了,但這次想先從 Flickr Gmap Show 開始改,畢竟快一年都沒動了,連 UI Control 都還是舊版的。

Thursday, April 30, 2009

藍天、白雲、草地、墓碑

早上一出門,看到...


 
讓原本硬梆梆的圍欄有點活潑的氣氛是好事,上面畫些圖案讓人看了比較不會感覺到正在施工的不便。不過,為什麼藍天、白雲、草地上要畫一群一群的墓碑呢?這種的會讓人看了心情好嗎?結果...




















 
這些花瓣真是讓人感到無力啊…

Wednesday, April 29, 2009

台灣電影上映日曆 - Yahoo!奇摩電影

Jeff Atwood 有言
Open source or not, if you aren't building software that someone finds useful, if you aren't convincing at least a small audience of programmers that your project is worthwhile enough to join

Then what are you really doing?
台灣電影上映日曆 從弄出來到現在已經一段時間了,中間雖然也有停止更新一陣子(atmovies 小變格式我沒發現),好像也沒人提醒我出問題了,可以得知有到這玩意兒的人很少。雖然是自動更新,不過程式執行的環境也有變化,從開始的 Java 到 Perl 到 Python,不過只能算是程式練習作品,不知道有沒有 someone finds useful :) (而且我也沒 Open source,有人有需要嗎?)

不過最近 atmovies 的電影介紹真是越來越混了,連一些大片上檔都有只一兩句話介紹,雖然 atmovies 的二輪分類是比較準一點,但有鑑於台灣網路=雅虎台灣,而且看Yahoo! 奇摩電影的人應該還是比較多,我就試著改用 Yahoo! 奇摩電影的資料。

有幾個地方會有差異,第一是二輪電影,兩個網路在判斷二輪電影似乎有不少的差別,而我現在處理的方式是把二輪上映的時間集中到當週的星期六 (一般都是星期五換片,但因為星期五首輪片太多,二輪就移到星期六),如果有人有注意的話會發現二輪片單有不同(應該是沒人這麼無聊的吧)。第二是上映廳數,在 Yahoo 上沒有容易的方法知道這個資訊,所以原本正在上映的片子會標上映廳數,現在變成標上目前評分 (Yahoo 的 5 級分)。再來就是 Yahoo 的電影介紹文字很多,如果有人是在 mobile device 上看的話 (有嗎?),不知道會變成什麼樣子…

在 Google Calendar 上使用的方式:
在 Other Calendar 的 Add 中選 Add by URL,然後用
http://www.csie.nctu.edu.tw/~wctang/taiwan-movies.ics

或是直接連這裡