目次

MapBox GL.js

Web上でjavascriptを使ってぐりぐり動かせる地図というとGoogleMapsが有名だが、カスタマイズ性の高さではMapBoxが変態的なレベルで高い。

MapBoxは、OpenStreetMapsをベースとして、それをアプリや地図上に可視化するマップタイルやAPIを開発・提供しているサービス(という認識でいいのかな)。

Webだけでなく、スマホアプリ組み込み用APIや、Unity用APIも用意されている。ここでの記述はWeb(javascript)に限定する。

料金は、月に50000回のロードまでは無料。小規模なサイトではまず大丈夫。

とりあえず地図を表示する

最初にとにかく使えるようにするには、ログイン後のページで右カラムに見られる「Integrate MapBox」を見ればよい。

<!DOCTYPE html>
<html>
<head>
  <script src='https://api.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.js'></script>
  <link href='https://api.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.css' rel='stylesheet'/>
</head>
<body style="padding: 0; margin: 0;">

<div id="map" style="width: 100%; height: 100vh"></div>
<script>
    mapboxgl.accessToken = 'pk.xxxxxxxxxxxxxxxxxxxxxxxxxxx'

    var map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/AccountName/MapStyleID',
    })
</script>
</body>
</html>

地図をカスタマイズする

上記15行目のstyleがその名の通り地図スタイルのURIなのだが、これは所定のものから選ぶだけで無く、自分でカスタマイズして発行することが出来る。

作る際はさすがに1からではなく、ある程度用意された既定のスタイルを変えていくのがやりやすい。

水系・道路・市区町村・ラベルなど細かくレイヤーが分かれているので、

という指定を行える。

GeoJSONを描画する

これを、そのまま読み込ませて描画させることが出来る。

読み込ませ方にも2通りあり、

1番目の方法は、あらかじめGeoJSONを新たな地図レイヤーとして含んだカスタムマップとして発行する。 JSONがMapBoxの独自形式に圧縮され、しかも表示中の箇所のみ読み込まれるようになるので、 ソースとなるGeoJSON自体のサイズが重たくても、読み込みがかなり軽減される。 MapBox Studio上で、レイヤーとして追加した後のデザインの変更も行える。

一方、2番目のように、javascriptで後付けで読み込ませることも出来る。
例えば、「GeoJSON内にある全ポリゴンについて、ある属性の平均値を算出したい」などの場合は、 1番目の方法だと全てのポリゴンが読み込まれないため、2番目の方法でもよい。 (1番目の方法でも、別途計算結果だけ用意するなどすればよいのだが)

どちらの方法でも、追加したデータは、その属性値などを基準としてjavascriptから動的にデザインをカスタマイズできる。

例えば人口5万人未満の市のみを表示するとか、人口の多寡に従って色をグラデーションしたり等が出来る。

以下に、ポリゴンを含むGeoJSONのレイヤーを、描画済みの地図に動的に追加する方法例を示す。


  let map      // new mapboxgl.Map() で作成した地図がこれに入っているとする
  let geoJson  // geoJsonデータがこれに入っているとする

  // sourceとしてgeoJsonを登録する
  map.addSource('my_source_id', {
    type: 'geojson',
    data: geoJson,
  })
  
  // sourceを使ってレイヤーを追加する
  map.addLayer {
    source: 'my_source_id'
    id: 'my_layer_id'
    type: 'fill'
    paint: {
      // 塗りつぶす色を指定
      'fill-color': {
        property: 'my-column-1',  // 色情報に、geoJsonのpropertiesの'my-column-1'の値を参照する
        stops: [[0, 'white'], [120, 'red']],  // 'my-column-1'が0なら白、120なら赤、その間をグラデーションで色を決める
        default: 'rgba(0, 0, 0, 0)',  // 'my-column-1'がnullなら透明にする
      },
      // 塗りつぶしの透明度を指定
      //   別途、マウスイベントで、ホバー中の要素の'feature-state'の'hover'属性がtrueになるようにしておく
      //   ここでは透明度を、'feature-state'の'hover'属性がtrueなら1, falseなら0.5に指定
      'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 1, 0.5],
    }
  }
  
  // マウスイベントを追加
  // 参考: https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/
  let hovered = null  // ホバー中の要素のIDを記録
  
  map.on('mousemove', 'my_layer_id', e => {
    if (e.features.length === 0) return
    let currentHovered = e.features[0].id
    if (currentHovered === hovered) return
    if (hovered) {
      map.setFeatureState({
        source: 'my_source_id',
        id: hovered,
      }, {hover: false})
    }
    hovered = currentHovered
    map.setFeatureState({
      source: 'my_source_id',
      id: hovered,
    }, {hover: true})
    
    this.map.getCanvas().style.cursor = 'pointer'  
  })
  map.on('mouseleave', 'my_layer_id', () => {
    if (hovered) {
      map.setFeatureState({
        source: 'my_source_id',
        id: this.hovered,
      }, {hover: false})
    }
    hovered = null
    map.getCanvas().style.cursor = ''
  })

