GeoRuby 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,6 +1,6 @@
1
1
  =GeoRuby
2
2
 
3
- This is GeoRuby 1.2.3 It is intended as a holder for data returned from PostGIS and the Spatial Extensions of MySql. The data model roughly follows the OGC "Simple Features for SQL" specification (see http://www.opengis.org/docs/99-049.pdf), although without any kind of advanced functionalities (such as geometric operators or reprojections).
3
+ This is GeoRuby 1.3.0 It is intended as a holder for data returned from PostGIS and the Spatial Extensions of MySql. The data model roughly follows the OGC "Simple Features for SQL" specification (see http://www.opengis.org/docs/99-049.pdf), although without any kind of advanced functionalities (such as geometric operators or reprojections). It also supports various output and input formats (GeoRSS, KML, Shapefile).
4
4
 
5
5
  ===Available data types
6
6
  The following geometric data types are provided :
@@ -24,22 +24,54 @@ GeoRSS Simple, GeoRSS W3CGeo, GeoRSS GML can also be input and output. Note that
24
24
 
25
25
  On top of that, there is now support for KML output and input. As for GeoRSS, a valid KML file will not be output, but only the geometric data. Options <tt>:id</tt>, <tt>:extrude</tt>, <tt>:tesselate</tt> and <tt>:altitude_mode</tt> can be given. Note that if the <tt>:altitude_mode</tt> option is not passed or set to <tt>clampToGround</tt>, the altitude data will not be output even if present. Envelopes output a LatLonAltBox instead of a geometry. For the output, the following geometric types are supported : Point, LineString, Polygon.
26
26
 
27
- ===SHP Reading
28
- Support for reading ESRI shapefiles (http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf) has been added. A tool called <tt>shp2sql.rb</tt> is also provided : it shows how to use the SHP reading functionality together with the spatial adapter plugin for Rails to import spatial features into MySQL and PostGIS.
27
+ ===SHP reading et writing
28
+ Georuby has support for reading ESRI shapefiles (http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf). A tool called <tt>shp2sql.rb</tt> is also provided : it shows how to use the SHP reading functionality together with the spatial adapter plugin for Rails to import spatial features into MySQL and PostGIS.
29
+
30
+ Here is an example of Shapefile reading, that goes through all the geometries in a file and disaply the values of the attributes :
31
+ ShpFile.open(shpfile) do |shp|
32
+ shp.each do |shape|
33
+ geom = shape.geometry #a GeoRuby SimpleFeature
34
+ att_data = shape.data #a Hash
35
+ shp.fields.each do |field|
36
+ puts att_data[field.name]
37
+ end
38
+ end
39
+ end
40
+
41
+ Support for ESRI shapefile creation and modification has been added as well. New shapefiles can be created given a geometry type and specifications for the DBF fields. Data can be added and removed from an existing shapefile. An update operation is also provided for convenience : it just performs a 'delete' and an 'add', which means the index of the modified record will change. Note that once a shapefile has been created, GeoRuby does not allow the modification of the schema (it will probably be done in a subsequent version).
42
+
43
+ Here is an example of how to create a new Shapefile with 2 fields :
44
+ shpfile = ShpFile.create('hello.shp',ShpType::POINT,[Dbf::Field.new("Hoyoyo","C",10),Dbf::Field.new("Boyoul","N",10,0)])
45
+ The file is then open for reading and writing.
46
+
47
+ Here is an example of how to write to a shapefile (created or not with GeoRuby) :
48
+ shpfile = ShpFile.open('places.shp')
49
+ shpfile.transaction do |tr|
50
+ tr.add(ShpRecord.new(Point.from_x_y(123.4,123.4),'Hoyoyo' => "AEZ",'Bouyoul' => 45))
51
+ tr.update(4,ShpRecord.new(Point.from_x_y(-16.67,16.41),'Hoyoyo' => "EatMe",'Bouyoul' => 42))
52
+ tr.delete(1)
53
+ end
54
+ shpfile.close
55
+
56
+ Note the transaction is just there so the operations on the files can be buffered. Nothing happens on the original files until the block has finished executing. Calling <tt>tr.rollback</tt> at anytime during the execution will prevent the modifications.
57
+
58
+ Also currently, error reporting is minimal and it has not been tested that thoroughly so caveat emptor and backup before performing any destructive operation.
29
59
 
30
60
  ===Installation
31
61
  To install the latest version, just type :
32
62
  gem install GeoRuby
33
63
 
34
64
  ===Changes since the last version
65
+ - Writing of ESRI shapefiles
35
66
  - Reading of ESRI shapefiles
36
67
  - Addition of a small tool to import spatial features in MySQL and PostGIS from a SHP file
37
68
 
38
69
  ===Coming in the next versions
39
- - Writing of SHP files
70
+ - Schema modification of existing shapefiles
71
+ - More error reporting when writing shapefiles
72
+ - More tests on writing shapefiles ; tests on real-world shapefiles
40
73
  - Better shp2sql import tool
41
74
  - Documentation
42
- - Geometric operators
43
75
 
44
76
  ===Acknowledgement
45
77
  The SHP reading part uses a modified version of the DBF library (http://rubyforge.org/projects/dbf/) by Keith Morrison (http://infused.org). Thanks also to Pramukta Kumar and Pete Schwamb for their contributions.
@@ -6,8 +6,6 @@ module GeoRuby
6
6
  module Dbf
7
7
 
8
8
  DBF_HEADER_SIZE = 32
9
- FPT_HEADER_SIZE = 512
10
- FPT_BLOCK_HEADER_SIZE = 8
11
9
  DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
12
10
  VERSION_DESCRIPTIONS = {
13
11
  "02" => "FoxBase",
@@ -33,10 +31,11 @@ module GeoRuby
33
31
  attr_reader :record_count
34
32
  attr_reader :version
35
33
  attr_reader :last_updated
34
+ attr_reader :header_length
35
+ attr_reader :record_length
36
36
 
37
37
  def initialize(file)
38
38
  @data_file = File.open(file, 'rb')
39
- @memo_file = open_memo(file)
40
39
  reload!
41
40
  end
42
41
 
@@ -56,52 +55,13 @@ module GeoRuby
56
55
 
57
56
  def reload!
58
57
  get_header_info
59
- get_memo_header_info if @memo_file
60
58
  get_field_descriptors
61
59
  end
62
60
 
63
- def has_memo_file?
64
- @memo_file ? true : false
65
- end
66
-
67
- def open_memo(file)
68
- %w(fpt FPT dbt DBT).each do |extension|
69
- filename = file.sub(/dbf$/i, extension)
70
- if File.exists?(filename)
71
- @memo_file_format = extension.downcase.to_sym
72
- return File.open(filename, 'rb')
73
- end
74
- end
75
- nil
76
- end
77
-
78
61
  def field(field_name)
79
62
  @fields.detect {|f| f.name == field_name.to_s}
80
63
  end
81
-
82
- def memo(start_block)
83
- @memo_file.rewind
84
- @memo_file.seek(start_block * @memo_block_size)
85
- if @memo_file_format == :fpt
86
- memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
87
- if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
88
- memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
89
- end
90
- else
91
- if version == "83" # dbase iii
92
- memo_string = ""
93
- loop do
94
- memo_string << block = @memo_file.read(512)
95
- break if block.strip.size < 512
96
- end
97
- elsif version == "8b" # dbase iv
98
- memo_type, memo_size = @memo_file.read(8).unpack("LL")
99
- memo_string = @memo_file.read(memo_size)
100
- end
101
- end
102
- memo_string
103
- end
104
-
64
+
105
65
  # An array of all the records contained in the database file
106
66
  def records
107
67
  seek_to_record(0)
@@ -114,7 +74,6 @@ module GeoRuby
114
74
  end
115
75
  end
116
76
  end
117
-
118
77
  alias_method :rows, :records
119
78
 
120
79
  # Jump to record
@@ -152,9 +111,6 @@ module GeoRuby
152
111
  record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
153
112
  end
154
113
  end
155
- when 'M'
156
- starting_block = unpack_integer(field)
157
- record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
158
114
  when 'L'
159
115
  record[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false rescue false
160
116
  when 'C'
@@ -175,17 +131,7 @@ module GeoRuby
175
131
  def get_field_descriptors
176
132
  @fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4CC'))}
177
133
  end
178
-
179
- def get_memo_header_info
180
- @memo_file.rewind
181
- if @memo_file_format == :fpt
182
- @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
183
- else
184
- @memo_block_size = 512
185
- @memo_next_available_block = File.size(@memo_file.path) / @memo_block_size
186
- end
187
- end
188
-
134
+
189
135
  def seek(offset)
190
136
  @data_file.seek(@header_length + offset)
191
137
  end
@@ -217,7 +163,7 @@ module GeoRuby
217
163
  class Field
218
164
  attr_reader :name, :type, :length, :decimal
219
165
 
220
- def initialize(name, type, length, decimal)
166
+ def initialize(name, type, length, decimal = 0)
221
167
  raise FieldError, "field length must be greater than 0" unless length > 0
222
168
  if type == 'N' and decimal != 0
223
169
  type = 'F'
@@ -1,50 +1,57 @@
1
+ require 'date'
2
+ require 'fileutils' if !defined?(FileUtils)
1
3
  require File.dirname(__FILE__) + '/dbf'
2
4
 
5
+
3
6
  module GeoRuby
4
7
  module Shp4r
5
8
 
6
9
  #Enumerates all the types of SHP geometries. The MULTIPATCH one is the only one not currently supported by GeoRuby.
7
10
  module ShpType
8
11
  NULL_SHAPE = 0
9
- POINT = 1
10
- POLYLINE = 3
11
- POLYGON = 5
12
+ POINT = 1
13
+ POLYLINE = 3
14
+ POLYGON = 5
12
15
  MULTIPOINT = 8
13
- POINTZ = 11
16
+ POINTZ = 11
14
17
  POLYLINEZ = 13
15
- POLYGONZ = 15
18
+ POLYGONZ = 15
16
19
  MULTIPOINTZ = 18
17
- POINTM = 21
20
+ POINTM = 21
18
21
  POLYLINEM = 23
19
- POLYGONM = 25
20
- MULTIPOINTM = 28
21
- MULTIPATCH = 31 #not supported here
22
+ POLYGONM = 25
23
+ MULTIPOINTM = 28
22
24
  end
23
-
25
+
24
26
  #An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf). Currently supports only the reading of geometries.
25
27
  class ShpFile
26
- attr_reader :shp_type, :record_count, :xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax
28
+ attr_reader :shp_type, :record_count, :xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax, :file_root, :file_length
27
29
 
28
30
  include Enumerable
29
31
 
30
32
  #Opens a SHP file. Both "abc.shp" and "abc" are accepted. The files "abc.shp", "abc.shx" and "abc.dbf" must be present
31
- def initialize(file, access = "r")
33
+ def initialize(file)
32
34
  #strip the shp out of the file if present
33
- file = file.gsub(/.shp$/i,"")
35
+ @file_root = file.gsub(/.shp$/i,"")
34
36
  #check existence of shp, dbf and shx files
35
- unless File.exists?(file + ".shp") and File.exists?(file + ".dbf") and File.exists?(file + ".shx")
36
- raise MalformedShpException.new("Missing one of shp, dbf or shx for: #{file}")
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}")
37
39
  end
38
40
 
39
- @dbf = Dbf::Reader.open(file + ".dbf")
40
- @shx = File.open(file + ".shx","rb")
41
- @shp = File.open(file + ".shp","rb")
41
+ @dbf = Dbf::Reader.open(@file_root + ".dbf")
42
+ @shx = File.open(@file_root + ".shx","rb")
43
+ @shp = File.open(@file_root + ".shp","rb")
42
44
  read_index
43
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
44
51
 
45
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>.
46
- def self.open(file,access = "r")
47
- shpfile = ShpFile.new(file,access)
53
+ def self.open(file)
54
+ shpfile = ShpFile.new(file)
48
55
  if block_given?
49
56
  yield shpfile
50
57
  shpfile.close
@@ -52,6 +59,30 @@ module GeoRuby
52
59
  shpfile
53
60
  end
54
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
55
86
 
56
87
  #Closes a shapefile
57
88
  def close
@@ -59,6 +90,21 @@ module GeoRuby
59
90
  @shx.close
60
91
  @shp.close
61
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
62
108
 
63
109
  #return the description of data fields
64
110
  def fields
@@ -92,8 +138,12 @@ module GeoRuby
92
138
 
93
139
  private
94
140
  def read_index
95
- file_length, @shp_type, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = @shx.read(100).unpack("x24Nx4VE8")
96
- @record_count = (file_length - 50) / 4
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
97
147
  unless @record_count == @dbf.record_count
98
148
  raise MalformedShpException.new("Not the same number of records in SHP and DBF")
99
149
  end
@@ -112,27 +162,19 @@ module GeoRuby
112
162
  when ShpType::POINT
113
163
  x, y = @shp.read(16).unpack("E2")
114
164
  geometry = GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
115
-
116
-
117
165
  when ShpType::POLYLINE #actually creates a multi_polyline
118
166
  @shp.seek(32,IO::SEEK_CUR) #extent
119
167
  num_parts, num_points = @shp.read(8).unpack("V2")
120
-
121
168
  parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
122
169
  parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
123
-
124
170
  points = Array.new(num_points) do
125
171
  x, y = @shp.read(16).unpack("E2")
126
172
  GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
127
173
  end
128
-
129
174
  line_strings = Array.new(num_parts) do |i|
130
175
  GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])])
