1.はじめに
ベクトルタイルのスタイルの話題です。この記事では、Mapboxスタイル と MapLibreスタイル で共通ですが、スタイルのExpressionについて書きたいと思います。(私はあまり詳しく見たことがなかったので、もう少し見てみようという趣旨。)
細かいところのメモなので、少し進んだ初心者の方向けです。
動機
私もそうですが、ベクトルタイルのスタイルを書くときに、Maputnik でのスタイル作成や ArcGIS Vector Map のスタイルファイル (たとえばこちらのWorld_Basemap_v2スタイル) を参考にしている人も多いと思います。
しかし、一方で、Mapbox や MapLibre のスタイル仕様(mapbox とmaplibre)を読み込んでみると、Maptnik や ArcGIS Vector Map で使われているスタイル表現では、mapbox v0.41.0 以降(MapLibreがMapboxから分化したのはそれ以降なのでMapLibreでも共通)で奨励されていないスタイル表現が使われています。(例:線幅のstops での設定など)
ここでは、一歩先に進むために、propetry expressiExpressionExpressionに準じた表現方法を少し考えてみたいと思います。
期待したい効果
推奨しているスタイル表現を使うことになるので、もしかしたら描画の速度が上がるといいなぁと期待しています。
また、filterの方法を見直すことで、レイヤ数を減らせるとよいなと思います。(レイヤ数が減れば描画速度が速くなるというわけでもなさそうなのですが、数百あるレイヤ数を少しでも効率的にまとめたいです。)
10月17日・追記
レイヤ数が減ると描画が早くなるのかということについて、以下に非常に素晴らしい記事を見つけました。自分のメモ用に、ここにリンクを書いておきます(すごいと思います)。
Mapbox Style Specification とMapLibre Style Specification について
前でも述べましたが、MapLibre GL JSは Mapbox GL JS v.1.13から分化していますが、スタイル仕様はそれ以前にはほぼ現在の形ができていたようで、基本的な表現に関しては両者のスタイル仕様はほとんど同じかと思います。(少なくともExpressionのところは同じに見えます。)
(追記)
下の方に書きましたが、例えば、dash-array にmatchを使えるかというところについて、mapboxとmaplibreで差がついたように、細かいところでは違いがあるかもしれません。(仕様上は明記されていないので、仕様としては違わないのですが、実装の部分で微妙は差があるかもしれませんね。)
2.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種類があります。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
"filter": ["==", "_symbol", 0]
地理院地図のスタイル(旧バージョン)に見るフィルタ
数年前に公開された地理院地図のMapbox GL JS 風スタイル(https://gsi-cyberjapan.github.io/gsivectortile-mapbox-gl-js/std.json )は以下のような感じです。条件式が一つなので、allを入れなくても機能すると思います。(多分、下の例なら["==","ftCode",5302]でいいのではないだろうか。)
"filter":["all",["in","ftCode",5302]]
Maputnikに見るフィルタ
Maputnikなどで作れることができる複数条件のフィルターです。allだけでなく、any、noneを使って条件を変えられます。また==のところはほかのLogical Operator(!=, >, >=, <, <=, in, !in, has. !has)でも大丈夫です。
"filter": [
"all",
["==", "$type", "LineString"],
["==", "feature", "road"]
],
UNVT (naru)に見るフィルタ
UNVTのnaruでは、matchを使った例が出てきます。以下の例は、ジオメトリでポリゴンかマルチポリゴンを選んでいる例です。ですが、ベクトルタイルの同じレイヤにポリゴン地物とライン地物を一緒に入れなければこのフィルタは不要になるかと思います。
"filter": [
"match",
[
"geometry-type"
],
[
"Polygon",
"MultiPolygon"
],
true,
false
],
こちらもnaruからですが、matchの中でgetを使って属性をとってきて、それがOKならばポリゴンかマルチポリゴンでフィルターするという例です。
"filter": [
"match",
[
"get",
"natural"
],
"wetland",
[
"match",
[
"geometry-type"
],
[
"Polygon",
"MultiPolygon"
],
true,
false
],
false
],
もし、ソースレイヤ中にポリゴンしかなければ、ポリゴンフィルタが不要になるのでもう少し簡単になります。
"filter": [
"match",
[
"get",
"natural"
],
"wetland",
true,
false
],
これは、matchを使わないで書けば、以下のものと同じかなと思います。
"filter": ["==", "natural", "wetland"]
地理院地図 最適化 スタイルに見るフィルタ
最近、地理院地図ベクターが最適化され、スタイルもそれ用に新しくなりました。
https://github.com/gsi-cyberjapan/optimal_bvmap/blob/main/style/std.json
適切なExpressionが使われています。すごい。
"filter": [
"in",
[
"get",
"vt_code"
],
[
"literal",
[
5101,
5103
]
]
],
シンプルな条件だと、以下のような感じで書いています。
"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の表現は奨励していません。
zoom の使い方
以下は、地理院地図最適化版のスタイルからです。interpolateのlinearの中でzoomを使っています。ズームレベル14で透過率が0で、ズームレベル16ではすべての地物の透過率が1になりますが、vt_codeが7509以外のものはZL15でも透過率が1になるというものだと思います。
"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
ここでstepを使うと、色などは不連続に変わります。内挿できないものにはよさそうな表現ですね。↓
ラベルをあるズームから出したい時なども使えます。アイコンをあるZLから出して、ラベルをもう少し後で出したい時など、ラベルとアイコンを一つのスタイルレイヤを同じで処理できるので便利です。
"text-field": [
"step",
[
"zoom"
],
"",
15,
[
"get",
"name"
]
],
{
"paint": {
"line-color": [
"interpolate",
[
"exponential",
0.5
],
[
"zoom"
],
10,
"#FF0000",
11,
"#0000FF"
]
}
}
なお、zoomについては、トップレベルのstepかinterpolateのexpressionにしか使うことができません。トップレベルということなので、例えば、matchと組み合わせて、matchのなかにstepやinterpolateを入れてzoomを使うというのはできないそうです。ですので、組み合わせて使う場合には順番に気を付けましょう。
"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)とそれ以外の線の色を分けています。(行政会などほかの種類の線との区分けは、ソースレイヤの区分けで対応。)
"paint": {
"line-color": [
"match",
[
"get",
"natural"
],
"coastline",
[
"rgb",
33,
150,
243
],
[
"rgb",
187,
222,
251
]
]
},
地理院地図の最適化スタイルでも同様の表現が見られますね。
"line-color": [
"match",
[
"get",
"vt_code"
],
[
7571,
7572
],
"rgba(20,90,255,1)",
"rgb(200,160,60)"
],
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を使えてもレイヤ順を維持するためにあえて複数レイヤにすることがあるかもしれません。
まとめ
ここでは、私が勉強しながらですが、スタイル作成の時の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
filter:
- match
- - get
- z_order
- - 1
- 2
- true
- false
filter:
- '!='
- - get
- fclass
- pier
filter:
- all
- - 'in'
- - get
- z_order
- - literal
- - 7 # airfield
- 5 #aerodrome
- 6 #helipad
- - '=='
- - get
- status
- true
filter:
- all
- - match
- - get
- z_order
- 5 #aerodrome
- - '!'
- - 'has'
- iata_code
- - 6 #helipad
- 7 #airfield
- true
- false
- - '==' #get status だけでも大丈夫かも??
- - get
- status
- true
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
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)
line-width:
- interpolate
- - linear
- - zoom
- 11.53
- 0.666667
- 18.17
- 2.66667
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
line-color:
- interpolate
- - exponential
- 0.5
- - zoom
- 10
- '#C7E4F2'
- 11
- - match
- - get
- z_order
- 1
- '#D8ECF6'
- '#C7E4F2'
layout
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
text-field:
- step
- - zoom
- - match
- - get
- z_order
- 2
- - get
- name
- ""
- 14
- - get
- name
unvt/charitesを活用したレイアウト
首都のラベルの位置をれぞれをを上下左右に調整するときに使います。otherというフォルダに上下左右にやりたい国のリストをYAMLファイルで準備しておきます。
そのあとに、以下のように記述。
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
リストを編集することで、ラベルの位置を調節できる。(ただし、国コードを使ってしまったので同じ国に二つ点があるときには調整できない。)