ベクトルタイル用スタイル・Expressionsについて（MapBoxスタイル仕様、MapLibreスタイル仕様）


# １．はじめに
ベクトルタイルのスタイルの話題です。この記事では、Mapboxスタイル と MapLibreスタイル で共通ですが、スタイルのExpressionについて書きたいと思います。（私はあまり詳しく見たことがなかったので、もう少し見てみようという趣旨。）
細かいところのメモなので、少し進んだ初心者の方向けです。

https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions

https://maplibre.org/maplibre-gl-js-docs/style-spec/expressions

## 動機
私もそうですが、ベクトルタイルのスタイルを書くときに、Maputnik でのスタイル作成や ArcGIS Vector Map のスタイルファイル （たとえば[こちらのWorld_Basemap_v2スタイル](https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/resources/styles
)） を参考にしている人も多いと思います。

しかし、一方で、Mapbox や MapLibre のスタイル仕様（mapbox とmaplibre）を読み込んでみると、Maptnik や ArcGIS Vector Map で使われているスタイル表現では、mapbox v0.41.0 以降（MapLibreがMapboxから分化したのはそれ以降なのでMapLibreでも共通）で奨励されていないスタイル表現が使われています。（例：線幅のstops での設定など）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/c7a01ed1-4a91-333f-7573-7a89fddf7a36.png)

ここでは、一歩先に進むために、**propetry expressiExpressionExpression**に準じた表現方法を少し考えてみたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/ff5c86c3-ec4f-6848-71b0-3fb8ba735b8d.png)

## 期待したい効果
推奨しているスタイル表現を使うことになるので、もしかしたら描画の速度が上がるといいなぁと期待しています。
~~また、filterの方法を見直すことで、レイヤ数を減らせるとよいなと思います。（レイヤ数が減れば描画速度が速くなるというわけでもなさそうなのですが、数百あるレイヤ数を少しでも効率的にまとめたいです。）~~

10月17日・追記
レイヤ数が減ると描画が早くなるのかということについて、以下に非常に素晴らしい記事を見つけました。自分のメモ用に、ここにリンクを書いておきます（すごいと思います）。

https://qiita.com/mg_kudo/items/f9b6ab97b2ab670b638c


## Mapbox Style Specification とMapLibre Style Specification について
前でも述べましたが、MapLibre GL JSは Mapbox GL JS v.1.13から分化していますが、スタイル仕様はそれ以前にはほぼ現在の形ができていたようで、基本的な表現に関しては両者のスタイル仕様はほとんど同じかと思います。（少なくともExpressionのところは同じに見えます。）

（追記）
下の方に書きましたが、例えば、dash-array にmatchを使えるかというところについて、mapboxとmaplibreで差がついたように、細かいところでは違いがあるかもしれません。（仕様上は明記されていないので、仕様としては違わないのですが、実装の部分で微妙は差があるかもしれませんね。）


# ２．Expressions とは
## スタイル仕様の該当箇所
Expressionについては、以下のところに書いてあります。
https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/
https://maplibre.org/maplibre-gl-js-docs/style-spec/expressions/

## 使える範囲
スタイル仕様のExpressionsの説明をみると以下のように書いてあります。
> You can define the value for any layout property, paint property, or filter as an expression.

ここで大事なのは（大事だと私が思うのは）、**expressionはレイアウト、ペイント、フィルターに使える**ということです。昔、私はレイヤのminzoom に expression表現を入れようとしてうまくいかなかった経験がありますが、使える範囲を間違っていたということですね。
Expression が使える要素、レイアウト、ペイント、フィルターであるということを忘れないようにしましょう。

## Expression 種類
続いて以下の説明を読むと、Expressionはオペレーターを使って、プロパティの値を決めるための式を定義するものだとわかりますが、オペレーターの種類はこの部分の下に書いてあります。
>　An expression defines a formula for computing the value of the property using the operators described below

Operatorの種類は、**Mathematical operatoers、Logical operators、String operators、Data operators、Camera operators の5種類**があります。５つのタイプはMapboxもMapLibreも共通ですが、Mapboxスタイル仕様の方にはチュートリアルへのリンクも付いています。
また、一つのExpressionは複数のタイプのオペレータを組み合わせてつくることもできるそうです。


