/ Technical Overview

GeoPBF — Lightweight Binary GIS Format

Protocol Buffers ベースの地理空間バイナリフォーマット。
軽量・変換ハブ・ArrayBuffer ネイティブ — ブラウザ GIS のワイヤフォーマット

Table of Contents
  1. なぜ GeoPBF か
  2. バイナリフォーマット構造
  3. デルタエンコーディング × Varint
  4. GIS 変換ハブ
  5. アンチメリジアン処理
  6. ArrayBuffer と Worker アーキテクチャ
  7. Gint 拡張 — 64bit 座標パッキング
  8. 他フォーマットとの比較
  9. API リファレンス

1. なぜ GeoPBF か

GeoJSON は読みやすく扱いやすいが、大規模データでは致命的な問題がある。 テキスト形式のため転送量が大きく、パースに CPU 時間がかかり、 ブラウザのメインスレッドをブロックする。 Shapefile は広く普及しているが複数ファイル構成で Web に向かず、 タイル MVT は zoom ごとに分割されネットワーク効率が悪い。

GeoPBF は ブラウザ GIS の通信レイヤー として設計されたバイナリフォーマットである。 Protocol Buffers の圧縮効率、座標のデルタエンコーディング、 ArrayBuffer ネイティブな設計により、転送・デコード・Worker 間受け渡しのすべてが高速になる。

2. バイナリフォーマット構造

GeoPBF ファイルは Header SectionBody Section(FARRAY) の 2 部構成である。

Header
固定長メタデータ
NAME · KEYS(属性名グローバル辞書)· PRECISION · BUFS(バイナリプール)
DESCRIPTION · LICENSE · ATTRIBUTION
メタデータは O(1) でバイナリスライスにより更新可能(再エンコード不要)
FARRAY
フィーチャ配列
FEATURE × N
  └ GEOMETRY: GTYPE · LENGTH · COORDS(デルタ圧縮)
  └ PROPERTIES: INDEX(KEYS参照)· VALUE(型付き)
フィールドタグ説明
GTYPE8Varint0:Point 1:MPoint 2:Line 3:MLine 4:Poly 5:MPoly 6:Collection
LENGTH9Packed Varintリング・マルチパートの頂点数列
COORDS10Packed SVarintデルタ圧縮座標列 X₀,Y₀,ΔX₁,ΔY₁,...(後述)
INDEX12Packed Varintグローバル KEYS 辞書へのインデックス
VALUE11Repeated MessageNULL/BOOL/INTEGER/FLOAT/STRING/DATE/COLOR/JSON/BLOB/IMAGE
O(1) ヘッダー更新

NAME・DESCRIPTION・LICENSE などのメタデータはヘッダーに独立して格納されているため、 バイナリスライスで直接書き換えが可能。フィーチャ本体を再エンコードする必要がなく、 巨大データセットの名称変更やライセンス情報の更新が即座に完了する。

3. デルタエンコーディング × Varint

座標を GeoJSON のように浮動小数点テキストで記録すると、 経度 139.7412345 だけで 11 バイト消費する。 GeoPBF は 2 段階の圧縮でこれを劇的に削減する。

① Raw float
139.74000
35.68000
139.74123
35.68123
139.74246
② Delta
139.74000
35.68000
+0.00123
+0.00123
+0.00123
③ × 10⁶ (int)
139740000
35680000
+1234
+1234
+1234
④ Varint bytes
4 bytes
4 bytes
2 bytes
2 bytes
2 bytes

隣接する頂点間の座標差は数百メートル程度(地図スケール次第)であり、 整数化後の差分は小さな値になる。 Varint は小さな整数を少ないバイト数で表現するため、 密集した頂点ほど圧縮率が高い という地理データに最適な特性を持つ。

GeoJSON との比較

GeoJSON の "coordinates":[139.741234,35.681234] は約 30 バイト(テキスト)。 GeoPBF のデルタ Varint 表現では初回 8 バイト、以降の頂点は 2〜3 バイト。 実際の海岸線データでは 60〜80% のサイズ削減 が典型的。 さらに gzip 圧縮(CompressionStream)を組み合わせることで追加 30〜50% 削減。

4. GIS 変換ハブ

GeoPBF は単なる保存フォーマットではなく、主要 GIS フォーマット間の 変換ハブ として機能する。 入力(デコード)と出力(エンコード)の両方に変換パスを持ち、 GeoPBF を中心に置くことで任意フォーマット間の相互変換が 1 ステップで可能になる。

フォーマット読み込み書き出し特記
GeoJSONFile/Blob/Object すべて対応
Shapefile✓ (.zip)✓ (.zip)マルチファイルを zip 単体で処理
KMZ / KMLGoogle Earth 互換
GMLOGC 標準
GPXGPS トラック・ウェイポイント
FlatGeoBuf高速ストリーミング対応
TopoJSONトポロジー保持 入出力
GeoPBF✓ (native)✓ (native)ネイティブ高速ロード
Gint✓ (decode)✓ (encode)Morton + VW 拡張(後述)
自動フォーマット判定