131
176
  end
132
-
133
177
  geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings)
134
-
135
-
136
178
  when ShpType::POLYGON
137
179
  #TODO : TO CORRECT
138
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
@@ -149,8 +191,6 @@ module GeoRuby
149
191
  GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])])
150
192
  end
151
193
  geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)])
152
-
153
-
154
194
  when ShpType::MULTIPOINT
155
195
  @shp.seek(32,IO::SEEK_CUR)
156
196
  num_points = @shp.read(4).unpack("V")[0]
@@ -289,15 +329,373 @@ module GeoRuby
289
329
  @geometry = geometry
290
330
  @data = data
291
331
  end
292
-
332
+
293
333
  #Tests if the geometry is a NULL SHAPE
294
334
  def has_null_shape?
295
335
  @geometry.nil?
296
336
  end
297
337
  end
298
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::SimpleFeatures::Point
406
+ "POINT"
407
+ elsif geom.is_a? GeoRuby::SimpleFeatures::LineString
408
+ "POLYLINE"
409
+ elsif geom.is_a? GeoRuby::SimpleFeatures::Polygon
410
+ "POLYGON"
411
+ elsif geom.is_a? GeoRuby::SimpleFeatures::MultiPoint
412
+ "MULTIPOINT"
413
+ elsif geom.is_a? GeoRuby::SimpleFeatures::MultiLineString
414
+ "POLYLINE"
415
+ elsif geom.is_a? GeoRuby::SimpleFeatures::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::SimpleFeatures::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::SimpleFeatures::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::SimpleFeatures::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::SimpleFeatures::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
+
299
689
  class MalformedShpException < StandardError
