🤖

GoogleMap のマーカーと、HTMLのリストデータの連動(Vanilla JavaScript)

2020/04/29に公開

※機能追加しました。
GoogleMap のマーカークリック時にアイコンが変更するようにしました。(スマホ対応注意点あり)

1. 作ったきっかけ

BMW 公式サイトのディーラー検索ページの UI がよくできているとおもったので、これを実装することにしました。

https://bmw-dealer.jp/dealer-locator/

具体的には、地図上のマーカーをクリックすると、左側のディーラーリストのうち、該当の店舗のリスト情報がハイライトされる。また、店舗リストの情報をクリックすると該当のマーカーが地図上でハイライトされる(実際には BMW のマークに代わる)

BMW公式ディーラー検索

よく見かける例として、地図のマーカーをクリックすると、InfoWindow(吹き出し)に情報が表示されるものがありますが、リストと地図が連動するこの形の方がよいと思い、今回の開発に至りました。

2. 作成したページ

作成したページがこちら
https://myfitness-site.netlify.app/map/index.html

画面下のリストの部分と、地図上のマーカーが相互に連動します。
(英語版ですが、日本語表示の変更ももちろん可能です

フィットネスジム検索サンプルページ

3. コードの説明

ソースコードは GitHub にアップしました。特に map フォルダ以下のコードを参照してください。
https://github.com/IoT-Arduino/MyFitness

<a id="markdown-31-json-データの取得" name="31-json-データの取得"></a>

3.1. json データの取得

ローカルにある json データから地図上に表示するジムのデータを取得します。
(GetData クラスの getLocation メソッド。)
json データは事前に作成しておきます。関連表示する画像データも image フォルダに保存しておきます。

fetch メソッドで、事前に定義した json ファイルのデータを取得し、map 関数を利用して、makers 配列にデータを格納し、以降の処理で利用できるようにします。

./map/main.js
class GetData {
  async getLocation() {
    try {
      const result = await fetch("location.json");
      const data = await result.json();
      let markers = data.items;

      markers = markers.map((item) => {
        const {
          title,
          latitude,
          longitude,
          address,
          id,
          category,
        } = item.fields;
        const image = item.fields.image.fields.file.url;
        return { title, latitude, longitude, address, id, category, image };
      });
      return markers;
    } catch (error) {
      console.log(error);
    }
  }
}

3.2. Map の初期化

initMap メソッドで、GoogleMap を初期化します。
(初期ズーム、地図の中心等が定義されます)

./map/main.js
// Mapの初期化
function initMap() {
  const myOptions = {
    zoom: 13,
    center: new google.maps.LatLng(37.789096, -122.40217),
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
  const map = new google.maps.Map(
    document.getElementById("map_canvas"),
    myOptions
  );

  // 中略
}

3.3. リスト要素へのイベントリスナー付与

先ほど定義された marker 配列データから、地図上に表示するマーカーと li リストのデータを生成します。また、この li リストに対し、イベントリスナーを登録します。(リストをクリックすると、対応するマーカーが反応し、InfoWindow(吹き出し)が表示され、地図の中心に表示されます)

./map/main.js
function initMap() {
  // 省略

  const markers = new GetData();
  // marker配列データからマーカーの作成およびli要素の作成
  markers
    .getLocation()
    .then((items) => {
      items.forEach((item) => {
        const id = item.id;
        const name = item.title;
        const latlng = new google.maps.LatLng(item.latitude, item.longitude);
        const address = item.address;
        const image = item.image;

        createMarker(name, latlng, map, id);

        maplistUl.innerHTML += `<li><img src=${image} /><div> <h4>${name}</h4> Address : ${address}</div></li>`;
      });
    })
    // 生成されたli要素へのイベントリスナーの付与。
    .then(() => {
      maplistLi = document.querySelectorAll("#mapList li");
      maplistLi.forEach((listItem, index) => {
        listItem.addEventListener("click", () => {
          google.maps.event.trigger(maplist[index], "click");
        });
      });
    });
}

3.4. マーカーをクリックしたときの処理

今度は、マーカーをクリックしたときに InfoWindow(吹き出し)の表示と、マーカーに関連した、リストデータの背景を変更する処理を記述します

先ほどのリストのイベントリスナーの処理とこちらの処理の記述で相互に連動する、動きが実現されます。

./map/main.js
function createMarker(name, latlng, map, id) {
  const infoWindow = new google.maps.InfoWindow();
  const marker = new google.maps.Marker({ position: latlng, map: map });

  google.maps.event.addListener(marker, "click", function(e) {
    // クリック済みのMakerに対応するliリストのCSS背景を初期化
    maplistLi.forEach((item) => {
      if (item.classList.contains("clicked")) {
        item.classList.remove("clicked");
      }
    });

    if (currentWindow) {
      currentWindow.close();
    }
    infoWindow.setContent(name);
    infoWindow.open(map, marker);
    currentWindow = infoWindow;
    map.panTo(latlng); //markerをクリックした時に地図の中心に

    // クリックされたMarkerに対応するli要素のcss背景を操作する。
    maplistLi[id - 1].classList.add("clicked");
  });
  maplist[cnt++] = marker;
}

4. 躓いた点等

4.1. jQuery から Vanilla JavaScript の違い

参考にしたサイトのコードは jQuery で書かれていました。jQuery では、DOM セレクタで取得された li 要素(NodeList)に対して、直接イベントリスナーを定義できますが、VanillaJS では、NodeList に対して、いったん forEach で個別の要素に展開してから、それぞれの要素にイベントリスナーを登録する必要があります。

4.1.1. jQuery のコード

//***** JQuery Sample *****
$(function() {
  $("#mapList li").click(function() {
    var no = $("#mapList li").index(this);
    google.maps.event.trigger(maplist[no], "click");
  });
});

4.1.2. VanillaJS のコード

// ***** VanillaJS で書き換え*****
document.addEventListener("DOMContentLoaded", function() {
  maplistLi.forEach((listItem, index) => {
    listItem.addEventListener("click", () => {
      google.maps.event.trigger(maplist[index], "click");
    });
  });
});

※ES6 になり、jQuery でできることは、Vanilla JavaScript でもほぼ同様にできるようになりましたが、微妙に違う点もあるので注意が必要です。

4.2. Map 表示のレスポンシブ対応

GoogleMaps のサンプルコードの多くは、サイズが height と width が 400px,600px 等で固定値で設定されているが、スマホでの見た目等も考慮して、レスポンシブ対応にしました。

HTML は .mapContainer > .mapComponent > #map_canvas の 3 階層構成
それぞれの要素に対して、以下の CSS を付与しました。

./map/style.css
.mapContainer  {
    width: calc(100% - 20px);
    margin: 100px auto 80px;
}

.mapComponent {
    position: relative;
    width: 100%;
    margin: 20px auto 50px;
    padding-top: 65%;
}

#map_canvas  {
    position: absolute;
    top: 0;
    margin: 20px auto 10px;
    width: 100%;
    height: 100%;
    box-shadow: 10px 10px 10px rgba (190,  190,  190,  0.4);
}

5. 利用にあたっての手順

  1. maps フォルダのデータをコピーして、

  2. GoogleMapsAPIKey の取得と API 利用制限の設定

  3. HTML ファイルでの script 読み込み設定
    (API キーを自分で取得したものに、書き換えてください)
    以下リンクの手順を参考に対応することができます。
    https://www.zenrin-datacom.net/business/gmapsapi/api_key/index.html

  4. アセットファイル(location.json と images ファイルの変更)
    サンプルでは 5 か所のデータですが、増やすことも減らすことも可能です。

  5. HTML ファイル、css ファイルの変更
    必要に応じて適宜変更してください。

5.1. 今後の発展

getLocation メソッドの fetch 関数の引数あたりを変更することで、ローカルの json ファイルだけでなく、データベース API との連携も可能になります。

6. 参考サイト

配列を使用して GoogleMap に複数のマーカと情報ウィンドウを表示する(その2)の
サンプル C「マップ外のリストからウインドウを開く場合」
https://ghweb.info/post-3762.html

Google Map をレスポンシブ対応に
https://www.webdesignleaves.com/wp/htmlcss/641/

公式ドキュメント:Google Maps JavaScript API
https://developers.google.com/maps/documentation/javascript/tutorial?hl=ja

Discussion