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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/README.md +23 -14
  4. data/ext/geos_c_impl/analysis.c +30 -25
  5. data/ext/geos_c_impl/analysis.h +8 -7
  6. data/ext/geos_c_impl/coordinates.c +27 -21
  7. data/ext/geos_c_impl/coordinates.h +5 -2
  8. data/ext/geos_c_impl/errors.c +19 -10
  9. data/ext/geos_c_impl/errors.h +11 -4
  10. data/ext/geos_c_impl/extconf.rb +42 -28
  11. data/ext/geos_c_impl/factory.c +540 -451
  12. data/ext/geos_c_impl/factory.h +105 -95
  13. data/ext/geos_c_impl/geometry.c +593 -387
  14. data/ext/geos_c_impl/geometry.h +10 -5
  15. data/ext/geos_c_impl/geometry_collection.c +306 -339
  16. data/ext/geos_c_impl/geometry_collection.h +6 -20
  17. data/ext/geos_c_impl/globals.c +169 -0
  18. data/ext/geos_c_impl/globals.h +46 -0
  19. data/ext/geos_c_impl/line_string.c +271 -231
  20. data/ext/geos_c_impl/line_string.h +5 -8
  21. data/ext/geos_c_impl/main.c +16 -16
  22. data/ext/geos_c_impl/point.c +65 -67
  23. data/ext/geos_c_impl/point.h +4 -7
  24. data/ext/geos_c_impl/polygon.c +137 -135
  25. data/ext/geos_c_impl/polygon.h +11 -11
  26. data/ext/geos_c_impl/preface.h +16 -10
  27. data/ext/geos_c_impl/ruby_more.c +67 -0
  28. data/ext/geos_c_impl/ruby_more.h +25 -0
  29. data/lib/rgeo/cartesian/analysis.rb +5 -3
  30. data/lib/rgeo/cartesian/bounding_box.rb +74 -79
  31. data/lib/rgeo/cartesian/calculations.rb +64 -33
  32. data/lib/rgeo/cartesian/factory.rb +57 -102
  33. data/lib/rgeo/cartesian/feature_classes.rb +68 -46
  34. data/lib/rgeo/cartesian/feature_methods.rb +67 -25
  35. data/lib/rgeo/cartesian/interface.rb +6 -41
  36. data/lib/rgeo/cartesian/planar_graph.rb +373 -0
  37. data/lib/rgeo/cartesian/sweepline_intersector.rb +147 -0
  38. data/lib/rgeo/cartesian/valid_op.rb +69 -0
  39. data/lib/rgeo/cartesian.rb +3 -0
  40. data/lib/rgeo/coord_sys/cs/entities.rb +303 -99
  41. data/lib/rgeo/coord_sys/cs/factories.rb +0 -2
  42. data/lib/rgeo/coord_sys/cs/wkt_parser.rb +90 -42
  43. data/lib/rgeo/coord_sys.rb +1 -20
  44. data/lib/rgeo/error.rb +15 -0
  45. data/lib/rgeo/feature/curve.rb +0 -11
  46. data/lib/rgeo/feature/factory.rb +26 -36
  47. data/lib/rgeo/feature/factory_generator.rb +6 -14
  48. data/lib/rgeo/feature/geometry.rb +146 -66
  49. data/lib/rgeo/feature/geometry_collection.rb +16 -9
  50. data/lib/rgeo/feature/line_string.rb +4 -5
  51. data/lib/rgeo/feature/linear_ring.rb +0 -1
  52. data/lib/rgeo/feature/multi_curve.rb +0 -6
  53. data/lib/rgeo/feature/multi_surface.rb +3 -4
  54. data/lib/rgeo/feature/point.rb +4 -5
  55. data/lib/rgeo/feature/polygon.rb +1 -2
  56. data/lib/rgeo/feature/surface.rb +3 -4
  57. data/lib/rgeo/feature/types.rb +69 -85
  58. data/lib/rgeo/geographic/factory.rb +98 -125
  59. data/lib/rgeo/geographic/interface.rb +69 -166
  60. data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
  61. data/lib/rgeo/geographic/projected_feature_methods.rb +67 -42
  62. data/lib/rgeo/geographic/projected_window.rb +36 -22
  63. data/lib/rgeo/geographic/{proj4_projector.rb → projector.rb} +3 -5
  64. data/lib/rgeo/geographic/simple_mercator_projector.rb +26 -25
  65. data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
  66. data/lib/rgeo/geographic/spherical_feature_methods.rb +86 -9
  67. data/lib/rgeo/geographic/spherical_math.rb +17 -20
  68. data/lib/rgeo/geographic.rb +1 -1
  69. data/lib/rgeo/geos/capi_factory.rb +87 -158
  70. data/lib/rgeo/geos/capi_feature_classes.rb +50 -36
  71. data/lib/rgeo/geos/ffi_factory.rb +105 -173
  72. data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
  73. data/lib/rgeo/geos/ffi_feature_methods.rb +105 -127
  74. data/lib/rgeo/geos/interface.rb +20 -59
  75. data/lib/rgeo/geos/utils.rb +5 -5
  76. data/lib/rgeo/geos/zm_factory.rb +53 -95
  77. data/lib/rgeo/geos/zm_feature_methods.rb +30 -33
  78. data/lib/rgeo/geos.rb +8 -8
  79. data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +9 -22
  80. data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -2
  81. data/lib/rgeo/impl_helper/basic_line_string_methods.rb +28 -56
  82. data/lib/rgeo/impl_helper/basic_point_methods.rb +2 -14
  83. data/lib/rgeo/impl_helper/basic_polygon_methods.rb +17 -26
  84. data/lib/rgeo/impl_helper/utils.rb +21 -0
  85. data/lib/rgeo/impl_helper/valid_op.rb +350 -0
  86. data/lib/rgeo/impl_helper/validity_check.rb +139 -0
  87. data/lib/rgeo/impl_helper.rb +1 -0
  88. data/lib/rgeo/version.rb +1 -1
  89. data/lib/rgeo/wkrep/wkb_generator.rb +73 -63
  90. data/lib/rgeo/wkrep/wkb_parser.rb +33 -31
  91. data/lib/rgeo/wkrep/wkt_generator.rb +52 -45
  92. data/lib/rgeo/wkrep/wkt_parser.rb +48 -35
  93. data/lib/rgeo.rb +1 -3
  94. metadata +50 -13
  95. data/lib/rgeo/coord_sys/srs_database/entry.rb +0 -107
  96. data/lib/rgeo/coord_sys/srs_database/sr_org.rb +0 -64
  97. 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
