ふんばりフロントエンジニアのブログ

新米フロントエンジニアの備忘録です。ふんばり温泉タオル欲しい...

GoogleMapsAPIで地図を実装! 基本からカスタムパノラマの実装、パノラマ間の遷移までまとめ

以前初めてGoogleMapの実装案件があったので、備忘録的にメモ。

今回の流れは下記の様な流れで進んでいきます。

1. GoogleMapの簡単な表示例
2. ストリートビューの簡単な表示例
3. GoogleMapにマーカーを立てる・マーカーにイベントを設置
4. カスタムパノラマの作り方

まず、簡単な例からいきましょう。

単なるGoogleMapを表示したい場合、必要となるのは下記のようなコードです。

<div id='map'></div>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=[自分のAPIキー]"></script>
#map {
    width: 100vw;
    height: 100vh;
  }
 const initMap = ()=> {
   const latLng = {lat: 35.659063, lng: 139.700641};
   
   const map = new google.maps.Map(document.getElementById('map'), 
        {
            center: latLng,
            zoom: 14
        }); 
  }
   initMap();

これで緯度: 35.659063, 経度: 139.700641(渋谷ハチ公)を中心とする地図を表示することができました。

まあでもこのあたりはみなさん知っていると思います。

さて、まずGoogleMapAPIには大きく分けて二つのオブジェクトがあります。

1. GoogleMapオブジェクト
2. StreetViewPanoramaオブジェクト

GoogleMapオブジェクトはみなさんがいつもみているような地図のオブジェクトです。

ではStreetViewPanoramaオブジェクトとはどのようなものなのでしょう?

 const initMap = ()=> {
 	    const latLng = {lat: 35.659063, lng: 139.700641};

        const map = new google.maps.Map(document.getElementById('map'),
          {
              center: latLng,
              zoom: 14
          });

        const stv = map.getStreetView(); // mapのストリートビュー情報を取得
        
        stv.setPosition(latLng); // ストリートビューの位置を指定
        stv.setVisible(true); // ストリートビューの表示をアクティブに変更
}
   initMap();

先ほど地図を表示できたコードにコメントアウトで説明を入れている三行を追加しました。

こちらのコードを実行すると、ハチ公のストリートビューが表示されているはずです。

では一旦ここで解説です。

まず、GoogleMapオブジェクトからそれに紐づくStreetViewオブジェクトを取得するには「getStreetView()」メソッドを使用します。

これによって、変数stvにはStreetViewオブジェクトが代入されています。

そしてStreetViewオブジェクトで使用することのできるメソッドを使って、位置を指定し表示を切り替えている、というわけですね。

setPositionは入れなくてもいい気がしていたのですが、こちらのメソッドで明示的に位置情報をセットしないといけないよう。

これらのメソッドに関してはSyncerの記事がかなりよくまとまっていたのでぜひ参考にしてみてください。

https://lab.syncer.jp/Web/API/Google_Maps/JavaScript/

まず、GoogleMapとストリートビューの切り替えに関しては基本的にこのような流れで進んでいきます。

では次に、必ずと言っていいほど実装するマーカーの実装について説明していきます。

というのも、今回の案件ではマーカーをクリックするとカスタムパノラマが表示されるという実装もあったためです。

const initMap = ()=> {
 	    const latLng = {lat: 35.659063, lng: 139.700641};

        const map = new google.maps.Map(document.getElementById('map'),
          {
              center: latLng,
              zoom: 14
          });

        const marker = new google.maps.Marker({
            position: new google.maps.LatLng(latLng.lat, latLng.lng),
            map: map,
            title: 'サンプルマーカー',
            icon: {
                url: 'img/marker-icon.png',
                scaledSize: new google.maps.Size(128, 128)
            }
        });
        
    
}
   initMap();

このマーカーはもちろんGoogleMapの地図上に表示されるため、マーカーを立てることができるのはGoogleMapオブジェクトに対して、ということになります。

iconプロパティで画像を指定するだけで簡単にアイコン画像を変えることができるのは便利ですね。

ただ、「size」ではなく「scaledSize」にしないとアイコン画像がうまいこと拡縮しないので、そこだけは注意が必要です。

さて、次はイベントの指定です。

