ベクトルタイル用スタイルの書き方メモ（Mapbox, Maplibre, ArcGIS API for Javascript）

# はじめに
ウェブ地図の分野ではラスタタイルだけでなく、ベクトルタイルが多く使われるようになってきています。ベクトルタイルは、machine readableですし、一つのソースデータから複数のデザイン（スタイル）の地図表現が可能であったりと、いろいろとメリットがあります。一方で、**スタイルを自分で作らないといけない（あるいは見つけないといけない）** という点がラスタタイルとの大きな違いです。

ベクトルタイルのスタイルは、json形式で、MapboxさんやMapLibreさんのスタイル仕様に則して記載することがデファクトスタンダードになっています。最近ではスタイルを書くのに便利なツール（例えばunvt/charites）も出てきています。

ここでは、スタイルを作成する際の参考として、私がよく使ったりするスタイルの表現方法をメモしたいと思います。いいメモになるのと、自分の頭の整理をするのにも役立つということでQiitaに書いてみますが、私と同じように初心者からスタートする方の参考になれば幸いです。

# 参照するStyle仕様
私はMapboxさんとMapLibreさんの2つのスタイル仕様に則してスタイルの記載をしています。MapLibreさんの設立の経緯を踏まえると、2つのスタイル仕様で共通点が多いですので、現時点ではどちらでも使えるスタイル表現を使うように心がけています。

* [Mapbox GL JS の Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/)
* [MapLibreのStyle Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/)

2022年1月現在、mapboxさんのスタイルはv.13.23.0、MapLibreさんのスタイルはv.14.0.0のものが上記URLから公開されています。

また、ArcGIS API for Javascriptでのベクトルタイルのスタイル仕様も、ほとんどmapboxのスタイル仕様と同じということなので、mapboxとMapLibreの共通のスタイルを使うとおおよそ動きます（使えない表現もあるようで、100%同じではないようです）。


# Style
スタイルはJSONファイルです。作成に当たってはテキストエディタで直接書いたり、ツールをつかってYAML形式のファイルからJSONを作ったりします。以下では、ルート要素でよく使うものを簡単に紹介したのち、特にlayersの要素のについて少し細かく説明します。


## ルート要素の一覧（主なもの）

ルートレベルでよく使う要素を以下にリストしました。全てを網羅しているわけではないですが、私が使うのはだいたい以下のものです。


|要素名  |必須か  |私の感想・コメント  |
|---|---|---|
|version|**必須**|8|
|name|任意|スタイルの名前。つけてもいいし、つけなくてもいい。|
|sprite|任意|地図記号や、線やポリゴンの塗りなどに使う画像の場所を指定します。仕様上は任意ですが、我々の業務では必須です。 |
|glyphs|任意|フォントの場所の指定。文字を使う際に必要です。仕様上は任意ですが、我々の業務では必須です|
|sources|**必須**|ソースを指定。最大・最小ズームレベルやデータAttributionも記載可能。複数のソースを記載することも可能ですが、ArcGIS OnlineやQGISのプラグインでは一つのソースしか対応していないと思います。 |
|layers|**必須**|ベクトルタイルのレイヤを記載する。まさに地図のスタイルで大事な部分です。|
|center|任意|最初に表示するときの中心|
|zoom|任意|最初に表示するときのズームレベル|
|metadata|任意|ArcGISオンラインや地理院地図Vectorなどで、スタイル仕様に書き切れない表現をmetadataとして記載していることがあるように思います。|

Mapboxのスタイル仕様では、これらに加えて、terrainやprojectionなどの要素もあり、土地の3D効果や投影法の関係の設定が出来ます（mapbox gl js ライブラリのみ。mapboxのトークン等が必要）。これらはmapbox gl js version 2以降で開発されたものなので、MapLibreやArcGIS API for Javascriptにはありません。

また、例えばEsriさんのArcGIS Onlineでは "_ssl" という要素を使うなど、ライブラリによって独自の拡張をしている例も散見されます。


## layers以外のルート要素についてのメモ（網羅的にあるわけではありません）

### glyphs
URLで場所を指定します。テキストフォントはpbf形式のものを使います。オープンソースのpbfフォントもありますが、ttf/otf形式からpbf形式のテキストフォントの作り方は、以前以下のページにメモを作りました。（フォントのライセンスにも気をつけましょう。）
https://speakerdeck.com/ubukawa/2021-07-28-unvt-exercise-text-font

### sources
ベクトルタイルのソースはtilesとしてpbfデータのURLを指定するか、urlとしてjsonの場所を指定する方法があります。ArcGIS Onlineではこのurlを使って、VectorTileServerへのパスを指定します。
ソースは複数のソースを記載することも可能なのですが、ArcGIS OnlineやQGISのベクトルタイルプラグインでは複数のソースに対応していなかったと思います。（私は、tippecanoeのtile-joinで複数のソースをマージしました。）

