/ gint

Gint — 使い方と仕組み

Geographic Interleaved Binary Format
interleave(Morton のビット交互配置)· integer(64 bit の整数の頂点符号)· intended(詳細度を最初から設計に組み込む)
データを丸ごと GPU に — どのズームでも描け、問いに答えられる

Part I — 使う
  1. なぜ Gint か — タイルは描き、Gint は知る
  2. はじめての Gint の層
  3. 階級区分図 — 三つの例
  4. 式 — 約束する範囲
  5. データが重なるとき
  6. API リファレンス
Part II — 仕組み
  1. 流れの全体
  2. 符号化 — トポロジ・頂点符号・順位
  3. 頂点シェーダの中の LOD
  4. 三角形に割らずに面を塗る
  5. 線とスタイル
  6. 球の上で — 地平線と日付変更線
  7. 3D で — 地形の上に
  8. 問いに答える — 選択と識別
  9. 設計思想
Part I — 使う

1. なぜ Gint か — タイルは描き、Gint は知る

ortho-earth のエンジンには二種類のベクタの層があります。基図はベクタタイル(MVT)から描きます。サーバーが世界をズームごとに 4z 枚に切り、 ズームのたびに新しい組を取り、タイルごとに一度 CPU で三角形に割ります。背景の絵にはそれが正しいやり方です。 けれど問いかけたいデータには向きません — 地物はタイルの縁で切られ、ズームごとに違う簡略化を受け、三角形としてしか残らないからです。

Gint の層は、GeoPBF のデータの幾何を丸ごと、一度だけ GPU に置きます。

ベクタタイルGint
ファイルズームごと・タイルごとに 1 枚1 ファイル
ズームした時の通信新しいタイル+0 — 詳細度は GPU が選ぶ
面の塗りタイルごとに CPU で三角形化巻き数 — 三角形化しない
地物タイルの縁で切れる丸ごと・共有する境界がぴたり一致
塗り替えタイルごとにスタイルをかけ直すテクスチャ 1 枚の更新 — 地物ごとに違う色にできる
問い—選択・識別・強調・表との突き合わせ

2. はじめての Gint の層

読み込む・足す・塗る・絞り込む・問う、の五段です。言葉は 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 }]・上の層から

2.1 読み込む

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 のような地物ごとに違う属性を持たせてください。

2.2 足す

map.addGint(pbf, options) は層を足し、その手綱(ハンドル)をすぐ返します。焼き — 辺の表、詳細度の梯子、地物ごとの箱 — は worker で走るので、 地図は描き続けます。await layer.ready は GPU に載ると true(失敗なら false)です。addGint はカメラを動かしません。

