Supercherenkoスマレジアプリと解説メディアの個人開発

地図の見た目はどこまで指定できるのか(MapLibre Style Spec v8 入門)

地図のスタイルをこれから書く開発者向けの入門記事です。MapLibre のスタイルファイルで「何を指定できるのか」を、仕様ファイルを数えながら見渡します。個々の書き方より、全体の量と偏りを掴むことが目的です。

前提を書きます。仕様は MapLibre Style Spec の version 8。数はリポジトリの `src/reference/v8.json` を2026年9月5日に取得して数えたものです。各機能がどのバージョンから使えるかも、この定義ファイルの sdk-support から引いています。この記事に載せるJSONは公式の gl-style-validate に通してエラーなしを確認していますが、ブラウザでの描画までは見ていません。

スタイルとタイルの関係そのものは前の記事に書きました。ここでは重なる部分を短く済ませます。

指定できるのは大きく3つ

スタイルファイルが決めているのは3つです。どのデータを使うか(sources)、どの順で描くか(layers 配列の並び)、そしてどう描くか(各レイヤーの layoutpaint)。この記事は3つめの話です。

描き方はレイヤーの型ごとに決まっていて、型は10種類あります。型ごとに指定できるプロパティを数えると、合計146個でした。内訳は layout が64個、paint が82個です。このうち10個は全型に共通の visibility なので、型に固有のものは136個ということになります。

型ごとの数を多い順に並べてみます。偏りははっきりしています。

  • symbol(アイコンと文字ラベル): 61個
  • line(線): 18個
  • circle(円): 13個
  • fill(面): 10個
  • fill-extrusion(3Dの押し出し): 10個
  • raster(画像タイル): 10個
  • hillshade(陰影起伏): 10個
  • heatmap(ヒートマップ): 6個
  • color-relief(標高の段彩): 4個
  • background(背景): 4個

この並びが、そのまま学習と調整にかかる手間の順番になります。

146個は layers 配列の中の話です。その外側、スタイルのルート直下にも見た目の設定があります。空の色を決める `sky` が7個、3Dの押し出しに当たる光の light が4個、地形の起伏を出す terrain が2個、投影法の projection が1個。どれもレイヤーではないので source を取らず、地図全体に一度だけかかります。sky は MapLibre GL JS 4.5.0 からで、仕様のページには次の断りが付いています。

Note: this definition is still experimental and is under development in maplibre-gl-js.

この記事では扱いませんが、レイヤーの外にもう一段あることは頭に置いてください。

面と線でできること

面から見ます。fill のプロパティは10個で、実務で触るのは色まわりの4つです。

  • fill-color: 塗りの色。既定は #000000
  • fill-outline-color: 縁の色
  • fill-opacity: 不透明度
  • fill-pattern: スプライトの画像で塗る

残りは位置のずらし(fill-translatefill-translate-anchor)、アンチエイリアスの有無、描画順の指定(fill-sort-key)、表示切り替え(visibility)です。面はこれで全部です。水域を塗るならこう書きます。

{ "id": "water", "type": "fill", "source": "gsi", "source-layer": "waterarea",
  "paint": { "fill-color": "#aecfe2", "fill-outline-color": "#8fb4cc",
    "fill-opacity": 0.9 } }

線は18個あり、面より少し込み入っています。役割で3つに分かれます。太さの系統が line-widthline-gap-widthline-offset。端と角の処理が line-capline-joinline-miter-limitline-round-limit。模様が line-dasharrayline-patternline-gradient です。

このうち line-gap-width の使いどころは説明が要ります。仕様の説明は「Draws a line casing outside of a line's actual path」で、線の実体の外側に縁を描くためのものです。道路を「白い縁の上に灰色の本体」で描く定番は、同じデータを2枚重ねて作ります。

{ "id": "road-casing", "type": "line", "source": "gsi", "source-layer": "road",
  "layout": { "line-cap": "round", "line-join": "round" },
  "paint": { "line-color": "#ffffff",
    "line-gap-width": ["interpolate", ["linear"], ["zoom"], 12, 1, 16, 6],
    "line-width": 1.5 } }

破線は配列で指定します。鉄道を4対3の破線で描くなら "line-dasharray": [4, 3] です。

