rgeo 2.3.1 → 3.0.0.pre.rc.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/README.md +11 -10
- data/ext/geos_c_impl/analysis.c +8 -6
- data/ext/geos_c_impl/analysis.h +1 -3
- data/ext/geos_c_impl/errors.c +10 -8
- data/ext/geos_c_impl/errors.h +7 -3
- data/ext/geos_c_impl/extconf.rb +3 -0
- data/ext/geos_c_impl/factory.c +273 -202
- data/ext/geos_c_impl/factory.h +51 -63
- data/ext/geos_c_impl/geometry.c +124 -22
- data/ext/geos_c_impl/geometry.h +8 -3
- data/ext/geos_c_impl/geometry_collection.c +81 -185
- data/ext/geos_c_impl/geometry_collection.h +1 -14
- data/ext/geos_c_impl/globals.c +91 -0
- data/ext/geos_c_impl/globals.h +45 -0
- data/ext/geos_c_impl/line_string.c +28 -29
- data/ext/geos_c_impl/line_string.h +1 -3
- data/ext/geos_c_impl/main.c +10 -9
- data/ext/geos_c_impl/point.c +9 -8
- data/ext/geos_c_impl/point.h +1 -3
- data/ext/geos_c_impl/polygon.c +43 -72
- data/ext/geos_c_impl/polygon.h +1 -3
- data/ext/geos_c_impl/preface.h +12 -0
- data/ext/geos_c_impl/ruby_more.c +65 -0
- data/ext/geos_c_impl/ruby_more.h +16 -0
- data/lib/rgeo/cartesian/calculations.rb +54 -17
- data/lib/rgeo/cartesian/factory.rb +6 -14
- data/lib/rgeo/cartesian/feature_classes.rb +68 -46
- data/lib/rgeo/cartesian/feature_methods.rb +67 -20
- data/lib/rgeo/cartesian/interface.rb +0 -36
- data/lib/rgeo/cartesian/planar_graph.rb +379 -0
- data/lib/rgeo/cartesian/sweepline_intersector.rb +149 -0
- data/lib/rgeo/cartesian/valid_op.rb +71 -0
- data/lib/rgeo/cartesian.rb +3 -0
- data/lib/rgeo/coord_sys/cs/wkt_parser.rb +6 -6
- data/lib/rgeo/coord_sys.rb +0 -11
- data/lib/rgeo/error.rb +15 -0
- data/lib/rgeo/feature/factory_generator.rb +0 -3
- data/lib/rgeo/feature/geometry.rb +107 -28
- data/lib/rgeo/feature/geometry_collection.rb +13 -5
- data/lib/rgeo/feature/line_string.rb +3 -3
- data/lib/rgeo/feature/multi_surface.rb +3 -3
- data/lib/rgeo/feature/point.rb +4 -4
- data/lib/rgeo/feature/surface.rb +3 -3
- data/lib/rgeo/geographic/factory.rb +6 -7
- data/lib/rgeo/geographic/interface.rb +6 -49
- data/lib/rgeo/geographic/proj4_projector.rb +0 -2
- data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
- data/lib/rgeo/geographic/projected_feature_methods.rb +67 -28
- data/lib/rgeo/geographic/simple_mercator_projector.rb +0 -2
- data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
- data/lib/rgeo/geographic/spherical_feature_methods.rb +79 -2
- data/lib/rgeo/geos/capi_factory.rb +21 -38
- data/lib/rgeo/geos/capi_feature_classes.rb +54 -11
- data/lib/rgeo/geos/ffi_factory.rb +6 -35
- data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
- data/lib/rgeo/geos/ffi_feature_methods.rb +39 -5
- data/lib/rgeo/geos/interface.rb +0 -24
- data/lib/rgeo/geos/zm_factory.rb +0 -19
- data/lib/rgeo/geos/zm_feature_methods.rb +16 -0
- data/lib/rgeo/geos.rb +6 -3
- data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +4 -4
- data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -1
- data/lib/rgeo/impl_helper/basic_line_string_methods.rb +15 -19
- data/lib/rgeo/impl_helper/basic_point_methods.rb +1 -1
- data/lib/rgeo/impl_helper/basic_polygon_methods.rb +1 -1
- data/lib/rgeo/impl_helper/valid_op.rb +354 -0
- data/lib/rgeo/impl_helper/validity_check.rb +139 -0
- data/lib/rgeo/impl_helper.rb +1 -0
- data/lib/rgeo/version.rb +1 -1
- metadata +45 -9
- data/lib/rgeo/coord_sys/srs_database/entry.rb +0 -107
- data/lib/rgeo/coord_sys/srs_database/sr_org.rb +0 -64
- data/lib/rgeo/coord_sys/srs_database/url_reader.rb +0 -65
@@ -45,6 +45,14 @@ module RGeo
|
|
45
45
|
@zgeometry.dimension
|
46
46
|
end
|
47
47
|
|
48
|
+
def coordinate_dimension
|
49
|
+
4
|
50
|
+
end
|
51
|
+
|
52
|
+
def spatial_dimension
|
53
|
+
3
|
54
|
+
end
|
55
|
+
|
48
56
|
def geometry_type
|
49
57
|
@zgeometry.geometry_type
|
50
58
|
end
|
@@ -83,6 +91,14 @@ module RGeo
|
|
83
91
|
simple?
|
84
92
|
end
|
85
93
|
|
94
|
+
def is_3d?
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def measured?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
86
102
|
def boundary
|
87
103
|
@factory.create_feature(nil, @zgeometry.boundary, @mgeometry.boundary)
|
88
104
|
end
|
data/lib/rgeo/geos.rb
CHANGED
@@ -33,9 +33,6 @@ module RGeo
|
|
33
33
|
require_relative "geos/capi_feature_classes"
|
34
34
|
require_relative "geos/capi_factory"
|
35
35
|
end
|
36
|
-
require_relative "geos/ffi_feature_methods"
|
37
|
-
require_relative "geos/ffi_feature_classes"
|
38
|
-
require_relative "geos/ffi_factory"
|
39
36
|
require_relative "geos/zm_feature_methods"
|
40
37
|
require_relative "geos/zm_feature_classes"
|
41
38
|
require_relative "geos/zm_factory"
|
@@ -56,6 +53,12 @@ module RGeo
|
|
56
53
|
FFI_SUPPORT_EXCEPTION = ex
|
57
54
|
end
|
58
55
|
|
56
|
+
if FFI_SUPPORTED
|
57
|
+
require_relative "geos/ffi_feature_methods"
|
58
|
+
require_relative "geos/ffi_feature_classes"
|
59
|
+
require_relative "geos/ffi_factory"
|
60
|
+
end
|
61
|
+
|
59
62
|
# Default preferred native interface
|
60
63
|
if CAPI_SUPPORTED
|
61
64
|
self.preferred_native_interface = :capi
|
@@ -20,7 +20,7 @@ module RGeo
|
|
20
20
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
21
21
|
elem
|
22
22
|
end
|
23
|
-
|
23
|
+
init_geometry
|
24
24
|
end
|
25
25
|
|
26
26
|
def num_geometries
|
@@ -91,7 +91,7 @@ module RGeo
|
|
91
91
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
92
92
|
elem
|
93
93
|
end
|
94
|
-
|
94
|
+
init_geometry
|
95
95
|
end
|
96
96
|
|
97
97
|
def geometry_type
|
@@ -152,7 +152,7 @@ module RGeo
|
|
152
152
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
153
153
|
elem
|
154
154
|
end
|
155
|
-
|
155
|
+
init_geometry
|
156
156
|
end
|
157
157
|
|
158
158
|
def geometry_type
|
@@ -176,7 +176,7 @@ module RGeo
|
|
176
176
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
177
177
|
elem
|
178
178
|
end
|
179
|
-
|
179
|
+
init_geometry
|
180
180
|
end
|
181
181
|
|
182
182
|
def geometry_type
|
@@ -16,7 +16,13 @@ module RGeo
|
|
16
16
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
17
17
|
elem
|
18
18
|
end
|
19
|
-
|
19
|
+
# LineStrings in general need to check that there's not one point
|
20
|
+
# GEOS doesn't allow instantiation of single point LineStrings so
|
21
|
+
# we should handle it.
|
22
|
+
if @points.size == 1
|
23
|
+
raise Error::InvalidGeometry, "LineString Cannot Have 1 Point"
|
24
|
+
end
|
25
|
+
init_geometry
|
20
26
|
end
|
21
27
|
|
22
28
|
def num_points
|
@@ -143,12 +149,6 @@ module RGeo
|
|
143
149
|
super
|
144
150
|
@points = obj.points
|
145
151
|
end
|
146
|
-
|
147
|
-
def validate_geometry
|
148
|
-
if @points.size == 1
|
149
|
-
raise Error::InvalidGeometry, "LineString cannot have 1 point"
|
150
|
-
end
|
151
|
-
end
|
152
152
|
end
|
153
153
|
|
154
154
|
module BasicLineMethods # :nodoc:
|
@@ -161,7 +161,7 @@ module RGeo
|
|
161
161
|
cstop = Feature.cast(stop, factory, Feature::Point)
|
162
162
|
raise Error::InvalidGeometry, "Could not cast end: #{stop}" unless cstop
|
163
163
|
@points = [cstart, cstop]
|
164
|
-
|
164
|
+
init_geometry
|
165
165
|
end
|
166
166
|
|
167
167
|
def geometry_type
|
@@ -171,18 +171,16 @@ module RGeo
|
|
171
171
|
def coordinates
|
172
172
|
@points.map(&:coordinates)
|
173
173
|
end
|
174
|
+
end
|
174
175
|
|
175
|
-
|
176
|
-
|
177
|
-
def validate_geometry
|
176
|
+
module BasicLinearRingMethods # :nodoc:
|
177
|
+
def initialize(factory, points)
|
178
178
|
super
|
179
|
-
|
180
|
-
raise Error::InvalidGeometry, "
|
179
|
+
unless @points.size >= 4 || @points.size == 0
|
180
|
+
raise Error::InvalidGeometry, "LinearRings must have 0 or >= 4 points"
|
181
181
|
end
|
182
182
|
end
|
183
|
-
end
|
184
183
|
|
185
|
-
module BasicLinearRingMethods # :nodoc:
|
186
184
|
def geometry_type
|
187
185
|
Feature::LinearRing
|
188
186
|
end
|
@@ -193,14 +191,12 @@ module RGeo
|
|
193
191
|
|
194
192
|
private
|
195
193
|
|
196
|
-
|
194
|
+
# Close ring if necessary.
|
195
|
+
def init_geometry
|
197
196
|
super
|
198
197
|
if @points.size > 0
|
199
198
|
@points << @points.first if @points.first != @points.last
|
200
199
|
@points = @points.chunk { |x| x }.map(&:first)
|
201
|
-
if !@factory.property(:uses_lenient_assertions) && !ring?
|
202
|
-
raise Error::InvalidGeometry, "LinearRing failed ring test"
|
203
|
-
end
|
204
200
|
end
|
205
201
|
end
|
206
202
|
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module RGeo
|
6
|
+
module ImplHelper
|
7
|
+
# Mixin based off of the JTS/GEOS IsValidOp class.
|
8
|
+
# Implements #valid? and #invalid_reason on Features that include this.
|
9
|
+
#
|
10
|
+
# @see https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/valid/IsValidOp.java
|
11
|
+
module ValidOp
|
12
|
+
# Validity of geometry
|
13
|
+
#
|
14
|
+
# @return Boolean
|
15
|
+
def valid?
|
16
|
+
invalid_reason.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reason for invalidity or nil if valid
|
20
|
+
#
|
21
|
+
# @return String
|
22
|
+
def invalid_reason
|
23
|
+
return @invalid_reason if defined?(@invalid_reason)
|
24
|
+
@invalid_reason = check_valid
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validity_helper
|
30
|
+
ValidOpHelpers
|
31
|
+
end
|
32
|
+
|
33
|
+
# Method that performs validity checking. Just checks the type of geometry
|
34
|
+
# and delegates to the proper validity checker.
|
35
|
+
#
|
36
|
+
# Returns a string describing the error or nil if it's a valid geometry.
|
37
|
+
# In some cases, "Unkown Validity" is returned if a dependent method has
|
38
|
+
# not been implemented.
|
39
|
+
#
|
40
|
+
# @return String
|
41
|
+
def check_valid
|
42
|
+
case self
|
43
|
+
when Feature::Point
|
44
|
+
check_valid_point
|
45
|
+
when Feature::LinearRing
|
46
|
+
check_valid_linear_ring
|
47
|
+
when Feature::LineString
|
48
|
+
check_valid_line_string
|
49
|
+
when Feature::Polygon
|
50
|
+
check_valid_polygon
|
51
|
+
when Feature::MultiPoint
|
52
|
+
check_valid_multi_point
|
53
|
+
when Feature::MultiPolygon
|
54
|
+
check_valid_multi_polygon
|
55
|
+
when Feature::GeometryCollection
|
56
|
+
check_valid_geometry_collection
|
57
|
+
else
|
58
|
+
raise NotImplementedError, "check_valid is not implemented for #{self}"
|
59
|
+
end
|
60
|
+
rescue RGeo::Error::UnsupportedOperation, NoMethodError
|
61
|
+
"Unkown Validity"
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_valid_point
|
65
|
+
validity_helper.check_invalid_coordinate(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_valid_line_string
|
69
|
+
# check coordinates are all valid
|
70
|
+
points.each do |pt|
|
71
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
72
|
+
return check unless check.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
# check more than 1 point
|
76
|
+
return Error::TOO_FEW_POINTS unless num_points > 1
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_valid_linear_ring
|
82
|
+
# check coordinates are all valid
|
83
|
+
points.each do |pt|
|
84
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
85
|
+
return check unless check.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# check closed
|
89
|
+
return Error::UNCLOSED_RING unless closed?
|
90
|
+
|
91
|
+
# check more than 3 points
|
92
|
+
return Error::TOO_FEW_POINTS unless num_points > 3
|
93
|
+
|
94
|
+
# check no self-intersections
|
95
|
+
validity_helper.check_no_self_intersections(self)
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_valid_polygon
|
99
|
+
# check coordinates are all valid
|
100
|
+
exterior_ring.points.each do |pt|
|
101
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
102
|
+
return check unless check.nil?
|
103
|
+
end
|
104
|
+
interior_rings.each do |ring|
|
105
|
+
ring.points.each do |pt|
|
106
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
107
|
+
return check unless check.nil?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# check closed
|
112
|
+
return Error::UNCLOSED_RING unless exterior_ring.closed?
|
113
|
+
return Error::UNCLOSED_RING unless interior_rings.all?(&:closed?)
|
114
|
+
|
115
|
+
# check more than 3 points in each ring
|
116
|
+
return Error::TOO_FEW_POINTS unless exterior_ring.num_points > 3
|
117
|
+
return Error::TOO_FEW_POINTS unless interior_rings.all? { |r| r.num_points > 3 }
|
118
|
+
|
119
|
+
# can skip this check if there's no holes
|
120
|
+
unless interior_rings.empty?
|
121
|
+
check = validity_helper.check_consistent_area(self)
|
122
|
+
return check unless check.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
# check that there are no self-intersections
|
126
|
+
check = validity_helper.check_no_self_intersecting_rings(self)
|
127
|
+
return check unless check.nil?
|
128
|
+
|
129
|
+
# can skip these checks if there's no holes
|
130
|
+
unless interior_rings.empty?
|
131
|
+
check = validity_helper.check_holes_in_shell(self)
|
132
|
+
return check unless check.nil?
|
133
|
+
|
134
|
+
check = validity_helper.check_holes_not_nested(self)
|
135
|
+
return check unless check.nil?
|
136
|
+
|
137
|
+
check = validity_helper.check_connected_interiors(self)
|
138
|
+
return check unless check.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def check_valid_multi_point
|
145
|
+
geometries.each do |pt|
|
146
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
147
|
+
return check unless check.nil?
|
148
|
+
end
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def check_valid_multi_polygon
|
153
|
+
geometries.each do |poly|
|
154
|
+
return poly.invalid_reason unless poly.invalid_reason.nil?
|
155
|
+
end
|
156
|
+
|
157
|
+
check = validity_helper.check_consistent_area_mp(self)
|
158
|
+
return check unless check.nil?
|
159
|
+
|
160
|
+
# check no shells are nested
|
161
|
+
check = validity_helper.check_shells_not_nested(self)
|
162
|
+
return check unless check.nil?
|
163
|
+
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def check_valid_geometry_collection
|
168
|
+
geometries.each do |geom|
|
169
|
+
return geom.invalid_reason unless geom.invalid_reason.nil?
|
170
|
+
end
|
171
|
+
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Helper functions for specific validity checks
|
178
|
+
##
|
179
|
+
module ValidOpHelpers
|
180
|
+
module_function
|
181
|
+
|
182
|
+
# Checks that the given point has valid coordinates.
|
183
|
+
#
|
184
|
+
# @param pt [RGeo::Feature::Point]
|
185
|
+
#
|
186
|
+
# @return [String] invalid_reason
|
187
|
+
def check_invalid_coordinate(pt)
|
188
|
+
x = pt.x
|
189
|
+
y = pt.y
|
190
|
+
return if x.finite? && y.finite? && x.real? && y.real?
|
191
|
+
|
192
|
+
Error::INVALID_COORDINATE
|
193
|
+
end
|
194
|
+
|
195
|
+
# Checks that the edges in the polygon form a consistent area.
|
196
|
+
#
|
197
|
+
# Specifically, checks that there are intersections no between the
|
198
|
+
# holes and the shell.
|
199
|
+
#
|
200
|
+
# Also checks that there are no duplicate rings.
|
201
|
+
#
|
202
|
+
# @param poly [RGeo::Feature::Polygon]
|
203
|
+
#
|
204
|
+
# @return [String] invalid_reason
|
205
|
+
def check_consistent_area(poly)
|
206
|
+
# Holes don't cross exterior check.
|
207
|
+
exterior = poly.exterior_ring
|
208
|
+
poly.interior_rings.each do |ring|
|
209
|
+
return Error::SELF_INTERSECTION if ring.crosses?(exterior)
|
210
|
+
end
|
211
|
+
|
212
|
+
# check interiors do not cross
|
213
|
+
poly.interior_rings.combination(2).each do |ring1, ring2|
|
214
|
+
return Error::SELF_INTERSECTION if ring1.crosses?(ring2)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Duplicate rings check
|
218
|
+
rings = [exterior] + poly.interior_rings
|
219
|
+
return Error::SELF_INTERSECTION if rings.uniq.size != rings.size
|
220
|
+
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
|
224
|
+
# Checks that the ring does not self-intersect. This is just a simplicity
|
225
|
+
# check on the ring.
|
226
|
+
#
|
227
|
+
# @param ring [RGeo::Feature::LinearRing]
|
228
|
+
#
|
229
|
+
# @return [String] invalid_reason
|
230
|
+
def check_no_self_intersections(ring)
|
231
|
+
return Error::SELF_INTERSECTION unless ring.simple?
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check that rings do not self intersect in a polygon
|
235
|
+
#
|
236
|
+
# @param poly [RGeo::Feature::Polygon]
|
237
|
+
#
|
238
|
+
# @return [String] invalid_reason
|
239
|
+
def check_no_self_intersecting_rings(poly)
|
240
|
+
exterior = poly.exterior_ring
|
241
|
+
|
242
|
+
check = check_no_self_intersections(exterior)
|
243
|
+
return check unless check.nil?
|
244
|
+
|
245
|
+
poly.interior_rings.each do |ring|
|
246
|
+
check = check_no_self_intersections(ring)
|
247
|
+
return check unless check.nil?
|
248
|
+
end
|
249
|
+
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# Checks holes are contained inside the exterior of a polygon.
|
254
|
+
# Assuming check_consistent_area has already passed on the polygon,
|
255
|
+
# a simple point in polygon check can be done on one of the points
|
256
|
+
# in each hole to verify (since we know none of them intersect).
|
257
|
+
#
|
258
|
+
# @param poly [RGeo::Feature::Polygon]
|
259
|
+
#
|
260
|
+
# @return [String] invalid_reason
|
261
|
+
def check_holes_in_shell(poly)
|
262
|
+
# get hole-less shell as test polygon
|
263
|
+
shell = poly.exterior_ring
|
264
|
+
shell = shell.factory.polygon(shell)
|
265
|
+
|
266
|
+
poly.interior_rings.each do |interior|
|
267
|
+
test_pt = interior.start_point
|
268
|
+
unless shell.contains?(test_pt) || poly.exterior_ring.contains?(test_pt)
|
269
|
+
return Error::HOLE_OUTSIDE_SHELL
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
|
276
|
+
# Checks that holes are not nested within each other.
|
277
|
+
#
|
278
|
+
# @param poly [RGeo::Feature::Polygon]
|
279
|
+
#
|
280
|
+
# @return [String] invalid_reason
|
281
|
+
def check_holes_not_nested(poly)
|
282
|
+
# convert holes from linear_rings to polygons
|
283
|
+
# Same logic that applies to check_holes_in_shell applies here
|
284
|
+
# since we've already passed the consistent area test, we just
|
285
|
+
# have to check if one point from each hole is contained in the other.
|
286
|
+
holes = poly.interior_rings
|
287
|
+
holes = holes.map { |v| v.factory.polygon(v) }
|
288
|
+
holes.combination(2).each do |p1, p2|
|
289
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
290
|
+
return Error::NESTED_HOLES
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
nil
|
295
|
+
end
|
296
|
+
|
297
|
+
# Checks that the interior of the polygon is connected.
|
298
|
+
# A disconnected interior can be described by this polygon for example
|
299
|
+
# POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (5 0, 10 5, 5 10, 0 5, 5 0))
|
300
|
+
#
|
301
|
+
# Which is a square with a diamond inside of it.
|
302
|
+
#
|
303
|
+
# @param poly [RGeo::Feature::Polygon]
|
304
|
+
#
|
305
|
+
# @return [String] invalid_reason
|
306
|
+
def check_connected_interiors(poly)
|
307
|
+
# This is not proper and will flag valid geometries as invalid, but
|
308
|
+
# is an ok approximation.
|
309
|
+
# Idea is to check if a single hole has multiple points on the
|
310
|
+
# exterior ring.
|
311
|
+
poly.interior_rings.each do |ring|
|
312
|
+
touches = Set.new
|
313
|
+
ring.points.each do |pt|
|
314
|
+
touches.add(pt) if poly.exterior_ring.contains?(pt)
|
315
|
+
end
|
316
|
+
|
317
|
+
return Error::DISCONNECTED_INTERIOR if touches.size > 1
|
318
|
+
end
|
319
|
+
|
320
|
+
nil
|
321
|
+
end
|
322
|
+
|
323
|
+
# Checks that polygons do not intersect in a multipolygon.
|
324
|
+
#
|
325
|
+
# @param mp [RGeo::Feature::MultiPolygon]
|
326
|
+
#
|
327
|
+
# @return [String] invalid_reason
|
328
|
+
def check_consistent_area_mp(mp)
|
329
|
+
mp.geometries.combination(2) do |p1, p2|
|
330
|
+
if p1.exterior_ring.crosses?(p2.exterior_ring)
|
331
|
+
return Error::SELF_INTERSECTION
|
332
|
+
end
|
333
|
+
end
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
# Checks that individual polygons within a multipolygon are not nested.
|
338
|
+
#
|
339
|
+
# @param mp [RGeo::Feature::MultiPolygon]
|
340
|
+
#
|
341
|
+
# @return [String] invalid_reason
|
342
|
+
def check_shells_not_nested(mp)
|
343
|
+
# Since we've passed the consistent area test, we can just check
|
344
|
+
# that one point lies in the other.
|
345
|
+
mp.geometries.combination(2) do |p1, p2|
|
346
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
347
|
+
return Error::NESTED_SHELLS
|
348
|
+
end
|
349
|
+
end
|
350
|
+
nil
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RGeo
|
4
|
+
module ImplHelper
|
5
|
+
# This helper enforces valid geometry computation, avoiding results such
|
6
|
+
# as a 0 area for a bowtie shaped polygon. Implementations that are part
|
7
|
+
# of RGeo core should all include this.
|
8
|
+
#
|
9
|
+
# You can play around validity checks if needed:
|
10
|
+
#
|
11
|
+
# - {check_validity!} is the method that will raise if your geometry is
|
12
|
+
# not valid. Its message will be the same as {invalid_reason}.
|
13
|
+
# - {make_valid} is the method you can call to get a valid copy of the
|
14
|
+
# current geometry.
|
15
|
+
# - finally, you can bypass any checked method by prepending `unsafe_` to
|
16
|
+
# it. At your own risk.
|
17
|
+
module ValidityCheck
|
18
|
+
# Every method that should not be overriden by the validity check.
|
19
|
+
# Those methods are either accessors or very basic methods not related
|
20
|
+
# to validity checks, or are used to check validity, in which case the
|
21
|
+
# `true/false` gives a correct information, no need to raise).
|
22
|
+
UNCHECKED_METHODS = [
|
23
|
+
# Basic methods
|
24
|
+
:factory, :geometry_type, :as_text, :as_binary, :srid,
|
25
|
+
:dimension, :coordinate_dimension, :spatial_dimension,
|
26
|
+
# Tests
|
27
|
+
:simple?, :closed?, :empty?, :is_3d?, :measured?,
|
28
|
+
# Accessors
|
29
|
+
:exterior_ring, :interior_rings, :[], :num_geometries, :num_interior_rings,
|
30
|
+
:geometry_n, :each, :points, :point_n, :start_point, :end_point, :x, :y, :z, :m,
|
31
|
+
# Trivial methods
|
32
|
+
:num_points, :locate_along, :locate_between,
|
33
|
+
# Comparison
|
34
|
+
:equals?, :rep_equals?, :eql?, :==, :'!='
|
35
|
+
].freeze
|
36
|
+
private_constant :UNCHECKED_METHODS
|
37
|
+
|
38
|
+
# Since methods have their unsafe_ counter part, it means that the `+`
|
39
|
+
# method would lead to having an `unsafe_+` method that is not simply
|
40
|
+
# callable. Here's a simple fallback:
|
41
|
+
SYMBOL2NAME = {
|
42
|
+
:+ => "add",
|
43
|
+
:- => "remove",
|
44
|
+
:* => "multiply"
|
45
|
+
}.tap { |h| h.default_proc = ->(_, key) { key.to_s } }.freeze
|
46
|
+
private_constant :SYMBOL2NAME
|
47
|
+
|
48
|
+
class << self
|
49
|
+
# Note for contributors: this should be called after all methods
|
50
|
+
# are loaded for a given feature classe. No worries though, this
|
51
|
+
# is tested.
|
52
|
+
def override_classes # :nodoc:
|
53
|
+
# Using pop here to be thread safe.
|
54
|
+
while (klass = classes.pop)
|
55
|
+
override(klass)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def included(klass) # :nodoc:
|
60
|
+
classes << klass
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def classes
|
66
|
+
@classes ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def override(klass)
|
70
|
+
methods_to_check = feature_methods(klass)
|
71
|
+
|
72
|
+
klass.class_eval do
|
73
|
+
methods_to_check.each do |method_sym|
|
74
|
+
copy = "unsafe_#{SYMBOL2NAME[method_sym]}".to_sym
|
75
|
+
alias_method copy, method_sym
|
76
|
+
undef_method method_sym
|
77
|
+
define_method(method_sym) do |*args|
|
78
|
+
check_validity!
|
79
|
+
args.each do |arg|
|
80
|
+
arg.check_validity! if RGeo::Feature::Geometry.check_type(arg)
|
81
|
+
end
|
82
|
+
method(copy).call(*args)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def feature_methods(klass)
|
89
|
+
feature_defs = Set.new
|
90
|
+
klass
|
91
|
+
.ancestors
|
92
|
+
.select { |ancestor| ancestor <= RGeo::Feature::Geometry }
|
93
|
+
.each { |ancestor| feature_defs.merge(ancestor.instance_methods(false)) }
|
94
|
+
feature_defs & klass.instance_methods - UNCHECKED_METHODS
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Raises {invalid_reason} if the polygon is not valid, does nothing
|
99
|
+
# otherwise.
|
100
|
+
def check_validity!
|
101
|
+
# This method will use a cached invalid_reason for performance purposes.
|
102
|
+
# DO NOT MUTATE GEOMETRIES.
|
103
|
+
return unless invalid_reason_memo
|
104
|
+
|
105
|
+
raise Error::InvalidGeometry, invalid_reason_memo
|
106
|
+
end
|
107
|
+
|
108
|
+
# Tell why the geometry is not valid, `nil` means it is valid.
|
109
|
+
def invalid_reason
|
110
|
+
if defined?(super) == "super"
|
111
|
+
raise Error::RGeoError, "ValidityCheck MUST be loaded before " \
|
112
|
+
"definition of #{self.class}##{__method__}."
|
113
|
+
end
|
114
|
+
|
115
|
+
raise Error::UnsupportedOperation, "Method #{self.class}##{__method__} not defined."
|
116
|
+
end
|
117
|
+
|
118
|
+
# Try and make the geometry valid, this may change its shape.
|
119
|
+
# Returns a valid copy of the geometry.
|
120
|
+
def make_valid
|
121
|
+
if defined?(super) == "super"
|
122
|
+
raise Error::RGeoError, "ValidityCheck MUST be loaded before " \
|
123
|
+
"definition of #{self.class}##{__method__}."
|
124
|
+
end
|
125
|
+
|
126
|
+
raise Error::UnsupportedOperation, "Method #{self.class}##{__method__} not defined."
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def invalid_reason_memo
|
132
|
+
# `defined?` is a bit faster than `instance_variable_defined?`.
|
133
|
+
return @invalid_reason_memo if defined?(@invalid_reason_memo)
|
134
|
+
|
135
|
+
@invalid_reason_memo = invalid_reason
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/rgeo/impl_helper.rb
CHANGED
data/lib/rgeo/version.rb
CHANGED