【GAS】スプレッドシートのデータと同期するGoogleマップアプリを作ってみた

Google Apps Script

本業ではよくGoogleマップを使う私です。

この度、GASスタンドにて新商品を発売しました。

マイGoogleマップ作成アプリ 〜 スプレッドシートでデータ管理して、マップを楽々更新できます! 〜 | GASスタンド
マイGoogleマップ作成アプリ 自分だけのマップを作成を作成したいときには、Googleの「マイマップ」を使うことが多いのではないかと思います。 マイマップに表示するデータの管理はスプレッドシートで行うことが多いかと思いますが、 スプレッドシートのデータとマイマップのデータを同期するのは意外と大変です。 参考:

このブログのタイトルのように、スプレッドシートのデータをGoogleマップで表示することができるアプリになります。
GAS開発者目線では、GASウェブアプリ + React + Googleマップで色々と試行錯誤できたのが楽しかったです。 @googlemap/js-api-loader とか初めて使って少し感動。

アプリが解決する課題

自分だけのマップを作成を作成したいときには、Googleの「マイマップ」を使うことが多いのではないかと思います。
マイマップに表示するデータの管理はスプレッドシートで行うことが多いかと思いますが、
スプレッドシートのデータとマイマップのデータを同期するのは意外と大変です。

参考:https://support.google.com/maps/thread/124907005/マイマップの更新について?hl=ja

そこで、スプレッドシートのデータと同期するようなマップがあったら便利かと思い、今回のアプリを作りました。
マイマップと同様に、複数のレイヤーも管理できるので、結構使い勝手もいいのではないかと思います。

どんな感じで使う?

ここからはツールの使用感について書いていきます。

Google Maps JavaScript APIのKeyを入力

事前準備として、API Keyを取得し、スプレッドシートに書き込んでおきます。
書き込む場所は以下画像の部分です。

API Keyの取得方法については、他サイトでもいっぱい情報があるので、こちらでは詳細を割愛します。

参考:

Googleマップを使ってみよう!【APIキー取得からサンプルコードまで】 | 株式会社ゼンリンデータコム
Google Maps APIの詳細から、Google Maps APIを使用するためのAPIキーの取得方法、サンプルコードでの動作確認までをご説明します。

レイヤーの追加

「管理メニュー」から「レイヤーを増やす」を選択します。

レイヤー名を適当に入力しまして、、、

簡単にレイヤーを増やすことができます。

このような形で、シートごとにレイヤーを管理していく形になります。

レイヤー作成時のダイアログにも表示させましたが、レイヤー名を変更するときは、シート名を変更すればよいです。また、削除したい場合はシートごと消してしまえばOKです。

データの更新

適当に東京タワーの情報を入力します。ここで入力するのは「名前」と「住所」と「詳細」のみ入力します。

データを入力し終えたら、「管理メニュー」から「住所から緯度経度に変換」を選択します。

しばらくすると、住所の情報をもとに、緯度経度が自動で挿入されます。

デプロイ

この作業は、アプリケーションを使用可能な状態にする手順です。なので、最初に1回だけ行うことになります。

GASエディタから「新しいデプロイ」を選択して、

デプロイタイプを「ウェブアプリ」に設定し、

適宜情報を入力して、「デプロイ」を押します。

すると、アプリにアクセスするためのURLが手に入ります。

地図画面の表示

先程の手順で手に入れたURLにアクセスしてみると、地図画面が表示されます。

(東京タワーにめっちゃズームしているのは、データが東京タワーの1件しかないからです。これは後ほど詳しく説明します。)

この状態で、スプレッドシートのデータを更新して、アプリをもう一度リロードすると、、、

無事データがマップに表示されました!スプレッドシートのデータとマップに表示されるマーカーが同期されていることがわかります。

レイヤーも複数用意すれば、以下画像のように、表示非表示を切り替えることもできます。マーカー色もスプレッドシート側で編集可能です。

創意工夫したところ

ここからはプログラムの実装部分で頑張ったことを、つらつらと書いていきます。

GASのWebアプリでReact + Google Map

生のJavaScriptでGoogle Mapを扱うときは <script> タグでGoogle Maps JavaScript APIのライブラリを持ってきて、 document.getElementById('map_area') とかでDOMを取得して、mapのインスタンスを作る、みたいなことをすると思います。

参考:

Overview  |  Maps JavaScript API  |  Google Developers
Get started with the Google Maps JavaScript API. View a simple example, learn the concepts, and create custom maps for your site.

ですが、ReactではDOMを直接操作するような実装は書きにくい(というかやりたくない)ので、別の方法を考えました。

@googlemaps/js-api-loader

Google公式が用意してくれているGoogle MapsのAPI Loaderです。

GitHub - googlemaps/js-api-loader: Load the Google Maps JavaScript API script dynamically.
Load the Google Maps JavaScript API script dynamically. - GitHub - googlemaps/js-api-loader: Load the Google Maps JavaScript API script dynamically.