|種類|説明|例|
|---|---|---|
|Data|地物データにアクセスする表現|get, has,id, geometry-type, properties, feature-state|
|Camera|zoomオペレータを使う表現のこと。ペイントプロパティだとズームレベルの途中（4.1から4.6など）でも再評価されますが、レイアウトプロパティだと整数値のズームレベルでしか再評価されないそうです。）|distance-from-center, pitch, zoom|
|String|文字列を操作するためのオペレータ|concat, downcase, is-supported-script, resolved-locale, upcase|
|Math|演算を行うためのオペレータ|-, +, *, /, %, ^, abs, acos, asin, atan, ceil,  ...|
|Logical|caseを使うとif/then/elseのロジックができる。matchを使えば特定のインプット値のものにたいして異なるアウトプットができる|!, !=, <, <=, ==, ..., all, any, case, coalesce, match, within |

スタイル仕様のOthersでリストされている表現（Comparison filterなど）は、説明の中で奨励されない表現と書かれているため、これらは将来使えない可能性もあるのだと思っていました。今回、一つ発見したのですが、Othersにリストされている表現の一部は、Logical operatorの中に含まれています（ ==、 all、 anyなど）。 なので、フィルタのなかで"=="を使うのは今後もOKということなんですね。以下のページ、otherの部分だけをみたら、Comparison filterがダメなような印象を受けてしまいました（otherのリストのうち、ほかの部分に定義されていない表現、たとえばstopsなどが推奨されないものになるということなんですね。）
https://docs.mapbox.com/mapbox-gl-js/style-spec/other/#other-filter
https://maplibre.org/maplibre-gl-js-docs/style-spec/other/#other-filter

## Expressionが返すタイプ
インプットする引数や、expressionの結果のタイプはスタイル仕様のほかのタイプと一緒だそうです。
つまり、boolean, string, number, color, and arrays ot these types. とのことです。例えば、filterプロパティではいつでもbooleanを返す必要がありますし、+や-のオペレーターではnumberのタイプを返すことになります。


# 3. 事例の研究

## Filterの例
### Arc Online のベクトルタイルスタイルに見るフィルタ
Esriの場合は、ベクトルタイルのフィルタはほとんどが以下の形式です。ベクトルデータ作成とスタイル作成を一緒にすることが多いので、もともとのGSIレイアウトを色濃く反映しているような印象を受けます。
参考：　https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/resources/styles

```json
"filter": ["==", "_symbol", 0]
```

### 地理院地図のスタイル（旧バージョン）に見るフィルタ
数年前に公開された地理院地図のMapbox GL JS　風スタイル（https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/std.json ）は以下のような感じです。条件式が一つなので、allを入れなくても機能すると思います。（多分、下の例なら["==","ftCode",5302]でいいのではないだろうか。）

```json
"filter":["all",["in","ftCode",5302]]
```

### Maputnikに見るフィルタ
Maputnikなどで作れることができる複数条件のフィルターです。allだけでなく、any、noneを使って条件を変えられます。また==のところはほかのLogical Operator（!=, >, >=, <, <=, in, !in, has. !has）でも大丈夫です。

```json
"filter": [
    "all",
    ["==", "$type", "LineString"],
    ["==", "feature", "road"]
  ],
```

### UNVT （naru）に見るフィルタ
UNVTのnaruでは、matchを使った例が出てきます。以下の例は、ジオメトリでポリゴンかマルチポリゴンを選んでいる例です。ですが、ベクトルタイルの同じレイヤにポリゴン地物とライン地物を一緒に入れなければこのフィルタは不要になるかと思います。

```json
"filter": [
  "match",
  [
    "geometry-type"
  ],
  [
    "Polygon",
    "MultiPolygon"
  ],
  true,
  false
],
```

こちらもnaruからですが、matchの中でgetを使って属性をとってきて、それがOKならばポリゴンかマルチポリゴンでフィルターするという例です。
```json
"filter": [
    "match",
    [
        "get",
        "natural"
    ],
    "wetland",
    [
        "match",
        [
            "geometry-type"
        ],
        [
            "Polygon",
            "MultiPolygon"
        ],
        true,
        false
    ],
    false
],
```

もし、ソースレイヤ中にポリゴンしかなければ、ポリゴンフィルタが不要になるのでもう少し簡単になります。

```json
"filter": [
    "match",
    [
        "get",
        "natural"
    ],
    "wetland",
    true,
    false
],
```

これは、matchを使わないで書けば、以下のものと同じかなと思います。
```json
"filter": ["==", "natural", "wetland"]
```

### 地理院地図　最適化　スタイルに見るフィルタ
最近、地理院地図ベクターが最適化され、スタイルもそれ用に新しくなりました。
https://github.com/gsi-cyberjapan/optimal_bvmap/blob/main/style/std.json

