mapbox-gl.js + Vue.js で OpenStreetMap タイルの地図を表示する

この記事は [CivicTechテック好き Advent Calendar 2020](https://qiita.com/advent-calendar/2020/civictech_tech) の 8 日目の記事です。

## この記事について

この記事は Vue.js と [mapbox-gl.js](https://docs.mapbox.com/mapbox-gl-js/api/) を組み合わせて [OpenStreetMap](http://osm.org/) (OSM) タイルの地図を表示する方法についての記事です。

地域課題と結びつくことが多いシビックテックのプロジェクトでは、地図をアプリに組み込む機会が結構あります。
以前 (例えば[初期版の紙マップ](https://github.com/codeforjapan/mapprint/blob/1.0/source/javascripts/all.ts)) は [Leaflet](https://leafletjs.com/) も選択肢でしたが、バイナリベクトルタイルの表示や TypeScript との相性の良さなどから、[現行版の紙マップ](https://github.com/codeforjapan/mapprint/blob/master/components/PrintableMap.vue) をはじめとして、mapbox-gl.js を使ったプロジェクトも増えています。東京都新型コロナウイルス感染症対策サイトで一時的に表示されていた人口推移（参考値）のマップ（現在は非表示）の実装を通して私も[触る機会](https://github.com/tokyo-metropolitan-gov/covid19/blob/43e8bc7f9bbeba5930bf82a732c296250ef6a66a/components/Heatmap.vue) があり、使いやすかったため[新しいプロジェクトでも採用](https://github.com/codeforinzai/mapping-workshop-form/blob/main/components/ClickableMap.vue)したりしています。
本記事では mapbox-gl.js を Vue.js のプロジェクトで使う際に、最初に作る最低限の地図の表示までの実装を紹介します。

Vue.js との組合せは [VueMapbox](https://soal.github.io/vue-mapbox/) など、ライブラリ化されたコンポーネントもありますが、今回は mapbox-gl.js を直接使います。地図を出すコンテナ要素の下に作られる DOM 要素たちのことを特に気にしなければ、それほど相性の悪いライブラリでもないため、素直に組み合わせることができます。

最小限のコードで動く完成品が [CodePen](https://codepen.io/osoken/pen/NWRrxxm) に置いてあります。


## 環境

- Vue.js 2.6.11
- mapbox-gl.js 1.13.0

## 作るもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51853/f303495c-5a02-fbab-0b2c-76167c82bc15.png)


タイルマップの標準的な入力処理と表示だけができるマップです。

### コンポーネントの実装

実装の方針として、テンプレートは `id` 付きの `div` 要素を一つ持ち、 `mouted` フックの中で `mapboxgl.Map` オブジェクトの初期化を行います。`mapboxgl.Map` オブジェクトへのアクセスは頻繁に行いたくなるので、このコンポーネントのデータオブジェクトとして保持しておき、また、マップの初期位置、初期ズームは外から与えたい場合が多いため、コンポーネントのプロパティに持たせます。最後に、mapbox-gl.js では地図を出すコンテナ要素に ID を使うため、1 ページに複数の地図を出したりできるように、この ID もプロパティとして外から与えられるようにしておきます。

これらのことをコードで表現すると以下のようになります。

```javascript

const mapComponent = {
  template: `<div :id="mapId"/>`,
  props: {
    defaultCenterLat: { type: Number, default: 35.6811574199219 },
    defaultCenterLng: { type: Number, default: 139.76702113084735 },
    defaultZoom: { type: Number, default: 14 },
    mapId: { type: String },
  },
  data() {
    return { map: null }
  },
  mounted() {
    this.createMap()
  },
  methods: {
    createMap() {
      this.map = new mapboxgl.Map({
        container: this.mapId,
        zoom: this.defaultZoom,
        style: {
          version: 8,
          sources: {
            OSM: {
              type: 'raster',
              tiles: [
                'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
                'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
              ],
              tileSize: 256,
              attribution : '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
            },
          },
          layers: [{ id: 'OSM', type: 'raster', source: 'OSM' }],
        },
        center: [this.defaultCenterLng, this.defaultCenterLat],
      })
    },
  },
}
```

### 呼び出し方について

あとは Vue コンポーネントとして登録し、`map-component` として `map-id` 指定と共に呼び出せば完成です。

```JavaScript

new Vue({
  el: '#app',
  components: {
    mapComponent,
  }
})
```

```html
<div id="app">
  <map-component :map-id="'map'"></map-component>
</div>
```

複数出す場合、例えば 2 枚の地図を出して、1 枚目と 2 枚目で初期位置を変えるなどする場合は、`map-id` として異なる文字列を与え、 `default-center-lat` や `default-center-lng` にそれぞれ緯度、経度を与えます。

```html

<div id="app">
  <map-component :map-id="'map1'"> </map-component>
  <map-component :map-id="'map2'" :default-center-lat="35.688515514961521" :default-center-lng="139.70118255576324"> </map-component>
</div>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51853/ccda7176-b8a6-7916-c712-257410006c84.png)


### スタイルについて

コントロールや帰属表示のスタイリングに、 mapbox-gl.js では専用の CSS を持っています。
https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css を `link` タグで読み込むか、mapbox-gl モジュールにバンドルされているものを `mapbox-gl/dist/mapbox-gl.css` からインポートする必要があります。

また、地図を出すコンテナの大きさが 0 だと、全く地図が描画されないように見えるのですが、JavaScript 側のバグを疑って時間を浪費してしまうことが多かったため、開発中は CSS の ID 指定でコンテナ要素の大きさを指定するなどしています。

```CSS

#map {
  height: 500px;
  width: 960px;
}
```

## まとめ

Vue.js と mapbox-gl.js を使って OSM タイルの地図を表示する方法の一つを紹介しました。ここから機能拡張していく前提で、`mapboxgl.Map` オブジェクトをデータオブジェクトに持ち、コンテナの ID と、初期の中心とズームをプロパティとして外部から与えられるだけの、簡素なものとして実装しました。