rgeo 2.3.0 → 3.0.0.pre.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/README.md +1 -0
  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 +251 -182
  10. data/ext/geos_c_impl/factory.h +43 -62
  11. data/ext/geos_c_impl/geometry.c +56 -24
  12. data/ext/geos_c_impl/geometry.h +8 -3
  13. data/ext/geos_c_impl/geometry_collection.c +41 -148
  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 +15 -51
  23. data/ext/geos_c_impl/polygon.h +1 -3
  24. data/ext/geos_c_impl/preface.h +8 -0
  25. data/lib/rgeo/cartesian/analysis.rb +2 -2
  26. data/lib/rgeo/cartesian/calculations.rb +54 -17
  27. data/lib/rgeo/cartesian/factory.rb +0 -7
  28. data/lib/rgeo/cartesian/feature_classes.rb +66 -46
  29. data/lib/rgeo/cartesian/feature_methods.rb +56 -20
  30. data/lib/rgeo/cartesian/interface.rb +0 -6
  31. data/lib/rgeo/cartesian/planar_graph.rb +379 -0
  32. data/lib/rgeo/cartesian/sweepline_intersector.rb +149 -0
  33. data/lib/rgeo/cartesian/valid_op.rb +71 -0
  34. data/lib/rgeo/cartesian.rb +3 -0
  35. data/lib/rgeo/coord_sys/cs/wkt_parser.rb +6 -6
  36. data/lib/rgeo/error.rb +15 -0
  37. data/lib/rgeo/feature/curve.rb +12 -2
  38. data/lib/rgeo/feature/geometry.rb +38 -28
  39. data/lib/rgeo/feature/geometry_collection.rb +13 -5
  40. data/lib/rgeo/feature/line_string.rb +3 -3
  41. data/lib/rgeo/feature/multi_curve.rb +6 -1
  42. data/lib/rgeo/feature/multi_surface.rb +3 -3
  43. data/lib/rgeo/feature/point.rb +4 -4
  44. data/lib/rgeo/feature/surface.rb +3 -3
  45. data/lib/rgeo/geographic/factory.rb +0 -7
  46. data/lib/rgeo/geographic/interface.rb +4 -18
  47. data/lib/rgeo/geographic/proj4_projector.rb +0 -2
  48. data/lib/rgeo/geographic/projected_feature_classes.rb +21 -9
  49. data/lib/rgeo/geographic/projected_feature_methods.rb +63 -30
  50. data/lib/rgeo/geographic/simple_mercator_projector.rb +0 -2
  51. data/lib/rgeo/geographic/spherical_feature_classes.rb +29 -9
  52. data/lib/rgeo/geographic/spherical_feature_methods.rb +68 -2
  53. data/lib/rgeo/geos/capi_factory.rb +21 -31
  54. data/lib/rgeo/geos/capi_feature_classes.rb +64 -11
  55. data/lib/rgeo/geos/ffi_factory.rb +0 -28
  56. data/lib/rgeo/geos/ffi_feature_classes.rb +34 -10
  57. data/lib/rgeo/geos/ffi_feature_methods.rb +53 -10
  58. data/lib/rgeo/geos/interface.rb +18 -10
  59. data/lib/rgeo/geos/zm_factory.rb +0 -12
  60. data/lib/rgeo/geos/zm_feature_methods.rb +30 -5
  61. data/lib/rgeo/impl_helper/basic_geometry_collection_methods.rb +18 -8
  62. data/lib/rgeo/impl_helper/basic_geometry_methods.rb +1 -1
  63. data/lib/rgeo/impl_helper/basic_line_string_methods.rb +37 -26
  64. data/lib/rgeo/impl_helper/basic_point_methods.rb +13 -3
  65. data/lib/rgeo/impl_helper/basic_polygon_methods.rb +8 -3
  66. data/lib/rgeo/impl_helper/valid_op.rb +354 -0
  67. data/lib/rgeo/impl_helper/validity_check.rb +138 -0
  68. data/lib/rgeo/impl_helper.rb +1 -0
  69. data/lib/rgeo/version.rb +1 -1
  70. data/lib/rgeo/wkrep/wkb_generator.rb +1 -1
  71. data/lib/rgeo/wkrep/wkt_generator.rb +6 -6
  72. metadata +30 -7
@@ -42,7 +42,6 @@ module RGeo
42
42
  end
