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.
- data/History.rdoc +7 -0
- data/README.rdoc +108 -0
- data/Version +1 -0
- data/lib/rgeo/shapefile.rb +62 -0
- data/lib/rgeo/shapefile/reader.rb +902 -0
- data/test/shapelib_testcases/readme.txt +11 -0
- data/test/shapelib_testcases/test.dbf +0 -0
- data/test/shapelib_testcases/test.shp +0 -0
- data/test/shapelib_testcases/test.shx +0 -0
- data/test/shapelib_testcases/test0.shp +0 -0
- data/test/shapelib_testcases/test0.shx +0 -0
- data/test/shapelib_testcases/test1.shp +0 -0
- data/test/shapelib_testcases/test1.shx +0 -0
- data/test/shapelib_testcases/test10.shp +0 -0
- data/test/shapelib_testcases/test10.shx +0 -0
- data/test/shapelib_testcases/test11.shp +0 -0
- data/test/shapelib_testcases/test11.shx +0 -0
- data/test/shapelib_testcases/test12.shp +0 -0
- data/test/shapelib_testcases/test12.shx +0 -0
- data/test/shapelib_testcases/test13.shp +0 -0
- data/test/shapelib_testcases/test13.shx +0 -0
- data/test/shapelib_testcases/test2.shp +0 -0
- data/test/shapelib_testcases/test2.shx +0 -0
- data/test/shapelib_testcases/test3.shp +0 -0
- data/test/shapelib_testcases/test3.shx +0 -0
- data/test/shapelib_testcases/test4.shp +0 -0
- data/test/shapelib_testcases/test4.shx +0 -0
- data/test/shapelib_testcases/test5.shp +0 -0
- data/test/shapelib_testcases/test5.shx +0 -0
- data/test/shapelib_testcases/test6.shp +0 -0
- data/test/shapelib_testcases/test6.shx +0 -0
- data/test/shapelib_testcases/test7.shp +0 -0
- data/test/shapelib_testcases/test7.shx +0 -0
- data/test/shapelib_testcases/test8.shp +0 -0
- data/test/shapelib_testcases/test8.shx +0 -0
- data/test/shapelib_testcases/test9.shp +0 -0
- data/test/shapelib_testcases/test9.shx +0 -0
- data/test/tc_shapelib_tests.rb +527 -0
- 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
|