rgeo 0.1.17 → 0.1.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/History.rdoc +8 -0
  2. data/README.rdoc +40 -21
  3. data/Version +1 -1
  4. data/lib/rgeo.rb +1 -0
  5. data/lib/rgeo/cartesian.rb +1 -0
  6. data/lib/rgeo/cartesian/analysis.rb +115 -0
  7. data/lib/rgeo/cartesian/feature_classes.rb +0 -14
  8. data/lib/rgeo/features/factory.rb +5 -5
  9. data/lib/rgeo/features/factory_generator.rb +10 -0
  10. data/lib/rgeo/geo_json/coder.rb +49 -14
  11. data/lib/rgeo/geo_json/interface.rb +6 -6
  12. data/lib/rgeo/shapefile.rb +60 -0
  13. data/lib/rgeo/shapefile/reader.rb +898 -0
  14. data/tests/shapefile/shapelib_testcases/readme.txt +11 -0
  15. data/tests/shapefile/shapelib_testcases/test.dbf +0 -0
  16. data/tests/shapefile/shapelib_testcases/test.shp +0 -0
  17. data/tests/shapefile/shapelib_testcases/test.shx +0 -0
  18. data/tests/shapefile/shapelib_testcases/test0.shp +0 -0
  19. data/tests/shapefile/shapelib_testcases/test0.shx +0 -0
  20. data/tests/shapefile/shapelib_testcases/test1.shp +0 -0
  21. data/tests/shapefile/shapelib_testcases/test1.shx +0 -0
  22. data/tests/shapefile/shapelib_testcases/test10.shp +0 -0
  23. data/tests/shapefile/shapelib_testcases/test10.shx +0 -0
  24. data/tests/shapefile/shapelib_testcases/test11.shp +0 -0
  25. data/tests/shapefile/shapelib_testcases/test11.shx +0 -0
  26. data/tests/shapefile/shapelib_testcases/test12.shp +0 -0
  27. data/tests/shapefile/shapelib_testcases/test12.shx +0 -0
  28. data/tests/shapefile/shapelib_testcases/test13.shp +0 -0
  29. data/tests/shapefile/shapelib_testcases/test13.shx +0 -0
  30. data/tests/shapefile/shapelib_testcases/test2.shp +0 -0
  31. data/tests/shapefile/shapelib_testcases/test2.shx +0 -0
  32. data/tests/shapefile/shapelib_testcases/test3.shp +0 -0
  33. data/tests/shapefile/shapelib_testcases/test3.shx +0 -0
  34. data/tests/shapefile/shapelib_testcases/test4.shp +0 -0
  35. data/tests/shapefile/shapelib_testcases/test4.shx +0 -0
  36. data/tests/shapefile/shapelib_testcases/test5.shp +0 -0
  37. data/tests/shapefile/shapelib_testcases/test5.shx +0 -0
  38. data/tests/shapefile/shapelib_testcases/test6.shp +0 -0
  39. data/tests/shapefile/shapelib_testcases/test6.shx +0 -0
  40. data/tests/shapefile/shapelib_testcases/test7.shp +0 -0
  41. data/tests/shapefile/shapelib_testcases/test7.shx +0 -0
  42. data/tests/shapefile/shapelib_testcases/test8.shp +0 -0
  43. data/tests/shapefile/shapelib_testcases/test8.shx +0 -0
  44. data/tests/shapefile/shapelib_testcases/test9.shp +0 -0
  45. data/tests/shapefile/shapelib_testcases/test9.shx +0 -0
  46. data/tests/shapefile/tc_shapelib_tests.rb +527 -0
  47. data/tests/tc_cartesian_analysis.rb +107 -0
  48. data/tests/tc_oneoff.rb +1 -0
  49. metadata +118 -12
