🌊

Mapbox GL JS v3のnested style

2023/08/22に公開

はじめに

先日、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ではインポートされたスタイルは「ベースマップ」という位置付けになり(以下ではベーススタイルと呼びます)、基本的にユーザーは内部構造を気にする必要はありません。

しかし、レイヤーが隠蔽されると以下の点で困ります。

  1. addLayerでレイヤーを挿入する場所が指定できない
  2. ベーススタイルの各レイヤーのプロパティ(例えば道路の色)が変更できない

これらを解決するために以下の機能が導入されました。

  1. slotでレイヤーの挿入位置を指定する
  2. 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をさがし、その位置にレイヤーを挿入する処理が行われます。

https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L676-L721

setConfigProperty

MapクラスにsetConfigProperty(importId: string, configName: string, value: any)というメソッドが追加されました。これはベーススタイルのschemaで定義されたconfigの値を書き換えるためのメソッドです。第一引数がimportsで指定したスタイルのid、第二引数が変更するconfigの名称、第三引数がconfigの値です。

わかりにくいので、まずベーススタイルの定義から見ていきます。以下のようにschemaというキーを用意し、その中でconfigを指定していきます。ここではbuildingColorというconfigを作成しています。このconfiggreen,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');

通常はsetPaintPropertysetLayoutPropertyで任意のレイヤーの任意のプロパティを変更します。しかし、インポートされたスタイルはレイヤーが隠蔽されていてこの方法で変更できません。そこで、かわりにsetConfigPropertyでプロパティを変更します。

そうすると「ベーススタイルで指定されたconfigの値しか変更できない」ということになります。これはレイヤーコントロールの柔軟性という観点ではマイナスに思えます。しかし、setConfigPropertyを導入することで、複雑なレイヤー構造を知ることなくユーザーが変更したいであろうプロパティのみを簡単に操作することができます。

slotsetPaintPropertyのまとめ

Nested styleでレイヤーを隠蔽し、かわりにslotsetPaintPropertyを導入することで簡単なレイヤー操作が実現されます。また、slotschemaを維持している限り、ユーザーの環境に影響を与えることなくベーススタイルのレイヤー構造を変更することができるようになります。これはベーススタイルの作成者にとって大きなメリットとなります。オブジェクト指向における「インターフェースを公開し、実装を隠蔽する」のと同じような発想です。

以下にnested styleのサンプルを作成しました。ぜひ挙動とコードを確認してみてください。

Mapbox Standard Style

GL JS v3と共にリリースされたMapbox Standard Styleにも以下のようにslotschemaが定義されています。つまり、先程見てきた簡単なレイヤーコントロール機能が使用できるということになります。

  "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として読み込まれます。以下のコードが該当箇所です。

https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L523-L532

schemaが定義されている場合には、空のスタイルが作成されます。そして、そのスタイルのimportsとしてスタイルが読み込まれます。つまり自動的にnested styleとなります。また、このとき使用されるidがハードコードされたbasemapです。

ここまで理解すると、Migration Guideの説明も納得できるかと思います。

以下の第一引数のbasemapは自動的にnested styleとしてロードされた際にハードコードされた値でした。第二引数のconfigおよび第三引数のvalueはMapbox Standard Styleのschemaに記述されていました。

map.setConfigProperty('basemap', 'showPointOfInterestLabels', false);

slotbottommiddleもMapbox Standard Styleを参照すれば見つかりました。

まとめ

GL JS v3ではnested styleという新しい機能が登場しました。これはスタイルを定義する際に別のスタイルをインポートする機能で、既存のスタイルを簡単に再利用することができます。また、インポートされたスタイルのレイヤーは隠蔽されるため、slotsetConfigPropertyが提供されます。レイヤーコントロールの複雑さを排除し、ベーススタイルをユーザーコードから疎結合にするためにレイヤーは隠蔽されます。

Mapbox Standard Styleもnested styleとして読み込まれるため、slotsetConfigPropertyでコントロールします。

