rgeo 2.3.1 → 3.0.0.pre.rc.2
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.
- 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