class GeoRuby::Shp4r::ShpFile

An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf). Currently supports only the reading of geometries.

Attributes

file_length[R]
file_root[R]
mmax[R]
mmin[R]
record_count[R]
shp_type[R]
xmax[R]
xmin[R]
ymax[R]
ymin[R]
zmax[R]
zmin[R]

Public Class Methods

create(file,shp_type,fields,&proc) click to toggle source

create a new Shapefile of the specified shp type (see ShpType) and with the attribute specified in the fields array (see Dbf::Field). If a block is given, the ShpFile object newly created is passed to it.

# File lib/geo_ruby/shp4r/shp.rb, line 64
def self.create(file,shp_type,fields,&proc)
  file_root = file.gsub(/.shp$/i,"")
  shx_io = File.open(file_root + ".shx","wb")
  shp_io = File.open(file_root + ".shp","wb")
  dbf_io = File.open(file_root + ".dbf","wb")
  str = [9994,0,0,0,0,0,50,1000,shp_type,0,0,0,0,0,0,0,0].pack("N7V2E8")
  shp_io << str
  shx_io << str
  rec_length = 1 + fields.inject(0) {|s,f| s + f.length} #+1 for the prefixed space (active record marker)

  dbf_io << [3,107,7,7,0,33 + 32 * fields.length,rec_length ].pack("c4Vv2x20") #32 bytes for first part of header

  fields.each do |field|
    dbf_io << [field.name,field.type,field.length,field.decimal].pack("a10xax4CCx14")
  end
  dbf_io << ['0d'].pack("H2")
  
  shx_io.close
  shp_io.close
  dbf_io.close

  open(file,&proc)

end
new(file) click to toggle source

Opens a SHP file. Both “abc.shp” and “abc” are accepted. The files “abc.shp”, “abc.shx” and “abc.dbf” must be present

# File lib/geo_ruby/shp4r/shp.rb, line 33
def initialize(file)
  #strip the shp out of the file if present

  @file_root = file.gsub(/.shp$/i,"")
  #check existence of shp, dbf and shx files       

  unless File.exists?(@file_root + ".shp") and File.exists?(@file_root + ".dbf") and File.exists?(@file_root + ".shx")
    raise MalformedShpException.new("Missing one of shp, dbf or shx for: #{@file}")
  end

  @dbf = Dbf::Reader.open(@file_root + ".dbf")
  @shx = File.open(@file_root + ".shx","rb")
  @shp = File.open(@file_root + ".shp","rb")
  read_index
end
open(file) { |shpfile| ... } click to toggle source

opens a SHP “file”. If a block is given, the ShpFile object is yielded to it and is closed upon return. Else a call to open is equivalent to ShpFile.new(...).

# File lib/geo_ruby/shp4r/shp.rb, line 53
def self.open(file)
  shpfile = ShpFile.new(file)
  if block_given?
    yield shpfile
    shpfile.close
  else
    shpfile
  end
end

Public Instance Methods

[](i) click to toggle source

Returns record i

# File lib/geo_ruby/shp4r/shp.rb, line 128
def [](i)
  get_record(i)
end
close() click to toggle source

Closes a shapefile

# File lib/geo_ruby/shp4r/shp.rb, line 88
def close
  @dbf.close
  @shx.close
  @shp.close
end
each() { |get_record(i)| ... } click to toggle source

Goes through each record

# File lib/geo_ruby/shp4r/shp.rb, line 120
def each
  (0...record_count).each do |i|
    yield get_record(i)
  end
end
Also aliased as: each_record
each_record()
Alias for: each
empty?() click to toggle source

Tests if the file has no record

# File lib/geo_ruby/shp4r/shp.rb, line 115
def empty?
  record_count == 0
end
fields() click to toggle source

return the description of data fields

# File lib/geo_ruby/shp4r/shp.rb, line 110
def fields
  @dbf.fields
end
records() click to toggle source

Returns all the records

# File lib/geo_ruby/shp4r/shp.rb, line 133
def records
  Array.new(record_count) do |i|
    get_record(i)
  end
end
reload!() click to toggle source

force the reopening of the files compsing the shp. Close before calling this.

# File lib/geo_ruby/shp4r/shp.rb, line 48
def reload!
  initialize(@file_root)
end
transaction() { |trs| ... } click to toggle source

starts a transaction, to buffer physical file operations on the shapefile components.

# File lib/geo_ruby/shp4r/shp.rb, line 95
def transaction
  trs = ShpTransaction.new(self,@dbf)
  if block_given?
    answer = yield trs
    if answer == :rollback
      trs.rollback
    elsif !trs.rollbacked
      trs.commit
    end
  else
    trs
  end
end

Private Instance Methods

get_record(i) click to toggle source

TODO : refactor to minimize redundant code

# File lib/geo_ruby/shp4r/shp.rb, line 153
def get_record(i)
  return nil if record_count <= i or i < 0
  dbf_record = @dbf.record(i)
  @shx.seek(100 + 8 * i) #100 is the header length

  offset,length = @shx.read(8).unpack("N2")
  @shp.seek(offset * 2 + 8)
  rec_shp_type = @shp.read(4).unpack("V")[0]

  case(rec_shp_type)
  when ShpType::POINT
    x, y = @shp.read(16).unpack("E2")
    geometry = GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
  when ShpType::POLYLINE #actually creates a multi_polyline

    @shp.seek(32,IO::SEEK_CUR) #extent 

    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    points = Array.new(num_points) do
      x, y = @shp.read(16).unpack("E2")
      GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
    end
    line_strings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])])
    end
    geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings)
  when ShpType::POLYGON
    #TODO : TO CORRECT

    #does not take into account the possibility that the outer loop could be after the inner loops in the SHP + more than one outer loop

    #Still sends back a multi polygon (so the correction above won't change what gets sent back)

    @shp.seek(32,IO::SEEK_CUR)
    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    points = Array.new(num_points) do 
      x, y = @shp.read(16).unpack("E2")
      GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
    end
    linear_rings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])])
    end
    geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)])
  when ShpType::MULTIPOINT
    @shp.seek(32,IO::SEEK_CUR)
    num_points = @shp.read(4).unpack("V")[0]
    points = Array.new(num_points) do
      x, y = @shp.read(16).unpack("E2")
      GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
    end
    geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points)


  when ShpType::POINTZ
    x, y, z, m = @shp.read(24).unpack("E4")
    geometry = GeoRuby::SimpleFeatures::Point.from_x_y_z_m(x,y,z,m)


  when ShpType::POLYLINEZ
    @shp.seek(32,IO::SEEK_CUR)
    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)
    zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    @shp.seek(16,IO::SEEK_CUR)
    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    points = Array.new(num_points) do |i|
      GeoRuby::SimpleFeatures::Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
    end
    line_strings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
    end
    geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings,GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)

    
  when ShpType::POLYGONZ
    #TODO : CORRECT


    @shp.seek(32,IO::SEEK_CUR)#extent 

    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)#extent 

    zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    @shp.seek(16,IO::SEEK_CUR)#extent 

    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    points = Array.new(num_points) do |i|
      Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
    end
    linear_rings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
    end
    geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)


  when ShpType::MULTIPOINTZ
    @shp.seek(32,IO::SEEK_CUR)
    num_points = @shp.read(4).unpack("V")[0]
    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)
    zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    @shp.seek(16,IO::SEEK_CUR)
    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    
    points = Array.new(num_points) do |i|
      Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
    end
    
    geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points,GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)

  when ShpType::POINTM
    x, y, m = @shp.read(24).unpack("E3")
    geometry = GeoRuby::SimpleFeatures::Point.from_x_y_m(x,y,m)

  when ShpType::POLYLINEM
    @shp.seek(32,IO::SEEK_CUR)
    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)
    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    points = Array.new(num_points) do |i|
      Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
    end
    line_strings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
    end
    geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings,GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)

    
  when ShpType::POLYGONM
    #TODO : CORRECT


    @shp.seek(32,IO::SEEK_CUR)
    num_parts, num_points = @shp.read(8).unpack("V2")
    parts =  @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
    parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1

    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)
    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    points = Array.new(num_points) do |i|
      Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
    end
    linear_rings = Array.new(num_parts) do |i|
      GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
    end
    geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)


  when ShpType::MULTIPOINTM
    @shp.seek(32,IO::SEEK_CUR)
    num_points = @shp.read(4).unpack("V")[0]
    xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
    @shp.seek(16,IO::SEEK_CUR)
    ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
    
    points = Array.new(num_points) do |i|
      Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
    end
    
    geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points,GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
  else
    geometry = nil
  end
  
  ShpRecord.new(geometry,dbf_record)
end
read_index() click to toggle source
# File lib/geo_ruby/shp4r/shp.rb, line 140
def read_index
  @file_length, @shp_type, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = @shx.read(100).unpack("x24Nx4VE8")
  @record_count = (@file_length - 50) / 4
  if @record_count == 0 
    #initialize the bboxes to default values so if data added, they will be replaced

    @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax =  Float::MAX, Float::MAX, -Float::MAX, -Float::MAX, Float::MAX, -Float::MAX, Float::MAX, -Float::MAX
  end
  unless @record_count == @dbf.record_count
    raise MalformedShpException.new("Not the same number of records in SHP and DBF")
  end
end