Rjb 1.0.9 に対応しました

概要

Java 側のメソッドで戻り値の型が java.lang.Object になっている場合には、 java.lang.String のオブジェクトがそのまま、 Ruby の String に変換されることなく返却されることが分かりました。

http://arton.no-ip.info/diary/20070915.html#c08

と arton さんに報告したところ、下記の通り、早くも Rjb 1.0.9 がリリースされました。

Rjb-1.0.9。

戻り値の型がjava.lang.Objectかつ実際の型がjava.lang.StringかつRjb::primitive_conversionが真の場合に、RubyのString型のオブジェクトに変換するようにしました。

その他、

http://arton.no-ip.info/diary/20070923.html#p01

Rjb 1.0.9 を実際に使ってみたところ、報告した Rjb - JRuby 間の相違が解消されたことが確認できました。
また、geotools.rb でこのリリースへの対応を行ったところ、多少の速度向上 (計測誤差の範囲内かもしれません) も見られました。

アップデート

Rjb 1.0.9 は、すでに RubyGems に入っているので、Mac OS X でも以下のように簡単にアップデートできました。

$ export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home
$ sudo gem install rjb
Password:
Select which gem to install for your platform (i686-darwin8.9.1)
 1. rjb 1.0.9 (ruby)
 2. rjb 1.0.9 (mswin32)
 3. rjb 1.0.8 (mswin32)
 4. rjb 1.0.8 (ruby)
 5. Skip this gem
 6. Cancel installation
> 1
Building native extensions.  This could take a while...
Successfully installed rjb-1.0.9

テスト

アップデート後、以下のコード:

require 'rjb'
ArrayList = Rjb::import('java.util.ArrayList')
l = ArrayList.new
l.add('string')
def show(o) print "#{o.inspect} (#{o.respond_to?('_invoke') ? 'Java::' + o.getClass.getName : 'Ruby::' + o.class.to_s})\n" end
show(l.get(0))
Rjb::primitive_conversion = true
show(l.get(0))

を実行したところ、

$ $ ruby list_test2.rb
#<#<Class:0x1149898>:0x114845c> (Java::java.lang.String)
"string" (Ruby::String)

となり、java.lang.Object 型として返される java.lang.String のインスタンスRuby の String に型変換されることを確認しました。

geotools.rb での速度向上

geotools.rb では、Rjb 1.0.9 リリース前には、java.lang.String のインスタンスを自分のコード内で変換していました:

52:              if attr.respond_to?('_invoke') # java.lang.String を変換
53:                attr = attr.toString if attr.getClass == Geo::Tools::String
54:              end # 今のところ、これで遅くなっている

Rjb 1.0.9 のリリースを受け、Rjb 1.0.9 (以降)が使われている場合にはこの処理を行わないよう、geotools.rb を書き換えました。
上記3行の処理を行わないようにしたことで、処理時間がどのくらい変わるかを確認してみました。確認のためのコードは:

require 'geotools'

start_time = Time.now
Geo::Reader.foreach('../transl_1_1.shp') do |geom, attrs| end
print "#{Time.now - start_time}s\n"

といったものです。

速度確認結果
pritive_conversion = true で String を自動型変換 (for Rjb 1.0.9) 63秒
primitive_conversion を使うが、String を手動型変換 (for Rjb 1.0.7 - 1.0.8) 66秒
geotools.rb レベルで型変換 (for - Rjb 1.0.6) 107秒
JRuby 85秒

現在の geotools.rb の実装

現在の geotools. rb の実装は、以下のようになっています。

# this code is under development and subject to major change.
require 'iconv'

