CRuby + Rjb + Geotools は JRuby + Geotools に比べて2倍くらい早い場合がある
JRuby + Geotools は CRuby + OGR に比べて処理時間が2倍程度になる場合がある
http://d.hatena.ne.jp/hfu/20070902/1188757700
で JRuby を試した後、Rjb のことを思い出す機会があり、作っていた Shapefile 用 FeatureReader / FeatureWriter を JRuby から CRuby + Rjb に移植しました。
処理速度の違いの概観をつかむべく最小限のテストをしてみました。レコード数が結構多い、ある Shapefile を「読んでそのまま書く」のに要した時間は、以下のようになりました。(ここで、Geotools による ruby Float (Java double) の Shapefile DBF へのマッピングがちょっと気持ち悪かったのですが、このことについては後で書こうと思います。)
CRuby+Rjb+Geotools は JRuby+Geotools に比べて2倍くらい早い場合があることになります。引用した過去のエントリの内容を考え合わせると、CRuby + Rjb で気軽に Geotools を使うと、CRuby で苦労して OGR を使うのとだいたい同じ程度の速度が出ることが期待できるかもしれないということになります。
使い慣れた CRuby で作業ができる点と、速度はそこそこである程度安定しているという点から、今後は CRuby + Rjb + Geotools を主に使っていきたいと思っています。
Rjb は、RubyGems でインストールできるので、以下のステップで簡単に試していただけることになります。
しかも、Windows では、ステップ 1, 2 はActiveScriptRubyをインストールしていただくという1ステップで済みます。(Mac では Java がインストール済みですのでステップ3が不要です。つまり、多くのプラットフォームで3ステップで行けるということになります。)
結構 Geotools 関係の検索からのアクセスをいただいていることもあり、Ruby on Geotools の記事もこれから少し入れていきたいと考えています。
せっかくなので、JRuby + Geotools と CRuby + Rjb + Geotools 用に作成した FeatureReader / Writer のソースを下に貼り付けます。
下のプログラムは、どちらも同じインタフェースを持ち、以下のようなコードで利用されます:
FeatureWriter::open(dst_filename) do |w| FeatureReader::each(src_filename) do |geom, attrs| # 普通はここでいろいろな処理をする w.write(geom, attrs) end end
- ここで、geom は幾何データです。JTS の Geometry クラスのインスタンスが戻ります。私の用途では OGR 起源のコード混ぜる機会があったので、Geometry の WKT 表現から JTS の Geometry クラスのインスタンスを作成するメソッドとして、FeatureWriter のインスタンスメソッドとして import_from_wkt を用意しています。w.import_from_wkt('POINT(0 0)') などとすることができます。
- ここで、attrs は属性データです。Ruby のハッシュを使っています。FeatureWriter でデータを書き出す際には、最初に書き出すデータで Shapefile のスキーマを決定します。最初に渡すデータにキーがなかった属性は、無視されることになります(が、そのような書き込みはしない場合が、少なくとも8割は占めると判断しました)。また、Shapefile DBF 上での属性項目の順番は、属性名(ハッシュのキー)をソートした順番となるようにしています。(それで十分な場合が少なくとも8割を占めると判断しました。)
以下は CRuby + Rjb + Geotools 版の実装です:
require 'rjb' class FeatureReader module Java File = Rjb::import('java.io.File') ShapefileDataStore = Rjb::import('org.geotools.data.shapefile.ShapefileDataStore') end def FeatureReader::each(shapefile) store = Java::ShapefileDataStore.new(Java::File.new(shapefile).toURL) iter = store.getFeatureSource.getFeatures.features feat_type = store.getFeatureSource.getSchema attr_names = [] feat_type.getAttributeCount.times do |i| attr_names << feat_type.getAttributeType(i).getName end while(iter.hasNext) feat = iter.next attrs = {} feat.getNumberOfAttributes.times do |i| attrs[attr_names[i]] = feat.getAttribute(i) end attrs.delete('the_geom') yield feat.getDefaultGeometry, attrs end iter.close end end class FeatureWriter module Java File = Rjb::import('java.io.File') ShapefileDataStore = Rjb::import('org.geotools.data.shapefile.ShapefileDataStore') AttributeTypeFactory = Rjb::import('org.geotools.feature.AttributeTypeFactory') FeatureTypeBuilder = Rjb::import('org.geotools.feature.FeatureTypeBuilder') GeometricAttributeType = Rjb::import('org.geotools.feature.type.GeometricAttributeType') String = Rjb::import('java.lang.String') Integer = Rjb::import('java.lang.Integer') Double = Rjb::import('java.lang.Double') WKTReader = Rjb::import('com.vividsolutions.jts.io.WKTReader') end def FeatureWriter::open(shapefile) w = FeatureWriter.new(shapefile) yield w w.close end def initialize(shapefile) @shapefile = shapefile @writer = nil @first = true @reader = Java::WKTReader.new end # WKT を Geometry にパーズ。このクラスの責務にすべきではないかもしれないけど単純さ重視 def import_from_wkt(wkt) @reader.read(wkt) end def setup(geom, attrs) attrs.delete('the_geom') ftb = Java::FeatureTypeBuilder.newInstance(@shapefile) attrs.each do |key, value| if value.methods.include?('_classname') attr_class = value.getClass elsif value.class == String attr_class = Java::String elsif value.class == Fixnum attr_class = Java::Integer elsif value.class == Float attr_class = Java::Double else raise "attribute #{key} has unrecognizable class #{value.class}" end ftb.addType(Java::AttributeTypeFactory.newAttributeType( key, attr_class)) end if geom.class == String geom = import_from_wkt(geom) end ftb.setDefaultGeometry(Java::GeometricAttributeType.new('the_geom', geom.getClass, true, nil, nil, nil)) ft = ftb.getFeatureType store = Java::ShapefileDataStore.new(Java::File.new(@shapefile).toURL) store.createSchema(ft) @writer = store.getFeatureWriter(@shapefile, store.getFeatureSource(@shapefile).getTransaction) @first = false end private :setup # ハッシュのキーに the_geom と設定することはできない。たぶん Geotools でもそう。 def write(geom, attrs) setup(geom, attrs) if @first feat = @writer.next if geom.class == String geom = import_from_wkt(geom) end feat.setDefaultGeometry(geom) attrs.each do |key, value| feat.setAttribute(key, value) end @writer.write end def close @writer.close unless @writer == nil end end
require 'java' class FeatureReader module Java include_class 'java.io.File' include_class 'org.geotools.data.shapefile.ShapefileDataStore' end def FeatureReader::each(shapefile) store = Java::ShapefileDataStore.new(Java::File.new(shapefile).toURL) iter = store.getFeatureSource.getFeatures.features feat_type = store.getFeatureSource.getSchema attr_names = [] feat_type.getAttributeCount.times do |i| attr_names << feat_type.getAttributeType(i).getName end while(iter.hasNext) feat = iter.next attrs = {} feat.getNumberOfAttributes.times do |i| attrs[attr_names[i]] = feat.getAttribute(i) end attrs.delete('the_geom') yield feat.getDefaultGeometry, attrs end iter.close end end class FeatureWriter module Java include_class 'java.io.File' include_class 'org.geotools.data.shapefile.ShapefileDataStore' include_class 'org.geotools.feature.AttributeTypeFactory' include_class 'org.geotools.feature.AttributeType' include_class 'org.geotools.feature.FeatureTypes' include_class 'java.lang.String' include_class 'java.lang.Integer' include_class 'java.lang.Double' end def FeatureWriter::open(shapefile) w = FeatureWriter.new(shapefile) yield w w.close end def initialize(shapefile) @shapefile = shapefile @writer = nil @first = true end def setup(geom, attrs) attrs.delete('the_geom') attr_types = Java::AttributeType[attrs.size].new attr_keys = attrs.keys.sort attr_keys.size.times do |i| key = attr_keys[i] value = attrs[key] if value.class == String attr_class = Java::String elsif value.class == Fixnum attr_class = Java::Integer elsif value.class == Float attr_class = Java::Double else raise "#{value.class} for #{key} is not supported." end attr_types[i] = Java::AttributeTypeFactory.newAttributeType(key, attr_class) end ft = Java::FeatureTypes.newFeatureType(attr_types, @shapefile, nil, false, nil, Java::AttributeTypeFactory.newAttributeType( 'the_geom', geom.class)) store = Java::ShapefileDataStore.new(Java::File.new(@shapefile).toURL) store.createSchema(ft) @writer = store.getFeatureWriter(@shapefile, store.getFeatureSource(@shapefile).getTransaction) @first = false end private :setup # ハッシュのキーに the_geom と設定することはできない。たぶん Geotools でもそう。 def write(geom, attrs) setup(geom, attrs) if @first feat = @writer.next feat.setDefaultGeometry(geom) attrs.each do |key, value| feat.setAttribute(key, value) end @writer.write end def close @writer.close unless @writer == nil end end
なお、属性に日本語文字列を使用する場合には、場合によっては文字コード関係の工夫が必要です。Blue blue glass moon さんのところなどを参照されると良いと思います。