Corredor

ウェブ、プログラミングの勉強メモ。

Cordova アプリでサクサク動く Google Map を実現する「cordova-plugin-googlemaps」

Google API Expertが解説する Google Maps APIプログラミングガイド

Google API Expertが解説する Google Maps APIプログラミングガイド

  • 作者: 勝又雅史,古籏一浩,石丸健太郎,安藤幸央
  • 出版社/メーカー: インプレス
  • 発売日: 2011/12/02
  • メディア: 単行本(ソフトカバー)
  • 購入: 2人 クリック: 69回
  • この商品を含むブログ (8件) を見る

cordova-plugin-googlemaps というプラグインを使うと、Cordova アプリ上に Google マップを表示でき、柔軟にカスタマイズすることができる。

API キーの取得などでちょっとつまづいたので丁寧に紹介。

今回も iOS 向けのサンプルアプリを以下に実装したので、feat/googleMaps ブランチのソースを見てみてもらいたい。実際に動作させる際には、後述の API キーの設定が必要になる。

Google Maps API キーを取得する

まずは、iOS アプリから Google Maps にアクセスする許可をもらうため、API キーというものを Google で発行する。

上記のページの「キーを取得する」ボタンから取得できる。細かな取得方法などは以下のサイトなどを参考のこと。

API キーを取得する際は、「キーの制限」で「iOS アプリ」を選択し、「リクエストを受けるアプリのバンドル ID」に config.xml に記載のアプリ識別子を設定する必要がある。<widget id="【ココがアプリ識別子】" 部分と一致させること。

登録できると、以下の URL で登録した API キーの参照・編集ができるはずだ。生成されたランダムな文字列が API キーになるので、これを控えておく。

プラグインのインストール

先に API キーの取得を行ったのには理由があって、プラグインをインストールする際に API キーを合わせて指定する必要があったためだ (後からでも良いのだが、合わせてやるようにしておくと間違いがない)。先程取得した API キーを挿入して、以下のようなコマンドでインストールできる。

$ cordova plugin add cordova-plugin-googlemaps --variable API_KEY_FOR_IOS="【ココに API キー】"

こうすると、config.xml に API キー情報が書き込まれる。コレが誤っていたり、API キー側に設定したアプリのバンドル識別子と config.xml の記載が食い違ったりしていると、マップが正しく表示されないので注意。

API キーの取り扱いについては、むやみに config.xml に API キーを書いたままコードを公開したりしないよう注意。と言っても突然課金が始まる、といったことはないが、API のリクエスト制限に達するとしばらく使えなくなったりするので、悪用されないようには注意したい。

サンプルアプリを動作させる場合は

拙作のサンプルアプリを動作させてみたい場合は、API キーにサンプルアプリのアプリ識別子を登録しておき、config.xml の「Your API Key Here」部分に API キーを追記したら、cordova prepare コマンドで config.xml を基にプラグイン・プラットフォーム情報を復元すれば良い。

プラグインのバージョンについて

現時点で、GoogleMaps プラグインは v1.4.1 がインストールされるはずだ。v2.0 Beta も存在していて、v1 系は2017年中に旧バージョンに落とされるようだが、現時点ではギリギリ v1 系が主流なので、v1.4.1 を使用することにする。

マップを表示するには

まずはとにかくマップを表示してみようと思う。まずは HTML と CSS で、ページ中にマップを表示する領域を作る。

<div id="map" style="width: 100%; height: 500px;"></div>

CSS は別途 CSS ファイルで定義しても問題ない。要素を特定するために id 属性を振っておく。

そして JavaScript 側で以下のように実装し、ページ読み込み時にマップを初期表示させる。

// マップオブジェクトを控えておく変数
var map = null;

document.addEventListener('deviceready', function() {
  // 対象の DOM 要素に Google マップを配置する
  var mapElement = document.getElementById('map');
  // マップの初期位置を表示する (座標は日本の中心あたりを適当に)
  map = plugin.google.maps.Map.getMap(mapElement, {
    camera: {
      latLng: {
        lat: 38.2586,
        lng: 137.6850
      },
      zoom: 4
    }
  });
  
  // マップが初期表示できる状態になったら何かする場合はこのように設定する
  map.addEventListener(plugin.google.maps.event.MAP_READY, function() {
    // ココに処理…
  });
}, false);