const initMap = ()=> {
 	    const latLng = {lat: 35.659063, lng: 139.700641};

        const map = new google.maps.Map(document.getElementById('map'),
          {
              center: latLng,
              zoom: 14
          });

        const marker = new google.maps.Marker({
            position: new google.maps.LatLng(latLng.lat, latLng.lng),
            map: map,
            title: 'サンプルマーカー',
            icon: {
                url: 'img/marker-icon.png',
                scaledSize: new google.maps.Size(128, 128)
            }
        });
        
         marker.addListener('click',()=>{ // addListenerなのに注意!
            alert('clicked marker!');
        });
        
    
}
   initMap();

追加したのはコメントが付いている箇所ですね。

コメントにも記載しましたが、addEveneListenerではなくaddListenerであることに注意しましょう。

これはGoogleMap独自のメソッドで、GoogleMapオブジェクトやStreetViewオブジェクトに対してイベントを設定する際にはこのaddListenerメソッドを使うことになります。

では、ここまでの内容で次のような実装をしてみましょう。

  • GoogleMapの中心座標は渋谷ハチ公
  • ハチ公にはマーカーを立てる
  • マーカーをクリックするとハチ公のストリートビューが表示される
const initMap = ()=> {
        const latLng = {lat: 35.659063, lng: 139.700641};

        const map = new google.maps.Map(document.getElementById('map'),
          {
              center: latLng,
              zoom: 14
          });

        const stv = map.getStreetView();

        const marker = new google.maps.Marker({
            position: new google.maps.LatLng(latLng.lat, latLng.lng),
            map: map,
            title: 'サンプルマーカー',
            icon: {
                url: 'img/marker-icon.png',
                scaledSize: new google.maps.Size(128, 128)
            }
        });

        marker.addListener('click',()=>{
            stv.setPosition(latLng);
            stv.setVisible(true);
        });

    
}
   initMap();

f:id:ma1129nm:20200414123611g:plain

さて、実装できたはいいもののスポットが一つであることは珍しく、現状のままの実装ではいけません。

では複数のスポットに対応してみましょう。

const initMap = ()=> {
         let markers = []; // マーカーを格納する配列
        const spotList = [ // マーカーの位置情報を格納している配列
            {
                lat: 35.659063,
                lng: 139.700641,
                name: 'ハチ公',
            },
            {
                lat: 35.658625,
                lng: 139.701057,
                name: 'モヤイ像',
            }
        ];

        const map = new google.maps.Map(document.getElementById('map'),
          {
              center: {lat: spotList[0].lat, lng: spotList[0].lng},
              zoom: 14
          });

        const stv = map.getStreetView();

       spotList.forEach((spot, index) => { //spotListの数だけマーカーを設置、イベントを設定
            markers[index] = new google.maps.Marker({
                position: new google.maps.LatLng(spot.lat, spot.lng),
                map: map,
                title: spot.name,
                icon: {
                    url: 'img/marker-icon.png',
                    scaledSize: new google.maps.Size(128, 128)
                }
            });

            markers[index].addListener('click',()=>{
                stv.setPosition(markers[index].position);
                stv.setVisible(true);
            });
        });    
}

f:id:ma1129nm:20200425150600g:plain

今回も追加したのはコメントが記載されている部分です。

ポイントとなるのは、位置情報を格納している配列「spotList」とマーカーを格納する配列「markers」ですね。

見ていただけるとわかると思いますが、一旦生成したマーカーをmarkersに格納して、その後にクリックイベントを設定しています。

一旦配列に入れることで後々マーカーに対する操作も行いやすくなるので、この方法はオススメです。

さて、ここまでで一旦GoogleMapのデフォルトの機能である「地図」、「ストリートビュー」、「マーカー」を使うことができたと思います。

ただ、なかなかこれで終わりというわけにはいかないんですよね...

そう、今回のメインとなる「カスタムパノラマ 」の実装について見ていきたいと思います。

カスタムパノラマの情報はGoogleMapAPIのドキュメント上に載っているのには載っているのですが、かなり情報が少なかったです...

とりあえず、カスタムパノラマ を表示してみましょう。

<div id='pano'></div>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=[自分のAPIキー]"></script>
const initMap = ()=> {
        const customStv = new google.maps.StreetViewPanorama(
                document.getElementById('pano'), {
                    pano: 'custompanorama',
                });

        customStv.registerPanoProvider(() => {
            return {
                location: {
                    pano: 'custompanorama',
                    description: 'カスタムパノラマ',
                    latLng: new google.maps.LatLng(35.6595126, 139.7005696)
                },
                links: [
                    {
                        heading: 250,
                        description: 'カスタムパノラマ2',
                        pano: 'custompanorama2'
                    }
                ],
                copyright: '©︎2019 Google 日本',
                tiles: {
                    tileSize: new google.maps.Size(1024, 512),
                    worldSize: new google.maps.Size(1024, 512),
                    centerHeading: 105,
                    getTileUrl: function (pano, zoom, tileX, tileY) {
                        return `img/${pano}_${zoom}_${tileX}_${tileY}.jpg`;
                    }
                }
            };
        });


}
   initMap();