適切なExpressionが使われています。すごい。

```json
"filter": [
	"in",
	[
		"get",
		"vt_code"
	],
	[
		"literal",
		[
            5101,
            5103
		]
	]
],
```

シンプルな条件だと、以下のような感じで書いています。

```json
"filter": [
    "==",
    [
        "get",
        "vt_code"
    ],
    5302
],
```

ここで、さて、、と考えるわけです。 ["==",["get","vt_code"],5302]　と　["==", "vt_code", 5302]　の違いを。

この前に書きましたが、"==" は今後も使える表現です。しかし、**前者はExpression の表現である ["==", value, value]である一方、 後者は推奨されないotherのフィルターにある ["==", key, value] になる**わけなんだと思います。
なるほど。以下の二つのページでも比べてみましょう。
* https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#==
* https://docs.mapbox.com/mapbox-gl-js/style-spec/other/#comparison-filters

私の実力では、 ["==",["get","vt_code"],5302]　と　["==", "vt_code", 5302] でどれだけ描画速度が変わってくるのか推測することは難しいです・・・。しかし、将来のためにはgetを入れて使った方がよいように思います。
（**この意味で、MapLibreやMapboxのユーザーさんは、もしかするとEsriさんのスタイルや、Maputnikさんのスタイルをそのまま使わない方がよよいかと思います。**）


## Paintやlayoutの例
### Stops （MapLibre や Mapboxでは奨励されていない）
Esriさんのスタイルや、Maputnikで作るスタイルでは、よくstopsという表現を使っています。これは直感的にもわかりやすい表現だとは思います。

```
"line-color": {
  "stops": [[12, "rgba(255,255,255,0.75)"], [13, "#995C6B"]]
},
"line-width": {
  "stops": [[8, 0.9], [10, 1.13], [12, 1.5], [13, 2.5], [15, 5], [17, 11], [19, 24]]
```
一方で、このstopsはまだ動きますが、MapLibreにしろ、Mapboxにしろ、stopsの表現は奨励していません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/930594f1-719f-6291-cd11-2429e53567da.png)

### zoom の使い方
以下は、地理院地図最適化版のスタイルからです。interpolateのlinearの中でzoomを使っています。ズームレベル14で透過率が０で、ズームレベル16ではすべての地物の透過率が１になりますが、vt_codeが7509以外のものはZL15でも透過率が１になるというものだと思います。

```
"line-opacity": [
    "interpolate",
    [
        "linear"
    ],
    [
        "zoom"
    ],
    14,
    0,
    15,
    [
        "case",
        [
            "==",
            [
                "get",
                "vt_code"
            ],
            7509
        ],
        0,
        1
    ],
    16,
    1
]
```
このズームの使い方は、地理院地図　最適化版などで研究するとよいと思います。
https://github.com/gsi-cyberjapan/optimal_bvmap/blob/main/style/std.json

以下によく使いそうな例を示しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/b9aae446-7b05-5c2a-63c5-67515b2398bf.png)

ここでstepを使うと、色などは不連続に変わります。内挿できないものにはよさそうな表現ですね。↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/019972d8-19ac-9ac4-1d46-5663d808ea3f.png)

ラベルをあるズームから出したい時なども使えます。アイコンをあるZLから出して、ラベルをもう少し後で出したい時など、ラベルとアイコンを一つのスタイルレイヤを同じで処理できるので便利です。

```json:テキストフィールドをZoomレベルによって変える
"text-field": [
  "step",
  [
    "zoom"
  ],
   "",
  15,
  [
    "get",
    "name"
  ]
],
```



```json:連続的に色を変える例
{
    "paint": {
        "line-color": [
            "interpolate",
            [
                "exponential",
                0.5
            ],
            [
                "zoom"
            ],
            10,
            "#FF0000",
            11,
            "#0000FF"
        ]
    }
}
```
なお、zoomについては、トップレベルのstepかinterpolateのexpressionにしか使うことができません。トップレベルということなので、例えば、matchと組み合わせて、matchのなかにstepやinterpolateを入れてzoomを使うというのはできないそうです。ですので、組み合わせて使う場合には順番に気を付けましょう。

```json:interpolateにmatchを入れた例
    "paint": {
        "line-color": [
            "interpolate",
            [
                "exponential",
                0.5
            ],
            [
                "zoom"
            ],
            10,
            [
                "match",
                [
                    "get",
                    "z_order"
                ],
                1,
                "#D8ECF6",
                "#C7E4F2"
            ],
            11,
            [
                "match",
                [
                    "get",
                    "z_order"
                ],
                1,
                "#B6DEF2",
                "#C7E4F2"
            ]
        ]
    }
```


