Protocol Buffers ベースの地理空間バイナリフォーマット。
軽量・変換ハブ・ArrayBuffer ネイティブ — ブラウザ GIS のワイヤフォーマット
GeoJSON は読みやすく扱いやすいが、大規模データでは致命的な問題がある。 テキスト形式のため転送量が大きく、パースに CPU 時間がかかり、 ブラウザのメインスレッドをブロックする。 Shapefile は広く普及しているが複数ファイル構成で Web に向かず、 タイル MVT は zoom ごとに分割されネットワーク効率が悪い。
GeoPBF は ブラウザ GIS の通信レイヤー として設計されたバイナリフォーマットである。 Protocol Buffers の圧縮効率、座標のデルタエンコーディング、 ArrayBuffer ネイティブな設計により、転送・デコード・Worker 間受け渡しのすべてが高速になる。
GeoPBF ファイルは Header Section と Body Section(FARRAY) の 2 部構成である。
| フィールド | タグ | 型 | 説明 |
|---|---|---|---|
| GTYPE | 8 | Varint | 0:Point 1:MPoint 2:Line 3:MLine 4:Poly 5:MPoly 6:Collection |
| LENGTH | 9 | Packed Varint | リング・マルチパートの頂点数列 |
| COORDS | 10 | Packed SVarint | デルタ圧縮座標列 X₀,Y₀,ΔX₁,ΔY₁,...(後述) |
| INDEX | 12 | Packed Varint | グローバル KEYS 辞書へのインデックス |
| VALUE | 11 | Repeated Message | NULL/BOOL/INTEGER/FLOAT/STRING/DATE/COLOR/JSON/BLOB/IMAGE |
NAME・DESCRIPTION・LICENSE などのメタデータはヘッダーに独立して格納されているため、 バイナリスライスで直接書き換えが可能。フィーチャ本体を再エンコードする必要がなく、 巨大データセットの名称変更やライセンス情報の更新が即座に完了する。
座標を GeoJSON のように浮動小数点テキストで記録すると、
経度 139.7412345 だけで 11 バイト消費する。
GeoPBF は 2 段階の圧縮でこれを劇的に削減する。
隣接する頂点間の座標差は数百メートル程度(地図スケール次第)であり、 整数化後の差分は小さな値になる。 Varint は小さな整数を少ないバイト数で表現するため、 密集した頂点ほど圧縮率が高い という地理データに最適な特性を持つ。
GeoJSON の "coordinates":[139.741234,35.681234] は約 30 バイト(テキスト)。
GeoPBF のデルタ Varint 表現では初回 8 バイト、以降の頂点は 2〜3 バイト。
実際の海岸線データでは 60〜80% のサイズ削減 が典型的。
さらに gzip 圧縮(CompressionStream)を組み合わせることで追加 30〜50% 削減。
GeoPBF は単なる保存フォーマットではなく、主要 GIS フォーマット間の 変換ハブ として機能する。 入力(デコード)と出力(エンコード)の両方に変換パスを持ち、 GeoPBF を中心に置くことで任意フォーマット間の相互変換が 1 ステップで可能になる。
| フォーマット | 読み込み | 書き出し | 特記 |
|---|---|---|---|
| GeoJSON | ✓ | ✓ | File/Blob/Object すべて対応 |
| Shapefile | ✓ (.zip) | ✓ (.zip) | マルチファイルを zip 単体で処理 |
| KMZ / KML | ✓ | ✓ | Google Earth 互換 |
| GML | ✓ | ✓ | OGC 標準 |
| GPX | ✓ | ✓ | GPS トラック・ウェイポイント |
| FlatGeoBuf | ✓ | ✓ | 高速ストリーミング対応 |
| TopoJSON | ✓ | ✓ | トポロジー保持 入出力 |
| GeoPBF | ✓ (native) | ✓ (native) | ネイティブ高速ロード |
| Gint | ✓ (decode) | ✓ (encode) | Morton + VW 拡張(後述) |
geopbf(input) に File・Blob・ArrayBuffer・
Object のいずれを渡しても、拡張子・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 をダウンロード
アンチメリジアン(経度 ±180°)は地理データにおける代表的な落とし穴である。 ベーリング海をまたぐようなポリゴンは、多くのフォーマットで座標が +179° から −179° へジャンプする 単一リングとして格納される。 レンダラー・空間インデックス・クリッピング処理のいずれもこのようなデータでは誤動作する。
GeoPBF はアンチメリジアンの正確さをフォーマット不変条件として扱う: GeoPBF に格納されたジオメトリはすべてアンチメリジアンで切断されていることが保証される。 アンチメリジアンをまたぐポリゴンはフォーマット内に存在できない。
setFeature() はすべてのフィーチャをバッファに書き込む前に
antimeridianFeature(q) を呼び出す — ソースフォーマットに関わらず。
GeoJSON・Shapefile・KMZ・GML・GPX・FlatGeoBuf・TopoJSON — すべて同じパスを通る。
呼び出し側はジオメトリを事前処理する必要がない。
カットは単純な ±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|) ← 球面上で幾何学的に正確
Morton コードは座標が有界な領域内に収まることを前提とする。 ±180° をまたぐジオメトリは整数空間の端から端へジャンプするような Morton コードを生成し、 二分探索と空間近接性の保証を破壊する。 エンコード時にカットを済ませることで、GeoPBF タイル内のすべての Morton コードが 下流で特別扱い不要な空間的一貫性を持つ。
GeoPBF の内部表現は一貫して ArrayBuffer である。
これはブラウザの並列処理基盤(Web Workers)と深く統合するための設計上の選択であり、
3 つの重要な特性をもたらす。
// ArrayBuffer はメインスレッド → Worker へゼロコピーで転送
// (コピーではなく所有権の移動 → メモリ消費が増えない)
worker.postMessage(pbf.arrayBuffer, [pbf.arrayBuffer]);
// 転送後 pbf.arrayBuffer は detached(メインスレッドからアクセス不能)
// Worker 側
onmessage = ({ data: buf }) => {
const view = new DataView(buf); // 即座にデコード開始
postMessage(result, [result]); // 処理後もゼロコピーで返却
};
// 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 }));
// 保存時: ブラウザネイティブの 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" }));
GeoJSON はオブジェクトグラフ(JSON)であり、Worker に渡すには
JSON.stringify → 転送 → JSON.parse という
シリアライズ/デシリアライズが必要で、大規模データでは数百ミリ秒を消費する。
GeoPBF は最初から ArrayBuffer であるため、
ゼロコピー転送 で Worker 間を移動でき、
受け取り側はポインタを進めるだけで座標を読める。
メインスレッドは一切ブロックされない。
GeoPBF に gint エンコードを適用すると、各頂点の座標が 64bit 整数 1 つ にパックされる。 この 64bit 整数は Morton コードと VW ウェイトを同時に格納する。
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;
| 指標 | 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 フォーマット内蔵 |
| トポロジー | なし | なし | なし | アーク共有・境界整合 |
// ── 読み込み ──────────────────────────────────────────────
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' });