rgeo 2.3.1 → 3.0.0.pre.rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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