nswtopo 2.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/bin/nswtopo +430 -0
  4. data/docs/README.md +78 -0
  5. data/docs/add.md +49 -0
  6. data/docs/config.md +24 -0
  7. data/docs/contours.md +37 -0
  8. data/docs/controls.md +9 -0
  9. data/docs/declination.md +15 -0
  10. data/docs/delete.md +15 -0
  11. data/docs/grid.md +5 -0
  12. data/docs/info.md +5 -0
  13. data/docs/init.md +38 -0
  14. data/docs/layers.md +11 -0
  15. data/docs/overlay.md +37 -0
  16. data/docs/relief.md +22 -0
  17. data/docs/render.md +43 -0
  18. data/docs/spot-heights.md +23 -0
  19. data/lib/nswtopo/archive.rb +93 -0
  20. data/lib/nswtopo/avl_tree.rb +128 -0
  21. data/lib/nswtopo/config.rb +73 -0
  22. data/lib/nswtopo/dither.rb +31 -0
  23. data/lib/nswtopo/font/chrome.rb +59 -0
  24. data/lib/nswtopo/font/generic.rb +25 -0
  25. data/lib/nswtopo/font.rb +43 -0
  26. data/lib/nswtopo/formats/kmz.rb +149 -0
  27. data/lib/nswtopo/formats/mbtiles.rb +64 -0
  28. data/lib/nswtopo/formats/pdf.rb +31 -0
  29. data/lib/nswtopo/formats/svg.rb +69 -0
  30. data/lib/nswtopo/formats/svgz.rb +13 -0
  31. data/lib/nswtopo/formats/zip.rb +40 -0
  32. data/lib/nswtopo/formats.rb +76 -0
  33. data/lib/nswtopo/geometry/overlap.rb +78 -0
  34. data/lib/nswtopo/geometry/r_tree.rb +47 -0
  35. data/lib/nswtopo/geometry/segment.rb +27 -0
  36. data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
  37. data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
  38. data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
  39. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
  40. data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
  41. data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
  42. data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
  43. data/lib/nswtopo/geometry/vector.rb +91 -0
  44. data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
  45. data/lib/nswtopo/geometry.rb +8 -0
  46. data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
  47. data/lib/nswtopo/gis/arcgis_server.rb +155 -0
  48. data/lib/nswtopo/gis/dem.rb +70 -0
  49. data/lib/nswtopo/gis/esri_hdr.rb +77 -0
  50. data/lib/nswtopo/gis/gdal_glob.rb +41 -0
  51. data/lib/nswtopo/gis/geojson/collection.rb +94 -0
  52. data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
  53. data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
  54. data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
  55. data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
  56. data/lib/nswtopo/gis/geojson/point.rb +9 -0
  57. data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
  58. data/lib/nswtopo/gis/geojson.rb +89 -0
  59. data/lib/nswtopo/gis/gps/gpx.rb +22 -0
  60. data/lib/nswtopo/gis/gps/kml.rb +66 -0
  61. data/lib/nswtopo/gis/gps.rb +20 -0
  62. data/lib/nswtopo/gis/projection.rb +56 -0
  63. data/lib/nswtopo/gis/shapefile.rb +24 -0
  64. data/lib/nswtopo/gis/world_file.rb +19 -0
  65. data/lib/nswtopo/gis.rb +9 -0
  66. data/lib/nswtopo/help_formatter.rb +59 -0
  67. data/lib/nswtopo/helpers/array.rb +30 -0
  68. data/lib/nswtopo/helpers/colour.rb +176 -0
  69. data/lib/nswtopo/helpers/concurrently.rb +27 -0
  70. data/lib/nswtopo/helpers/dir.rb +7 -0
  71. data/lib/nswtopo/helpers/hash.rb +15 -0
  72. data/lib/nswtopo/helpers/tar_writer.rb +11 -0
  73. data/lib/nswtopo/helpers.rb +6 -0
  74. data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
  75. data/lib/nswtopo/layer/contour.rb +233 -0
  76. data/lib/nswtopo/layer/control.rb +94 -0
  77. data/lib/nswtopo/layer/declination.rb +53 -0
  78. data/lib/nswtopo/layer/feature.rb +87 -0
  79. data/lib/nswtopo/layer/grid.rb +120 -0
  80. data/lib/nswtopo/layer/import.rb +25 -0
  81. data/lib/nswtopo/layer/labels/fence.rb +20 -0
  82. data/lib/nswtopo/layer/labels.rb +630 -0
  83. data/lib/nswtopo/layer/overlay.rb +53 -0
  84. data/lib/nswtopo/layer/raster.rb +63 -0
  85. data/lib/nswtopo/layer/relief.rb +143 -0
  86. data/lib/nswtopo/layer/spot.rb +171 -0
  87. data/lib/nswtopo/layer/vector.rb +263 -0
  88. data/lib/nswtopo/layer/vegetation.rb +73 -0
  89. data/lib/nswtopo/layer.rb +78 -0
  90. data/lib/nswtopo/log.rb +28 -0
  91. data/lib/nswtopo/map.rb +296 -0
  92. data/lib/nswtopo/os.rb +75 -0
  93. data/lib/nswtopo/safely.rb +13 -0
  94. data/lib/nswtopo/version.rb +4 -0
  95. data/lib/nswtopo/zip.rb +15 -0
  96. data/lib/nswtopo.rb +249 -0
  97. metadata +142 -0
