rgeo 0.1.17 → 0.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +8 -0
- data/README.rdoc +40 -21
- data/Version +1 -1
- data/lib/rgeo.rb +1 -0
- data/lib/rgeo/cartesian.rb +1 -0
- data/lib/rgeo/cartesian/analysis.rb +115 -0
- data/lib/rgeo/cartesian/feature_classes.rb +0 -14
- data/lib/rgeo/features/factory.rb +5 -5
- data/lib/rgeo/features/factory_generator.rb +10 -0
- data/lib/rgeo/geo_json/coder.rb +49 -14
- data/lib/rgeo/geo_json/interface.rb +6 -6
- data/lib/rgeo/shapefile.rb +60 -0
- data/lib/rgeo/shapefile/reader.rb +898 -0
- data/tests/shapefile/shapelib_testcases/readme.txt +11 -0
- data/tests/shapefile/shapelib_testcases/test.dbf +0 -0
- data/tests/shapefile/shapelib_testcases/test.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test0.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test0.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test1.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test1.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test10.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test10.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test11.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test11.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test12.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test12.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test13.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test13.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test2.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test2.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test3.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test3.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test4.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test4.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test5.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test5.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test6.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test6.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test7.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test7.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test8.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test8.shx +0 -0
- data/tests/shapefile/shapelib_testcases/test9.shp +0 -0
- data/tests/shapefile/shapelib_testcases/test9.shx +0 -0
- data/tests/shapefile/tc_shapelib_tests.rb +527 -0
- data/tests/tc_cartesian_analysis.rb +107 -0
- data/tests/tc_oneoff.rb +1 -0
- 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
|