#An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf). Currently supports only the reading of geometries. class GeoRuby::Shp4r::ShpFile attr_reader :shp_type, :record_count, :xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax, :file_root, :file_length include Enumerable #Opens a SHP file. Both "abc.shp" and "abc" are accepted. The files "abc.shp", "abc.shx" and "abc.dbf" must be present 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 = ::GeoRuby::Shp4r::Dbf::Reader.open(@file_root + ".dbf") @shx = File.open(@file_root + ".shx","rb") @shp = File.open(@file_root + ".shp","rb") read_index end #force the reopening of the files compsing the shp. Close before calling this. def reload! initialize(@file_root) 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(...). def self.open(file) shpfile = ShpFile.new(file) if block_given? yield shpfile shpfile.close else shpfile end end #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. 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 #Closes a shapefile def close @dbf.close @shx.close @shp.close end #starts a transaction, to buffer physical file operations on the shapefile components. 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 #return the description of data fields def fields @dbf.fields end #Tests if the file has no record def empty? record_count == 0 end #Goes through each record def each (0...record_count).each do |i| yield get_record(i) end end alias :each_record :each #Returns record +i+ def [](i) get_record(i) end #Returns all the records def records Array.new(record_count) do |i| get_record(i) end end private 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 #TODO : refactor to minimize redundant code 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 end