f:id:ma1129nm:20200425150919g:plain

今回カスタムパノラマ画像として、下記のような画像を用意しました。
f:id:ma1129nm:20200425150852j:plain

f:id:ma1129nm:20200414123652j:plain

ちなみに、ストリートビューで公開されている画像は下記の手順でDLすることができます。

http://sanshonoki.hatenablog.com/entry/2017/05/12/215744

先ほどと比較するとなんだかわからないものがいっぱい出てきてしまった感があります笑

ただ、流れとしては単純で下記のような流れでカスタムパノラマを表示することができます。

1. StreetViewPanoramaオブジェクトを作成
2. StreetViewPanoramaオブジェクトのregisterPanoProviderメソッドでカスタムパノラマの情報を登録

一番ややこしそうなのは「registerPanoProvider」メソッドの中だと思います。

GoogleMapAPIのドキュメントでは、カスタムパノラマを表示するにはregisterPanoProviderメソッド内でStreetViewPanoramaDataを返却する必要がある、と記載しています。

つまり、returnで返却しているオブジェクトの部分 = StreetViewPanoramaDataということになりますね。

では、StreetViewPanoramaDataの中で重要なものだけピックアップして説明していこうと思います。

  • location

StreetViewオブジェクトの位置情報やパノラマID等、大枠に関する情報を指定するプロパティです。
パノラマIDは最重要と言ってもいいかもしれません。

  • links

ストリートビュー内で矢印が表示されていると思いますが、矢印の部分がこのlinksです。
linksで指定した通りに矢印が表示され、クリックするとlinksのpanoに記載されているパノラマIDのストリートビューに遷移します。
headingは角度なので、好きな角度を指定することができます。

  • tiles

カスタムパノラマ画像の情報を指定するプロパティです。
getTileUrlはよしなに自オブジェクトのパノラマIDを見て画像を探してくれるので、基本的にこのままで問題ないと思います。

さて、簡単に説明していきましたがカスタムパノラマが一枚の場合はいいですが、二枚三枚となると一々この処理をしないといけないのか...と鬱になってきますよね。

ということで、このあたりの処理を関数化していきましょう!

const initMap = ()=> {
        const customStvList = { // カスタムパノラマの情報を格納する配列
            custompanorama: {
                pano: 'custompanorama',
                description: 'カスタムパノラマ',
                links: [
                    {
                        heading: 195,
                        description: 'カスタムパノラマ2',
                        pano: 'custompanorama2'
                    }
                ],
                lat: 35.6595126,
                lng: 139.7005696,
            }
        };

        const customStv = new google.maps.StreetViewPanorama(document.getElementById('pano'), {});

        const initCustomStv = (pano) => { // カスタムパノラマを生成する関数
            customStv.setPano(pano);
            registerCustomStv(customStv, createCustomStv(
              customStvList[pano].pano,
              customStvList[pano].description,
              customStvList[pano].links,
              customStvList[pano].lat,
              customStvList[pano].lng
            ))
        };

        const registerCustomStv = (stv, stvPanoramaData) => { // カスタムパノラマを登録する関数
            stv.registerPanoProvider(() => {
                return stvPanoramaData;
            });
        };

        const createCustomStv = (pano, description, links, lat, lng) => { // streetViewPanoramaDataを返却する関数

            return {
                location: {
                    pano: pano,
                    description: description,
                    latLng: new google.maps.LatLng(lat, lng)
                },
                links: links,
                copyright: '©︎2019 Google 日本',
                tiles: {
                    tileSize: new google.maps.Size(1024, 512),
                    worldSize: new google.maps.Size(1024, 512),
                    centerHeading: 105,
                    getTileUrl: function (panoId, zoom, tileX, tileY) {
                        panoId = pano;
                        return `img/${panoId}_${zoom}_${tileX}_${tileY}.jpg`;
                    }
                }
            };
        };

        initCustomStv('custompanorama');

}
   initMap();