@@ -0,0 +1,898 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Shapefile reader for RGeo
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ begin
38
+ require 'dbf'
39
+ rescue ::LoadError => ex_
40
+ end
41
+
42
+
43
+ module RGeo
44
+
45
+ module Shapefile
46
+
47
+
48
+ # Represents a shapefile that is open for reading.
49
+ #
50
+ # You can use this object to read a shapefile straight through,
51
+ # yielding the data in a block; or you can perform random access
52
+ # reads of indexed records.
53
+ #
54
+ # You must close this object after you are done, in order to close
55
+ # the underlying files. Alternatively, you can pass a block to
56
+ # Reader::open, and the reader will be closed automatically for
57
+ # you at the end of the block.
58
+ #
59
+ # === Dependencies
60
+ #
61
+ # Attributes in shapefiles are stored in a ".dbf" (dBASE) format
62
+ # file. The "dbf" gem is required to read these files. If this
63
+ # gem is not installed, shapefile reading will still function,
64
+ # but attributes will not be available.
65
+ #
66
+ # Correct interpretation of the polygon shape type requires some
67
+ # functionality that is available in the RGeo::Geos module. Hence,
68
+ # reading a polygon shapefile will generally fail if that module is
69
+ # not available or the GEOS library is not installed. It is possible
70
+ # to bypass this requirement by relaxing the polygon tests and making
71
+ # some assumptions about the file format. See the documentation for
72
+ # Reader::open for details.
73
+ #
74
+ # === Shapefile support
75
+ #
76
+ # This class supports shapefiles formatted according to the 1998
77
+ # "ESRI Shapefile Technical Description". It converts shapefile
78
+ # data to RGeo geometry objects, as follows:
79
+ #
80
+ # * Shapefile records are represented by the
81
+ # RGeo::Shapefile::Reader::Record class, which provides the
82
+ # geometry, the attributes, and the record number (0-based).
83
+ # * Attribute reading is supported by the "dbf" gem, which provides
84
+ # the proper typecasting for numeric, string, boolean, and
85
+ # date/time column types. Data in unrecognized column types are
86
+ # returned as strings.
87
+ # * All shape types documented in the 1998 publication are supported,
88
+ # including point, polyline, polygon, multipoint, and multipatch,
89
+ # along with Z and M versions.
90
+ # * Null shapes are translated into nil geometry objects. That is,
91
+ # Record#geometry will return nil if that record has a null shape.
92
+ # * The point shape type yields Point geometries.
93
+ # * The multipoint shape type yields MultiPoint geometries.
94
+ # * The polyline shape type yields MultiLineString geometries.
95
+ # * The polygon shape type yields MultiPolygon geometries.
96
+ # * The multipatch shape type yields GeometryCollection geometries.
97
+ # (See below for an explanation of why we do not return a
98
+ # MultiPolygon.)
99
+ #
100
+ # Some special notes and limitations in our shapefile support:
101
+ #
102
+ # * Our implementation assumes that shapefile data is in a Cartesian
103
+ # coordinate system when it performs certain computations, such as
104
+ # directionality of polygon rings. It also ignores the 180 degree
105
+ # longitude seam, so it may not correctly interpret objects whose
106
+ # coordinates are in lat/lon space and which span that seam.
107
+ # * The ESRI polygon specification allows interior rings to touch
108
+ # their exterior ring in a finite number of points. This technically
109
+ # violates the OGC Polygon definition. However, such a structure
110
+ # remains a legal OGC MultiPolygon, and it is in principle possible
111
+ # to detect this case and transform the geometry type accordingly.
112
+ # We do not yet do this. Therefore, it is possible for a shapefile
113
+ # with polygon type to yield an illegal geometry.
114
+ # * The ESRI polygon specification clearly specifies the winding order
115
+ # for inner and outer rings: outer rings are clockwise while inner
116
+ # rings are counterclockwise. We have heard it reported that there
117
+ # may be shapefiles out there that do not conform to this spec. Such
118
+ # shapefiles may not read correctly.
119
+ # * The ESRI multipatch specification includes triangle strips and
120
+ # triangle fans as ways of constructing polygonal patches. We read
121
+ # in the aggregate polygonal patches, and do not preserve the
122
+ # individual triangles.
123
+ # * The ESRI multipatch specification allows separate patch parts to
124
+ # share common boundaries, thus effectively becoming a single
125
+ # polygon. It is in principle possible to detect this case and
126
+ # merge the constituent polygons; however, such a data structure
127
+ # implies that the intent is for such polygons to remain distinct
128
+ # objects even though they share a common boundary. Therefore, we
129
+ # do not attempt to merge such polygons. However, this means it is
130
+ # possible for a multipatch to violate the OGC MultiPolygon
131
+ # assertions, which do not allow constituent polygons to share a
132
+ # common boundary. Therefore, when reading a multipatch, we return
133
+ # a GeometryCollection instead of a MultiPolygon.
134
+
135
+ class Reader
136
+
137
+
138
+ # Values less than this value are considered "no value" in the
139
+ # shapefile format specification.
140
+ NODATA_LIMIT = -1e38
141
+
142
+
143
+ # Create a new shapefile reader. You must pass the path for the
144
+ # main shapefile (e.g. "path/to/file.shp"). You may also omit the
145
+ # ".shp" extension from the path. All three files that make up the
146
+ # shapefile (".shp", ".idx", and ".dbf") must be present for
147
+ # successful opening of a shapefile.
148
+ #
149
+ # If you provide a block, the shapefile reader will be yielded to
150
+ # the block, and automatically closed at the end of the block.
151
+ # If you do not provide a block, the shapefile reader will be
152
+ # returned from this call. It is then the caller's responsibility
153
+ # to close the reader when it is done.
154
+ #
155
+ # Options include:
156
+ #
157
+ # <tt>:default_factory</tt>::
158
+ # The default factory for parsed geometries, used when no factory
159
+ # generator is provided. If no default is provided either, the
160
+ # default cartesian factory will be used as the default.
161
+ # <tt>:factory_generator</tt>::
162
+ # A factory generator that should return a factory based on the
163
+ # srid and dimension settings in the input. The factory generator
164
+ # should understand the configuration options
165
+ # <tt>:support_z_coordinate</tt> and <tt>:support_m_coordinate</tt>.
166
+ # See RGeo::Features::FactoryGenerator for more information.
167
+ # If no generator is provided, the <tt>:default_factory</tt> is
168
+ # used.
169
+ # <tt>:srid</tt>::
170
+ # If provided, this option is passed to the factory generator.
171
+ # This is useful because shapefiles do not contain a SRID.
172
+ # <tt>:assume_inner_follows_outer</tt>::
173
+ # If set to true, some assumptions are made about ring ordering
174
+ # in a polygon shapefile. See below for details. Default is false.
175
+ #
176
+ # === Ring ordering in polygon shapefiles
177
+ #
178
+ # The ESRI polygon shape type specifies that the ordering of rings
179
+ # in the shapefile is not significant. That is, rings can be in any
180
+ # order, and inner rings need not necessarily follow the outer ring
181
+ # they are associated with. This specification causes some headache
182
+ # in the process of constructing polygons from a shapefile, because
183
+ # it becomes necessary to run some geometric analysis on the rings
184
+ # that are read in, in order to determine which inner rings should
185
+ # go with which outer rings.
186
+ #
187
+ # RGeo's shapefile reader uses GEOS to perform this analysis.
188
+ # However, this means that if GEOS is not available, the analysis
189
+ # will fail. It also means reading polygons may be slow, especially
190
+ # for polygon records with a large number of parts. Therefore, it
191
+ # is possible to turn off this analysis by setting the
192
+ # <tt>:assume_inner_follows_outer</tt> switch when creating a
193
+ # Reader. This causes the shapefile reader to assume that inner
194
+ # rings always follow their corresponding outer ring in the file.
195
+ # This is probably true for most well-behaved shapefiles out there,
196
+ # but since it is not part of the specification, this shortcutting
197
+ # is not turned on by default. However, if you are running RGeo on
198
+ # a platform without GEOS, you have no choice but to turn on this
199
+ # switch and make this assumption about your input shapefiles.
200
+
201
+ def self.open(path_, opts_={}, &block_)
202
+ file_ = new(path_, opts_)
203
+ if block_
204
+ begin
205
+ yield file_
206
+ ensure
207
+ file_.close
208
+ end
209
+ nil
210
+ else
211
+ file_
212
+ end
213
+ end
214
+
215
+
216
+ # Low-level creation of a Reader. The arguments are the same as
217
+ # those passed to Reader::open, except that this doesn't take a
218
+ # block. You should use Reader::open instead.
219
+
220
+ def initialize(path_, opts_={}) # :nodoc:
221
+ path_.sub!(/\.shp$/, '')
222
+ @base_path = path_
223
+ @opened = true
224
+ @main_file = ::File.open(path_+'.shp', 'rb:ascii-8bit')
225
+ @index_file = ::File.open(path_+'.shx', 'rb:ascii-8bit')
226
+ @attr_dbf = ::DBF::Table.new(path_+'.dbf') rescue nil
227
+ @main_length, @shape_type_code, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin, @mmax = @main_file.read(100).unpack('x24Nx4VE8')
228
+ @main_length *= 2
229
+ index_length_ = @index_file.read(100).unpack('x24Nx72').first
230
+ @num_records = (index_length_ - 50) / 4
231
+ @cur_record_index = 0
232
+
233
+ if @num_records == 0
234
+ @xmin = @xmax = @ymin = @ymax = @zmin = @zmax = @mmin = @mmax = nil
235
+ else
236
+ case @shape_type_code
237
+ when 11, 13, 15, 18, 31
238
+ if @mmin < NODATA_LIMIT || @mmax < NODATA_LIMIT
239
+ @mmin = @mmax = nil
240
+ end
241
+ if @zmin < NODATA_LIMIT || @zmax < NODATA_LIMIT
242
+ @zmin = @zmax = nil
243
+ end
244
+ when 21, 23, 25, 28
245
+ @zmin = @zmax = nil
246
+ else
247
+ @mmin = @mmax = @zmin = @zmax = nil
248
+ end
249
+ end
250
+
251
+ factory_generator_ = opts_[:factory_generator]
252
+ if factory_generator_
253
+ factory_config_ = {}
254
+ factory_config_[:srid] = opts_[:srid] if opts_[:srid]
255
+ unless @zmin.nil?
256
+ factory_config_[:support_z_coordinate] = true
257
+ end
258
+ unless @mmin.nil?
259
+ factory_config_[:support_m_coordinate] = true
260
+ end
261
+ @factory = factory_generator_.call(factory_config_)
262
+ else
263
+ @factory = opts_[:default_factory] || Cartesian.preferred_factory
264
+ end
265
+ @factory_supports_z = @factory.has_capability?(:z_coordinate)
266
+ @factory_supports_m = @factory.has_capability?(:m_coordinate)
267
+
268
+ @assume_inner_follows_outer = opts_[:assume_inner_follows_outer]
269
+ end
270
+
271
+
272
+ # Close the shapefile.
273
+ # You should not use this Reader after it has been closed.
274
+ # Most methods will return nil.
275
+
276
+ def close
277
+ if @opened
278
+ @main_file.close
279
+ @index_file.close
280
+ @attr_dbf.close if @attr_dbf
281
+ @opened = false
282
+ end
283
+ end
284
+
285
+
286
+ # Returns true if this Reader is still open, or false if it has
287
+ # been closed.
288
+
289
+ def open?
290
+ @opened
291
+ end
292
+
293
+
294
+ # Returns true if attributes are available. This may be false
295
+ # because there is no ".dbf" file or because the dbf gem is not
296
+ # available.
297
+
298
+ def attributes_available?
299
+ @opened ? (@attr_dbf ? true : false) : nil
300
+ end
301
+
302
+
303
+ # Returns the factory used by this reader.
304
+
305
+ def factory
306
+ @opened ? @factory : nil
307
+ end
308
+
309
+
310
+ # Returns the number of records in the shapefile.
311
+
312
+ def num_records
313
+ @opened ? @num_records : nil
314
+ end
315
+ alias_method :size, :num_records
316
+
317
+
318
+ # Returns the shape type code.
319
+
320
+ def shape_type_code
321
+ @shape_type_code
322
+ end
323
+
324
+
325
+ # Returns the minimum x.
326
+
327
+ def xmin
328
+ @opened ? @xmin : nil
329
+ end
330
+
331
+
332
+ # Returns the maximum x.
333
+
334
+ def xmax
335
+ @opened ? @xmax : nil
336
+ end
337
+
338
+
339
+ # Returns the minimum y.
340
+
341
+ def ymin
342
+ @opened ? @ymin : nil
343
+ end
344
+
345
+
346
+ # Returns the maximum y.
347
+
348
+ def ymax
349
+ @opened ? @ymax : nil
350
+ end
351
+
352
+
353
+ # Returns the minimum z, or nil if the shapefile does not contain z.
354
+
355
+ def zmin
356
+ @opened ? @zmin : nil
357
+ end
358
+
359
+
360
+ # Returns the maximum z, or nil if the shapefile does not contain z.
361
+
362
+ def zmax
363
+ @opened ? @zmax : nil
364
+ end
365
+
366
+
367
+ # Returns the minimum m, or nil if the shapefile does not contain m.
368
+
369
+ def mmin
370
+ @opened ? @mmin : nil
371
+ end
372
+
373
+
374
+ # Returns the maximum m, or nil if the shapefile does not contain m.
375
+
376
+ def mmax
377
+ @opened ? @mmax : nil
378
+ end
379
+
380
+
381
+ # Returns the current file pointer as a record index (0-based).
382
+ # This is the record number that will be read when Reader#next
383
+ # is called.
384
+
385
+ def cur_index
386
+ @opened ? @cur_record_index : nil
387
+ end
388
+
389
+
390
+ # Read and return the next record as a Reader::Record.
391
+
392
+ def next
393
+ @cur_record_index < @num_records ? _read_next_record : nil
394
+ end
395
+
396
+
397
+ # Read the remaining records starting with the current record index,
398
+ # and yield the Reader::Record for each one.
399
+
400
+ def each
401
+ while @cur_record_index < @num_records
402
+ yield _read_next_record
403
+ end
404
+ end
405
+
406
+
407
+ # Seek to the given record index.
408
+
409
+ def seek_index(index_)
410
+ if index_ >= 0 && index_ <= @num_records
411
+ if index_ < @num_records && index_ != @cur_record_index
412
+ @index_file.seek(100+8*index_)
413
+ offset_ = @index_file.read(4).unpack('N').first
414
+ @main_file.seek(offset_*2)
415
+ end
416
+ @cur_record_index = index_
417
+ true
418
+ else
419
+ false
420
+ end
421
+ end
422
+
423
+
424
+ # Rewind to the beginning of the file.
425
+ # Equivalent to seek_index(0).
426
+
427
+ def rewind
428
+ seek_index(0)
429
+ end
430
+
431
+
432
+ # Get the given record number. Equivalent to seeking to that index
433
+ # and calling next.
434
+
435
+ def get(index_)
436
+ seek_index(index_) ? self.next : nil
437
+ end
438
+ alias_method :[], :get
439
+
440
+
441
+ def _read_next_record # :nodoc:
442
+ num_, length_ = @main_file.read(8).unpack('NN')
443
+ data_ = @main_file.read(length_ * 2)
444
+ shape_type_ = data_[0,4].unpack('V').first
445
+ geometry_ =
446
+ case shape_type_
447
+ when 1 then _read_point(data_)
448
+ when 3 then _read_polyline(data_)
449
+ when 5 then _read_polygon(data_)
450
+ when 8 then _read_multipoint(data_)
451
+ when 11 then _read_point(data_, :z)
452
+ when 13 then _read_polyline(data_, :z)
453
+ when 15 then _read_polygon(data_, :z)
454
+ when 18 then _read_multipoint(data_, :z)
455
+ when 21 then _read_point(data_, :m)
456
+ when 23 then _read_polyline(data_, :m)
457
+ when 25 then _read_polygon(data_, :m)
458
+ when 28 then _read_multipoint(data_, :m)
459
+ when 31 then _read_multipatch(data_)
460
+ else nil
461
+ end
462
+ dbf_record_ = @attr_dbf ? @attr_dbf.record(@cur_record_index) : nil
463
+ attrs_ = {}
464
+ attrs_.merge!(dbf_record_.attributes) if dbf_record_
465
+ result_ = Record.new(@cur_record_index, geometry_, attrs_)
466
+ @cur_record_index += 1
467
+ result_
468
+ end
469
+
470
+
471
+ def _read_point(data_, opt_=nil) # :nodoc:
472
+ case opt_
473
+ when :z
474
+ x_, y_, z_, m_ = data_[4,32].unpack('EEEE')
475
+ m_ = 0 if m_.nil? || m_ < NODATA_LIMIT
476
+ when :m
477
+ x_, y_, m_ = data_[4,24].unpack('EEE')
478
+ z_ = 0
479
+ else
480
+ x_, y_ = data_[4,16].unpack('EE')
481
+ z_ = m_ = 0
482
+ end
483
+ extras_ = []
484
+ extras_ << z_ if @factory_supports_z
485
+ extras_ << m_ if @factory_supports_m
486
+ @factory.point(x_, y_, *extras_)
487
+ end
488
+
489
+
490
+ def _read_multipoint(data_, opt_=nil) # :nodoc:
491
+ # Read number of points
492
+ num_points_ = data_[36,4].unpack('V').first
493
+
494
+ # Read remaining data
495
+ size_ = num_points_*16
496
+ size_ += 16 + num_points_*8 if opt_
497
+ size_ += 16 + num_points_*8 if opt_ == :z
498
+ values_ = data_[40, size_].unpack('E*')
499
+
500
+ # Extract XY, Z, and M values
501
+ xys_ = values_.slice!(0, num_points_*2)
502
+ ms_ = nil
503
+ zs_ = nil
504
+ if opt_
505
+ ms_ = values_.slice!(2, num_points_)
506
+ if opt_ == :z
507
+ zs_ = ms_
508
+ ms_ = values_.slice!(4, num_points_)
509
+ ms_.map!{ |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
510
+ end
511
+ end
512
+
513
+ # Generate points
514
+ points_ = (0..num_points_-1).map do |i_|
515
+ extras_ = []
516
+ extras_ << zs_[i_] if zs_ && @factory_supports_z
517
+ extras_ << ms_[i_] if ms_ && @factory_supports_m
518
+ @factory.point(xys_[i_*2], xys_[i_*2+1], *extras_)
519
+ end
520
+
521
+ # Return a MultiPoint
522
+ @factory.multi_point(points_)
523
+ end
524
+
525
+
526
+ def _read_polyline(data_, opt_=nil) # :nodoc:
527
+ # Read counts
528
+ num_parts_, num_points_ = data_[36,8].unpack('VV')
529
+
530
+ # Read remaining data
531
+ size_ = num_parts_*4 + num_points_*16
532
+ size_ += 16 + num_points_*8 if opt_
533
+ size_ += 16 + num_points_*8 if opt_ == :z
534
+ values_ = data_[44, size_].unpack("V#{num_parts_}E*")
535
+
536
+ # Parts array
537
+ part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]
538
+
539
+ # Extract XY, Z, and M values
540
+ xys_ = values_.slice!(0, num_points_*2)
541
+ ms_ = nil
542
+ zs_ = nil
543
+ if opt_
544
+ ms_ = values_.slice!(2, num_points_)
545
+ if opt_ == :z
546
+ zs_ = ms_
547
+ ms_ = values_.slice!(4, num_points_)
548
+ ms_.map!{ |val_| val_ < NODATA_LIMIT ? 0 : val_ }
549
+ end
550
+ end
551
+
552
+ # Generate points
553
+ points_ = (0..num_points_-1).map do |i_|
554
+ extras_ = []
555
+ extras_ << zs_[i_] if zs_ && @factory_supports_z
556
+ extras_ << ms_[i_] if ms_ && @factory_supports_m
557
+ @factory.point(xys_[i_*2], xys_[i_*2+1], *extras_)
558
+ end
559
+
560
+ # Generate LineString objects (parts)
561
+ parts_ = (0..num_parts_-1).map do |i_|
562
+ @factory.line_string(points_[part_indexes_[i_]...part_indexes_[i_+1]])
563
+ end
564
+
565
+ # Generate MultiLineString
566
+ @factory.multi_line_string(parts_)
567
+ end
568
+
569
+
570
+ def _read_polygon(data_, opt_=nil) # :nodoc:
571
+ # Read counts
572
+ num_parts_, num_points_ = data_[36,8].unpack('VV')
573
+
574
+ # Read remaining data
575
+ size_ = num_parts_*4 + num_points_*16
576
+ size_ += 16 + num_points_*8 if opt_
577
+ size_ += 16 + num_points_*8 if opt_ == :z
578
+ values_ = data_[44, size_].unpack("V#{num_parts_}E*")
579
+
580
+ # Parts array
581
+ part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]
582
+
583
+ # Extract XY, Z, and M values
584
+ xys_ = values_.slice!(0, num_points_*2)
585
+ ms_ = nil
586
+ zs_ = nil
587
+ if opt_
588
+ ms_ = values_.slice!(2, num_points_)
589
+ if opt_ == :z
590
+ zs_ = ms_
591
+ ms_ = values_.slice!(4, num_points_)
592
+ ms_.map!{ |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
593
+ end
594
+ end
595
+
596
+ # Generate points
597
+ points_ = (0..num_points_-1).map do |i_|
598
+ extras_ = []
599
+ extras_ << zs_[i_] if zs_ && @factory_supports_z
600
+ extras_ << ms_[i_] if ms_ && @factory_supports_m
601
+ @factory.point(xys_[i_*2], xys_[i_*2+1], *extras_)
602
+ end
603
+
604
+ # The parts are LinearRing objects
605
+ parts_ = (0..num_parts_-1).map do |i_|
606
+ @factory.linear_ring(points_[part_indexes_[i_]...part_indexes_[i_+1]])
607
+ end
608
+
609
+ # Get a GEOS factory if needed.
610
+ geos_factory_ = nil
611
+ unless @assume_inner_follows_outer
612
+ geos_factory_ = Geos.factory
613
+ unless geos_factory_
614
+ raise Errors::RGeoError, "GEOS is not available, but is required for correct interpretation of polygons in shapefiles."
615
+ end
616
+ end
617
+
618
+ # Special case: if there's only one part, treat it as an outer
619
+ # ring, regardless of its direction. This isn't strictly compliant
620
+ # with the shapefile spec, but the shapelib test cases seem to
621
+ # include this case, so we'll relax the assertions here.
622
+ if parts_.size == 1
623
+ return @factory.multi_polygon([@factory.polygon(parts_[0])])
624
+ end
625
+
626
+ # Collect some data on the rings: the ring direction, a GEOS
627
+ # polygon (for intersection calculation), and an initial guess
628
+ # of which polygon index the ring belongs to.
629
+ parts_.map! do |ring_|
630
+ [ring_, Cartesian::Analysis.ring_direction(ring_) < 0, geos_factory_ ? geos_factory_.polygon(ring_) : nil, nil]
631
+ end
632
+
633
+ # Initial population of the polygon data array.
634
+ # Each element is an array of the part data for the rings, first
635
+ # the outer ring and then the inner rings.
636
+ # Here we populate the outer rings, and we do an initial
637
+ # assignment of rings to polygon index. The initial guess is that
638
+ # inner rings always follow their outer ring.
639
+ polygons_ = []
640
+ parts_.each do |part_data_|
641
+ if part_data_[1]
642
+ polygons_ << [part_data_]
643
+ elsif @assume_inner_follows_outer && polygons_.size > 0
644
+ polygons_.last << part_data_
645
+ end
646
+ part_data_[3] = polygons_.size - 1
647
+ end
648
+
649
+ # If :assume_inner_follows_outer is in effect, we assume this
650
+ # initial guess is the correct one, and we don't run the
651
+ # potentially expensive intersection tests.
652
+ unless @assume_inner_follows_outer
653
+ case polygons_.size
654
+ when 0
655
+ # Skip this algorithm if there's no outer
656
+ when 1
657
+ # Shortcut if there's only one outer. Assume all the inners
658
+ # are members of this one polygon.
659
+ parts_.each do |part_data_|
660
+ unless part_data_[1]
661
+ polygons_[0] << part_data_
662
+ end
663
+ end
664
+ else
665
+ # Go through the remaining (inner) rings, and assign them to
666
+ # the correct polygon. For each inner ring, we find the outer
667
+ # ring containing it, and add it to that polygon's data. We
668
+ # check the initial guess first, and if it fails we go through
669
+ # the remaining polygons in order.
670
+ parts_.each do |part_data_|
671
+ unless part_data_[1]
672
+ # This will hold the polygon index for this inner ring.
673
+ parent_index_ = nil
674
+ # The initial guess. It could be -1 if this inner ring
675
+ # appeared before any outer rings had appeared.
676
+ first_try_ = part_data_[3]
677
+ if first_try_ >= 0 && part_data_[2].within?(polygons_[first_try_].first[2])
678
+ parent_index_ = first_try_
679
+ end
680
+ # If the initial guess didn't work, go through the
681
+ # remaining polygons and check their outer rings.
682
+ unless parent_index_
683
+ polygons_.each_with_index do |poly_data_, index_|
684
+ if index_ != first_try_ && part_data_[2].within?(poly_data_.first[2])
685
+ parent_index_ = index_
686
+ break
687
+ end
688
+ end
689
+ end
690
+ # If we found a match, append this inner ring to that
691
+ # polygon data. Otherwise, just throw away the inner ring.
692
+ if parent_index_
693
+ polygons_[parent_index_] << part_data_
694
+ end
695
+ end
696
+ end
697
+ end
698
+ end
699
+
700
+ # Generate the actual polygons from the collected polygon data
701
+ polygons_.map! do |poly_data_|
702
+ outer_ = poly_data_[0][0]
703
+ inner_ = poly_data_[1..-1].map{ |part_data_| part_data_[0] }
704
+ @factory.polygon(outer_, inner_)
705
+ end
706
+
707
+ # Finally, return the MultiPolygon.
708
+ @factory.multi_polygon(polygons_)
709
+ end
710
+
711
+
712
+ def _read_multipatch(data_) # :nodoc:
713
+ # Read counts
714
+ num_parts_, num_points_ = data_[36,8].unpack('VV')
715
+
716
+ # Read remaining data
717
+ values_ = data_[44, 32 + num_parts_*8 + num_points_*32].unpack("V#{num_parts_*2}E*")
718
+
719
+ # Parts arrays
720
+ part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]
721
+ part_types_ = values_.slice!(0, num_parts_)
722
+
723
+ # Extract XY, Z, and M values
724
+ xys_ = values_.slice!(0, num_points_*2)
725
+ zs_ = values_.slice!(2, num_points_)
726
+ zs_.map!{ |val_| val_ < NODATA_LIMIT ? 0 : val_ } if zs_
727
+ ms_ = values_.slice!(4, num_points_)
728
+ ms_.map!{ |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
729
+
730
+ # Generate points
731
+ points_ = (0..num_points_-1).map do |i_|
732
+ extras_ = []
733
+ extras_ << zs_[i_] if zs_ && @factory_supports_z
734
+ extras_ << ms_[i_] if ms_ && @factory_supports_m
735
+ @factory.point(xys_[i_*2], xys_[i_*2+1], *extras_)
736
+ end
737
+
738
+ # Create the parts
739
+ parts_ = (0..num_parts_-1).map do |i_|
740
+ ps_ = points_[part_indexes_[i_]...part_indexes_[i_+1]]
741
+ # All part types just translate directly into rings, except for
742
+ # triangle fan, which requires that we reorder the vertices.
743
+ if part_types_[i_] == 0
744
+ ps2_ = []
745
+ i2_ = 0
746
+ while i2_ < ps_.size
747
+ ps2_ << ps_[i2_]
748
+ i2_ += 2
749
+ end
750
+ i2_ -= 1
751
+ i2_ -= 2 if i2_ >= ps_.size
752
+ while i2_ > 0
753
+ ps2_ << ps_[i2_]
754
+ i2_ -= 2
755
+ end
756
+ ps_ = ps2_
757
+ end
758
+ @factory.linear_ring(ps_)
759
+ end
760
+
761
+ # Get a GEOS factory if needed.
762
+ geos_factory_ = nil
763
+ unless @assume_inner_follows_outer
764
+ geos_factory_ = Geos.factory
765
+ unless geos_factory_
766
+ raise Errors::RGeoError, "GEOS is not available, but is required for correct interpretation of polygons in shapefiles."
767
+ end
768
+ end
769
+
770
+ # Walk the parts and generate polygons
771
+ polygons_ = []
772
+ state_ = :empty
773
+ sequence_ = []
774
+ # We deliberately include num_parts_ so there's an extra iteration
775
+ # with a null part_ and type_. This is so the state handling block
776
+ # can finish up any currently live sequence.
777
+ (0..num_parts_).each do |index_|
778
+ part_ = parts_[index_]
779
+ type_ = part_types_[index_]
780
+
781
+ # This section handles any state.
782
+ # It either stays in the state and goes to the next part,
783
+ # or it wraps up the state. Either way, at the end of this
784
+ # case block, the state must be :empty.
785
+ case state_
786
+ when :outer
787
+ if type_ == 3
788
+ # Inner ring in an outer-led sequence.
789
+ # Just add it to the sequence and continue.
790
+ sequence_ << part_
791
+ next
792
+ else
793
+ # End of an outer-led sequence.
794
+ # Add the polygon and reset the state.
795
+ polygons_ << @factory.polygon(sequence_[0], sequence_[1..-1])
796
+ state_ = :empty
797
+ sequence_ = []
798
+ end
799
+ when :first
800
+ if type_ == 5
801
+ # Unknown ring in a first-led sequence.
802
+ # Just add it to the sequence and continue.
803
+ sequence_ << part_
804
+ else
805
+ # End of a first-led sequence.
806
+ # Need to determine which is the outer ring before we can
807
+ # add the polygon.
808
+ # If :assume_inner_follows_outer is in effect, we assume
809
+ # the first ring is the outer one. Otherwise, we have to
810
+ # use GEOS to determine containment.
811
+ unless @assume_inner_follows_outer
812
+ geos_polygons_ = sequence_.map{ |ring_| geos_factory_.polygon(ring_) }
813
+ outer_poly_ = nil
814
+ outer_index_ = 0
815
+ geos_polygons_.each_with_index do |poly_, index_|
816
+ if outer_poly_
817
+ if poly_.contains?(outer_poly_)
818
+ outer_poly_ = poly_
819
+ outer_index_ = index_
820
+ break;
821
+ end
822
+ else
823
+ outer_poly_ = poly_
824
+ end
825
+ end
826
+ sequence_.slice!(outer_index_)
827
+ sequence_.unshift(outer_poly_)
828
+ end
829
+ polygons_ << @factory.polygon(sequence_[0], sequence_[1..-1])
830
+ state_ = :empty
831
+ sequence_ = []
832
+ end
833
+ end
834
+
835
+ # State is now :empty. We allow any type except 3 (since an
836
+ # (inner must come during an outer-led sequence).
837
+ # We treat a type 5 ring that isn't part of a first-led sequence
838
+ # as an outer ring.
839
+ case type_
840
+ when 0, 1
841
+ polygons_ << @factory.polygon(part_)
842
+ when 2, 5
843
+ sequence_ << part_
844
+ state_ = :outer
845
+ when 4
846
+ sequence_ << part_
847
+ state_ = :first
848
+ end
849
+ end
850
+
851
+ # Return the geometry as a collection.
852
+ @factory.collection(polygons_)
853
+ end
854
+
855
+
856
+ # Shapefile records are provided to the caller as objects of this
857
+ # type. The record includes the record index (0-based), the
858
+ # geometry (which may be nil if the shape type is the null type),
859
+ # and a hash of attributes from the associated dbf file.
860
+ #
861
+ # You should not need to create objects of this type yourself.
862
+
863
+ class Record
864
+
865
+ def initialize(index_, geometry_, attributes_) # :nodoc:
866
+ @index = index_
867
+ @geometry = geometry_
868
+ @attributes = attributes_
869
+ end
870
+
871
+ # The 0-based record number
872
+ attr_reader :index
873
+
874
+ # The geometry contained in this shapefile record
875
+ attr_reader :geometry
876
+
877
+ # The attributes as a hash.
878
+ attr_reader :attributes
879
+
880
+ # Returns an array of keys for all this record's attributes.
881
+ def keys
882
+ @attributes.keys
883
+ end
884
+
885
+ # Returns the value for the given attribute key.
886
+ def [](key_)
887
+ @attributes[key_]
888
+ end
889
+
890
+ end
891
+
892
+
893
+ end
894
+
895
+
896
+ end
897
+
898
+ end