おまけ - 隠蔽されたレイヤーにまつわる挙動

レイヤーはStyleクラスで管理されます。以前から_layersでレイヤーが管理されていましたが、v3からは_ownLayersが追加されています。

レイヤーIDの管理

_layersはインポートされたスタイルも含め、すべてのレイヤーが管理されます。ただし、スタイル間でレイヤーIDが重複するのを避けるため、インポートされたスタイルのレイヤーのIDには以下のようなサフィックスが付加されます。

[レイヤーID]\u001F[import id]

このサフィックスを付加する処理は以下のコードで行われます。

https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L578

makeFQIDの処理は以下の通り、idscope(import id)を区切り文字\u001Fで繋いでいます。

https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/util/fqid.js#L5-L8

レイヤーの参照

それではユーザーのコードで使用される、レイヤーを参照するものをいくつか見てみます。

Map#getStyle()

getStyleはスタイル情報を取得するメソッドで、内部でStyle#serialize()を呼び出します。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/ui/map.js#L1993-L1997

以下ではserializeによって得られるlayersimportsについて見てみます。

layers

serialize()では以下の部分でlayersを作成します。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L1836

ここで重要なのが引数のthis._ownOrderです。この配列は_ownLayersで管理されているレイヤーのレイヤーIDが並び順で入っています。つまり、this._serializeLayersは自分自身のレイヤーだけを対象として処理を行い、インポートされたスタイルのレイヤーは返しません。

imports

serialize()では以下の部分でimportsを作成します。
https://github.com/mapbox/mapbox-gl-js/blob/main/src/style/style.js#L1823

this.stylesheetにはスタイルのJSONデータがそのまま入っています。したがいまして、以下のようにアクセスするとインポートされたスタイルの情報がそのまま参照できます。

map.getStyle().imports[0].data;

ただし、これは元データを読み込んでいるだけなので、これを使ってプロパティの変更等はできません。

Map#addLayer()

ここでは第二引数のbeforeIdが指定された際の挙動を見ていきます。addLayerStyle#addLayer()を呼び出します。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/ui/map.js#L2548

befoerIdが指定されているとthis._ownOrderの中から一致するレイヤーIDを探します。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L1448-L1452

つまり、インポートされたスタイルのレイヤーは含まれないので、たとえサフィックスを付加したレイヤーIDを指定してもレイヤーが見つからず、エラーとなります。

Map#setPaintProperty

setPaintPropertyStyle#setPaintPropertyを呼び出します。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/ui/map.js#L2753

そこではgetLayerでレイヤーを探しますが、実はこのメソッドは_layersを検索します。つまり、インポートされたスタイルのレイヤーも対象となります。
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L1700
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.1/src/style/style.js#L1568-L1570

ということで、setPaintPropertyは以下のようにサフィックスを付加したレイヤーIDを指定すると、インポートされたスタイルのレイヤーのプロパティを直接変更できます。

map.setPaintProperty('road\u001Fstreets', 'line-color', 'red');

ただし、FQIDを手動で作成しているということは、内部構造に依存したコードなので変更に弱いです。また、インポートされたスタイルのレイヤーを検索しなくなるように今後変更される予定です。したがいまして、直接インポートされたスタイルのレイヤーのプロパティを変更するのは控えたほうが良いでしょう。

Map#setLayoutPropertyも同様です。

2023/10/29更新

以下のように、setPaintPropertyおよびsetLayoutPropertygetOwnLayerが使用されるように変更されました(v3.0.0-beta.2)。したがいまして、現在は上記の方法でインポートされたスタイルのレイヤーを操作することはできなくなりました。

https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.2/src/style/style.js#L1702-L1705
https://github.com/mapbox/mapbox-gl-js/blob/v3.0.0-beta.2/src/style/style.js#L1670-L1673

変更が加えられたcommit
https://github.com/mapbox/mapbox-gl-js/commit/4199a0cddca3606e200f5e9f47d254ab319b0fe3

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

Discussion