Geographic Interleaved Binary Format
interleave(Morton のビット交互配置)·
integer(64 bit の整数の頂点符号)·
intended(詳細度を最初から設計に組み込む)
データを丸ごと GPU に — どのズームでも描け、問いに答えられる
ortho-earth のエンジンには二種類のベクタの層があります。基図はベクタタイル(MVT)から描きます。サーバーが世界をズームごとに 4z 枚に切り、 ズームのたびに新しい組を取り、タイルごとに一度 CPU で三角形に割ります。背景の絵にはそれが正しいやり方です。 けれど問いかけたいデータには向きません — 地物はタイルの縁で切られ、ズームごとに違う簡略化を受け、三角形としてしか残らないからです。
Gint の層は、GeoPBF のデータの幾何を丸ごと、一度だけ GPU に置きます。
| ベクタタイル | Gint | |
|---|---|---|
| ファイル | ズームごと・タイルごとに 1 枚 | 1 ファイル |
| ズームした時の通信 | 新しいタイル | +0 — 詳細度は GPU が選ぶ |
| 面の塗り | タイルごとに CPU で三角形化 | 巻き数 — 三角形化しない |
| 地物 | タイルの縁で切れる | 丸ごと・共有する境界がぴたり一致 |
| 塗り替え | タイルごとにスタイルをかけ直す | テクスチャ 1 枚の更新 — 地物ごとに違う色にできる |
| 問い | — | 選択・識別・強調・表との突き合わせ |
読み込む・足す・塗る・絞り込む・問う、の五段です。言葉は MapLibre GL JS から借りています — paint のプロパティ、式、setPaint、setFilter —
ので、知っていることがそのまま使えます。MapLibre の写しではありません。最初の一歩を短くするために語彙を借り、下で動いているのは Gint の層です。
import { createGlobe, geopbf } from "@ortho-earth/globe";
const map = await createGlobe({ target: "#map" });
// 1. 読み込む — geopbf が読める形式なら何でも。{ gint: true } で GPU 用の形も作る(IndexedDB に残る)
const pbf = await geopbf("https://example.com/municipalities.geojson", { gint: true });
// 2. 足す — 上に層が一枚増える。何も置き換えない
const layer = map.addGint(pbf, { tip: true });
await layer.ready; // true=GPU に載った
// 3. 塗る — MapLibre の paint と式。地物ごとに一度だけ評価
await layer.setPaint({
"fill-color": ["interpolate", ["linear"], ["get", "pop"], 0, "#fff5eb", 1000000, "#7f2704"],
"fill-opacity": 0.85,
"line-color": "#ffffff",
"line-width": 0.5,
});
// 4. 絞り込む — 地物を隠す。作り直しはしない
await layer.setFilter([">=", ["get", "pop"], 10000]);
// 5. 問う
layer.on("click", e => console.log(e.fid, e.properties, e.lngLat));
const hit = layer.query([139.767, 35.681]); // { fid, properties } か null
const hits = map.queryAll([139.767, 35.681]); // [{ layer, fid, feature }]・上の層から
geopbf(input, { gint: true }) は URL・File・ArrayBuffer・GeoJSON のオブジェクトを受け、
GeoPBF が読める形式なら何でも読みます — GeoJSON・Shapefile・GeoPackage・FlatGeobuf・GeoParquet・KML・GPX など。
gint: true は GPU が描く形も作ります。トポロジの符号化器(WebAssembly・worker で動く)が、共有する境界をすべて一本の共有する弧にします。
結果は IndexedDB に残るので、次の訪問では符号化を飛ばします。geopbf は @ortho-earth/globe からも export されているので、別のパッケージは要りません。
properties がまったく同じ地物は、一つの地物 — fid が一つのマルチパートの地物 — になります。色も強調も答えも一緒です。
分けておきたいなら、id のような地物ごとに違う属性を持たせてください。
map.addGint(pbf, options) は層を足し、その手綱(ハンドル)をすぐ返します。焼き — 辺の表、詳細度の梯子、地物ごとの箱 — は worker で走るので、
地図は描き続けます。await layer.ready は GPU に載ると true(失敗なら false)です。addGint はカメラを動かしません。
塗る前の層は既定のスタイルで描きます。面の輪郭はオレンジ(#FF6B35)、線はシアン(#00B4D8)。
面が数画素より小さくなると、線の網の代わりにベタ塗りで見せ、寄ると戻ります。
層は足した順に重なります。order(小さいほど下)を渡せば、呼んだ順に関係なく重ね順が決まります。
層は何枚でも同時に載せられ、両方の土台(WebGPU と WebGL2)で動きます。
setPaint(paint, filter?) は MapLibre の paint プロパティを受けます。値は定数か式(§4)です。
式は JavaScript で地物ごとに一度だけ評価され、結果はスタイル表 — 地物 1 つにテクセル 1 つ — に書かれます。
GPU は描きながらその表を読むだけで、フレームごとに式を走らせることはありません。塗り替えは、地物を一巡する評価とテクスチャ 1 枚の更新です。
幾何は作り直しません。日本の 1,919 市区町村なら表は 31 KB です。
| プロパティ | 対象 | 値 |
|---|---|---|
fill-color | 面 | 色 |
fill-opacity | 面 | 0〜1・色の α に掛かる |
line-color | 線と面の輪郭 | 色 |
line-width | 線と面の輪郭 | CSS 画素・1/8 刻み・最大 31.875 · 0=線を描かない |
circle-color | 点 | 色 |
circle-radius | 点 | CSS 画素・1/4 刻み・最大 63.75 · 0=点を描かない |
line-opacity | 線・輪郭・点 | 0〜1・線または点の色の α に掛かる |
どのプロパティも地物ごとに変えられます(data-driven)。色は CSS の色です:#rgb・#rgba・#rrggbb・#rrggbbaa、
カンマ区切りの rgb()/rgba()、hsl()/hsla()、CSS の色名。
線と点は地物ごとの色の枠を 1 つ共有するので、line-opacity は点も薄くします。
fill-color だけです。書かなければ、どのズームでも輪郭だけになります。setPaint は paint を丸ごと置き換えます。欲しいプロパティは毎回すべて渡してください。setPaint(null) で既定のスタイルに戻ります。
プロパティを一つずつ変えたいときは MapLibre 形の入口(§6.5)を使います。欠けた値が何を意味するかを決めて、式に書いてください。そのプロパティを持たない地物は、interpolate では塗られず、
step では一番下の色に、match では fallback になります。入力を ["coalesce", ["get", "pop"], …] で包むか、
["has", "pop"] で確かめるか、その地物を絞り込みで外します。
setFilter(式) は、式が偽になる地物をすべて隠します。setFilter(null) で全部を戻します。
paint と同じく一度だけ評価され、同じ表に書かれます — 作り直しはしません。
隠した地物は描かれず、塗りにも加わらないので、下にあるものの塗りを乱しません(§5)。
ホバーと問いからも外れます。隠した地物が無いものとして、ポインタの下の次の地物が答えになります。
絞り込みは paint と同じ表に住みます。最初の setPaint より前に渡した絞り込みは預かられ、setPaint と一緒に効きます。
setPaint(paint, filter) なら一度に両方を設定できます。
カーソルを持つ層は一度に一枚です。ホバー・tip・強調・layer.on("click") はその層のものです。
interactive な層を足すとその層がカーソルを持ち、layer.activate() で移せます。interactive: false で足した層はカーソルを取りません —
それまでカーソルを持っていた層が持ったままです。
ホバーは連続(32 ms ごとの選択)なので一枚に絞り、選択は一回で済ませます。クリックは稀なので、全部の層に問えます。
線だけの層(面も点も無い)は、まだホバーに答えません。map.on("click") か query で問ってください。
layer.on("hover", f => …); // { fid, properties } か null
layer.on("mouseenter", f => …); // 地物に入った:{ fid, properties }
layer.on("mouseleave", e => …); // 出た:{ fid }
layer.on("click", e => …); // { fid, properties, lngLat: [lng, lat] }
map.on("click", ({ lngLat, hits }) => { // 点の下のすべての Gint の層・上から
for (const { layer, fid, feature } of hits) …
});
fid は地物の、その層の中での番号です — 二つの層のどちらにも 7 番がある — ので、必ず層と組にして持ってください。
layer.query(lngLat) と map.queryAll(lngLat) は、GPU を往復せず JavaScript ですぐに答えます。
50 m 以内の点、次に 30 m 以内の線、次にその位置を含むいちばん小さい面を探します — 市の中の区をクリックすれば区が返ります。
絞り込みで隠した地物は飛ばします。
map.queryAll と map.on("click") は、いま見えている物に答えます。setVisible(false) で消した層や、ズームの範囲の外の層は入りません。
interactive: false で足した層は入ります — クリック一つで「この筆は危険区域の中でもある」と分かります。
地球儀が自分で描く層(国の輪郭など)は出てきません。layer.query は、その層にわざわざ問う口です。層を消していても、ズームの範囲の外でも答えます。label: { field: ["get", "name"], size: 13, color: "#fff", halo: "#000" }、または後から setLabel(…)。
基図の注記と同じ衝突とフェードを共有し、絞り込みに従います。field は式(または properties の関数)です。ただの文字列は、そのまますべての地物に表示されます。setFeatureState(fid, { selected: true }) を、paint の ["feature-state", "selected"] で読みます。
同じ処理の中の何回もの呼び出しは、1 フレームにまとめて描きます。["zoom"] は設定した時に読みます。カメラが 0.25 段以上動いて止まると、層が自分で評価し直します。
変わるのは止まった時で、ズームの最中ではありません。setData(pbf) でデータを差し替えます。手綱・イベント・paint・filter はそのままです。setOrder(n)・setVisible(bool)・remove() — §6。
値は境界のファイルではなく、表 — 国勢調査の CSV、表計算 — にあることがよくあります。データを書き換える必要はありません。
表から match を組みます。ラベルが二千あっても構いません。式はフレームごとではなく、一度しか走らないからです。
// 自分の表:市区町村コード → 値(例:国勢調査の人口)
const values = { "13101": 66680, "13102": 169179, /* … */ };
const breaks = [10000, 50000, 100000, 300000, 1000000];
const colors = ["#f7fbff", "#c6dbef", "#6baed6", "#3182bd", "#08519c", "#08306b"];
const classOf = v => breaks.filter(b => v >= b).length;
// "N03_007"=国土数値情報「行政区域」のコードの属性名。自分のデータの属性名に
const fill = ["match", ["to-string", ["get", "N03_007"]]];
for (const [code, v] of Object.entries(values)) fill.push(code, colors[classOf(v)]);
fill.push("rgba(0,0,0,0)"); // 表に無い:塗らない
await layer.setPaint({ "fill-color": fill, "fill-opacity": 0.85, "line-color": "#ffffff", "line-width": 0.3 });
match は === で比べます。文字の "13101" と数の 13101 は別物なので、入力を to-string に通しています。
色の数は描く手間を変えません。地物ごとに違う色でも構いません — 塗りは 6 色でも 1,919 色でも 2 パスです。
法務省の登記所備付地図(geopbf は XML をそのまま読めます)は、筆ごとに 地番 を持ちます。
札幌市中央区 — 57,341 筆 — では、同じ欄に言葉も入っています:道・河川・無地番・筆界未定地。式一本で、公共用地と筆界未定地が地図に出ます。
await parcels.setPaint({
"fill-color": ["match", ["get", "地番"],
["道", "河川", "無地番"], "#e41a1c88", // 道・河川・無地番
"筆界未定地", "#ff7f00aa", // 筆界未定地
"rgba(0,0,0,0)"], // ほかの筆:塗らない
"line-color": "#ffffff66",
"line-width": 0.5,
});
match のラベルは配列にもできます。どれか一つに当たれば、その出力になります。
筆の下に災害の層を敷きます。カーソルは筆が持ち、クリックは両方に届きます。
// 土砂災害警戒区域(国土数値情報 A33)。A33_002 = 2:特別警戒区域
const zones = map.addGint(await geopbf(zonesUrl, { gint: true }), { order: -1, interactive: false });
await zones.setPaint({
"fill-color": ["match", ["to-number", ["get", "A33_002"]], 2, "#c0392b80", "#d9a4416b"],
"line-width": 0, // 塗りだけ・輪郭なし
}); // interactive: false — カーソルは筆のまま
map.on("click", ({ hits }) => {
const parcel = hits.find(h => h.layer === parcels);
const zone = hits.find(h => h.layer === zones);
if (parcel && zone) console.log(parcel.feature.properties["地番"], "は警戒区域の中");
});
層の表示の切り替えは setVisible です — 焼き直しはありません。「この筆はどの区域にあるか」「この点の下に何があるか」という問いこそ、
Gint がある理由です。層は絵であるだけでなく、データです。
式は MapLibre のものです。次の範囲を Gint の層に約束します。型定義(globe.d.ts)に並べた範囲で、検定で確かめています。
評価器は、MapLibre のスタイルファイルを読む基図と共用なので、これより多くの演算子を受け付けますが、それらは Gint の層には約束しません。
| 種類 | 演算子 |
|---|---|
| データ | get has feature-state geometry-type zoom literal |
| 選ぶ | match case step coalesce |
| 補間 | interpolate — ["linear"] と ["exponential", base]・数と色 |
| 比較と論理 | == != > >= < <= ! all any in |
| 算術 | + - * / % ^ min max |
| 型と文字 | to-number to-string concat |
| 変数 | let var |
setPaint・setFilter を呼んだ時(と地物の状態が変わった時)に走ります。GPU は結果を読むだけです。["zoom"] は写し。評価した時のズームです。カメラが止まると層が評価し直します(§2.6)。フレームの間の補間はありません。==・!=・match は === で比べます。型が違うときは to-string や to-number で揃えてください。in は ["in", 探す値, 対象]。対象は文字列か配列(["literal", […]])です。match の枝の中の interpolate も効きます。実際のデータは重なります。市区町村の上の都道府県の面、二重に登録された区域、重ねて描かれた筆。 Gint は非ゼロ巻き数の規則で塗り — 偶奇の規則は使いません — ので、重なりの結果は予測できます。
fid の大きい方 — が勝ちます。
その地物が透明か絞り込みで隠れていれば、下のものが見えます。二つの重なりまでは厳密で、三つ以上が積み重なる所の結果は保証しません。巻き数の和は色以上のものを運びます。読み方を変えると、別々の地物が重なる所、一つの地物が二重に登録された所、環が逆向きに回っている所が分かります。 開発の場では、エンジンはちょうどその画素だけを塗ります。登記所備付地図に当てると、誰にも見えなかったデータの品質の問いが、一枚の絵になります。 この見せ方は今は開発用の道具で、API には含めていません。
制約が一つあります。地物ごとの塗りは、地物番号を浮動小数の的に足し込みます。32 bit の浮動小数を混ぜられない GPU では番号が 16 bit になり、 一枚の層で一つずつ色を付けられる地物は 2,047 までです。それより大きい層は地物ごとの塗りを失い、層の素のスタイルで描かれます。 この制約に当たったら、同じスタイルの地物をマルチポリゴンにまとめてください — 塗りの手間は同じです。
Gint の層を一番上に足し、手綱をすぐ返します。pbf が GPU 用の形を持たなければ null。interactive が false でなければ、その層がアクティブになります(§2.5)。
pbf(GeoPBF):geopbf(input, { gint: true }) の結果。options.order(Number):重ね順。小さいほど下。既定=足した順。options.minZoom / options.maxZoom(Number):この層を描くズームの範囲。既定=minZoom はデータの広さから(狭い範囲ほど高いズームから)、maxZoom は座標の精度から。options.interactive(Boolean):false=カーソルを取らない(持っている層が持ったまま)。map.on("click") と queryAll には答える。既定 true。options.tip(Boolean | Function):true=全属性を「キー: 値」で出す tip。関数 properties → string[] なら自分の行を書く。既定=tip なし。options.label(Object):{ field, size, color, halo, haloW, minZoom, maxZoom } — setLabel を参照。options.style(Object):paint なしの見た目 — { fillColor: [r, g, b, a], lineWidth, ptRadius }・色は 0〜1。options.fillMaxEdges(Number):辺がこの数を超えると塗りを止める(既定 2,000,000)。0=輪郭だけ。options.lowFill(Boolean):塗りが止まっている時(fillMaxEdges 超え)も、低いズームの一色の塗りだけは描く — 非常に大きな層向け。GPU に載ると true、焼きが失敗すると false。
paint(と filter)を地物ごとに評価し、表を 1 枚 GPU へ送ります。paint は丸ごと置き換え。null=既定のスタイル。filter を省くと今の絞り込みのまま。
paint(Object):fill-color・fill-opacity・line-color・line-opacity・line-width・circle-color・circle-radius(§2.3)。filter(式 | null):真偽の式(§4)。絞り込みだけ。最初の setPaint より前なら預かられ、その時に効きます。null=全部を見せる。
地物ごとの一時的な状態。["feature-state", key] で読みます。state は混ぜ合わせ、null で消えます。fid を省いた removeFeatureState() は全部を消します。
field(式 | Function | String):文字 — ["get", "name"] や ["concat", …] のような式、関数 properties → string、または固定の文字列。size・color・halo・haloW:文字の大きさ(px)・色・縁取りの色と幅。minZoom・maxZoom:ラベルを出すズームの範囲(既定=層の範囲)。錨:面と線は地物の箱の中心、点はその点。隠した地物にはラベルが付きません。
paint なしの見た目を変えます(options.style と同じオブジェクト)。
データを差し替えます。手綱・イベント・paint・filter はそのままで、paint は新しい地物で評価し直します。新しいデータが GPU に載ると true。
重ね順を変える(小さいほど下)· 見せる/隠す(焼き直しなし)· 層を取り除く。アクティブな層を取り除くと、activate() を呼ぶか interactive な層を足すまで、どの層もカーソルを持ちません。
アクティブな層だけ。線だけの層は、まだホバーに答えません。
"hover":{ fid, properties }。地物から外れると null。"mouseenter" / "mouseleave":地物の縁で — { fid, properties } / { fid }。"click":{ fid, properties, lngLat }。lngLat=ポインタの [lng, lat]。カーソル(ホバー・tip・強調・layer.on("click"))をこの層に渡します。
同期・JavaScript。50 m 以内の点、なければ 30 m 以内の線、なければその位置を含むいちばん小さい面。絞り込みで隠した地物は飛ばします。interactive かどうかに関わらず、層を消していても、ズームの範囲の外でも答えます。
いま見えている — 表示中で、ズームの範囲の中の — すべての Gint の層に layer.query を、上の層から。feature は { fid, properties }。地球儀が自分で描く層(国の輪郭など)は含みません。
地球の上のクリックのたびに、hits=map.queryAll(lngLat) で呼ばれます。ほかに map.on("move" | "settle" | "load", …) と map.off(type, callback)。"load" は地図が描き終わっていればすぐ呼ばれます。
map.addSource(id, { type: "geojson", data }) と map.addLayer({ type: "fill" | "line" | "circle", … }) は、下に Gint の層を作ります。
MapLibre のコードがそのまま動き、setPaintProperty・setLayoutProperty(visibility)・setFilter・moveLayer・
setFeatureState({ source, id }, state)・queryRenderedFeatures・map.on("click", layerId, …) がすべて効きます。
一つの source の層は一枚の Gint の層になります:paint は混ぜ合わされ、filter は all でつながります。
ズームの範囲は MapLibre に従い、minzoom の無い層はズーム 0 から描きます。
以前からの一枚きりの口(applyGintData・paint・paintTable・onGintClick)は、既存のアプリのために残しています。新しいコードは addGint を使ってください。
符号化器(Rust を WebAssembly にしたもの・worker で動く)は、すべての環と線を分岐点で弧に切り、弧を一本ずつ一度だけ持ちます。
面は弧の参照の並びです — [fid][環の数][弧の数][±弧番号…](逆向きの弧は ~番号)。
隣り合う二つの市町村は境界の弧を共有するので、両方に同じ簡略化がかかります。どのズームでも、すき間も重なりも出ません。
線と隣接の一覧は別の流れに入ります。
経度・緯度を整数にし — ix = (lon + 180) × 107、iy = (lat + 90) × 107(約 1 cm)—
二つのビットを交互に並べて(Morton 順)一つの 64 bit の符号にします。符号は頂点の詳細度も運びます。
長い弧には大円に沿って 1° ごとに L1 の頂点(錨)を足します。長い辺が簡略化で弦になり、地球の丸みを横切ってしまうことはありません。
途中の頂点は Visvalingam–Whyatt の実効面積(両隣と作る三角形の面積)で重みを付けます。先に消えた頂点より上に来ないよう累積最大で持ち、緯度の cos で補正します。 その面積を 6 bit の順位にします。
同じ式に「画面の 1 画素が覆う地面の面積」を入れると、今のズームに要る順位が出ます(§9)。順位とズームは同じ単位で話します。
結果は一つのバッファです。64 byte のヘッダ(magic "Gint"・版・件数・bbox・流れの長さ)の後に、弧の頂点符号・点の符号・
弧ごとのメタ [開始, 長さ, 重み, –, bbox]、そして面・線・隣接の流れが続きます。
IndexedDB に保存するので、二度目の訪問では符号化を丸ごと飛ばします。
焼きの段で、弧は整数テクスチャの中の辺(頂点 A → 頂点 B)になります。毎フレーム:
rank = floor(1.5·log2(画素の面積) + 61.524)。ズームしても何もアップロードしません。変わるのは uniform 一つです。
全国規模のデータは数百万の辺を持ちます。頂点シェーダがそれを全部触らずに済むよう、二つの仕組みがあります。
面は三角形に割りません。面の辺を、その地物の bbox の中心を要にした扇としてステンシルに描きます。 表向きの三角形は +1、裏向きは −1(非ゼロ巻き数)。二つ目のパスで、数がゼロでない画素を塗ります。 凹んだ形も穴も特別扱い無しに正しく出ます。扇どうしの重なりは打ち消し合います。
fillMaxEdges)、線だけ描きます。辺は一本ずつ小さな四角(6 頂点)として描き、カプセルの符号付き距離関数で塗ります。どの太さでも、継ぎ目と端は余分な幾何無しに丸くなります。 色・太さ・点の半径・フラグは GPU 上の地物ごとのスタイル表から、共通のスタイルは 256 枠のスタイル表と破線表から引きます — 地図の塗り替えはテクスチャの更新であって、作り直しではありません。
copyTextureToBuffer + mapAsync)。地理データをどう表すかは、どの詳細度を「現実」とみなすかの選択でもあります。 Natural Earth の自然/文化の区別が地球上の現象を人の目から整理するように、Gint の詳細度の設計は「見えるものは縮尺で変わる」という考えを実装したものです。
Visvalingam–Whyatt の重みは「この頂点を消すと、形がどれだけ失われるか」を量ったものです。 海岸線や境界の本質的な構造と、細かい縮尺でだけ意味を持つ細部とを、数で分ける物差しです。 それを頂点符号そのものに詰めることで、データは「この縮尺で何を見せるべきか」の答えを自分で持ち運びます。
平面の地図には無く、球にだけある癖が二つあります。日付変更線をまたぐ面は切らねばならず、地平線をまたぐ面は地平線に沿って閉じねばなりません。 平面の描画器と地球儀の描画器を分けるのは、こうした静かな驚きです。
API も同じ考えを反対側から守ります。データはそのまま — 塗りはデータの見え方で、一度だけ計算して横に置いておくものです。 MapLibre の言葉を借りるのは写しになるためではなく、人がすでに知っていることをそのまま持ち込めるようにするためです。