300
690
  end
301
-
691
+
692
+ class UnexistantRecordException < StandardError
693
+ end
694
+
695
+ class IncompatibleGeometryException < StandardError
696
+ end
697
+
698
+ class IncompatibleDataException < StandardError
699
+ end
302
700
  end
303
701
  end
@@ -32,6 +32,10 @@ module GeoRuby#:nodoc:
32
32
  #to be implemented in subclasses
33
33
  def bounding_box
34
34
  end
35
+
36
+ #to be implemented in subclasses
37
+ def m_range
38
+ end
35
39
 
36
40
  #Returns an Envelope object for the geometry
37
41
  def envelope
@@ -49,6 +49,20 @@ module GeoRuby
49
49
  end
50
50
  end
51
51
 
52
+ def m_range
53
+ if with_m
54
+ max_m, min_m = -Float::MAX, Float::MAX
55
+ each do |lr|
56
+ lrmr = lr.m_range
57
+ max_m = lrmr[1] if lrmr[1] > max_m
58
+ min_m = lrmr[0] if lrmr[0] < min_m
59
+ end
60
+ [min_m,max_m]
61
+ else
62
+ [0,0]
63
+ end
64
+ end
65
+
52
66
  #tests the equality of geometry collections
53
67
  def ==(other_collection)