camera オプションの latlng で座標を指定する。いつも経度と緯度がごっちゃになるので整理。

  • lat : latitude・緯度 … 南北 → 縦位置 (y 座標)
  • lng : longitude・経度 … 東西 → 横位置 (x 座標)

こんな感じ。

コレでページを初期表示した時に日本列島を表示できているはずだ。タップやスライド、ピンチ操作などができると思う。

マップが表示される仕組み

このマップオブジェクトは、実際はブラウザがレンダリングしているものではなく、ネイティブで動作させているものを HTML 上に透過表示させているに過ぎないのだ。動作がサクサクしているのはネイティブで動作しているから。

そのため、マップを表示している領域上に、HTML で独自のメニューを置いたりもできる。div#map 要素内に Form 部品を配置し、スタイリングしてやれば良い。

指定座標にアニメーション移動してマーカーを打ってみる

では、先程のマップの初期表示が終わったところで、指定の座標値にアニメーションしながら移動し、マーカーを打ってみたいと思う。

// 変数「map」は、先程紹介した初期表示処理 getMap() ができているテイ
var map;
// マップ上に打ったマーカーを控えておくための変数
var markerCache;

// マップが初期表示できる状態になったら処理を開始する
map.addEventListener(plugin.google.maps.event.MAP_READY, function() {
  // 指定座標に向けてアニメーション移動する
  map.animateCamera({
    target: {
      lat: 35.658581,
      lng: 139.745433
    },
    zoom: 17,
    tilt: 60,
    bearing: 140,
    duration: 5000
  }, function() {
    // アニメーション後のコールバック関数
    
    // ズームが終わったらマーカーを付ける
    map.addMarker({
      // アニメーション移動で指定した座標と同じ座標
      position: {
        lat: 35.658581,
        lng: 139.745433
      },
      // マーカーをクリックした時に表示するインフォメーション
      title: 'Welecome to\nCordova GoogleMaps plugin',
      snippet: 'This plugin is awesome!',
      animation: plugin.google.maps.Animation.BOUNCE
    }, function(marker) {
      // マーカーを付けた後のコールバック関数
      
      // 後でマーカーを削除するため、マーカーオブジェクトを退避する
      markerCache = marker;
      // インフォウィンドウを表示する
      marker.showInfoWindow();
      // マーカーのインフォメーションウィンドウがクリックされた時のイベントを設定する
      marker.on(plugin.google.maps.event.INFO_CLICK, function() {
        alert('Hello world!');
      });
    });
  });
});

コレで、マップが初期表示された後に、東京タワーめがけてカメラがズームしていって、マーカーが地図上に打たれるようになった。少々コードが長くなった上にコールバック関数が多く、この調子だと「コールバック地獄」が目に見えているのが分かるかと思う。実際は適宜関数を外出しして管理してほしい。

マーカーをクリックするとインフォメーションのフキダシウィンドウが出るのだが、ココに改行 ¥n を含められるのがこのプラグインの強みらしい。今回は指定の座標地点にマーカーを置いたが、他にもマップを円で囲んだり、任意の図形を描いたりと色々なことができる。

一度打ったマーカーは、marker.remove() を呼ぶことで削除できるのだが、全く別の処理でマーカーを消す場合は、コールバック関数で受け取った marker を一旦別の場所に保持しておく必要がある。というワケでグローバル付近に markerCache という変数を作っておいた次第。マーカーを含めた全てのオブジェクトをまっさらに削除するのであれば、map.clear() というメソッドがある。

任意の住所文字列を基に地図検索 (ジオコーディング) をする

ユーザが入力した住所や地名などを基に地図検索することをジオコーディングというそうなのだが、これをやってみようと思う。

まずはユーザに向けて検索窓を提供する。

<!-- マップを表示する領域 -->
<div id="map"></div>

<!-- 検索窓 -->
<p>
  <input type="text" id="address" value="" placeholder="例 : 東京スカイツリー">
  <input type="button" id="search" value="住所検索">
</p>

次に、「住所検索」ボタンが押された時に、#address の値を拾ってジオコーディングを行う処理を書く。