さて、関数化してみるとなんとなくさっきよりも処理が追いやすくなったのではないかなと思います。

まず、カスタムパノラマを生成するにあたって必要な情報をあらかじめ配列「customStvList」として作成します。

で、「registerCustomStv」の中で引数として指定されたパノラマIDをもとに一致するデータをセットしていくわけですね。

最終的には「initCustomStv」の引数としてパノラマIDを指定すると、そのパノラマIDに応じたカスタムパノラマを生成してくれる処理となっています。

これで、あとは「customStvList」の中にどんどんカスタムパノラマの情報を格納していけばカスタムパノラマを生成することができます。

では、カスタムパノラマの情報を追加してカスタムパノラマの中を移動してみましょう。

const initMap = () => {

        const customStvList = { // カスタムパノラマの情報を格納する配列
            custompanorama: {
                pano: 'custompanorama',
                description: 'カスタムパノラマ',
                links: [
                    {
                        heading: 195,
                        description: 'カスタムパノラマ2',
                        pano: 'custompanorama2'
                    }
                ],
                lat: 35.6595126,
                lng: 139.7005696,
            },
            custompanorama2: {
                pano: 'custompanorama2',
                description: 'カスタムパノラマ2',
                links: [
                    {
                        heading: 25,
                        description: 'カスタムパノラマ',
                        pano: 'custompanorama'
                    }
                ],
                lat: 35.6525126,
                lng: 139.7001696,
            }
        };

        const customStv = new google.maps.StreetViewPanorama(document.getElementById('pano'), {});
        const initialSpot = 'custompanorama';

        const initCustomStv = (pano) => { // カスタムパノラマを生成する関数
            registerCustomStv(customStv, createCustomStv(
              customStvList[pano].pano,
              customStvList[pano].description,
              customStvList[pano].links,
              customStvList[pano].lat,
              customStvList[pano].lng
            ))
        };

        const registerCustomStv = (stv, stvPanoramaData) => { // カスタムパノラマを登録する関数
            stv.registerPanoProvider(() => {
                return stvPanoramaData;
            });
        };

        const createCustomStv = (pano, description, links, lat, lng) => { // streetViewPanoramaDataを返却する関数

            return {
                location: {
                    pano: pano,
                    description: description,
                    latLng: new google.maps.LatLng(lat, lng)
                },
                links: links,
                copyright: '©︎2019 Google 日本',
                tiles: {
                    tileSize: new google.maps.Size(1024, 512),
                    worldSize: new google.maps.Size(1024, 512),
                    centerHeading: 105,
                    getTileUrl: function (panoId, zoom, tileX, tileY) {
                        panoId = pano;
                        return `img/${panoId}_${zoom}_${tileX}_${tileY}.jpg`;
                    }
                }
            };
        };

        const setCustomStvEvent = () => {
            customStv.addListener('pano_changed',()=>{
                let currentPano = customStv.getPano();
                initCustomStv(currentPano);
            });
        };

        const initialize = () => {
            customStv.setPano(initialSpot);
            initCustomStv('custompanorama');
            setCustomStvEvent();
        };

        initialize();
    }


    window.onload = () => {
        initMap();
    }

f:id:ma1129nm:20200414123845g:plain

さて、カスタムパノラマの情報を一つ追加して処理もまとめてみました。

ちゃんとカスタムパノラマ間を移動することができていますね。

矢印をクリックしたら、というイベントはないのですが「pano_changed」というパノラマIDが変わったら発火する、というイベントはあるんですね。

先ほども言った通り、矢印をクリックするとその矢印に紐づいているパノラマIDのカスタムパノラマに遷移しようとする、つまりパノラマIDが書き換わるため、このイベントが発火する、ということになります。

なので、こちらのイベントで先ほど定義したカスタムパノラマを生成する関数「initCustomStv」を次のパノラマIDを引数にして実行してあげれば次のカスタムパノラマに遷移することができるわけです。

ちょっと引っかかるのが「次のパノラマID」だと思いますが、「pano_changed」というパノラマIDが変わったら発火する、というイベントですのでこのイベント内で「getPano()」という現在表示されているカスタムパノラマのパノラマIDを取得するメソッドを実行すると次のパノラマIDが取得できるということですね。

というわけでGoogleMapsAPIの基本的な使い方をご紹介してみました。

有料になっちゃいましたがまだまだ動的に地図を作成したいときには使うことが多いツールだと思いますので少しでも参考になれば嬉しいです。