rgeo 2.3.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/README.md +23 -14
- data/ext/geos_c_impl/analysis.c +30 -25
- data/ext/geos_c_impl/analysis.h +8 -7
- 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 +42 -28
- data/ext/geos_c_impl/factory.c +540 -451
- data/ext/geos_c_impl/factory.h +105 -95
- data/ext/geos_c_impl/geometry.c +593 -387
- data/ext/geos_c_impl/geometry.h +10 -5
- data/ext/geos_c_impl/geometry_collection.c +306 -339
- data/ext/geos_c_impl/geometry_collection.h +6 -20
- data/ext/geos_c_impl/globals.c +169 -0
- data/ext/geos_c_impl/globals.h +46 -0
- data/ext/geos_c_impl/line_string.c +271 -231
- data/ext/geos_c_impl/line_string.h +5 -8
- data/ext/geos_c_impl/main.c +16 -16
- data/ext/geos_c_impl/point.c +65 -67
- data/ext/geos_c_impl/point.h +4 -7
- data/ext/geos_c_impl/polygon.c +137 -135
- data/ext/geos_c_impl/polygon.h +11 -11
- data/ext/geos_c_impl/preface.h +16 -10
- 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 +303 -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 +69 -85
- data/lib/rgeo/geographic/factory.rb +98 -125
- data/lib/rgeo/geographic/interface.rb +69 -166
- 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 +26 -25
- 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 +105 -173
- data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
- data/lib/rgeo/geos/ffi_feature_methods.rb +105 -127
- data/lib/rgeo/geos/interface.rb +20 -59
- data/lib/rgeo/geos/utils.rb +5 -5
- data/lib/rgeo/geos/zm_factory.rb +53 -95
- data/lib/rgeo/geos/zm_feature_methods.rb +30 -33
- 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 +50 -13
- 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
@@ -0,0 +1,373 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RGeo
|
4
|
+
module Cartesian
|
5
|
+
# A Doubly Connected Edge List (DCEL) implementation of a Planar Graph.
|
6
|
+
# It represents geometries as vertices and half-edges.
|
7
|
+
#
|
8
|
+
# It includes an incident_edges hash that maps vertices to an array
|
9
|
+
# of half-edges whose origins are that vertex.
|
10
|
+
#
|
11
|
+
# Upon instantiation, the graph will compute the intersections using
|
12
|
+
# the SweeplineIntersector, populate the incident_edges map, and
|
13
|
+
# link all cyclic edges.
|
14
|
+
class PlanarGraph
|
15
|
+
# HalfEdge represents an edge as 2 directed edges.
|
16
|
+
# One half-edge will have it's origin at edge.s, the other
|
17
|
+
# at edge.e. Both half-edges will be linked as each other's twins.
|
18
|
+
#
|
19
|
+
# HalfEdges also contain references to the next and prev half-edges,
|
20
|
+
# where next's origin is this half-edges destination. Prev's destination
|
21
|
+
# is this half-edge's origin.
|
22
|
+
class HalfEdge
|
23
|
+
include Comparable
|
24
|
+
|
25
|
+
# Creates 2 half edges from an edge.
|
26
|
+
# They will be assigned as each other's twins.
|
27
|
+
# The Half Edges will be returned in the order of points
|
28
|
+
# of the edge (start, end).
|
29
|
+
#
|
30
|
+
# @param edge [RGeo::Cartesian::Segment]
|
31
|
+
#
|
32
|
+
# @return [Array]
|
33
|
+
def self.from_edge(edge)
|
34
|
+
e1 = new(edge.s)
|
35
|
+
e2 = new(edge.e)
|
36
|
+
|
37
|
+
e1.twin = e2
|
38
|
+
e2.twin = e1
|
39
|
+
[e1, e2]
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(origin)
|
43
|
+
@origin = origin
|
44
|
+
@twin = nil
|
45
|
+
@next = nil
|
46
|
+
@prev = nil
|
47
|
+
end
|
48
|
+
attr_reader :origin
|
49
|
+
attr_accessor :twin, :next, :prev
|
50
|
+
|
51
|
+
# HalfEdges will be sorted around their shared vertex
|
52
|
+
# in a CW fashion. This means that face interiors will be
|
53
|
+
# a CCW.
|
54
|
+
def <=>(other)
|
55
|
+
angle <=> other.angle
|
56
|
+
end
|
57
|
+
|
58
|
+
# Will find attempt to find a cycle starting at this
|
59
|
+
# HalfEdge. Will end upon finding the first repeated HalfEdge
|
60
|
+
# or a HalfEdge where +next+ is nil.
|
61
|
+
#
|
62
|
+
# If a block is given, each HalfEdge seen will be yielded to the block.
|
63
|
+
#
|
64
|
+
# @return [Enumerator]
|
65
|
+
def and_connected
|
66
|
+
return to_enum(__method__) unless block_given?
|
67
|
+
|
68
|
+
hedges = Set.new
|
69
|
+
yield(self)
|
70
|
+
hedges << self
|
71
|
+
|
72
|
+
n = self.next
|
73
|
+
until hedges.include?(n) || n.nil?
|
74
|
+
yield(n)
|
75
|
+
hedges << n
|
76
|
+
n = n.next
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the destination of the half edge
|
81
|
+
#
|
82
|
+
# @return [RGeo::Feature::Point]
|
83
|
+
def destination
|
84
|
+
twin&.origin
|
85
|
+
end
|
86
|
+
|
87
|
+
# Compute the angle from the positive x-axis.
|
88
|
+
# Used for sorting at each node.
|
89
|
+
#
|
90
|
+
# @return [Float]
|
91
|
+
def angle
|
92
|
+
@angle ||= Math.atan2(destination.y - origin.y, destination.x - origin.x)
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{self}>"
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
dst = destination
|
101
|
+
pr = prev&.origin
|
102
|
+
n = @next&.origin
|
103
|
+
"HalfEdge(#{origin}, #{dst}), Prev: #{pr}, Next: #{n}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Create a new PlanarGraph
|
108
|
+
#
|
109
|
+
# @param edges [Array<RGeo::Cartesian::Segment>] of Segments
|
110
|
+
def initialize(edges = [])
|
111
|
+
@edges = []
|
112
|
+
@incident_edges = {}
|
113
|
+
|
114
|
+
add_edges(edges)
|
115
|
+
end
|
116
|
+
attr_reader :edges, :incident_edges
|
117
|
+
|
118
|
+
# Insert an edge into the graph. This will automatically
|
119
|
+
# calculate intersections and add new vertices if necessary.
|
120
|
+
#
|
121
|
+
# @param edge [RGeo::Cartesian::Segment]
|
122
|
+
def add_edge(edge)
|
123
|
+
@edges << edge
|
124
|
+
create_half_edge(edge)
|
125
|
+
|
126
|
+
# Check if a new intersection was made and handle it.
|
127
|
+
intersection_map.each do |seg, ints|
|
128
|
+
compute_split_edges(seg, ints)
|
129
|
+
end
|
130
|
+
|
131
|
+
link_half_edges
|
132
|
+
end
|
133
|
+
|
134
|
+
# Insert multiple edges into the graph. Like +add_edge+, this automatically
|
135
|
+
# calculates intersections and adds new vertices.
|
136
|
+
#
|
137
|
+
# @param new_edges [Array<RGeo::Cartesian::Segment>]
|
138
|
+
def add_edges(new_edges)
|
139
|
+
@edges.concat(new_edges)
|
140
|
+
new_edges.each do |edge|
|
141
|
+
create_half_edge(edge)
|
142
|
+
end
|
143
|
+
|
144
|
+
intersection_map.each do |seg, ints|
|
145
|
+
compute_split_edges(seg, ints)
|
146
|
+
end
|
147
|
+
|
148
|
+
link_half_edges
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
# Creates a map of +proper_intersections+ for each segment
|
154
|
+
# from a sweepline intersector.
|
155
|
+
#
|
156
|
+
# Can be used to determine which edges need to be split
|
157
|
+
# after adding edges.
|
158
|
+
def intersection_map
|
159
|
+
intersector = SweeplineIntersector.new(edges)
|
160
|
+
intersections = intersector.proper_intersections
|
161
|
+
|
162
|
+
intersection_map = {}
|
163
|
+
intersections.each do |int|
|
164
|
+
# check the int_point against each edge.
|
165
|
+
# if it is not on the boundary of the edge, add it to the
|
166
|
+
# list of intersections for that edge.
|
167
|
+
s1_intersects = int.point != int.s1.s && int.point != int.s1.e
|
168
|
+
if s1_intersects
|
169
|
+
intersection_map[int.s1] ||= []
|
170
|
+
intersection_map[int.s1] << int.point
|
171
|
+
end
|
172
|
+
|
173
|
+
s2_intersects = int.point != int.s2.s && int.point != int.s2.e
|
174
|
+
|
175
|
+
next unless s2_intersects
|
176
|
+
|
177
|
+
intersection_map[int.s2] ||= []
|
178
|
+
intersection_map[int.s2] << int.point
|
179
|
+
end
|
180
|
+
intersection_map
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_half_edges
|
184
|
+
@edges.each do |edge|
|
185
|
+
create_half_edge(edge)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def create_half_edge(edge)
|
190
|
+
e1, e2 = HalfEdge.from_edge(edge)
|
191
|
+
insert_half_edge(e1)
|
192
|
+
insert_half_edge(e2)
|
193
|
+
end
|
194
|
+
|
195
|
+
def insert_half_edge(half_edge)
|
196
|
+
@incident_edges[half_edge.origin.coordinates] ||= []
|
197
|
+
@incident_edges[half_edge.origin.coordinates] << half_edge
|
198
|
+
end
|
199
|
+
|
200
|
+
# Links all half-edges where possible.
|
201
|
+
# Defines +next+ and +prev+ for every half-edge by rotating
|
202
|
+
# through all half-edges originating at a vertex.
|
203
|
+
#
|
204
|
+
# Assuming half-edges are sorted CCW, every sequential pair of
|
205
|
+
# half-edges (e1, e2) can be linked by saying e1.prev = e2.twin
|
206
|
+
# and e2.twin.next = e1.
|
207
|
+
def link_half_edges
|
208
|
+
incident_edges.each_value do |hedges|
|
209
|
+
hedges.sort!
|
210
|
+
[*hedges, hedges.first].each_cons(2) do |e1, e2|
|
211
|
+
e1.prev = e2.twin
|
212
|
+
e2.twin.next = e1
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# It is possible that intersections occur when new edges are added.
|
218
|
+
# This will split those edges into more half-edges while preserving
|
219
|
+
# the existing half-edges when possible since geometries may reference
|
220
|
+
# them.
|
221
|
+
def compute_split_edges(seg, ints)
|
222
|
+
points = ints.uniq.sort do |a, b|
|
223
|
+
a.distance(seg.s) <=> b.distance(seg.s)
|
224
|
+
end
|
225
|
+
points = [seg.s] + points + [seg.e]
|
226
|
+
|
227
|
+
he_start = incident_edges[points.first.coordinates].find { |he| he.destination == points.last }
|
228
|
+
he_end = incident_edges[points.last.coordinates].find { |he| he.destination == points.first }
|
229
|
+
|
230
|
+
points.each_cons(2) do |s, e|
|
231
|
+
edge = Segment.new(s, e)
|
232
|
+
if s == he_start.origin
|
233
|
+
he = HalfEdge.new(e)
|
234
|
+
he_start.twin = he
|
235
|
+
he.twin = he_start
|
236
|
+
insert_half_edge(he)
|
237
|
+
elsif e == he_end.origin
|
238
|
+
he = HalfEdge.new(s)
|
239
|
+
he_end.twin = he
|
240
|
+
he.twin = he_end
|
241
|
+
insert_half_edge(he)
|
242
|
+
else
|
243
|
+
create_half_edge(edge)
|
244
|
+
end
|
245
|
+
@edges << edge
|
246
|
+
end
|
247
|
+
@edges.delete(seg)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# GeometryGraph is a PlanarGraph that is built by adding
|
252
|
+
# geometries instead of edges. The GeometryGraph will
|
253
|
+
# hold a reference to an arbitrary HalfEdge on the
|
254
|
+
# interior of the geometry for every boundary in the geometry.
|
255
|
+
# For example, a polygon will have a reference to a HalfEdge for its
|
256
|
+
# exterior shell and one for every hole.
|
257
|
+
class GeometryGraph < PlanarGraph
|
258
|
+
# GeomEdge will be used to store the references to the HalfEdges
|
259
|
+
GeomEdge = Struct.new(:exterior_edge, :interior_edges)
|
260
|
+
|
261
|
+
def initialize(geom)
|
262
|
+
super()
|
263
|
+
@parent_geometry = geom
|
264
|
+
@geom_edges = []
|
265
|
+
add_geometry(geom)
|
266
|
+
end
|
267
|
+
attr_reader :parent_geometry, :geom_edges
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
# Adds a geometry to the graph and finds its
|
272
|
+
# reference HalfEdge(s).
|
273
|
+
#
|
274
|
+
# @param geom [RGeo::Feature::Instance]
|
275
|
+
def add_geometry(geom)
|
276
|
+
case geom
|
277
|
+
when Feature::Point
|
278
|
+
# Can't handle points yet, so just add an empty entry for them
|
279
|
+
@geom_edges << GeomEdge.new
|
280
|
+
when Feature::LineString, Feature::LinearRing
|
281
|
+
add_line_string(geom)
|
282
|
+
when Feature::Polygon
|
283
|
+
add_polygon(geom)
|
284
|
+
when Feature::GeometryCollection
|
285
|
+
add_collection(geom)
|
286
|
+
else
|
287
|
+
raise Error::RGeoError, "Invalid Geometry Type: #{geom.class}"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Adds a LineString or LinearRing
|
292
|
+
# to the graph.
|
293
|
+
#
|
294
|
+
# @param geom [RGeo::Feature::LineString]
|
295
|
+
def add_line_string(geom)
|
296
|
+
add_edges(geom.segments)
|
297
|
+
|
298
|
+
hedge = @incident_edges[geom.start_point.coordinates].first unless geom.empty?
|
299
|
+
|
300
|
+
@geom_edges << GeomEdge.new(hedge, nil)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Adds a Polygon to the graph.
|
304
|
+
#
|
305
|
+
# Will add each shell seperately and find a CCW half-edge
|
306
|
+
# for the exterior ring and a CW half-edge for interior rings
|
307
|
+
# since these are designated as being on the interior of the polygon.
|
308
|
+
#
|
309
|
+
# It is possible that the half-edge will be nil if a suitable one cannot
|
310
|
+
# be found. This indicates the polygon is likely invalid.
|
311
|
+
#
|
312
|
+
# @param geom [RGeo::Feature::Polygon]
|
313
|
+
def add_polygon(geom)
|
314
|
+
exterior = geom.exterior_ring
|
315
|
+
add_edges(exterior.segments)
|
316
|
+
|
317
|
+
hedge = find_hedge(exterior)
|
318
|
+
|
319
|
+
interior_hedges = []
|
320
|
+
geom.interior_rings.each do |interior|
|
321
|
+
add_edges(interior.segments)
|
322
|
+
interior_hedges << find_hedge(interior, ccw: false)
|
323
|
+
end
|
324
|
+
|
325
|
+
@geom_edges << GeomEdge.new(hedge, interior_hedges)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Adds a GeometryCollection to the graph.
|
329
|
+
#
|
330
|
+
# @param col [RGeo::Feature::GeometryCollection]
|
331
|
+
def add_collection(col)
|
332
|
+
col.each do |geom|
|
333
|
+
add_geometry(geom)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Finds a Half-Edge that is part of a CCW or CW rotation
|
338
|
+
# from the input ring. Returns nil if none found.
|
339
|
+
#
|
340
|
+
# Will only consider half-edges that are colinear with
|
341
|
+
# the first or last segments of the ring.
|
342
|
+
#
|
343
|
+
# @param ring [RGeo::Feature::LinearRing]
|
344
|
+
# @param ccw [Boolean] true for CCW, false for CW
|
345
|
+
# @return [HalfEdge, nil]
|
346
|
+
def find_hedge(ring, ccw: true)
|
347
|
+
return nil if ring.num_points == 0
|
348
|
+
ccw_target = ccw ? 1 : -1
|
349
|
+
|
350
|
+
coords = ring.start_point.coordinates
|
351
|
+
hedges = incident_edges[coords]
|
352
|
+
|
353
|
+
# find half-edges that are colinear to the start or end
|
354
|
+
# segment of the ring.
|
355
|
+
start_seg = Segment.new(ring.start_point, ring.point_n(1))
|
356
|
+
end_seg = Segment.new(ring.point_n(ring.num_points - 2), ring.end_point)
|
357
|
+
colinear_hedges = hedges.select do |he|
|
358
|
+
start_seg.side(he.destination) == 0 || end_seg.side(he.destination) == 0
|
359
|
+
end
|
360
|
+
|
361
|
+
colinear_hedges.find do |hedge|
|
362
|
+
pts = []
|
363
|
+
hedge.and_connected do |he|
|
364
|
+
pts << he.origin
|
365
|
+
end
|
366
|
+
pts << hedge.origin
|
367
|
+
lr = parent_geometry.factory.line_string(pts)
|
368
|
+
Analysis.ring_direction(lr) == ccw_target
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# -----------------------------------------------------------------------------
|
4
|
+
#
|
5
|
+
# Simple Sweepline Intersector Class
|
6
|
+
#
|
7
|
+
# -----------------------------------------------------------------------------
|
8
|
+
|
9
|
+
module RGeo
|
10
|
+
module Cartesian
|
11
|
+
# Implements a Sweepline intersector to find all intersections
|
12
|
+
# in a group of segments. The idea is to use a horizontal line starting
|
13
|
+
# at y = +Infinity that sweeps down to y = -Infinity and every time it hits
|
14
|
+
# a new line, it will check if it intersects with any of the segments
|
15
|
+
# the line currently intersects at that y value.
|
16
|
+
# This is a more simplistic implementation that uses an array to hold
|
17
|
+
# observed segments instead of a sorted BST, so performance may be significantly
|
18
|
+
# worse in the case of lots of segments overlapping in y-ranges.
|
19
|
+
class SweeplineIntersector
|
20
|
+
Event = Struct.new(:point, :segment, :is_start)
|
21
|
+
Intersection = Struct.new(:point, :s1, :s2)
|
22
|
+
|
23
|
+
def initialize(segments)
|
24
|
+
@segments = segments
|
25
|
+
end
|
26
|
+
attr_reader :segments
|
27
|
+
|
28
|
+
# Returns the "proper" intersections from the list of segments.
|
29
|
+
#
|
30
|
+
# This will only return intersections that are not the start/end or
|
31
|
+
# end/start of the 2 segments. This could be useful for finding intersections
|
32
|
+
# in a ring for example, because knowing that segments are connected in a linestring
|
33
|
+
# is not always helpful, but those are reported by default.
|
34
|
+
#
|
35
|
+
# Note: This is not the true definition of a proper intersection. A
|
36
|
+
# truly proper intersection does not include colinear intersections and
|
37
|
+
# the intersection must lie in the interior of both segments.
|
38
|
+
#
|
39
|
+
# @return [Array<RGeo::Cartesian::SweeplineIntersector::Intersection>]
|
40
|
+
def proper_intersections
|
41
|
+
return @proper_intersections if defined?(@proper_intersections)
|
42
|
+
|
43
|
+
@proper_intersections = []
|
44
|
+
intersections.each do |intersection|
|
45
|
+
s1 = intersection.s1
|
46
|
+
s2 = intersection.s2
|
47
|
+
pt = intersection.point
|
48
|
+
|
49
|
+
@proper_intersections << intersection unless (pt == s1.s && pt == s2.e) || (pt == s1.e && pt == s2.s)
|
50
|
+
end
|
51
|
+
@proper_intersections
|
52
|
+
end
|
53
|
+
|
54
|
+
# Computes the intersections of the input segments.
|
55
|
+
#
|
56
|
+
# Creates an event queue from the +events+ and adds segments to the
|
57
|
+
# +observed_segments+ array while their ending event has not been popped
|
58
|
+
# from the queue.
|
59
|
+
#
|
60
|
+
# Compares the new segment from the +is_start+ event to each observed segment
|
61
|
+
# then adds it to +observed_segments+. Records any intersections in to the
|
62
|
+
# returned array.
|
63
|
+
#
|
64
|
+
# @return [Array<RGeo::Cartesian::SweeplineIntersector::Intersection>]
|
65
|
+
def intersections
|
66
|
+
return @intersections if defined?(@intersections)
|
67
|
+
|
68
|
+
@intersections = []
|
69
|
+
observed_segments = Set.new
|
70
|
+
events.each do |e|
|
71
|
+
seg = e.segment
|
72
|
+
|
73
|
+
if e.is_start
|
74
|
+
observed_segments.each do |oseg|
|
75
|
+
int_pt = seg.segment_intersection(oseg)
|
76
|
+
if int_pt
|
77
|
+
intersect = Intersection.new(int_pt, seg, oseg)
|
78
|
+
@intersections << intersect
|
79
|
+
end
|
80
|
+
end
|
81
|
+
observed_segments << seg
|
82
|
+
else
|
83
|
+
observed_segments.delete(seg)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@intersections
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns an ordered array of events from the input segments. Events
|
90
|
+
# are the start and endpoints of each segment with an is_start tag to
|
91
|
+
# indicate if this is the starting or ending event for that segment.
|
92
|
+
#
|
93
|
+
# Ordering is done by greatest-y -> smallest-x -> is_start = true.
|
94
|
+
#
|
95
|
+
# @return [Array]
|
96
|
+
def events
|
97
|
+
return @events if defined?(@events)
|
98
|
+
|
99
|
+
@events = []
|
100
|
+
segments.each do |segment|
|
101
|
+
event_pair = create_event_pair(segment)
|
102
|
+
@events.concat(event_pair)
|
103
|
+
end
|
104
|
+
|
105
|
+
@events.sort! do |a, b|
|
106
|
+
if a.point == b.point
|
107
|
+
if a.is_start
|
108
|
+
-1
|
109
|
+
else
|
110
|
+
1
|
111
|
+
end
|
112
|
+
elsif a.point.y == b.point.y
|
113
|
+
a.point.x <=> b.point.x
|
114
|
+
else
|
115
|
+
b.point.y <=> a.point.y
|
116
|
+
end
|
117
|
+
end
|
118
|
+
@events
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Creates a pair of events from a segment
|
124
|
+
#
|
125
|
+
# @param segment [Segment]
|
126
|
+
#
|
127
|
+
# @return [Array]
|
128
|
+
def create_event_pair(segment)
|
129
|
+
s = segment.s
|
130
|
+
e = segment.e
|
131
|
+
|
132
|
+
s_event = Event.new(s, segment)
|
133
|
+
e_event = Event.new(e, segment)
|
134
|
+
|
135
|
+
if s.y > e.y || (s.y == e.y && s.x < e.x)
|
136
|
+
s_event.is_start = true
|
137
|
+
e_event.is_start = false
|
138
|
+
else
|
139
|
+
s_event.is_start = false
|
140
|
+
e_event.is_start = true
|
141
|
+
end
|
142
|
+
|
143
|
+
[s_event, e_event]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RGeo
|
4
|
+
module Cartesian
|
5
|
+
module ValidOp
|
6
|
+
include ImplHelper::ValidOp
|
7
|
+
|
8
|
+
def validity_helper
|
9
|
+
ValidOpHelpers
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ValidOpHelpers
|
14
|
+
include ImplHelper::ValidOpHelpers
|
15
|
+
|
16
|
+
module_function(*ImplHelper::ValidOpHelpers.singleton_methods) # rubocop:disable Style/AccessModifierDeclarations
|
17
|
+
|
18
|
+
module_function
|
19
|
+
|
20
|
+
# Checks that there are no invalid intersections between the components
|
21
|
+
# of a polygon.
|
22
|
+
#
|
23
|
+
# @param [RGeo::Feature::Polygon] poly
|
24
|
+
#
|
25
|
+
# @return [String] invalid_reason
|
26
|
+
def check_consistent_area(poly)
|
27
|
+
# Get set of unique coords
|
28
|
+
pts = poly.exterior_ring.coordinates.to_set
|
29
|
+
poly.interior_rings.each do |ring|
|
30
|
+
pts += ring.coordinates
|
31
|
+
end
|
32
|
+
num_points = pts.size
|
33
|
+
|
34
|
+
# if additional nodes were added, there must be an intersection
|
35
|
+
# through a boundary.
|
36
|
+
return Error::SELF_INTERSECTION if poly.send(:graph).incident_edges.size > num_points
|
37
|
+
|
38
|
+
rings = [poly.exterior_ring] + poly.interior_rings
|
39
|
+
return Error::SELF_INTERSECTION if rings.uniq.size != rings.size
|
40
|
+
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks that the interior of a polygon is connected.
|
45
|
+
#
|
46
|
+
# Process to do this is to walk around an interior cycle of the
|
47
|
+
# exterior shell in the polygon's geometry graph. It will keep track
|
48
|
+
# of all the nodes it visited and if that set is a superset of the
|
49
|
+
# coordinates in the exterior_ring, the interior is connected, otherwise
|
50
|
+
# it is split somewhere.
|
51
|
+
#
|
52
|
+
# @param [RGeo::Feature::Polygon] poly
|
53
|
+
#
|
54
|
+
# @return [String] invalid_reason
|
55
|
+
def check_connected_interiors(poly)
|
56
|
+
exterior_coords = poly.exterior_ring.coordinates.to_set
|
57
|
+
|
58
|
+
visited = Set.new
|
59
|
+
poly.send(:graph).geom_edges.first.exterior_edge.and_connected do |hedge|
|
60
|
+
visited << hedge.origin.coordinates
|
61
|
+
end
|
62
|
+
|
63
|
+
return Error::DISCONNECTED_INTERIOR unless exterior_coords.subset?(visited)
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/rgeo/cartesian.rb
CHANGED
@@ -8,8 +8,11 @@
|
|
8
8
|
|
9
9
|
require_relative "cartesian/calculations"
|
10
10
|
require_relative "cartesian/feature_methods"
|
11
|
+
require_relative "cartesian/valid_op"
|
11
12
|
require_relative "cartesian/feature_classes"
|
12
13
|
require_relative "cartesian/factory"
|
13
14
|
require_relative "cartesian/interface"
|
14
15
|
require_relative "cartesian/bounding_box"
|
15
16
|
require_relative "cartesian/analysis"
|
17
|
+
require_relative "cartesian/sweepline_intersector"
|
18
|
+
require_relative "cartesian/planar_graph"
|