ベクタードライバーのPython実装チュートリアル
Added in version 3.1.
はじめに
GDAL 3.1以降,Pythonで読み取り専用のベクタードライバーを作成する機能が追加されました.ベクタードライバーの動作原理についての一般的な原則を説明する ベクタードライバー実装チュートリアル を最初に読むことを強くお勧めします.
この機能はGDAL/OGR SWIG Pythonバインディングの使用を必要としません(ただし,ベクターPythonドライバーはそれらを使用するかもしれません.)
注:プロジェクト方針により,これは"実験的"な機能と見なされ,GDALプロジェクトはそのようなPythonドライバーをGDALリポジトリに含めることはありません. GDALmasterに含めることを目指すドライバーは,まずC++に移植する必要があります. その理由は次のとおりです:
Pythonコードの正確性は主に実行時にチェックできますが,C++は静的解析(コンパイル時およびその他のチェッカー)の恩恵を受けます.
PythonコードはPython Global Interpreter Lockの下で実行されるため,スケーリングされません.
GDALのすべてのビルドにPythonが利用可能とは限りません.
Pythonインタプリタへのリンクメカニズム
ドライバーの場所
ドライバーファイル名は gdal_ または ogr_ で始まり, .py 拡張子を持つ必要があります. これらは以下のディレクトリで検索されます:
GDAL_PYTHON_DRIVER_PATH
構成オプションで指定されたディレクトリ(UNIXでは : で区切られた複数のパスがあるかもしれません)定義されていない場合,
GDAL_DRIVER_PATH
構成オプションで指定されたディレクトリです.定義されていない場合,ネイティブプラグインが配置されているディレクトリ(UNIXビルドでコンパイル時にハードコードされています)です.
GDALはドライバー .py スクリプトでインポートされるPython依存関係を管理しません.必要な依存関係がインストールされていることを確認するのはユーザーの責任です.
インポートセクション
ドライバーはベースクラスをロードするために以下のインポートセクションを持つ必要があります.
from gdal_python_driver import BaseDriver, BaseDataset, BaseLayer
gdal_python_driver
モジュールはGDALによって動的に作成され,ファイルシステムに存在しません.
メタデータセクション
.pyファイルの最初の1000行には,必須およびオプションのKEY=VALUEドライバー指令が定義されている必要があります. これらはPythonインタプリタを使用せずにC++コードによって解析されるため,以下の制約を厳守することが重要です:
各宣言は1行であり,
# gdal: DRIVER_
で始まる必要があります(シャープ文字とgdalの間にスペース文字, コロン文字とDRIVER_の間にスペース文字)値は文字列型のリテラル値である必要があります(# gdal: DRIVER_SUPPORTED_API_VERSIONは整数の配列を受け入れることができます), 式, 関数呼び出し, エスケープシーケンスなどは使用できません.
文字列はシングルクォートまたはダブルクォートで囲まれている必要があります
以下の指令が宣言されている必要があります:
# gdal: DRIVER_NAME = "NAME"
: ドライバーの短い名前# gdal: DRIVER_SUPPORTED_API_VERSION = [1]
: ドライバーがサポートするAPIバージョン. GDAL 3.1で唯一サポートされているバージョンである1を含める必要があります# gdal: DRIVER_DCAP_VECTOR = "YES"
: ベクタードライバーを宣言します# gdal: DRIVER_DMD_LONGNAME = "ドライバーの長い名前"
追加の指令:
# gdal: DRIVER_DMD_EXTENSIONS = "ext1 ext2"
: ドライバーによって認識される拡張子(ドットなし)のリストで,スペースで区切られています# gdal: DRIVER_DMD_HELPTOPIC = "https://example.com/my_help.html"
: ドライバーのヘルプページへのURL# gdal: DRIVER_DMD_OPENOPTIONLIST = "<OpenOptionList><Option name='OPT1' type='boolean' description='bla' default='NO'/></OpenOptionList>"
ここでXMLはOptionOptionList
です.および
GDAL_DMD_
またはGDAL_DCAP
で始まるgdal.hで見つかるすべてのメタデータ項目を作成します. これは# gdal: DRIVER_
で始まる項目名とGDAL_DMD_
またはGDAL_DCAP
メタデータ項目の値です. たとえば#define GDAL_DMD_CONNECTION_PREFIX "DMD_CONNECTION_PREFIX"
は# gdal: DRIVER_DMD_CONNECTION_PREFIX
になります.
例:
# gdal: DRIVER_NAME = "DUMMY"
# gdal: DRIVER_SUPPORTED_API_VERSION = [1]
# gdal: DRIVER_DCAP_VECTOR = "YES"
# gdal: DRIVER_DMD_LONGNAME = "my dummy plugin"
# gdal: DRIVER_DMD_EXTENSIONS = "foo bar"
# gdal: DRIVER_DMD_HELPTOPIC = "https://example.com/my_help.html"
ドライバークラス
エントリーポイント .py スクリプトには gdal_python_driver.BaseDriver
を継承する単一のクラスが含まれている必要があります.
そのクラスは以下のメソッドを定義する必要があります:
- identify(self, filename, first_bytes, open_flags, open_options={})
- パラメータ:
filename (str) -- ファイル名, または一般的には接続文字列.
first_bytes (binary) -- ファイルの最初のバイト(ファイルの場合). 少なくとも1024(ファイルが少なくとも1024バイトの場合), または以前にネイティブドライバーがより多くを要求した場合はそれ以上です.
open_flags (int) -- オープンフラグ. 現在は無視されます.
open_options (dict) -- オープンオプション.
- 戻り値:
ファイルがドライバーによって認識される場合はTrue, そうでない場合はFalse, 最初のバイトからそれがわからない場合は-1です.
- open(self, filename, first_bytes, open_flags, open_options={})
- パラメータ:
filename (str) -- ファイル名, または一般的には接続文字列.
first_bytes (binary) -- ファイルの最初のバイト(ファイルの場合). 少なくとも1024(ファイルが少なくとも1024バイトの場合), または以前にネイティブドライバーがより多くを要求した場合はそれ以上です.
open_flags (int) -- オープンフラグ. 現在は無視されます.
open_options (dict) -- オープンオプション.
- 戻り値:
gdal_python_driver.BaseDataset または None から派生したオブジェクト
例:
# Required: class deriving from BaseDriver
class Driver(BaseDriver):
def identify(self, filename, first_bytes, open_flags, open_options={}):
return filename == 'DUMMY:'
# Required
def open(self, filename, first_bytes, open_flags, open_options={}):
if not self.identify(filename, first_bytes, open_flags):
return None
return Dataset(filename)
データセットクラス
Driver.open() メソッドは成功した場合, gdal_python_driver.BaseDataset
を継承するクラスからのオブジェクトを返す必要があります.
レイヤー
このオブジェクトの役割はベクターレイヤーを格納することです. 実装オプションは2つあります. レイヤーの数が少ないか, 構築が高速である場合, __init__
メソッドは gdal_python_driver.BaseLayer
を継承するクラスからのオブジェクトのシーケンスである layers
属性を定義できます.
例:
class Dataset(BaseDataset):
def __init__(self, filename):
self.layers = [Layer(filename)]
それ以外の場合, 以下の2つのメソッドを定義する必要があります:
- layer_count(self)
- 戻り値:
レイヤーの数
- layer(self, idx)
- パラメータ:
idx (int) -- 返すレイヤーのインデックス. 通常は0から self.layer_count() - 1 の間ですが, 呼び出しコードは任意の値を渡すかもしれません. 無効なインデックスの場合は, None を返すべきです.
- 戻り値:
gdal_python_driver.BaseLayer または None から派生したオブジェクト. C++コードはそのオブジェクトをキャッシュし, このメソッドは特定のidx値に対して1回だけ呼び出されます.
例:
class Dataset(BaseDataset):
def layer_count(self):
return 1
def layer(self, idx):
return [Layer(self.filename)] if idx = 0 else None
メタデータ
データセットは,デフォルトのメタデータドメインの metadata
辞書を定義できます. あるいは,以下のメソッドを実装することもできます.
- metadata(self, domain)
- パラメータ:
domain (str) -- メタデータドメイン. デフォルトの場合は空の文字列
- 戻り値:
None, または文字列型の key:value ペアの辞書;
その他のメソッド
以下のメソッドはオプションで実装することができます:
- close(self)
C++の対応するGDALDatasetオブジェクトの破棄時に呼び出されます. たとえばデータベース接続を閉じるのに便利です.
レイヤークラス
データセットオブジェクトは gdal_python_driver.BaseLayer
を継承するクラスからの1つまたは複数のオブジェクトをインスタンス化します.
メタデータ, およびその他の定義
以下の属性は必須であり, __init__ 時に定義する必要があります:
- name
レイヤー名, 文字列型. 設定されていない場合,
name
メソッドを定義する必要があります.
- fields
フィールド定義のシーケンス(空にすることもできます). 各フィールドは以下のプロパティを持つ辞書です:
- name
必須
- type
ogr.OFT_ (SWIG Pythonバインディングから), または以下の文字列値のいずれか:
String
,Integer
,Integer16
,Integer64
,Boolean
,Real
,Float
,Binary
,Date
,Time
,DateTime
のいずれか
その属性が設定されていない場合,
fields
メソッドを定義し,そのようなシーケンスを返す必要があります.
- geometry_fields
ジオメトリフィールド定義のシーケンス(空にすることもできます). 各フィールドは以下のプロパティを持つ辞書です:
- name
必須. 空にすることもできます
- type
必須. ogr.wkb_ (SWIG Pythonバインディングから), または以下の文字列値のいずれか:
Unknown
,Point
,LineString
,Polygon
,MultiPoint
,MultiLineString
,MultiPolygon
,GeometryCollections
またはOGRGeometryTypeToName()
で返される他のすべての値
- srs
ジオメトリフィールドに添付されたSRSは,
OGRSpatialReference::SetFromUserInput()
によって取り込むことができる文字列として, PROJ文字列, WKT文字列,AUTHORITY:CODE
などがあります.
その属性が設定されていない場合,
geometry_fields
メソッドを定義し,そのようなシーケンスを返す必要があります.
以下の属性はオプションです:
- fid_name
地物ID列名, 文字列型. 空の文字列にすることもできます. 設定されていない場合,
fid_name
メソッドを定義することができます.
- metadata
デフォルトのメタデータドメインのメタデータに対応する key: value 文字列の辞書. あるいは,ドメイン引数を受け入れる
metadata
メソッドを定義することができます.
- iterator_honour_attribute_filter
レイヤーに設定できる
attribute_filter
属性を考慮する場合, True に設定できます.
- iterator_honour_spatial_filter
レイヤーに設定できる
spatial_filter
属性を考慮する場合, True に設定できます.
- feature_count_honour_attribute_filter
レイヤーに設定できる
attribute_filter
属性を考慮する場合, True に設定できます.
- feature_count_honour_spatial_filter
レイヤーに設定できる
spatial_filter
属性を考慮する場合, True に設定できます.
地物イテレータ
レイヤークラスはイテレータインターフェースを実装する必要があります. 通常は __iter__
メソッドを使用します.
生成されたイテレータは,各地物の内容に対して辞書を生成する必要があります. 返された辞書で許可されるキーは次のとおりです:
- id
強く推奨されます. 値はFIDとして認識される整数である必要があります.
- type
必須. 値は文字列
"OGRFeature"
である必要があります.
- fields
必須. 値はフィールド名であるキーを持つ辞書か, None である必要があります.
- geometry_fields
必須. 値はジオメトリフィールド名(名前のないジオメトリ列の場合は空の文字列)であるキーを持つ辞書か, None である必要があります.
各キーの値は, WKT文字列としてエンコードされたジオメトリ; ISO WKBとしてエンコードされた`bytes-like object <https://docs.python.org/3/glossary.html#term-bytes-like-object>`__; または None である必要があります.
- style
オプション. 値は 地物スタイル仕様 に準拠する文字列である必要があります.
フィルタリング
デフォルトでは, OGR APIのユーザーによって設定された属性フィルタまたは空間フィルタは,レイヤーのすべての地物を反復処理することで, ドライバーの一般的なC++側によって評価されます.
レイヤーオブジェクトの iterator_honour_attribute_filter
(resp. iterator_honour_spatial_filter
) 属性が True
に設定されている場合, 属性フィルタ(resp. 空間フィルタ)は地物イテレータメソッドによって尊重される必要があります.
属性フィルタはレイヤーオブジェクトの attribute_filter
属性に設定されます. これは OGR SQL に準拠する文字列です. 属性フィルタがOGR APIによって変更されると, attribute_filter_changed
オプションメソッドが呼び出されます(オプションメソッドについては以下のパラグラフを参照してください). attribute_filter_changed
の実装は, SetAttributeFilter
メソッドを呼び出すことで, ドライバーの一般的なC++側による評価にフォールバックすることを決定できます(以下のパススルー例を参照してください)
ジオメトリフィルタはレイヤーオブジェクトの spatial_filter
属性に設定されます. これはISO WKTとしてエンコードされた文字列です. レイヤーのCRSで表現することがOGR APIのユーザーの責任です. 属性フィルタがOGR APIによって変更されると, spatial_filter_changed
オプションメソッドが呼び出されます(オプションメソッドについては以下のパラグラフを参照してください). spatial_filter_changed
の実装は, SetSpatialFilter
メソッドを呼び出すことで,ドライバーの一般的なC++側による評価にフォールバックすることを決定できます(以下のパススルー例を参照してください)
オプションのメソッド
以下のメソッドはオプションで実装することができます:
- extent(self, force_computation)
- 戻り値:
レイヤーの空間範囲のリスト [xmin,ymin,xmax,ymax]
- feature_count(self, force_computation)
- 戻り値:
レイヤーの地物数
self.feature_count_honour_attribute_filter または self.feature_count_honour_spatial_filter がTrue に設定されている場合, 属性フィルタおよび/または空間フィルタはこのメソッドによって尊重される必要があります.
- feature_by_id(self, fid)
- パラメータ:
fid (int) -- 地物ID
- 戻り値:
__next__
メソッドの形式のいずれかで地物オブジェクト, またはfidに一致するオブジェクトがない場合はNone
- attribute_filter_changed(self)
このメソッドは, self.attribute_filter が変更されたときに呼び出されます. ドライバーは, self.iterator_honour_attribute_filter または feature_count_honour_attribute_filter 属性の値を変更する可能性があります.
- spatial_filter_changed(self)
このメソッドは, self.spatial_filter が変更されたときに呼び出されます(その値はWKTでエンコードされたジオメトリです). ドライバーは, self.iterator_honour_spatial_filter または feature_count_honour_spatial_filter 属性の値を変更する可能性があります.
- test_capability(self, cap)
- パラメータ:
string (cap) -- 可能な値は BaseLayer.FastGetExtent, BaseLayer.FastSpatialFilter, BaseLayer.FastFeatureCount, BaseLayer.RandomRead, BaseLayer.StringsAsUTF8 または
OGRLayer::TestCapability()
でサポートされている他の文字列です.- 戻り値:
機能がサポートされている場合はTrue, それ以外はFalseです.
完全な例
以下の例は, SWIG Python GDAL API への呼び出しを転送するパススルードライバーです. 実用的な用途はなく, APIのほとんどの使用例を示すことを目的としています. 実際のドライバーは, デモンストレーションされたAPIの一部のみを使用します. たとえば, パススルードライバーは属性フィルタと空間フィルタを完全にダミーの方法で実装し, ドライバーのC++部分にコールバックします. iterator_honour_attribute_filter
および iterator_honour_spatial_filter
属性, attribute_filter_changed
および spatial_filter_changed
メソッドの実装は, 同じ結果で省略することができます.
ドライバーによって認識される接続文字列は PASSHTROUGH:connection_string_supported_by_non_python_drivers
です. ドライバー名の接頭辞は絶対的な要件ではなく, この特定のドライバーに固有のものであり, ある程度人工的です(接頭辞がない場合, 接続文字列は直接ネイティブドライバーに移動します). Other examples の段落で言及されているCityJSONドライバーはそれを必要としません
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This code is in the public domain, so as to serve as a template for
# real-world plugins.
# or, at the choice of the licensee,
# Copyright 2019 Even Rouault
# SPDX-License-Identifier: MIT
# gdal: DRIVER_NAME = "PASSTHROUGH"
# API version(s) supported. Must include 1 currently
# gdal: DRIVER_SUPPORTED_API_VERSION = [1]
# gdal: DRIVER_DCAP_VECTOR = "YES"
# gdal: DRIVER_DMD_LONGNAME = "Passthrough driver"
# gdal: DRIVER_DMD_CONNECTION_PREFIX = "PASSTHROUGH:"
from osgeo import gdal, ogr
from gdal_python_driver import BaseDriver, BaseDataset, BaseLayer
class Layer(BaseLayer):
def __init__(self, gdal_layer):
self.gdal_layer = gdal_layer
self.name = gdal_layer.GetName()
self.fid_name = gdal_layer.GetFIDColumn()
self.metadata = gdal_layer.GetMetadata_Dict()
self.iterator_honour_attribute_filter = True
self.iterator_honour_spatial_filter = True
self.feature_count_honour_attribute_filter = True
self.feature_count_honour_spatial_filter = True
def fields(self):
res = []
layer_defn = self.gdal_layer.GetLayerDefn()
for i in range(layer_defn.GetFieldCount()):
ogr_field_def = layer_defn.GetFieldDefn(i)
field_def = {"name": ogr_field_def.GetName(),
"type": ogr_field_def.GetType()}
res.append(field_def)
return res
def geometry_fields(self):
res = []
layer_defn = self.gdal_layer.GetLayerDefn()
for i in range(layer_defn.GetGeomFieldCount()):
ogr_field_def = layer_defn.GetGeomFieldDefn(i)
field_def = {"name": ogr_field_def.GetName(),
"type": ogr_field_def.GetType()}
srs = ogr_field_def.GetSpatialRef()
if srs:
field_def["srs"] = srs.ExportToWkt()
res.append(field_def)
return res
def test_capability(self, cap):
if cap in (BaseLayer.FastGetExtent, BaseLayer.StringsAsUTF8,
BaseLayer.RandomRead, BaseLayer.FastFeatureCount):
return self.gdal_layer.TestCapability(cap)
return False
def extent(self, force_computation):
# Impedance mismatch between SWIG GetExtent() and the Python
# driver API
minx, maxx, miny, maxy = self.gdal_layer.GetExtent(force_computation)
return [minx, miny, maxx, maxy]
def feature_count(self, force_computation):
# Dummy implementation: we call back the generic C++ implementation
return self.gdal_layer.GetFeatureCount(True)
def attribute_filter_changed(self):
# Dummy implementation: we call back the generic C++ implementation
if self.attribute_filter:
self.gdal_layer.SetAttributeFilter(str(self.attribute_filter))
else:
self.gdal_layer.SetAttributeFilter(None)
def spatial_filter_changed(self):
# Dummy implementation: we call back the generic C++ implementation
# the 'inf' test is just for a test_ogrsf oddity
if self.spatial_filter and 'inf' not in self.spatial_filter:
self.gdal_layer.SetSpatialFilter(
ogr.CreateGeometryFromWkt(self.spatial_filter))
else:
self.gdal_layer.SetSpatialFilter(None)
def _translate_feature(self, ogr_f):
fields = {}
layer_defn = ogr_f.GetDefnRef()
for i in range(ogr_f.GetFieldCount()):
if ogr_f.IsFieldSet(i):
fields[layer_defn.GetFieldDefn(i).GetName()] = ogr_f.GetField(i)
geom_fields = {}
for i in range(ogr_f.GetGeomFieldCount()):
g = ogr_f.GetGeomFieldRef(i)
if g:
geom_fields[layer_defn.GetGeomFieldDefn(
i).GetName()] = g.ExportToIsoWKb()
return {'id': ogr_f.GetFID(),
'type': 'OGRFeature',
'style': ogr_f.GetStyleString(),
'fields': fields,
'geometry_fields': geom_fields}
def __iter__(self):
for f in self.gdal_layer:
yield self._translate_feature(f)
def feature_by_id(self, fid):
ogr_f = self.gdal_layer.GetFeature(fid)
if not ogr_f:
return None
return self._translate_feature(ogr_f)
class Dataset(BaseDataset):
def __init__(self, gdal_ds):
self.gdal_ds = gdal_ds
self.layers = [Layer(gdal_ds.GetLayer(idx))
for idx in range(gdal_ds.GetLayerCount())]
self.metadata = gdal_ds.GetMetadata_Dict()
def close(self):
del self.gdal_ds
self.gdal_ds = None
class Driver(BaseDriver):
def _identify(self, filename):
prefix = 'PASSTHROUGH:'
if not filename.startswith(prefix):
return None
return gdal.OpenEx(filename[len(prefix):], gdal.OF_VECTOR)
def identify(self, filename, first_bytes, open_flags, open_options={}):
return self._identify(filename) is not None
def open(self, filename, first_bytes, open_flags, open_options={}):
gdal_ds = self._identify(filename)
if not gdal_ds:
return None
return Dataset(gdal_ds)
その他の例
CityJSONドライバーを含むその他の例は, https://github.com/OSGeo/gdal/tree/master/examples/pydrivers で見つけることができます.