AngularJSとOnsen UI で作るGoogle Mapsのサンプルアプリ
今回の記事は、Onsen UI blogで2月に公開した”Creating Google Maps Sample App with AngularJS and Onsen UI“の翻訳記事です。
以下のツールを使用して、このサンプルを構築します。
Onsen UI ( HTML5 を使用したハイブリッドアプリを作成するためのフレームワーク )
AngularJS ( Google 社が開発した JavaScript のフレームワーク )
Google Maps JavaScript API v3
ここでは、Monacaを使用して、アプリを開発します。Monaca は、クロウド型のアプリ開発環境です。このツールを使用すれば、マルチプラットフォーム ( iOS/Android/Windows8 ) に対応するハイブリッドアプリを開発できます。Monaca には、Onsen UI のフルサポート、テンプレートの提供など、開発を容易にしてくれる作り込みが多数してあります。これらの点が、今回、Monaca を使用して、開発を行う理由です。
このサンプルアプリに実装している機能は、上記ツールの解説に必要となる、最低限に抑えてあります。また、ここでは、複雑・大規模アプリの構築ではなく、拡張可能な、ひな形となるアプリの構築に焦点を当てています。よって、アプリ内の機能のいくつかは、表示されているだけで、機能しません。開発者側で自由に書き換えれるように、枠組みだけ、提供しています。
実装している機能を、以下に記します。
- マーカーの表示 ( 画面上で長押し )
- マーカーの削除 ( 1 件、画面上でタップ )
- マーカーの削除 ( 全件、<ons-button> を使用)
- マーカー間の距離 ( 2 点間の距離、総距離 ) の計算 ( <ons-button> を使用 )
- メニューの表示 ( <ons-sliding-menu> を使用 )
<ons-sliding-menu> を使用して、メインのインターフェイス ( ページ選択の画面 ) を配置し、その上に、map.html と settings.html へのリンクを置いています。map.html には、map 関連の要素と Onsen UI の要素 ( <ons-button> など ) を記述しています。スライティングメニュー上から、map 画面または settings 画面へ遷移できます。
上のサンプルアプリは、実際に操作できます。または、こちらのリンクからブラウザー表示も行えます。GitHub上にも、コードを置いていますので、開発者の環境で検証もできます。
また、上述の HTML コンテンツの他にも、controller.js と style.css の 2 つの重要な部品から、このアプリは構成されています。
コントローラー
コントローラーには、アプリのロジックを記述しています。このアプリには、2 つのコントローラーを使用しています。1 つは、ons-sliding-menu の制御用で、もう 1 つは、地図の制御用です。どちらのコントローラーも、controller.js 内に記述されています。
SlidingMenuController
ons-sliding-menu 要素と Google Maps 関連の要素を、同時に使用する際の問題点として、スクロール処理のイベントに、どちらも影響されることが挙げられます ( ここでは、水平方向のスワイプ )。よって、スライディングメニューのスワイプ時の挙動を、コントローラー側で制御する必要があります。
ここでは、この問題を解消するために、 ‘postopen’ または ‘postclose’ イベントの制御を行う要素に、リスナーを登録します。スライディングメニューのスワイプは、デフォルトでは、無効化されていますが、ons-toolbar-button をクリックして、メニューを開くと有効化され、メニューが閉じるまで ( スワイプまたは ons-toolbar-button の再実行 )、そのまま有効化されています。このリスナーは、スライディングメニューを初回に開いた後に、設定されます。
MapController
このコントローラーには、map オブジェクト関連のロジックと Onsen UI 関連のロジック ( Google Maps と Onsen UI との連携部分 ) を記述しています。最初の処理は、maker 用の配列の作成 ( 後から使用 ) と map 要素の作成です。ここでは、地図
の初期化時に、AngularJS の $timeout を使用して、地図の読み込みを、100ms ほど、遅延させます。DOM を読み込む前に、AngularJS のコントローラーの初期化が行われるため、この処理が必要となります。
地図の読み込み後、タップイベント ( マーカーの表示 ) に対して、リスナーを設定します。ここでは、Hammer.JS ライブラリーを使用します。Hammer.JS は、Onsen UI に、初めから組み込まれています。
地図の初期化後、上述した機能が使用でき ます。上述の機能には、それぞれ、対応するメソッドがあります ( $scope.addOnClick()、$scope.deleteAllMarkers()、$scope.calculateDistance() )。
$scope.addOnClick() は、上述したリスナーと紐付けされています。画面のクリック/タップ位置 ( X値、Y値 ) を取得して、経緯・緯度の座標に変換します。Google Maps では、このような形式でエンコード化が行われているため、この変換処理が必要となります。この処理には、以下のような関数を使用します。
$scope.overlay.getProjection().fromContainerPixelToLatLng(point);
地図を入れる div の左上の地点に、座標の原点があります ( 端末の画面の左上ではありません )。これが、Y 座標の取得時に、(-) 44px 分だけ、差し引く理由です ( 44px は、画面上部に置かれた <ons-toolbar> 要素の高さです )。座標の変換後、地図上に、マーカーを表示できます ( マーカー用の配列を使用 )。
次に、マーカーに対するクリック/タップに備えて、リスナーを設定します。マーカーが押された場合、ons.notification.confirm 要素を使用して、マーカーを削除するか否か、ユーザー側に確認します。次に、ユーザーの選択肢に応じて、ons.notification.alert 要素を表示します。
// controller.js
(function() {
var app = angular.module('myApp', ['onsen']);
//Sliding menu controller, swiping management
app.controller('SlidingMenuController', function($scope){
$scope.checkSlidingMenuStatus = function(){
$scope.slidingMenu.on('postclose', function(){
$scope.slidingMenu.setSwipeable(false);
});
$scope.slidingMenu.on('postopen', function(){
$scope.slidingMenu.setSwipeable(true);
});
};
$scope.checkSlidingMenuStatus();
});
//Map controller
app.controller('MapController', function($scope, $timeout){
$scope.map;
$scope.markers = [];
$scope.markerId = 1;
//Map initialization
$timeout(function(){
var latlng = new google.maps.LatLng(35.7042995, 139.7597564);
var myOptions = {
zoom: 8,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
$scope.map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
$scope.overlay = new google.maps.OverlayView();
$scope.overlay.draw = function() {}; // empty function required
$scope.overlay.setMap($scope.map);
$scope.element = document.getElementById('map_canvas');
$scope.hammertime = Hammer($scope.element).on("hold", function(event) {
$scope.addOnClick(event);
});
},100);
//Delete all Markers
$scope.deleteAllMarkers = function(){
if($scope.markers.length == 0){
ons.notification.alert({
message: 'There are no markers to delete!!!'
});
return;
}
for (var i = 0; i < $scope.markers.length; i++) {
//Remove the marker from Map
$scope.markers[i].setMap(null);
}
//Remove the marker from array.
$scope.markers.length = 0;
$scope.markerId = 0;
ons.notification.alert({
message: 'All Markers deleted.'
});
};
$scope.rad = function(x) {
return x * Math.PI / 180;
};
//Calculate the distance between the Markers
$scope.calculateDistance = function(){
if($scope.markers.length < 2){
ons.notification.alert({
message: 'Insert at least 2 markers!!!'
});
}
else{
var totalDistance = 0;
var partialDistance = [];
partialDistance.length = $scope.markers.length - 1;
for(var i = 0; i < partialDistance.length; i++){
var p1 = $scope.markers[i];
var p2 = $scope.markers[i+1];
var R = 6378137; // Earth’s mean radius in meter
var dLat = $scope.rad(p2.position.lat() - p1.position.lat());
var dLong = $scope.rad(p2.position.lng() - p1.position.lng());
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos($scope.rad(p1.position.lat())) * Math.cos($scope.rad(p2.position.lat())) *
Math.sin(dLong / 2) * Math.sin(dLong / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
totalDistance += R * c / 1000; //distance in Km
partialDistance[i] = R * c / 1000;
}
ons.notification.confirm({
message: 'Do you want to see the partial distances?',
callback: function(idx) {
ons.notification.alert({
message: "The total distance is " + totalDistance.toFixed(1) + " km"
});
switch(idx) {
case 0:
break;
case 1:
for (var i = (partialDistance.length - 1); i >= 0 ; i--) {
ons.notification.alert({
message: "The partial distance from point " + (i+1) + " to point " + (i+2) + " is " + partialDistance[i].toFixed(1) + " km"
});
}
break;
}
}
});
}
};
//Add single Marker
$scope.addOnClick =