rgeo 2.4.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/README.md +21 -11
- data/ext/geos_c_impl/analysis.c +29 -26
- data/ext/geos_c_impl/analysis.h +8 -5
- data/ext/geos_c_impl/coordinates.c +27 -21
- data/ext/geos_c_impl/coordinates.h +5 -2
- data/ext/geos_c_impl/errors.c +19 -10
- data/ext/geos_c_impl/errors.h +11 -4
- data/ext/geos_c_impl/extconf.rb +41 -29
- data/ext/geos_c_impl/factory.c +441 -351
- data/ext/geos_c_impl/factory.h +98 -55
- data/ext/geos_c_impl/geometry.c +563 -384
- data/ext/geos_c_impl/geometry.h +10 -3
- data/ext/geos_c_impl/geometry_collection.c +288 -316
- data/ext/geos_c_impl/geometry_collection.h +6 -18
- data/ext/geos_c_impl/globals.c +99 -21
- data/ext/geos_c_impl/globals.h +3 -2
- data/ext/geos_c_impl/line_string.c +263 -222
- data/ext/geos_c_impl/line_string.h +5 -6
- data/ext/geos_c_impl/main.c +8 -9
- data/ext/geos_c_impl/point.c +62 -65
- data/ext/geos_c_impl/point.h +4 -5
- data/ext/geos_c_impl/polygon.c +134 -132
- data/ext/geos_c_impl/polygon.h +11 -9
- data/ext/geos_c_impl/preface.h +10 -12
- data/ext/geos_c_impl/ruby_more.c +67 -0
- data/ext/geos_c_impl/ruby_more.h +25 -0
- data/lib/rgeo/cartesian/analysis.rb +5 -3
- data/lib/rgeo/cartesian/bounding_box.rb +74 -79
- data/lib/rgeo/cartesian/calculations.rb +64 -33
- data/lib/rgeo/cartesian/factory.rb +57 -102
- data/lib/rgeo/cartesian/feature_classes.rb +68 -46
- data/lib/rgeo/cartesian/feature_methods.rb +67 -25
- data/lib/rgeo/cartesian/interface.rb +6 -41
- data/lib/rgeo/cartesian/planar_graph.rb +373 -0
- data/lib/rgeo/cartesian/sweepline_intersector.rb +147 -0
- data/lib/rgeo/cartesian/valid_op.rb +69 -0
- data/lib/rgeo/cartesian.rb +3 -0
- data/lib/rgeo/coord_sys/cs/entities.rb +299 -99
- data/lib/rgeo/coord_sys/cs/factories.rb +0 -2
- data/lib/rgeo/coord_sys/cs/wkt_parser.rb +90 -42
- data/lib/rgeo/coord_sys.rb +1 -20
- data/lib/rgeo/error.rb +15 -0
- data/lib/rgeo/feature/curve.rb +0 -11
- data/lib/rgeo/feature/factory.rb +26 -36
- data/lib/rgeo/feature/factory_generator.rb +6 -14
- data/lib/rgeo/feature/geometry.rb +146 -66
- data/lib/rgeo/feature/geometry_collection.rb +16 -9
- data/lib/rgeo/feature/line_string.rb +4 -5
- data/lib/rgeo/feature/linear_ring.rb +0 -1
- data/lib/rgeo/feature/multi_curve.rb +0 -6
- data/lib/rgeo/feature/multi_surface.rb +3 -4
- data/lib/rgeo/feature/point.rb +4 -5
- data/lib/rgeo/feature/polygon.rb +1 -2
- data/lib/rgeo/feature/surface.rb +3 -4
- data/lib/rgeo/feature/types.rb +73 -83
- data/lib/rgeo/geographic/factory.rb +98 -125
- data/lib/rgeo/geographic/interface.rb +66 -163
- data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
- data/lib/rgeo/geographic/projected_feature_methods.rb +67 -42
- data/lib/rgeo/geographic/projected_window.rb +36 -22
- data/lib/rgeo/geographic/{proj4_projector.rb → projector.rb} +3 -5
- data/lib/rgeo/geographic/simple_mercator_projector.rb +24 -23
- data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
- data/lib/rgeo/geographic/spherical_feature_methods.rb +86 -9
- data/lib/rgeo/geographic/spherical_math.rb +17 -20
- data/lib/rgeo/geographic.rb +1 -1
- data/lib/rgeo/geos/capi_factory.rb +87 -158
- data/lib/rgeo/geos/capi_feature_classes.rb +50 -36
- data/lib/rgeo/geos/ffi_factory.rb +95 -165
- data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
- data/lib/rgeo/geos/ffi_feature_methods.rb +105 -126
- data/lib/rgeo/geos/interface.rb +20 -59
- data/lib/rgeo/geos/utils.rb +3 -3
- data/lib/rgeo/geos/zm_factory.rb +53 -95
- data/lib/rgeo/geos/zm_feature_methods.rb +30 -32
- data/lib/rgeo/geos.rb +8 -8
- data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +9 -22
- data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -2
- data/lib/rgeo/impl_helper/basic_line_string_methods.rb +28 -56
- data/lib/rgeo/impl_helper/basic_point_methods.rb +2 -14
- data/lib/rgeo/impl_helper/basic_polygon_methods.rb +17 -26
- data/lib/rgeo/impl_helper/utils.rb +21 -0
- data/lib/rgeo/impl_helper/valid_op.rb +350 -0
- data/lib/rgeo/impl_helper/validity_check.rb +139 -0
- data/lib/rgeo/impl_helper.rb +1 -0
- data/lib/rgeo/version.rb +1 -1
- data/lib/rgeo/wkrep/wkb_generator.rb +73 -63
- data/lib/rgeo/wkrep/wkb_parser.rb +33 -31
- data/lib/rgeo/wkrep/wkt_generator.rb +52 -45
- data/lib/rgeo/wkrep/wkt_parser.rb +48 -35
- data/lib/rgeo.rb +1 -3
- metadata +51 -16
- data/lib/rgeo/coord_sys/srs_database/entry.rb +0 -107
- data/lib/rgeo/coord_sys/srs_database/sr_org.rb +0 -64
- data/lib/rgeo/coord_sys/srs_database/url_reader.rb +0 -65
@@ -16,15 +16,19 @@ module RGeo
|
|
16
16
|
raise Error::InvalidGeometry, "Could not cast #{elem}" unless elem
|
17
17
|
elem
|
18
18
|
end
|
19
|
-
|
19
|
+
# LineStrings in general need to check that there's not one point
|
20
|
+
# GEOS doesn't allow instantiation of single point LineStrings so
|
21
|
+
# we should handle it.
|
22
|
+
raise Error::InvalidGeometry, "LineString Cannot Have 1 Point" if @points.size == 1
|
23
|
+
init_geometry
|
20
24
|
end
|
21
25
|
|
22
26
|
def num_points
|
23
27
|
@points.size
|
24
28
|
end
|
25
29
|
|
26
|
-
def point_n(
|
27
|
-
|
30
|
+
def point_n(idx)
|
31
|
+
idx < 0 ? nil : @points[idx]
|
28
32
|
end
|
29
33
|
|
30
34
|
def points
|
@@ -43,11 +47,6 @@ module RGeo
|
|
43
47
|
@points.size == 0
|
44
48
|
end
|
45
49
|
|
46
|
-
def is_empty?
|
47
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
48
|
-
empty?
|
49
|
-
end
|
50
|
-
|
51
50
|
def boundary
|
52
51
|
array = []
|
53
52
|
array << @points.first << @points.last if !empty? && !closed?
|
@@ -63,26 +62,15 @@ module RGeo
|
|
63
62
|
end
|
64
63
|
|
65
64
|
def closed?
|
66
|
-
|
67
|
-
@closed = @points.size > 2 && @points.first == @points.last
|
68
|
-
end
|
69
|
-
@closed
|
70
|
-
end
|
65
|
+
return @closed if defined?(@closed)
|
71
66
|
|
72
|
-
|
73
|
-
warn "The is_closed? method is deprecated, please use the closed? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
74
|
-
closed?
|
67
|
+
@closed = @points.size > 2 && @points.first == @points.last
|
75
68
|
end
|
76
69
|
|
77
70
|
def ring?
|
78
71
|
closed? && simple?
|
79
72
|
end
|
80
73
|
|
81
|
-
def is_ring?
|
82
|
-
warn "The is_ring? method is deprecated, please use the ring? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
83
|
-
ring?
|
84
|
-
end
|
85
|
-
|
86
74
|
def rep_equals?(rhs)
|
87
75
|
if rhs.is_a?(self.class) && rhs.factory.eql?(@factory) && @points.size == rhs.num_points
|
88
76
|
rhs.points.each_with_index { |p, i| return false unless @points[i].rep_equals?(p) }
|
@@ -92,10 +80,7 @@ module RGeo
|
|
92
80
|
end
|
93
81
|
|
94
82
|
def hash
|
95
|
-
@hash ||=
|
96
|
-
hash = [factory, geometry_type].hash
|
97
|
-
@points.inject(hash) { |h, p| (1_664_525 * h + p.hash).hash }
|
98
|
-
end
|
83
|
+
@hash ||= [factory, geometry_type, *@points].hash
|
99
84
|
end
|
100
85
|
|
101
86
|
def coordinates
|
@@ -123,15 +108,15 @@ module RGeo
|
|
123
108
|
def point_intersect_segment?(point, start_point, end_point)
|
124
109
|
return false unless point_collinear?(point, start_point, end_point)
|
125
110
|
|
126
|
-
if start_point.x
|
127
|
-
between_coordinate?(point.x, start_point.x, end_point.x)
|
128
|
-
else
|
111
|
+
if start_point.x == end_point.x
|
129
112
|
between_coordinate?(point.y, start_point.y, end_point.y)
|
113
|
+
else
|
114
|
+
between_coordinate?(point.x, start_point.x, end_point.x)
|
130
115
|
end
|
131
116
|
end
|
132
117
|
|
133
|
-
def point_collinear?(
|
134
|
-
(
|
118
|
+
def point_collinear?(pt1, pt2, pt3)
|
119
|
+
(pt2.x - pt1.x) * (pt3.y - pt1.y) == (pt3.x - pt1.x) * (pt2.y - pt1.y)
|
135
120
|
end
|
136
121
|
|
137
122
|
def between_coordinate?(coord, start_coord, end_coord)
|
@@ -143,25 +128,17 @@ module RGeo
|
|
143
128
|
super
|
144
129
|
@points = obj.points
|
145
130
|
end
|
146
|
-
|
147
|
-
def validate_geometry
|
148
|
-
if @points.size == 1
|
149
|
-
raise Error::InvalidGeometry, "LineString cannot have 1 point"
|
150
|
-
end
|
151
|
-
end
|
152
131
|
end
|
153
132
|
|
154
133
|
module BasicLineMethods # :nodoc:
|
155
134
|
def initialize(factory, start, stop)
|
156
135
|
self.factory = factory
|
157
136
|
cstart = Feature.cast(start, factory, Feature::Point)
|
158
|
-
unless cstart
|
159
|
-
raise Error::InvalidGeometry, "Could not cast start: #{start}"
|
160
|
-
end
|
137
|
+
raise Error::InvalidGeometry, "Could not cast start: #{start}" unless cstart
|
161
138
|
cstop = Feature.cast(stop, factory, Feature::Point)
|
162
139
|
raise Error::InvalidGeometry, "Could not cast end: #{stop}" unless cstop
|
163
140
|
@points = [cstart, cstop]
|
164
|
-
|
141
|
+
init_geometry
|
165
142
|
end
|
166
143
|
|
167
144
|
def geometry_type
|
@@ -171,18 +148,14 @@ module RGeo
|
|
171
148
|
def coordinates
|
172
149
|
@points.map(&:coordinates)
|
173
150
|
end
|
151
|
+
end
|
174
152
|
|
175
|
-
|
176
|
-
|
177
|
-
def validate_geometry
|
153
|
+
module BasicLinearRingMethods # :nodoc:
|
154
|
+
def initialize(factory, points)
|
178
155
|
super
|
179
|
-
if @points.size
|
180
|
-
raise Error::InvalidGeometry, "Line must have 0 or 2 points"
|
181
|
-
end
|
156
|
+
raise Error::InvalidGeometry, "LinearRings must have 0 or >= 4 points" if @points.size.between?(1, 3)
|
182
157
|
end
|
183
|
-
end
|
184
158
|
|
185
|
-
module BasicLinearRingMethods # :nodoc:
|
186
159
|
def geometry_type
|
187
160
|
Feature::LinearRing
|
188
161
|
end
|
@@ -193,15 +166,14 @@ module RGeo
|
|
193
166
|
|
194
167
|
private
|
195
168
|
|
196
|
-
|
169
|
+
# Close ring if necessary.
|
170
|
+
def init_geometry
|
197
171
|
super
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
end
|
204
|
-
end
|
172
|
+
|
173
|
+
return if @points.empty?
|
174
|
+
|
175
|
+
@points << @points.first if @points.first != @points.last
|
176
|
+
@points = @points.chunk { |x| x }.map(&:first)
|
205
177
|
end
|
206
178
|
end
|
207
179
|
end
|
@@ -15,10 +15,8 @@ module RGeo
|
|
15
15
|
@y = y.to_f
|
16
16
|
@z = factory.property(:has_z_coordinate) ? extra.shift.to_f : nil
|
17
17
|
@m = factory.property(:has_m_coordinate) ? extra.shift.to_f : nil
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
validate_geometry
|
18
|
+
raise ArgumentError, "Too many arguments for point initializer" unless extra.empty?
|
19
|
+
init_geometry
|
22
20
|
end
|
23
21
|
|
24
22
|
def x
|
@@ -49,20 +47,10 @@ module RGeo
|
|
49
47
|
false
|
50
48
|
end
|
51
49
|
|
52
|
-
def is_empty?
|
53
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
54
|
-
empty?
|
55
|
-
end
|
56
|
-
|
57
50
|
def simple?
|
58
51
|
true
|
59
52
|
end
|
60
53
|
|
61
|
-
def is_simple?
|
62
|
-
warn "The is_simple? method is deprecated, please use the simple? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
63
|
-
simple?
|
64
|
-
end
|
65
|
-
|
66
54
|
def envelope
|
67
55
|
self
|
68
56
|
end
|
@@ -12,17 +12,13 @@ module RGeo
|
|
12
12
|
def initialize(factory, exterior_ring, interior_rings)
|
13
13
|
self.factory = factory
|
14
14
|
@exterior_ring = Feature.cast(exterior_ring, factory, Feature::LinearRing)
|
15
|
-
unless @exterior_ring
|
16
|
-
raise Error::InvalidGeometry, "Failed to cast exterior ring #{exterior_ring}"
|
17
|
-
end
|
15
|
+
raise Error::InvalidGeometry, "Failed to cast exterior ring #{exterior_ring}" unless @exterior_ring
|
18
16
|
@interior_rings = (interior_rings || []).map do |elem|
|
19
17
|
elem = Feature.cast(elem, factory, Feature::LinearRing)
|
20
|
-
unless elem
|
21
|
-
raise Error::InvalidGeometry, "Could not cast interior ring #{elem}"
|
22
|
-
end
|
18
|
+
raise Error::InvalidGeometry, "Could not cast interior ring #{elem}" unless elem
|
23
19
|
elem
|
24
20
|
end
|
25
|
-
|
21
|
+
init_geometry
|
26
22
|
end
|
27
23
|
|
28
24
|
def exterior_ring
|
@@ -33,8 +29,8 @@ module RGeo
|
|
33
29
|
@interior_rings.size
|
34
30
|
end
|
35
31
|
|
36
|
-
def interior_ring_n(
|
37
|
-
|
32
|
+
def interior_ring_n(idx)
|
33
|
+
idx < 0 ? nil : @interior_rings[idx]
|
38
34
|
end
|
39
35
|
|
40
36
|
def interior_rings
|
@@ -53,11 +49,6 @@ module RGeo
|
|
53
49
|
@exterior_ring.empty?
|
54
50
|
end
|
55
51
|
|
56
|
-
def is_empty?
|
57
|
-
warn "The is_empty? method is deprecated, please use the empty? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
|
58
|
-
empty?
|
59
|
-
end
|
60
|
-
|
61
52
|
def boundary
|
62
53
|
array = []
|
63
54
|
array << @exterior_ring unless @exterior_ring.empty?
|
@@ -66,18 +57,18 @@ module RGeo
|
|
66
57
|
end
|
67
58
|
|
68
59
|
def rep_equals?(rhs)
|
69
|
-
|
70
|
-
rhs.
|
71
|
-
|
72
|
-
|
73
|
-
|
60
|
+
proper_match = rhs.is_a?(self.class) &&
|
61
|
+
rhs.factory.eql?(@factory) &&
|
62
|
+
@exterior_ring.rep_equals?(rhs.exterior_ring) &&
|
63
|
+
@interior_rings.size == rhs.num_interior_rings
|
64
|
+
|
65
|
+
return false unless proper_match
|
66
|
+
|
67
|
+
rhs.interior_rings.each_with_index { |r, i| return false unless @interior_rings[i].rep_equals?(r) }
|
74
68
|
end
|
75
69
|
|
76
70
|
def hash
|
77
|
-
@hash ||=
|
78
|
-
hash = [geometry_type, @exterior_ring].hash
|
79
|
-
@interior_rings.inject(hash) { |h, r| (1_664_525 * h + r.hash).hash }
|
80
|
-
end
|
71
|
+
@hash ||= [geometry_type, @exterior_ring, *@interior_rings].hash
|
81
72
|
end
|
82
73
|
|
83
74
|
def coordinates
|
@@ -88,7 +79,8 @@ module RGeo
|
|
88
79
|
if Feature::Point === rhs
|
89
80
|
contains_point?(rhs)
|
90
81
|
else
|
91
|
-
raise(
|
82
|
+
raise(
|
83
|
+
Error::UnsupportedOperation,
|
92
84
|
"Method Polygon#contains? is only defined for Point"
|
93
85
|
)
|
94
86
|
end
|
@@ -98,7 +90,7 @@ module RGeo
|
|
98
90
|
|
99
91
|
def contains_point?(point)
|
100
92
|
ring_encloses_point?(@exterior_ring, point) &&
|
101
|
-
|
93
|
+
@interior_rings.none? do |exclusion|
|
102
94
|
ring_encloses_point?(exclusion, point, on_border_return: true)
|
103
95
|
end
|
104
96
|
end
|
@@ -122,7 +114,6 @@ module RGeo
|
|
122
114
|
encloses_point
|
123
115
|
end
|
124
116
|
|
125
|
-
|
126
117
|
def copy_state_from(obj)
|
127
118
|
super
|
128
119
|
@exterior_ring = obj.exterior_ring
|
@@ -9,6 +9,27 @@
|
|
9
9
|
module RGeo
|
10
10
|
module ImplHelper # :nodoc:
|
11
11
|
module Utils # :nodoc:
|
12
|
+
# Helper function to create coord_sys from
|
13
|
+
# common options in most factories. Returns
|
14
|
+
# a hash with finalized coord sys info after processing.
|
15
|
+
#
|
16
|
+
# The reason we return the data as a hash instead of assigning
|
17
|
+
# instance variables is because some classes need to do this
|
18
|
+
# multiple times with different values and others pass the data
|
19
|
+
# to a CAPI or FFI.
|
20
|
+
def self.setup_coord_sys(srid, coord_sys, coord_sys_class)
|
21
|
+
coord_sys_class = CoordSys::CONFIG.default_coord_sys_class unless coord_sys_class.is_a?(Class)
|
22
|
+
|
23
|
+
coord_sys = coord_sys_class.create_from_wkt(coord_sys) if coord_sys.is_a?(String)
|
24
|
+
|
25
|
+
srid ||= coord_sys.authority_code if coord_sys
|
26
|
+
srid = srid.to_i
|
27
|
+
# Create a coord sys based on the SRID if one was not given
|
28
|
+
coord_sys = coord_sys_class.create(srid) if coord_sys.nil? && srid != 0
|
29
|
+
|
30
|
+
{ coord_sys: coord_sys, srid: srid }
|
31
|
+
end
|
32
|
+
|
12
33
|
private
|
13
34
|
|
14
35
|
def symbolize_hash(hash)
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module RGeo
|
6
|
+
module ImplHelper
|
7
|
+
# Mixin based off of the JTS/GEOS IsValidOp class.
|
8
|
+
# Implements #valid? and #invalid_reason on Features that include this.
|
9
|
+
#
|
10
|
+
# @see https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/valid/IsValidOp.java
|
11
|
+
module ValidOp
|
12
|
+
# Validity of geometry
|
13
|
+
#
|
14
|
+
# @return Boolean
|
15
|
+
def valid?
|
16
|
+
invalid_reason.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reason for invalidity or nil if valid
|
20
|
+
#
|
21
|
+
# @return String
|
22
|
+
def invalid_reason
|
23
|
+
return @invalid_reason if defined?(@invalid_reason)
|
24
|
+
@invalid_reason = check_valid
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validity_helper
|
30
|
+
ValidOpHelpers
|
31
|
+
end
|
32
|
+
|
33
|
+
# Method that performs validity checking. Just checks the type of geometry
|
34
|
+
# and delegates to the proper validity checker.
|
35
|
+
#
|
36
|
+
# Returns a string describing the error or nil if it's a valid geometry.
|
37
|
+
# In some cases, "Unkown Validity" is returned if a dependent method has
|
38
|
+
# not been implemented.
|
39
|
+
#
|
40
|
+
# @return String
|
41
|
+
def check_valid
|
42
|
+
case self
|
43
|
+
when Feature::Point
|
44
|
+
check_valid_point
|
45
|
+
when Feature::LinearRing
|
46
|
+
check_valid_linear_ring
|
47
|
+
when Feature::LineString
|
48
|
+
check_valid_line_string
|
49
|
+
when Feature::Polygon
|
50
|
+
check_valid_polygon
|
51
|
+
when Feature::MultiPoint
|
52
|
+
check_valid_multi_point
|
53
|
+
when Feature::MultiPolygon
|
54
|
+
check_valid_multi_polygon
|
55
|
+
when Feature::GeometryCollection
|
56
|
+
check_valid_geometry_collection
|
57
|
+
else
|
58
|
+
raise NotImplementedError, "check_valid is not implemented for #{self}"
|
59
|
+
end
|
60
|
+
rescue RGeo::Error::UnsupportedOperation, NoMethodError
|
61
|
+
"Unkown Validity"
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_valid_point
|
65
|
+
validity_helper.check_invalid_coordinate(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_valid_line_string
|
69
|
+
# check coordinates are all valid
|
70
|
+
points.each do |pt|
|
71
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
72
|
+
return check unless check.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
# check more than 1 point
|
76
|
+
return Error::TOO_FEW_POINTS unless num_points > 1
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_valid_linear_ring
|
82
|
+
# check coordinates are all valid
|
83
|
+
points.each do |pt|
|
84
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
85
|
+
return check unless check.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# check closed
|
89
|
+
return Error::UNCLOSED_RING unless closed?
|
90
|
+
|
91
|
+
# check more than 3 points
|
92
|
+
return Error::TOO_FEW_POINTS unless num_points > 3
|
93
|
+
|
94
|
+
# check no self-intersections
|
95
|
+
validity_helper.check_no_self_intersections(self)
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_valid_polygon
|
99
|
+
# check coordinates are all valid
|
100
|
+
exterior_ring.points.each do |pt|
|
101
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
102
|
+
return check unless check.nil?
|
103
|
+
end
|
104
|
+
interior_rings.each do |ring|
|
105
|
+
ring.points.each do |pt|
|
106
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
107
|
+
return check unless check.nil?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# check closed
|
112
|
+
return Error::UNCLOSED_RING unless exterior_ring.closed?
|
113
|
+
return Error::UNCLOSED_RING unless interior_rings.all?(&:closed?)
|
114
|
+
|
115
|
+
# check more than 3 points in each ring
|
116
|
+
return Error::TOO_FEW_POINTS unless exterior_ring.num_points > 3
|
117
|
+
return Error::TOO_FEW_POINTS unless interior_rings.all? { |r| r.num_points > 3 }
|
118
|
+
|
119
|
+
# can skip this check if there's no holes
|
120
|
+
unless interior_rings.empty?
|
121
|
+
check = validity_helper.check_consistent_area(self)
|
122
|
+
return check unless check.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
# check that there are no self-intersections
|
126
|
+
check = validity_helper.check_no_self_intersecting_rings(self)
|
127
|
+
return check unless check.nil?
|
128
|
+
|
129
|
+
# can skip these checks if there's no holes
|
130
|
+
unless interior_rings.empty?
|
131
|
+
check = validity_helper.check_holes_in_shell(self)
|
132
|
+
return check unless check.nil?
|
133
|
+
|
134
|
+
check = validity_helper.check_holes_not_nested(self)
|
135
|
+
return check unless check.nil?
|
136
|
+
|
137
|
+
check = validity_helper.check_connected_interiors(self)
|
138
|
+
return check unless check.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def check_valid_multi_point
|
145
|
+
geometries.each do |pt|
|
146
|
+
check = validity_helper.check_invalid_coordinate(pt)
|
147
|
+
return check unless check.nil?
|
148
|
+
end
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def check_valid_multi_polygon
|
153
|
+
geometries.each do |poly|
|
154
|
+
return poly.invalid_reason unless poly.invalid_reason.nil?
|
155
|
+
end
|
156
|
+
|
157
|
+
check = validity_helper.check_consistent_area_mp(self)
|
158
|
+
return check unless check.nil?
|
159
|
+
|
160
|
+
# check no shells are nested
|
161
|
+
check = validity_helper.check_shells_not_nested(self)
|
162
|
+
return check unless check.nil?
|
163
|
+
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def check_valid_geometry_collection
|
168
|
+
geometries.each do |geom|
|
169
|
+
return geom.invalid_reason unless geom.invalid_reason.nil?
|
170
|
+
end
|
171
|
+
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Helper functions for specific validity checks
|
178
|
+
##
|
179
|
+
module ValidOpHelpers
|
180
|
+
module_function
|
181
|
+
|
182
|
+
# Checks that the given point has valid coordinates.
|
183
|
+
#
|
184
|
+
# @param point [RGeo::Feature::Point]
|
185
|
+
#
|
186
|
+
# @return [String] invalid_reason
|
187
|
+
def check_invalid_coordinate(point)
|
188
|
+
x = point.x
|
189
|
+
y = point.y
|
190
|
+
return if x.finite? && y.finite? && x.real? && y.real?
|
191
|
+
|
192
|
+
Error::INVALID_COORDINATE
|
193
|
+
end
|
194
|
+
|
195
|
+
# Checks that the edges in the polygon form a consistent area.
|
196
|
+
#
|
197
|
+
# Specifically, checks that there are intersections no between the
|
198
|
+
# holes and the shell.
|
199
|
+
#
|
200
|
+
# Also checks that there are no duplicate rings.
|
201
|
+
#
|
202
|
+
# @param poly [RGeo::Feature::Polygon]
|
203
|
+
#
|
204
|
+
# @return [String] invalid_reason
|
205
|
+
def check_consistent_area(poly)
|
206
|
+
# Holes don't cross exterior check.
|
207
|
+
exterior = poly.exterior_ring
|
208
|
+
poly.interior_rings.each do |ring|
|
209
|
+
return Error::SELF_INTERSECTION if ring.crosses?(exterior)
|
210
|
+
end
|
211
|
+
|
212
|
+
# check interiors do not cross
|
213
|
+
poly.interior_rings.combination(2).each do |ring1, ring2|
|
214
|
+
return Error::SELF_INTERSECTION if ring1.crosses?(ring2)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Duplicate rings check
|
218
|
+
rings = [exterior] + poly.interior_rings
|
219
|
+
return Error::SELF_INTERSECTION if rings.uniq.size != rings.size
|
220
|
+
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
|
224
|
+
# Checks that the ring does not self-intersect. This is just a simplicity
|
225
|
+
# check on the ring.
|
226
|
+
#
|
227
|
+
# @param ring [RGeo::Feature::LinearRing]
|
228
|
+
#
|
229
|
+
# @return [String] invalid_reason
|
230
|
+
def check_no_self_intersections(ring)
|
231
|
+
return Error::SELF_INTERSECTION unless ring.simple?
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check that rings do not self intersect in a polygon
|
235
|
+
#
|
236
|
+
# @param poly [RGeo::Feature::Polygon]
|
237
|
+
#
|
238
|
+
# @return [String] invalid_reason
|
239
|
+
def check_no_self_intersecting_rings(poly)
|
240
|
+
exterior = poly.exterior_ring
|
241
|
+
|
242
|
+
check = check_no_self_intersections(exterior)
|
243
|
+
return check unless check.nil?
|
244
|
+
|
245
|
+
poly.interior_rings.each do |ring|
|
246
|
+
check = check_no_self_intersections(ring)
|
247
|
+
return check unless check.nil?
|
248
|
+
end
|
249
|
+
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# Checks holes are contained inside the exterior of a polygon.
|
254
|
+
# Assuming check_consistent_area has already passed on the polygon,
|
255
|
+
# a simple point in polygon check can be done on one of the points
|
256
|
+
# in each hole to verify (since we know none of them intersect).
|
257
|
+
#
|
258
|
+
# @param poly [RGeo::Feature::Polygon]
|
259
|
+
#
|
260
|
+
# @return [String] invalid_reason
|
261
|
+
def check_holes_in_shell(poly)
|
262
|
+
# get hole-less shell as test polygon
|
263
|
+
shell = poly.exterior_ring
|
264
|
+
shell = shell.factory.polygon(shell)
|
265
|
+
|
266
|
+
poly.interior_rings.each do |interior|
|
267
|
+
test_pt = interior.start_point
|
268
|
+
return Error::HOLE_OUTSIDE_SHELL unless shell.contains?(test_pt) || poly.exterior_ring.contains?(test_pt)
|
269
|
+
end
|
270
|
+
|
271
|
+
nil
|
272
|
+
end
|
273
|
+
|
274
|
+
# Checks that holes are not nested within each other.
|
275
|
+
#
|
276
|
+
# @param poly [RGeo::Feature::Polygon]
|
277
|
+
#
|
278
|
+
# @return [String] invalid_reason
|
279
|
+
def check_holes_not_nested(poly)
|
280
|
+
# convert holes from linear_rings to polygons
|
281
|
+
# Same logic that applies to check_holes_in_shell applies here
|
282
|
+
# since we've already passed the consistent area test, we just
|
283
|
+
# have to check if one point from each hole is contained in the other.
|
284
|
+
holes = poly.interior_rings
|
285
|
+
holes = holes.map { |v| v.factory.polygon(v) }
|
286
|
+
holes.combination(2).each do |p1, p2|
|
287
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
288
|
+
return Error::NESTED_HOLES
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
nil
|
293
|
+
end
|
294
|
+
|
295
|
+
# Checks that the interior of the polygon is connected.
|
296
|
+
# A disconnected interior can be described by this polygon for example
|
297
|
+
# POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (5 0, 10 5, 5 10, 0 5, 5 0))
|
298
|
+
#
|
299
|
+
# Which is a square with a diamond inside of it.
|
300
|
+
#
|
301
|
+
# @param poly [RGeo::Feature::Polygon]
|
302
|
+
#
|
303
|
+
# @return [String] invalid_reason
|
304
|
+
def check_connected_interiors(poly)
|
305
|
+
# This is not proper and will flag valid geometries as invalid, but
|
306
|
+
# is an ok approximation.
|
307
|
+
# Idea is to check if a single hole has multiple points on the
|
308
|
+
# exterior ring.
|
309
|
+
poly.interior_rings.each do |ring|
|
310
|
+
touches = Set.new
|
311
|
+
ring.points.each do |pt|
|
312
|
+
touches.add(pt) if poly.exterior_ring.contains?(pt)
|
313
|
+
end
|
314
|
+
|
315
|
+
return Error::DISCONNECTED_INTERIOR if touches.size > 1
|
316
|
+
end
|
317
|
+
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
|
321
|
+
# Checks that polygons do not intersect in a multipolygon.
|
322
|
+
#
|
323
|
+
# @param mpoly [RGeo::Feature::MultiPolygon]
|
324
|
+
#
|
325
|
+
# @return [String] invalid_reason
|
326
|
+
def check_consistent_area_mp(mpoly)
|
327
|
+
mpoly.geometries.combination(2) do |p1, p2|
|
328
|
+
return Error::SELF_INTERSECTION if p1.exterior_ring.crosses?(p2.exterior_ring)
|
329
|
+
end
|
330
|
+
nil
|
331
|
+
end
|
332
|
+
|
333
|
+
# Checks that individual polygons within a multipolygon are not nested.
|
334
|
+
#
|
335
|
+
# @param mpoly [RGeo::Feature::MultiPolygon]
|
336
|
+
#
|
337
|
+
# @return [String] invalid_reason
|
338
|
+
def check_shells_not_nested(mpoly)
|
339
|
+
# Since we've passed the consistent area test, we can just check
|
340
|
+
# that one point lies in the other.
|
341
|
+
mpoly.geometries.combination(2) do |p1, p2|
|
342
|
+
if p1.contains?(p2.exterior_ring.start_point) || p2.contains?(p1.exterior_ring.start_point)
|
343
|
+
return Error::NESTED_SHELLS
|
344
|
+
end
|
345
|
+
end
|
346
|
+
nil
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|