// 「住所検索」ボタンが押された時の処理
document.getElementById('search').addEventListener('click', function() {
  // ユーザ入力値を取得する (確実に文字列化するため空文字を結合)
  var addressValue = document.getElementById('address').value + '';
  // 入力チェック
  if(!addressValue) {
    alert('検索文字列を入力してください');
    return;
  }
    
  // ジオコーディングを行う
  plugin.google.maps.Geocoder.geocode({
      address: addressValue
  }, function(results) {
    // ジオコーディング語のコールバック関数
    
    // 検索結果がない場合は終了
    if(!results.length) {
      alert('指定の情報が見つかりませんでした');
      return;
    }
    
    // 検索結果1件目の位置に移動する
    var resultPosition = results[0].position;
    // 検索した位置にアニメーション移動する
    map.animateCamera({
      target: resultPosition,
      zoom: 18,
      duration: 2000
    }, function() {
      // アニメーション移動後のコールバック関数
      // マーカーを打つ
      map.addMarker({
        position: resultPosition,
        title: addressValue
      }, function(marker) {
        // マーカーを打った後のコールバック関数
        // マーカーオブジェクトを退避する
        markerCache = marker;
        // インフォウィンドウを表示する
        marker.showInfoWindow();
      });
    });
  });
});

見事なコールバック地獄になっているので、実際は適宜処理を分割したい。

ひとまず、ジオコーディングを行う最小構成は

plugin.google.maps.Geocoder.geocode({
  address: '検索する住所情報'
}, function(results) {
  // 検索結果1件目の位置を取得する
  var resultPosition = results[0].position;
});

この形になる。

あとはココから、検索した住所に対して移動するだとか、マーカーを打つだとかすれば良い。

ステータスバープラグインとの相性について

この GoogleMaps プラグインだが、cordova-plugin-statusbar プラグインとの相性が悪い。cordova-plugin-statusbar プラグインによってステータスバーを表示するよう設定していると、その高さ分だけ Google マップが表示領域の上側にズレて表示されてしまう。

GitHub にも Issue は上がっており、解消方法はいくつかあるのだが、手っ取り早いのは div#mappadding-top:20px; を付与するという方法。マップの表示領域の上側に、ステータスバーの高さ 20px 分の余白を予め開けておくというワケだ。


他にも色々な API が用意されていてココでは紹介しきれないので、公式の README や、紹介記事を見てみてほしい。

手軽に快適な動作の Google マップを実現できるのでオススメ。

Cordova アプリでダイアログ表示したりビープ音を鳴らしたりして通知できる「cordova-plugin-dialogs」

Apache Cordova 4 Programming (Mobile Programming)

Apache Cordova 4 Programming (Mobile Programming)

cordova-plugin-dialogs というプラグインを使うと、ユーザへの通知に関する処理が手軽に実装できる。

今回も iOS 向けのサンプルアプリを作ったので、以下の feat/pluginDialogs ブランチを見てみてもらいたい。

プラグインのインストール

いつもどおり cordova コマンドでプラグインをインストール。

$ cordova plugin add cordova-plugin-dialogs

このプラグインをインストールすることで使えるようになるメソッドは以下の4つ。

  • navigator.notification.alert()
  • navigator.notification.confirm()
  • navigator.notification.prompt()
  • navigator.notification.beep()

順に説明していく。

alert() : アラート表示

JavaScript だと、通常の alert() 関数 (window.alert()) が存在していて、Cordova アプリ上でも window.alert() は使えるのだが、このプラグインの navigator.notification.alert() を使うと、ボタンラベルを変えたりコールバック関数を設定したりなど、よりリッチなアラートが表示できる。

window.navigator.notification.alert(
  'アラートメッセージ',
  function() {
    console.log('「オッケー」ボタン押下後のコールバック関数');
  },
  'アラートタイトル',
  'オッケー'
);

confirm() : 確認ダイアログ表示

こちらも、window.confirm() でも最低限の確認ダイアログを表示させることはできるが、navigator.notification.confirm() はよりカスタマイズしやすい API になっている。

window.navigator.notification.confirm(
  '確認ダイアログメッセージ',
  function(buttonIndex) {
    console.log('押下したボタンの Index (1 から始まるので注意) : ' + buttonIndex);
    if(buttonIndex === 1) {
      console.log('オッケー ボタンが押されました');
    }
    else if(buttonIndex === 2) {
      console.log('ダメー ボタンが押されました');
    }
  },
  '確認ダイアログタイトル',
  ['オッケー', 'ダメー']
);

