maplibre-sprites-icons について
MapLibre Sprites and Iconsは、MapLibre GL JSで地図を開発する人向けのスキルです。sprite、ランタイム画像、Markerの選択と設定に加え、アイコンやルートシールドの表示不具合の調査方法を説明します。
主な機能
- 01Marker、sprite、addImageの比較
- 02単一または複数のspriteシート設定
- 03SVGからspriteアセットを作成
- 04ランタイム画像の登録
- 05アイコンとルートシールドの問題診断
活用シーン
- 01ポイントデータのシンボルレイヤー用アイコン方式の選択
- 02ベースマップスタイルへの独自アイコンシート追加
- 03シンボルレイヤーでアイコンが出ない問題の調査
- 04ルートシールドのバッジ欠落原因の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ maplibre/ maplibre-agent-skills --skill maplibre-sprites-iconsインストール後、Claude Code で/maplibre-sprites-iconsを入力して読み込まれたか確認します。
詳細
MapLibre のスプライトとアイコン
MapLibre GL JS のマップ上のアイコンは、スタイルの sprite URL から配信される スプライト(PNG アトラスと JSON インデックス)からシンボルレイヤーで描画されるマップの一部か、実行時に addImage で登録される画像のいずれかです。または、スタイルの外側で Marker としてマップ上に重ねて表示されます。アイコンの色、ハロー、画像に対する図と地のコントラストについては、maplibre-cartography を参照してください。テキストとグリフの設定については、maplibre-fonts-glyphs を参照してください。
このスキルを使用する場面
- マップ上のアイコンに
Marker、スプライト、addImageのどれを使うか選ぶ場合 - シンボルレイヤーのアイコンが描画されない場合
spriteを制御できないスタイルに独自のアイコンシートを追加する場合- SVG から
spriteを設定、セルフホスト、または生成する場合(glyphsについては、maplibre-fonts-glyphs を参照してください) - アプリが実行時に追加するレイヤー用に、SVG を含むアイコンを登録する場合
- ルートシールドが数字だけで表示されたり、バッジが表示されなかったりする場合
- シンボルレイヤーを読み取る、または生成し、シールドを認識する必要があるツールを作成する場合
Marker、スプライト、addImage の選択
次の順に、2 つの質問を確認します。以下のセクションもこの順序に従っています。
- アイコンはマップの一部ですか、それともマップ上に重ねて表示しますか? マップの一部である場合、シンボルレイヤーの
icon-imageとして配置します。ラベルと一緒に配置され、衝突すると非表示になり、レイヤーの順序に沿って描画されます。ポイントデータセットのサイズは問いません。マップ上に重ねる少数の注釈で、ユーザーがドラッグするものや、独自の HTML が必要なものには、Markerを使います。 - シンボルレイヤーの場合、画像はどこから取得しますか?
- スタイルから取得します。 POI、町のスポット、シールド、パターンテクスチャ、その他スタイルに組み込むアイコンやテクスチャで、視覚的階層と衝突判定に参加させるものには、スプライトを使います。スプライトを制御できないベースマップに独自のセットを追加する場合は、2 つ目のシートを使います。これもスプライトです。
- アプリから実行時に取得します。 GeoJSON ソースなど、アプリが追加するレイヤー用で、どのシートにも含まれていないアイコンには、
addImageを使います。意図して使用してください。スプライトは PNG 1 つと JSON 1 つで配信されるのに対し、各画像には個別のリクエストとaddImageの呼び出しが必要です。また、そのアイコンはスプライトアイコンと同様に、スタイルのラベルや他のアイコンと衝突します。
スプライト
スタイルの sprite 値は、拡張子を含まないベース URLです。MapLibre が .json、.png、および @2x の各バリエーションを自動的に追加します。[1] シンボルレイヤーでは、スプライト画像の名前を icon-image で指定します(その他のレイヤーでは *-pattern プロパティを使います)。名前はスプライトの JSON インデックス内の ID と完全に一致している必要があり、一致しない場合、アイコンは描画されません。
{
"sprite": "https://example.com/sprites/poi",
"layers": [
{
"id": "cafes",
"type": "symbol",
"source": "places",
"source-layer": "poi",
"layout": { "icon-image": "cafe" }
}
]
}
テストには https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite を使用できます。本番環境では使用しないでください。
同じスタイルで複数のスプライトシートを使う
sprite は文字列ひとつに限りません。MapLibre GL JS 3.0 以降(#1805)では、{id, url} オブジェクトの配列も受け付けるため[1]、スタイルはベースマッププロバイダーのシートと並べて独自のアイコンを読み込めます。3.0 より前のスタイルで指定できるスプライトはひとつだけだったため、独自のアイコンを追加するにはプロバイダーのシートに統合する必要がありました。配列形式を使えば、各シートはホストされている場所にそのまま置いておけます。
{
"sprite": [
{ "id": "default", "url": "https://tiles.openfreemap.org/sprites/ofm_f384/ofm" },
{ "id": "my-icons", "url": "https://example.com/sprites/poi" }
]
}
この形式には、次のルールがあります。
- 画像は
id:image-nameで参照します —"icon-image": "my-icons:cafe"。プレフィックスのない"cafe"はdefaultシートに対して解決されます。 - 例外は
defaultというidです。このシートの画像にはプレフィックスが不要です。そのため、既存のスタイルの文字列形式のspriteを配列形式に変換しても、icon-imageの値はそのまま機能します。ベースマップのシートにid: "default"を指定すれば、スタイルに既にあるシールドレイヤーや POI レイヤーも、変更せずに解決されます。 - **すべての ID と URL は一意でなければなりません。**重複は検証エラーになります(
all the sprites' ids must be unique, but <id> is duplicated)。後勝ちで統合されるわけではありません。 - 各 URL に拡張子は付けません — 文字列形式の場合と同様に、MapLibre が各エントリーに
.json、.pngおよび@2x版を追加します。 - コードからは、スタイルの読み込み後に
map.addSprite('my-icons', 'https://example.com/sprites/poi')でシートを追加できます。文字列形式のspriteは、配列のdefaultエントリーになります。[3] - ❌
{ "id": "my-icons", "url": "https://example.com/sprites/poi.json" }→poi.json.jsonをリクエストします - ❌ 独自のツールで
spriteの型をstringにすること:配列の値は文字列化されると[object Object]になり、ローダーはInvalid sprite URL "[object Object]", must be absoluteというエラーを返して拒否します。スタイルのspriteを読み取る箇所では、どちらの形式にも対応してください。
スプライトのセルフホスト
サードパーティへの依存を避けるには、スタイルまたはタイルセットプロバイダーが提供する既存のスプライトディレクトリ(PNG と JSON、およびすべての @2x ファイル)をコピーして独自のドメインでホストし、スタイルの sprite プロパティにベース URL を指定します。再公開する前に必ずプロバイダーのライセンスを確認し、必要な場合は帰属表示を追加してください。
スプライトアセットは、自分で管理する静的ホスト(スタイルと同じオリジン)でホストしてください。**本番環境のスタイルで raw.githubusercontent.com を指定しないでください。**Raw はリポジトリのファイルを配信するためのもので、本番アセット向けではありません。認証されていないリクエストは厳しくレート制限されるため、実際のユーザーには断続的に HTTP 429 が発生します [5]。キャッシュ期間は 5 分に固定され、制御できません。SLA もありません。また、プライベートリポジトリの URL は、認証済みの共同作業者以外にはすべて 404 を返します(ログイン中は動作しても、ほかのすべてのユーザーでは失敗します)[6]。
SVG からスプライトを構築する
spritezero、spreet、Martin などのツールを使って、SVG のディレクトリからスプライトアセットを生成できます。
アイコンの入手先としては、Maki や Temaki が便利です。これらは地図スタイル用 SVG アイコンの一般的なソースリポジトリですが、派生スプライトアセットを再公開する前に、各リポジトリのライセンスを確認してください。
addImageを使用した実行時の画像
アプリが実行時に追加するレイヤーの場合、スタイルの読み込み後に各画像をIDで一度登録し、そのIDをレイヤーのicon-imageに指定します。先にhasImage()でIDを確認してください。スタイルにすでに存在するIDに対してaddImageを呼び出すと、置き換えられずにエラー(An image named "<id>" already exists.)が発生します。[3] [8]
map.on('load', async () => {
// PNG, WebP, or JPEG: loadImage() resolves to a response whose data is the image
const { data } = await map.loadImage('https://example.com/icons/cafe.png');
if (!map.hasImage('cafe')) map.addImage('cafe', data);
// SVG: an Image element, rasterized at its width and height
const park = new Image(24, 24);
park.crossOrigin = 'anonymous'; // or addImage throws on a cross-origin SVG
await new Promise((resolve, reject) => {
park.onload = resolve;
park.onerror = reject;
park.src = 'https://example.com/icons/park.svg';
});
if (!map.hasImage('park')) map.addImage('park', park);
map.addSource('venues', { type: 'geojson', data: 'https://example.com/venues.geojson' });
map.addLayer({
id: 'venues',
type: 'symbol',
source: 'venues',
layout: { 'icon-image': ['get', 'category'] } // 'cafe' or 'park'
});
});
- ❌
map.loadImage(url, callback)。GL JS 4.0.0ではコールバック形式が削除されたため[4]、コールバックは実行されません。プロミスをawaitしてください。[3] - ❌
map.loadImage()経由でのSVG。対応形式はPNG、WebP、JPEGです。[3] SVGをオンデマンドで読み込むには、GL JS 6のsetMissingStyleImageResolverで、MapLibreのDisplay a remote SVG symbolの例と同様に、同じImageの方法を使用します。 - ❌ 通常のSVGでの
sdf: true。sdfを指定した画像では、アルファ値が距離フィールドとして読み取られ、不透明なピクセルはすべてicon-color(デフォルトは黒)で描画されるため、マルチカラーのアイコンは単色のシルエットになります。sdfを指定しない場合、icon-colorとicon-halo-*は何も行いません。これらはSDFアイコンでのみ機能します。[2] - シンボルレイヤーでは、デフォルトで衝突するアイコンが非表示になります(
icon-allow-overlapはfalseで、設定されている場合はicon-overlapがこれを上書きします)。どのアイコンを表示するかはsymbol-sort-keyで決まります。重なりを有効にすると(icon-allow-overlap: trueまたはicon-overlap: "always")、衝突を確認せずにすべてのアイコンが描画されます。GL JSの大規模データのガイドでは、フィーチャー数が多い場合にこの方法が提案されています。その場合、アイコンは積み重なって表示されます。 - ❌ 衝突するアイコンを非表示にするための
"icon-optional": true。これはアイコンとテキストの両方があるシンボルにのみ適用され、アイコンが衝突してテキストが衝突しない場合に、アイコンなしでテキストを表示します。text-fieldのないレイヤーでは何も変わりません。
addImageのオプションでは、スプライトインデックスと同じ画像ごとのメタデータを指定します。pixelRatio、sdf、stretchX、stretchY、content、textFitWidth、textFitHeightです。[3]
マーカー
Marker[7]は、スタイルの外側にある、マップキャンバス上のHTML要素です。すべてのレイヤー(ラベルを含む)の上に描画され、衝突判定には含まれません。
new maplibregl.Marker({ draggable: true })
.setLngLat([-122.42, 37.77])
.setPopup(new maplibregl.Popup().setText('Pickup point'))
.addTo(map);
Markerは、カメラが移動するたびに位置が更新される単一のDOM要素です。 マップ上に少数の注釈を重ねる用途には適していますが、ポイントデータセットの場合は、ポイントをGeoJSONソースに入れてシンボルレイヤーとして描画してください。
アイコンが描画されない場合
GL JSは見つからないアイコンをすべて報告します。マップでstyleimagemissingイベントが発生し、コンソールには画像IDごとに一度、Image "<id>" could not be loadedという警告が表示されます。[8] 読み込みに失敗したsprite URLもマップのerrorイベントを発生させます。このイベントをリッスンするものがない場合は、console.errorに出力されます。[8]
loadイベントの後にmap.hasImage(id)を使うと、2つの原因を見分けられます。[3]
- スプライト自体が読み込まれていない場合:スプライトJSONに含まれるIDに対して
hasImage()がfalseになります。スタイルでspriteが設定されていること、URLに拡張子が含まれていないこと、その.jsonと.pngが200を返すこと、そしてCORSを確認してください。 - スプライトは読み込まれたものの、そのIDが含まれていない場合:ほかのスプライトIDに対しては
trueが返ります。icon-imageの値をスプライトJSONのキーと比較してください。デフォルト以外のスプライトの場合は、id:プレフィックスも含めます。map.listImages()を使うと、マップ内のすべてのIDを確認できます。
壊れたルートシールド
壊れているように見えるルートシールド(数字だけが浮いている、バッジが表示されない)の原因は、ほとんどの場合スプライト画像の欠落です。シールドの番号はテキスト(フォント)なので、通常は正しく描画されます。背後のバッジはスプライトの icon-image です。次の順に診断してください。
- グリフが読み込まれていることを確認します。
glyphsサーバーに対してtext-fontの正確な名前を指定して確認し、HTTP 200 が返ることを期待します。200 が返る場合、原因はスプライト画像の欠落、または参照の破損だと推測できます。 - スプライトにシールド画像が含まれていることを確認します。 OSM Bright のシールドレイヤーは、米国のネットワークでは
icon-image: "{network}_{ref_length}"(例:us-interstate_2、us-highway_3、us-state_2)、その他の道路番号ではroad_{ref_length}を使用します。OSM Liberty ではdefault_{ref_length}を使用します。アイコンがなければ省略されるため、スプライトの JSON でこれらのキーを grep してください。
現在、https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite と https://openmaptiles.github.io/osm-bright-gl-style/sprite のスプライトシートには、road_1~_6、us-state_1~_6、us-highway_1~_3、us-interstate_1~_3(openmaptiles.github.io では _1~_5)が含まれています。ただし、最小構成のスプライトやカスタムスプライトには、汎用の road_* しか含まれていない場合があります。シールド画像がなく、タイルに network、ref、ref_length が設定されている場合(OSM US OpenMapTiles のタイルには設定されています)、それらを含むスプライトを sprite に指定してください。すると、{network}_{ref_length} を使うスタイルレイヤーは、レイヤーを編集しなくても解決されます。
シールドとは、ルート番号の背後に表示されるバッジ画像で、サイズの決め方は2種類あります。 OSM Bright と OSM Liberty は、OpenMapTiles の ref_length 属性で適切なものを選び、ルート番号の長さごとにあらかじめ描画されたバッジを使用します。[9] OSM Bright の州間高速道路シールドレイヤーの要約:
{
"id": "highway-shield-us-interstate",
"layout": {
"icon-image": "{network}_{ref_length}",
"symbol-placement": {
"base": 1,
"stops": [
[7, "point"],
[7, "line"],
[8, "line"]
]
},
"text-field": "{ref}"
}
}
すべてのルート番号の長さで同じバッジ画像を使うには、伸縮可能にします。バッジのスプライト JSON エントリでは、stretchX と stretchY に伸縮可能なピクセル範囲を指定し、content にテキストを収める領域を指定します。シールドレイヤーでは、icon-text-fit を "both" または "width" に設定し、テキストの周囲の余白として icon-text-fit-padding も設定します。[1] [2] これにより、I-5 と I-405 は幅の異なる同じ画像を共有できます。OSM Bright と OSM Liberty は伸縮可能なバッジを使用していないため、シールドレイヤーを認識するツールは両方の方式に対応する必要があります。伸縮可能なバッジだけが icon-text-fit を設定します(デフォルトは none)。また、text-anchor のデフォルトは center なので、これを未設定のままにしても、レイヤーの種類を判断する手がかりにはなりません。
スタイルからレイアウト値を読み取るコードでは、それらが文字列であると決めつけてはいけません。OSM Bright の3つのシールドレイヤーと OSM Liberty の1つのシールドレイヤーでは、symbol-placement にレガシーなズーム関数が設定されています。これは stops を持つオブジェクトで、point と line を切り替えます。シンボルのレイアウト値はどれも式にすることができ、その場合は配列になります。layer.layout['symbol-placement'] === 'point' のようなチェックはこれらのレイヤーでは false となるため、コードはレイヤーをスキップします。値を比較する前に、それが文字列、ズーム関数、式のどれであるかを確認してください。
関連スキル
- maplibre-cartography — 航空画像上のポイントシンボルにおけるアイコンの色、
icon-halo-color/icon-halo-width、図と地のコントラスト、およびスタイルレイヤーの順序。 - maplibre-fonts-glyphs — シールドのテキスト部分に関する
glyphsURL、フォント PBF、GL JS のローカルフォントへのフォールバック。 - maplibre-source-wiring — スタイルのほかのルートプロパティの中での
spriteの位置、およびセルフホストアセットの CORS。 - maplibre-v6-migration — v6 では
styleimagemissingは通知専用です。スプライトシートにないicon-imageを v6 のマップに提供する方法はsetMissingStyleImageResolverです。
参考資料
- スタイル仕様:
sprite— 文字列形式と{id, url}配列形式、画像名のプレフィックス付与、defaultID、スプライトインデックスファイルのcontent、stretchX/stretchY、textFitWidth/textFitHeightフィールド - スタイル仕様:シンボルレイヤーのプロパティ —
icon-text-fitの値(デフォルトのnone、width、height、both)とicon-text-fit-padding。icon-colorとicon-halo-*はSDF専用です Map.addImage()(MapLibre GL JS API) — 同じページにあるhasImage()、listImages()、addSprite()、loadImage()も参照してください。loadImageはPNG、WebP、またはJPEGを受け取り、dataが画像であるレスポンスに解決されます- MapLibre GL JS 変更履歴 — 「1つのスタイルファイルで複数の
sprite宣言をサポート」が3.0.0でリリースされました。map.loadImageは4.0.0でPromiseを返すようになり、コールバックが削除されました(#3233、#3422) raw.githubusercontent.comの未認証リクエストのレート制限(GitHub Community Discussion) — 匿名リクエストにはレート制限があり、本番環境のトラフィックではHTTP 429が断続的に発生しますraw.githubusercontent.comとプライベートリポジトリ(GitHub Community Discussion) — プライベートリポジトリのraw URLは、匿名リクエストに対して404/403を返しますMarker(MapLibre GL JS API) —draggableオプション、setLngLat、setPopup- GL JS 6.11.2の
image_manager.ts—styleimagemissingと画像欠落時の警告。重複IDエラーはstyle.tsにあり、リスナーのないerrorイベントはevented.tsでconsole.errorに出力されます - OpenMapTilesの
transportation_nameレイヤー —ref_length:「高速道路のラベルの背景としてシールドアイコンを使用するのに便利です。」
このスキルはスナップショットです。 一次情報源(上記の参考資料、MapLibreの最新ドキュメント、またはMapLibreを実行したときの動作)がこの内容と矛盾する場合は、一次情報源を優先してください。それに従ったうえで、参照元とMapLibreのバージョンを明記して相違点を報告してください。インストール済みのコピーを編集しても、ほかの人の役には立たず、次回の更新で上書きされます。