rgeo 2.3.1 → 3.0.1
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 +23 -14
- data/ext/geos_c_impl/analysis.c +30 -25
- data/ext/geos_c_impl/analysis.h +8 -7
- data/ext/geos_c_impl/coordinates.c +27 -21
- data/ext/geos_c_impl/coordinates.h +5 -2
- data/ext/geos_c_impl/errors.c +19 -10
- data/ext/geos_c_impl/errors.h +11 -4
- data/ext/geos_c_impl/extconf.rb +42 -28
- data/ext/geos_c_impl/factory.c +540 -451
- data/ext/geos_c_impl/factory.h +105 -95
- data/ext/geos_c_impl/geometry.c +593 -387
- data/ext/geos_c_impl/geometry.h +10 -5
- data/ext/geos_c_impl/geometry_collection.c +306 -339
- data/ext/geos_c_impl/geometry_collection.h +6 -20
- data/ext/geos_c_impl/globals.c +169 -0
- data/ext/geos_c_impl/globals.h +46 -0
- data/ext/geos_c_impl/line_string.c +271 -231
- data/ext/geos_c_impl/line_string.h +5 -8
- data/ext/geos_c_impl/main.c +16 -16
- data/ext/geos_c_impl/point.c +65 -67
- data/ext/geos_c_impl/point.h +4 -7
- data/ext/geos_c_impl/polygon.c +137 -135
- data/ext/geos_c_impl/polygon.h +11 -11
- data/ext/geos_c_impl/preface.h +16 -10
- data/ext/geos_c_impl/ruby_more.c +67 -0
- data/ext/geos_c_impl/ruby_more.h +25 -0
- data/lib/rgeo/cartesian/analysis.rb +5 -3
- data/lib/rgeo/cartesian/bounding_box.rb +74 -79
- data/lib/rgeo/cartesian/calculations.rb +64 -33
- data/lib/rgeo/cartesian/factory.rb +57 -102
- data/lib/rgeo/cartesian/feature_classes.rb +68 -46
- data/lib/rgeo/cartesian/feature_methods.rb +67 -25
- data/lib/rgeo/cartesian/interface.rb +6 -41
- data/lib/rgeo/cartesian/planar_graph.rb +373 -0
- data/lib/rgeo/cartesian/sweepline_intersector.rb +147 -0
- data/lib/rgeo/cartesian/valid_op.rb +69 -0
- data/lib/rgeo/cartesian.rb +3 -0
- data/lib/rgeo/coord_sys/cs/entities.rb +303 -99
- data/lib/rgeo/coord_sys/cs/factories.rb +0 -2
- data/lib/rgeo/coord_sys/cs/wkt_parser.rb +90 -42
- data/lib/rgeo/coord_sys.rb +1 -20
- data/lib/rgeo/error.rb +15 -0
- data/lib/rgeo/feature/curve.rb +0 -11
- data/lib/rgeo/feature/factory.rb +26 -36
- data/lib/rgeo/feature/factory_generator.rb +6 -14
- data/lib/rgeo/feature/geometry.rb +146 -66
- data/lib/rgeo/feature/geometry_collection.rb +16 -9
- data/lib/rgeo/feature/line_string.rb +4 -5
- data/lib/rgeo/feature/linear_ring.rb +0 -1
- data/lib/rgeo/feature/multi_curve.rb +0 -6
- data/lib/rgeo/feature/multi_surface.rb +3 -4
- data/lib/rgeo/feature/point.rb +4 -5
- data/lib/rgeo/feature/polygon.rb +1 -2
- data/lib/rgeo/feature/surface.rb +3 -4
- data/lib/rgeo/feature/types.rb +69 -85
- data/lib/rgeo/geographic/factory.rb +98 -125
- data/lib/rgeo/geographic/interface.rb +69 -166
- data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
- data/lib/rgeo/geographic/projected_feature_methods.rb +67 -42
- data/lib/rgeo/geographic/projected_window.rb +36 -22
- data/lib/rgeo/geographic/{proj4_projector.rb → projector.rb} +3 -5
- data/lib/rgeo/geographic/simple_mercator_projector.rb +26 -25
- data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
- data/lib/rgeo/geographic/spherical_feature_methods.rb +86 -9
- data/lib/rgeo/geographic/spherical_math.rb +17 -20
- data/lib/rgeo/geographic.rb +1 -1
- data/lib/rgeo/geos/capi_factory.rb +87 -158
- data/lib/rgeo/geos/capi_feature_classes.rb +50 -36
- data/lib/rgeo/geos/ffi_factory.rb +105 -173
- data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
- data/lib/rgeo/geos/ffi_feature_methods.rb +105 -127
- data/lib/rgeo/geos/interface.rb +20 -59
- data/lib/rgeo/geos/utils.rb +5 -5
- data/lib/rgeo/geos/zm_factory.rb +53 -95
- data/lib/rgeo/geos/zm_feature_methods.rb +30 -33
- data/lib/rgeo/geos.rb +8 -8
- data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +9 -22
- data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -2
- data/lib/rgeo/impl_helper/basic_line_string_methods.rb +28 -56
- data/lib/rgeo/impl_helper/basic_point_methods.rb +2 -14
- data/lib/rgeo/impl_helper/basic_polygon_methods.rb +17 -26
- data/lib/rgeo/impl_helper/utils.rb +21 -0
- data/lib/rgeo/impl_helper/valid_op.rb +350 -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
- data/lib/rgeo/wkrep/wkb_generator.rb +73 -63
- data/lib/rgeo/wkrep/wkb_parser.rb +33 -31
- data/lib/rgeo/wkrep/wkt_generator.rb +52 -45
- data/lib/rgeo/wkrep/wkt_parser.rb +48 -35
- data/lib/rgeo.rb +1 -3
- metadata +50 -13
- 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
@@ -16,15 +16,19 @@ 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
|
+
raise Error::InvalidGeometry, "LineString Cannot Have 1 Point" if @points.size == 1
|
23
|
+
init_geometry
|
20
24
|
end
|
21
25
|
|
22
26
|
def num_points
|
23
27
|
@points.size
|
24
28
|
end
|
25
29
|
|
26
|
-
def point_n(
|
27
|
-
|
30
|
+
def point_n(idx)
|
31
|
+
idx < 0 ? nil : @points[idx]
|
28
32
|
end
|
29
33
|
|
30
34
|
def points
|
@@ -43,11 +47,6 @@ module RGeo
|
|
43
47
|
@points.size == 0
|
44
48
|
end
|
45
49
|
|
46
|
-
def is_empty?
|
47
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
48
|
-
empty?
|
49
|
-
end
|
50
|
-
|
51
50
|
def boundary
|
52
51
|
array = []
|
53
52
|
array << @points.first << @points.last if !empty? && !closed?
|
@@ -63,26 +62,15 @@ module RGeo
|
|
63
62
|
end
|
64
63
|
|
65
64
|
def closed?
|
66
|
-
|
67
|
-
@closed = @points.size > 2 && @points.first == @points.last
|
68
|
-
end
|
69
|
-
@closed
|
70
|
-
end
|
65
|
+
return @closed if defined?(@closed)
|
71
66
|
|
72
|
-
|
73
|
-
warn "The is_closed? method is deprecated, please use the closed? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
74
|
-
closed?
|
67
|
+
@closed = @points.size > 2 && @points.first == @points.last
|
75
68
|
end
|
76
69
|
|
77
70
|
def ring?
|
78
71
|
closed? && simple?
|
79
72
|
end
|
80
73
|
|
81
|
-
def is_ring?
|
82
|
-
warn "The is_ring? method is deprecated, please use the ring? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
83
|
-
ring?
|
84
|
-
end
|
85
|
-
|
86
74
|
def rep_equals?(rhs)
|
87
75
|
if rhs.is_a?(self.class) && rhs.factory.eql?(@factory) && @points.size == rhs.num_points
|
88
76
|
rhs.points.each_with_index { |p, i| return false unless @points[i].rep_equals?(p) }
|
@@ -92,10 +80,7 @@ module RGeo
|
|
92
80
|
end
|
93
81
|
|
94
82
|
def hash
|
95
|
-
@hash ||=
|
96
|
-
hash = [factory, geometry_type].hash
|
97
|
-
@points.inject(hash) { |h, p| (1_664_525 * h + p.hash).hash }
|
98
|
-
end
|
83
|
+
@hash ||= [factory, geometry_type, *@points].hash
|
99
84
|
end
|
100
85
|
|
101
86
|
def coordinates
|
@@ -123,15 +108,15 @@ module RGeo
|
|
123
108
|
def point_intersect_segment?(point, start_point, end_point)
|
124
109
|
return false unless point_collinear?(point, start_point, end_point)
|
125
110
|
|
126
|
-
if start_point.x
|
127
|
-
between_coordinate?(point.x, start_point.x, end_point.x)
|
128
|
-
else
|
111
|
+
if start_point.x == end_point.x
|
129
112
|
between_coordinate?(point.y, start_point.y, end_point.y)
|
113
|
+
else
|
114
|
+
between_coordinate?(point.x, start_point.x, end_point.x)
|
130
115
|
end
|
131
116
|
end
|
132
117
|
|
133
|
-
def point_collinear?(
|
134
|
-
(
|
118
|
+
def point_collinear?(pt1, pt2, pt3)
|
119
|
+
(pt2.x - pt1.x) * (pt3.y - pt1.y) == (pt3.x - pt1.x) * (pt2.y - pt1.y)
|
135
120
|
end
|
136
121
|
|
137
122
|
def between_coordinate?(coord, start_coord, end_coord)
|
@@ -143,25 +128,17 @@ module RGeo
|
|
143
128
|
super
|
144
129
|
@points = obj.points
|
145
130
|
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
131
|
end
|
153
132
|
|
154
133
|
module BasicLineMethods # :nodoc:
|
155
134
|
def initialize(factory, start, stop)
|
156
135
|
self.factory = factory
|
157
136
|
cstart = Feature.cast(start, factory, Feature::Point)
|
158
|
-
unless cstart
|
159
|
-
raise Error::InvalidGeometry, "Could not cast start: #{start}"
|
160
|
-
end
|
137
|
+
raise Error::InvalidGeometry, "Could not cast start: #{start}" unless cstart
|
161
138
|
cstop = Feature.cast(stop, factory, Feature::Point)
|
162
139
|
raise Error::InvalidGeometry, "Could not cast end: #{stop}" unless cstop
|
163
140
|
@points = [cstart, cstop]
|
164
|
-
|
141
|
+
init_geometry
|
165
142
|
end
|
166
143
|
|
167
144
|
def geometry_type
|
@@ -171,18 +148,14 @@ module RGeo
|
|
171
148
|
def coordinates
|
172
149
|
@points.map(&:coordinates)
|
173
150
|
end
|
151
|
+
end
|
174
152
|
|
175
|
-
|
176
|
-
|
177
|
-
def validate_geometry
|
153
|
+
module BasicLinearRingMethods # :nodoc:
|
154
|
+
def initialize(factory, points)
|
178
155
|
super
|
179
|
-
if @points.size
|
180
|
-
raise Error::InvalidGeometry, "Line must have 0 or 2 points"
|
181
|
-
end
|
156
|
+
raise Error::InvalidGeometry, "LinearRings must have 0 or >= 4 points" if @points.size.between?(1, 3)
|
182
157
|
end
|
183
|
-
end
|
184
158
|
|
185
|
-
module BasicLinearRingMethods # :nodoc:
|
186
159
|
def geometry_type
|
187
160
|
Feature::LinearRing
|
188
161
|
end
|
@@ -193,15 +166,14 @@ module RGeo
|
|
193
166
|
|
194
167
|
private
|
195
168
|
|
196
|
-
|
169
|
+
# Close ring if necessary.
|
170
|
+
def init_geometry
|
197
171
|
super
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
end
|
204
|
-
end
|
172
|
+
|
173
|
+
return if @points.empty?
|
174
|
+
|
175
|
+
@points << @points.first if @points.first != @points.last
|
176
|
+
@points = @points.chunk { |x| x }.map(&:first)
|
205
177
|
end
|
206
178
|
end
|
207
179
|
end
|
@@ -15,10 +15,8 @@ module RGeo
|
|
15
15
|
@y = y.to_f
|
16
16
|
@z = factory.property(:has_z_coordinate) ? extra.shift.to_f : nil
|
17
17
|
@m = factory.property(:has_m_coordinate) ? extra.shift.to_f : nil
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
validate_geometry
|
18
|
+
raise ArgumentError, "Too many arguments for point initializer" unless extra.empty?
|
19
|
+
init_geometry
|
22
20
|
end
|
23
21
|
|
24
22
|
def x
|
@@ -49,20 +47,10 @@ module RGeo
|
|
49
47
|
false
|
50
48
|
end
|
51
49
|
|
52
|
-
def is_empty?
|
53
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
54
|
-
empty?
|
55
|
-
end
|
56
|
-
|
57
50
|
def simple?
|
58
51
|
true
|
59
52
|
end
|
60
53
|
|
61
|
-
def is_simple?
|
62
|
-
warn "The is_simple? method is deprecated, please use the simple? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
63
|
-
simple?
|
64
|
-
end
|
65
|
-
|
66
54
|
def envelope
|
67
55
|
self
|
68
56
|
end
|
@@ -12,17 +12,13 @@ module RGeo
|
|
12
12
|
def initialize(factory, exterior_ring, interior_rings)
|
13
13
|
self.factory = factory
|
14
14
|
@exterior_ring = Feature.cast(exterior_ring, factory, Feature::LinearRing)
|
15
|
-
unless @exterior_ring
|
16
|
-
raise Error::InvalidGeometry, "Failed to cast exterior ring #{exterior_ring}"
|
17
|
-
end
|
15
|
+
raise Error::InvalidGeometry, "Failed to cast exterior ring #{exterior_ring}" unless @exterior_ring
|
18
16
|
@interior_rings = (interior_rings || []).map do |elem|
|
19
17
|
elem = Feature.cast(elem, factory, Feature::LinearRing)
|
20
|
-
unless elem
|
21
|
-
raise Error::InvalidGeometry, "Could not cast interior ring #{elem}"
|
22
|
-
end
|
18
|
+
raise Error::InvalidGeometry, "Could not cast interior ring #{elem}" unless elem
|
23
19
|
elem
|
24
20
|
end
|
25
|
-
|
21
|
+
init_geometry
|
26
22
|
end
|
27
23
|
|
28
24
|
def exterior_ring
|
@@ -33,8 +29,8 @@ module RGeo
|
|
33
29
|
@interior_rings.size
|
34
30
|
end
|
35
31
|
|
36
|
-
def interior_ring_n(
|
37
|
-
|
32
|
+
def interior_ring_n(idx)
|
33
|
+
idx < 0 ? nil : @interior_rings[idx]
|
38
34
|
end
|
39
35
|
|
40
36
|
def interior_rings
|
@@ -53,11 +49,6 @@ module RGeo
|
|
53
49
|
@exterior_ring.empty?
|
54
50
|
end
|
55
51
|
|
56
|
-
def is_empty?
|
57
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
58
|
-
empty?
|
59
|
-
end
|
60
|
-
|
61
52
|
def boundary
|
62
53
|
array = []
|
63
54
|
array << @exterior_ring unless @exterior_ring.empty?
|
@@ -66,18 +57,18 @@ module RGeo
|
|
66
57
|
end
|
67
58
|
|
68
59
|
def rep_equals?(rhs)
|
69
|
-
|
70
|
-
rhs.
|
71
|
-
|
72
|
-
|
73
|
-
|
60
|
+
proper_match = rhs.is_a?(self.class) &&
|
61
|
+
rhs.factory.eql?(@factory) &&
|
62
|
+
@exterior_ring.rep_equals?(rhs.exterior_ring) &&
|
63
|
+
@interior_rings.size == rhs.num_interior_rings
|
64
|
+
|
65
|
+
return false unless proper_match
|
66
|
+
|
67
|
+
rhs.interior_rings.each_with_index { |r, i| return false unless @interior_rings[i].rep_equals?(r) }
|
74
68
|
end
|
75
69
|
|
76
70
|
def hash
|
77
|
-
@hash ||=
|
78
|
-
hash = [geometry_type, @exterior_ring].hash
|
79
|
-
@interior_rings.inject(hash) { |h, r| (1_664_525 * h + r.hash).hash }
|
80
|
-
end
|
71
|
+
@hash ||= [geometry_type, @exterior_ring, *@interior_rings].hash
|
81
72
|
end
|
82
73
|
|
83
74
|
def coordinates
|
@@ -88,7 +79,8 @@ module RGeo
|
|
88
79
|
if Feature::Point === rhs
|
89
80
|
contains_point?(rhs)
|
90
81
|
else
|
91
|
-
raise(
|
82
|
+
raise(
|
83
|
+
Error::UnsupportedOperation,
|
92
84
|
"Method Polygon#contains? is only defined for Point"
|
93
85
|
)
|
94
86
|
end
|
@@ -98,7 +90,7 @@ module RGeo
|
|
98
90
|
|
99
91
|
def contains_point?(point)
|
100
92
|
ring_encloses_point?(@exterior_ring, point) &&
|
101
|
-
|
93
|
+
@interior_rings.none? do |exclusion|
|
102
94
|
ring_encloses_point?(exclusion, point, on_border_return: true)
|
103
95
|
end
|
104
96
|
end
|
@@ -122,7 +114,6 @@ module RGeo
|
|
122
114
|
encloses_point
|
123
115
|
end
|
124
116
|
|
125
|
-
|
126
117
|
def copy_state_from(obj)
|
127
118
|
super
|
128
119
|
@exterior_ring = obj.exterior_ring
|
@@ -9,6 +9,27 @@
|
|
9
9
|
module RGeo
|
10
10
|
module ImplHelper # :nodoc:
|
11
11
|
module Utils # :nodoc:
|
12
|
+
# Helper function to create coord_sys from
|
13
|
+
# common options in most factories. Returns
|
14
|
+
# a hash with finalized coord sys info after processing.
|
15
|
+
#
|
16
|
+
# The reason we return the data as a hash instead of assigning
|
17
|
+
# instance variables is because some classes need to do this
|
18
|
+
# multiple times with different values and others pass the data
|
19
|
+
# to a CAPI or FFI.
|
20
|
+
def self.setup_coord_sys(srid, coord_sys, coord_sys_class)
|
21
|
+
coord_sys_class = CoordSys::CONFIG.default_coord_sys_class unless coord_sys_class.is_a?(Class)
|
22
|
+
|
23
|
+
coord_sys = coord_sys_class.create_from_wkt(coord_sys) if coord_sys.is_a?(String)
|
24
|
+
|
25
|
+
srid ||= coord_sys.authority_code if coord_sys
|
26
|
+
srid = srid.to_i
|
27
|
+
# Create a coord sys based on the SRID if one was not given
|
28
|
+
coord_sys = coord_sys_class.create(srid) if coord_sys.nil? && srid != 0
|
29
|
+
|
30
|
+
{ coord_sys: coord_sys, srid: srid }
|
31
|
+
end
|
32
|
+
|
12
33
|
private
|
13
34
|
|
14
35
|
def symbolize_hash(hash)
|
@@ -0,0 +1,350 @@
|
|
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 point [RGeo::Feature::Point]
|
185
|
+
#
|
186
|
+
# @return [String] invalid_reason
|
187
|
+
def check_invalid_coordinate(point)
|
188
|
+
x = point.x
|
189
|
+
y = point.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
|
+
return Error::HOLE_OUTSIDE_SHELL unless shell.contains?(test_pt) || poly.exterior_ring.contains?(test_pt)
|
269
|
+
end
|
270
|
+
|
271
|
+
nil
|
272
|
+
end
|
273
|
+
|
274
|
+
# Checks that holes are not nested within each other.
|
275
|
+
#
|
276
|
+
# @param poly [RGeo::Feature::Polygon]
|
277
|
+
#
|
278
|
+
# @return [String] invalid_reason
|
279
|
+
def check_holes_not_nested(poly)
|
280
|
+
# convert holes from linear_rings to polygons
|
281
|
+
# Same logic that applies to check_holes_in_shell applies here
|
282
|
+
# since we've already passed the consistent area test, we just
|
283
|
+
# have to check if one point from each hole is contained in the other.
|
284
|
+
holes = poly.interior_rings
|
285
|
+
holes = holes.map { |v| v.factory.polygon(v) }
|
286
|
+
holes.combination(2).each do |p1, p2|
|
287
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
288
|
+
return Error::NESTED_HOLES
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
nil
|
293
|
+
end
|
294
|
+
|
295
|
+
# Checks that the interior of the polygon is connected.
|
296
|
+
# A disconnected interior can be described by this polygon for example
|
297
|
+
# POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (5 0, 10 5, 5 10, 0 5, 5 0))
|
298
|
+
#
|
299
|
+
# Which is a square with a diamond inside of it.
|
300
|
+
#
|
301
|
+
# @param poly [RGeo::Feature::Polygon]
|
302
|
+
#
|
303
|
+
# @return [String] invalid_reason
|
304
|
+
def check_connected_interiors(poly)
|
305
|
+
# This is not proper and will flag valid geometries as invalid, but
|
306
|
+
# is an ok approximation.
|
307
|
+
# Idea is to check if a single hole has multiple points on the
|
308
|
+
# exterior ring.
|
309
|
+
poly.interior_rings.each do |ring|
|
310
|
+
touches = Set.new
|
311
|
+
ring.points.each do |pt|
|
312
|
+
touches.add(pt) if poly.exterior_ring.contains?(pt)
|
313
|
+
end
|
314
|
+
|
315
|
+
return Error::DISCONNECTED_INTERIOR if touches.size > 1
|
316
|
+
end
|
317
|
+
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
|
321
|
+
# Checks that polygons do not intersect in a multipolygon.
|
322
|
+
#
|
323
|
+
# @param mpoly [RGeo::Feature::MultiPolygon]
|
324
|
+
#
|
325
|
+
# @return [String] invalid_reason
|
326
|
+
def check_consistent_area_mp(mpoly)
|
327
|
+
mpoly.geometries.combination(2) do |p1, p2|
|
328
|
+
return Error::SELF_INTERSECTION if p1.exterior_ring.crosses?(p2.exterior_ring)
|
329
|
+
end
|
330
|
+
nil
|
331
|
+
end
|
332
|
+
|
333
|
+
# Checks that individual polygons within a multipolygon are not nested.
|
334
|
+
#
|
335
|
+
# @param mpoly [RGeo::Feature::MultiPolygon]
|
336
|
+
#
|
337
|
+
# @return [String] invalid_reason
|
338
|
+
def check_shells_not_nested(mpoly)
|
339
|
+
# Since we've passed the consistent area test, we can just check
|
340
|
+
# that one point lies in the other.
|
341
|
+
mpoly.geometries.combination(2) do |p1, p2|
|
342
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
343
|
+
return Error::NESTED_SHELLS
|
344
|
+
end
|
345
|
+
end
|
346
|
+
nil
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|