通常の confirm() だと「OK」と「キャンセル」ボタンのみだが、このプラグインを使うと3つ以上のボタンを用意することができる。それぞれラベルを自分で指定できる他、コールバック関数で押されたボタンを判別できる。ただし、buttonIndex0 からではなく 1 から始まるので注意。

prompt() : プロンプト表示

ユーザに任意の情報を入力させるプロンプトを表示する navigator.notification.prompt()。これも window.prompt() よりリッチなカスタマイズができる。

window.navigator.notification.prompt(
  'プロンプトメッセージ',
  function(results) {
    console.log('押下したボタンの Index : ' + results.buttonIndex);
    console.log('入力内容 : ' + results.input1);
  },
  'プロンプトタイトル',
  ['オッケー', 'ダメー'],
  'デフォルトテキスト'
);

こちらもボタンを3つ以上置いたりできる。押下したボタンの情報と入力値はコールバック関数にオブジェクトにまとめて渡されるので、それぞれ results.buttonIndexresults.input1 で取り出して使える。

beep() : ビープ音再生

このプラグインの本命かもしれない。手軽にビープ音を再生できるのが navigator.notification.beep() メソッド。プラットフォームに合わせて、プラグインがお抱えの音声ファイルを裏で持っているらしく、iOS の場合はピロロロという音が再生される。

// ビープ音を2回繰り返して再生する
window.navigator.notification.beep(2);

引数でリピート回数を指定できる。音色の変更には対応していないが、サクッと通知音を鳴らすにはもってこいだろう。ちなみに Browser プラットフォームでの検証時も、うまくバックグラウンドでそれらしいビープ音を再生してくれる。


以上。ビープ音とともにアラートでユーザの操作を一度奪い、強くユーザにお知らせを出すなど、色々な使い方ができそうなプラグインだ。

Cordova アプリ内でファイル操作を行える「cordova-plugin-file」

cordova-plugin-file というプラグインを使うと、Cordova で作ったアプリの中でファイルを生成したり編集したりすることができる。

今回もこのプラグインを使った iOS 向けのサンプルアプリを作ったので、実際にどんなコードになるかは、以下のリポジトリの feat/pluginFile ブランチを見てみてもらいたい。

プラグインのインストール

プラグインのインストールはいつもどおり cordova コマンドより。

$ cordova plugin add cordova-plugin-file

そういば、Cordova v7.0.0 以降は --save オプションを書かなくても config.xml に記述されるのがデフォルトになった。また、config.xmlpackage.json に同じ情報が同時に追記されるようになった。競合する設定が合った場合は package.json の記述が優先されるみたい。

ファイルはどこに格納されるのか?

プラグインによってファイルが操作できると言ったが、このファイルはどこに保持されるのか?答えは、プラグインの公式 README.md に記載されている。

iOS 実機の場合は、/var/mobile/Applications/【アプリ ID】/ 配下に決まったディレクトリがあり、その配下にファイルが置かれることになる。

iOS シミュレータの場合は、以下のようなパスに格納される。

/Users/【ユーザ名】/Library/Developer/CoreSimulator/Devices/【デバイス ID】/data/Containers/Data/Application/【アプリ ID】/

これらのフォルダにアクセスするための定数プロパティがプラグインによって用意されており、実装する際はこの定数プロパティを使えば良い。

例えば一時的なファイルを生成して扱うのであれば、アプリのディレクトリ直下から見て ./tmp/ ディレクトリ配下、つまり JS のコードでは cordova.file.tempDirectory というプロパティを使うと良い。また、同期したいファイルであれば、./Library/Cloud/ ディレクトリを指す cordova.file.syncedDataDirectory プロパティが良いだろう。

実際にファイル操作をしてみる

では実際にファイルを操作してみよう。JavaScript コードの書き方は、HTML5 で登場した FileSystem API と同じで、ディレクトリ指定部分に先程紹介したプロパティを使えば良い。

ファイルの存在チェック

ファイルが存在するか確認するコードであれば以下のようになる。

// ファイルのフルパスを作る
var fileFullPath = cordova.file.tempDirectory + 'Temp.txt';
window.resolveLocalFileSystemURL(fileFullPath, function(fileSystem) {
  if(fileSystem.isFile) {
    console.log('Temp.txt ファイルが存在する');
  }
  else {
    console.log('Temp.txt ファイルが存在しない');
  }
}, function(error) {
  console.log('ファイル存在確認中にエラーが発生', error.code);
});

