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 へのマッピングがちょっと気持ち悪かったのですが、このことについては後で書こうと思います。)

JRuby + Geotools
285秒
CRuby + Rjb + Geotools
128秒

CRuby+Rjb+GeotoolsJRuby+Geotools に比べて2倍くらい早い場合があることになります。引用した過去のエントリの内容を考え合わせると、CRuby + Rjb で気軽に Geotools を使うと、CRuby で苦労して OGR を使うのとだいたい同じ程度の速度が出ることが期待できるかもしれないということになります。
使い慣れた CRuby で作業ができる点と、速度はそこそこである程度安定しているという点から、今後は CRuby + Rjb + Geotools を主に使っていきたいと思っています。
Rjb は、RubyGems でインストールできるので、以下のステップで簡単に試していただけることになります。

  1. Ruby + RubyGems をインストール
  2. Rjb をインストール
  3. Java をインストール
  4. Geotools で配布される jar ファイルを lib/ext に入れるなりする

しかも、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

次に、JRuby + Geotools 版の実装です:

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 さんのところなどを参照されると良いと思います。