rgeo-shapefile 0.2.0

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 (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