## layersについてのメモ（網羅的にあるわけではありません）
layersは一番よく使うというか、重要な部分なので、少し細かくメモをしたいと思います。

### id
layersの中に複数のレイヤを記載していきます。これらはidで特定されます。レイヤのidを指定する際、重複してはいけません。重複すると地図がでません。
また、idの名前にスラッシュ（/）を入れることも可能ですが、unvt/charitesはidをyamlファイルの名前として使うので/があると少し問題になりました（今後改良予定？？）。ArcGIS Proでベクトルタイルのスタイルを作るとたまにスラッシュが入ることがあるようです。

### type
fill, line, symbolなどがあります。ラベルやアノテーションはsymbolにします。（spriteを使った塗りや線や要注意）
また、ポリゴンの3D（建物など）はfill-extrusionにしますが、ArcGIS Onlineでは建物の3D表現は出来ませんから（画面が傾かない）、普通のfillにします。

### filter
ベクトルタイルの値を使って表示するデータを選ぶ際にはfilterは便利です。ベクトルタイルレイヤの地物を全て表示するときにはfilterは不要です。いくつか例を見てみます。

#### filter(1): 条件が一つだけのフィルター
最初に紹介するのがシンプルなフィルターです。これはArcGIS Proでスタイルを作ったときによく見られるフィルターです。"=="以外にも、"in"とか、"!="とかの条件も使えると思います。

```json:esriさんのLivingAtlasより
"filter" : ["==", "_symbol", 1],
```

#### filter(2): 複数の条件を使うフィルター
all、anyやnoneを使って、複数の（一つでも可）条件を組み合わせることも出来ます。

```json:例えば2つの条件で絞るとき
"filter": [
  "all",
  ["==", "type", 1],
  ["==", "perenniality", 1]
],
```

maputnik editorを使ってフィルターを書くと、一つの条件でも自動的にこのような表現（allとかanyがついている）になります。例えば、次の例はもっとシンプルに ["==", "ftCode", 5000] と書いてもいいような気もします。

```json:地理院地図Vector（仮称）の標準地図風スタイルより
"filter":["all",["in","ftCode",5000]],
```

