bluemixでgwtアプリケーションを動かす
TRANSCRIPT
Bluemixで GWT
概要• GWTは、 Googleが提供しているクライアント・サイド用のツールキット
• Javaから JavaScriptに変換して実行するため、クライアントサイドもJavaで書くことができる
• 今回は、これを使って簡単なアプリケーションを作成して、 Bluemixにデプロイしてみます
おことわり
• 本資料の記載内容は、私が個人的に調べた内容であり、正式な日本 IBMのテストやレビューを受けておりません。内容について、できる限り正確を期すよう努めてはおりますが、いかなる明示または暗黙の保証も責任も負いかねます。本資料の情報は、使用先の責任において使用されるべきものであることを、あらかじめご了承ください。
前提• Java SE 7残念ながら、 Bluemixの runtimeは、まだ IBM JDK 7ベースなので、 Java8でそのままコンパイルすると動きません。 Java8を開発に使う場合用は、 -target 1.7、 -source 1.7を指定し、 Java8で追加されたライブラリを使わないように注意する必要があります。
• Eclipse LUNAEclipse IDE for Java EE Developersをインストールしておいてください。
• BluemixBluemixへのアカウント登録と、 Cloud Foundry CLIのインストールは終わっていることを前提とします。
• GWT紙面の都合上、 GWTの詳細については解説しませんので、適宜 GWTの解説書を参照してください。
GWTプラグインのインストール• Eclipseを起動して、プラグインをインストールする
• Help => Install New Software...
• ダイアログ右上の Add...をクリック
• 以下のように入力
GWTプラグインのインストール• 少し待つとインストール項目の選択になるので、以下のように選択する
あとはウィザードに従ってインストールを進めてください最後の Eclipseを再起動するか聞かれるので、再起動してください
Hello World
プロジェクト作成• まずは Hello Worldを確認します。
• Eclipseで、 File => New => otherを選びます
• 下のダイアログが表示されるので、 Google => Web Application Projectを選びます
プロジェクト作成• Nextを押して、以下のようにします (Use Google App Engineのチェックを外してください )
プロジェクト設定• Finishを押すとプロジェクトが出来ます。
• 既にサンプルが作成されているので、 Project Explorerで helloを右クリックして、 Run As => Web Application(GWT Super Dev Mode)を選びます。
• しばらくすると、 Development Modeビューに以下のように URLが表示されるので、ダブルクリックするとデフォルトのブラウザが開きます。
サンプルの起動• GWT Userのところに名前を入れて、 Enterを押すと、ダイアログが表示されます
Bluemixにデプロイ• 次に Bluemixにデプロイしてみます
• 前提で述べた通り、 2015/7現在、 Bluemix側の Javaは Java7なので、もしも手元の環境が Java8の場合は、 Project Explorerで、 helloを右クリックして Propertiesを選び、以下のように設定を変更します
Javaの設定• 同様に Facetを設定します
GWT compile
• GWT compileを実行しますこれによって、クライアント側の Javaのコードが JavaScriptに変換されます。 Project Explorerから helloを右クリックし Google => GWT Compileを選びます
GWT Compile
• Consoleにエラーが出ていないことを確認し、生成された場所を確認します
Bluemixにアップロード• cf loginを実行してログインしたら、 GWT Compileされた場所の 1つ上のディレクトリで cf pushします。
$ cd /Users/shanai/workspace/gwt/hello/war/hello$ cd ..$ lsHello.css Hello.html WEB-INF favicon.icohello$ cf push ruimohelloUpdating app ruimohello in org [email protected] / space dev as [email protected]... state since cpu memory disk details#0 running 2015-07-18 09:12:46 AM 0.0% 179.1M of 1G 228.2M of 1G
ここの名前は、 Bluemix内で他とぶつからない名前を指定します
Bluemixで実行• ダッシュボードで確認しますこ
こをクリック
実行中になっているのを確認
Bluemixで実行
地図アプリケーションの作成
地図アプリケーション• ある程度インタラクティブで、かつクライアント側の JavaScriptとの連携が必要なアプリケーションが例としてふさわしいので、簡単な地図アプリケーションを作成します現在地と周辺地
図が表示される地図をクリックすると、周辺施設が表示される
選んだ施設が右に追加される
施設をクリックするとピンが表示さ
れる
全画面アプリケーション• 最初に作成した Hello, Worldアプリケーションは、画面の一部に GWTの動的コンテンツを埋め込んでいました。
<table align="center"> <tr> <td colspan="2" style="font-weight:bold;">Please enter your name:</td> </tr> <tr> <td id="nameFieldContainer"></td> <td id="sendButtonContainer"></td> </tr> <tr> <td colspan="2" style="color:red;" id="errorLabelContainer"></td> </tr></table>
Hello.html
public void onModuleLoad() { final Button sendButton = new Button("Send"); final TextBox nameField = new TextBox(); nameField.setText("GWT User"); final Label errorLabel = new Label();
sendButton.addStyleName("sendButton");
RootPanel.get("nameFieldContainer").add(nameField); RootPanel.get("sendButtonContainer").add(sendButton); RootPanel.get("errorLabelContainer").add(errorLabel);
Hello.java
全画面アプリケーション• 今回は画面全体を GWTで描画します。 GWTのレイアウト・パネルを使うことで画面サイズの変更に柔軟に対応することができます。
RootPanel
DockLayoutPanel
HTML(text)
SplitLayoutPanel
GoogleMap
DockLayoutPanel
更新
DataGrid
施設リスト
プロジェクト作成• それでは、 Hello, World作成時同様にプロジェクトを作成します。今回はmymapという名前にしました。
画面の雛形• 画面の雛形を作成します。
• src/mymap/ client/Mymap.javaのonModuleLoad()を右のように変更します。
RootPanelDockLayoutPanel
SplitLayoutPanel
public void onModuleLoad() { DockLayoutPanel dlp = new DockLayoutPanel(Style.Unit.EM); dlp.addStyleName("rootLayout"); dlp.addNorth(new HTML("My application"), 2);
SplitLayoutPanel slp = new SplitLayoutPanel(); slp.addEast(new HTML("施設リスト "), 120); slp.add(new HTML("<div id='map-canvas'></div>"));
dlp.add(slp); RootPanel.get().add(dlp);}
施設リスト
My application
<div id='map-canvas'></div>
画面の雛形• 一方、war/Mymap.htmlの方は、<body>内の h1や tableタグを削除します。
<body> <!-- OPTIONAL: include this if you want history support --> <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe> <!-- RECOMMENDED if your web app will not function without JavaScript enabled --> <noscript> <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif"> Your web browser must have JavaScript enabled in order for this application to display correctly. </div> </noscript> </body>
画面の雛形• Mymap.cssの内容は、元あったものを全て削除して以下のようにします。
これで全画面アプリケーションになりますhtml, body { height: 100%; margin: 0px !important;}
.rootLayout { height: 100%; width: 100%;}
GWT標準の cssが、 bodyにマージンを設定してしまうので、これで削除しないと、余計なスクロールバーが出てしまう
画面の確認• ここまでできたら、 Hello, Worldの時と同様に、 Run As => Web
Application (Gwt Super Dev Mode)で起動して画面を確認します。 GWTのレイアウト・パネルのおかげでリサイズしても画面の構成が維持されることが確認できます。
マウスで境界をドラッグ可能
コード変更時の注意• htmlや cssを修正した場合ブラウザをリロードしてください。それでも表示されない場合は、 1つ下の「クライアント側の Javaコードを修正した時」の方法を実行
• クライアント側の Javaコードを修正した時ブラウザ右下のアイコンをクリック
• サーバ側の Javaコードを修正した時
サーバをリロードします
地図を表示する• それでは地図を表示してみましょう。
• Google Map APIの使用には設定が必要ですhttps://console.developers.google.com/ にアクセスして自分のGoogleアカウントでログインしてください。
• 以下のようにして、 Google Mapを有効にします。
API Keyを取得する• 以下の手順で API Keyを取得します。
今回は開発用なのでリファラ制限を指定していませんが、公開する際にはリファラの設定をし
てください。無料で使用できるのは、現在
1000リクエスト / 日までなので、キーが漏れて他の人に使われてしまうと APIが使えなくなって
しまいます。(Bluemix上にデプロイした際の
URLを指定 )
API Keyを保存
メモしておきます
Google Mapの組込み<script type="text/javascript" language="javascript" src="mymap/mymap.nocache.js"></script><script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=xxxxxxxxxxxxxxxx"></script><script type="text/javascript"> function initialize() { currentLoc(function(latitude, longitude) { showMap(latitude, longitude); }); }
続く ...
war/Mymap.htmlにscriptタグを追加します。ここに API Keyを指定し
ます初期化メソッド。 currentLoc()
で現在地を取得して、 showMap()で地図を表示
( どちらも次頁で解説 )
Google Mapの組込み前頁から続き
function currentLoc(f) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function(pos) { f(pos.coords.latitude, pos.coords.longitude); }, function(e) { alert(' 現在地の取得が許可されていません。 '); } ); } else { alert(' 現在地が取得できません。 '); } }
function showMap(latitude, longitude) { map = new google.maps.Map(document.getElementById('map-canvas'), { center: new google.maps.LatLng(latitude, longitude), zoom: 15 }); }</script>
navigatorを使って現在地を取得するメ
ソッド
指定された場所の地図を表示
これは、地図を表示する divタグの id 属性 (Mymap.javaと
対応 )
GWT側から JavaScriptを呼び出し• あとは、この initialize() メソッドを呼び出せば okです。通常であれば
ページの onloadなどをトリガーにすれば良いのですが、今回は、 GWT側の初期化が終わってから呼ばれるようにします。
• GWTは、 Javaを JavaScriptに変換する仕組みですが、生のJavaScriptとやりとりするための仕組みが用意されています。
• Mymap.javaを以下のように変更します。public class Mymap implements EntryPoint { native void initialize() /*-{ $wnd.initialize(); }-*/;
public void onModuleLoad() { initialize(); DockLayoutPanel dlp = new DockLayoutPanel(Style.Unit.EM); dlp.addStyleName("rootLayout"); dlp.addNorth(new HTML("My application"), 2);
このように特殊なタグで囲むと、 JavaScriptを直接書くことができる。$wndを指定すると、Window オブジェ
クトを取得できるので、これでMymap.html側の initialize()を呼び出せ
る
地図の表示• 最後に、Mymap.cssを変更して地図がパネル内にフィットするようにします。#map-canvas, .rootLayout {
height: 100%; width: 100%;}
アプリケーションを実行すると、以下のように現在地の地図が表示されます
この状態のアプリケーションは、mymap00.zipで公開されてい
ます
施設情報の表示
緯度、経度から施設情報を取得• 地図がクリックされたら、クリックされた場所をもとに緯度、経度を取得して、その周辺の施設情報を取得します。
• これは、 Googleの Places APIで取得できます。Mapの時と同じように以下のようにして APIを有効化します。
施設情報の取得• Places APIは以下のような GETリクエストを送ることで、 JSONで情報を取得できます。https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=35.587981,139.663820&radius=500&key=xxxxxxxxxxxxxxxx
• locationパラメータが緯度、経度
• radiusが半径
• keyには、Mapの時と同じキーを指定
• GETなので普通にブラウザでアクセスすれば、 JSONが表示されます。
施設情報の取得{... "results" : [ { "geometry" : { "location" : { "lat" : 35.5887497, "lng" : 139.674198 },... }, "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png", "id" : "dc3f145c15f68b3fa5f0b3a41d59f819f3975dd1", "name" : " 田園調布1丁目 ", "place_id" : "ChIJpUK62EH1GGARS_Z2KSq8N88",... }, { "geometry" : { "location" : { "lat" : 35.587554, "lng" : 139.668713 } },... "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", "id" : "10fbe9518e5a11444c7b7c358eb0dd270e0ab6ee", "name" : " 多摩川浅間神社 ",
位置と施設のセットが複数返ってくる
SOP(Same Origin Policy)
• GWTで他のサーバに httpリクエストを上げようとすると、 SOPに引っかかります。
• GWTは Javaで書いたコードを JavaScriptに変換して、ブラウザで動かす仕組みなので、ホストページ (今回なら、Mymap.html)を取得したサーバとは別のサーバに httpリクエストすることは直接にはできません。
• これを回避する方法は幾つかあるのですが、ここではもっとも無難と思われる、自分のサーバを経由する方法を使います。
SOP
ブラウザ自分のサーバ
Mymap.html
JavaScript
Google Places
ブラウザ自分のサーバ
Mymap.html
JavaScript
Google Places
直接の相手が、Mymap.htmlを取得したサーバと同一なら ok
サーバキーを取得する• Googleのデベロッパコンソールで、今度はサーバキーを取得します
サーバキーを取得する
今回はテスト用なので、 IPアドレス制限はしません。本稼動する際は IPアド
レスを指定してください。
サーバキーを取得する
メモする
キーを管理するクラスを作る• 今のようにキーを htmlファイルなどに埋め込んでいると、キーを変更したい時に面倒ですし、うっかりキーが漏れてしまう危険性もあります。
• 今回は Bluemix(Cloud Foundry)を使うこともあるので、キーは環境変数でアプリケーションに渡すようにします。
• そのためにキーを管理するクラスを作成しておきます。
キーを管理するクラスpackage mymap.client;
import java.io.Serializable;import static java.util.Objects.requireNonNull;
public class Settings implements Serializable { private static final long serialVersionUID = -6808664382199552658L;
String apiKey; String serverApiKey;
Settings() { apiKey = ""; serverApiKey = ""; } public Settings(String apiKey, String serverApiKey) { this.apiKey = requireNonNull(apiKey, "apiKey"); this.serverApiKey = requireNonNull(serverApiKey, "serverApiKey"); }
public String getApiKey() { return this.apiKey; } public String getServerApiKey() { return this.serverApiKey; }}
サーバ、クライアント間でやりとりできるように直列化可能と
する
GWTの直列化の制限により、デフォルトコンストラクタを用意 (アプリケーションからは使
えないようにパッケージプライベートに )。フィールドも残念ながら GWTの制限で final
宣言できないので注意
nullでないことが保証されているイ
ミュータブルクラス
Settingsのファクトリpackage mymap.server;
import static java.util.Objects.requireNonNull;import mymap.client.Settings;
public class SettingsFactory { static final SettingsFactory THE_INSTANCE = new SettingsFactory(); final Settings settings;
public SettingsFactory() { settings = new Settings (requireNonNull(System.getenv("apiKey"), "Set Google api key by setting environment variable 'apiKey'"), requireNonNull(System.getenv("serverApiKey"), "Set Gooogle api server key by setting environment variable 'serverApiKey'")); } public Settings getSettings() { return settings; }
public static SettingsFactory getInstance() { return THE_INSTANCE; }}
Settingsのファクトリクラス
環境変数 apiKey/serverApiKeyから取得
GWT RPCを作成• 今回は開発の手間を減らすため、クライアント・サーバ間の通信に、 GWT RPCを使います。
• JSONのパースがどこかで必要ですが、残念ながら Javaは標準ではJSONのパーサが無く、ライブラリの追加など面倒なので、今回はサーバからは JSONを文字列でそのまま返して、クライアント側でパースすることにします。
• まずサーバとのインターフェイスを作成します。package mymap.client;
import com.google.gwt.user.client.rpc.RemoteService;import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("places")public interface PlacesService extends RemoteService { String places(double latitude, double longitude);}
緯度、経度を受け取って、結果を JSON 文字列で返す
Asyncインターフェイスの生成すると、 Eclipseで Asyncインターフェイスを作るか聞かれるので、電球をクリックして作成しま
す
Asyncインターフェイスの作成
そのまま Finishで作成
package mymap.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface PlacesServiceAsync { void places(double latitude, double longitude, AsyncCallback<String> callback);}
クライアント側は、このインターフェイスで呼び出すことになりま
す
サーバ間通信• サーバ間通信は Java標準の HttpURLConnectionを使いますが、資源
管理を楽にするためヘルパクラスを作っておきます。
package mymap.server;
import java.net.HttpURLConnection;import java.net.URI;
public abstract class WithUrlConnection<T> { public T process(String url) throws Exception { URI uri = new URI(url); HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection(); try { return perform(conn); } finally { conn.disconnect(); } }
abstract protected T perform(HttpURLConnection conn) throws Exception;}
典型的なテンプレート・メソッドパターンで資源管理を行うだ
け
Places APIの呼び出しpackage mymap.server;
import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.HttpURLConnection;
import mymap.client.PlacesService;import mymap.client.Settings;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
@SuppressWarnings("serial")public class PlacesServiceImpl extends RemoteServiceServlet implements PlacesService { static final Settings settings = SettingsFactory.getInstance().getSettings(); @Override public String places(double latitude, double longitude) { final String url = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=" + latitude + "," + longitude + "&radius=200&key=" + settings.getServerApiKey();続く
PlacesServiceを実装
Settingsを取得
キーを埋め込んだ Places APIのURL
Places APIの呼び出し try { return new WithUrlConnection<String>() { @Override protected String perform(HttpURLConnection conn) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); copy(conn.getInputStream(), baos);
return new String(baos.toByteArray(), "utf-8"); } }.process(url); } catch (Exception e) { e.printStackTrace(); return null; } }
void copy(InputStream is, OutputStream os) throws IOException { byte[] buf = new byte[4096]; int readLen; while ((readLen = is.read(buf)) != -1) { os.write(buf, 0, readLen); } }}
さきほどのヘルパクラスでPlaces APIをリクエストし、レスポンスを utf-8 文字列とし
て返す本番ではエラーレスポンスを返すなりしてくださ
い
Mapのクリックを検知する• サーバ側ができたので、今度はクライアント側を作成します。だいたい以下のような流れになります。• まずMapのクリックを検知できるようにします。• クリックされたら、先ほど作った PlacesServiceを使って施設情報を取得します。
• 取得されたら施設情報をクライアントにポップアップします。
ロギング設定を追加しておく• そろそろプログラムが複雑になってきたので、クライアント側にログを追加しておきます。 src/mymap/Mymap.gwt.xmlを変更します。<?xml version="1.0" encoding="UTF-8"?><!-- When updating your version of GWT, you should also update this DTD reference, so that your app can take advantage of the latest GWT module capabilities.--><!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.6.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.6.0/distro-source/core/src/gwt-module.dtd"><module rename-to='mymap'> <!-- Inherit the core Web Toolkit stuff. --> <inherits name='com.google.gwt.user.User'/> <inherits name="com.google.gwt.logging.Logging"/>... <!-- allow Super Dev Mode --> <add-linker name="xsiframe"/> <set-property name='gwt.logging.popupHandler' value='DISABLED'/></module>
追加
これを指定しないと、アプリケーション画面にオーバーレイでロギングが表示されて、わずらわしい
クリックの検知package mymap.client;
import java.util.logging.Level;import java.util.logging.Logger;
...public class Mymap implements EntryPoint { Logger logger = Logger.getLogger("MymapLogger"); private final PlacesServiceAsync placesService = GWT .create(PlacesService.class);
native void initialize() /*-{ var that = this; $wnd.onMapClicked = function(latitude, longitude) { [email protected]::onMapClicked(DD)(latitude, longitude); }; $wnd.initialize(); }-*/;
public void onModuleLoad() { logger.log(Level.INFO, "Application start"); initialize();... }
public void onMapClicked(double latitude, double longitude) { logger.log(Level.INFO, "Map clicked on (" + latitude + ", " + longitude + ")"); }}
ロギングのimport
ロガー生成Placesの RPCは、このようにして生成
する
html側に、 onMapClicked変数を用意して、そこに、下の onMapClicked 関数を代入しておく (詳細は後
述 )。
クリックの検知• html側を変更して、Mapのクリックを検出できるようにする
function showMap(latitude, longitude) { map = new google.maps.Map(document.getElementById('map-canvas'), { center: new google.maps.LatLng(latitude, longitude), zoom: 15 }); google.maps.event.addListener(map, 'click', function(e) { onMapClicked(e.latLng.lat(), e.latLng.lng()); }); }
native void initialize() /*-{ var that = this; $wnd.onMapClicked = function(latitude, longitude) { [email protected]::onMapClicked(DD)(latitude, longitude); }; $wnd.initialize(); }-*/;
addListenerでクリックのリスナを登
録
onMapClickedは、 GWT側で登録し
たもの
JavaScriptから、 Java側を呼びたい時は、@ 完全修飾クラス名 :: メソッド名 ( 引数のシグニチャ )( 引数 )を指定する。このシグニチャは、バイトコード
での指定方法と同じ。詳細は以下の 4.3.2, 4.3.3を参照 (http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-
4.html)
実行してみる
Chromeの開発者ツールや、 Firefoxの Firebugの
Consoleでアプリケーションのログを確認できる。
地図をクリックすると緯度、経度がログされる
クライアント側のデバッグ• Chromeの開発者ツールではクライアント側を Javaでデバッグ (?)できる
ここでブレークポイントを置いたり、変数をインスペクトしたりで
きる
Map APIのキーも埋め込まないように• Mymap.htmlは、Mymap.jspにして、 key部分は Settingsから取るようにする。Mymap.htmlをMymap.jspにコピーして以下を変更する
<%@ page import="mymap.server.SettingsFactory"%><!doctype html> <script type="text/javascript" language="javascript" src="mymap/mymap.nocache.js"></script> <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<%= SettingsFactory.getInstance().getSettings().getApiKey() %>"> </script>
• web.xmlのwelcome ページを変更する <welcome-file-list> <welcome-file>Mymap.jsp</welcome-file> </welcome-file-list>
実際は 1行で
実行設定を変更する
Eclipseの Run => Run Configurations...
で、 Argumentsを変更。
Environmentで環境変数apiKey/serverApiKeyにキーを指
定
実行してみる• サーバ側を変更しているので、一度 Eclipse上で停止してから再実行する。
• 地図クリックで、ブラウザの Consoleにクリック位置がログされれば、ここまでの動作は ok。
この状態のアプリケーションは、mymap01.zipで公開されてい
ます
Places APIの結果を表示する
Places APIを呼び出す• まず施設のモデルクラスを作成します
package mymap.client;
import static java.util.Objects.requireNonNull;
import com.google.gwt.json.client.JSONObject;import java.io.Serializable;
public class Place implements Serializable { private static final long serialVersionUID = 286987229306285007L; private String name; private double latitude; private double longitude;
Place() { name = ""; latitude = 0; longitude = 0; }
このあたりの GWT 特有の注意は、Settingsの時と同じ
Places APIを呼び出す
続き
public Place(String name, double latitude, double longitude) { this.name = requireNonNull(name); this.latitude = latitude; this.longitude = longitude; } public String getName() {return name;} public double getLatitude() {return latitude;} public double getLongitude() {return longitude;}
public static Place parse(JSONObject placeJson) { JSONObject geo = placeJson.get("geometry").isObject(); JSONObject loc = geo.get("location").isObject();
return new Place (placeJson.get("name").isString().stringValue(), loc.get("lat").isNumber().doubleValue(), loc.get("lng").isNumber().doubleValue()); }}
イミュータブルクラス
GWTが提供しているJSONパーサで施設名と
位置を取り出す
Places APIの呼び出し public void onMapClicked(double latitude, double longitude) { logger.log(Level.INFO, "Map clicked on (" + latitude + ", " + longitude + ")");
placesService.places (latitude, longitude, new AsyncCallback<String>() { public void onFailure(Throwable t) { logger.log(Level.SEVERE, "Cannot get places.", t); } public void onSuccess(String json) { List<Place> places = getPlaces(json); logger.log(Level.INFO, "Got places. count = " + places.size()); } }); } List<Place> getPlaces(String placesApiJsonResponse) { JSONObject jo = JSONParser.parseStrict(placesApiJsonResponse).isObject(); JSONArray results = jo.get("results").isArray(); List<Place> places = new ArrayList<>(); for (int i = 0; i < results.size(); ++i) { places.add(Place.parse(results.get(i).isObject())); }
return places; }
正常時と異常時の処理を実装する
JavaScriptからの RPC 呼び出しは非同期に行うので、このようにコールバックを渡す
Places APIが返したJSONの中から
resultsの項目をパース
web.xmlにサービスを登録 <servlet> <servlet-name>placesServlet</servlet-name> <servlet-class>mymap.server.PlacesServiceImpl</servlet-class> </servlet> <servlet-mapping> <servlet-name>placesServlet</servlet-name> <url-pattern>/mymap/places</url-pattern> </servlet-mapping>
既存の greetServletは不要なので削除して上のように書き換える。あるいは余力のある人は、Servlet 3.0のアノテーションにしても良いで
しょう。
実行してみる
このように Places APIから返ってきた件数がログされれば ok
施設をポップアップする
施設のポップアップ• これまでは、こういうケースはダイアログで施設の一覧を表示するのが一般的だったように思いますが、今はポップアップで表示するのがはやりのようなので、今回もポップアップします。ポップアップは、ポップアップ外の領域のクリックで消すことができます。
• GWTには PopupPanelというパーツが用意されているので、ポップアップの生成自体は簡単です。ただ Java 7なので GUIの構築コードが少々長いです ...
ポップアップの構造PopupPanel
DockLayoutPanel
閉じる
DataGrid
施設一覧
ポップアップの生成• さきほどの、 Places API 呼び出しの onSuccess()側にポップアップ生成処理を実装します。public void onSuccess(String json) { List<Place> places = getPlaces(json); final PopupPanel pp = new PopupPanel(true); final Button closeButton = new Button(" 閉じる "); closeButton.addStyleName("closeButton");
DataGrid<Place> ct = new DataGrid<Place>(); TextColumn<Place> nameColumn = new TextColumn<Place>() { @Override public String getValue(Place p) { return p.getName(); } }; ct.addStyleName("placesDataGrid"); ct.addColumn(nameColumn, " 名称 "); ct.setRowData(0, places);続く
DataGridは、各行の表示するモデルクラスを型パラメータで指定す
る
DataGridの列の定義。モデルクラスの何を表示するかを実装 (この場合は
名称 )
列のヘッダ
DataGridにデータを設定。第一引数は、設定を開始する行位置 (0なの
で先頭 )
ポップアップの生成續き final SingleSelectionModel<Place> selectionModel = new SingleSelectionModel<Place>(); ct.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { Place selected = selectionModel.getSelectedObject(); if (selected != null) { addMyPlace(selected); pp.hide(); } } }); DockLayoutPanel dl = new DockLayoutPanel(Unit.EM); dl.addNorth(closeButton, 2); dl.add(ct); pp.setWidget(dl);
closeButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { pp.hide(); } }); pp.setPopupPosition(30, 30); pp.show();
logger.log(Level.INFO, "onMapClicked ended.");}
void addMyPlace(Place newPlace) { // 施設の登録 }
DataGridの行をクリックした時の処理を実装 (addMyPlace()で施設を登録する
(後述 ))
ポップアップは、ポップアップ外のクリックでも消せるが、ボタンも配置して
おく
このあと実装する
ポップアップの cssを調整• ポップアップが正しく表示されるように war/Mymap.cssを調整する
.gwt-PopupPanel { height: 80%; width: 90%;}
.popupContent, .placesDataGrid { height: 100%; width: 100%;}
.popupContent > div { height: 100%; width: 100%;}
実行してみる
クリックした場所周辺の施設が一覧されるようになった。閉じるボタンか、ポップアップ外のクリックで消すことができる
この状態のアプリケーションは、mymap02.zipで公開されてい
ます
選んだ施設をサーバに登録する
施設登録用の RPCを作成• PlacesServiceに APIを追加する。
public interface PlacesService extends RemoteService { String places(double latitude, double longitude); void addMyPlace(Place newPlace);}
すると、 Async側と不整合があるというエラーになるので、電球をクリックして Async
側を生成する
施設登録用の RPCを作成
同様に、 PlacesSerivceImplもエラーになるので電球をクリックして、 Add
unimplemented methodsを選ぶ
施設登録用の RPCを作成• サーバ側は、とりあえずメモリに保持するだけの実装にします ( 将来はデータベースに保管 )
public class PlacesServiceImpl extends RemoteServiceServlet implements PlacesService { static final Settings settings = SettingsFactory.getInstance().getSettings(); static final ArrayList<Place> places = new ArrayList<>();
... @Override public void addMyPlace(Place newPlace) { synchronized (places) { places.add(newPlace); } }}
とりあえずメモリに保持する仮実装
RPCを呼び出すpublic class Mymap implements EntryPoint {... void addMyPlace(Place newPlace) { placesService.addMyPlace (newPlace, new AsyncCallback<Void>() { public void onFailure(Throwable t) { logger.log(Level.SEVERE, "Cannot add my place.", t); }
public void onSuccess(Void v) { updateEastContent(); } }); }
void updateEastContent() { // サーバから登録施設を取り出して表示する。 logger.log(Level.INFO, "Update place list."); }}
addMyPlaceで RPCを実行
ここで登録施設を表示する
実行してみる
施設リストの更新処理の開始まで実行されれば ok
登録施設の表示
登録施設の表示• 登録施設の表示は、以下のようになります。
• 現在地図に表示されている緯度、経度の情報から、その中に含まれる施設のみを選んで取り出します。
• これを画面右側の領域に一覧として表示します。• 現在の GWTの DataGridは、 0 件の場合に更新中のアニメーションが表示されたままになってしまうバグがあるようなので、 0 件の場合は DataGridのかわりに、「結果が無い」というテキストを表示するようにします。
地図領域の取得• 地図領域の取得のために、Mymap.jspの中で、mapに
bounds_changedイベントのリスナを登録します
<script type="text/javascript"> var currentMapBounds = [0, 0, 0, 0];
function initialize() {... google.maps.event.addListener(map, 'click', function(e) { onMapClicked(e.latLng.lat(), e.latLng.lng()); }); google.maps.event.addListener(map, 'bounds_changed', function() { currentMapBounds[0] = map.getBounds().getNorthEast().lat(); currentMapBounds[1] = map.getBounds().getNorthEast().lng(); currentMapBounds[2] = map.getBounds().getSouthWest().lat(); currentMapBounds[3] = map.getBounds().getSouthWest().lng(); }); } </script> これで、Map 領域が変更された時に自動的に、
currentMapBoundsが更新されます
領域から登録施設取得 RPCの作成• これまで同様に、 RPCに APIを追加します。
public interface PlacesService extends RemoteService { String places(double latitude, double longitude); List<Place> myPlaces(double latitude0, double longitude0, double latitude1, double longitude1); void addMyPlace(Place newPlace);}
public class PlacesServiceImpl extends RemoteServiceServlet implements PlacesService {... @Override public List<Place> myPlaces(double latitude0, double longitude0, double latitude1, double longitude1) { synchronized (places) { return new ArrayList<>(places); } }} 今回もとりあえず現在メモリに保持し
ている登録施設を全て返す簡易実装
施設一覧領域を作成する• Mymap.javaに施設一覧領域を作成します。
public class Mymap implements EntryPoint {... DockLayoutPanel eastContentPanel = new DockLayoutPanel(Unit.EM) { Widget primaryChild; @Override public void add(Widget widget) { if (primaryChild != null) { remove(primaryChild); primaryChild = null; } super.add(widget); this.primaryChild = widget; } }; Button refreshEastContentButton = new Button(" 更新 ");... public void onModuleLoad() { logger.log(Level.INFO, "Application start"); refreshEastContentButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { updateEastContent(); } }); eastContentPanel.addNorth(refreshEastContentButton, 2);
initialize();... }}
0 件の時に DataGridとテキスト表示を切り替えたいので DockLayoutPanelの子ウィジェットを置き換えたいが、そういうメソッドが無いので継承して拡張す
る
登録施設一覧の更新処理をボタンに登録
施設一覧領域を作成する
public void onModuleLoad() {... initialize(); DockLayoutPanel dlp = new DockLayoutPanel(Style.Unit.EM); dlp.addStyleName("rootLayout"); dlp.addNorth(new HTML("My application"), 2);
SplitLayoutPanel slp = new SplitLayoutPanel(); slp.addEast(mainEastContent(), 120); slp.add(new HTML("<div id='map-canvas'></div>"));... }... Widget mainEastContent() { updateEastContent(); return eastContentPanel; }
native double[] getCurrentMapBounds() /*-{ return $wnd.currentMapBounds; }-*/;}
これまで固定文字列を入れていた施設一覧を、ここで作成
更新処理をしてから、施設一覧のパネルを返す
Mymap.jsp側のcurrentMapBoundsを取得
施設一覧領域を作成する• 施設一覧の更新処理です。
void updateEastContent() { logger.log(Level.INFO, "Update place list."); double[] cmb = getCurrentMapBounds(); if (cmb == null) return; placesService.myPlaces (cmb[0], cmb[1], cmb[2], cmb[3], new AsyncCallback<List<Place>>() { public void onFailure(Throwable t) { logger.log(Level.SEVERE, "Cannot get my places.", t); }続く
現在の地図領域で登録施設を検索
施設一覧領域を作成する public void onSuccess(List<Place> places) { if (places.isEmpty()) { eastContentPanel.add(new HTML(" レコード無し ")); } else { DataGrid<Place> myPlaces = new DataGrid<Place>(); TextColumn<Place> nameColumn = new TextColumn<Place>() { @Override public String getValue(Place p) { return p.getName(); } }; myPlaces.addColumn(nameColumn, " 名称 "); myPlaces.setRowData(0, places); final SingleSelectionModel<Place> selectionModel = new SingleSelectionModel<Place>(); myPlaces.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { Place selected = selectionModel.getSelectedObject(); // TODO: 施設がクリックされた時の処理 } }); eastContentPanel.add(myPlaces); } } });}
結果 0 件なら「レコード無し」表示
このあたりの DataGridの扱いはポップアップの時と
同様
登録施設がクリックされた時の処理は今後ここに実装
実行してみる
地図クリックでポップアップから施設を選ぶと、ここに登録されていく
この状態のアプリケーションは、mymap03.zipで公開されてい
ます
登録施設がクリックされたら、ピンを立てる
ピンを立てる• 登録施設一覧から施設がクリックされたら、地図上にピンを立ててみましょう。まずMymap.jspにピンを立てるメソッドを用意します。
var map; var currentPin = null;...
function pinPlace(name, latitude, longitude) { if (currentPin !== null) { currentPin.setMap(null); currentPin = null; }
var latlng = new google.maps.LatLng(latitude, longitude); currentPin = new google.maps.Marker({ position: latlng, map: map, title: name }); map.setCenter(latlng); }
function initialize() {
後でピンを消す時にはピンのインスタンスが必要になるので保存してお
く
既にピンが立っていれば消す
指定位置にピンを立てる
ピンを立てる• 登録施設一覧クリック処理を追加します。
void updateEastContent() {... selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { Place selected = selectionModel.getSelectedObject(); if (selected != null) { pinPlace(selected); } } });... }
native void pinPlace(Place place) /*-{ $wnd.pinPlace([email protected]::getName()(), [email protected]::getLatitude()(), [email protected]::getLongitude()()); }-*/;}
ピン立て処理を呼び出す
Mymap.jsp側の処理を呼び出す
実行してみる
登録施設一覧をクリックすると
ピンが立つようになった
現在地にもピンを立てる
現在地にもピンを立てる• 現在地にもピンが無いと分かりにくですね。現在地には別の色のピンを
立てましょう。これはMymap.jsp側の変更だけで実現できます。 var currentLocPin = null;... function pinCurrentLoc() { if (currentLocPin != null) { currentLocPin.setMap(null); currentLocPin = null; }
currentLoc(pinCurrentLocAt); } function pinCurrentLocAt(latitude, longitude) { var latlng = new google.maps.LatLng(latitude, longitude); currentLocPin = new google.maps.Marker({ position: latlng, map: map, title: '現 ', icon: 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=現 |00FFFF|000000' }); }
function showMap(latitude, longitude) {... pinCurrentLocAt(latitude, longitude); setInterval(pinCurrentLoc, 5000); }
5 秒に 1回、現在地のピンを更新
少し違うピンを立てます (後述 )
現在地ピンも、後で消せるように保存
違う色のピン• ピンには引数でビットマップを渡せるのですが、ビットマップを手で作るのは面倒です。 Googleの Dynamic Iconというサービスを使うと、引数を渡せば作ってくれます。 https://developers.google.com/chart/image/docs/gallery/dynamic_icons
• 今回は次のように指定していますhttp://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=現 |00FFFF|000000「現」がピン内の文字、その後が背景色 (シアン )、その後が文字色( 黒 )です
実行してみる
現在地も分かるようになりました
この状態のアプリケーションは、mymap04.zipで公開されてい
ます
Bluemixで確認• 最後に Bluemixで確認します。
• 方法は Hello Worldの時と全く同じです。 Eclipseで GWT Compileをしてから、 cf pushで uploadしてください。
• ただし既に述べた通り API keyを環境変数で設定する必要があります。
ダッシュボードで、アイコン部分をクリックします (今回は
ruimomapという名前で cf pushしました )
環境変数の設定
環境変数の設定
保存を押すと、設定されてアプリケーションも再起動されます
これまでと同じ画面が表示されれば成功です。お疲れ様でした !DBアクセスも入れたかったのですが紙面が尽きました。次の資料に入れたいと思
います
サンプル
サンプル• サンプルは以下の場所で取得できます http://www.ruimo.com/
bluemix/samples/gwt/
• mymap00.zip地図を表示する (war/Mymap.html 内の TODO の場所に、自分の API Keyを指定してください )
• mymap01.zip地図のクリックで、ブラウザのコンソールに位置がログされる。自分の API Key を、 Eclipse の実行構成に設定する必要があります。詳細は本文を参照。
• mymap02.zip施設一覧のポップアップを実装。
• mymap03.zip施設を登録できるように。
• mymap04.zipピンを立てるように。