setStyleのoption.diffのお話
はじめに
この記事はMapbox Newsletter WEEKLY TIPSの解説 -「マップスタイルを変更」の続きです。
setStyleにはdiffというオプションが存在します。説明文を読むと、setStyle実行時に差分だけをいい感じに変更してくれそうなのですが、実際にはどのような挙動を行うのかを確認していきます。
ドキュメントを読む
早速オプションの説明を読んでみましょう。
If a style is already set when this is used and the diff option is set to true, the map renderer will attempt to compare the given style against the map's current state and perform only the changes necessary to make the map style match the desired state.
「スタイルがすでにセットされていてdiffオプションがtrueのとき、マップレンダラーは現在の状態(スタイル)と(パラメータとして)与えられたスタイル(つまり、次のスタイル)を比較し、マップスタイルがあるべきステートになるように必要最低限の変更を実行しようとします。」ということなので、前後のスタイルの差分だけを反映させるということがわかります。もし、一つのレイヤーの色を変えるだけだとすると、スタイルを全部入れ替えるよりも素早く変更が完了することが期待できます。
Changes in sprites (images used for icons and patterns) and glyphs (fonts for label text) cannot be diffed. If the sprites or fonts used in the current style and the given style are different in any way, the map renderer will force a full update, removing the current style and building the given one from scratch.
「スプライト(アイコンやパターンに使用される画像)における変更およびグリフ(ラベルテキスト用のフォント)は差分が取れません。もし現在のスタイルと与えられたスタイル(つまり、次のスタイル)でスプライトかフォントが異なる場合、マップレンダラーは強制的にフルアップデートします。つまり、現在のスタイルを削除し、与えられたスタイル(つまり、次のスタイル)をゼロから構築します。」ということなので、スプライトやグリフに違いがある場合は強制的に全部入れ替えとなります。スプライトやグリフはスタイル定義の中ではファイルへのURLの記載となっているので、それだけでは中身に差分があるかどうかわかりません。また、もし中身を確認したとしても、一部のアイコンに変更がある場合にそのアイコンが使用されているレイヤーだけ更新するというのも困難です。ということで、スタイル定義のURLが完全一致している場合のみ、差分更新ができるということになります。注意が必要ですね。
ちなみに、diffはデフォルトでtrueです。
コードを読む
それでは実際に処理を追いかけてみましょう。
map#setStyle
setStyleはif文で分岐しています。基本的に前半がoption.diff===true後半がoption.diff===falseのときの処理と考えて良いです。
option.diff===false
簡単な後半の方から見ていきます。this._updateStyleの主要な処理は以下の部分です。
既存のスタイルを破棄し、
新しいスタイルを作成し、
スタイルをロードします。
つまり、option.diff===falseであればスタイルを作り直しているということがわかります。
option.diff===true
前半部分はどうでしょうか。this._diffStyleが実際の処理を行います。
_diffStyleの中でスタイルを読み込んだりして、this._updateDiffに処理が移ります。
さらにthis.style.setStateへと処理が渡ります。
Style#setState
ここからは処理がMapクラスからStyleクラスに移ります。
diffStylesでスタイルの差分をとります。
全般的な差分チェック
diffStylesの中では、まずスタイルの全般的な設定の差分をチェックします。spriteやglyphsもチェックされています。
ソースの差分チェック
次にソースの差分をチェックしています。
具体的には現在のスタイルにあって次のスタイルにないソースは削除されます。
逆に現在のスタイルになくて次のスタイルにあるソースは追加したりアップデートしたりします。
レイヤーの差分チェック
次にレイヤーの差分をチェックしています。
まず、削除されたソースを使っていたレイヤーを削除します。
そして差分を確認します。具体的には現在のスタイルにあって次のスタイルにないレイヤーは削除されます。
現在のスタイルと次のスタイルでレイヤーの順番が異なる場合、レイヤーの削除および追加で対応します。
そして最後に各レイヤーのプロパティ等の変更箇所を洗い出します。
差分情報の適用
これでようやくStyle#setStateのコードに帰ってきました。
さて、changesの中に差分情報がぎっしり詰め込まれていますが、以下の部分でsupportedDiffOperationsではないものが含まれていないかチェックしています。
supportedDiffOperationsは以下で定義されていますが、よく見るとsetGlyphsとsetSpriteはコメントアウトされています。なので、もしスプライトとグリフに変更があった場合にはunimplementedOps.length > 0という条件を満たしてしまい、例外が発生します。つまり、最初にドキュメントを読んで確認した、「スプライトやグリフに違いがある場合は強制的に全部入れ替え」が実行されます。
あとは差分情報にしたがってレイヤーのプロパティ等を変更していきます。
動きを確認する
長いコードで疲れたので、実際に動きを試してみます。
Studioで作ったスタイル
StudioでStreetsをベースに2つのスタイルを作成します。一つはwaterレイヤーの色を青(#0000ff)、もう一つはwaterレイヤーの色を緑(#00ff00)にします。
| blue | green |
|---|---|
![]() |
![]() |
こちらのサンプルを流用してBlue/Greenを切り替えられるようにします。
こちらが実装です。
blue/greenを切り替えると一瞬チラチラしますね。option.diffはデフォルトでtrueのハズですが、本当に差分更新が行われているのか確認します。開発者ツールで確認しますが、CodePenだとわかりにくいのでローカルにHTMLファイルなどを準備して確認するのが良いかと思います。
以下のようにStyle#setStateの中にブレークポイントをはり、blue/greenを入れ替えて見ます。

すると、changesの中にsetSpriteが入っているのが確認できます。先程コードを読んで確認しましたが、setSpriteが含まれると例外が発生して強制的に全部入れ替えになります。

実際、ステップ実行すると以下のように例外のコードに入ります。

ということは、スタイル定義の中のスプライトが別物ということになります。以下のコマンドを実行してスタイルの違いを確認します。
% TOKEN="YOUR PUBLIC TOKEN HERE" && diff <(curl -s "https://api.mapbox.com/styles/v1/yochi/clkzgcnei003701pod9bb574h?access_token=${TOKEN}" | jq) <(curl -s "https://api.mapbox.com/styles/v1/yochi/clkzge8ug003g01r8by1k4ouh?access_token=${TOKEN}" | jq)
結果は以下のとおりです。座標やZoomに差分がありますが、これは気にする必要はありません。また、fill-colorの違いはwaterレイヤーの色の違いです。さて、大事なのがspriteの違いです。同じStreetsからスタイルを作成してもspriteのURLが異なります(パスの中にスタイルIDが入っているため)。これによりoption.diffがtrueであるにも関わらず、差分更新されません。
3c3
< "name": "blue",
---
> "name": "green",
105,106c105,106
< -123.67205718097136,
< 6.0166254905848575
---
> -92.25,
> 37.75
108c108
< "zoom": 1.758223194268734,
---
> "zoom": 2,
167c167
< "sprite": "mapbox://sprites/yochi/clkzgcnei003701pod9bb574h/6iqitl5z21pbbxxwuij8b59i2",
---
> "sprite": "mapbox://sprites/yochi/clkzge8ug003g01r8by1k4ouh/6iqitl5z21pbbxxwuij8b59i2",
1008c1008
< "fill-color": "rgb(0, 0, 255)"
---
> "fill-color": "rgb(0, 255, 0)"
13603,13605c13603,13605
< "created": "2023-08-06T13:01:12.954Z",
< "modified": "2023-08-06T13:04:49.661Z",
< "id": "clkzgcnei003701pod9bb574h",
---
> "created": "2023-08-06T13:02:27.400Z",
> "modified": "2023-08-06T13:04:39.976Z",
> "id": "clkzge8ug003g01r8by1k4ouh",
スタイルを自分でホストする
spriteが異なる以上、差分更新は期待できません。そこで、ダウンロードしたスタイルファイルのspriteをStreets v12のデフォルトmapbox://sprites/mapbox/streets-v12に変更し、スタイルファイルを自分のサーバにホストするようにします。
今回はGist上にスタイルファイルを置きました。
結果は以下のとおりです。部分更新が働いたため、海の色がなめらかに変化するのがわかります。内部的にはwaterレイヤーに対してsetPaintPropertyでプロパティを変更しているだけなので高速でなめらかです。
ブレークポイントをはり、Style#setStateが最後まで実行される様子も確認してみてください。
まとめ
Map#setStyleのoptions.diffは以下のような挙動をします。
- 同じレイヤーがあればプロパティ等を変更する
- 現在のスタイルにしかないソース・レイヤーは削除される
そのため、自分で追加したレイヤーを残したままスタイルを変更するという挙動は基本的には出来ません。
無理やりやるのであれば、
- 変更前後でベースとなるコアスタイルに対して、自分で作成するソース・レイヤーをスタイルとして予め追加・作成
- そのレイヤーのプロパティのみ変化させる
という手法が可能です。しかし、あまり活用できそうな事例はないかもしれません。
実は、setStyleに関してX(Twitter)で以下のようなご意見を頂戴しております。
あとから追加したレイヤーを隠したまま『背景地図』だけを切り替えたい
diffオプションでこれを実現できそうに見えますが、実はあまり関係ない機能でした。Mobile SDKでは自分で追加したレイヤーを永続化させることでこの挙動を実現する「Persistent Layer」という機能がありますが、JavaScriptではその機能がないというのが現状です。


Discussion