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.
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