rgeo 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/README.md +21 -11
  4. data/ext/geos_c_impl/analysis.c +29 -26
  5. data/ext/geos_c_impl/analysis.h +8 -5
  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 +41 -29
  11. data/ext/geos_c_impl/factory.c +441 -351
  12. data/ext/geos_c_impl/factory.h +98 -55
  13. data/ext/geos_c_impl/geometry.c +563 -384
  14. data/ext/geos_c_impl/geometry.h +10 -3
  15. data/ext/geos_c_impl/geometry_collection.c +288 -316
  16. data/ext/geos_c_impl/geometry_collection.h +6 -18
  17. data/ext/geos_c_impl/globals.c +99 -21
  18. data/ext/geos_c_impl/globals.h +3 -2
  19. data/ext/geos_c_impl/line_string.c +263 -222
  20. data/ext/geos_c_impl/line_string.h +5 -6
  21. data/ext/geos_c_impl/main.c +8 -9
  22. data/ext/geos_c_impl/point.c +62 -65
  23. data/ext/geos_c_impl/point.h +4 -5
  24. data/ext/geos_c_impl/polygon.c +134 -132
  25. data/ext/geos_c_impl/polygon.h +11 -9
  26. data/ext/geos_c_impl/preface.h +10 -12
  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 +299 -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 +73 -83
  58. data/lib/rgeo/geographic/factory.rb +98 -125
  59. data/lib/rgeo/geographic/interface.rb +66 -163
  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 +24 -23
  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 +95 -165
  72. data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
  73. data/lib/rgeo/geos/ffi_feature_methods.rb +105 -126
  74. data/lib/rgeo/geos/interface.rb +20 -59
  75. data/lib/rgeo/geos/utils.rb +3 -3
  76. data/lib/rgeo/geos/zm_factory.rb +53 -95
  77. data/lib/rgeo/geos/zm_feature_methods.rb +30 -32
  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 +51 -16
  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