module Geo
  # Geo::Tools module, to include nesessary classes from Geotools
  module Tools
    QUALIFIED_NAMES = %w{java.lang.System java.lang.String java.lang.Integer java.lang.Double java.lang.Long java.io.File java.util.HashMap org.geotools.data.shapefile.ShapefileDataStore org.geotools.feature.AttributeTypeFactory org.geotools.feature.FeatureTypeBuilder org.geotools.feature.type.GeometricAttributeType com.vividsolutions.jts.io.WKTReader org.geotools.referencing.crs.EPSGCRSAuthorityFactory org.geotools.referencing.operation.DefaultCoordinateOperationFactory org.geotools.geometry.DirectPosition2D org.geotools.data.vpf.file.VPFFile org.geotools.data.vpf.VPFLibrary org.geotools.gce.geotiff.GeoTiffWriter org.geotools.gce.geotiff.GeoTiffWriteParams org.geotools.coverage.grid.GridCoverageFactory com.vividsolutions.jts.geom.Envelope org.geotools.geometry.Envelope2D org.geotools.factory.Hints org.geotools.geometry.jts.JTS org.geotools.filter.text.cql2.CQL}
    begin
      require 'rjb'
      QUALIFIED_NAMES.each do |qn|
        sn = qn.split('.').last
        module_eval "#{sn} = Rjb::import('#{qn}')"
      end
      IMPLEMENTATION = 'rjb'
    rescue LoadError
      require 'java'
      QUALIFIED_NAMES.each do |qn|
        include_class qn
      end
      IMPLEMENTATION = 'java'
    end
  end

  # Geo module variables
  @@wkt_reader = nil
  @@epsg_crs_authority_factory = nil

  # Geo module 'good-wrapper' / 'Grossklasstum' classes
  class Reader
    if Tools::IMPLEMENTATION == 'java'
      print "DEBUG: jruby mode\n"
      def iterate
        while(@iter.hasNext)
          feat = @iter.next
          attrs = {}
          @attr_names.each do |attr_name|
            attrs[attr_name] = feat.getAttribute(attr_name)
          end
          yield feat.getDefaultGeometry, attrs
        end
      end
    else
      begin
        Rjb::primitive_conversion = true
        print "DEBUG: rjb primitive_conversion mode\n"
        if Rjb::VERSION == '1.0.7' or Rjb::VERSION == '1.0.8'
          print "DEBUG: Rjb 1.0.7/1.0.8 mode. Install 1.0.9 for better performance.\n"
          def iterate
            while(@iter.hasNext)
              feat = @iter.next
              attrs = {}
              @attr_names.each do |attr_name|
                attr = feat.getAttribute(attr_name)
                if attr.respond_to?('_invoke')
                  attr = attr.toString if attr.getClass == Geo::Tools::String
                end
                attrs[attr_name] = attr
              end
              yield feat.getDefaultGeometry, attrs
            end
          end
        else
          def iterate
            while(@iter.hasNext)
              feat = @iter.next
              attrs = {}
              @attr_names.each do |attr_name|
                attr = feat.getAttribute(attr_name)
                attrs[attr_name] = attr
              end
              yield feat.getDefaultGeometry, attrs
            end
          end
        end
      rescue NoMethodError
        print "DEBUG: rjb conventional mode\n"
        def iterate
          while(@iter.hasNext)
            feat = @iter.next
            attrs = {}
            @attr_names.each do |attr_name|
              attr = feat.getAttribute(attr_name)
              if attr.getClass.equals(Tools::Integer)
                attr = attr.intValue
              elsif attr.getClass.equals(Tools::Double)
                attr = attr.doubleValue
              elsif attr.getClass.equals(Tools::String)
                if @sjis_workaround
                  attr = Iconv.conv('UTF-8', 'Shift_JIS', attr.getBytes('iso-8859-1'))
                else
                  attr = attr.toString
                end
              elsif attr.getClass.equals(Tools::Long)
                attr = attr.longValue
              end
              attrs[attr_name] = attr
            end
            yield feat.getDefaultGeometry, attrs
          end
        end
      end
    end

    # keys for options: :sjis_workaround (boolean), :whitelist (array)
    # :bbox (array [xmin, ymin, xmax, ymax])
    def Reader::foreach(shapefile, options = {})
      r = Reader.new(shapefile, options)
      r.iterate do |geom, attrs|
        yield geom, attrs
      end
      r.close
    end

    def initialize(shapefile, options = {})
      @sjis_workaround = options[:sjis_workaround]
      @sjis_workaround = false if @sjis_workaround == nil
      if(Tools::IMPLEMENTATION == 'java' && @sjis_workaround)
        raise "sjis_workaround for JRuby is not implemented."
      end
      store = Tools::ShapefileDataStore.new(Tools::File.new(shapefile).toURL)
      if options.has_key?(:bbox)
        b = options[:bbox]
        f = Geo::Tools::CQL::toFilter("BBOX(the_geom, #{b[0]}, #{b[1]}, #{b[2]}, #{b[3]})") # TODO: NONE and ALL are warned to be already initialized constants [Rjb]
        @iter = store.getFeatureSource.getFeatures(f).features
      else
        @iter = store.getFeatureSource.getFeatures.features
      end
      feat_type = store.getFeatureSource.getSchema
      if options[:whitelist] == nil
        @attr_names = []
        feat_type.getAttributeCount.times do |i|
          name = feat_type.getAttributeType(i).getName
          @attr_names << name unless name == 'the_geom'
        end
      else
        @attr_names = options[:whitelist]
        #TODO: whitelist checking necessary?
      end
    end

    def close
      @iter.close
    end
  end

  class Writer
    def Writer::open(shapefile)
      w = Writer.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')
      ftb = Tools::FeatureTypeBuilder.newInstance(@shapefile)
      attrs.each do |key, value|
        if value.methods.include?('_classname')
          attr_class = value.getClass
        elsif value.class == String
          attr_class = Tools::String
        elsif value.class == Fixnum
          attr_class = Tools::Integer
        elsif value.class == Float
          attr_class = Tools::Double
        else
          raise "attribute #{key} has unrecognizable class #{value.class}"
        end
        ftb.addType(Tools::AttributeTypeFactory.newAttributeType(key, attr_class))
      end
      if geom.class == String
        geom = import_wkt_geometry(geom)
      end
      ftb.setDefaultGeometry(Tools::GeometricAttributeType.new('the_geom', geom.getClass, true, nil, nil, nil))
      ft = ftb.getFeatureType
      store = Tools::ShapefileDataStore.new(Tools::File.new(@shapefile).toURL)
      store.createSchema(ft)
      @writer = store.getFeatureWriter(@shapefile, store.getFeatureSource(@shapefile).getTransaction)
      @first = false
    end
    private :setup

    def write(geom, attrs = {})
      setup(geom, attrs) if @first
      feat = @writer.next
      if geom.class == String
        geom = import_wkt_geometry(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

  class Transform
    def initialize(src_crs, dst_crs)
      cof = Geo::Tools::DefaultCoordinateOperationFactory.new
      @co = cof.createOperation(src_crs, dst_crs)
      @mt = @co.getMathTransform
    end

    def transform(x, y) # z?
      r = Geo::Tools::DirectPosition2D.new
      @mt.transform(Geo::Tools::DirectPosition2D.new(x, y), r)
      return r.x, r.y
    end

    def transform(geom)
      Geo::Tools::JTS::transform(geom, @mt)
    end

    ## TODO: accessor to @mt or @co
  end

  # create GridCoverage of buf which just fits bbox
  def Geo::create_grid_coverage(buf, crs, bbox)
    Geo::Tools::System::setProperty('com.apple.eawt.CocoaComponent.CompatibilityMode', 'false')
    f = Tools::GridCoverageFactory.new
    n_pix_x = buf[0].size
    n_pix_y = buf.size
    env = Tools::Envelope2D.new(crs, 
                                bbox[0] - (bbox[2] - bbox[0]) / n_pix_x / 2,
                                bbox[1] + (bbox[3] - bbox[1]) / n_pix_y / 2,
                                bbox[2] - bbox[0], bbox[3] - bbox[1])
    if Tools::IMPLEMENTATION == 'java'
      f.create('', buf.to_java(:float), env)
    else
      f._invoke('create', 'Ljava.lang.CharSequence;[[FLorg.opengis.geometry.Envelope;', '', buf, env)
    end
  end

  def Geo::write_grid_coverage(coverage, filename)
    w = Tools::GeoTiffWriter.new(Tools::File.new(filename))
    # p = Tools::GeoTiffWriteParams.new
    # p.setCompressionMode(Tools::GeoTiffWriteParams.MODE_EXPLICIT)
    # p.setCompressionType('LZW')
    # p.setCompressionQuality(0.75)
    # format = w.getFormat
    ### -> rt.java problem: no sun.jdbc.odbc.ee.DataSource for Mac OS X
    ### so, no compression for GeoTiff file
    # params = format.getWriteParameters
    # params.parameter(
    #  format.GEOTOOLS_WRITE_PARAMS.getName.toString).setValue(p)
    # w.write(coverage, params.values.toArray)
    w.write(coverage, nil)
  end

  # Geo module convenient methods
  def Geo::import_wkt_geometry(wkt)
    @@wkt_reader = Geo::Tools::WKTReader.new if @@wkt_reader == nil
    @@wkt_reader.read(wkt)
  end

  def Geo::import_epsg_crs(epsg_code)
    @@epsg_crs_authority_factory = Geo::Tools::EPSGCRSAuthorityFactory.new if @@epsg_crs_authority_factory == nil
    if epsg_code.class == Fixnum
      return @@epsg_crs_authority_factory.createCoordinateReferenceSystem("EPSG:#{epsg_code}")
    elsif epsg_code.class == String
      return @@epsg_crs_authority_factory.createCoordinateReferenceSystem(epsg_code)
    else
      raise "Geo::import_epsg_crs: can not handle epsg_code = #{epsg_code}"
    end
  end

  def Geo::bbox2polygon(b)
    Geo::import_wkt_geometry("POLYGON ((#{b[0]} #{b[1]}, #{b[2]} #{b[1]}, #{b[2]} #{b[3]}, #{b[0]} #{b[3]}, #{b[0]} #{b[1]}))")
  end
  def dms2dec(d, m, s)
    d + m / 60.0 + s / 3600.0
  end

  def dec2dms(dec)
    #TODO
    raise "not implemented."
  end
end

# ad hoc tests
if __FILE__ == $0
  ## TODO: better separate tests as unit tests.
  start_time = Time.now
  Geo::Writer.open('test.shp') do |w|
    Geo::Reader.foreach('transl_1_1.shp', {:whitelist => ['exs', 'soc'], :bbox => [135, 35, 136, 36]}) do |geom, attrs|
      #print "#{attrs.inspect}\n"
      w.write(geom, attrs)
    end
  end
  print "#{Time.now - start_time} sec.\n"
  
  exit # ここから下は別の話題
  ix = Geo::import_epsg_crs(2451)       # EPSG:2451 - 平面直角座標系 IX 系
  wgs84 = Geo::import_epsg_crs(4326)    # EPSG:4326 - WGS84
  
  t = Geo::Transform.new(ix, wgs84)     # IX 系から WGS84 への座標変換器
  t_inv = Geo::Transform.new(wgs84, ix) # WGS84 から IX 系への座標変換器
  
  pt = t.transform(0, 0)                 # IX 系の原点を WGS84 に座標変換
  pt_inv = t_inv.transform(pt[0], pt[1]) # その点を IX 系に戻す。元に戻るか?
  
  print "IX origin is #{pt.inspect} in WGS84\n"
  print "#{pt_inv.inspect} must be (0, 0)\n"
end