ベクタードライバーの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インタプリタへのリンクメカニズム

See Linking mechanism to a Python interpreter

ドライバーの場所

ドライバーファイル名は 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 で見つけることができます.