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