lib/geo_ruby/shp4r/shp.rb
require 'date'
require 'fileutils' unless defined?(FileUtils)
module GeoRuby
module Shp4r
# Enumerates all the types of SHP geometries.
# The MULTIPATCH one is the only one not currently supported by Geo_ruby.
module ShpType
NULL_SHAPE = 0
POINT = 1
POLYLINE = 3
POLYGON = 5
MULTIPOINT = 8
POINTZ = 11
POLYLINEZ = 13
POLYGONZ = 15
MULTIPOINTZ = 18
POINTM = 21
POLYLINEM = 23
POLYGONM = 25
MULTIPOINTM = 28
end
# An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf).
# Currently supports only the reading of geometries.
class 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.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
# 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 <tt>open</tt> is equivalent to <tt>ShpFile.new(...)</tt>.
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.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
# 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_method :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
fail MalformedShpException.new('Not the same number of records in SHP and DBF')
end
end
# TODO : refactor to minimize redundant code
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
end
# A SHP record : contains both the geometry and the data fields (from the DBF)
class ShpRecord
attr_reader :geometry, :data
def initialize(geometry, data)
@geometry = geometry
@data = data
end
# Tests if the geometry is a NULL SHAPE
def has_null_shape?
@geometry.nil?
end
end
# An object returned from ShpFile#transaction. Buffers updates to a Shapefile
class ShpTransaction
attr_reader :rollbacked
def initialize(shp, dbf)
@deleted = {}
@added = []
@shp = shp
@dbf = dbf
end
# delete a record. Does not take into account the records added in the current transaction
def delete(i)
fail UnexistantRecordException.new("Invalid index : #{i}") if @shp.record_count <= i
@deleted[i] = true
end
# Update a record. In effect just a delete followed by an add.
def update(i, record)
delete(i)
add(record)
end
# add a ShpRecord at the end
def add(record)
record_type = to_shp_type(record.geometry)
fail IncompatibleGeometryException.new('Incompatible type') unless record_type == @shp.shp_type
@added << record
end
# updates the physical files
def commit
@shp.close
@shp_r = open(@shp.file_root + '.shp', 'rb')
@dbf_r = open(@shp.file_root + '.dbf', 'rb')
@shp_io = open(@shp.file_root + '.shp.tmp.shp', 'wb')
@shx_io = open(@shp.file_root + '.shx.tmp.shx', 'wb')
@dbf_io = open(@shp.file_root + '.dbf.tmp.dbf', 'wb')
index = commit_delete
min_x, max_x, min_y, max_y, min_z, max_z, min_m, max_m = commit_add(index)
commit_finalize(min_x, max_x, min_y, max_y, min_z, max_z, min_m, max_m)
@shp_r.close
@dbf_r.close
@dbf_io.close
@shp_io.close
@shx_io.close
FileUtils.move(@shp.file_root + '.shp.tmp.shp', @shp.file_root + '.shp')
FileUtils.move(@shp.file_root + '.shx.tmp.shx', @shp.file_root + '.shx')
FileUtils.move(@shp.file_root + '.dbf.tmp.dbf', @shp.file_root + '.dbf')
@deleted = {}
@added = []
@shp.reload!
end
# prevents the udpate from taking place
def rollback
@deleted = {}
@added = []
@rollbacked = true
end
private
def geom_type(geom)
case geom
when GeoRuby::SimpleFeatures::Point then 'POINT'
when GeoRuby::SimpleFeatures::LineString then 'POLYLINE'
when GeoRuby::SimpleFeatures::Polygon then 'POLYGON'
when GeoRuby::SimpleFeatures::MultiPoint then 'MULTIPOINT'
when GeoRuby::SimpleFeatures::MultiLineString then 'POLYLINE'
when GeoRuby::SimpleFeatures::MultiPolygon then 'POLYGON'
else false
end
end
def to_shp_type(geom)
return false unless klass = geom_type(geom)
klass += 'Z' if geom.with_z
klass += 'M' if geom.with_m
GeoRuby::Shp4r.const_get('ShpType::' + klass)
end
def commit_add(index)
max_x, min_x, max_y, min_y, max_z, min_z, max_m, min_m = @shp.xmax, @shp.xmin, @shp.ymax, @shp.ymin, @shp.zmax, @shp.zmin, @shp.mmax, @shp.mmin
@added.each do |record|
@dbf_io << ['20'].pack('H2')
@dbf.fields.each do |field|
data = record.data[field.name]
str = if field.type == 'D'
sprintf('%04i%02i%02i', data.year, data.month, data.mday)
elsif field.type == 'L'
data ? 'T' : 'F'
else
data.to_s
end
@dbf_io << [str].pack("A#{field.length}")
end
shp_str, min_xp, max_xp, min_yp, max_yp, min_zp, max_zp, min_mp, max_mp = build_shp_geometry(record.geometry)
max_x = max_xp if max_xp > max_x
min_x = min_xp if min_xp < min_x
max_y = max_yp if max_yp > max_y
min_y = min_yp if min_yp < min_y
max_z = max_zp if max_zp > max_z
min_z = min_zp if min_zp < min_z
max_m = max_mp if max_mp > max_m
min_m = min_mp if min_mp < min_m
length = (shp_str.length / 2 + 2).to_i # num of 16-bit words; geom type is included (+2)
@shx_io << [(@shp_io.pos / 2).to_i, length].pack('N2')
@shp_io << [index, length, @shp.shp_type].pack('N2V')
@shp_io << shp_str
index += 1
end
@shp_io.flush
@shx_io.flush
@dbf_io.flush
[min_x, max_x, min_y, max_y, min_z, max_z, min_m, max_m]
end
def commit_delete
@shp_r.rewind
header = @shp_r.read(100)
@shp_io << header
@shx_io << header
index = 1
until @shp_r.eof?
icur, length = @shp_r.read(8).unpack('N2')
unless @deleted[icur - 1]
@shx_io << [(@shp_io.pos / 2).to_i, length].pack('N2')
@shp_io << [index, length].pack('N2')
@shp_io << @shp_r.read(length * 2)
index += 1
else
@shp_r.seek(length * 2, IO::SEEK_CUR)
end
end
@shp_io.flush
@shx_io.flush
@dbf_r.rewind
@dbf_io << @dbf_r.read(@dbf.header_length)
icur = 0
until @dbf_r.eof?
unless @deleted[icur]
@dbf_io << @dbf_r.read(@dbf.record_length)
else
@dbf_r.seek(@dbf.record_length, IO::SEEK_CUR)
end
icur += 1
end
@dbf_io.flush
index
end
def commit_finalize(min_x, max_x, min_y, max_y, min_z, max_z, min_m, max_m)
# update size in shp and dbf + extent and num records in dbf
@shp_io.seek(0, IO::SEEK_END)
shp_size = @shp_io.pos / 2
@shx_io.seek(0, IO::SEEK_END)
shx_size = @shx_io.pos / 2
@shp_io.seek(24)
@shp_io.write([shp_size].pack('N'))
@shx_io.seek(24)
@shx_io.write([shx_size].pack('N'))
@shp_io.seek(36)
@shx_io.seek(36)
str = [min_x, min_y, max_x, max_y, min_z, max_z, min_m, max_m].pack('E8')
@shp_io.write(str)
@shx_io.write(str)
@dbf_io.seek(4)
@dbf_io.write([@dbf.record_count + @added.length - @deleted.length].pack('V'))
end
def build_shp_geometry(geometry)
m_range = nil
answer =
case @shp.shp_type
when ShpType::POINT
bbox = geometry.bounding_box
[geometry.x, geometry.y].pack('E2')
when ShpType::POLYLINE
str, bbox = create_bbox(geometry)
build_polyline(geometry, str)
when ShpType::POLYGON
str, bbox = create_bbox(geometry)
build_polygon(geometry, str)
when ShpType::MULTIPOINT
str, bbox = create_bbox(geometry)
build_multi_point(geometry, str)
when ShpType::POINTZ
bbox = geometry.bounding_box
[geometry.x, geometry.y, geometry.z, geometry.m].pack('E4')
when ShpType::POLYLINEZ
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_polyline(geometry, str)
build_polyline_zm(geometry, :@z, [bbox[0].z, bbox[1].z], str)
build_polyline_zm(geometry, :@m, m_range, str)
when ShpType::POLYGONZ
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_polygon(geometry, str)
build_polygon_zm(geometry, :@z, [bbox[0].z, bbox[1].z], str)
build_polygon_zm(geometry, :@m, m_range, str)
when ShpType::MULTIPOINTZ
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_multi_point(geometry, str)
build_multi_point_zm(geometry, :@z, [bbox[0].z, bbox[1].z], str)
build_multi_point_zm(geometry, :@m, m_range, str)
when ShpType::POINTM
bbox = geometry.bounding_box
[geometry.x, geometry.y, geometry.m].pack('E3')
when ShpType::POLYLINEM
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_polyline(geometry, str)
build_polyline_zm(geometry, :@m, m_range, str)
when ShpType::POLYGONM
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_polygon(geometry, str)
build_polygon_zm(geometry, :@m, m_range, str)
when ShpType::MULTIPOINTM
str, bbox = create_bbox(geometry)
m_range = geometry.m_range
build_multi_point(geometry, str)
build_multi_point_zm(geometry, :@m, m_range, str)
end
m_range ||= [0, 0]
[answer, bbox[0].x, bbox[1].x, bbox[0].y, bbox[1].y, bbox[0].z || 0, bbox[1].z || 0, m_range[0], m_range[1]]
end
def create_bbox(geometry)
bbox = geometry.bounding_box
[[bbox[0].x, bbox[0].y, bbox[1].x, bbox[1].y].pack('E4'), bbox]
end
def build_polyline(geometry, str)
if geometry.is_a? GeoRuby::SimpleFeatures::LineString
str << [1, geometry.length, 0].pack('V3')
geometry.each do |point|
str << [point.x, point.y].pack('E2')
end
else
# multilinestring
str << [geometry.length, geometry.reduce(0) { |a, e| a + e.length }].pack('V2')
str << geometry.reduce([0]) do |a, e|
a << (a.last + e.length) # last element of the previous array is dropped
end.pack("V#{geometry.length}")
geometry.each do |ls|
ls.each do |point|
str << [point.x, point.y].pack('E2')
end
end
end
str
end
def build_polyline_zm(geometry, zm, range, str)
str << range.pack('E2')
if geometry.is_a? GeoRuby::SimpleFeatures::LineString
geometry.each do |point|
str << [point.instance_variable_get(zm)].pack('E')
end
else
# multilinestring
geometry.each do |ls|
ls.each do |point|
str << [point.instance_variable_get(zm)].pack('E')
end
end
end
str
end
def build_polygon(geometry, str)
if geometry.is_a? GeoRuby::SimpleFeatures::Polygon
str << [geometry.length,
geometry.reduce(0) { |a, e| a + e.length }
].pack('V2')
# last element of the previous array is dropped
str << geometry.reduce([0]) do |a, e|
a << (a.last + e.length)
end.pack("V#{geometry.length}")
geometry.each do |lr|
lr.each do |point|
str << [point.x, point.y].pack('E2')
end
end
else
# multipolygon
num_rings = geometry.reduce(0) { |a, e| a + e.length }
str << [num_rings, geometry.reduce(0) { |l, poly| l + poly.reduce(0) { |l2, lr| l2 + lr.length } }].pack('V2')
# last element of the previous array is dropped
str << geometry.reduce([0]) do |a, e|
e.reduce(a) { |a2, lr| a2 << (a2.last + lr.length) }
end.pack("V#{num_rings}")
geometry.each do |poly|
poly.each do |lr|
lr.each do |point|
str << [point.x, point.y].pack('E2')
end
end
end
end
str
end
def build_polygon_zm(geometry, zm, range, str)
str << range.pack('E2')
if geometry.is_a? GeoRuby::SimpleFeatures::Polygon
geometry.each do |lr|
lr.each do |point|
str << [point.instance_variable_get(zm)].pack('E')
end
end
else
geometry.each do |poly|
poly.each do |lr|
lr.each do |point|
str << [point.instance_variable_get(zm)].pack('E')
end
end
end
end
str
end
def build_multi_point(geometry, str)
str << [geometry.length].pack('V')
geometry.each do |point|
str << [point.x, point.y].pack('E2')
end
str
end
def build_multi_point_zm(geometry, zm, range, str)
str << range.pack('E2')
geometry.each do |point|
str << [point.instance_variable_get(zm)].pack('E')
end
str
end
end
class MalformedShpException < StandardError
end
class UnexistantRecordException < StandardError
end
class IncompatibleGeometryException < StandardError
end
class IncompatibleDataException < StandardError
end
end
end