「Software Design 2023年12月号 位置情報エンジニアリングのすすめ 第5回」をMapbox GL JSで実装してみる
はじめに
前回に引き続き、Software Designで連載中の「位置情報エンジニアリングのすすめ」をMapbox GL JSで実装してみます。
地図の表示
環境構築
紙面の通り、Viteをインストールします。
% npm create vite@latest webmap
Need to install the following packages:
create-vite@5.0.0
Ok to proceed? (y) y
✔ Select a framework: › Vanilla
✔ Select a variant: › TypeScript
Scaffolding project in /Users/yochi/Downloads/20231130/webmap...
Done. Now run:
cd webmap
npm install
npm run dev
上記で指示された通りコマンドを打っていきます。
% cd webmap
% npm install
% npm run dev
Mapbox GL JSのインストール
下記コマンドでMapbox GL JSをインストールします。
% npm install mapbox-gl
MapbLibre GL JSはTypeScriptで記述されているそうですが、残念ながらMapbox GL JSはJavaScriptです。そこで、以下のように型定義ファイルもインストールします。
% npm install @types/mapbox-gl
地図の表示
HTMLは紙面の通り記述します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mapbox GL JSでWeb地図</title>
</head>
<body style="margin: 0;">
<div id="map" style="height: 100vh;"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
JavaScriptは以下のとおりです。違いはライブラリの名称と、Map
オブジェクトのインスタンス化の際にオプションとしてaccessToken
を設定することです。
import { Map } from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'
const map = new Map({
accessToken:'YOUR_PUBLIC_TOKEN',
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [143.95, 43.65],
zoom: 6,
});
ポイントを表示
データの取得
以下からHokkaidoの.shp.zipをダウンロードします。
紙面の通りGeoJSONに変換します。
% ogr2ogr -f GeoJSON poi.json gis_osm_pois_free_1.shp
データの表示
変換したGeoJSONを表示します。紙面ではMap
オブジェクトのインスタンス化の際に直接Styleとして記述していました。今回、streets-v12
を使っている都合上、Map#addSource
、Map#addLayer
で追加します。コードは以下のようになります。
import { Map } from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'
import poiGeojson from './poi.json';
const map = new Map({
accessToken:'YOUR_PUBLIC_TOKEN',
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [143.95, 43.65],
zoom: 6,
});
map.on('style.load', () => {
map.addSource('poi', {
type: 'geojson',
data: poiGeojson,
});
map.addLayer({
id: 'poi',
type: 'circle',
source: 'poi',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000',
},
});
});
クラスタ化
クラスタの使い方
クラスタは複数のPointデータを集約し、一つのPointデータとしてソースに追加されます。そのため、まずaddSource
でクラスターであることを宣言します。
map.addSource('poi', {
type: 'geojson',
data: poiGeojson as any,
cluster: true, //ここ
clusterMaxZoom: 15,
});
あとはデータとして存在していることになるのでレイヤーにソースを使用することで表示できます。
以下のサンプルコードが参考になります。
コード
クラスタ化も基本的にMapLibre GL JSと同じコードで動きます。
import { Map } from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'
import poiGeojson from './poi.json';
const map = new Map({
accessToken:'YOUR_PUBLIC_TOKEN',
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [143.95, 43.65],
zoom: 6,
});
map.on('style.load', () => {
map.addSource('poi', {
type: 'geojson',
data: poiGeojson as any,
cluster: true,
clusterMaxZoom: 15,
});
map.addLayer({
id: 'poi',
type: 'circle',
source: 'poi',
paint: {
'circle-radius': [
'interpolate',
['linear'],
['get', 'point_count'],
1, 10,
500, 50,
2000, 70,
],
'circle-color': '#aaaaff',
'circle-stroke-color': '#000000',
'circle-stroke-width': 1,
'circle-opacity': 0.8,
},
});
});
ここでは以下のcircle-radius
のExpressionsを見ていきます。
'circle-radius': [
'interpolate',
['linear'],
['get', 'point_count'],
1, 10,
500, 50,
2000, 70,
],
interpolate
は補間です。第1引数は補間の方法です。ここではlinear
が指定されているので線形補間です。第2引数が入力値です。ここではget
を使用してpoint_count
プロパティの値を取得し入力値として使用します。それ以降の引数は入力値,出力する値
のペアとなります。例えば1,10
はpoint_count
が1
の時、円の半径が10
となります。また、線形補間となるので、point_count
の値が1
〜500
の時、半径は10
〜50
の間で線形補間されます。
結果は以下のようになります。
建物データ、道路ラインをベクタータイルに変換
紙面では建物ポリゴンを表示してから道路ラインも表示するというステップを踏んでいますが、ここではまとめてやってしまいます。GeoJSONSeq
と指定すると出力形式がndjsonになります。ndjsonでは1行1フィーチャーになります。
% ogr2ogr -f GeoJSONSeq building.jsonl gis_osm_buildings_a_free_1.shp
% ogr2ogr -f GeoJSONSeq road.jsonl gis_osm_roads_free_1.shp
次にtippecanoeでタイルセットに変換します。並列処理を行う-Pオプション
がline-delimited JSON (ndjson)のときのみ使用可能です。そのため、先ほどGeoJSONSeq
を指定しました。また、-M
オプションは一つのタイルサイズの上限値です。デフォルトでは500KBなので、5MBは相当大きい値です。
% tippecanoe -e tiles -pC -pf -M 5000000 -P -L road:road.jsonl -L building:building.jsonl
建物データ、道路ラインの表示
Mapbox GL JSに関する部分およびstyle.load
に関する部分以外は紙面の通りコードを書きます。fill-extrusion
は高さ情報を用いて建物等を3D表示できるレイヤーです。
import { Map } from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'
import poiGeojson from './poi.json';
const map = new Map({
accessToken:'YOUR_PUBLIC_TOKEN',
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [143.95, 43.65],
zoom: 6,
});
map.on('style.load', () => {
map.addSource('vectortile', {
type: 'vector',
tiles: [
`${window.location.origin}/tiles/{z}/{x}/{y}.pbf`,
],
maxzoom: 14,
});
map.addLayer({
id: 'building2',
type: 'fill-extrusion',
source: 'vectortile',
'source-layer': 'building',
paint: {
'fill-extrusion-color': '#a66',
'fill-extrusion-height': 10,
'fill-extrusion-opacity': 0.6,
},
});
map.addLayer({
id: 'road2',
type: 'line',
source: 'vectortile',
'source-layer': 'road',
paint: {
'line-color': [
'case',
['==', ['get', 'fclass'], 'primary'], '#f00',
['==', ['get', 'fclass'], 'secondary'], '#ff0',
['==', ['get', 'fclass'], 'teriary'], '#0a0',
['==', ['get', 'fclass'], 'residential'], '#00f',
'#000',
],
'line-width': [
'case',
['==', ['get', 'fclass'], 'primary'], 10,
['==', ['get', 'fclass'], 'secondary'], 8,
['==', ['get', 'fclass'], 'teriary'], 6,
['==', ['get', 'fclass'], 'residential'], 4,
2,
],
},
});
});
結果は以下のようになります。
まとめ
今回も、MapLibre GL JSとほとんど同じコードで動くことがわかりました。
Appendix A. 第4回をクラスタ化
せっかくなので第4回で作成したものをクラスタ化しました。
複数種類のデータ(school_type
)が一つのGeoJSONに含まれているため、fetch
でGeoJSONをダウンロードしてからaddSource
の際にデータをフィルタリングしています。
const response = await fetch("https://gist.githubusercontent.com/OttyLab/f4526ddf444b8f4add296ad337bcc601/raw/58732f3b6b3479d2f50b015fc1167dcfaeb238fe/merge.geojson");
const data = await response.json();
schools.forEach(s => {
map.addSource(`source_point_${s.type}`, {
type: "geojson",
data,
filter: ["==", ["get", "school_type"], s.type],
cluster: true,
clusterMaxZoom: 15
});
Appendix B. tippecanoe
tippecanoeはもともとMapboxで開発されていました。しかし、メインで開発を行っていたエンジニアが転職したため、現在はFeltで開発が継続されています。brewでインストールした場合、feltの方(つまり最新)が使用されます。
Appendix C. consoleに表示されるエラー
Vite v5 + Mapbox GL JSではconsoleに以下のエラーが表示されます。
Uncaught Error: Unimplemented type: 4
at lm.skip (3519c730-a3f6-4996-9e1f-531464dd0686:9404:21)
at lm.readFields (3519c730-a3f6-4996-9e1f-531464dd0686:9266:75)
at new Gf.VectorTile (3519c730-a3f6-4996-9e1f-531464dd0686:9131:28)
at 3519c730-a3f6-4996-9e1f-531464dd0686:27067:145
at Object.fn (3519c730-a3f6-4996-9e1f-531464dd0686:17358:17)
at fw.process (3519c730-a3f6-4996-9e1f-531464dd0686:16857:18)
at MessageChannel._channel.port2.onmessage (3519c730-a3f6-4996-9e1f-531464dd0686:16812:45)
これは存在しないタイルを取得しようとした際に、Viteが404エラーではなくindex.htmlを返してくることに起因しています。Mapbox GL JSはダウンロードしたデータをベクタータイルとしてパースしようとしてエラーが発生しています。ちなみに、Vite v4では404エラーとなります。MapLibre GL JSではエラーは表示されないので、例外処理を行っていると予想されます。
Discussion