ラベルとアイコンだけが桁違いに多い

symbol は61個で、2番目に多い line の3倍以上あります。しかも内訳が偏っていて、layout が47個、paint は14個です。47個を並べてみます。

symbol-placement, symbol-spacing, symbol-avoid-edges, symbol-sort-key,
symbol-z-order, icon-allow-overlap, icon-overlap, icon-ignore-placement,
icon-optional, icon-rotation-alignment, icon-size, icon-text-fit,
icon-text-fit-padding, icon-image, icon-rotate, icon-padding,
icon-keep-upright, icon-offset, icon-anchor, icon-pitch-alignment,
text-pitch-alignment, text-rotation-alignment, text-field, text-font,
text-size, text-max-width, text-line-height, text-letter-spacing,
text-justify, text-radial-offset, text-variable-anchor,
text-variable-anchor-offset, text-anchor, text-max-angle,
text-writing-mode, text-rotate, text-padding, text-keep-upright,
text-transform, text-offset, text-allow-overlap, text-overlap,
text-ignore-placement, text-optional, symbol-height-offset,
symbol-height-anchor, visibility

対する paint の14個は、色と不透明度と縁取り、それに位置の微調整だけです。text-colortext-halo-colortext-halo-widthtext-halo-blur とアイコン側の同じ組み合わせ。

事実として、146個のうち61個が symbol に集まり、その47個が layout 側にあります。理由は仕事の中身の違いです。面と線は、置き場所が地物の座標で決まっています。文字とアイコンは、座標のそばのどこに置くかを描画側が決めなければなりません。他のラベルとぶつかったらどうするか(text-allow-overlaptext-paddingtext-ignore-placement)、線に沿わせるのか点に置くのか(symbol-placementtext-max-angle)、地図を回したとき文字も回すのか(text-rotation-alignmenttext-keep-upright)、長い名前をどこで折るのか(text-max-widthtext-line-height)。この判断の全部がプロパティになっています。塗るだけの処理は最後に来るので、paint は14個で足ります。

帰結として、地図の見た目で時間を取られるのはほぼラベルです。面と線は10個と18個なので、半日も触れば底が見えます。学習の順番も、面から線、最後に記号でよいことになります。

日本語の地図で覚えておく価値があるのは2つです。1つめは縦書きで、text-writing-mode["horizontal", "vertical"] を渡します。仕様は指定の性質にこう断りを入れています。

Note that the property values act as a hint, so that a symbol whose language doesn’t support the provided orientation will be laid out in its natural orientation.

つまり縦書きに対応しない言語のラベルは、指定しても横のまま置かれます。MapLibre GL JS での対応は 1.3.0 からです。

2つめは text-variable-anchor です。

To increase the chance of placing high-priority labels on the map, you can provide an array of text-anchor locations.

ラベルの置き場所を候補の配列で渡しておくと、上が詰まっていれば下、それも駄目なら左、と順に試してくれます。0.54.0 からの機能です。地名が密集する市街地では、これがあるかないかで消えるラベルの数が変わります。

{ "id": "place", "type": "symbol", "source": "gsi", "source-layer": "label",
  "layout": { "text-field": ["get", "knj"],
    "text-font": ["Noto Sans Regular"],
    "text-size": ["step", ["zoom"], 11, 13, 13, 15, 16],
    "text-writing-mode": ["horizontal", "vertical"],
    "text-variable-anchor": ["center", "top", "bottom", "left", "right"],
    "text-radial-offset": 0.6,
    "icon-image": "dot", "icon-size": 0.8 },
  "paint": { "text-color": "#14171c", "text-halo-color": "#ffffff",
    "text-halo-width": 1.4, "icon-color": "#14171c" } }

最後の icon-color には条件があります。仕様は「This can only be used with SDF icons」と書いていて、スプライトの索引で sdf: true になっている画像にしか当たりません。普通のPNGアイコンの色は、スタイルからは変えられません。

残りの型でできること

ここまでで触れていない6つを、何のための型かという観点で並べます。括弧内は MapLibre GL JS で使えるようになったバージョンです。

