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
Public Class Methods
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
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
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
Returns record i
# File lib/geo_ruby/shp4r/shp.rb, line 136 def [](i) get_record(i) end
Closes a shapefile
# File lib/geo_ruby/shp4r/shp.rb, line 95 def close @dbf.close @shx.close @shp.close end
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
Tests if the file has no record
# File lib/geo_ruby/shp4r/shp.rb, line 123 def empty? record_count == 0 end
return the description of data fields
# File lib/geo_ruby/shp4r/shp.rb, line 118 def fields @dbf.fields end
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
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
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
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
# 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