43
43
  srid ||= @coord_sys.authority_code if @coord_sys
44
44
  @srid = srid.to_i
45
- @lenient_assertions = opts[:uses_lenient_assertions] ? true : false
46
45
  @buffer_resolution = opts[:buffer_resolution].to_i
47
46
  @buffer_resolution = 1 if @buffer_resolution < 1
48
47
 
@@ -103,7 +102,6 @@ module RGeo
103
102
  "wkbg" => @wkb_generator.properties,
104
103
  "wktp" => @wkt_parser.properties,
105
104
  "wkbp" => @wkb_parser.properties,
106
- "lena" => @lenient_assertions,
107
105
  "bufr" => @buffer_resolution
108
106
  }
109
107
  hash_["proj4"] = @proj4.marshal_dump if @proj4
@@ -131,7 +129,6 @@ module RGeo
131
129
  wkb_generator: symbolize_hash(data["wkbg"]),
132
130
  wkt_parser: symbolize_hash(data["wktp"]),
133
131
  wkb_parser: symbolize_hash(data["wkbp"]),
134
- uses_lenient_assertions: data["lena"],
135
132
  buffer_resolution: data["bufr"],
136
133
  proj4: proj4,
137
134
  coord_sys: coord_sys
@@ -144,7 +141,6 @@ module RGeo
144
141
  coder["has_z_coordinate"] = @has_z
145
142
  coder["has_m_coordinate"] = @has_m
146
143
  coder["srid"] = @srid
147
- coder["lenient_assertions"] = @lenient_assertions
148
144
  coder["buffer_resolution"] = @buffer_resolution
149
145
  coder["wkt_generator"] = @wkt_generator.properties
150
146
  coder["wkb_generator"] = @wkb_generator.properties
@@ -180,7 +176,6 @@ module RGeo
180
176
  wkb_generator: symbolize_hash(coder["wkb_generator"]),
181
177
  wkt_parser: symbolize_hash(coder["wkt_parser"]),
182
178
  wkb_parser: symbolize_hash(coder["wkb_parser"]),
183
- uses_lenient_assertions: coder["lenient_assertions"],
184
179
  buffer_resolution: coder["buffer_resolution"],
185
180
  proj4: proj4,
186
181
  coord_sys: coord_sys
@@ -199,8 +194,6 @@ module RGeo
199
194
  @has_z
200
195
  when :has_m_coordinate
201
196
  @has_m
202
- when :uses_lenient_assertions
203
- @lenient_assertions
204
197
  when :buffer_resolution
205
198
  @buffer_resolution
206
199
  when :is_cartesian
@@ -9,76 +9,96 @@
9
9
  module RGeo
10
10
  module Cartesian
11
11
  class PointImpl # :nodoc:
12
- include RGeo::Feature::Point
13
- include RGeo::ImplHelper::BasicGeometryMethods
14
- include RGeo::ImplHelper::BasicPointMethods
15
- include RGeo::Cartesian::GeometryMethods
16
- include RGeo::Cartesian::PointMethods
12
+ include Feature::Point
13
+ include ImplHelper::ValidityCheck
14
+ include ImplHelper::BasicGeometryMethods
15
+ include ImplHelper::BasicPointMethods
16
+ include ImplHelper::ValidOp
17
+ include GeometryMethods
18
+ include PointMethods
17
19
  end
18
20
 
19
21
  class LineStringImpl # :nodoc:
20
- include RGeo::Feature::LineString
21
- include RGeo::ImplHelper::BasicGeometryMethods
22
- include RGeo::ImplHelper::BasicLineStringMethods
23
- include RGeo::Cartesian::GeometryMethods
24
- include RGeo::Cartesian::LineStringMethods
22
+ include Feature::LineString
23
+ include ImplHelper::ValidityCheck
24
+ include ImplHelper::BasicGeometryMethods
25
+ include ImplHelper::BasicLineStringMethods
26
+ include ImplHelper::ValidOp
27
+ include GeometryMethods
28
+ include LineStringMethods
25
29
  end
26
30
 
27
31
  class LineImpl # :nodoc:
28
- include RGeo::Feature::Line
29
- include RGeo::ImplHelper::BasicGeometryMethods
30
- include RGeo::ImplHelper::BasicLineStringMethods
31
- include RGeo::ImplHelper::BasicLineMethods
32
- include RGeo::Cartesian::GeometryMethods
33
- include RGeo::Cartesian::LineStringMethods
32
+ include Feature::Line
33
+ include ImplHelper::ValidityCheck
34
+ include ImplHelper::BasicGeometryMethods
35
+ include ImplHelper::BasicLineStringMethods
36
+ include ImplHelper::BasicLineMethods
37
+ include ImplHelper::ValidOp
38
+ include GeometryMethods
39
+ include LineStringMethods
34
40
  end
35
41
 
36
42
  class LinearRingImpl # :nodoc:
37
- include RGeo::Feature::LinearRing
38
- include RGeo::ImplHelper::BasicGeometryMethods
39
- include RGeo::ImplHelper::BasicLineStringMethods
40
- include RGeo::ImplHelper::BasicLinearRingMethods
41
- include RGeo::Cartesian::GeometryMethods
42
- include RGeo::Cartesian::LineStringMethods
43
+ include Feature::LinearRing
44
+ include ImplHelper::ValidityCheck
45
+ include ImplHelper::BasicGeometryMethods
46
+ include ImplHelper::BasicLineStringMethods
47
+ include ImplHelper::BasicLinearRingMethods
48
+ include ImplHelper::ValidOp
49
+ include GeometryMethods
50
+ include LineStringMethods
43
51
  end
44
52
 
45
53
  class PolygonImpl # :nodoc:
46
- include RGeo::Feature::Polygon
47
- include RGeo::ImplHelper::BasicGeometryMethods
48
- include RGeo::ImplHelper::BasicPolygonMethods
49
- include RGeo::Cartesian::GeometryMethods
54
+ include Feature::Polygon
55
+ include ImplHelper::ValidityCheck
56
+ include ImplHelper::BasicGeometryMethods
57
+ include ImplHelper::BasicPolygonMethods
58
+ include ValidOp
59
+ include GeometryMethods
50
60
  end
51
61
 
52
62
  class GeometryCollectionImpl # :nodoc:
53
- include RGeo::Feature::GeometryCollection
54
- include RGeo::ImplHelper::BasicGeometryMethods
55
- include RGeo::ImplHelper::BasicGeometryCollectionMethods
56
- include RGeo::Cartesian::GeometryMethods
63
+ include Feature::GeometryCollection
64
+ include ImplHelper::ValidityCheck
65
+ include ImplHelper::BasicGeometryMethods
66
+ include ImplHelper::BasicGeometryCollectionMethods
67
+ include ImplHelper::ValidOp
68
+ include GeometryMethods
57
69
  end
58
70
 
59
71
  class MultiPointImpl # :nodoc:
60
- include RGeo::Feature::MultiPoint
61
- include RGeo::ImplHelper::BasicGeometryMethods
62
- include RGeo::ImplHelper::BasicGeometryCollectionMethods
63
- include RGeo::ImplHelper::BasicMultiPointMethods
64
- include RGeo::Cartesian::GeometryMethods
72
+ include Feature::MultiPoint
73
+ include ImplHelper::ValidityCheck
74
+ include ImplHelper::BasicGeometryMethods
75
+ include ImplHelper::BasicGeometryCollectionMethods
76
+ include ImplHelper::BasicMultiPointMethods
77
+ include ImplHelper::ValidOp
78
+ include GeometryMethods
65
79
  end
66
80
 
67
81
  class MultiLineStringImpl # :nodoc:
68
- include RGeo::Feature::MultiLineString
69
- include RGeo::ImplHelper::BasicGeometryMethods
70
- include RGeo::ImplHelper::BasicGeometryCollectionMethods
71
- include RGeo::ImplHelper::BasicMultiLineStringMethods
72
- include RGeo::Cartesian::GeometryMethods
73
- include RGeo::Cartesian::MultiLineStringMethods
82
+ include Feature::MultiLineString
83
+ include ImplHelper::ValidityCheck
84
+ include ImplHelper::BasicGeometryMethods
85
+ include ImplHelper::BasicGeometryCollectionMethods
86
+ include ImplHelper::BasicMultiLineStringMethods
87
+ include ImplHelper::ValidOp
88
+ include GeometryMethods
89
+ include MultiLineStringMethods
74
90
  end
75
91
 
76
92
  class MultiPolygonImpl # :nodoc:
77
- include RGeo::Feature::MultiPolygon
78
- include RGeo::ImplHelper::BasicGeometryMethods
79
- include RGeo::ImplHelper::BasicGeometryCollectionMethods
80
- include RGeo::ImplHelper::BasicMultiPolygonMethods
81
- include RGeo::Cartesian::GeometryMethods
93
+ include Feature::MultiPolygon
94
+ include ImplHelper::ValidityCheck
95
+ include ImplHelper::BasicGeometryMethods
96
+ include ImplHelper::BasicGeometryCollectionMethods
97
+ include ImplHelper::BasicMultiPolygonMethods
98
+ include ImplHelper::ValidOp
99
+ include GeometryMethods
82
100
  end
101
+
102
+ ImplHelper::ValidityCheck.override_classes
83
103
  end
84
104
  end
@@ -16,6 +16,12 @@ module RGeo
16
16
  def envelope
17
17
  BoundingBox.new(factory).add(self).to_geometry
18
18
  end
19
+
20
+ private
21
+
22
+ def graph
23
+ @graph ||= GeometryGraph.new(self)
24
+ end
19
25
  end
20
26
 
21
27
  module PointMethods # :nodoc:
@@ -49,32 +55,62 @@ module RGeo
49
55
  end
50
56
  end
51
57
 
58
+ def simple?
59
+ # Use a SweeplineIntersector to determine if there are any self-intersections
60
+ # in the ring. The GeometryGraph of the ring could be used by comparing the
61
+ # edges to number of segments (graph.incident_edges.length == segments.length),
62
+ # but this adds computational and memory overhead if graph isn't already memoized.
63
+ # Since graph is not used elsewhere in LineStringMethods, we will just use the
64
+ # SweeplineIntersector for now.
65
+ li = SweeplineIntersector.new(segments)
66
+ li.proper_intersections.empty?
67
+ end
68
+
52
69
  def is_simple?
53
- len = segments.length
54
- return false if segments.any?(&:degenerate?)
55
- return true if len == 1
56
- return segments[0].s != segments[1].e if len == 2
57
- segments.each_with_index do |seg, index|
58
- nindex = index + 1
59
- nindex = nil if nindex == len
60
- return false if nindex && seg.contains_point?(segments[nindex].e)
61
- pindex = index - 1
62
- pindex = nil if pindex < 0
63
- return false if pindex && seg.contains_point?(segments[pindex].s)
64
- next unless nindex
65
- oindex = nindex + 1
66
- while oindex < len
67
- oseg = segments[oindex]
68
- return false if !(index == 0 && oindex == len - 1 && seg.s == oseg.e) && seg.intersects_segment?(oseg)
69
- oindex += 1
70
- end
71
- end
72
- true
70
+ warn "The is_simple? method is deprecated, please use the simple? counterpart, will be removed in v3" unless ENV["RGEO_SILENCE_DEPRECATION"]
71
+ simple?
73
72
  end
74
73
 
75
74
  def length
76
75
  segments.inject(0.0) { |sum, seg| sum + seg.length }
77
76
  end
77
+
78
+ def crosses?(rhs)
79
+ case rhs
80
+ when Feature::LineString
81
+ crosses_line_string?(rhs)
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ # Determines if a cross occurs with another linestring.
90
+ # Process is to get the number of proper intersections in each geom
91
+ # then overlay and get the number of proper intersections from that.
92
+ # If the overlaid number is higher than the sum of individual self-ints
93
+ # then there is an intersection. Finally, we need to check the intersection
94
+ # to see that it is not a boundary point of either segment.
95
+ #
96
+ # @param rhs [Feature::LineString]
97
+ #
98
+ # @return [Boolean]
99
+ def crosses_line_string?(rhs)
100
+ self_ints = SweeplineIntersector.new(segments).proper_intersections
101
+ self_ints += SweeplineIntersector.new(rhs.segments).proper_intersections
102
+ overlay_ints = SweeplineIntersector.new(segments + rhs.segments).proper_intersections
103
+
104
+ (overlay_ints - self_ints).each do |int|
105
+ s1s = int.s1.s
106
+ s1e = int.s1.e
107
+ s2s = int.s2.s
108
+ s2e = int.s2.e
109
+ return true unless [s1s, s1e, s2s, s2e].include?(int.point)
110
+ end
111
+
112
+ false
113
+ end
78
114
  end
79
115
 
80
116
  module MultiLineStringMethods # :nodoc:
@@ -78,12 +78,6 @@ module RGeo
78
78
  # Support a Z coordinate. Default is false.