塗る前の層は既定のスタイルで描きます。面の輪郭はオレンジ(#FF6B35)、線はシアン(#00B4D8)。 面が数画素より小さくなると、線の網の代わりにベタ塗りで見せ、寄ると戻ります。 層は足した順に重なります。order(小さいほど下)を渡せば、呼んだ順に関係なく重ね順が決まります。 層は何枚でも同時に載せられ、両方の土台(WebGPU と WebGL2)で動きます。

2.3 塗る

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 は点も薄くします。

欠けた値

欠けた値が何を意味するかを決めて、式に書いてください。そのプロパティを持たない地物は、interpolate では塗られず、 step では一番下の色に、match では fallback になります。入力を ["coalesce", ["get", "pop"], …] で包むか、 ["has", "pop"] で確かめるか、その地物を絞り込みで外します。

2.4 絞り込む

setFilter(式) は、式が偽になる地物をすべて隠します。setFilter(null) で全部を戻します。 paint と同じく一度だけ評価され、同じ表に書かれます — 作り直しはしません。 隠した地物は描かれず、塗りにも加わらないので、下にあるものの塗りを乱しません(§5)。 ホバーと問いからも外れます。隠した地物が無いものとして、ポインタの下の次の地物が答えになります。

絞り込みは paint と同じ表に住みます。最初の setPaint より前に渡した絞り込みは預かられ、setPaint と一緒に効きます。 setPaint(paint, filter) なら一度に両方を設定できます。

2.5 問う

カーソルを持つ層は一度に一枚です。ホバー・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 以内の線、次にその位置を含むいちばん小さい面を探します — 市の中の区をクリックすれば区が返ります。 絞り込みで隠した地物は飛ばします。

2.6 手綱のほかの動詞

3. 階級区分図 — 三つの例

3.1 市区町村を自分の表で塗る

値は境界のファイルではなく、表 — 国勢調査の 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 パスです。

3.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 のラベルは配列にもできます。どれか一つに当たれば、その出力になります。

3.3 二枚の層 — 危険区域の中の筆

筆の下に災害の層を敷きます。カーソルは筆が持ち、クリックは両方に届きます。

// 土砂災害警戒区域(国土数値情報 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 がある理由です。層は絵であるだけでなく、データです。

4. 式 — 約束する範囲

式は 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

4.1 決まり

  1. 一度だけ、フレームごとではない。setPaint・setFilter を呼んだ時(と地物の状態が変わった時)に走ります。GPU は結果を読むだけです。
  2. ["zoom"] は写し。評価した時のズームです。カメラが止まると層が評価し直します(§2.6)。フレームの間の補間はありません。
  3. 例外を投げない。式が失敗したり型が合わなかったりした地物は既定のまま — 塗りなし・1 px の線・1.5 px の点。評価器に無い演算子も既定になります。
  4. 厳密な比較。==・!=・match は === で比べます。型が違うときは to-string や to-number で揃えてください。
  5. in は ["in", 探す値, 対象]。対象は文字列か配列(["literal", […]])です。
  6. 色は RGB で補間し、入れ子にもできます — match の枝の中の interpolate も効きます。

5. データが重なるとき

実際のデータは重なります。市区町村の上の都道府県の面、二重に登録された区域、重ねて描かれた筆。 Gint は非ゼロ巻き数の規則で塗り — 偶奇の規則は使いません — ので、重なりの結果は予測できます。

品質が見えるようになる

巻き数の和は色以上のものを運びます。読み方を変えると、別々の地物が重なる所、一つの地物が二重に登録された所、環が逆向きに回っている所が分かります。 開発の場では、エンジンはちょうどその画素だけを塗ります。登記所備付地図に当てると、誰にも見えなかったデータの品質の問いが、一枚の絵になります。 この見せ方は今は開発用の道具で、API には含めていません。

制約が一つあります。地物ごとの塗りは、地物番号を浮動小数の的に足し込みます。32 bit の浮動小数を混ぜられない GPU では番号が 16 bit になり、 一枚の層で一つずつ色を付けられる地物は 2,047 までです。それより大きい層は地物ごとの塗りを失い、層の素のスタイルで描かれます。 この制約に当たったら、同じスタイルの地物をマルチポリゴンにまとめてください — 塗りの手間は同じです。

6. API リファレンス

6.1 層を足す

map.addGint(pbf, options) → 手綱 | null

Gint の層を一番上に足し、手綱をすぐ返します。pbf が GPU 用の形を持たなければ null。interactive が false でなければ、その層がアクティブになります(§2.5)。

layer.ready → Promise<boolean>

GPU に載ると true、焼きが失敗すると false。

6.2 スタイル

await layer.setPaint(paint, filter?)

paint(と filter)を地物ごとに評価し、表を 1 枚 GPU へ送ります。paint は丸ごと置き換え。null=既定のスタイル。filter を省くと今の絞り込みのまま。

await layer.setFilter(filter)

絞り込みだけ。最初の setPaint より前なら預かられ、その時に効きます。null=全部を見せる。

layer.setFeatureState(fid, state) · layer.removeFeatureState(fid?)

地物ごとの一時的な状態。["feature-state", key] で読みます。state は混ぜ合わせ、null で消えます。fid を省いた removeFeatureState() は全部を消します。

await layer.setLabel(label | null)

錨:面と線は地物の箱の中心、点はその点。隠した地物にはラベルが付きません。

layer.style(style)

paint なしの見た目を変えます(options.style と同じオブジェクト)。

6.3 データ・重ね順・表示

await layer.setData(pbf, { minZoom?, maxZoom? }) → boolean

データを差し替えます。手綱・イベント・paint・filter はそのままで、paint は新しい地物で評価し直します。新しいデータが GPU に載ると true。

layer.setOrder(order) · layer.setVisible(visible) · layer.remove()

重ね順を変える(小さいほど下)· 見せる/隠す(焼き直しなし)· 層を取り除く。アクティブな層を取り除くと、activate() を呼ぶか interactive な層を足すまで、どの層もカーソルを持ちません。

6.4 イベントと問い

layer.on(type, callback) → layer

アクティブな層だけ。線だけの層は、まだホバーに答えません。

layer.activate()

カーソル(ホバー・tip・強調・layer.on("click"))をこの層に渡します。

layer.query([lng, lat]) → { fid, properties } | null

同期・JavaScript。50 m 以内の点、なければ 30 m 以内の線、なければその位置を含むいちばん小さい面。絞り込みで隠した地物は飛ばします。interactive かどうかに関わらず、層を消していても、ズームの範囲の外でも答えます。

map.queryAll([lng, lat]) → [{ layer, fid, feature }]

いま見えている — 表示中で、ズームの範囲の中の — すべての Gint の層に layer.query を、上の層から。feature は { fid, properties }。地球儀が自分で描く層(国の輪郭など)は含みません。

map.on("click", ({ lngLat, hits }) => …)

地球の上のクリックのたびに、hits=map.queryAll(lngLat) で呼ばれます。ほかに map.on("move" | "settle" | "load", …) と map.off(type, callback)。"load" は地図が描き終わっていればすぐ呼ばれます。

6.5 MapLibre 形の入口

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 を使ってください。

Part II — 仕組み

7. 流れの全体

1
符号化 — データごとに一度
GeoPBF → GintBUF · worker の中の WASM · IndexedDB に保存
共有する弧のトポロジ · 64 bit の頂点符号 · Visvalingam–Whyatt の順位 · 長い弧に 1° ごとの錨
↓
2
焼き — 読み込みごとに一度
GintBUF → GPU のテクスチャ · 焼きの worker
辺のメタ · 段の梯子 · 間引きの塊 · 境界メタ · 地物ごとの bbox とスタイル表
↓
3
描画 — 毎フレーム
WebGPU か WebGL2 · 両方で同じパス
間引き · 頂点シェーダの中の LOD · 巻き数の塗り · カプセルの線 · ドレープ · 選択

8. 符号化 — トポロジ・頂点符号・順位

8.1 共有する弧

符号化器(Rust を WebAssembly にしたもの・worker で動く)は、すべての環と線を分岐点で弧に切り、弧を一本ずつ一度だけ持ちます。 面は弧の参照の並びです — [fid][環の数][弧の数][±弧番号…](逆向きの弧は ~番号)。 隣り合う二つの市町村は境界の弧を共有するので、両方に同じ簡略化がかかります。どのズームでも、すき間も重なりも出ません。 線と隣接の一覧は別の流れに入ります。

8.2 64 bit の頂点符号

経度・緯度を整数にし — ix = (lon + 180) × 107、iy = (lat + 90) × 107(約 1 cm)— 二つのビットを交互に並べて(Morton 順)一つの 64 bit の符号にします。符号は頂点の詳細度も運びます。

L1最上位ビットが立つ — 弧の端点(または 1° の錨)。常に残す。順位 63 として読む。
L2弧の途中の頂点 — 8 単位(8×10-7°)の格子に寄せ、空いた下位 6 bit に順位(0〜63)を入れる。

長い弧には大円に沿って 1° ごとに L1 の頂点(錨)を足します。長い辺が簡略化で弦になり、地球の丸みを横切ってしまうことはありません。

8.3 Visvalingam–Whyatt の順位

途中の頂点は Visvalingam–Whyatt の実効面積(両隣と作る三角形の面積)で重みを付けます。先に消えた頂点より上に来ないよう累積最大で持ち、緯度の cos で補正します。 その面積を 6 bit の順位にします。

rank = clamp( floor( 1.5 · log2(面積 [度²]) + 61.524 ), 0, 63 )

同じ式に「画面の 1 画素が覆う地面の面積」を入れると、今のズームに要る順位が出ます(§9)。順位とズームは同じ単位で話します。

8.4 GintBUF

結果は一つのバッファです。64 byte のヘッダ(magic "Gint"・版・件数・bbox・流れの長さ)の後に、弧の頂点符号・点の符号・ 弧ごとのメタ [開始, 長さ, 重み, –, bbox]、そして面・線・隣接の流れが続きます。 IndexedDB に保存するので、二度目の訪問では符号化を丸ごと飛ばします。

9. 頂点シェーダの中の LOD

焼きの段で、弧は整数テクスチャの中の辺(頂点 A → 頂点 B)になります。毎フレーム:

  1. 要る順位。1 画素が覆う地面の面積を順位の式に通す:rank = floor(1.5·log2(画素の面積) + 61.524)。
  2. 捨てる。頂点シェーダは、始点の順位がそれより低い辺を捨てる。
  3. 前方へ寄せる。残った辺の終点を、次に残る頂点まで前へ寄せる。簡略化した線はつながったまま — すき間も T 字の食い違いも出ない。判断は頂点ごとで、共有する弧は両側の面に同じ判断を下すから。

ズームしても何もアップロードしません。変わるのは uniform 一つです。

9.1 段と塊

全国規模のデータは数百万の辺を持ちます。頂点シェーダがそれを全部触らずに済むよう、二つの仕組みがあります。

10. 三角形に割らずに面を塗る

面は三角形に割りません。面の辺を、その地物の bbox の中心を要にした扇としてステンシルに描きます。 表向きの三角形は +1、裏向きは −1(非ゼロ巻き数)。二つ目のパスで、数がゼロでない画素を塗ります。 凹んだ形も穴も特別扱い無しに正しく出ます。扇どうしの重なりは打ち消し合います。

11. 線とスタイル

辺は一本ずつ小さな四角(6 頂点)として描き、カプセルの符号付き距離関数で塗ります。どの太さでも、継ぎ目と端は余分な幾何無しに丸くなります。 色・太さ・点の半径・フラグは GPU 上の地物ごとのスタイル表から、共通のスタイルは 256 枠のスタイル表と破線表から引きます — 地図の塗り替えはテクスチャの更新であって、作り直しではありません。

12. 球の上で — 地平線と日付変更線

13. 3D で — 地形の上に

14. 問いに答える — 選択と識別

15. 設計思想

地理データをどう表すかは、どの詳細度を「現実」とみなすかの選択でもあります。 Natural Earth の自然/文化の区別が地球上の現象を人の目から整理するように、Gint の詳細度の設計は「見えるものは縮尺で変わる」という考えを実装したものです。

Visvalingam–Whyatt の重みは「この頂点を消すと、形がどれだけ失われるか」を量ったものです。 海岸線や境界の本質的な構造と、細かい縮尺でだけ意味を持つ細部とを、数で分ける物差しです。 それを頂点符号そのものに詰めることで、データは「この縮尺で何を見せるべきか」の答えを自分で持ち運びます。

平面の地図には無く、球にだけある癖が二つあります。日付変更線をまたぐ面は切らねばならず、地平線をまたぐ面は地平線に沿って閉じねばなりません。 平面の描画器と地球儀の描画器を分けるのは、こうした静かな驚きです。

API も同じ考えを反対側から守ります。データはそのまま — 塗りはデータの見え方で、一度だけ計算して横に置いておくものです。 MapLibre の言葉を借りるのは写しになるためではなく、人がすでに知っていることをそのまま持ち込めるようにするためです。

gint · @ortho-earth/core · Kenji Yoshida · 2026