54
68
  if(other_collection.class != self.class)
@@ -47,6 +47,19 @@ module GeoRuby
47
47
  [Point.from_x_y(min_x,min_y),Point.from_x_y(max_x,max_y)]
48
48
  end
49
49
  end
50
+
51
+ def m_range
52
+ if with_m
53
+ max_m, min_m = -Float::MAX, Float::MAX
54
+ each do |point|
55
+ max_m = point.m if point.m > max_m
56
+ min_m = point.m if point.m < min_m
57
+ end
58
+ [min_m,max_m]
59
+ else
60
+ [0,0]
61
+ end
62
+ end
50
63
 
51
64
  #Tests the equality of line strings
52
65
  def ==(other_line_string)
@@ -114,6 +114,10 @@ module GeoRuby
114
114
  [Point.from_x_y_z(@x,@y,@z),Point.from_x_y_z(@x,@y,@z)]
115
115
  end
116
116
  end
117
+
118
+ def m_range
119
+ [@m,@m]
120
+ end
117
121
 
118
122
  #tests the equality of the position of points + m
119
123
  def ==(other_point)
@@ -36,6 +36,20 @@ module GeoRuby
36
36
  end
37
37
  end
38
38
 
39
+ def m_range
40
+ if with_m
41
+ max_m, min_m = -Float::MAX, Float::MAX
42
+ each do |lr|
43
+ lrmr = lr.m_range
44
+ max_m = lrmr[1] if lrmr[1] > max_m
45
+ min_m = lrmr[0] if lrmr[0] < min_m
46
+ end
47
+ [min_m,max_m]
48
+ else
49
+ [0,0]
50
+ end
51
+ end
52
+
39
53
  #tests for other equality. The SRID is not taken into account.
