SandboxとGoogleMapとJavaFX

先日、Twitterにてid:nowokay(@kis)とJavaFXでGoogleMapが〜というような話をしていたのでちょっと試してみました。実装しても対して難しい事ではありませんが、デフォルトでサポートして欲しいような部分とも言えます。
サンプルはこちらから(使用例はこちら)。コードはこちらからチェックしてみてください。

実装上のポイント

まず、Mapを表示するノードですが、CustomNodeを使い内部ではImageViewをラップしているような感じです。機能を拡張するような場合、ImageViewを継承するよりもずっと楽です。

public class GoogleMapView extends CustomNode, Resizable {
    public-init var map:GoogleMap;
    override function create() {
        ImageView {
            image: bind map.image;
        }
    }
    override var width = bind map.width;
    override var height = bind map.height;
    override function getPrefWidth(height:Number):Number {
        width;
    }
    override function getPrefHeight(width:Number):Number {
        height;
    }
}

画像はGoogleMapに保存するとして、それ以外はResizable にしているくらいしか書くことはないです。

次にGoogleMapですが、役割としては画像のホルダーとなっています。

public class GoogleMap {
    /** 緯度 */
    public var lng:Number = 43.0552480;
    /** 経度 */
    public var lat:Number = 141.3455050;
    /***/
    package var width:Integer = 400;
    /** 高さ */
    package var height:Integer = 300;
    /** ズーム */
    public var zoom:Integer = 14 on replace {
        updateImage();
    };
    public var enable:Boolean = false;
    public function setSize(width:Integer, height:Integer):Void {
        this.width = width;
        this.height = height;
        updateImage();
    }
    public var placemark:GeoService.Placemark on replace {
        if (placemark != null) {
            lng = placemark.lng;
            lat = placemark.lat;
            updateImage();
        }
    }
    var url:String = bind "http://maps.google.com/staticmap?center={lng},{lat}&markers={lng},{lat},rede&zoom={zoom}&size={width}x{height}";
    function updateImage() {
        if (not enable) return;
        var placeholder = image;
        image = Image {
            url: url
            width: width, height: height
            backgroundLoading: true
            placeholder: placeholder;
        }
    }
    package var image:Image = Image {
        url: url
    }
}

緯度、経度、画像サイズ、ズームなどのパラメータを持ち、変更時には画像を更新します。この時、backgroundLoadingをtrueにしておかなければ、読み込み待ちが発生しますので注意してください。尚、読み込み中の待ち受け画像はそのまま直前の画像を指定しておけばupdateImageで処理が止まらずにスムーズに動くでしょう。

最後にGeoServiceですが、これはおまけのようなもので、検索から緯度経度を取得し、Mapに渡すためのユーティリティクラスです。findLocation関数を使えば、検索をバックグラウンドで行い検索が終わったならばコールバック関数がよばれます。

public function findLocation(query:String, callback:function(placemarks:Placemark[]):Void):Void {
    GeoService {
        query: query
        onDone: callback
    }.start();
}

Placemark(位置情報)は複数返ることもあるので、リストに表示するなどの処理も考えられるでしょう。

def locationBase = "http://maps.google.com/maps/geo";
public class GeoService {
    public var query:String;
    public var onDone:function(placemarks:Placemark[]):Void;
    var placemarks:Placemark[];
    var placemark:Placemark;
    
    public function start() {
        HttpRequest {
            location: "{locationBase}?q={URLConverter{}.encodeString(query)}&output=xml";
            onInput: function(input:InputStream) {
                try {
                    PullParser {
                        documentType: PullParser.XML
                        input: input
                        onEvent: function(event:Event) {
                            if (event.type == PullParser.START_ELEMENT and event.qname.name == "Placemark") {
                                placemark = Placemark {};
                            } else if (event.type == PullParser.TEXT and event.qname.name == "address") {
                                placemark.address = event.text;
                            } else if (event.type == PullParser.TEXT and event.qname.name == "coordinates") {
                                var points = event.text.split(",");
                                placemark.lng = Number.parseFloat(points[1]);
                                placemark.lat = Number.parseFloat(points[0]);
                            } else if (event.type == PullParser.END_ELEMENT and event.qname.name == "Placemark") {
                                insert placemark into placemarks;
                                placemark = null;
                            } else  {
                            }
                        }
                    }.parse();
                } finally {
                    input.close();
                }
            }
            onDone: function() {
                onDone(this.placemarks)
            }
        }.start();
    }
}

GeoServiceによる検索はこの手のWEBサービスを使う場合のパターンです。HttpRequestでHTTPリクエストを投げ、PullParser でXML(またはJSON)をパースします。PullParserはイベントドリブンなパースクラスで、各イベントをハンドリングしながら、検索結果をplacemarksに詰めていくわけです。
これらの処理はHttpRequestのstartと同時にバックグラウンドで処理されるため、メインの処理はブロックされません。最後まで処理が終わったならば、onDone(コールバック関数)が呼ばれます。おそらくは大抵のWEBサービスはこんなコードになると思われます。

Sandbox

さて、実装はそんな所ですが、このスクリプトをAppletなどでWEBページに貼り付ける場合、Sandboxによるセキュリティの問題が発生します。JavaのSandboxモデルでは、そのプログラム(Applet)をダウンロードしたホストへのアクセスは可能ですが、他のホスト(ここではGoogle)へのアクセスは署名無しでは実行できません。悪意のあるプログラムを防ぎ、安全にブラウザ上で実行させるためのものですが・・・Googleへアクセスし地図の画像を取得することもダメです。
逆に言えば、ブラウザ上のJavaScript等よりもずっとセキュリティが高いわけなのですが、流石にこれでは使い勝手が悪いです。年間数万の証明書を購入して署名するか、オレオレ証明書でやる必要があります。しかし、オレオレ証明書の確認メッセージは「え・・、実行したくないな」と思わせる節があります。
WEBサービスへのアクセスとはいえセキュリティを重視するのか、それとも利便性を優先した方がいいのか・・・なんらかの動きがあるんでしょうかね。