circle(0.10.0)は点を丸で描きます。よく使うのは circle-radiuscircle-color、それに縁の circle-stroke-widthcircle-stroke-color。三角点や標高点のような点データを、アイコンを用意せずに出せます。

fill-extrusion(0.27.0)は面を高さ方向に押し出します。上端と下端は fill-extrusion-heightfill-extrusion-base で決めます。高さについての仕様の但し書きが面白いところです。

Negative values extrude below ground level, so a floor that is entirely underground can be expressed.

raster(0.10.0)は画像タイルを貼り、色を後から調整します。raster-brightness-minraster-brightness-maxraster-saturationraster-contrastraster-hue-rotate。航空写真の彩度を落として下地に敷き、その上にベクターの線とラベルを載せる、という組み方ができます。

hillshade(0.43.0)は標高データから陰影を作ります。hillshade-exaggeration で強さ、hillshade-illumination-direction で光の向き。DEM の形式については仕様が「Mapbox Terrain RGB, Mapzen Terrarium tiles and custom encodings」に対応すると書いていて、ソース側の encoding と合わせる必要があります。

color-relief(5.6.0)は標高そのものを色に変えます。color-relief-color は式で書き、入力に ["elevation"] を取ります。この型は新しいので、バージョンを先に確かめてください。他の型が 0.x 台から使えるのに対して、これだけ 5.6.0 です。

heatmap(0.41.0)は点の密度を色にします。heatmap-color の入力は ["heatmap-density"] です。

段彩と陰影を重ねるとこうなります。

{ "id": "relief", "type": "color-relief", "source": "dem",
  "paint": { "color-relief-color":
    ["interpolate", ["linear"], ["elevation"],
      0, "#e8f0e4", 500, "#dfe3c8", 1500, "#cbb89a", 3000, "#ffffff"],
    "color-relief-opacity": 0.6 } },
{ "id": "hill", "type": "hillshade", "source": "dem",
  "paint": { "hillshade-exaggeration": 0.4,
    "hillshade-shadow-color": "#5c6672" } }

値は定数でなくてよい

ここまでの例で、数字の代わりに配列を書いた箇所がありました。式です。どのプロパティが式を受け取れるのかも数えられます。

  • ズームで変えられるもの: 132個
  • 地物の属性で変えられるもの: 63個
  • 色の傾斜として書くもの: 3個(heatmap-colorcolor-relief-colorline-gradient
  • 定数しか受け取らないもの: 78個

ズームで変える書き方は2つです。interpolate が連続的に、step が段階的に値を変えます。道路の太さをズーム12で1px、16で6pxにするなら ["interpolate", ["linear"], ["zoom"], 12, 1, 16, 6]。文字サイズを段階で切り替えるなら ["step", ["zoom"], 11, 13, 13, 15, 16] で、ズーム13未満は11px、13以上15未満は13px、15以上は16pxになります。

属性で変えるなら ["get", "属性名"] を使います。前の記事で書いた等高線の例が、標高の値から線の太さを導く形でした。63個がこれを受け取れるので、地図の見た目の大半はタイルの属性から自動で決められることになります。

どこから触るか

判断の目安を4つ置きます。

最初の1枚は background と fill と line で作ります。合計32個しかなく、地図の形はこれで出ます。ラベルを最初に入れると、置き場所の調整で止まって前に進めません。

ラベルに入るときは、47個を一度に見ないことです。最初に触るのは text-fieldtext-fonttext-sizetext-colortext-halo-width の5つ。残りは、ラベルが消える・重なる・変な向きになる、という症状が出てから対応するものを引きます。

標高を使う表現はバージョン要件を先に見ますcolor-relief は 5.6.0、hillshade は 0.43.0 です。古い記事を読んで書き写すと、手元の MapLibre では動かないことがあります。定義ファイルの sdk-support が一次情報です。

書いたら検証にかけます。この記事のJSONはすべて次のコマンドを通してあります。プロパティ名の打ち間違いも、型の取り違えも、ここで止まります。

npx -p @maplibre/maplibre-gl-style-spec gl-style-validate style.json

146個という数は、最初に聞くと多く感じます。ただ内訳を見れば、面と線とラベルの初歩で32個と5個、あとは必要になったときに引けばよいものです。全部を覚える種類の仕様ではありません。

記事の一覧へ