nofxx-georuby 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/History.txt +4 -0
  2. data/LICENSE +21 -0
  3. data/README.txt +59 -0
  4. data/Rakefile +49 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/geo_ruby.rb +21 -0
  7. data/lib/geo_ruby/base/envelope.rb +167 -0
  8. data/lib/geo_ruby/base/ewkb_parser.rb +216 -0
  9. data/lib/geo_ruby/base/ewkt_parser.rb +336 -0
  10. data/lib/geo_ruby/base/geometry.rb +234 -0
  11. data/lib/geo_ruby/base/geometry_collection.rb +136 -0
  12. data/lib/geo_ruby/base/geometry_factory.rb +81 -0
  13. data/lib/geo_ruby/base/georss_parser.rb +135 -0
  14. data/lib/geo_ruby/base/helper.rb +18 -0
  15. data/lib/geo_ruby/base/line_string.rb +184 -0
  16. data/lib/geo_ruby/base/linear_ring.rb +12 -0
  17. data/lib/geo_ruby/base/multi_line_string.rb +39 -0
  18. data/lib/geo_ruby/base/multi_point.rb +41 -0
  19. data/lib/geo_ruby/base/multi_polygon.rb +37 -0
  20. data/lib/geo_ruby/base/point.rb +310 -0
  21. data/lib/geo_ruby/base/polygon.rb +150 -0
  22. data/lib/geo_ruby/shp4r/dbf.rb +180 -0
  23. data/lib/geo_ruby/shp4r/shp.rb +701 -0
  24. data/spec/data/multipoint.dbf +0 -0
  25. data/spec/data/multipoint.shp +0 -0
  26. data/spec/data/multipoint.shx +0 -0
  27. data/spec/data/point.dbf +0 -0
  28. data/spec/data/point.shp +0 -0
  29. data/spec/data/point.shx +0 -0
  30. data/spec/data/polygon.dbf +0 -0
  31. data/spec/data/polygon.shp +0 -0
  32. data/spec/data/polygon.shx +0 -0
  33. data/spec/data/polyline.dbf +0 -0
  34. data/spec/data/polyline.shp +0 -0
  35. data/spec/data/polyline.shx +0 -0
  36. data/spec/geo_ruby/base/envelope_spec.rb +45 -0
  37. data/spec/geo_ruby/base/ewkb_parser_spec.rb +158 -0
  38. data/spec/geo_ruby/base/ewkt_parser_spec.rb +179 -0
  39. data/spec/geo_ruby/base/geometry_collection_spec.rb +55 -0
  40. data/spec/geo_ruby/base/geometry_factory_spec.rb +11 -0
  41. data/spec/geo_ruby/base/geometry_spec.rb +32 -0
  42. data/spec/geo_ruby/base/georss_parser_spec.rb +218 -0
  43. data/spec/geo_ruby/base/line_string_spec.rb +208 -0
  44. data/spec/geo_ruby/base/linear_ring_spec.rb +14 -0
  45. data/spec/geo_ruby/base/multi_line_string_spec.rb +35 -0
  46. data/spec/geo_ruby/base/multi_point_spec.rb +29 -0
  47. data/spec/geo_ruby/base/multi_polygon_spec.rb +35 -0
  48. data/spec/geo_ruby/base/point_spec.rb +275 -0
  49. data/spec/geo_ruby/base/polygon_spec.rb +108 -0
  50. data/spec/geo_ruby/shp4r/shp_spec.rb +238 -0
  51. data/spec/geo_ruby_spec.rb +27 -0
  52. data/spec/spec.opts +6 -0
  53. data/spec/spec_helper.rb +12 -0
  54. 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