geopbf(input)FileBlobArrayBufferObject のいずれを渡しても、拡張子・MIME タイプ・マジックバイトから フォーマットを自動判定して適切なデコーダを呼び出す。 呼び出し側はフォーマットを意識する必要がない。

// どんな形式でも同じ API で読み込める
const pbf = await geopbf(file);          // File(Shapefile .zip, GeoJSON, KMZ ...)
const pbf = await geopbf(arrayBuffer);    // ArrayBuffer(fetch 結果など)
const pbf = await geopbf(geojsonObject);  // GeoJSON オブジェクト直接

// 各フォーマットへのエクスポート
const geojson  = pbf.geojson;    // → GeoJSON オブジェクト
const topojson = pbf.topojson;   // → TopoJSON(トポロジー保持)
const shp      = pbf.shape();    // → Shapefile (.zip Blob)
const kmz      = pbf.kmz();      // → KMZ Blob
const gml      = pbf.gml();      // → GML 文字列
const fgb      = await pbf.fgb(); // → FlatGeoBuf ArrayBuffer

// ネイティブ保存(gzip 済み .geopbf)
await pbf.save('coastline');    // → coastline.geopbf をダウンロード

5. アンチメリジアン処理

アンチメリジアン(経度 ±180°)は地理データにおける代表的な落とし穴である。 ベーリング海をまたぐようなポリゴンは、多くのフォーマットで座標が +179° から −179° へジャンプする 単一リングとして格納される。 レンダラー・空間インデックス・クリッピング処理のいずれもこのようなデータでは誤動作する。

GeoPBF はアンチメリジアンの正確さをフォーマット不変条件として扱う: GeoPBF に格納されたジオメトリはすべてアンチメリジアンで切断されていることが保証される。 アンチメリジアンをまたぐポリゴンはフォーマット内に存在できない。

エンコード時に自動適用 — 全インポートパス共通

setFeature() はすべてのフィーチャをバッファに書き込む前に antimeridianFeature(q) を呼び出す — ソースフォーマットに関わらず。 GeoJSON・Shapefile・KMZ・GML・GPX・FlatGeoBuf・TopoJSON — すべて同じパスを通る。 呼び出し側はジオメトリを事前処理する必要がない。

5.1 球面大円交点演算

カットは単純な ±180° 座標クランプではない。 antimeridianCut() は球面の公式を使って、 各交差セグメントがアンチメリジアンと交わる正確な緯度を計算する — 経線収束が顕著な極近傍では特に重要である。 カット後の各サブポリゴンは、正確な交点緯度に頂点を挿入した有効な閉リングとなる。

// 交差検出: 符号反転 + スパン 180° 超 → アンチメリジアン越え(単なる座標ジャンプとの区別)
// p[i][0] * p[i+1][0] < 0  &&  abs(p[i][0] - p[i+1][0]) > 180

// 球面交点緯度(大円の公式)
// x = sin(Δlat/2)·sin(avgLng)·cos(halfΔlng) - sin(avgLat)·cos(avgLng)·sin(halfΔlng)
// z = cos(lat0)·cos(lat1)·sin(Δlng)
// intersectLat = atan2(x, |z|)   ← 球面上で幾何学的に正確
Gint との関係

Morton コードは座標が有界な領域内に収まることを前提とする。 ±180° をまたぐジオメトリは整数空間の端から端へジャンプするような Morton コードを生成し、 二分探索と空間近接性の保証を破壊する。 エンコード時にカットを済ませることで、GeoPBF タイル内のすべての Morton コードが 下流で特別扱い不要な空間的一貫性を持つ。

6. ArrayBuffer と Worker アーキテクチャ

GeoPBF の内部表現は一貫して ArrayBuffer である。 これはブラウザの並列処理基盤(Web Workers)と深く統合するための設計上の選択であり、 3 つの重要な特性をもたらす。

Main Thread
geopbf(file)
pbf.arrayBuffer
worker.postMessage(buf, [buf])
— 転送後は参照不能 —
Encoder Worker
gint encode
topology analysis
postMessage(result, [result])
zero-copy 返却
Render Worker
SharedArrayBuffer
複数 Worker が同時参照
LOD フィルタ処理
Canvas / WebGL 描画

6.1 Transferable(ゼロコピー転送)

// ArrayBuffer はメインスレッド → Worker へゼロコピーで転送
// (コピーではなく所有権の移動 → メモリ消費が増えない)
worker.postMessage(pbf.arrayBuffer, [pbf.arrayBuffer]);
// 転送後 pbf.arrayBuffer は detached(メインスレッドからアクセス不能)

// Worker 側
onmessage = ({ data: buf }) => {
	const view = new DataView(buf);  // 即座にデコード開始
	postMessage(result, [result]);    // 処理後もゼロコピーで返却
};

6.2 SharedArrayBuffer(複数 Worker 同時参照)

// gint デコード後の座標データを SharedArrayBuffer に展開
// → 複数の描画 Worker が同時に、コピーなしで参照できる
const sab = new SharedArrayBuffer(buf.byteLength);
new Uint8Array(sab).set(new Uint8Array(buf));

