FlatGeobufデータ構造解析
FlatGeobuf 公式の「Observable Notebook」より以下のファイルをDL。
公式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)
0x6667620366676201
はb"fgb\x03fgb\x01"
一方、UScounties.fgbはb"fgb\x03fgb\x00"
なので末尾1byteだけ違う…。
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
の間は一貫性が保証される。
Magic bytesに続く4byte(b"\xdc\x02\x00\x00"
)はHeaderデータのデータ長?
header.fbsを取得し、Python向けにコンパイル。
flatc --python header.fbs
コンパイルすると以下のファイルが作成される。
-
FlatGeobuf/
__init__.py
Column.py
ColumnType.py
Crs.py
GeometryType.py
Header.py
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()
>>>
以下の形式でパースしても問題がないので、0x2dc
はヘッダーデータ長っぽい。
>>> header = FlatGeobuf.Header.Header.GetRootAs(data[12:12+0x2dc], 0)
QGIS 3.16で読んだ場合の地物数と列名が一致するので問題なさそう。
データを眺めてみると、header.Columns(0)
のフィールド値がヘッダーデータの末尾の方にあり、header.Columns(5)
のフィールド値がヘッダーデータの前方寄りにある逆転状態なことに気がつく。
これがFlatBuffersのデータ格納オーダーらしい。
header.IndexNodeSize()
が16
なので、header.fbs
の定義によると、OptionalなHilbert R-Treeインデックスを持っている…?
index_node_size: ushort = 16; // Index node size (0 = no index)