@@ -0,0 +1,50 @@
1
+ module StraightSkeleton
2
+ module Node
3
+ attr_reader :point, :travel, :neighbours, :normals, :original
4
+
5
+ def active?
6
+ @nodes.include? self
7
+ end
8
+
9
+ def terminal?
10
+ @neighbours.one?
11
+ end
12
+
13
+ def reflex?
14
+ normals.inject(&:cross) * @nodes.direction <= 0
15
+ end
16
+
17
+ def splits?
18
+ terminal? || reflex?
19
+ end
20
+
21
+ def prev
22
+ @neighbours[0]
23
+ end
24
+
25
+ def next
26
+ @neighbours[1]
27
+ end
28
+
29
+ def index
30
+ @index ||= @nodes.index self
31
+ end
32
+
33
+ # ###########################################
34
+ # solve for vector p:
35
+ # n0.(p - @point) = travel - @travel
36
+ # n1.(p - @point) = travel - @travel
37
+ # ###########################################
38
+
39
+ def project(travel)
40
+ det = normals.inject(&:cross) if normals.all?
41
+ case
42
+ when det && det.nonzero?
43
+ x = normals.map { |normal| travel - @travel + normal.dot(point) }
44
+ [normals[1][1] * x[0] - normals[0][1] * x[1], normals[0][0] * x[1] - normals[1][0] * x[0]] / det
45
+ when normals[0] then normals[0].times(travel - @travel).plus(point)
46
+ when normals[1] then normals[1].times(travel - @travel).plus(point)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,295 @@
1
+ module StraightSkeleton
2
+ DEFAULT_ROUNDING_ANGLE = 15
3
+
4
+ class Nodes
5
+ def self.stitch(normal, *edge)
6
+ edge[1].neighbours[0], edge[0].neighbours[1] = edge
7
+ edge[1].normals[0] = edge[0].normals[1] = normal
8
+ end
9
+
10
+ def initialize(data)
11
+ @active, @indices = Set[], Hash.new.compare_by_identity
12
+ data.to_d.map do |points|
13
+ next points unless points.length > 2
14
+ points.inject [] do |points, point|
15
+ points.last == point ? points : points << point
16
+ end
17
+ end.map.with_index do |(*points, point), index|
18
+ points.first == point ? [points, :ring, (index unless points.hole?)] : [points << point, :segments, nil]
19
+ end.each do |points, pair, index|
20
+ normals = points.send(pair).map(&:difference).map(&:normalised).map(&:perp)
21
+ points.map do |point|
22
+ Vertex.new self, point
23
+ end.each do |node|
24
+ @active << node
25
+ end.send(pair).zip(normals).each do |edge, normal|
26
+ Nodes.stitch normal, *edge
27
+ @indices[normal] = index if index
28
+ end
29
+ end
30
+ end
31
+
32
+ # #################################
33
+ # solve for vector p and scalar t:
34
+ # n0.p - t = x0
35
+ # n1.p - t = x1
36
+ # n2.p - t = x2
37
+ # #################################
38
+
39
+ def self.solve(n0, n1, n2, x0, x1, x2)
40
+ det = n2.cross(n1) + n1.cross(n0) + n0.cross(n2)
41
+ return if det.zero?
42
+ travel = (x0 * n1.cross(n2) + x1 * n2.cross(n0) + x2 * n0.cross(n1)) / det
43
+ point = [n1.minus(n2).perp.times(x0), n2.minus(n0).perp.times(x1), n0.minus(n1).perp.times(x2)].inject(&:plus) / det
44
+ [point, travel]
45
+ end
46
+
47
+ # #################################
48
+ # solve for vector p and scalar t:
49
+ # n0.p - t = x0
50
+ # n1.p - t = x1
51
+ # n2 x p = x2
52
+ # #################################
53
+
54
+ def self.solve_asym(n0, n1, n2, x0, x1, x2)
55
+ det = n0.minus(n1).dot(n2)
56
+ return if det.zero?
57
+ travel = (x0 * n1.dot(n2) - x1 * n2.dot(n0) + x2 * n0.cross(n1)) / det
58
+ point = (n2.times(x0 - x1).plus n0.minus(n1).perp.times(x2)) / det
59
+ [point, travel]
60
+ end
61
+
62
+ def collapse(edge)
63
+ (n00, n01), (n10, n11) = edge.map(&:normals)
64
+ p0, p1 = edge.map(&:point)
65
+ t0, t1 = edge.map(&:travel)
66
+ return if p0.equal? p1
67
+ good = [n00 && !n00.cross(n01).zero?, n11 && !n11.cross(n10).zero?]
68
+ point, travel = case
69
+ when good.all? then Nodes::solve(n00, n01, n11, n00.dot(p0) - t0, n01.dot(p1) - t1, n11.dot(p1) - t1)
70
+ when good[0] then Nodes::solve_asym(n00, n01, n10, n00.dot(p0) - t0, n01.dot(p0) - t0, n10.cross(p1))
71
+ when good[1] then Nodes::solve_asym(n11, n10, n10, n11.dot(p1) - t1, n10.dot(p1) - t1, n01.cross(p0))
72
+ end || return
73
+ return if travel * direction < @travel * direction
74
+ return if @limit && travel.abs > @limit.abs
75
+ @candidates << Collapse.new(self, point, travel, edge)
76
+ end
77
+
78
+ def split(node)
79
+ bounds = node.project(@limit).zip(node.point).map do |centre, coord|
80
+ [coord, centre - @limit, centre + @limit].minmax
81
+ end if @limit
82
+ @index.search(bounds).map do |edge|
83
+ p0, p1, p2 = [*edge, node].map(&:point)
84
+ t0, t1, t2 = [*edge, node].map(&:travel)
85
+ (n00, n01), (n10, n11), (n20, n21) = [*edge, node].map(&:normals)
86
+ next if p0 == p2 || p1 == p2
87
+ next if node.terminal? and Split === node and node.source.normals[0].equal? n01
88
+ next if node.terminal? and Split === node and node.source.normals[1].equal? n01
89
+ next unless node.terminal? || [n20, n21].compact.inject(&:plus).dot(n01) < 0
90
+ point, travel = case
91
+ when n20 && n21 then Nodes::solve(n20, n21, n01, n20.dot(p2) - t2, n21.dot(p2) - t2, n01.dot(p0) - t0)
92
+ when n20 then Nodes::solve_asym(n01, n20, n20, n01.dot(p0) - t0, n20.dot(p2) - t2, n20.cross(p2))
93
+ when n21 then Nodes::solve_asym(n01, n21, n21, n01.dot(p0) - t0, n21.dot(p2) - t2, n21.cross(p2))
94
+ end || next
95
+ next if travel * @direction < node.travel
96
+ next if @limit && travel.abs > @limit.abs
97
+ next if point.minus(p0).dot(n01) * @direction < 0
98
+ Split.new self, point, travel, node, edge[0]
99
+ end.compact.each do |split|
100
+ @candidates << split
101
+ end
102
+ end
103
+
104
+ def include?(node)
105
+ @active.include? node
106
+ end
107
+
108
+ def insert(node)
109
+ @active << node
110
+ @track[node.normals[1]] << node if node.normals[1]
111
+ 2.times.inject [node] do |nodes|
112
+ [nodes.first.prev, *nodes, nodes.last.next].compact
113
+ end.segments.uniq.each do |edge|
114
+ collapse edge
115
+ end
116
+ split node if node.splits?
117
+ end
118
+
119
+ def track(normal)
120
+ @track[normal].select(&:active?).map do |node|
121
+ [node, node.next]
122
+ end
123
+ end
124
+
125
+ def nodeset
126
+ [].tap do |result|
127
+ pending, processed = @active.dup, Set[]
128
+ while pending.any?
129
+ nodes = pending.take 1
130
+ while node = nodes.last.next and !processed.include?(node)
131
+ nodes.push node
132
+ processed << node
133
+ end
134
+ while node = nodes.first.prev and !processed.include?(node)
135
+ nodes.unshift node
136
+ processed << node
137
+ end
138
+ pending.subtract nodes
139
+ nodes << nodes.first if nodes.first == nodes.last.next
140
+ result << nodes
141
+ end
142
+ end
143
+ end
144
+
145
+ attr_reader :direction
146
+
147
+ def progress(limit: nil, rounding_angle: DEFAULT_ROUNDING_ANGLE, cutoff_angle: nil, interval: nil, splits: true, &block)
148
+ return self if limit && limit.zero?
149
+
150
+ nodeset.tap do
151
+ @active.clear
152
+ @indices = nil
153
+ end.map do |*nodes, node|
154
+ nodes.first == node ? [nodes, :ring] : [nodes << node, :segments]
155
+ end.each.with_index do |(nodes, pair), index|
156
+ normals = nodes.send(pair).map do |edge|
157
+ edge[0].normals[1]
158
+ end
159
+ nodes.map do |node|
160
+ Vertex.new self, node.project(@limit)
161
+ end.each do |node|
162
+ @active << node
163
+ end.send(pair).zip(normals).each do |edge, normal|
164
+ Nodes.stitch normal, *edge
165
+ end
166
+ end if @limit
167
+
168
+ @candidates, @travel, @limit, @direction = AVLTree.new, 0, limit && limit.to_d, limit ? limit <=> 0 : 1
169
+
170
+ rounding_angle *= Math::PI / 180
171
+ cutoff_angle *= Math::PI / 180 if cutoff_angle
172
+
173
+ @track = Hash.new do |hash, normal|
174
+ hash[normal] = Set[]
175
+ end.compare_by_identity
176
+
177
+ @active.group_by(&:point).reject do |point, nodes|
178
+ case nodes.length
179
+ when 1 then true
180
+ when 2
181
+ nodes[0].next&.point == nodes[1].prev&.point &&
182
+ nodes[1].next&.point == nodes[0].prev&.point
183
+ else false
184
+ end
185
+ end.each do |point, nodes|
186
+ @active.subtract nodes
187
+ nodes.inject [] do |events, node|
188
+ events << [:incoming, node.prev] if node.prev
189
+ events << [:outgoing, node.next] if node.next
190
+ events
191
+ end.sort_by do |event, node|
192
+ case event
193
+ when :incoming then [-@direction * node.normals[1].angle, 1]
194
+ when :outgoing then [-@direction * node.normals[0].negate.angle, 0]
195
+ end
196
+ end.ring.map(&:transpose).each do |events, neighbours|
197
+ node = Vertex.new self, point
198
+ case events
199
+ when [:outgoing, :incoming] then next
200
+ when [:outgoing, :outgoing]
201
+ Nodes.stitch neighbours[1].normals[0], node, neighbours[1]
202
+ when [:incoming, :incoming]
203
+ Nodes.stitch neighbours[0].normals[1], neighbours[0], node
204
+ when [:incoming, :outgoing]
205
+ Nodes.stitch neighbours[0].normals[1], neighbours[0], node
206
+ Nodes.stitch neighbours[1].normals[0], node, neighbours[1]
207
+ end
208
+ @active << node
209
+ end
210
+ end
211
+
212
+ @active.reject(&:terminal?).select do |node|
213
+ direction * Math::atan2(node.normals.inject(&:cross), node.normals.inject(&:dot)) < -cutoff_angle
214
+ end.each do |node|
215
+ @active.delete node
216
+ 2.times.map do
217
+ Vertex.new self, node.point
218
+ end.each.with_index do |vertex, index|
219
+ vertex.normals[index] = node.normals[index]
220
+ vertex.neighbours[index] = node.neighbours[index]
221
+ vertex.neighbours[index].neighbours[1-index] = vertex
222
+ @active << vertex
223
+ end
224
+ end if cutoff_angle
225
+
226
+ @active.reject(&:terminal?).select(&:reflex?).each do |node|
227
+ angle = Math::atan2 node.normals.inject(&:cross).abs, node.normals.inject(&:dot)
228
+ extras = (angle / rounding_angle).floor
229
+ next unless extras > 0
230
+ extra_normals = extras.times.map do |n|
231
+ node.normals[0].rotate_by(angle * (n + 1) * -direction / (extras + 1))
232
+ end.each do |normal|
233
+ @indices[normal] = @indices[node.normals[0]] if @indices
234
+ end
235
+ extra_nodes = extras.times.map do
236
+ Vertex.new self, node.point
237
+ end.each do |extra_node|
238
+ @active << extra_node
239
+ end
240
+ edges = [node.neighbours[0], node, *extra_nodes, node.neighbours[1]].segments
241
+ normals = [node.normals[0], *extra_normals, node.normals[1]]
242
+ edges.zip(normals).each do |edge, normal|
243
+ Nodes.stitch normal, *edge
244
+ end
245
+ end
246
+
247
+ @active.select(&:next).map do |node|
248
+ [node, node.next]
249
+ end.each do |edge|
250
+ collapse edge
251
+ @track[edge[0].normals[1]] << edge[0]
252
+ end.map do |edge|
253
+ [edge.map(&:point).transpose.map(&:minmax), edge]
254
+ end.tap do |bounds_edges|
255
+ @index = RTree.load bounds_edges
256
+ end
257
+
258
+ @active.select(&:splits?).each do |node|
259
+ split node
260
+ end if splits
261
+
262
+ travel = 0
263
+ while candidate = @candidates.pop
264
+ next unless candidate.viable?
265
+ @travel = candidate.travel
266
+ while travel < @travel
267
+ yield :interval, travel, readout(travel)
268
+ travel += interval
269
+ end if interval && block_given?
270
+ candidate.replace! do |node, index = 0|
271
+ @active.delete node
272
+ yield :nodes, *[node, candidate].rotate(index).map(&:original) if block_given?
273
+ end
274
+ end
275
+
276
+ self
277
+ end
278
+
279
+ def readout(travel = @limit)
280
+ nodeset.map do |nodes|
281
+ nodes.map do |node|
282
+ node.project(travel).to_f
283
+ end
284
+ end.map do |points|
285
+ points.segments.reject do |segment|
286
+ segment.inject(&:==)
287
+ end.map(&:last).unshift(points.first)
288
+ end.reject(&:one?)
289
+ end
290
+
291
+ def index(node)
292
+ @indices.values_at(*node.normals).find(&:itself)
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,33 @@
1
+ module StraightSkeleton
2
+ class Split
3
+ include InteriorNode
4
+
5
+ def initialize(nodes, point, travel, source, node)
6
+ @original, @nodes, @point, @travel, @source, @normal = self, nodes, point, travel, source, node.normals[1]
7
+ end
8
+
9
+ attr_reader :source
10
+
11
+ def viable?
12
+ return false unless @source.active?
13
+ @edge = @nodes.track(@normal).find do |edge|
14
+ (n00, n01), (n10, n11) = edge.map(&:normals)
15
+ p0, p1 = edge.map(&:point)
16
+ next if point.minus(p0).cross(n00 ? n00.plus(n01) : n01) < 0
17
+ next if point.minus(p1).cross(n11 ? n11.plus(n10) : n10) > 0
18
+ true
19
+ end
20
+ end
21
+
22
+ def split!(index, &block)
23
+ @neighbours = [@source.neighbours[index], @edge[1-index]].rotate index
24
+ @neighbours.inject(&:equal?) ? block.call(prev, prev.is_a?(Collapse) ? 1 : 0) : insert! if @neighbours.any?
25
+ end
26
+
27
+ def replace!(&block)
28
+ dup.split!(0, &block)
29
+ dup.split!(1, &block)
30
+ block.call @source
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module StraightSkeleton
2
+ class Vertex
3
+ include Node
4
+
5
+ def initialize(nodes, point)
6
+ @original, @nodes, @point, @neighbours, @normals, @travel = self, nodes, point, [nil, nil], [nil, nil], 0
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'straight_skeleton/node'
2
+ require_relative 'straight_skeleton/interior_node'
3
+ require_relative 'straight_skeleton/collapse'
4
+ require_relative 'straight_skeleton/split'
5
+ require_relative 'straight_skeleton/vertex'
6
+ require_relative 'straight_skeleton/nodes'
@@ -0,0 +1,91 @@
1
+ module Vector
2
+ def rotate_by(angle)
3
+ cos = Math::cos(angle)
4
+ sin = Math::sin(angle)
5
+ [self[0] * cos - self[1] * sin, self[0] * sin + self[1] * cos]
6
+ end
7
+
8
+ def rotate_by!(angle)
9
+ replace rotate_by(angle)
10
+ end
11
+
12
+ def rotate_by_degrees(angle)
13
+ rotate_by(angle * Math::PI / 180.0)
14
+ end
15
+
16
+ def rotate_by_degrees!(angle)
17
+ replace rotate_by_degrees(angle)
18
+ end
19
+
20
+ def plus(other)
21
+ [self, other].transpose.map { |values| values.inject(:+) }
22
+ end
23
+
24
+ def minus(other)
25
+ [self, other].transpose.map { |values| values.inject(:-) }
26
+ end
27
+
28
+ def dot(other)
29
+ [self, other].transpose.map { |values| values.inject(:*) }.inject(:+)
30
+ end
31
+
32
+ def times(scalar)
33
+ map { |value| value * scalar }
34
+ end
35
+
36
+ def /(scalar)
37
+ map { |value| value / scalar }
38
+ end
39
+
40
+ def negate
41
+ map { |value| -value }
42
+ end
43
+
44
+ def to_d
45
+ map(&:to_d)
46
+ end
47
+
48
+ def to_f
49
+ map(&:to_f)
50
+ end
51
+
52
+ def angle
53
+ Math::atan2 at(1), at(0)
54
+ end
55
+
56
+ def norm
57
+ Math::sqrt(dot self)
58
+ end
59
+
60
+ def normalised
61
+ self / norm
62
+ end
63
+
64
+ def proj(other)
65
+ dot(other) / other.norm
66
+ end
67
+
68
+ def perp
69
+ [-self[1], self[0]]
70
+ end
71
+
72
+ def cross(other)
73
+ perp.dot other
74
+ end
75
+
76
+ def within?(polygon)
77
+ polygon.map do |point|
78
+ point.minus self
79
+ end.ring.inject(0) do |winding, (p0, p1)|
80
+ case
81
+ when p1[1] > 0 && p0[1] <= 0 && p0.minus(p1).cross(p0) >= 0 then winding + 1
82
+ when p0[1] > 0 && p1[1] <= 0 && p1.minus(p0).cross(p0) >= 0 then winding - 1
83
+ when p0[1] == 0 && p1[1] == 0 && p0[0] >= 0 && p1[0] < 0 then winding + 1
84
+ when p0[1] == 0 && p1[1] == 0 && p1[0] >= 0 && p0[0] < 0 then winding - 1
85
+ else winding
86
+ end
87
+ end != 0
88
+ end
89
+ end
90
+
91
+ Array.send :include, Vector
@@ -0,0 +1,179 @@
1
+ module VectorSequence
2
+ def perps
3
+ ring.map(&:difference).map(&:perp)
4
+ end
5
+
6
+ def signed_area
7
+ 0.5 * ring.map { |p1, p2| p1.cross p2 }.inject(&:+)
8
+ end
9
+
10
+ def clockwise?
11
+ signed_area < 0
12
+ end
13
+ alias hole? clockwise?
14
+
15
+ def anticlockwise?
16
+ signed_area >= 0
17
+ end
18
+
19
+ def centroid
20
+ ring.map do |p1, p2|
21
+ (p1.plus p2).times(p1.cross p2)
22
+ end.inject(&:plus) / (6.0 * signed_area)
23
+ end
24
+
25
+ def convex?
26
+ ring.map(&:difference).ring.all? do |directions|
27
+ directions.inject(&:cross) >= 0
28
+ end
29
+ end
30
+
31
+ def surrounds?(points)
32
+ points.all? do |point|
33
+ point.within? self
34
+ end
35
+ end
36
+
37
+ def convex_hull
38
+ start = min_by(&:reverse)
39
+ hull, remaining = uniq.partition { |point| point == start }
40
+ remaining.sort_by do |point|
41
+ [point.minus(start).angle, point.minus(start).norm]
42
+ end.inject(hull) do |memo, p3|
43
+ while memo.many? do
44
+ p1, p2 = memo.last(2)
45
+ (p3.minus p1).cross(p2.minus p1) < 0 ? break : memo.pop
46
+ end
47
+ memo << p3
48
+ end
49
+ end
50
+
51
+ def minimum_bounding_box(*margins)
52
+ polygon = convex_hull
53
+ return polygon[0], [0, 0], 0 if polygon.one?
54
+ indices = [%i[min_by max_by], [0, 1]].inject(:product).map do |min, axis|
55
+ polygon.map.with_index.send(min) { |point, index| point[axis] }.last
56
+ end
57
+ calipers = [[0, -1], [1, 0], [0, 1], [-1, 0]]
58
+ rotation = 0.0
59
+ candidates = []
60
+
61
+ while rotation < Math::PI / 2
62
+ edges = indices.map do |index|
63
+ polygon[(index + 1) % polygon.length].minus polygon[index]
64
+ end
65
+ angle, which = [edges, calipers].transpose.map do |edge, caliper|
66
+ Math::acos caliper.proj(edge).clamp(-1, 1)
67
+ end.map.with_index.min_by { |angle, index| angle }
68
+
69
+ calipers.each { |caliper| caliper.rotate_by!(angle) }
70
+ rotation += angle
71
+
72
+ break if rotation >= Math::PI / 2
73
+
74
+ dimensions = [0, 1].map do |offset|
75
+ polygon[indices[offset + 2]].minus(polygon[indices[offset]]).proj(calipers[offset + 1])
76
+ end
77
+
78
+ centre = polygon.values_at(*indices).map do |point|
79
+ point.rotate_by(-rotation)
80
+ end.partition.with_index do |point, index|
81
+ index.even?
82
+ end.map.with_index do |pair, index|
83
+ 0.5 * pair.map { |point| point[index] }.inject(:+)
84
+ end.rotate_by(rotation)
85
+
86
+ if rotation < Math::PI / 4
87
+ candidates << [centre, dimensions, rotation]
88
+ else
89
+ candidates << [centre, dimensions.reverse, rotation - Math::PI / 2]
90
+ end
91
+
92
+ indices[which] += 1
93
+ indices[which] %= polygon.length
94
+ end
95
+
96
+ candidates.min_by do |centre, dimensions, rotation|
97
+ dimensions.zip(margins).map do |dimension, margin|
98
+ margin ? dimension + 2 * margin : dimension
99
+ end.inject(:*)
100
+ end
101
+ end
102
+
103
+ def path_length
104
+ segments.map(&:difference).sum(&:norm)
105
+ end
106
+
107
+ def trim(margin)
108
+ start = [margin, 0].max
109
+ stop = path_length - start
110
+ return [] unless start < stop
111
+ points, total = [], 0
112
+ segments.each do |segment|
113
+ distance = segment.distance
114
+ case
115
+ when total + distance <= start
116
+ when total <= start
117
+ points << segment.along((start - total) / distance)
118
+ points << segment.along((stop - total) / distance) if total + distance >= stop
119
+ else
120
+ points << segment[0]
121
+ points << segment.along((stop - total) / distance) if total + distance >= stop
122
+ end
123
+ total += distance
124
+ break if total >= stop
125
+ end
126
+ points
127
+ end
128
+
129
+ def crop(length)
130
+ trim(0.5 * (path_length - length))
131
+ end
132
+
133
+ def sample_at(interval, along: false, angle: false)
134
+ Enumerator.new do |yielder|
135
+ segments.inject [0.5, 0] do |(alpha, sum), segment|
136
+ loop do
137
+ fraction = alpha * interval / segment.distance
138
+ break unless fraction < 1
139
+ segment[0] = segment.along(fraction)
140
+ sum += alpha * interval
141
+ yielder << case
142
+ when along then [segment[0], sum]
143
+ when angle then [segment[0], segment.difference.angle]
144
+ else segment[0]
145
+ end
146
+ alpha = 1.0
147
+ end
148
+ [alpha - segment.distance / interval, sum + segment.distance]
149
+ end
150
+ end.entries
151
+ end
152
+
153
+ def in_sections(count)
154
+ segments.each_slice(count).map do |segments|
155
+ segments.inject do |section, segment|
156
+ section << segment[1]
157
+ end
158
+ end
159
+ end
160
+
161
+ def douglas_peucker(tolerance)
162
+ chunks, simplified = [self], []
163
+ while chunk = chunks.pop
164
+ direction = chunk.last.minus(chunk.first).normalised
165
+ deltas = chunk.map do |point|
166
+ point.minus(chunk.first).cross(direction).abs
167
+ end
168
+ delta, index = deltas.each.with_index.max_by(&:first)
169
+ if delta < tolerance
170
+ simplified.prepend chunk.first
171
+ else
172
+ chunks << chunk[0..index] << chunk[index..-1]
173
+ end
174
+ end
175
+ simplified << last
176
+ end
177
+ end
178
+
179
+ Array.send :include, VectorSequence
@@ -0,0 +1,8 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require_relative 'geometry/vector'
4
+ require_relative 'geometry/segment'
5
+ require_relative 'geometry/vector_sequence'
6
+ require_relative 'geometry/overlap'
7
+ require_relative 'geometry/r_tree'
8
+ require_relative 'geometry/straight_skeleton'