error.code については、プラグインが以下のようにコード値と内容を決めている。例えば 1 なら「ファイルが存在しないよ」エラー、というワケだ。

ファイルを新たに作る・既存ファイルを取得する

ファイルを新たに作るのと、既存のファイルを取得して処理する基本構文は同じなので、まとめて紹介する。

// exclusive
//   true  : ファイルが存在する場合にファイルを新規生成しようとした時 (create: true) に、エラー扱いにしてエラー処理の関数を呼び出す
//   false : 上記のような場合にもエラーにはせず、
//           ファイルが存在する場合にファイルを新規生成しようとした場合 (create: true) は既存ファイルの取得処理として扱う
//           ファイルが存在しない場合にファイルを取得しようとした時 (create: false) は、exclusive の値に関わらずエラーとなる
// create
//   true  : ファイルが存在しない場合にファイルを新規生成する
//           ファイルが存在する場合は、exclusive: true ならエラーとし、exclusive: false ならファイル取得処理 (create :false と同じ) として扱う
//   false : 既に存在するファイルを取得する
//           ファイルが存在しない場合は exclusive の値に関わらずエラーとなる
// 今回はファイルを新規作成するための処理にするため、create: true を設定する (exclusive: false なのでファイルが存在した場合もエラーにはしない)
var options = {
  exclusive: false,
  create: true
};

// 生成したい (or 取得したい) ファイル名
var fileName = 'Temp.txt';

// tmp/ ディレクトリ配下を操作する
window.resolveLocalFileSystemURL(cordova.file.tempDirectory, function(fileSystem) {
  console.log('tmp ディレクトリ配下を操作します');
  
  // ファイルを生成 (or 取得) する
  fileSystem.getFile(fileName, options, function(fileEntry) {
    // 生成 or 取得したファイル情報が FileEntry オブジェクトで返される
    console.log('ファイル生成 成功', fileEntry);
  }, function(getFileError) {
    console.log('ファイル生成 失敗', getFileError.code);
  });
}, function(error) {
  console.log('tmp ディレクトリ操作 エラー', error.code);
});

オプションとして渡す連想配列の createexclusive の Boolean 値を変えてやれば、これがそのまま既存ファイルの取得処理になるというワケ。

実はファイルの移動や削除、テキストファイルへの書き込みなどの場合も、この基本形は変わらずfileEntrymoveTo() したり remove() したりすることになるのである。

// create: false 指定で既存ファイルを取得したとして、そのファイルを削除するサンプル
fileEntry.remove(function() {
  console.log('ファイル削除 成功');
}, function(error) {
  console.log('ファイル削除 失敗', error.code);
});

テキストファイルの末尾に追記する

テキストファイルへの書き込みは fileEntry.createWriter() を使うのだが、ファイル末尾に追記していきたい場合は、少々手間がかかる。以下をまるごとスニペットにしてしまうのが良いだろう。

var fileName = 'Temp.txt';

window.resolveLocalFileSystemURL(cordova.file.tempDirectory, function(fileSystem) {
  fileSystem.getFile(fileName, { create: false }, function(fileEntry) {
    fileEntry.createWriter(function(writer) {
      // ファイルの末尾まで移動する
      writer.seek(writer.length);
      
      // 書き込み終了時の処理を定義する
      writer.onwriteend = function(event) {
        if(this.error) {
          console.log('ファイル追記 追記処理中にエラー発生', this.error, event);
        }
        else {
          console.log('ファイル追記 成功', event);
        }
      };
      
      // 書き込みたいテキストの用意 : ココでは現在日時 + 改行コードを書き込む
      var text = new Date() + '¥n';
      
      // テキスト書き込み
      writer.write(text);
    });
  }, function(getFileError) {
    console.log('ファイル操作 エラー', getFileError.code);
  });
}, function(error) {
  console.log('tmp ディレクトリ操作 エラー', error.code);
});

インデントが深くなっていくので、適宜関数化しておくと良い。

「ファイルの末尾に追記」を制御できると、テキストファイルにロギングしたりする関数も作れたりする。


以上。cordova-plugin-file プラグインはとりあえず入れておくとアプリ内のディレクトリ参照・ファイル操作が楽になるのでオススメ。

HTML5/JavaScriptとPhoneGapで作るiPhoneアプリ開発入門

HTML5/JavaScriptとPhoneGapで作るiPhoneアプリ開発入門