GoogleMapウィジェットで楽々ランチマップづくり

どうもこんばんは、みかんです。

**！！！！とつぜんですがたいせつなおしらせ！！！！**

2020年1月27日
App Maker の提供終了に向けた対応のご案内
https://support.google.com/a/answer/9682494?p=am_announcement&visit_id=637157414793613981-426262357&rd=1


??!???!?!?!???!?

残念ながら2021年1月19日には終了してしまうそうです(涙)
移行先はAppSheetかApp EngineかGoogleフォームだそうです・・・

消すのは忍びないので記事は残して置きますが、何かの間違いじゃ無い限り役に立つことはないでしょう・・・


**！！！！おしらせはおわりです！！！！！**

今回は「GoogleMapウィジェットで楽々ランチマップづくり」について書いてみます。
下記ステップでやっていきます。

* 1 下準備
* 2 Google Mapウィジェットを配置してみる
* 3 マップに緯度経度をプロットする
* 4 みんなで追加できるようにする

# 1 下準備

ランチマップアプリを作るつもりで始めるので、Starter Appを使いたいのですが、どうせなら一画面に収めたらどうか、と思ったのでBlack Applicationで始めます！

|![ap01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/52fb685c-65c4-2660-d4b2-a58a43f205de.png)|
|:-:|

# 2 GoogleMapウィジェットを配置してみる

兎にも角にも、まずはGoogleMapウィジェットを配置します。

|![gif001-compressor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/dd8dde4f-c190-3b92-0f97-092c996d2ddf.gif)|
|:-:|

こんな手軽に配置できるのは嬉しいですね。普通の開発方法で考えると、ライブラリ読み込んだりなんだり、色々ありますからね。
とりあえず、画面いっぱいに広げてたあと、緯度(latitude)と経度(longitude)をいじってみて、秋葉原駅が中心になるようにしています。
**zoom**をいじると、拡大縮小のコントールができます。22が最大値です。

**address**プロパティをいじってみた様子も乗せてみます。

|![gif002-compressor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/cbd62f8a-40eb-cffc-3f5b-be96e1c848ec.gif)|
|:-:|

入力した住所を中心に指すようになりました。この時、もともとの緯度経度の値は変化が無いので、住所をクリアしてみると、秋葉原駅に戻っています。

さて、とりあえず実行してみましょうか。

|![ap02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/76b6377c-735c-b77a-7aca-93d8b9d4158e.png)|
|:-:|

こ、これは？！
全面的に「For development purposes only」と表示されていますね。
これを表示しないようにして、普通に利用してもらうためには**APIキー**が必要になります。

プロパティエディタを見ると、apiKeyを入力する欄があります。

|![ap03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/409eb0f1-1756-1e3d-8ed4-d30e212dcb77.png)|
|:-:|

