#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