### matchによる色分け
例えば、以下は水系のラインの表現ですが、matchを使って、海岸線(coastline)とそれ以外の線の色を分けています。（行政会などほかの種類の線との区分けは、ソースレイヤの区分けで対応。）

```json
"paint": {
        "line-color": [
          "match",
          [
            "get",
            "natural"
          ],
          "coastline",
          [
            "rgb",
            33,
            150,
            243
          ],
          [
            "rgb",
            187,
            222,
            251
          ]
        ]
      },
```

地理院地図の最適化スタイルでも同様の表現が見られますね。
```json
"line-color": [
	"match",
	[
		"get",
		"vt_code"
	],
	[
		7571,
		7572
	],
	"rgba(20,90,255,1)",
	"rgb(200,160,60)"
],
```

:::note info
dash-arrayについて
ラインについて、実線と点線をmatchを使って分けられないと思っていたので、当初、私は別々のスタイルレイヤにしていました。しかし、実線のところのdash-arrayを1,0とすると、点線でなくて実線がでます~~ので、この方法を使えば点線と実線を一つのレイヤでmatchを使って表すことができると思います。(ただし、interpolateはdash arrayには使えないので、step、zoomでやるのかなと思います。)~~
dash-array では matchがサポートされていないようです。（mapbox GL JS の2.3では大丈夫？？かもしれませんが、MapLibreがどうかはわかりません。）
mapbox --> https://github.com/mapbox/mapbox-gl-js/issues/3045
maplibre --> https://github.com/maplibre/maplibre-gl-js/issues/1235
:::

もう一つ注意点ですが、複数レイヤで重ねる代わりに、matchを使って色を分けると、異なる線の間でのオーバーレイの順番が狂うことがあります（下図）。matchで出てくる順を変えても線の上下は変わりません。道路などでは重ね合わせの順番が重要ですから、matchを使えてもレイヤ順を維持するためにあえて複数レイヤにすることがあるかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/c3d4a715-5eb5-def8-d06f-bf734825c459.png)



# まとめ
ここでは、私が勉強しながらですが、スタイル作成の時のExpressionについてまとめました。

私はこれからスタイルづくりをunvt/charitesでやるわけですが、このメモにあるExpressionを使いつつ、また再利用可能なスタイル要素はincludeして（ - !! inc/file）、効率よくスタイルを作っていきたいなと思います。

# 謝辞
unvt/naru の開発者の@hfuさんに感謝します。naruで作ったスタイルの表現は進んでいたと思います。
また、最近では地理院地図の最適版のスタイルを作成された方に感謝します。スタイルづくりの参考になります。

# 参考ページ
ベクトルタイル用スタイルの書き方メモ（私の前の記事）：　https://qiita.com/T-ubu/items/02a9725dd6329d35d477

Esri Living Atlas -World base map v2　のスタイル：　https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer/resources/styles

スタイルレイヤ数を減らすとベクトルタイルの描画が速くなるという検証（@mg_kudo , 2022）：　https://qiita.com/mg_kudo/items/f9b6ab97b2ab670b638c

Maptnik: https://maputnik.github.io/editor/

地理院地図　最適化版　のスタイル：　https://github.com/gsi-cyberjapan/optimal_bvmap/blob/main/style/std.json

# おまけ：　自分で使った表現（YAMLで書いたもの）
ここまでの調査を踏まえて、自分でもスタイルを書いてみました。unvt/charitesを使ったので、主にYAMLで書きましたが、以下のような表現も使えるのでここにメモしておきます。

## filter
```YAML: 簡単なフィルタ
filter: 
  - match
  - - get
    - z_order
  - - 1
    - 2
  - true
  - false
```

```YAML:簡単なフィルタ、別バージョン
filter:
  - '!='
  - - get
    - fclass
  - pier
```
```YAML:allを使った例
filter:
  - all
  - - 'in'
    - - get 
      - z_order
    - - literal
      - - 7 # airfield
        - 5 #aerodrome
        - 6 #helipad
  - - '=='
    - - get 
      - status
    - true
```
```YAML:allを使った例２
filter:
  - all
  - - match
    - - get
      - z_order
    - 5 #aerodrome
    - - '!'
      - - 'has'
        - iata_code
    - - 6 #helipad
      - 7 #airfield
    - true
    - false
  - - '=='　#get status だけでも大丈夫かも？？
    - - get 
      - status
    - true
```

