Mapbox GL JS v3のnested style
はじめに
先日、Mapbox Standard StyleとMapbox GL JS v3.0.0-beta.1が公開されました。最も目を引く機能は3D Tilesのサポートでしょう。3Dモデルを地図上に表示することができるため、以下のよう東京タワーもきれいに表現されています。
さて、そんな目玉機能の裏でスタイルスペックの大きな変更がありました。この記事ではその変更内容について解説します。
サマリー
- Nested styleはレイヤーが隠蔽される
- 隠蔽されたレイヤーは
slot
,setConfigProperty
でコントロールする - Mapbox Standard Styleはnested styleとしてロードされる
Standard Styleを使ってみると「あれ、レイヤーが見えないぞ」と思われたかもしれません。この記事ではその疑問を解消いたします。
注意事項
Mapbox Standard StyleおよびMapbox GL JS v3はベータ版としてリリースされています。そのため、機能内容・機能名称・メソッド名等が今後変更される可能性があります。また、正式リリースまではサポート対象外となります。
Nested style
Nested styleはその名の通りネストできるスタイルです。具体的にはスタイルの中でベースとなるスタイルをインポートすることができます。以下の例のimports
の部分が該当箇所です。
{
"version": 8,
"name": "My Style",
"imports": [{ "id": "streets", url:"mapbox://styles/mapbox/streets-v12" }],
"sources": {
...
},
"layers": [
...
]
}
今までは既存のスタイルに自分のレイヤーを追加するには以下のような方法を使用していました。
- 既存のスタイルをロードし、コードで
addLayer
して自分のレイヤーを追加 - 既存のスタイルをコピーし、それを編集することで自分のレイヤーを追加(Studioでの編集作業)
GL JS v3ではnested styleにより、既存のスタイルをインポートしてその上に自分でデザインしたスタイルを被せることが簡単にできるようになります。
Nested style の特徴
Nesed styleの最も重要な特徴の一つが「インポートされたスタイルのレイヤーが隠蔽されること」です。
従来Mapboxのスタイルは、既存のスタイルのレイヤーもユーザーが作成したレイヤーも区別なく扱われるため、柔軟なカスタマイズが可能です。その一方で既存のスタイルの複雑なレイヤー構造を理解する必要があるという側面があります。
Nested styleではインポートされたスタイルは「ベースマップ」という位置付けになり(以下ではベーススタイルと呼びます)、基本的にユーザーは内部構造を気にする必要はありません。
しかし、レイヤーが隠蔽されると以下の点で困ります。
-
addLayer
でレイヤーを挿入する場所が指定できない - ベーススタイルの各レイヤーのプロパティ(例えば道路の色)が変更できない
これらを解決するために以下の機能が導入されました。
-
slot
でレイヤーの挿入位置を指定する -
setConfigProperty
で決められたプロパティを変更する
それぞれの機能について見ていきましょう。
slot
slot
はレイヤーの一種です。fill
のようなレイヤーと同じ位置付けです。"type":"slot"
として定義します。具体的には以下のように使用します。
"layers": [
{
"id": "road",
"type": "line",
"source": "road",
"paint": {
...
}
},
{
"id": "here",
"type": "slot",
},
{
"id": "building",
"type": "fill",
"source": "building",
"paint": {
...
}
},
]
さて、このslot
レイヤーが含まれるスタイルをインポートした場合、以下のようにaddLayer
を呼び出すことでslot
レイヤーの位置に自分で作成したレイヤーを挿入する事ができます。
map.addLayer({
id: 'my-layer',
type: 'fill',
slot: 'here',
source: 'my-source',
paint: {
...
}
});
通常はaddLayer
の第二引数にbeforeId
として挿入位置を指定します。しかし、インポートされたスタイルはレイヤーが隠蔽されていてこの方法で挿入できません。そこで、かわりにslot
で挿入位置を指定します。
そうすると「ベーススタイルで指定されたslot
の位置にしかレイヤーを挿入できない」ということになります。これはレイヤーコントロールの柔軟性という観点ではマイナスに思えます。しかし、slot
を導入することで、複雑なレイヤー構造を知ることなくスタイルが推奨する場所に自分で作成したレイヤーを簡単に追加することができます。
Google MapsではWebGLオーバーレイを使用すると、GeoJsonLayer
が「”道路の上”かつ”シンボルの下”」のようないい感じの場所に自動的に挿入されていました(参考記事)。slot
はこれと似たようなユーザー体験をもたらします。
また、レイヤーが隠蔽されているのにどうやってslot
の位置を知るのが疑問に思われるかもしれません。確かにレイヤーはユーザーからは隠蔽されていますが、内部的には保持されています。addLayer
の際、以下のコードでslot
をさがし、その位置にレイヤーを挿入する処理が行われます。
setConfigProperty
MapクラスにsetConfigProperty(importId: string, configName: string, value: any)
というメソッドが追加されました。これはベーススタイルのschema
で定義されたconfig
の値を書き換えるためのメソッドです。第一引数がimports
で指定したスタイルのid
、第二引数が変更するconfig
の名称、第三引数がconfig
の値です。
わかりにくいので、まずベーススタイルの定義から見ていきます。以下のようにschema
というキーを用意し、その中でconfig
を指定していきます。ここではbuildingColor
というconfig
を作成しています。このconfig
はgreen
,yellow
,red
の3種類の値をとり、デフォルトはgreen
です。また、building
レイヤーは["config", "buildingColor"]
というExpressionsを使用することで、ポリゴンの色としてbuildingColor
の値を使用します。
"schema": {
"buildingColor": {
"default": "green",
"type": "boolean",
"values": [
"green",
"yellow",
"red",
],
},
},
"layers": [
...
{
"id": "building",
"type": "fill",
"source": "building",
"paint": {
"fill-color": ["config", "buildingColor"],
}
},
]
さて、ユーザーのコードから以下のようにしてbuildingColor
を変更します。
map.setConfigProperty('streets', 'buildingColor', 'yellow');
通常はsetPaintProperty
やsetLayoutProperty
で任意のレイヤーの任意のプロパティを変更します。しかし、インポートされたスタイルはレイヤーが隠蔽されていてこの方法で変更できません。そこで、かわりにsetConfigProperty
でプロパティを変更します。
そうすると「ベーススタイルで指定されたconfig
の値しか変更できない」ということになります。これはレイヤーコントロールの柔軟性という観点ではマイナスに思えます。しかし、setConfigProperty
を導入することで、複雑なレイヤー構造を知ることなくユーザーが変更したいであろうプロパティのみを簡単に操作することができます。
slot
、setPaintProperty
のまとめ
Nested styleでレイヤーを隠蔽し、かわりにslot
とsetPaintProperty
を導入することで簡単なレイヤー操作が実現されます。また、slot
とschema
を維持している限り、ユーザーの環境に影響を与えることなくベーススタイルのレイヤー構造を変更することができるようになります。これはベーススタイルの作成者にとって大きなメリットとなります。オブジェクト指向における「インターフェースを公開し、実装を隠蔽する」のと同じような発想です。
以下にnested styleのサンプルを作成しました。ぜひ挙動とコードを確認してみてください。
Mapbox Standard Style
GL JS v3と共にリリースされたMapbox Standard Styleにも以下のようにslot
とschema
が定義されています。つまり、先程見てきた簡単なレイヤーコントロール機能が使用できるということになります。
"schema": {
"showPlaceLabels": {
"default": true,
"type": "boolean",
"metadata": {
"mapbox:title": "Place labels visibility",
"mapbox:description": "Shows and hides place label layers."
}
},
"showRoadLabels": {
"default": true,
"type": "boolean",
"metadata": {
"mapbox:title": "Road labels visibility",
"mapbox:description": "Shows and hides all road labels, including road shields."
}
},
"showPointOfInterestLabels": {
"default": true,
"type": "boolean",
"metadata": {
"mapbox:title": "POI labels visibility",
"mapbox:description": "Shows or hides all POI icons and text."
}
},
"showTransitLabels": {
"default": true,
"type": "boolean",
"metadata": {
"mapbox:title": "Transit labels visibility",
"mapbox:description": "Shows or hides all transit icons and text."
}
},
"lightPreset": {
"default": "day",
"values": [
"dawn",
"day",
"dusk",
"night"
],
"metadata": {
"mapbox:title": "Light presets",
"mapbox:description": "Switch between 4 time-of-day states: dusk, dawn, day and night."
}
},
"font": {
"default": "DIN Pro",
"type": "string",
"values": [
"Alegreya",
"Alegreya SC",
"Asap",
"Barlow",
"DIN Pro",
"EB Garamond",
"Faustina",
"Frank Ruhl Libre",
"Heebo",
"Inter",
"League Mono",
"Montserrat",
"Poppins",
"Raleway",
"Roboto",
"Roboto Mono",
"Rubik",
"Source Code Pro",
"Spectral",
"Ubuntu"
],
"metadata": {
"mapbox:title": "Font",
"mapbox:description": "Defines font family for the style from predefined options."
}
}
},
"layers": [
...,
{
"id": "bottom",
"type": "slot"
},
...
{
"id": "middle",
"type": "slot"
},
...
{
"id": "top",
"type": "slot"
}
]
しかし、ここで一つ疑問が生じます。Mapbox Standard StyleはGL JS v3のデフォルトのスタイルとして使用されるため、nested styleではないはずです。いったいどうなっているのでしょうか。
実は、schema
が定義されているスタイルは自動的にnested styleとして読み込まれます。以下のコードが該当箇所です。
schema
が定義されている場合には、空のスタイルが作成されます。そして、そのスタイルのimports
としてスタイルが読み込まれます。つまり自動的にnested styleとなります。また、このとき使用されるidがハードコードされたbasemap
です。
ここまで理解すると、Migration Guideの説明も納得できるかと思います。
以下の第一引数のbasemap
は自動的にnested styleとしてロードされた際にハードコードされた値でした。第二引数のconfig
および第三引数のvalue
はMapbox Standard Styleのschema
に記述されていました。
map.setConfigProperty('basemap', 'showPointOfInterestLabels', false);
slot
のbottom
、middle
もMapbox Standard Styleを参照すれば見つかりました。
まとめ
GL JS v3ではnested styleという新しい機能が登場しました。これはスタイルを定義する際に別のスタイルをインポートする機能で、既存のスタイルを簡単に再利用することができます。また、インポートされたスタイルのレイヤーは隠蔽されるため、slot
やsetConfigProperty
が提供されます。レイヤーコントロールの複雑さを排除し、ベーススタイルをユーザーコードから疎結合にするためにレイヤーは隠蔽されます。
Mapbox Standard Styleもnested styleとして読み込まれるため、slot
とsetConfigProperty
でコントロールします。
おまけ - 隠蔽されたレイヤーにまつわる挙動
レイヤーはStyleクラスで管理されます。以前から_layers
でレイヤーが管理されていましたが、v3からは_ownLayers
が追加されています。
レイヤーIDの管理
_layers
はインポートされたスタイルも含め、すべてのレイヤーが管理されます。ただし、スタイル間でレイヤーIDが重複するのを避けるため、インポートされたスタイルのレイヤーのIDには以下のようなサフィックスが付加されます。
[レイヤーID]\u001F[import id]
このサフィックスを付加する処理は以下のコードで行われます。
makeFQID
の処理は以下の通り、id
とscope
(import id)を区切り文字\u001F
で繋いでいます。
レイヤーの参照
それではユーザーのコードで使用される、レイヤーを参照するものをいくつか見てみます。
Map#getStyle()
getStyle
はスタイル情報を取得するメソッドで、内部でStyle#serialize()
を呼び出します。
以下ではserialize
によって得られるlayers
とimports
について見てみます。
layers
serialize()
では以下の部分でlayers
を作成します。
ここで重要なのが引数のthis._ownOrder
です。この配列は_ownLayers
で管理されているレイヤーのレイヤーIDが並び順で入っています。つまり、this._serializeLayers
は自分自身のレイヤーだけを対象として処理を行い、インポートされたスタイルのレイヤーは返しません。
imports
serialize()
では以下の部分でimports
を作成します。
this.stylesheet
にはスタイルのJSONデータがそのまま入っています。したがいまして、以下のようにアクセスするとインポートされたスタイルの情報がそのまま参照できます。
map.getStyle().imports[0].data;
ただし、これは元データを読み込んでいるだけなので、これを使ってプロパティの変更等はできません。
Map#addLayer()
ここでは第二引数のbeforeId
が指定された際の挙動を見ていきます。addLayer
はStyle#addLayer()
を呼び出します。
befoerId
が指定されているとthis._ownOrder
の中から一致するレイヤーIDを探します。
つまり、インポートされたスタイルのレイヤーは含まれないので、たとえサフィックスを付加したレイヤーIDを指定してもレイヤーが見つからず、エラーとなります。
Map#setPaintProperty
setPaintProperty
はStyle#setPaintProperty
を呼び出します。
そこではgetLayer
でレイヤーを探しますが、実はこのメソッドは_layers
を検索します。つまり、インポートされたスタイルのレイヤーも対象となります。
ということで、setPaintProperty
は以下のようにサフィックスを付加したレイヤーIDを指定すると、インポートされたスタイルのレイヤーのプロパティを直接変更できます。
map.setPaintProperty('road\u001Fstreets', 'line-color', 'red');
ただし、FQID
を手動で作成しているということは、内部構造に依存したコードなので変更に弱いです。また、インポートされたスタイルのレイヤーを検索しなくなるように今後変更される予定です。したがいまして、直接インポートされたスタイルのレイヤーのプロパティを変更するのは控えたほうが良いでしょう。
Map#setLayoutProperty
も同様です。
2023/10/29更新
以下のように、setPaintProperty
およびsetLayoutProperty
でgetOwnLayer
が使用されるように変更されました(v3.0.0-beta.2)。したがいまして、現在は上記の方法でインポートされたスタイルのレイヤーを操作することはできなくなりました。
変更が加えられたcommit
Discussion