40
54
  def ==(other_polygon)
41
55
  if other_polygon.class != self.class or
@@ -24,7 +24,7 @@ spec = Gem::Specification::new do |s|
24
24
  s.platform = Gem::Platform::RUBY
25
25
 
26
26
  s.name = 'GeoRuby'
27
- s.version = "1.2.4"
27
+ s.version = "1.3.0"
28
28
  s.summary = "Ruby data holder for OGC Simple Features"
29
29
  s.description = <<EOF
30
30
  GeoRuby is intended as a holder for data returned from PostGIS and MySQL Spatial queries. The data model roughly follows the OGC "Simple Features for SQL" specification (see www.opengis.org/docs/99-049.pdf), although without any kind of advanced functionalities (such as geometric operators or reprojections)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,150 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'geo_ruby'
4
+ require 'test/unit'
5
+
6
+ include GeoRuby::SimpleFeatures
7
+ include GeoRuby::Shp4r
8
+
9
+ class TestShp < Test::Unit::TestCase
10
+
11
+ def cp_all_shp(file1,file2)
12
+ FileUtils.copy(file1 + ".shp",file2 + ".shp")
13
+ FileUtils.copy(file1 + ".shx",file2 + ".shx")
14
+ FileUtils.copy(file1 + ".dbf",file2 + ".dbf")
15
+ end
16
+
17
+ def rm_all_shp(file)
18
+ FileUtils.rm(file + ".shp")
19
+ FileUtils.rm(file + ".shx")
20
+ FileUtils.rm(file + ".dbf")
21
+ end
22
+
23
+ def test_point
24
+ cp_all_shp(File.dirname(__FILE__) + '/data/point',
25
+ File.dirname(__FILE__) + '/data/point2')
26
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/point2.shp')
27
+
28
+ shpfile.transaction do |tr|
29
+ assert(tr.instance_of?(ShpTransaction))
30
+ tr.add(ShpRecord.new(Point.from_x_y(123.4,123.4),'Hoyoyo' => 5))
31
+ tr.add(ShpRecord.new(Point.from_x_y(-16.67,16.41),'Hoyoyo' => -7))
32
+ tr.delete(1)
33
+ end
34
+
35
+ assert_equal(3,shpfile.record_count)
36
+
37
+ shpfile.close
38
+ rm_all_shp(File.dirname(__FILE__) + '/data/point2')
39
+ end
40
+
41
+ def test_linestring
42
+ cp_all_shp(File.dirname(__FILE__) + '/data/polyline',
43
+ File.dirname(__FILE__) + '/data/polyline2')
44
+
45
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/polyline2.shp')
46
+
47
+ shpfile.transaction do |tr|
48
+ assert(tr.instance_of?(ShpTransaction))
49
+ tr.add(ShpRecord.new(LineString.from_coordinates([[123.4,123.4],[45.6,12.3]]),'Chipoto' => 5.6778))
50
+ tr.add(ShpRecord.new(LineString.from_coordinates([[23.4,13.4],[45.6,12.3],[12,-67]]),'Chipoto' => -7.1))
51
+ tr.delete(0)
52
+ end
53
+
54
+ assert_equal(2,shpfile.record_count)
55
+ shpfile.close
56
+ #rm_all_shp(File.dirname(__FILE__) + '/data/polyline2')
57
+ end
58
+
59
+ def test_polygon
60
+ cp_all_shp(File.dirname(__FILE__) + '/data/polygon',
61
+ File.dirname(__FILE__) + '/data/polygon2')
62
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/polygon2.shp')
63
+
64
+ shpfile.transaction do |tr|
65
+ assert(tr.instance_of?(ShpTransaction))
66
+ tr.delete(0)
67
+ tr.add(ShpRecord.new(Polygon.from_coordinates([[[0,0],[40,0],[40,40],[0,40],[0,0]],[[10,10],[10,20],[20,20],[10,10]]]),'Hello' => "oook"))
68
+ end
69
+
70
+ assert_equal(1,shpfile.record_count)
71
+
72
+ shpfile.close
73
+ rm_all_shp(File.dirname(__FILE__) + '/data/polygon2')
74
+ end
75
+
76
+ def test_multipoint
77
+ cp_all_shp(File.dirname(__FILE__) + '/data/multipoint',
78
+ File.dirname(__FILE__) + '/data/multipoint2')
79
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/multipoint2.shp')
80
+
81
+ shpfile.transaction do |tr|
82
+ assert(tr.instance_of?(ShpTransaction))
83
+ tr.add(ShpRecord.new(MultiPoint.from_coordinates([[45.6,-45.1],[12.4,98.2],[51.2,-0.12],[156.12345,56.109]]),'Hello' => 5,"Hoyoyo" => "AEZAE"))
84
+ end
85
+
86
+ assert_equal(2,shpfile.record_count)
87
+
88
+ shpfile.close
89
+ rm_all_shp(File.dirname(__FILE__) + '/data/multipoint2')
90
+ end
91
+
92
+ def test_multi_polygon
93
+ cp_all_shp(File.dirname(__FILE__) + '/data/polygon',
94
+ File.dirname(__FILE__) + '/data/polygon4')
95
+
96
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/polygon4.shp')
97
+
98
+ shpfile.transaction do |tr|
99
+ assert(tr.instance_of?(ShpTransaction))
100
+ tr.add(ShpRecord.new(MultiPolygon.from_polygons([Polygon.from_coordinates([[[0,0],[40,0],[40,40],[0,40],[0,0]],[[10,10],[10,20],[20,20],[10,10]]])]),'Hello' => "oook"))
101
+ end
102
+
103
+ assert_equal(2,shpfile.record_count)
104
+
105
+ shpfile.close
106
+
107
+ rm_all_shp(File.dirname(__FILE__) + '/data/polygon4')
108
+ end
109
+
110
+ def test_rollback
111
+ cp_all_shp(File.dirname(__FILE__) + '/data/polygon',
112
+ File.dirname(__FILE__) + '/data/polygon5')
113
+
114
+ shpfile = ShpFile.open(File.dirname(__FILE__) + '/data/polygon5.shp')
115
+
116
+ shpfile.transaction do |tr|
117
+ assert(tr.instance_of?(ShpTransaction))
118
+ tr.add(ShpRecord.new(MultiPolygon.from_polygons([Polygon.from_coordinates([[[0,0],[40,0],[40,40],[0,40],[0,0]],[[10,10],[10,20],[20,20],[10,10]]])]),'Hello' => "oook"))
119
+ tr.rollback
120
+ end
121
+ assert_equal(1,shpfile.record_count)
122
+
123
+ shpfile.close
124
+
125
+ rm_all_shp(File.dirname(__FILE__) + '/data/polygon5')
126
+
127
+ end
128
+
129
+ def test_creation
130
+ shpfile = ShpFile.create(File.dirname(__FILE__) + '/data/point3.shp',ShpType::POINT,[Dbf::Field.new("Hoyoyo","C",10,0)])
131
+ shpfile.transaction do |tr|
132
+ tr.add(ShpRecord.new(Point.from_x_y(123,123.4),'Hoyoyo' => "HJHJJ"))
133
+ end
134
+ assert(1,shpfile.record_count)
135
+ shpfile.close
136
+ rm_all_shp(File.dirname(__FILE__) + '/data/point3')
137
+ end
138
+
139
+ def test_creation_multipoint
140
+ shpfile = ShpFile.create(File.dirname(__FILE__) + '/data/multipoint3.shp',ShpType::MULTIPOINT,[Dbf::Field.new("Hoyoyo","C",10),Dbf::Field.new("Hello","N",10)])
141
+ shpfile.transaction do |tr|
142
+ tr.add(ShpRecord.new(MultiPoint.from_coordinates([[123,123.4],[345,12.2]]),'Hoyoyo' => "HJHJJ","Hello" => 5))
143
+ end
144
+ assert(1,shpfile.record_count)
145
+ shpfile.close
146
+ rm_all_shp(File.dirname(__FILE__) + '/data/multipoint3')
147
+ end
148
+
149
+
150
+ end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: GeoRuby
5
5
  version: !ruby/object:Gem::Version