途中で参考としてURLを挙げているが、このようにMapBoxが公式で紹介しているサンプルは他にもいろいろあり、コードをそのままコピペして使える。

サンプル毎に、どのように動くかもちゃんと表示されるのが有難い。

若干、caseの条件分岐を配列で指定する書き方などは独自性が強く、調べる必要があるのだが、出来ることの幅はかなり広い。

ハマったところメモ

地図上に描画されたオブジェクトはDOMツリーに構造づけられているわけでもないため、個別のオブジェクトの状態がどうなっているのかを追いづらい。

またエラーもそれなりには出してくれるが、暗黙にスルーされることもあるため、デバッグが少し大変。

IDは整数値

上記の例でも表しているように、feature にIDを与えておくと、map.setFeatureState() でそのIDを指定して個別に状態を変更できる。

geoJson
  ...
  "features": [
    {
      "type": "Feature",
      "id": 0,
      ~~~~~~~~
      "properties": {...},
      "geometry": { "type": "Polygon", "coordinates": [...] }
    },
    ...
  ]
map.addSource('my_source_id', {
  type: 'geojson',
  data: geoJson,
})

// ID:0 を指定して hover というステートを個別に変更する
map.setFeatureState({
  source: 'my_source_id',
  id: 0,
}, {hover: false})

また、レイヤーの設定において、描画する際にそのステートを参照して描画色や透明度などを切り替えられる。

map.addLayer {
  source: 'my_source_id'
  type: 'fill'
  paint: {
    // hoverステートがfalseなら不透明、trueなら半透明
    'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 1, 0.5],
  }
}

ここで、IDは整数値または整数に変換できる文字列でないといけない。

それ以外を与えた場合は、ただ ['feature-state', 'hover'] の返値がnullとなるだけで、エラーは表示されない。

ズームと属性の両方からサイズを決定

円(type: 'circle')を描画するレイヤーは、半径を指定できるが、それは常にブラウザに対して同じサイズで表示され、地図のズームによって調整されるわけではない。 これでは、世界地図レベルでは異様に大きく、市街地レベルでは極端に小さく見えてしまう。

また、featureの属性値によっても半径を変えて、値の大きいfeatureは視覚的にも大きく見せたい、という場合もある。

ズームレベルと属性値の2つから半径を調整するには、以下のように書く。

'circle-radius': [
  "interpolate",
  ["exponential"],
  ["zoom"],
  0, 0,
  20, ['get', 'radius']
],
// zoomレベルが0の時は半径0, 20の時はfeature.properties.radiusの値を用い、その間は補間する

['get', '属性名'] によって、feature.properties.属性名 の値を取得できるので、それを用いる。

また、属性値そのままではなく、簡単に演算した結果を半径としたい場合は、以下を参考に、演算子前置き型の記述で与えることは出来る。
……が、長ったらしくなるので事前に計算しておいてpropertiesに持たせておいた方が楽。