rgeo 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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