APIキーは、App Makerで用意するわけではなく、Google Map Platformのプロジェクトで用意する必要があります。
[こちら](https://developers.google.com/maps/documentation/javascript/get-api-key?hl=ja)が公式ガイドになります。(えいご..)

すごくざっくりと紹介すると、下記のような手順で用意します。

1. Googleアカウントを用意する
2. Google Cloud Platformのプロジェクトを用意する
3. そのプロジェクトに課金設定を行う
4. そのプロジェクトの「APIとサービス」 > 「ライブラリ」で「Maps JavaScript API」を有効にする
5. そのプロジェクトの「APIとサービス」 > 「認証情報」で「＋認証情報を作成」を押してAPIキーを発行する
6. そのAPIキーにわかりやすい名前をつける
7. そのAPIキーの「アプリケーションの制限」を「HTTPリファラー」にする
8. そのAPIキーの「ウェブサイトの制限をApp MakerのURLにする」(後述します)
9. そのAPIキーの「APIの制限」を「キーを制限」にし、「Maps JavaScript API」のみを指定する

課金設定済みGCPプロジェクトが無いと時間がかかるかもしれません。

> そのAPIキーの「ウェブサイトの制限をApp MakerのURLにする」(後述します)

この手順は、アプリの作成前に予め実施することは難しいです。
ウェブサイトのURLを指定するので、下の画像でいうと
`https://script.google.com/a/macros/yoshidumi.co.jp/s/*`
といった感じで指定すればOKのような気がします。

|![ap04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/a1e2b53b-e669-f083-ced8-521fb9802364.png)|
|:-:|

|![ap05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/17b75efe-48c0-f054-f3e9-f36fe53718fb.png)|
|:-:|

ただ、これで実行すると、こんな感じになります。

|![ap06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/63eff9d4-bceb-8c1d-294a-4eadd896ac4d.png)|
|:-:|

エラーを見ると、
`https://n-g3nj4ougpr2yrvezfgswl7stknkm6ueu6wck4kq-0lu-script.googleusercontent.com/userCodeAppPanel`
というURLからのアクセスは許可されていないと言っているようですね。
アドレスバーで表示されているURLは、GoogleMapに通信をしているホストでは無いということですね。script.google.comですらないドメインのサブドメインが割り当てられているようです。
`https://n-g3nj4ougpr2yrvezfgswl7stknkm6ueu6wck4kq-0lu-script.googleusercontent.com/*`
といった感じで指定すると、上手く動作しました。(ちなみに、設定してから何分間は反映に時間がかかるものなので、すぐリロードしてもエラーは出続けます。コーヒーでも飲んで待ちましょう。)

思いっきりURL晒してますが、これはプレビュー専用のURLでさらに自分しかアクセスできない感じのURLです。

また、これを実際にデプロイしたりすると、当然URLは変わります。プレビューでなければコンソールも最初は閉じているので、一回実行してみる → JavaScriptコンソールを表示する → エラーになっているURLを拾って、APIキーの設定をアップデートする
ということをする必要があります。

このように、面倒くささを少し感じる設定ですが、**絶対に**HTTPリファラーの設定は行ってください。開発中に用意する場合であっても設定してください。
「いや、誰にも教えないし・・・」
という感想を持ったとしたらアウトです。詳しくは説明しませんが、サイトを利用する人であれば**誰でも**入手できる情報です。そして他の用途にも使い回されてしまいます(APIの制限で多少軽減される)。そして使ったサービスが課金の対象だったとしたら・・・？
App Makerで色々する中で、想定外の料金がかかるケースはほとんど無いのですがGoogle Mapの場合は話が別です。
ちょっと怖い感じの内容になりましたが、設定を忘れなければ大丈夫です。
とにかく、APIキーを作る上で「アプリケーションの制限」無しはNGです。

# 3 マップに緯度経度をプロットする

気を取り直して、マップにプロットをしていきたいと思います。
ランチマップを作ろうと思ったら、お店の位置を地図に出さないと行けないですからね。

さて、どのプロパティエディタに何を入れれば良いでしょうか・・・？

**ないんだな、それが**

え・・・？ 緯度経度が入ったデータソースを指定して、バチッと表示されるんじゃないの・・・？と思った貴方。
私もそう思っていたんですが、まだ見つけられていません。

じゃあどうやって追加するの？？？？
というと、下記のような方法を使います。

**onAttach**イベントのカスタムアクションに記載

```javascript
//GoogleMapウィジェットのonAttachからコール
function mapSample(widget) {

  var mapJs = widget.getMapJs();
  
  var marker = new google.maps.Marker({
    position: new google.maps.LatLng(35.69834524173287, 139.77311216735836),
    animation: google.maps.Animation.DROP,
    title: "marker"
  });
  marker.setMap(mapJs);

  var contentString = 
      "<div>"+
      "<h1>マーカーサンプル</h1>"+
      "<p>"+
      "Maps JavaScript APIのリファレンスは下記！"+
      "</p>"+
      "<p>"+
      "<a href='https://developers.google.com/maps/documentation/javascript/tutorial'>https://developers.google.com/maps/documentation/javascript/tutorial</a>"+
      "</p>"+
      "</div>";
  var infoWindow = new google.maps.InfoWindow(
    {content:contentString}
  );
  
  marker.addListener("click", function(){
    infoWindow.open(mapJs, marker);
  });

}
```

`var mapJs = widget.getMapJs();`
こいつがミソですね。何をやっているかと言うと、GoogleMapウィジェットから**GoogleMap用のJavaScriptライブラリのオブジェクト**を取得しています。
普通のフロント開発の時に利用するものと一緒です。
ということで、ここから先何かしようと思ったら普通のフロント開発と同じ様にコーディングが必要ということです。残念でした（？）。

とりあえず、上のコードで何がどうなったかを見てみます。

|![gif003-compressor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/365c7d59-fabb-a745-cb64-43411f84d588.gif)|
|:-:|

ム、gifだと**DROP**パラメータによってマーカーが上から落ちてきてる感じがわからないですね。しょうがない。

座標は、最初にlatitude,longitudeプロパティと同じものを使っています。
もともと中央にあったマーカーと位置がかぶるので、あらかじめ**showAddressMarker**プロパティのチェックを外しています。

マーカーをクリックされた時にちょっとした情報ウィンドウが表示されるようにしています。これもGoogleMapのライブラリ側の機能であり、App Makerの機能ではありません。App Makerにもポップアップという機能がありますが、クリックされたマーカーの位置に合わせて出すのは恐らく**至難のワザ**なので試してもいません。

というわけで、App MakerからGoogleMapのライブラリを使うまでの紹介はできたと思います。逆に言えば、これくらいでGoogleMapのAPIを試す環境が手に入るということでもあるので、そういう使い方も良いよなぁと思います。
サンプルコードにも混ぜましたがドキュメントのURLは[こちら](https://developers.google.com/maps/documentation/javascript/tutorial)。

# 4 みんなで追加できるようにする

店舗が追加されるたびにアプリのコードを書き換えるというのはナンセンスです。単純に手間です。
とくれば、App Makerのモデルを使って、店舗の情報をDBに格納し、その内容をスクリプトでGoogleMapに反映するというものを作るしか無いですね！

モデルはこんな感じです。

|![ap08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/12b3a929-68f9-ace2-62d2-99571a7c81ea.png)|
|:-:|

評価とウェブサイトのURL、そして位置です。
あとカテゴリ(ラーメン、和風、海鮮など)もあったほうがいいかもですね。

画面はこんな感じです。

|![ap09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/a6f997f9-11d9-5bb7-890d-0ccdc5cef637.png)|
|:-:|

地図の左に入力用フォーム、地図の下にテーブルを配置しました。
入力フォームとテーブルの**Rating**に関しては専用のウィジェットに置き換えました。せっかくなので。

最後にスクリプトです。すべてクライアントスクリプトです。

|![ap11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/d1ac16f7-8c5c-0f68-8421-44a188ab44bf.png)|
|:-:|

4つの関数と、1つのグローバル変数を定義しています。それぞれ見ていきます。

```javascript:createMarker
//ランチデータからマーカーを作って返す
function createMarker(lunchData) {
 return new google.maps.Marker({
    position: new google.maps.LatLng(lunchData.Latitude, lunchData.Longitude),
    animation: google.maps.Animation.DROP,
    title: lunchData.ShopName
  });
}
```

`new google.maps.Marker()`を使ってマーカーオブジェクトを作ります。
普通のjsonオブジェクトを作るのではなく、GoogleMapに用意されたクラスを用いてオブジェクトを作る必要があります。

```javascript:bindInformationToMarker
//マーカーにクリック時に表示するための情報を紐付ける
function bindInformationToMarker(lunchData, marker, map) {
   var contentString = 
      "<div>"+
      "<h1>" + lunchData.ShopName + "</h1>"+
      "<p>"+
      lunchData.Description+
      "</p>"+
       "<p> Rating : " + lunchData.Rating + "</p>" + 
      "<p>"+
      "<a href='" + lunchData.ShopUrl + "'>" + lunchData.ShopUrl + "</a>"+
      "</p>"+
      "</div>";
  var infoWindow = new google.maps.InfoWindow(
    {content:contentString}
  );
  marker.addListener("click", function(){
    infoWindow.open(map, marker);
  });
}
```

マーカーをクリックした時に情報ウィンドウが出るような設定をまとめています。
Ratingは☆マークにしたかったのですが、面倒だったのでそのままです。

```javascript:loadMarker
//消す時に必要なのでグローバルにマーカーの一覧を持つ
var markers = [];

//GoogleMapウィジェットのonDataLoadからコール
//データ作成ボタンのonClickからもコール
function loadMarker(widget) {
  var mapJs = widget.getMapJs();
  var dataList =  widget.datasource.items;
  
  //既存のマーカーをクリアする
  markers.forEach(function(marker){
    marker.setMap(null);
  });
  markers.length = 0;
  
  //マーカーを追加する
  dataList.forEach(function(data){
    var marker = createMarker(data);
    marker.setMap(mapJs);
    bindInformationToMarker(data, marker, mapJs);
    markers.push(marker);
  });
}
```

今まで定義した関数を使いながら、GoogleMapにマーカーを乗せているコードです。
ランチデータは`widget.datasource.items`で持ってきています。フロント側にデータが読み込まれてからでないと使えないので、GoogleMapウィジェットのデータソースに`LunchData`を設定しておいて、自動で読み込まれた後に呼び出されるイベントの`onDataLoad`で`loadMarker(widget);`という形で呼び出しています。

新しくデータを追加したタイミングでもマーカーの再配置を行ってもらいたいので、入力フォームの「データ作成」ボタンでも、下記のように呼び出しています。

```javascript:onClick
widget.datasource.createItem(function(){
  loadMarker(app.pages.NewPage.descendants.GoogleMap2);
});
```
createItem関数に設定した関数の中で記述しているのは、データの作成処理が終わるのを待ってから実行するためですね。

GoogleMapに一度追加したマーカーを取り除く方法は、
`marker.setMap(<マップオブジェクト>)`
しか無いため、マーカー側のオブジェクトを覚えておく必要があります。
危険な香りを感じつつも、関数のすぐ上でマーカーを配列で取っておける変数を定義しています。

```javascript
//消す時に必要なのでグローバルにマーカーの一覧を持つ
var markers = [];
```

最後に、initMap関数です。

```javascript:initMap
//GoogleMapウィジェットのonAttachからコール
function initMap(widget) {
  var mapJs = widget.getMapJs();
  mapJs.addListener('click', function(e) {
    app.pages.NewPage.descendants.LatField.value = String(e.latLng.lat());
    app.pages.NewPage.descendants.LonField.value = String(e.latLng.lng());
  });
}
```

データを追加する時に緯度経度を手打ちするのが異常にめんどくさかったので、
入力フォームの移動経度のフィールドに、地図をクリックした時の座標が入るようにしています。
無くてもいいんですが、せっかくアプリにしているので使いやすいようにしたいですよね。

最低限の実装をしてみたところで、動きを見てみます。

|![gif004-compressor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26085/e3c312d6-54ad-329e-0708-ab4503e70648.gif)|
|:-:|

ここでは入れていないですが、店舗のURLを入れると情報ウィンドウからサイトに飛べるようになります。

ということで、簡単ではありますがランチマップの作り方を書いてみました。
これを社内にばらまいて、どんどん入力してもらえればすばらしいランチタイムが過ごせそうですね。

・・・まあ全然使い勝手良いとは言えないレベルなので、カテゴリ入れたり検索できるようにしたり、コメントつけられるようにしたいですね。
本家GoogleMapでもできるのでは？という感じですが、社内に閉じているほうが参考になる情報が集まりやすいのかなと思います。
`◯◯さんがラーメン◯◯に評価5をつけました`
みたいなやつとかをslackに投げるとかもいいですね。アイデア次第でどんどん実装できる面白さがあると思います。

以上です。良いApp Makerライフを!