- validate_geometry
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(n)
27
- n < 0 ? nil : @points[n]
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
- unless defined?(@closed)
67
- @closed = @points.size > 2 && @points.first == @points.last
68
- end
69
- @closed
70
- end
65
+ return @closed if defined?(@closed)
71
66
 
72
- def is_closed?
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 ||= begin
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 != end_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?(a, b, c)
134
- (b.x - a.x) * (c.y - a.y) == (c.x - a.x) * (b.y - a.y)
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
- validate_geometry
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
- private
176
-
177
- def validate_geometry
153
+ module BasicLinearRingMethods # :nodoc:
154
+ def initialize(factory, points)
178
155
  super
179
- if @points.size > 2
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
- def validate_geometry
169
+ # Close ring if necessary.
170
+ def init_geometry
197
171
  super
198
- if @points.size > 0
199
- @points << @points.first if @points.first != @points.last
200
- @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
- 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
- if extra.size > 0
19
- raise ArgumentError, "Too many arguments for point initializer"
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
- validate_geometry
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(n)
37
- n < 0 ? nil : @interior_rings[n]
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
- if rhs.is_a?(self.class) && rhs.factory.eql?(@factory) && @exterior_ring.rep_equals?(rhs.exterior_ring) && @interior_rings.size == rhs.num_interior_rings
70
- rhs.interior_rings.each_with_index { |r, i| return false unless @interior_rings[i].rep_equals?(r) }
71
- else
72
- false
73
- end
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 ||= begin
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(Error::UnsupportedOperation,
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
- !@interior_rings.any? do |exclusion|
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