Open11

FlatGeobufデータ構造解析

mugwort_rcmugwort_rc

公式READMEによると、FlatGeobufのデータ仕様は以下のみ。

  • MB: Magic bytes (0x6667620366676201)
  • H: Header (variable size flatbuffer)
  • I (optional): Static packed Hilbert R-tree index (static size custom buffer)
  • DATA: Features (variable size flatbuffers)
mugwort_rcmugwort_rc

0x6667620366676201b"fgb\x03fgb\x01"

一方、UScounties.fgbはb"fgb\x03fgb\x00"なので末尾1byteだけ違う…。

mugwort_rcmugwort_rc

The fourth byte in the magic bytes indicates major specification version.
The last byte of the magic bytes indicate patch level.
Patch level is backwards compatible so an implementation for a major version should accept any patch level version.

ということは、UScounties.fgbはv3.0のFlatGeobufファイルであると。

後方互換があるので、v3.xの間は一貫性が保証される。

mugwort_rcmugwort_rc

Magic bytesに続く4byte(b"\xdc\x02\x00\x00")はHeaderデータのデータ長?

mugwort_rcmugwort_rc

header.fbsを取得し、Python向けにコンパイル。

flatc --python header.fbs

コンパイルすると以下のファイルが作成される。

  • FlatGeobuf/
    • __init__.py
    • Column.py
    • ColumnType.py
    • Crs.py
    • GeometryType.py
    • Header.py
mugwort_rcmugwort_rc

Python内でヘッダーを読んでみる。

>>> import FlatGeobuf.Header
>>> data = open("UScounties.fgb", "rb").read()
>>> header = FlatGeobuf.Header.Header.GetRootAs(data, 12)  # 8 + 4 ?
>>> header.Name()
"UScounties"
>>> header.EnvelopeLength()
4
>>> header.Envelope(0)
-179.14733999999999
>>> header.Envelope(1)
17.884812999999998
>>> header.Envelope(2)
179.77847
>>> header.Envelope(3)
71.3525606439998
>>> header.GeometryType()
6
>>> header.HasZ()
False
>>> header.HasM()
False
>>> header.HasT()
False
>>> header.HasTm()
False
>>> header.FeaturesCount()
3221
>>> header.IndexNodeSize()
16
>>> header.Title()
>>> header.Description()
>>> header.Metadata()
>>> crs = header.Crs()
>>> crs.Org()
b'EPSG'
>>> crs.Code()
4269
>>> crs.Name()
b'NAD83'
>>> crs.Description()
>>> crs.Wkt()
b'GEOGCRS["NAD83",DATUM["North American Datum 1983",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["latitude",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["longitude",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4269]]'
>>> crs.CodeString()
>>> header.ColumnsLength()
6
>>> c = header.Columns(0)
>>> c.Name()
b'STATE_FIPS'
>>> c.Type()
11
>>> c.Title()
>>> c.Description()
>>> c.Width()
-1
>>> c.Precision()
-1
>>> c.Scale()
-1
>>> c.Nullable()
True
>>> c.Unique()
False
>>> c.PrimaryKey()
False
>>> c.Metadata()
>>>
mugwort_rcmugwort_rc

以下の形式でパースしても問題がないので、0x2dcはヘッダーデータ長っぽい。

>>> header = FlatGeobuf.Header.Header.GetRootAs(data[12:12+0x2dc], 0)
mugwort_rcmugwort_rc

QGIS 3.16で読んだ場合の地物数と列名が一致するので問題なさそう。

mugwort_rcmugwort_rc

データを眺めてみると、header.Columns(0)のフィールド値がヘッダーデータの末尾の方にあり、header.Columns(5)のフィールド値がヘッダーデータの前方寄りにある逆転状態なことに気がつく。

これがFlatBuffersのデータ格納オーダーらしい。

FlatBuffers byte order

mugwort_rcmugwort_rc

header.IndexNodeSize()16なので、header.fbsの定義によると、OptionalなHilbert R-Treeインデックスを持っている…?

  index_node_size: ushort = 16; // Index node size (0 = no index)