6
- version: 1.2.4
7
- date: 2007-07-20 00:00:00 +02:00
6
+ version: 1.3.0
7
+ date: 2007-08-26 00:00:00 +02:00
8
8
  summary: Ruby data holder for OGC Simple Features
9
9
  require_paths:
10
10
  - lib
@@ -51,19 +51,26 @@ files:
51
51
  - test/test_ewkt_parser.rb
52
52
  - test/test_georss_kml.rb
53
53
  - test/test_shp.rb
54
+ - test/test_shp_write.rb
54
55
  - test/test_simple_features.rb
55
56
  - README
56
57
  - MIT-LICENSE
57
58
  - rakefile.rb
59
+ - test/data/multipoint.shp
58
60
  - test/data/point.shp
59
61
  - test/data/polygon.shp
60
62
  - test/data/polyline.shp
63
+ - test/data/polyline2.shp
64
+ - test/data/multipoint.dbf
61
65
  - test/data/point.dbf
62
66
  - test/data/polygon.dbf
63
67
  - test/data/polyline.dbf
68
+ - test/data/polyline2.dbf
69
+ - test/data/multipoint.shx
64
70
  - test/data/point.shx
65
71
  - test/data/polygon.shx
66
72
  - test/data/polyline.shx
73
+ - test/data/polyline2.shx
67
74
  - tools/db.yml
68
75
  - tools/lib/spatial_adapter/test/db/database_mysql.yml
69
76
  - tools/lib/spatial_adapter/test/db/database_postgis.yml
@@ -99,6 +106,7 @@ test_files:
99
106
  - test/test_ewkt_parser.rb
100
107
  - test/test_georss_kml.rb
101
108
  - test/test_shp.rb
109
+ - test/test_shp_write.rb
102
110
  - test/test_simple_features.rb
103
111
  rdoc_options:
104
112
  - --main