rgeo 2.3.1 → 3.0.0.pre.rc.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/README.md +11 -10
  4. data/ext/geos_c_impl/analysis.c +8 -6
  5. data/ext/geos_c_impl/analysis.h +1 -3
  6. data/ext/geos_c_impl/errors.c +10 -8
  7. data/ext/geos_c_impl/errors.h +7 -3
  8. data/ext/geos_c_impl/extconf.rb +3 -0
  9. data/ext/geos_c_impl/factory.c +273 -202
  10. data/ext/geos_c_impl/factory.h +51 -63
  11. data/ext/geos_c_impl/geometry.c +124 -22
  12. data/ext/geos_c_impl/geometry.h +8 -3
  13. data/ext/geos_c_impl/geometry_collection.c +81 -185
  14. data/ext/geos_c_impl/geometry_collection.h +1 -14
  15. data/ext/geos_c_impl/globals.c +91 -0
  16. data/ext/geos_c_impl/globals.h +45 -0
  17. data/ext/geos_c_impl/line_string.c +28 -29
  18. data/ext/geos_c_impl/line_string.h +1 -3
  19. data/ext/geos_c_impl/main.c +10 -9
  20. data/ext/geos_c_impl/point.c +9 -8
  21. data/ext/geos_c_impl/point.h +1 -3
  22. data/ext/geos_c_impl/polygon.c +43 -72
  23. data/ext/geos_c_impl/polygon.h +1 -3
  24. data/ext/geos_c_impl/preface.h +12 -0
  25. data/ext/geos_c_impl/ruby_more.c +65 -0
  26. data/ext/geos_c_impl/ruby_more.h +16 -0
  27. data/lib/rgeo/cartesian/calculations.rb +54 -17
  28. data/lib/rgeo/cartesian/factory.rb +6 -14
  29. data/lib/rgeo/cartesian/feature_classes.rb +68 -46
  30. data/lib/rgeo/cartesian/feature_methods.rb +67 -20
  31. data/lib/rgeo/cartesian/interface.rb +0 -36
  32. data/lib/rgeo/cartesian/planar_graph.rb +379 -0
  33. data/lib/rgeo/cartesian/sweepline_intersector.rb +149 -0
  34. data/lib/rgeo/cartesian/valid_op.rb +71 -0
  35. data/lib/rgeo/cartesian.rb +3 -0
  36. data/lib/rgeo/coord_sys/cs/wkt_parser.rb +6 -6
  37. data/lib/rgeo/coord_sys.rb +0 -11
  38. data/lib/rgeo/error.rb +15 -0
  39. data/lib/rgeo/feature/factory_generator.rb +0 -3
  40. data/lib/rgeo/feature/geometry.rb +107 -28
  41. data/lib/rgeo/feature/geometry_collection.rb +13 -5
  42. data/lib/rgeo/feature/line_string.rb +3 -3
  43. data/lib/rgeo/feature/multi_surface.rb +3 -3
  44. data/lib/rgeo/feature/point.rb +4 -4
  45. data/lib/rgeo/feature/surface.rb +3 -3
  46. data/lib/rgeo/geographic/factory.rb +6 -7
  47. data/lib/rgeo/geographic/interface.rb +6 -49
  48. data/lib/rgeo/geographic/proj4_projector.rb +0 -2
  49. data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
  50. data/lib/rgeo/geographic/projected_feature_methods.rb +67 -28
  51. data/lib/rgeo/geographic/simple_mercator_projector.rb +0 -2
  52. data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
  53. data/lib/rgeo/geographic/spherical_feature_methods.rb +79 -2
  54. data/lib/rgeo/geos/capi_factory.rb +21 -38
  55. data/lib/rgeo/geos/capi_feature_classes.rb +54 -11
  56. data/lib/rgeo/geos/ffi_factory.rb +6 -35
  57. data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
  58. data/lib/rgeo/geos/ffi_feature_methods.rb +39 -5
  59. data/lib/rgeo/geos/interface.rb +0 -24
  60. data/lib/rgeo/geos/zm_factory.rb +0 -19
  61. data/lib/rgeo/geos/zm_feature_methods.rb +16 -0
  62. data/lib/rgeo/geos.rb +6 -3
  63. data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +4 -4
  64. data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -1
  65. data/lib/rgeo/impl_helper/basic_line_string_methods.rb +15 -19
  66. data/lib/rgeo/impl_helper/basic_point_methods.rb +1 -1
  67. data/lib/rgeo/impl_helper/basic_polygon_methods.rb +1 -1
  68. data/lib/rgeo/impl_helper/valid_op.rb +354 -0
  69. data/lib/rgeo/impl_helper/validity_check.rb +139 -0
  70. data/lib/rgeo/impl_helper.rb +1 -0
  71. data/lib/rgeo/version.rb +1 -1
  72. metadata +45 -9
  73. data/lib/rgeo/coord_sys/srs_database/entry.rb +0 -107
  74. data/lib/rgeo/coord_sys/srs_database/sr_org.rb +0 -64
  75. 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
- validate_geometry
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
- validate_geometry
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
- validate_geometry
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
- validate_geometry
179
+ init_geometry
180
180
  end
181
181
 
182
182
  def geometry_type
@@ -52,7 +52,7 @@ module RGeo
52
52
  @factory = obj.factory
53
53
  end
54
54
 
55
- def validate_geometry
55
+ def init_geometry
56
56
  end
57
57
  end
58
58
  end
@@ -16,7 +16,13 @@ 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
+ 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
- validate_geometry
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
- private
176
-
177
- def validate_geometry
176
+ module BasicLinearRingMethods # :nodoc:
177
+ def initialize(factory, points)
178
178
  super
179
- if @points.size > 2
180
- raise Error::InvalidGeometry, "Line must have 0 or 2 points"
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
- def validate_geometry
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
@@ -18,7 +18,7 @@ module RGeo
18
18
  if extra.size > 0
19
19
  raise ArgumentError, "Too many arguments for point initializer"
20
20
  end
21
- validate_geometry
21
+ init_geometry
22
22
  end
23
23
 
24
24
  def x
@@ -22,7 +22,7 @@ module RGeo
22
22
  end
23
23
  elem
24
24
  end
25
- validate_geometry
25
+ init_geometry
26
26
  end
27
27
 
28
28
  def exterior_ring
@@ -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
@@ -7,3 +7,4 @@ require_relative "impl_helper/basic_geometry_collection_methods"
7
7
  require_relative "impl_helper/basic_point_methods"
8
8
  require_relative "impl_helper/basic_line_string_methods"
9
9
  require_relative "impl_helper/basic_polygon_methods"
10
+ require_relative "impl_helper/valid_op"
data/lib/rgeo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RGeo
4
- VERSION = "2.3.1"
4
+ VERSION = "3.0.0-rc.2"
5
5
  end