追記
機能強化版のマップアプリを作成しました!こちらも是非ご覧ください!
本文
本業ではよくGoogleマップを使う私です。
この度、GASスタンドにて新商品を発売しました。
このブログのタイトルのように、スプレッドシートのデータを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の取得方法については、他サイトでもいっぱい情報があるので、こちらでは詳細を割愛します。
参考:
レイヤーの追加
「管理メニュー」から「レイヤーを増やす」を選択します。
レイヤー名を適当に入力しまして、、、
簡単にレイヤーを増やすことができます。
このような形で、シートごとにレイヤーを管理していく形になります。
レイヤー作成時のダイアログにも表示させましたが、レイヤー名を変更するときは、シート名を変更すればよいです。また、削除したい場合はシートごと消してしまえば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のインスタンスを作る、みたいなことをすると思います。
参考:
ですが、ReactではDOMを直接操作するような実装は書きにくい(というかやりたくない)ので、別の方法を考えました。
@googlemaps/js-api-loader
Google公式が用意してくれているGoogle MapsのAPI Loaderです。
これをスクリプトタグで読み込んでおきます。
<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のお作法は下記のブログが参考になります。
ポイントは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件だけとかだと、先程みたいにめっちゃズームしてるみたいになっちゃうんですけどね、、、(それは仕方ないか)
おわりに
色々と試行錯誤して、割と使い勝手の良いツールになったのではないかと思います。
まだまだ、カスタマイズの余地はあるかと思いますが、とりあえず最低限の機能でリリースです。
是非覗いてみてください(宣伝)