🦔

setStyleのoption.diffのお話

2023/08/10に公開

はじめに

この記事は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

setStyleif文で分岐しています。基本的に前半がoption.diff===true後半がoption.diff===falseのときの処理と考えて良いです。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1872-L1885

option.diff===false

簡単な後半の方から見ていきます。this._updateStyleの主要な処理は以下の部分です。

既存のスタイルを破棄し、

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1899

新しいスタイルを作成し、

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1904

スタイルをロードします。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1908

つまり、option.diff===falseであればスタイルを作り直しているということがわかります。

option.diff===true

前半部分はどうでしょうか。this._diffStyleが実際の処理を行います。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1878

_diffStyleの中でスタイルを読み込んだりして、this._updateDiffに処理が移ります。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1933

さらにthis.style.setStateへと処理が渡ります。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/ui/map.js#L1943

Style#setState

ここからは処理がMapクラスからStyleクラスに移ります。

diffStylesでスタイルの差分をとります。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style/style.js#L648-L649

全般的な差分チェック

diffStylesの中では、まずスタイルの全般的な設定の差分をチェックします。spriteglyphsもチェックされています。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L353-L385

ソースの差分チェック

次にソースの差分をチェックしています。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L394

具体的には現在のスタイルにあって次のスタイルにないソースは削除されます。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L165-L170

逆に現在のスタイルになくて次のスタイルにあるソースは追加したりアップデートしたりします。
https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L173-L186

レイヤーの差分チェック

次にレイヤーの差分をチェックしています。

まず、削除されたソースを使っていたレイヤーを削除します。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L402-L410

そして差分を確認します。具体的には現在のスタイルにあって次のスタイルにないレイヤーは削除されます。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L239-L248

現在のスタイルと次のスタイルでレイヤーの順番が異なる場合、レイヤーの削除および追加で対応します。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L251-L271

そして最後に各レイヤーのプロパティ等の変更箇所を洗い出します。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style-spec/diff.js#L274-L325

差分情報の適用

これでようやくStyle#setStateのコードに帰ってきました。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style/style.js#L648-L649

さて、changesの中に差分情報がぎっしり詰め込まれていますが、以下の部分でsupportedDiffOperationsではないものが含まれていないかチェックしています。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style/style.js#L655-L658

supportedDiffOperationsは以下で定義されていますが、よく見るとsetGlyphssetSpriteはコメントアウトされています。なので、もしスプライトとグリフに変更があった場合にはunimplementedOps.length > 0という条件を満たしてしまい、例外が発生します。つまり、最初にドキュメントを読んで確認した、「スプライトやグリフに違いがある場合は強制的に全部入れ替え」が実行されます。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style/style.js#L89-L106

あとは差分情報にしたがってレイヤーのプロパティ等を変更していきます。

https://github.com/mapbox/mapbox-gl-js/blob/v2.15.0/src/style/style.js#L660-L667

動きを確認する

長いコードで疲れたので、実際に動きを試してみます。

Studioで作ったスタイル

StudioでStreetsをベースに2つのスタイルを作成します。一つはwaterレイヤーの色を青(#0000ff)、もう一つはwaterレイヤーの色を緑(#00ff00)にします。

blue green
blue green

こちらのサンプルを流用してBlue/Greenを切り替えられるようにします。

こちらが実装です。

blue/greenを切り替えると一瞬チラチラしますね。option.diffはデフォルトでtrueのハズですが、本当に差分更新が行われているのか確認します。開発者ツールで確認しますが、CodePenだとわかりにくいのでローカルにHTMLファイルなどを準備して確認するのが良いかと思います。

以下のようにStyle#setStateの中にブレークポイントをはり、blue/greenを入れ替えて見ます。

debug

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

changes

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

exception

ということは、スタイル定義の中のスプライトが別物ということになります。以下のコマンドを実行してスタイルの違いを確認します。

% 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.difftrueであるにも関わらず、差分更新されません。

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#setStyleoptions.diffは以下のような挙動をします。

  • 同じレイヤーがあればプロパティ等を変更する
  • 現在のスタイルにしかないソース・レイヤーは削除される

そのため、自分で追加したレイヤーを残したままスタイルを変更するという挙動は基本的には出来ません。

無理やりやるのであれば、

  1. 変更前後でベースとなるコアスタイルに対して、自分で作成するソース・レイヤーをスタイルとして予め追加・作成
  2. そのレイヤーのプロパティのみ変化させる

という手法が可能です。しかし、あまり活用できそうな事例はないかもしれません。

実は、setStyleに関してX(Twitter)で以下のようなご意見を頂戴しております。

あとから追加したレイヤーを隠したまま『背景地図』だけを切り替えたい

diffオプションでこれを実現できそうに見えますが、実はあまり関係ない機能でした。Mobile SDKでは自分で追加したレイヤーを永続化させることでこの挙動を実現する「Persistent Layer」という機能がありますが、JavaScriptではその機能がないというのが現状です。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion