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 72
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.reduce(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 34
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.exist?(@file_root + '.shp') &&
         File.exist?(@file_root + '.dbf') &&
         File.exist?(@file_root + '.shx')
    fail 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 59
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 136
def [](i)
  get_record(i)
end
close() click to toggle source

Closes a shapefile

# File lib/geo_ruby/shp4r/shp.rb, line 95
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 128
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 123
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 118
def fields
  @dbf.fields
end
records() click to toggle source

Returns all the records

# File lib/geo_ruby/shp4r/shp.rb, line 141
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 52
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 103
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(index) click to toggle source

TODO : refactor to minimize redundant code

# File lib/geo_ruby/shp4r/shp.rb, line 162
def get_record(index)
  return nil if record_count <= index || index < 0
  dbf_record = @dbf.record(index)
  @shx.seek(100 + 8 * index) # 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)])
    outer, inner = linear_rings.partition(&:clockwise?)

    # Make polygons from the outer rings so we can concatenate
    # them with inner rings.
    outer.map! { |ring| GeoRuby::SimpleFeatures::Polygon.from_linear_rings([ring]) }

    # We make the assumption that all vertices of holes are
    # entirely contained.
    inner.each do |inner_ring|
      outer_poly = outer.find { |outer_poly| outer_poly[0].contains_point?(inner_ring[0]) }
      if outer_poly
        outer_poly << inner_ring
      else
        # TODO - what to do here?  technically the geometry is
        # not well formed (or our above assumption does not
        # hold).
        $stderr.puts 'Failed to find polygon for inner ring!'
      end
    end

    geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons(outer)
  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|
      GeoRuby::SimpleFeatures::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|
      GeoRuby::SimpleFeatures::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|
      GeoRuby::SimpleFeatures::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|
      GeoRuby::SimpleFeatures::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|
      GeoRuby::SimpleFeatures::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 149
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
    fail MalformedShpException.new('Not the same number of records in SHP and DBF')
  end
end