GML版基盤地図情報のRubyハッシュへのコンバータ

GML基盤地図情報Rubyハッシュへのコンバータを作りました。
d:id:hfu:20080401 の続きとして、道路縁だけでなくすべての基盤地図情報地物に対応し、ダウンロードした ZIP ファイルを含むフォルダを直接解釈できるようにし、地物ごとにハッシュを yield するコンバータを作りました。
あまり趣味的なことをすることが道義的に許される状況にないのですが、お世話になっている方々にもあるいは役にも立つかも知れないし、そうでない方々にも役立つかも知れないということで、作成かつ発信させていただくことにしました。個人的な動機は、基盤地図情報と一緒に配布されているコンバータが Mac OS X で高速には動作させにくく、出力が Shapefile であり、その属性名が日本語であって扱いにくいことから、Mac OS X で軽快に動作する単純なコンバータが欲しかったからです。また、そのコンバータを使って基盤地図情報の調べ物の敷居を低くしたかったからです。

このコンバータの先に geotools.rb を付けても良いですし、sequel 経由で PostGIS に入れても良いと思っています。Ruby でも JRuby でも動作します。

実装

# Usage of the works is permitted provided that this insturment is retained
# with the works, so that any entity that uses the works is notified of this
# instrument.
# 
# DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.

require 'find'
require 'rexml/document'
require 'rexml/streamlistener'
require 'rubygems'
require 'zip/zipfilesystem'

class Parser
  include REXML::StreamListener
  ATTR_NAMES = %w{fid lfSpanFr lfSpanTo devDate orgGILvl orgMDId name type admCode alti}
  GEOM_NAMES = %w{pos loc area}
  FEATURE_TYPES = %w{AdmArea AdmBdry BldA BldL CommBdry CommPt CstLine ElevPt RailCL RdCompt RdEdg WL}
  #INTERIOR_OR_EXTERIOR = %w{gml:interior gml:exterior}

  def initialize
    @mode = 'default'
    #@interior_or_exterior = 'default'
    @feat = Hash.new {|h, k| h[k] = ''}
  end

  def tag_start(name, attrs)
   if FEATURE_TYPES.include? name
     @feat = {:feature_type => name.to_sym}
   elsif ATTR_NAMES.include? name
     @mode = name
   elsif GEOM_NAMES.include? name
     @mode = name
     @wkt = case @mode
     when 'pos':
       'POINT ('
     when 'loc':
       'LINESTRING ('
     when 'area':
       'POLYGON ('
     end
   #elsif INTERIOR_OR_EXTERIOR.include? name
   #  @interior_or_exterior = name
   end
  end

  def tag_end(name)
    if FEATURE_TYPES.include? name
      @block.call(@feat)
    elsif ATTR_NAMES.include? name
      @mode = 'default'
    elsif GEOM_NAMES.include? name
      @wkt.chop!.chop! if @mode == 'area'
      @wkt += ')'
      @feat[:the_geom] = @wkt
      @mode = 'default'
    #elsif INTERIOR_OR_EXTERIOR.include? name
    #  @interior_or_exterior = 'default'
    end
  end

  def text(text)
    return if @mode == 'default'
    text.chomp!
    size = text.size
    return if size == 0
    if ATTR_NAMES.include? @mode
      @feat[@mode.to_sym] = text
    elsif GEOM_NAMES.include? @mode
      geometry(text)
    end
  end

  def geometry(posList)
    @wkt += '(' if @mode == 'area'
    posList.split("\n").each {|s|
      coords = s.chomp.split(' ')
      next unless coords.size == 2
      @wkt += "#{coords[1]} #{coords[0]}, "
    }
    @wkt.chop!.chop!
    @wkt += '), ' if @mode == 'area'
  end

  def process_zip_directory(src_dir, &block)
    Find.find(src_dir) {|path|
      next unless /zip$/.match path
      Zip::ZipFile.open(path) {|zipfile|
        zipfile.dir.foreach('/') {|f|
          next unless /xml$/.match f
          next unless /GML/.match f
          process_stream(zipfile.file.new(f)) {|feat|
            yield feat
          }
        }
      }
    }
  end

  def process_stream(stream, &block)
    @block = block
    REXML::Document.parse_stream(stream, self)
  end
end

if __FILE__ == $0
  Parser.new.process_zip_directory('../../../src/fgd/2500/') {|feat| 
    p feat
  }
end

現在のところ、ダウンロード提供されていることが確認できている基盤地図情報項目にのみ対応していますが、クラス定義開始の直下にある定数を、http://fgd.gsi.go.jp/spec/2008/FGD_DLFileSpecV2.0.pdf を参考にするなりして増強すれば、スキーマのみ公開されているその他の基盤地図情報項目にも対応できることになるはずです。

ポリゴンについて、現在 exterior を解釈しているのか interior を解釈しているのかが分かるようにしていましたが、基盤地図情報GML インスタンスでは、必ず exterior が1回、最初に出現する(これは、WKT の exterior/interior 出現パターンと同一である)ようでしたので、exterior/interior 判断の部分はコメントアウトしました。用途によってはコメントアウトを外すこともあるかもしれません。