これをスクリプトタグで読み込んでおきます。

<script src="https://unpkg.com/@googlemaps/js-api-loader@1.14.2/dist/index.min.js"></script>

APIのロード

その後は、APIをロードする部分をHooksにして、、、

<script type="text/babel">
  const useGoogleMapLoader = (apiKey, mapRef, config) => {
    const [googleMap, setGoogleMap] = React.useState(null);

    const loader = new google.maps.plugins.loader.Loader({
      apiKey,
    });

    React.useEffect(() => {
      loader.load().then(() => {
        const map = new google.maps.Map(mapRef.current, config);
        setGoogleMap(map);
      });
    }, []);

    return googleMap;
  }
</script>

Googleマップを扱うためのラッパーコンポーネントを作り、、、

<script type="text/babel">
  const MapContext = React.createContext(null);

  const GoogleMapWrapper = (props) => {
    const mapContainerRef = React.useRef(null);
    const googleMap = useGoogleMapLoader(props.apiKey, mapContainerRef, props.config);

    return (
      <React.Fragment>
        <div
          style={{
            height: "100%",
            width: "100%",
            margin: 0,
          }}
          ref={mapContainerRef}
        />
        <MapContext.Provider value={googleMap}>
          {props.children}
        </MapContext.Provider>
      </React.Fragment>
    );
  };
</script>

実際に配置してやるだけ。

<?!= HtmlService.createTemplateFromFile('GoogleMapWrapper').evaluate().getContent(); ?>
<?!= HtmlService.createTemplateFromFile('MarkerLayer').evaluate().getContent(); ?>

<script type="text/babel">
  const MapArea = (props) => {
    const initialConfig = {
      zoom: 1,
      center: { lat: 35.6432027, lng: 139.6729435 },
      gestureHandling: 'greedy',
    };

    return (
      <GoogleMapWrapper apiKey={props.apiKey} config={initialConfig}>
        <MarkerLayer layerData={props.layerData} />
      </GoogleMapWrapper>
    );
  };
</script>

ここらへんのReact + GASのお作法は下記のブログが参考になります。

GASでReact + Redux(+ Material UI)なWebアプリケーションを作ってみた
むしゃくしゃしてやった。後悔はしている。(時間かけすぎた)ということで、タイトルの通りなのですが、GASのWebアプリケーションにReact + Reduxを組み込んだものを作ってみました。その時の知見を書き綴っていこうと思います...

ポイントはGoogleMapのインスタンスをコンテキストで、子コンポーネントに流してあげていることですね。
なので、上記の <MarkerLayer /> コンポーネント内で、 useContext(MapContext) みたいにしてやれば、いつでもGoogleMapのインスタンスを受け取れるようになっています。

fitBounds

マップアプリを作る際に、意外とネックになるのが、「初期表示における地図の中心位置」と「拡大レベル」です。
東京あたりに初期表示位置をハードコードしたとして、もし大阪あたりにのみマーカーが出現していると、初期表示時には見えない状態なので、使い勝手が悪いです。
それを解決するために、初期表示時には「全てのマーカーが見えるように」地図の中心と拡大レベルを調整する実装をしています。
それを簡単に実現しているのが、この「fitBounds」関数です。

以下は、使い方説明のためのなんちゃってコードです。(lat1とかlat2とか、実際に定義してやると動くと思います)

// LatLngBoundsインスタンスを作って、
const bounds = new google.maps.LatLngBounds();

// マーカーのあるLatLngをLatLngインスタンスにしておく
const latLng1 = new google.maps.LatLng(lat1, lng1);
const latLng2 = new google.maps.LatLng(lat2, lng2);

// boundsにマーカーの場所を教えてあげる
bounds.extend(latLng1);
bounds.extend(latLng2);

// googleMapインスタンスのfitBounds関数を実行すると、地図の中心とズームがいい感じになる
googleMap.fitBounds(bounds);

データが1件だけとかだと、先程みたいにめっちゃズームしてるみたいになっちゃうんですけどね、、、(それは仕方ないか)

おわりに

色々と試行錯誤して、割と使い勝手の良いツールになったのではないかと思います。
まだまだ、カスタマイズの余地はあるかと思いますが、とりあえず最低限の機能でリリースです。

是非覗いてみてください(宣伝)

マイGoogleマップ作成アプリ 〜 スプレッドシートでデータ管理して、マップを楽々更新できます! 〜 | GASスタンド
マイGoogleマップ作成アプリ 自分だけのマップを作成を作成したいときには、Googleの「マイマップ」を使うことが多いのではないかと思います。 マイマップに表示するデータの管理はスプレッドシートで行うことが多いかと思いますが、 スプレッドシートのデータとマイマップのデータを同期するのは意外と大変です。 参考:
タイトルとURLをコピーしました