rgeo-shapefile 0.2.0

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