79
79
  # [<tt>:has_m_coordinate</tt>]
80
80
  # Support an M coordinate. Default is false.
81
- # [<tt>:uses_lenient_assertions</tt>]
82
- # If set to true, assertion checking is disabled. This includes
83
- # simplicity checking on LinearRing, and validity checks on
84
- # Polygon and MultiPolygon. This may speed up creation of certain
85
- # objects, at the expense of not doing the proper checking for
86
- # OGC compliance. Default is false.
87
81
  # [<tt>:wkt_parser</tt>]
88
82
  # Configure the parser for WKT. The value is a hash of
89
83
  # configuration parameters for WKRep::WKTParser.new. Default is
@@ -0,0 +1,379 @@
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
+ unless intersection_map[int.s1]
170
+ intersection_map[int.s1] = []
171
+ end
172
+ intersection_map[int.s1] << int.point
173
+ end
174
+
175
+ s2_intersects = int.point != int.s2.s && int.point != int.s2.e
176
+ next unless s2_intersects
177
+ unless intersection_map[int.s2]
178
+ intersection_map[int.s2] = []
179
+ end
180
+ intersection_map[int.s2] << int.point
181
+ end
182
+ intersection_map
183
+ end
184
+
185
+ def create_half_edges
186
+ @edges.each do |edge|
187
+ create_half_edge(edge)
188
+ end
189
+ end
190
+
191
+ def create_half_edge(edge)
192
+ e1, e2 = HalfEdge.from_edge(edge)
193
+ insert_half_edge(e1)
194
+ insert_half_edge(e2)
195
+ end
196
+
197
+ def insert_half_edge(he)
198
+ unless incident_edges[he.origin.coordinates]
199
+ @incident_edges[he.origin.coordinates] = []
200
+ end
201
+ @incident_edges[he.origin.coordinates] << he
202
+ end
203
+
204
+ # Links all half-edges where possible.
205
+ # Defines +next+ and +prev+ for every half-edge by rotating
206
+ # through all half-edges originating at a vertex.
207
+ #
208
+ # Assuming half-edges are sorted CCW, every sequential pair of
209
+ # half-edges (e1, e2) can be linked by saying e1.prev = e2.twin
210
+ # and e2.twin.next = e1.
211
+ def link_half_edges
212
+ incident_edges.each_value do |hedges|
213
+ hedges.sort!
214
+ [*hedges, hedges.first].each_cons(2) do |e1, e2|
215
+ e1.prev = e2.twin
216
+ e2.twin.next = e1
217
+ end
218
+ end
219
+ end
220
+
221
+ # It is possible that intersections occur when new edges are added.
222
+ # This will split those edges into more half-edges while preserving
223
+ # the existing half-edges when possible since geometries may reference
224
+ # them.
225
+ def compute_split_edges(seg, ints)
226
+ points = ints.uniq.sort do |a, b|
227
+ a.distance(seg.s) <=> b.distance(seg.s)
228
+ end
229
+ points = [seg.s] + points + [seg.e]
230
+
231
+ he_start = incident_edges[points.first.coordinates].find { |he| he.destination == points.last }
232
+ he_end = incident_edges[points.last.coordinates].find { |he| he.destination == points.first }
233
+
234
+ points.each_cons(2) do |s, e|
235
+ edge = Segment.new(s, e)
236
+ if s == he_start.origin
237
+ he = HalfEdge.new(e)
238
+ he_start.twin = he
239
+ he.twin = he_start
240
+ insert_half_edge(he)
241
+ elsif e == he_end.origin
242
+ he = HalfEdge.new(s)
243
+ he_end.twin = he
244
+ he.twin = he_end
245
+ insert_half_edge(he)
246
+ else
247
+ create_half_edge(edge)
248
+ end
249
+ @edges << edge
250
+ end
251
+ @edges.delete(seg)
252
+ end
253
+ end
254
+
255
+ # GeometryGraph is a PlanarGraph that is built by adding
256
+ # geometries instead of edges. The GeometryGraph will
257
+ # hold a reference to an arbitrary HalfEdge on the
258
+ # interior of the geometry for every boundary in the geometry.
259
+ # For example, a polygon will have a reference to a HalfEdge for its
260
+ # exterior shell and one for every hole.
261
+ class GeometryGraph < PlanarGraph
262
+ # GeomEdge will be used to store the references to the HalfEdges
263
+ GeomEdge = Struct.new(:exterior_edge, :interior_edges)
264
+
265
+ def initialize(geom)
266
+ super()
267
+ @parent_geometry = geom
268
+ @geom_edges = []
269
+ add_geometry(geom)
270
+ end
271
+ attr_reader :parent_geometry, :geom_edges
272
+
273
+ private
274
+
275
+ # Adds a geometry to the graph and finds its
276
+ # reference HalfEdge(s).
277
+ #
278
+ # @param geom [RGeo::Feature::Instance]
279
+ def add_geometry(geom)
280
+ case geom
281
+ when Feature::Point
282
+ # Can't handle points yet, so just add an empty entry for them
283
+ @geom_edges << GeomEdge.new
284
+ when Feature::LineString, Feature::LinearRing
285
+ add_line_string(geom)
286
+ when Feature::Polygon
287
+ add_polygon(geom)
288
+ when Feature::GeometryCollection
289
+ add_collection(geom)
290
+ else
291
+ raise Error::RGeoError, "Invalid Geometry Type: #{geom.class}"
292
+ end
293
+ end
294
+
295
+ # Adds a LineString or LinearRing
296
+ # to the graph.
297
+ #
298
+ # @param geom [RGeo::Feature::LineString]
299
+ def add_line_string(geom)
300
+ add_edges(geom.segments)
301
+
302
+ hedge = unless geom.empty?
303
+ @incident_edges[geom.start_point.coordinates].first
304
+ end
305
+
306
+ @geom_edges << GeomEdge.new(hedge, nil)
307
+ end
308
+
309
+ # Adds a Polygon to the graph.
310
+ #
311
+ # Will add each shell seperately and find a CCW half-edge
312
+ # for the exterior ring and a CW half-edge for interior rings
313
+ # since these are designated as being on the interior of the polygon.
314
+ #
315
+ # It is possible that the half-edge will be nil if a suitable one cannot
316
+ # be found. This indicates the polygon is likely invalid.
317
+ #
318
+ # @param geom [RGeo::Feature::Polygon]
319
+ def add_polygon(geom)
320
+ exterior = geom.exterior_ring
321
+ add_edges(exterior.segments)
322
+
323
+ hedge = find_hedge(exterior)
324
+
325
+ interior_hedges = []
326
+ geom.interior_rings.each do |interior|
327
+ add_edges(interior.segments)
328
+ interior_hedges << find_hedge(interior, ccw: false)
329
+ end
330
+
331
+ @geom_edges << GeomEdge.new(hedge, interior_hedges)
332
+ end
333
+
334
+ # Adds a GeometryCollection to the graph.
335
+ #
336
+ # @param col [RGeo::Feature::GeometryCollection]
337
+ def add_collection(col)
338
+ col.each do |geom|
339
+ add_geometry(geom)
340
+ end
341
+ end
342
+
343
+ # Finds a Half-Edge that is part of a CCW or CW rotation
344
+ # from the input ring. Returns nil if none found.
345
+ #
346
+ # Will only consider half-edges that are colinear with
347
+ # the first or last segments of the ring.
348
+ #
349
+ # @param ring [RGeo::Feature::LinearRing]
350
+ # @param ccw [Boolean] true for CCW, false for CW
351
+ # @return [HalfEdge, nil]
352
+ def find_hedge(ring, ccw: true)
353
+ return nil if ring.num_points.zero?
354
+ ccw_target = ccw ? 1 : -1
355
+
356
+ coords = ring.start_point.coordinates
357
+ hedges = incident_edges[coords]
358
+
359
+ # find half-edges that are colinear to the start or end
360
+ # segment of the ring.
361
+ start_seg = Segment.new(ring.start_point, ring.point_n(1))
362
+ end_seg = Segment.new(ring.point_n(ring.num_points - 2), ring.end_point)
363
+ colinear_hedges = hedges.select do |he|
364
+ start_seg.side(he.destination).zero? || end_seg.side(he.destination).zero?
365
+ end
366
+
367
+ colinear_hedges.find do |hedge|
368
+ pts = []
369
+ hedge.and_connected do |he|
370
+ pts << he.origin
371
+ end
372
+ pts << hedge.origin
373
+ lr = parent_geometry.factory.line_string(pts)
374
+ Analysis.ring_direction(lr) == ccw_target
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end