nofxx-georuby 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/LICENSE +21 -0
- data/README.txt +59 -0
- data/Rakefile +49 -0
- data/VERSION.yml +4 -0
- data/lib/geo_ruby.rb +21 -0
- data/lib/geo_ruby/base/envelope.rb +167 -0
- data/lib/geo_ruby/base/ewkb_parser.rb +216 -0
- data/lib/geo_ruby/base/ewkt_parser.rb +336 -0
- data/lib/geo_ruby/base/geometry.rb +234 -0
- data/lib/geo_ruby/base/geometry_collection.rb +136 -0
- data/lib/geo_ruby/base/geometry_factory.rb +81 -0
- data/lib/geo_ruby/base/georss_parser.rb +135 -0
- data/lib/geo_ruby/base/helper.rb +18 -0
- data/lib/geo_ruby/base/line_string.rb +184 -0
- data/lib/geo_ruby/base/linear_ring.rb +12 -0
- data/lib/geo_ruby/base/multi_line_string.rb +39 -0
- data/lib/geo_ruby/base/multi_point.rb +41 -0
- data/lib/geo_ruby/base/multi_polygon.rb +37 -0
- data/lib/geo_ruby/base/point.rb +310 -0
- data/lib/geo_ruby/base/polygon.rb +150 -0
- data/lib/geo_ruby/shp4r/dbf.rb +180 -0
- data/lib/geo_ruby/shp4r/shp.rb +701 -0
- data/spec/data/multipoint.dbf +0 -0
- data/spec/data/multipoint.shp +0 -0
- data/spec/data/multipoint.shx +0 -0
- data/spec/data/point.dbf +0 -0
- data/spec/data/point.shp +0 -0
- data/spec/data/point.shx +0 -0
- data/spec/data/polygon.dbf +0 -0
- data/spec/data/polygon.shp +0 -0
- data/spec/data/polygon.shx +0 -0
- data/spec/data/polyline.dbf +0 -0
- data/spec/data/polyline.shp +0 -0
- data/spec/data/polyline.shx +0 -0
- data/spec/geo_ruby/base/envelope_spec.rb +45 -0
- data/spec/geo_ruby/base/ewkb_parser_spec.rb +158 -0
- data/spec/geo_ruby/base/ewkt_parser_spec.rb +179 -0
- data/spec/geo_ruby/base/geometry_collection_spec.rb +55 -0
- data/spec/geo_ruby/base/geometry_factory_spec.rb +11 -0
- data/spec/geo_ruby/base/geometry_spec.rb +32 -0
- data/spec/geo_ruby/base/georss_parser_spec.rb +218 -0
- data/spec/geo_ruby/base/line_string_spec.rb +208 -0
- data/spec/geo_ruby/base/linear_ring_spec.rb +14 -0
- data/spec/geo_ruby/base/multi_line_string_spec.rb +35 -0
- data/spec/geo_ruby/base/multi_point_spec.rb +29 -0
- data/spec/geo_ruby/base/multi_polygon_spec.rb +35 -0
- data/spec/geo_ruby/base/point_spec.rb +275 -0
- data/spec/geo_ruby/base/polygon_spec.rb +108 -0
- data/spec/geo_ruby/shp4r/shp_spec.rb +238 -0
- data/spec/geo_ruby_spec.rb +27 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +12 -0
- metadata +123 -0
@@ -0,0 +1,180 @@
|
|
1
|
+
# Copyright 2006 Keith Morrison (http://infused.org)
|
2
|
+
# Modified version of his DBF library (http://rubyforge.org/projects/dbf/)
|
3
|
+
|
4
|
+
module GeoRuby
|
5
|
+
module Shp4r
|
6
|
+
module Dbf
|
7
|
+
|
8
|
+
DBF_HEADER_SIZE = 32
|
9
|
+
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
|
10
|
+
VERSION_DESCRIPTIONS = {
|
11
|
+
"02" => "FoxBase",
|
12
|
+
"03" => "dBase III without memo file",
|
13
|
+
"04" => "dBase IV without memo file",
|
14
|
+
"05" => "dBase V without memo file",
|
15
|
+
"30" => "Visual FoxPro",
|
16
|
+
"31" => "Visual FoxPro with AutoIncrement field",
|
17
|
+
"7b" => "dBase IV with memo file",
|
18
|
+
"83" => "dBase III with memo file",
|
19
|
+
"8b" => "dBase IV with memo file",
|
20
|
+
"8e" => "dBase IV with SQL table",
|
21
|
+
"f5" => "FoxPro with memo file",
|
22
|
+
"fb" => "FoxPro without memo file"
|
23
|
+
}
|
24
|
+
|
25
|
+
class DBFError < StandardError; end
|
26
|
+
class UnpackError < DBFError; end
|
27
|
+
|
28
|
+
class Reader
|
29
|
+
attr_reader :field_count
|
30
|
+
attr_reader :fields
|
31
|
+
attr_reader :record_count
|
32
|
+
attr_reader :version
|
33
|
+
attr_reader :last_updated
|
34
|
+
attr_reader :header_length
|
35
|
+
attr_reader :record_length
|
36
|
+
|
37
|
+
def initialize(file)
|
38
|
+
@data_file = File.open(file, 'rb')
|
39
|
+
reload!
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.open(file)
|
43
|
+
reader = Reader.new(file)
|
44
|
+
if block_given?
|
45
|
+
yield reader
|
46
|
+
reader.close
|
47
|
+
else
|
48
|
+
reader
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
@data_file.close
|
54
|
+
end
|
55
|
+
|
56
|
+
def reload!
|
57
|
+
get_header_info
|
58
|
+
get_field_descriptors
|
59
|
+
end
|
60
|
+
|
61
|
+
def field(field_name)
|
62
|
+
@fields.detect {|f| f.name == field_name.to_s}
|
63
|
+
end
|
64
|
+
|
65
|
+
# An array of all the records contained in the database file
|
66
|
+
def records
|
67
|
+
seek_to_record(0)
|
68
|
+
@records ||= Array.new(@record_count) do |i|
|
69
|
+
if active_record?
|
70
|
+
build_record
|
71
|
+
else
|
72
|
+
seek_to_record(i + 1)
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
alias_method :rows, :records
|
78
|
+
|
79
|
+
# Jump to record
|
80
|
+
def record(index)
|
81
|
+
seek_to_record(index)
|
82
|
+
active_record? ? build_record : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :row, :record
|
86
|
+
|
87
|
+
def version_description
|
88
|
+
VERSION_DESCRIPTIONS[version]
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def active_record?
|
94
|
+
@data_file.read(1).unpack('H2').to_s == '20' rescue false
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_record
|
98
|
+
record = DbfRecord.new
|
99
|
+
@fields.each do |field|
|
100
|
+
case field.type
|
101
|
+
when 'N'
|
102
|
+
record[field.name] = unpack_integer(field) rescue nil
|
103
|
+
when 'F'
|
104
|
+
record[field.name] = unpack_float(field) rescue nil
|
105
|
+
when 'D'
|
106
|
+
raw = unpack_string(field).to_s.strip
|
107
|
+
unless raw.empty?
|
108
|
+
begin
|
109
|
+
record[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
|
110
|
+
rescue
|
111
|
+
record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when 'L'
|
115
|
+
record[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false rescue false
|
116
|
+
when 'C'
|
117
|
+
record[field.name] = unpack_string(field).strip
|
118
|
+
else
|
119
|
+
record[field.name] = unpack_string(field)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
record
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_header_info
|
126
|
+
@data_file.rewind
|
127
|
+
@version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
|
128
|
+
@field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_field_descriptors
|
132
|
+
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4CC'))}
|
133
|
+
end
|
134
|
+
|
135
|
+
def seek(offset)
|
136
|
+
@data_file.seek(@header_length + offset)
|
137
|
+
end
|
138
|
+
|
139
|
+
def seek_to_record(index)
|
140
|
+
seek(@record_length * index)
|
141
|
+
end
|
142
|
+
|
143
|
+
def unpack_field(field)
|
144
|
+
@data_file.read(field.length).unpack("a#{field.length}")
|
145
|
+
end
|
146
|
+
|
147
|
+
def unpack_string(field)
|
148
|
+
unpack_field(field).to_s
|
149
|
+
end
|
150
|
+
|
151
|
+
def unpack_integer(field)
|
152
|
+
unpack_string(field).to_i
|
153
|
+
end
|
154
|
+
|
155
|
+
def unpack_float(field)
|
156
|
+
unpack_string(field).to_f
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
class FieldError < StandardError; end
|
162
|
+
|
163
|
+
class Field
|
164
|
+
attr_reader :name, :type, :length, :decimal
|
165
|
+
|
166
|
+
def initialize(name, type, length, decimal = 0)
|
167
|
+
raise FieldError, "field length must be greater than 0" unless length > 0
|
168
|
+
if type == 'N' and decimal != 0
|
169
|
+
type = 'F'
|
170
|
+
end
|
171
|
+
@name, @type, @length, @decimal = name.strip, type,length, decimal
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class DbfRecord < Hash
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,701 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'fileutils' if !defined?(FileUtils)
|
3
|
+
require File.dirname(__FILE__) + '/dbf'
|
4
|
+
|
5
|
+
|
6
|
+
module GeoRuby
|
7
|
+
module Shp4r
|
8
|
+
|
9
|
+
#Enumerates all the types of SHP geometries. The MULTIPATCH one is the only one not currently supported by Geo_ruby.
|
10
|
+
module ShpType
|
11
|
+
NULL_SHAPE = 0
|
12
|
+
POINT = 1
|
13
|
+
POLYLINE = 3
|
14
|
+
POLYGON = 5
|
15
|
+
MULTIPOINT = 8
|
16
|
+
POINTZ = 11
|
17
|
+
POLYLINEZ = 13
|
18
|
+
POLYGONZ = 15
|
19
|
+
MULTIPOINTZ = 18
|
20
|
+
POINTM = 21
|
21
|
+
POLYLINEM = 23
|
22
|
+
POLYGONM = 25
|
23
|
+
MULTIPOINTM = 28
|
24
|
+
end
|
25
|
+
|
26
|
+
#An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf). Currently supports only the reading of geometries.
|
27
|
+
class ShpFile
|
28
|
+
attr_reader :shp_type, :record_count, :xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax, :file_root, :file_length
|
29
|
+
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
#Opens a SHP file. Both "abc.shp" and "abc" are accepted. The files "abc.shp", "abc.shx" and "abc.dbf" must be present
|
33
|
+
def initialize(file)
|
34
|
+
#strip the shp out of the file if present
|
35
|
+
@file_root = file.gsub(/.shp$/i,"")
|
36
|
+
#check existence of shp, dbf and shx files
|
37
|
+
unless File.exists?(@file_root + ".shp") and File.exists?(@file_root + ".dbf") and File.exists?(@file_root + ".shx")
|
38
|
+
raise MalformedShpException.new("Missing one of shp, dbf or shx for: #{@file}")
|
39
|
+
end
|
40
|
+
|
41
|
+
@dbf = Dbf::Reader.open(@file_root + ".dbf")
|
42
|
+
@shx = File.open(@file_root + ".shx","rb")
|
43
|
+
@shp = File.open(@file_root + ".shp","rb")
|
44
|
+
read_index
|
45
|
+
end
|
46
|
+
|
47
|
+
#force the reopening of the files compsing the shp. Close before calling this.
|
48
|
+
def reload!
|
49
|
+
initialize(@file_root)
|
50
|
+
end
|
51
|
+
|
52
|
+
#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>.
|
53
|
+
def self.open(file)
|
54
|
+
shpfile = ShpFile.new(file)
|
55
|
+
if block_given?
|
56
|
+
yield shpfile
|
57
|
+
shpfile.close
|
58
|
+
else
|
59
|
+
shpfile
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#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.
|
64
|
+
def self.create(file,shp_type,fields,&proc)
|
65
|
+
file_root = file.gsub(/.shp$/i,"")
|
66
|
+
shx_io = File.open(file_root + ".shx","wb")
|
67
|
+
shp_io = File.open(file_root + ".shp","wb")
|
68
|
+
dbf_io = File.open(file_root + ".dbf","wb")
|
69
|
+
str = [9994,0,0,0,0,0,50,1000,shp_type,0,0,0,0,0,0,0,0].pack("N7V2E8")
|
70
|
+
shp_io << str
|
71
|
+
shx_io << str
|
72
|
+
rec_length = 1 + fields.inject(0) {|s,f| s + f.length} #+1 for the prefixed space (active record marker)
|
73
|
+
dbf_io << [3,107,7,7,0,33 + 32 * fields.length,rec_length ].pack("c4Vv2x20") #32 bytes for first part of header
|
74
|
+
fields.each do |field|
|
75
|
+
dbf_io << [field.name,field.type,field.length,field.decimal].pack("a10xax4CCx14")
|
76
|
+
end
|
77
|
+
dbf_io << ['0d'].pack("H2")
|
78
|
+
|
79
|
+
shx_io.close
|
80
|
+
shp_io.close
|
81
|
+
dbf_io.close
|
82
|
+
|
83
|
+
open(file,&proc)
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
#Closes a shapefile
|
88
|
+
def close
|
89
|
+
@dbf.close
|
90
|
+
@shx.close
|
91
|
+
@shp.close
|
92
|
+
end
|
93
|
+
|
94
|
+
#starts a transaction, to buffer physical file operations on the shapefile components.
|
95
|
+
def transaction
|
96
|
+
trs = ShpTransaction.new(self,@dbf)
|
97
|
+
if block_given?
|
98
|
+
answer = yield trs
|
99
|
+
if answer == :rollback
|
100
|
+
trs.rollback
|
101
|
+
elsif !trs.rollbacked
|
102
|
+
trs.commit
|
103
|
+
end
|
104
|
+
else
|
105
|
+
trs
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
#return the description of data fields
|
110
|
+
def fields
|
111
|
+
@dbf.fields
|
112
|
+
end
|
113
|
+
|
114
|
+
#Tests if the file has no record
|
115
|
+
def empty?
|
116
|
+
record_count == 0
|
117
|
+
end
|
118
|
+
|
119
|
+
#Goes through each record
|
120
|
+
def each
|
121
|
+
(0...record_count).each do |i|
|
122
|
+
yield get_record(i)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
alias :each_record :each
|
126
|
+
|
127
|
+
#Returns record +i+
|
128
|
+
def [](i)
|
129
|
+
get_record(i)
|
130
|
+
end
|
131
|
+
|
132
|
+
#Returns all the records
|
133
|
+
def records
|
134
|
+
Array.new(record_count) do |i|
|
135
|
+
get_record(i)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def read_index
|
141
|
+
@file_length, @shp_type, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = @shx.read(100).unpack("x24Nx4VE8")
|
142
|
+
@record_count = (@file_length - 50) / 4
|
143
|
+
if @record_count == 0
|
144
|
+
#initialize the bboxes to default values so if data added, they will be replaced
|
145
|
+
@xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = Float::MAX, Float::MAX, -Float::MAX, -Float::MAX, Float::MAX, -Float::MAX, Float::MAX, -Float::MAX
|
146
|
+
end
|
147
|
+
unless @record_count == @dbf.record_count
|
148
|
+
raise MalformedShpException.new("Not the same number of records in SHP and DBF")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
#TODO : refactor to minimize redundant code
|
153
|
+
def get_record(i)
|
154
|
+
return nil if record_count <= i or i < 0
|
155
|
+
dbf_record = @dbf.record(i)
|
156
|
+
@shx.seek(100 + 8 * i) #100 is the header length
|
157
|
+
offset,length = @shx.read(8).unpack("N2")
|
158
|
+
@shp.seek(offset * 2 + 8)
|
159
|
+
rec_shp_type = @shp.read(4).unpack("V")[0]
|
160
|
+
|
161
|
+
case(rec_shp_type)
|
162
|
+
when ShpType::POINT
|
163
|
+
x, y = @shp.read(16).unpack("E2")
|
164
|
+
geometry = GeoRuby::Base::Point.from_x_y(x,y)
|
165
|
+
when ShpType::POLYLINE #actually creates a multi_polyline
|
166
|
+
@shp.seek(32,IO::SEEK_CUR) #extent
|
167
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
168
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
169
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
170
|
+
points = Array.new(num_points) do
|
171
|
+
x, y = @shp.read(16).unpack("E2")
|
172
|
+
GeoRuby::Base::Point.from_x_y(x,y)
|
173
|
+
end
|
174
|
+
line_strings = Array.new(num_parts) do |i|
|
175
|
+
GeoRuby::Base::LineString.from_points(points[(parts[i])...(parts[i+1])])
|
176
|
+
end
|
177
|
+
geometry = GeoRuby::Base::MultiLineString.from_line_strings(line_strings)
|
178
|
+
when ShpType::POLYGON
|
179
|
+
#TODO : TO CORRECT
|
180
|
+
#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
|
181
|
+
#Still sends back a multi polygon (so the correction above won't change what gets sent back)
|
182
|
+
@shp.seek(32,IO::SEEK_CUR)
|
183
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
184
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
185
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
186
|
+
points = Array.new(num_points) do
|
187
|
+
x, y = @shp.read(16).unpack("E2")
|
188
|
+
GeoRuby::Base::Point.from_x_y(x,y)
|
189
|
+
end
|
190
|
+
linear_rings = Array.new(num_parts) do |i|
|
191
|
+
GeoRuby::Base::LinearRing.from_points(points[(parts[i])...(parts[i+1])])
|
192
|
+
end
|
193
|
+
geometry = GeoRuby::Base::MultiPolygon.from_polygons([GeoRuby::Base::Polygon.from_linear_rings(linear_rings)])
|
194
|
+
when ShpType::MULTIPOINT
|
195
|
+
@shp.seek(32,IO::SEEK_CUR)
|
196
|
+
num_points = @shp.read(4).unpack("V")[0]
|
197
|
+
points = Array.new(num_points) do
|
198
|
+
x, y = @shp.read(16).unpack("E2")
|
199
|
+
GeoRuby::Base::Point.from_x_y(x,y)
|
200
|
+
end
|
201
|
+
geometry = GeoRuby::Base::MultiPoint.from_points(points)
|
202
|
+
|
203
|
+
|
204
|
+
when ShpType::POINTZ
|
205
|
+
x, y, z, m = @shp.read(24).unpack("E4")
|
206
|
+
geometry = GeoRuby::Base::Point.from_x_y_z_m(x,y,z,m)
|
207
|
+
|
208
|
+
|
209
|
+
when ShpType::POLYLINEZ
|
210
|
+
@shp.seek(32,IO::SEEK_CUR)
|
211
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
212
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
213
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
214
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
215
|
+
@shp.seek(16,IO::SEEK_CUR)
|
216
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
217
|
+
@shp.seek(16,IO::SEEK_CUR)
|
218
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
219
|
+
points = Array.new(num_points) do |i|
|
220
|
+
GeoRuby::Base::Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
221
|
+
end
|
222
|
+
line_strings = Array.new(num_parts) do |i|
|
223
|
+
GeoRuby::Base::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::Base::default_srid,true,true)
|
224
|
+
end
|
225
|
+
geometry = GeoRuby::Base::MultiLineString.from_line_strings(line_strings,GeoRuby::Base::default_srid,true,true)
|
226
|
+
|
227
|
+
|
228
|
+
when ShpType::POLYGONZ
|
229
|
+
#TODO : CORRECT
|
230
|
+
|
231
|
+
@shp.seek(32,IO::SEEK_CUR)#extent
|
232
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
233
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
234
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
235
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
236
|
+
@shp.seek(16,IO::SEEK_CUR)#extent
|
237
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
238
|
+
@shp.seek(16,IO::SEEK_CUR)#extent
|
239
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
240
|
+
points = Array.new(num_points) do |i|
|
241
|
+
Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
242
|
+
end
|
243
|
+
linear_rings = Array.new(num_parts) do |i|
|
244
|
+
GeoRuby::Base::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::Base::default_srid,true,true)
|
245
|
+
end
|
246
|
+
geometry = GeoRuby::Base::MultiPolygon.from_polygons([GeoRuby::Base::Polygon.from_linear_rings(linear_rings)],GeoRuby::Base::default_srid,true,true)
|
247
|
+
|
248
|
+
|
249
|
+
when ShpType::MULTIPOINTZ
|
250
|
+
@shp.seek(32,IO::SEEK_CUR)
|
251
|
+
num_points = @shp.read(4).unpack("V")[0]
|
252
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
253
|
+
@shp.seek(16,IO::SEEK_CUR)
|
254
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
255
|
+
@shp.seek(16,IO::SEEK_CUR)
|
256
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
257
|
+
|
258
|
+
points = Array.new(num_points) do |i|
|
259
|
+
Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
260
|
+
end
|
261
|
+
|
262
|
+
geometry = GeoRuby::Base::MultiPoint.from_points(points,GeoRuby::Base::default_srid,true,true)
|
263
|
+
|
264
|
+
when ShpType::POINTM
|
265
|
+
x, y, m = @shp.read(24).unpack("E3")
|
266
|
+
geometry = GeoRuby::Base::Point.from_x_y_m(x,y,m)
|
267
|
+
|
268
|
+
when ShpType::POLYLINEM
|
269
|
+
@shp.seek(32,IO::SEEK_CUR)
|
270
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
271
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
272
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
273
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
274
|
+
@shp.seek(16,IO::SEEK_CUR)
|
275
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
276
|
+
points = Array.new(num_points) do |i|
|
277
|
+
Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
278
|
+
end
|
279
|
+
line_strings = Array.new(num_parts) do |i|
|
280
|
+
GeoRuby::Base::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::Base::default_srid,false,true)
|
281
|
+
end
|
282
|
+
geometry = GeoRuby::Base::MultiLineString.from_line_strings(line_strings,GeoRuby::Base::default_srid,false,true)
|
283
|
+
|
284
|
+
|
285
|
+
when ShpType::POLYGONM
|
286
|
+
#TODO : CORRECT
|
287
|
+
|
288
|
+
@shp.seek(32,IO::SEEK_CUR)
|
289
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
290
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
291
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
292
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
293
|
+
@shp.seek(16,IO::SEEK_CUR)
|
294
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
295
|
+
points = Array.new(num_points) do |i|
|
296
|
+
Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
297
|
+
end
|
298
|
+
linear_rings = Array.new(num_parts) do |i|
|
299
|
+
GeoRuby::Base::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::Base::default_srid,false,true)
|
300
|
+
end
|
301
|
+
geometry = GeoRuby::Base::MultiPolygon.from_polygons([GeoRuby::Base::Polygon.from_linear_rings(linear_rings)],GeoRuby::Base::default_srid,false,true)
|
302
|
+
|
303
|
+
|
304
|
+
when ShpType::MULTIPOINTM
|
305
|
+
@shp.seek(32,IO::SEEK_CUR)
|
306
|
+
num_points = @shp.read(4).unpack("V")[0]
|
307
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
308
|
+
@shp.seek(16,IO::SEEK_CUR)
|
309
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
310
|
+
|
311
|
+
points = Array.new(num_points) do |i|
|
312
|
+
Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
313
|
+
end
|
314
|
+
|
315
|
+
geometry = GeoRuby::Base::MultiPoint.from_points(points,GeoRuby::Base::default_srid,false,true)
|
316
|
+
else
|
317
|
+
geometry = nil
|
318
|
+
end
|
319
|
+
|
320
|
+
ShpRecord.new(geometry,dbf_record)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
#A SHP record : contains both the geometry and the data fields (from the DBF)
|
325
|
+
class ShpRecord
|
326
|
+
attr_reader :geometry , :data
|
327
|
+
|
328
|
+
def initialize(geometry, data)
|
329
|
+
@geometry = geometry
|
330
|
+
@data = data
|
331
|
+
end
|
332
|
+
|
333
|
+
#Tests if the geometry is a NULL SHAPE
|
334
|
+
def has_null_shape?
|
335
|
+
@geometry.nil?
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
#An object returned from ShpFile#transaction. Buffers updates to a Shapefile
|
340
|
+
class ShpTransaction
|
341
|
+
attr_reader :rollbacked
|
342
|
+
|
343
|
+
def initialize(shp, dbf)
|
344
|
+
@deleted = Hash.new
|
345
|
+
@added = Array.new
|
346
|
+
@shp = shp
|
347
|
+
@dbf = dbf
|
348
|
+
end
|
349
|
+
|
350
|
+
#delete a record. Does not take into account the records added in the current transaction
|
351
|
+
def delete(i)
|
352
|
+
raise UnexistantRecordException.new("Invalid index : #{i}") if @shp.record_count <= i
|
353
|
+
@deleted[i] = true
|
354
|
+
end
|
355
|
+
|
356
|
+
#Update a record. In effect just a delete followed by an add.
|
357
|
+
def update(i, record)
|
358
|
+
delete(i)
|
359
|
+
add(record)
|
360
|
+
end
|
361
|
+
|
362
|
+
#add a ShpRecord at the end
|
363
|
+
def add(record)
|
364
|
+
record_type = to_shp_type(record.geometry)
|
365
|
+
raise IncompatibleGeometryException.new("Incompatible type") unless record_type==@shp.shp_type
|
366
|
+
@added << record
|
367
|
+
end
|
368
|
+
|
369
|
+
#updates the physical files
|
370
|
+
def commit
|
371
|
+
@shp.close
|
372
|
+
@shp_r = open(@shp.file_root + ".shp", "rb")
|
373
|
+
@dbf_r = open(@shp.file_root + ".dbf", "rb")
|
374
|
+
@shp_io = open(@shp.file_root + ".shp.tmp.shp", "wb")
|
375
|
+
@shx_io = open(@shp.file_root + ".shx.tmp.shx", "wb")
|
376
|
+
@dbf_io = open(@shp.file_root + ".dbf.tmp.dbf", "wb")
|
377
|
+
index = commit_delete
|
378
|
+
min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m = commit_add(index)
|
379
|
+
commit_finalize(min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m)
|
380
|
+
@shp_r.close
|
381
|
+
@dbf_r.close
|
382
|
+
@dbf_io.close
|
383
|
+
@shp_io.close
|
384
|
+
@shx_io.close
|
385
|
+
FileUtils.move(@shp.file_root + ".shp.tmp.shp", @shp.file_root + ".shp")
|
386
|
+
FileUtils.move(@shp.file_root + ".shx.tmp.shx", @shp.file_root + ".shx")
|
387
|
+
FileUtils.move(@shp.file_root + ".dbf.tmp.dbf", @shp.file_root + ".dbf")
|
388
|
+
|
389
|
+
@deleted = Hash.new
|
390
|
+
@added = Array.new
|
391
|
+
|
392
|
+
@shp.reload!
|
393
|
+
end
|
394
|
+
|
395
|
+
#prevents the udpate from taking place
|
396
|
+
def rollback
|
397
|
+
@deleted = Hash.new
|
398
|
+
@added = Array.new
|
399
|
+
@rollbacked = true
|
400
|
+
end
|
401
|
+
|
402
|
+
private
|
403
|
+
|
404
|
+
def to_shp_type(geom)
|
405
|
+
root = if geom.is_a? GeoRuby::Base::Point
|
406
|
+
"POINT"
|
407
|
+
elsif geom.is_a? GeoRuby::Base::LineString
|
408
|
+
"POLYLINE"
|
409
|
+
elsif geom.is_a? GeoRuby::Base::Polygon
|
410
|
+
"POLYGON"
|
411
|
+
elsif geom.is_a? GeoRuby::Base::MultiPoint
|
412
|
+
"MULTIPOINT"
|
413
|
+
elsif geom.is_a? GeoRuby::Base::MultiLineString
|
414
|
+
"POLYLINE"
|
415
|
+
elsif geom.is_a? GeoRuby::Base::MultiPolygon
|
416
|
+
"POLYGON"
|
417
|
+
else
|
418
|
+
false
|
419
|
+
end
|
420
|
+
return false if !root
|
421
|
+
|
422
|
+
if geom.with_z
|
423
|
+
root = root + "Z"
|
424
|
+
elsif geom.with_m
|
425
|
+
root = root + "M"
|
426
|
+
end
|
427
|
+
eval "ShpType::" + root
|
428
|
+
end
|
429
|
+
|
430
|
+
def commit_add(index)
|
431
|
+
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
|
432
|
+
@added.each do |record|
|
433
|
+
@dbf_io << ['20'].pack('H2')
|
434
|
+
@dbf.fields.each do |field|
|
435
|
+
data = record.data[field.name]
|
436
|
+
str = if field.type == 'D'
|
437
|
+
sprintf("%04i%02i%02i",data.year,data.month,data.mday)
|
438
|
+
elsif field.type == 'L'
|
439
|
+
if data
|
440
|
+
"T"
|
441
|
+
else
|
442
|
+
"F"
|
443
|
+
end
|
444
|
+
else
|
445
|
+
data.to_s
|
446
|
+
end
|
447
|
+
@dbf_io << [str].pack("A#{field.length}")
|
448
|
+
end
|
449
|
+
|
450
|
+
shp_str,min_xp,max_xp,min_yp,max_yp,min_zp,max_zp,min_mp,max_mp = build_shp_geometry(record.geometry)
|
451
|
+
max_x = max_xp if max_xp > max_x
|
452
|
+
min_x = min_xp if min_xp < min_x
|
453
|
+
max_y = max_yp if max_yp > max_y
|
454
|
+
min_y = min_yp if min_yp < min_y
|
455
|
+
max_z = max_zp if max_zp > max_z
|
456
|
+
min_z = min_zp if min_zp < min_z
|
457
|
+
max_m = max_mp if max_mp > max_m
|
458
|
+
min_m = min_mp if min_mp < min_m
|
459
|
+
length = (shp_str.length/2 + 2).to_i #num of 16-bit words; geom type is included (+2)
|
460
|
+
@shx_io << [(@shp_io.pos/2).to_i,length].pack("N2")
|
461
|
+
@shp_io << [index,length,@shp.shp_type].pack("N2V")
|
462
|
+
@shp_io << shp_str
|
463
|
+
index += 1
|
464
|
+
end
|
465
|
+
@shp_io.flush
|
466
|
+
@shx_io.flush
|
467
|
+
@dbf_io.flush
|
468
|
+
[min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m]
|
469
|
+
end
|
470
|
+
|
471
|
+
def commit_delete
|
472
|
+
@shp_r.rewind
|
473
|
+
header = @shp_r.read(100)
|
474
|
+
@shp_io << header
|
475
|
+
@shx_io << header
|
476
|
+
index = 1
|
477
|
+
while(!@shp_r.eof?)
|
478
|
+
icur,length = @shp_r.read(8).unpack("N2")
|
479
|
+
unless(@deleted[icur-1])
|
480
|
+
@shx_io << [(@shp_io.pos/2).to_i,length].pack("N2")
|
481
|
+
@shp_io << [index,length].pack("N2")
|
482
|
+
@shp_io << @shp_r.read(length * 2)
|
483
|
+
index += 1
|
484
|
+
else
|
485
|
+
@shp_r.seek(length * 2,IO::SEEK_CUR)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
@shp_io.flush
|
489
|
+
@shx_io.flush
|
490
|
+
|
491
|
+
@dbf_r.rewind
|
492
|
+
@dbf_io << @dbf_r.read(@dbf.header_length)
|
493
|
+
icur = 0
|
494
|
+
while(!@dbf_r.eof?)
|
495
|
+
unless(@deleted[icur])
|
496
|
+
@dbf_io << @dbf_r.read(@dbf.record_length)
|
497
|
+
else
|
498
|
+
@dbf_r.seek(@dbf.record_length,IO::SEEK_CUR)
|
499
|
+
end
|
500
|
+
icur += 1
|
501
|
+
end
|
502
|
+
@dbf_io.flush
|
503
|
+
index
|
504
|
+
end
|
505
|
+
|
506
|
+
def commit_finalize(min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m)
|
507
|
+
#update size in shp and dbf + extent and num records in dbf
|
508
|
+
@shp_io.seek(0,IO::SEEK_END)
|
509
|
+
shp_size = @shp_io.pos / 2
|
510
|
+
@shx_io.seek(0,IO::SEEK_END)
|
511
|
+
shx_size= @shx_io.pos / 2
|
512
|
+
@shp_io.seek(24)
|
513
|
+
@shp_io.write([shp_size].pack("N"))
|
514
|
+
@shx_io.seek(24)
|
515
|
+
@shx_io.write([shx_size].pack("N"))
|
516
|
+
@shp_io.seek(36)
|
517
|
+
@shx_io.seek(36)
|
518
|
+
str = [min_x,min_y,max_x,max_y,min_z,max_z,min_m,max_m].pack("E8")
|
519
|
+
@shp_io.write(str)
|
520
|
+
@shx_io.write(str)
|
521
|
+
|
522
|
+
@dbf_io.seek(4)
|
523
|
+
@dbf_io.write([@dbf.record_count + @added.length - @deleted.length].pack("V"))
|
524
|
+
end
|
525
|
+
|
526
|
+
def build_shp_geometry(geometry)
|
527
|
+
m_range = nil
|
528
|
+
answer =
|
529
|
+
case @shp.shp_type
|
530
|
+
when ShpType::POINT
|
531
|
+
bbox = geometry.bounding_box
|
532
|
+
[geometry.x,geometry.y].pack("E2")
|
533
|
+
when ShpType::POLYLINE
|
534
|
+
str,bbox = create_bbox(geometry)
|
535
|
+
build_polyline(geometry,str)
|
536
|
+
when ShpType::POLYGON
|
537
|
+
str,bbox = create_bbox(geometry)
|
538
|
+
build_polygon(geometry,str)
|
539
|
+
when ShpType::MULTIPOINT
|
540
|
+
str,bbox = create_bbox(geometry)
|
541
|
+
build_multi_point(geometry,str)
|
542
|
+
when ShpType::POINTZ
|
543
|
+
bbox = geometry.bounding_box
|
544
|
+
[geometry.x,geometry.y,geometry.z,geometry.m].pack("E4")
|
545
|
+
when ShpType::POLYLINEZ
|
546
|
+
str,bbox = create_bbox(geometry)
|
547
|
+
m_range = geometry.m_range
|
548
|
+
build_polyline(geometry,str)
|
549
|
+
build_polyline_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
550
|
+
build_polyline_zm(geometry,:@m,m_range,str)
|
551
|
+
when ShpType::POLYGONZ
|
552
|
+
str,bbox = create_bbox(geometry)
|
553
|
+
m_range = geometry.m_range
|
554
|
+
build_polygon(geometry,str)
|
555
|
+
build_polygon_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
556
|
+
build_polygon_zm(geometry,:@m,m_range,str)
|
557
|
+
when ShpType::MULTIPOINTZ
|
558
|
+
str,bbox = create_bbox(geometry)
|
559
|
+
m_range = geometry.m_range
|
560
|
+
build_multi_point(geometry,str)
|
561
|
+
build_multi_point_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
562
|
+
build_multi_point_zm(geometry,:@m,m_range,str)
|
563
|
+
when ShpType::POINTM
|
564
|
+
bbox = geometry.bounding_box
|
565
|
+
[geometry.x,geometry.y,geometry.m].pack("E3")
|
566
|
+
when ShpType::POLYLINEM
|
567
|
+
str,bbox = create_bbox(geometry)
|
568
|
+
m_range = geometry.m_range
|
569
|
+
build_polyline(geometry,str)
|
570
|
+
build_polyline_zm(geometry,:@m,m_range,str)
|
571
|
+
when ShpType::POLYGONM
|
572
|
+
str,bbox = create_bbox(geometry)
|
573
|
+
m_range = geometry.m_range
|
574
|
+
build_polygon(geometry,str)
|
575
|
+
build_polygon_zm(geometry,:@m,m_range,str)
|
576
|
+
when ShpType::MULTIPOINTM
|
577
|
+
str,bbox = create_bbox(geometry)
|
578
|
+
m_range = geometry.m_range
|
579
|
+
build_multi_point(geometry,str)
|
580
|
+
build_multi_point_zm(geometry,:@m,m_range,str)
|
581
|
+
end
|
582
|
+
m_range ||= [0,0]
|
583
|
+
[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]]
|
584
|
+
end
|
585
|
+
|
586
|
+
def create_bbox(geometry)
|
587
|
+
bbox = geometry.bounding_box
|
588
|
+
[[bbox[0].x,bbox[0].y,bbox[1].x,bbox[1].y].pack("E4"),bbox]
|
589
|
+
end
|
590
|
+
|
591
|
+
def build_polyline(geometry,str)
|
592
|
+
if geometry.is_a? GeoRuby::Base::LineString
|
593
|
+
str << [1,geometry.length,0].pack("V3")
|
594
|
+
geometry.each do |point|
|
595
|
+
str << [point.x,point.y].pack("E2")
|
596
|
+
end
|
597
|
+
else
|
598
|
+
#multilinestring
|
599
|
+
str << [geometry.length,geometry.inject(0) {|l, ls| l + ls.length}].pack("V2")
|
600
|
+
str << geometry.inject([0]) {|a,ls| a << (a.last + ls.length)}.pack("V#{geometry.length}") #last element of the previous array is dropped
|
601
|
+
geometry.each do |ls|
|
602
|
+
ls.each do |point|
|
603
|
+
str << [point.x,point.y].pack("E2")
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
str
|
608
|
+
end
|
609
|
+
|
610
|
+
def build_polyline_zm(geometry,zm,range,str)
|
611
|
+
str << range.pack("E2")
|
612
|
+
if geometry.is_a? GeoRuby::Base::LineString
|
613
|
+
geometry.each do |point|
|
614
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
615
|
+
end
|
616
|
+
else
|
617
|
+
#multilinestring
|
618
|
+
geometry.each do |ls|
|
619
|
+
ls.each do |point|
|
620
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
str
|
625
|
+
end
|
626
|
+
|
627
|
+
def build_polygon(geometry,str)
|
628
|
+
if geometry.is_a? GeoRuby::Base::Polygon
|
629
|
+
str << [geometry.length,geometry.inject(0) {|l, lr| l + lr.length}].pack("V2")
|
630
|
+
str << geometry.inject([0]) {|a,lr| a << (a.last + lr.length)}.pack("V#{geometry.length}") #last element of the previous array is dropped
|
631
|
+
geometry.each do |lr|
|
632
|
+
lr.each do |point|
|
633
|
+
str << [point.x,point.y].pack("E2")
|
634
|
+
end
|
635
|
+
end
|
636
|
+
else
|
637
|
+
#multipolygon
|
638
|
+
num_rings = geometry.inject(0) {|l, poly| l + poly.length}
|
639
|
+
str << [num_rings, geometry.inject(0) {|l, poly| l + poly.inject(0) {|l2,lr| l2 + lr.length} }].pack("V2")
|
640
|
+
str << geometry.inject([0]) {|a,poly| poly.inject(a) {|a2, lr| a2 << (a2.last + lr.length)}}.pack("V#{num_rings}") #last element of the previous array is dropped
|
641
|
+
geometry.each do |poly|
|
642
|
+
poly.each do |lr|
|
643
|
+
lr.each do |point|
|
644
|
+
str << [point.x,point.y].pack("E2")
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
str
|
650
|
+
end
|
651
|
+
|
652
|
+
def build_polygon_zm(geometry,zm,range,str)
|
653
|
+
str << range.pack("E2")
|
654
|
+
if geometry.is_a? GeoRuby::Base::Polygon
|
655
|
+
geometry.each do |lr|
|
656
|
+
lr.each do |point|
|
657
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
658
|
+
end
|
659
|
+
end
|
660
|
+
else
|
661
|
+
geometry.each do |poly|
|
662
|
+
poly.each do |lr|
|
663
|
+
lr.each do |point|
|
664
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
665
|
+
end
|
666
|
+
end
|
667
|
+
end
|
668
|
+
end
|
669
|
+
str
|
670
|
+
end
|
671
|
+
|
672
|
+
def build_multi_point(geometry,str)
|
673
|
+
str << [geometry.length].pack("V")
|
674
|
+
geometry.each do |point|
|
675
|
+
str << [point.x,point.y].pack("E2")
|
676
|
+
end
|
677
|
+
str
|
678
|
+
end
|
679
|
+
|
680
|
+
def build_multi_point_zm(geometry,zm,range,str)
|
681
|
+
str << range.pack("E2")
|
682
|
+
geometry.each do |point|
|
683
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
684
|
+
end
|
685
|
+
str
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
class MalformedShpException < StandardError
|
690
|
+
end
|
691
|
+
|
692
|
+
class UnexistantRecordException < StandardError
|
693
|
+
end
|
694
|
+
|
695
|
+
class IncompatibleGeometryException < StandardError
|
696
|
+
end
|
697
|
+
|
698
|
+
class IncompatibleDataException < StandardError
|
699
|
+
end
|
700
|
+
end
|
701
|
+
end
|