// tile worker × N が並列で LOD フィルタ & 描画
tileWorkers.forEach(w => w.postMessage({ sab, zoom, viewport }));

6.3 CompressionStream(ネイティブ gzip)

// 保存時: ブラウザネイティブの gzip 圧縮(外部ライブラリ不要)
let blob = new Blob([buf]);
blob = await (new Response(
	blob.stream().pipeThrough(new CompressionStream("gzip"))
)).blob();
postMessage(new File([blob], `${name}.geopbf`, { type: "application/x-geopbf" }));
Worker との相性が良い理由

GeoJSON はオブジェクトグラフ(JSON)であり、Worker に渡すには JSON.stringify → 転送 → JSON.parse という シリアライズ/デシリアライズが必要で、大規模データでは数百ミリ秒を消費する。 GeoPBF は最初から ArrayBuffer であるため、 ゼロコピー転送 で Worker 間を移動でき、 受け取り側はポインタを進めるだけで座標を読める。 メインスレッドは一切ブロックされない。

7. Gint 拡張 — 64bit 座標パッキング

GeoPBF に gint エンコードを適用すると、各頂点の座標が 64bit 整数 1 つ にパックされる。 この 64bit 整数は Morton コードと VW ウェイトを同時に格納する。

L1 node
(anchor)
1
Morton code (63 bits) — 精度 10⁻⁷ deg 固定
bit 63 = 1 → L1(アンカー頂点、常に保持)
L2 node
(detail)
0
Morton code (58 bits)
VW rank
(6 bits)
bit 63 = 0 → L2(詳細頂点、LOD フィルタ対象) ·  bits 0-5 = VW ウェイトランク(0〜63)

1 つの 64bit 整数に「どこ(Morton)」と「どの LOD(VW ランク)」を同時に収めることで、 空間クエリと LOD フィルタが単純な整数演算だけで完結する。

// L1 / L2 判定(最上位ビット)
const isAnchor = (code >> 63n) === 1n;

// Morton コード取り出し(L2 の場合上位 58bit)
const mortonCode = code & 0x7FFFFFFFFFFFFFF8n;  // bits 3-62

// VW ウェイトランク取り出し(L2 の場合下位 6bit)
const vwRank = Number(code & 0x3Fn);  // bits 0-5 → 0〜63

// ズームレベル z での LOD フィルタ(1px² に対応するランク閾値)
const minRank = rankFromZoom(z);  // z が大きいほど低い閾値(より多くの頂点)
const visible = isAnchor || vwRank >= minRank;

8. 他フォーマットとの比較

指標 GeoJSON Shapefile MVT(タイル) GeoPBF
エンコーディング テキスト UTF-8 バイナリ(複数ファイル) PBF バイナリ PBF + デルタ + Varint
ファイル数 1 .shp + .dbf + .shx + ... zoom × tile 数 1
Web 転送効率 低(テキスト大) 中(zoom 分割) 高(バイナリ + gzip)
Worker 転送 JSON シリアライズ必要 変換必要 ArrayBuffer ArrayBuffer ゼロコピー
LOD サポート なし なし zoom 別ファイル VW ウェイト内包
空間インデックス なし .shx(線形) タイル座標 Morton 64bit
フォーマット変換 要外部ライブラリ 要外部ライブラリ 限定的 9 フォーマット内蔵
トポロジー なし なし なし アーク共有・境界整合

9. API リファレンス

// ── 読み込み ──────────────────────────────────────────────
const pbf = await geopbf(input, options);
// input: File | Blob | ArrayBuffer | GeoJSON object | URL string
// options: { name, gint, nocache, clean }
//   gint:    false → トポロジーエンコード(gint変換)をスキップ
//   nocache: true  → IndexedDB キャッシュを無視して再取得
//   clean:   true  → トポロジー整合クリーン処理を実行

// ── データアクセス ──────────────────────────────────────────
pbf.length          // フィーチャ数
pbf.keys            // 属性名配列 ['name', 'rank', ...]
pbf.arrayBuffer     // 生 ArrayBuffer(Worker 転送用)
pbf.geojson         // GeoJSON FeatureCollection に変換
pbf.topojson        // TopoJSON に変換(トポロジー保持)
pbf.getFeature(i)   // i 番目のフィーチャだけをデコード

// ── エクスポート ────────────────────────────────────────────
await pbf.save('name')      // .geopbf(gzip)としてダウンロード
pbf.shape()               // Shapefile .zip
pbf.kmz()                 // KMZ
pbf.gml()                 // GML 文字列
pbf.gpx()                 // GPX 文字列
await pbf.fgb()           // FlatGeoBuf ArrayBuffer

// ── メタデータ O(1) 更新 ────────────────────────────────────
pbf.header({ name: 'New Name', description: '...', license: 'MIT' });

// ── 描画 ───────────────────────────────────────────────────
pbf.draw(canvasCtx, { fill: '#5a8050', stroke: '#a8d47e' });

geopbf v1.0.0 · Kenji Yoshida · MIT License · 2026