nofxx-georuby 1.3.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|