Web上でjavascriptを使ってぐりぐり動かせる地図というとGoogleMapsが有名だが、カスタマイズ性の高さではMapBoxが変態的なレベルで高い。
MapBoxは、OpenStreetMapsをベースとして、それをアプリや地図上に可視化するマップタイルやAPIを開発・提供しているサービス(という認識でいいのかな)。
Webだけでなく、スマホアプリ組み込み用APIや、Unity用APIも用意されている。ここでの記述はWeb(javascript)に限定する。
料金は、月に50000回のロードまでは無料。小規模なサイトではまず大丈夫。
最初にとにかく使えるようにするには、ログイン後のページで右カラムに見られる「Integrate MapBox」を見ればよい。
<div>
を用意して、数行のコードでそこに地図を描画するよう指定できる
<!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からではなく、ある程度用意された既定のスタイルを変えていくのがやりやすい。
水系・道路・市区町村・ラベルなど細かくレイヤーが分かれているので、
という指定を行える。
これを、そのまま読み込ませて描画させることが出来る。
読み込ませ方にも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ツリーに構造づけられているわけでもないため、個別のオブジェクトの状態がどうなっているのかを追いづらい。
またエラーもそれなりには出してくれるが、暗黙にスルーされることもあるため、デバッグが少し大変。
上記の例でも表しているように、feature
にIDを与えておくと、map.setFeatureState()
でそのIDを指定して個別に状態を変更できる。
... "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に持たせておいた方が楽。