```YAML:all なし、status(boolean)をそのまま使った例
filter:
- match
- - get
  - z_order
- - 3 #bus_station
  - 4 #ferry_terminal
  - 8 # station
##  - 10 # bus_stop(B)
##  - 9 #halt (R)
##  - 15 # stop  (R)
##  - 2 #tram_stop (T)
##  - 12 #stop position (T)
- - get
  - status
#- - '=='
#  - - get
#    - status
#  - true
- false
```

※その後、いろいろ試しましたが、上の例にあるようにstatusでフィルタリングするのであれば、データ変換の時にmodify.jsをいじってベクトルタイルにする前にフィルタリングしておくとよいと思うようになり、そのようにしました。


## paint

```YAML:属性に応じて色を変えた
paint:
  fill-color:
  - match
  - - get
    - z_order
  - - 13
    - 14
  - '#F2F2DC' #farm
  - - 5
    - 6
    - 15
  - '#E2EBCF' #grass
  - - 4
    - 16
  - '#F2F2E3' #heath
  - - 7
    - 12
  - '#E1EDC9' #orchard
  - - 1
    - 3
    - 8
    - 10
    - 11
  - '#BAE3BA' #park
  - 9
  - '#ECE7DE' #quarry
  - 2
  - '#E5DFD1' #rock
  - '#FAF9F6' #other (There should be no feature. color is just same as the landmass)
```

```YAML:線幅を段階的に変えたいとき
  line-width:
  - interpolate
  - - linear
  - - zoom
  - 11.53
  - 0.666667
  - 18.17
  - 2.66667
```

```YAML:線の幅をスケールごとに変える場合で、属性によって違う太さにしたい場合
  line-width:
  - interpolate
  - - linear
  - - zoom
  - 7.22
  - - match
    - - get 
      - z_order
    - 1
    - 1
    - 0
  - 10.53
  - - match
    - - get 
      - z_order
    - 1
    - 1.333333
    - 0
  - 11.53
  - - match
    - - get 
      - z_order
    - 1
    - 1.8841 #calc
    - 2
    - 0.88
    - 0.666667
  - 18.17
  - - match
    - - get 
      - z_order
    - 1
    - 4.4444
    - 2
    - 3.52
    - 2.666667
```




```YAML:線の色を変えたとき（スタイルの例でexponential,0.5を使っていたので真似した）
  line-color:
  - interpolate
  - - exponential
    - 0.5
  - - zoom
  - 10
  - '#C7E4F2'
  - 11
  - - match
    - - get 
      - z_order
    - 1
    - '#D8ECF6'
    - '#C7E4F2'
```

## layout

```YAML:ズームを上げたときにフォントを切り替える場合
  text-font:
#  - !!inc/file fonts/nsd-ita.yml　には NotoSansDisplay-Italicという文字が入っている。
  - step
  - - zoom
  - - literal
    - - !!inc/file fonts/nsd-ita.yml
  - 11
  - - literal 
    - - !!inc/file fonts/nsd-med-ita.yml
```

```YAML:属性によってラベルを出現させるズームレベルを変える場合
  text-field:
  - step
  - - zoom
  - - match
    - - get
      - z_order
    - 2
    - - get
      - name
    - ""
  - 14
  - - get
    - name
```

### unvt/charitesを活用したレイアウト
首都のラベルの位置をれぞれをを上下左右に調整するときに使います。otherというフォルダに上下左右にやりたい国のリストをYAMLファイルで準備しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/b396b683-5628-db35-7f2e-e14380034a8c.png)

そのあとに、以下のように記述。

```YAML:layoutの一部
  text-anchor: 
  - match
  - - get
    - iso3cd
  - !!inc/file other/capital-top.yml
  - bottom
  - !!inc/file other/capital-left.yml
  - right
  - !!inc/file other/capital-bottom.yml
  - top
  - left
  text-offset:
  - match
  - - get
    - iso3cd
  - !!inc/file other/capital-top.yml
  - - literal
    - - 0
      - -1
  - !!inc/file other/capital-left.yml
  - - literal
    - - -1
      - 0
  - !!inc/file other/capital-bottom.yml
  - - literal
    - - 0
      - 1
  - - literal
    - - 1
      - 0
```
リストを編集することで、ラベルの位置を調節できる。（ただし、国コードを使ってしまったので同じ国に二つ点があるときには調整できない。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/79082a65-a62d-3d89-414c-78279d0d687a.png)