MapLibre GL JSのスタイルファイルの説明とMaputnikでの設定

# はじめに
[以前の記事](https://qiita.com/k96mz/items/a39625d616ea84d750ee)では、ベクトルタイルのスタイル作成の練習をするために、Maputnikを利用して地理院地図Vectorのスタイルを作成しました。本記事では、作成したスタイルを編集してみようと思います。また、スタイルの基本についても記載します。

# MapLibre GL JSのstyleの基本
スタイルファイルは、ルート要素と、その下にぶら下がる入れ子になった要素から成り立ちます。[ルート要素](https://maplibre.org/maplibre-style-spec/root/)について主要なものを以下の表にまとめました。

## ルート要素
| 要素 | 必須 or オプション | 例 | 説明 |
|:-:|:-:|:-:|:-:|
| version  | **必須** | 8 |　Style specificationのバージョン  |
| name  | オプション  |　"Bright" |スタイルの名前、人が見分ける用  |
| metadata | オプション  |　{ "maputnik:renderer": "mlgljs" } |スタイルファイルがどのように作成されたのか等が記載される。例えば、スタイルエディタのバージョンなど。 |
| center | オプション |　[138.34, 35.66] |　デフォルト中心位置、[経度, 緯度]の順番  |
| zoom　| オプション | 12.5　|　デフォルトズームレベル  |
| bearing | オプション | 29　| 方位。例えば90であれば、東の方角が上となる。  |
| pitch | オプション | 59　| 傾き。0であれば、真上からの視点となる。最大値は60の模様  |
| sources　| **必須** | 下段参照　| どのデータを表示するのかを指定。sourcesはスタイリングのための色や太さ情報を持っていないため、sourcesを指定するだけでは地図は表示されない。layersが必要。この特性により、一つのソースから複数のスタイルを作成することが出来る。|
| sprite　| オプション | "https\://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite"　|　地図記号、線、ポリゴンの塗りなどに使う画像の場所を指定  |
| glyphs　| オプション | "https\://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf"　|　フォントの場所をpbfフォーマットで指定。{fontstack}がフォント名を、{range}がフォントの実体を参照？  |
| layers　| **必須** | 下段参照　|　最重要。スタイルで使用するレイヤを指定。backgroundレイヤを除き、参照するソースを指定する必要あり。|




ここで、center、zoom、bearing、pitchについては、htmlファイルでも定義することが出来ます。どちらで定義しても良いのですが、両方で定義するとhtmlファイルの設定が優先されます。


## sources
sourcesについては、少し細かく見ていきます。詳しくは[MapLibre Style SpecのSources](https://maplibre.org/maplibre-style-spec/sources/)にありますが、必要そうな情報を記載します。

記載例は以下の通りです。
```json:
"sources": {
    "v": {
      "type": "vector",
      "tiles": [
        "https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf"
      ],
      "minzoom": 4,
      "maxzoom": 16,
      "attribution": "<a href=\"https://maps.gsi.go.jp/vector/\" target=\"_blank\">国土地理院ベクトルタイル提供実験</a>"
    }
  }
```

sourcesでよく使用されるベクトルタイルのプロパティを表にまとめました。

| プロパティ名  | 必須 or オプション | 例 | 説明 |
|:-:|:-:|:-:|:-:|
| type  | **必須** | "vector" |　ベクトルタイルを指定  |
| url  | オプション  |　"https\://demotiles.maplibre.org/tiles/tiles.json" | ベクトルタイルのソースをjsonファイルのURLで指定する時に使用する  |
| tiles | オプション  |　["https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf"] |ベクトルタイルのソースをpbfのURLで指定する時に使用する  |
| bounds | オプション |　[123, 24, 147, 44] |　地図の南西と北東の角を指定。[sw.lng, sw.lat, ne.lng, ne.lat]の形式で記載。このオプションが記載されていれば、範囲外のタイルはリクエストされない。タイルが読み込まれないだけで、範囲外への移動は可能であることに注意。 |
| minzoom　| オプション | 0　|　タイルが存在する最小ズームレベル  |
| maxzoom　| オプション | 15　|　タイルが存在する最大ズームレベル。例えば15で指定して、実際のズームレベルが16の場合は、ズームレベル15のタイルがオーバーズーミングされる。  |
| attribution　| オプション | "\<a href=\"https\://maps.gsi.go.jp/vector/\" target=\"_blank\">国土地理院ベクトルタイル提供実験</a>"　|　出典の記載  |


## layers
次に一番重要なレイヤーを見ていきます。レイヤーについても詳しくは[MapLibre Style SpecのLayers](https://maplibre.org/maplibre-style-spec/layers/)にありますが、必要そうな情報を記載します。

記載例は以下の通りです。
```json:
"layers": [
    {
      "id": "bg",
      "type": "background",
      "paint": { "background-color": "rgba(232, 225, 225, 1)" }
    },
    {
      "id": "coastline",
      "type": "line",
      "source": "v",
      "source-layer": "coastline"
    },
    {
      "id": "waterarea",
      "type": "fill",
      "source": "v",
      "source-layer": "waterarea",
      "paint": { "fill-color": "rgba(20, 101, 223, 1)" }
    }
  ],
```
こちらもよく使用されるプロパティを表にまとめました。

| プロパティ名  | 必須 or オプション | 例 | 説明 |
|:-:|:-:|:-:|:-:|
| id  | **必須** | "coastline" |　レイヤーidを指定（ユニークな値とする）  |
| type  | **必須** | "fill" |　fill: ポリゴン、line: 線、symbol: アイコン or テキストラベル、circle: 円、fill-extrusion: 3Dポリゴン、background: 背景色 |
| source  | **必須**  | "v" | sourcesで使用したデータソース名称を指定  |
| source-layer | **必須**  | "coastline" |ベクトルタイルのソースのどのレイヤを使用するのかを指定 |
| minzoom　| オプション | 0　|　レイヤに対する最小ズームレベル。この数値以下のズームレベルでは、該当レイヤは描画されない  |
| maxzoom　| オプション | 15　|　レイヤに対する最大ズームレベル。この数値以上のズームレベルでは、該当レイヤは描画されない  |
| filter　| オプション | 下段参照 |　ベクトルタイルのプロパティの値を使用して、条件に合致する地物のみを表示する。ベクトルタイルレイヤの地物を全て表示するときにはfilterは不要。  |
| layout　| オプション | 下段参照 |　レイヤーをどのように表示するか指定するプロパティ(Line及びSymbolのみ)  |
| paint　| オプション | 下段参照 |　レイヤーをどのように描画するか指定するプロパティ  |


# Maputnikでのスタイルの編集
次にMaputnikで実際にスタイルの編集をしてみます。
[以前の記事](https://qiita.com/k96mz/items/a39625d616ea84d750ee)でも紹介した[地理院地図Vector提供実験](https://github.com/gsi-cyberjapan/gsimaps-vector-experiment?tab=readme-ov-file)のウェブサイトの[属性等の仕様詳細](https://maps.gsi.go.jp/help/pdf/vector/dataspec.pdf)を見ながら作業します。今回は、建物データをMaputnikで編集します。
建物の属性を見てみると、以下の図のように記載されています。
![GSeditI.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/26fe22ba-649d-c680-1528-2995edbaa029.jpeg)

さらに簡潔に表にしてみると以下になります。

| 地物名  | source-layer | ftCode | ズームレベル |
|:-:|:-:|:-:|:-:|
| 普通建物 | building | 3101 |　15, 16  |
| 堅ろう建物 | building | 3102 |　14, 15, 16  |
| 高層建物 | building | 3103 |　14, 15, 16  |
| 普通無壁舎 | building | 3111 |　15, 16  |
| 堅ろう無壁舎 | building | 3112 |　15, 16  |

### layersでのminzoom, maxzoomと、オリジナルデータとの関係
例えば、普通建物のデータはズームレベル15,16に存在していますが、layersにおいてminzoomを16とすると、ズームレベル16まではデータが存在するにも関わらず、表示されないこととなります。

## filter
これらをMaputnikのfilterを使用して編集してみます。

IDをbuilding3101として新しくレイヤを追加しました。

![Maputnik1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/73b3a2e3-9e96-5fb0-df01-8671c8c05281.jpeg)

そして、適当に建物に色をつけて表示させると以下の図のようになります。
![Maputnik2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/c4979316-8ec1-21d9-8cfe-c7cc55f4b144.jpeg)

ここで、Filterを使用して、ftCodeが3101の普通建物のみ表示させてみます。
「Filter」は「**every** filter matches」を選択し、「ftCode == 3101」とします。そうすると、ftCodeが3101の普通建物のみ表示されました。先ほどの画像と比べて、表示されている建物の数が減っていますね。

![Maputnik3edit.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/b9f997a8-e8d9-d314-cbb5-9edb4779e1e9.jpeg)

ちなみに、画面下部におけるJSON Editorでは以下のように示されています。
![MaputnikJson1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/cbd1abf0-60d5-880d-9d4b-29ab6c09d705.jpeg)

次にIDをbuilding3102_3103として新しくレイヤを追加しました。

![Maputnik4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/0b188e02-3f68-0d0a-43e6-4e3a6c6b2fee.jpeg)

Filterを使用して、堅ろう建物(3102)と高層建物(3103)を表示させてみます。
「Filter」は「**any** filter matches」を選択し、「ftCode == 3102」、「ftCode == 3103」とします。建物に青っぽい色をつけて、堅ろう建物(3102)と高層建物(3103)を表示しました。

![Maputnik5edit.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/1a029bfd-47dd-b5f6-32f8-38218071e4d6.jpeg)

ここで、「**no** filter matches」としてみると、堅ろう建物(3102)と高層建物(3103)以外の建物、つまり普通建物(3101)、普通無壁舎(3111)、堅ろう無壁舎(3112)が表示されます。さらに、Layersの順番をbuilding3101を一番下に持ってくる（画面上では一番上に表示される）と普通無壁舎(3111)、堅ろう無壁舎(3112)が青色で表示されます。これはつまり、普通建物(3101)、普通無壁舎(3111)、堅ろう無壁舎(3112)が青色で表示されており、その上からbuilding3101により普通建物(3101)が灰色で塗られているため、普通無壁舎(3111)、堅ろう無壁舎(3112)のみが青色で表示されているということです。

![Maputnik6edit.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/bdf7f25c-820e-528a-2a23-e401c3de8a54.jpeg)

<br>

filterの3つの機能をまとめてみると以下になります。
**①every filter matches：記載されているすべての条件を満たす地物を表示**
今は一つの条件しかありませんが、複数の条件がある場合において、一つでもfalseがあると条件が満たされません。
JSONファイルでは"all"を使用し、以下のように記載されています。
```json:
  "filter": [
    "all",
    ["==", "ftCode", 3101]
  ]
```
allについては、[MapLibre Expressionsのall](https://maplibre.org/maplibre-style-spec/expressions/#all)に詳しく記載されています。

<br>

**②any filter matches：記載されているいずれかの条件を満たす地物を表示**
今回の例では、堅ろう建物(3102)と高層建物(3103)の両方が条件を満たします。
JSONファイルでは"any"を使用し、以下のように記載されています。
```json:
  "filter": [
    "any",
    ["==", "ftCode", 3102],
    ["==", "ftCode", 3103]
  ]
```
anyについては、[MapLibre Expressionsのany](https://maplibre.org/maplibre-style-spec/expressions/#any)に詳しく記載されています。

<br>

**③no filter matches：記載されている条件をすべて満たさない地物を表示**
JSONファイルでは"none"を使用し、以下のように記載されています。
```json:
  "filter": [
    "none",
    ["==", "ftCode", 3102],
    ["==", "ftCode", 3103]
  ]
```
noneについては、なぜかMapLibre Expressionsに記載がありませんでした。

Maputnikから作成したJSONファイルをエクスポートして、"center"と"zoom"の値を編集したものは以下になります。

```json:style2.json
{
  "version": 8,
  "name": "Empty Style",
  "metadata": { "maputnik:renderer": "mlgljs" },
  "center": [139.80336, 35.74141],
  "zoom": 15,
  "sources": {
    "v": {
      "type": "vector",
      "tiles": [
        "https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf"
      ],
      "minzoom": 4,
      "maxzoom": 16,
      "attribution": "<a href=\"https://maps.gsi.go.jp/vector/\" target=\"_blank\">国土地理院ベクトルタイル提供実験</a>",
      "bounds": [123, 24, 147, 44]
    }
  },
  "sprite": "",
  "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
  "layers": [
    {
      "id": "bg",
      "type": "background",
      "paint": { "background-color": "rgba(232, 225, 225, 1)" }
    },
    {
      "id": "coastline",
      "type": "line",
      "source": "v",
      "source-layer": "coastline"
    },
    {
      "id": "waterarea",
      "type": "fill",
      "source": "v",
      "source-layer": "waterarea",
      "paint": { "fill-color": "rgba(20, 101, 223, 1)" }
    },
    {
      "id": "building3102_3103",
      "type": "fill",
      "source": "v",
      "source-layer": "building",
      "filter": ["none", ["==", "ftCode", 3102], ["==", "ftCode", 3103]],
      "paint": { "fill-color": "rgba(71, 102, 191, 1)" }
    },
    {
      "id": "building3101",
      "type": "fill",
      "source": "v",
      "source-layer": "building",
      "filter": ["all", ["==", "ftCode", 3101]],
      "layout": { "visibility": "visible" },
      "paint": { "fill-color": "rgba(146, 134, 134, 1)" }
    }
  ],
  "id": "hfkgus5"
}
```

[MapLibre GL JSで表示](https://k96mz.github.io/20240522StyleQiita/index2.html)させてみました。

また、設定したスタイルファイルでのMaputnikのリンクは[こちら](https://maputnik.github.io/editor/?style=https://k96mz.github.io/20240522StyleQiita/style2.json)です。


今回はレイヤを分けましたが、MapLibre GL JSではmatchなどを使用するとレイヤを分けずに書けるのかもしれません。今後試してみたいと思います。

## Paint properties

次にMaputnikのPaint propertiesの機能について見ていきたいと思います。

### 共通設定
Opacity : 透過度。1が100%(不透明)で0が0%(透明)です。

Color : 色指定。カラーネーム(ex. red)、カラーコード(ex. #000000は黒)、RGB関数(ex. rgba(240, 90, 70, .75))、HSLa(ex. hsla(240, 90%, 70%, .75))などが使用できます。

Pattern: 塗りつぶす画像を指定します。

### Type: Background
背景を指定します。
https://maplibre.org/maplibre-style-spec/layers/#background

Backgroundでは以下のPaint Properitesが利用可能です。

| Maputnikのプロパティ名  | MapLibre GL JS | デフォルト値 | 説明 |
|:-:|:-:|:-:|:-:|
| Color | background-color | #000000（黒） |　背景色。background-patternが設定されている時は無効となる  |
| Pattern | background-pattern | なし |　塗りつぶす画像を指定  |
| Opacity | background-opacity | 1（透過なし） |　背景の透過度  |

### Type: Fill
ポリゴンの塗りつぶしを指定します。建物や水域などの表示に使用します。
https://maplibre.org/maplibre-style-spec/layers/#fill

Fillでは以下のPaint Properitesが利用可能です。

| Maputnikのプロパティ名  | MapLibre GL JS | デフォルト値 | 説明 |
|:-:|:-:|:-:|:-:|
| Opacity | fill-opacity | 1（透過なし） |　塗りつぶしの透過度  |
| Color | fill-color | #000000（黒） |　塗りつぶす色。fill-patternが設定されている時は無効  |
| Antialias | fill-antialias | true |　trueにするとアンチエイリアス（ギザギザを目立たなくする処理）がオン  |
| Outline color | fill-outline-color | なし |　ポリゴンの外枠の色。有効にするには、fill-antialiasをtrueにする必要あり。fill-patternが設定されている時は無効  |
| Pattern | fill-pattern | なし |　塗りつぶす画像を指定  |
| Translate | fill-translate | [0, 0] |　オフセット（位置を基準点からの距離で表した値）の指定。例えば、[3, 5]だと東に3ピクセル、南に5ピクセル移動|
| Translate anchor | fill-translate-anchor | map |　fill-translateの設定が必要。map に関連付けるか、viewport に関連付けるかを指定する。mapの場合は地図を回転させても同じようにずれるが、viewportでは地図を回転させるとそれに合わせてTranslateが変動する |

### Type: Line
ラインの表示を指定します。海岸線などに使用します。
https://maplibre.org/maplibre-style-spec/layers/#line

Lineでは以下のPaint Properitesが利用可能です。

| Maputnikのプロパティ名  | MapLibre GL JS | デフォルト値 | 説明 |
|:-:|:-:|:-:|:-:|
| Opacity | line-opacity | 1（透過なし） |　ラインの透過度  |
| Color | line-color | #000000（黒） |　ラインの色。line-patternが設定されている時は無効  |
| Width | line-width | 1 |　ラインの太さをピクセルで指定  |
| Offset | line-offset | 0 |　オフセットの指定  |
| Blur | line-blur | 0 |　ぼかしの指定  |
| Dasharray | line-dasharray | [0, 0] |　ダッシュパターンの配列などを指定。例えば、[1, 2]とすれば、1つ線があり、2つ空白があるというイメージ|
| Pattern | line-pattern | なし |　Spriteの画像を指定。画像の幅は２の倍数である必要がある |
| Translate | line-translate | [0, 0] |　線のオフセット座標([x, y])を左上を原点として指定  |
| Translate anchor | line-translate-anchor | map |　line-translateの設定が必要。map に関連付けるか、viewport に関連付けるかを指定する。mapの場合は地図を回転させても同じようにずれるが、viewportでは地図を回転させるとそれに合わせてTranslateが変動する  |
| Gap width | line-gap-width | 0 |　ラインの外側にラインを描く。値はライン間の距離を示す  |


## Layout properties
次にLayout propertiesを見ていきます。Layout propertyは基本的にはラインとシンボルにのみ存在します。（Fill等でもvisibilityの設定で存在しますが、あまり使わなさそうです。）ここではラインに関するLayout propertyのみ扱います。
https://maplibre.org/maplibre-style-spec/layers/#line

| Maputnikのプロパティ名  | MapLibre GL JS | デフォルト値 | 説明 |
|:-:|:-:|:-:|:-:|
| Cap | line-cap | butt |　ライン終点の表示。butt: ライン手前終点のお尻に合わせた角型。　round: 丸型。　square: buttと同じ形だが、roundの終点（つまり先に少し伸びている箇所）に合わせた角型  |
| Join | line-join | miter |　ラインが接合される際の形状。bevel: 角型　round: 丸型　miter: とがった形状 |
| Miter Limit | line-miter-limit | 2 |　２つのパス区分の継ぎ目が鋭角の場合に line-join を miter から bevel とするための比率。line-joinにおいて、miterであることが必要。  |
| Round limit | line-round-limit | 1.05 |　つなぎ目が浅い角度の時に、roundからmiterとする際に使用。line-joinにおいて、roundであることが必要。  |


# まとめ
本記事では、MapLibre GL JSのスタイルファイルの基本と、それがMaputnikでどのように設定されているのかを説明しました。Symbolについては、[こちらの記事](https://qiita.com/k96mz/items/318e915097bfbaa435b9)をご覧ください。ズームレベル毎の処理について今回は扱わなかったので、また記事を書こうと思います。


# Reference

https://qiita.com/T-ubu/items/02a9725dd6329d35d477

https://smellman.github.io/vector_tile_book/docs/styling/maputnik.html

https://qiita.com/k96mz/items/318e915097bfbaa435b9