#### filter(3): matchを使ったフィルター
もう少し高度な技としては、matchを使うという方法があります（[mapbox仕様の説明はこちら](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#match)）。
matchで細かいフィルタリングをするという前提だと、ベクトルタイルの構造をかなりシンプルにすることもできると思います。ただ、このmatchはArcGIS Onlineでは上手く反応しないと思いますので、自分の使うライブラリでの対応はよくチェックしましょう。
以下の例ではgeometry-typeを判断してそれがポリゴンやマルチポリゴンであればtrueを返し、違えばfalseを返すという方法です。match、属性、値、真の時、偽の時という感じで書きます。
また下の例の、もう一つのポイントとして、geometry-typeというものですが、これはソースデータでこの属性を作っていなくても、ベクトルタイルの地物がもっている属性のようです。点と線など、異なるタイプの地物を同じベクトルタイルレイヤにして、そこからフィルターを使って描画するということも可能です。（例えば、waterAreaとwaterLineを異なるベクトルレイヤにせず、一つのwaterというレイヤにすることも可能。）

```json:optgeoのkokoromiシリーズより
"filter": ["match", ["geometry-type"], ["Polygon", "MultiPolygon"], true, false],
```
matchで属性値を評価するためには、getを使わないといけません。

```json:matchの例
"filter": ["match", ["get", "gridcode"], [20, 30, 80], true, false],
```

matchの中に、matchを入れたりcaseを入れたりすることも可能です。自分に適したフィルターの方法を探しましょう。

```json:matchとcaseを使った例
"filter": [
 "match",
  ["get", "z_order"],
  [15, 13, 11],
  ["case", ["has", "tunnel"], true, false],
  false
],
```
上の例はallを使ったフィルターでも出来るかもしれませんね。

```json:上と同じかな（テストしていません）
"filter":[
   "all",
   ["in","ftCode", 15, 13, 11],
   ["has","tunnel"]
],
```

### paintなどで使える便利な表現
#### stops
例えば、あるズームレベルからあるズームレベルにかけて、徐々に値を変えたい場合は以下のようなstopsが使えます。下の例は、ZL3より小さいと#D6EBF5、ZL3からZL5にかけて徐々に色が変わり、ZL5より大きいと#FAF9F6になるような色の表現です(maputnikでもよく使いますし、ArcGISでもよく使っています。)。基準となるズームレベルは2つだけでなくて、3つ以上でも使えます。

```json:
"line-color": {"stops": [[3, "#D6EBF5"], [5, "#FAF9F6"]]},
```
アイコンや塗りのopacityの値の設定などでもよくstopsを使います。

```json:
"icon-opacity": {"stops": [[13, 0], [14, 1]]},
```

#### step
zoomレベルに応じて、段階的な値を返します。stepと書いて、その後現在のズームを返すzoomと書いた後に、ストップアウトプット0，ストップインプット1、ストップアウトプット1、というように続けていきます。アウトプットは数値であることが必要なようですね。mapboxの説明は[こちら](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#step)。

```json:
"text-offset": [
  "step",
  ["zoom"],
  ["literal", [0, -0.5]],
  16,
  ["literal", [0, -1]],
  17,
  ["literal", [0, 0]]
],
```
#### interpolate
線の太さなどの数値の調整に使いますが、数値データをズームレベルの間でスムーズに変えられます。内挿の計算にはlinearやexponentialがあるので、好きな方法を選べます。特に、指数関数的に値を変更できるので地図の縮尺とともに線を太くしたい場合などに便利です。（[mapboxの説明](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#interpolate)）

```json:
"line-width": [
  "interpolate",
  ["exponential", 2],
  ["zoom"],
  6,
  1,
  22,
  64
],
```
#### match
paintなどでもmatchが使えます。例えば、ベクトルタイルの属性に応じて色を変えたいときなどに使います。ただし、ArcGISでは思ったように動かなかったかもしれません（バージョンによるのかもしれませんが）。
以下の例ではgridcodeが20のとき、30のとき、それ以外のときに分けて色を指定しています。matchが使えないと塗りたい色事にスタイルレイヤを作らないといけないですが、この表現が使えるとスタイルレイヤの数を減らすことが出来ます。

```json:gridcodeに応じて色を決める例
"fill-color": [
   "match",
   ["get", "gridcode"],
   20,
   "#DAEDD0",
   30,
   "#ECF3DD",
   "#F6EFEF"
]
```

#### 色の指定について
paintなどでよく色を指定しますが、ライブラリや使うツールによって指定の仕方に少し特徴があるようです。
例えば、ArcGISではRGBのインデックスカラー、maputnikはRGBAカラーでの指定など、癖があるようです。
rgbaの透過に対応していないソフトがあったり、ソフトやライブラリによって癖があるようなので、気をつけましょう。


#### 建物に高さを与えるとき（fill-extrusion）
fill-extrusionのレイヤを作った場合（高さのある建物など）には、paintの中でfill-extrusion-heitghtを与えることが出来ます。height属性が入っている場合にはその高さか5m、heightがなければ建物の階数に3.66mをかけたものか5mというような感じで建物に高さを与える場合は以下の様な表現になります。

```json:
"fill-extrusion-height": [
   "case",
   ["has", "height"],
   ["max",
      ["to-number",["get", "height"]],
      5
   ],
   ["max",
      ["*", ["to-number", ["get", "building_levels"]], 3.66],
      5
   ]
],
```


### layout

#### text-optional
layoutの要素で大事なものはほかにもありますが、text-optionalの効能について感動したのでメモします。
これはデフォルトではfalseですが、falseだとテキストがぶつかったときにアイコンも一緒に表示されなくなってしまいます。これをtrueにしておくと、例えば町の表示などでテキストが出せないときにアイコンだけ出してくれるのでうまく活用しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/d165ddbf-cb98-ef78-0783-a777ac04bc43.png)


### maxzoomやminzoom
layerの表示範囲を決めるのに、maxzoomとminzoomを指定します。オーバーズーミングがあるので、ソースの最大ズームレベルより大きいmaxzoomを指定することも可能です。
一つのレイヤ中で、属性に応じてminzoomを決めたいとおもってmatchを試したことがありますが、maxzoomやminzoomではmatchは効かないようでした。属性ごとにminzoomを調整したい場合は、データ変換の段階で調整した方が良いかもしれません。

# まとめ
途中で力尽きましたが、ここではスタイルを書く際に便利な表現をいくつか紹介しました。
実際のスタイリングは最近は[unvt/charites](https://github.com/unvt/charites)を使ってYAMLファイルで書きますが、ここで紹介しているのは基礎のようなものなので、自分のメモとしても記録しておきます。


（追記）以下に新しい記事も書いたので、リンクをメモしておきます。

https://qiita.com/T-ubu/items/961176fb92fb66a927e0

# References
* [Mapbox GL JS の Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/)
* [MapLibreのStyle Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/)
* ArcGIS Living Atlasのスタイル・[World_Basemap_v2のデフォルトスタイル](https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/resources/styles)
* [optgeo/kokoromi-nz](https://github.com/optgeo/kokoromi-nz)
* [unvt/charites](https://github.com/unvt/charites)