nswtopo 2.0.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/bin/nswtopo +430 -0
- data/docs/README.md +78 -0
- data/docs/add.md +49 -0
- data/docs/config.md +24 -0
- data/docs/contours.md +37 -0
- data/docs/controls.md +9 -0
- data/docs/declination.md +15 -0
- data/docs/delete.md +15 -0
- data/docs/grid.md +5 -0
- data/docs/info.md +5 -0
- data/docs/init.md +38 -0
- data/docs/layers.md +11 -0
- data/docs/overlay.md +37 -0
- data/docs/relief.md +22 -0
- data/docs/render.md +43 -0
- data/docs/spot-heights.md +23 -0
- data/lib/nswtopo/archive.rb +93 -0
- data/lib/nswtopo/avl_tree.rb +128 -0
- data/lib/nswtopo/config.rb +73 -0
- data/lib/nswtopo/dither.rb +31 -0
- data/lib/nswtopo/font/chrome.rb +59 -0
- data/lib/nswtopo/font/generic.rb +25 -0
- data/lib/nswtopo/font.rb +43 -0
- data/lib/nswtopo/formats/kmz.rb +149 -0
- data/lib/nswtopo/formats/mbtiles.rb +64 -0
- data/lib/nswtopo/formats/pdf.rb +31 -0
- data/lib/nswtopo/formats/svg.rb +69 -0
- data/lib/nswtopo/formats/svgz.rb +13 -0
- data/lib/nswtopo/formats/zip.rb +40 -0
- data/lib/nswtopo/formats.rb +76 -0
- data/lib/nswtopo/geometry/overlap.rb +78 -0
- data/lib/nswtopo/geometry/r_tree.rb +47 -0
- data/lib/nswtopo/geometry/segment.rb +27 -0
- data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
- data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
- data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
- data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
- data/lib/nswtopo/geometry/vector.rb +91 -0
- data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
- data/lib/nswtopo/geometry.rb +8 -0
- data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
- data/lib/nswtopo/gis/arcgis_server.rb +155 -0
- data/lib/nswtopo/gis/dem.rb +70 -0
- data/lib/nswtopo/gis/esri_hdr.rb +77 -0
- data/lib/nswtopo/gis/gdal_glob.rb +41 -0
- data/lib/nswtopo/gis/geojson/collection.rb +94 -0
- data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
- data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
- data/lib/nswtopo/gis/geojson/point.rb +9 -0
- data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
- data/lib/nswtopo/gis/geojson.rb +89 -0
- data/lib/nswtopo/gis/gps/gpx.rb +22 -0
- data/lib/nswtopo/gis/gps/kml.rb +66 -0
- data/lib/nswtopo/gis/gps.rb +20 -0
- data/lib/nswtopo/gis/projection.rb +56 -0
- data/lib/nswtopo/gis/shapefile.rb +24 -0
- data/lib/nswtopo/gis/world_file.rb +19 -0
- data/lib/nswtopo/gis.rb +9 -0
- data/lib/nswtopo/help_formatter.rb +59 -0
- data/lib/nswtopo/helpers/array.rb +30 -0
- data/lib/nswtopo/helpers/colour.rb +176 -0
- data/lib/nswtopo/helpers/concurrently.rb +27 -0
- data/lib/nswtopo/helpers/dir.rb +7 -0
- data/lib/nswtopo/helpers/hash.rb +15 -0
- data/lib/nswtopo/helpers/tar_writer.rb +11 -0
- data/lib/nswtopo/helpers.rb +6 -0
- data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
- data/lib/nswtopo/layer/contour.rb +233 -0
- data/lib/nswtopo/layer/control.rb +94 -0
- data/lib/nswtopo/layer/declination.rb +53 -0
- data/lib/nswtopo/layer/feature.rb +87 -0
- data/lib/nswtopo/layer/grid.rb +120 -0
- data/lib/nswtopo/layer/import.rb +25 -0
- data/lib/nswtopo/layer/labels/fence.rb +20 -0
- data/lib/nswtopo/layer/labels.rb +630 -0
- data/lib/nswtopo/layer/overlay.rb +53 -0
- data/lib/nswtopo/layer/raster.rb +63 -0
- data/lib/nswtopo/layer/relief.rb +143 -0
- data/lib/nswtopo/layer/spot.rb +171 -0
- data/lib/nswtopo/layer/vector.rb +263 -0
- data/lib/nswtopo/layer/vegetation.rb +73 -0
- data/lib/nswtopo/layer.rb +78 -0
- data/lib/nswtopo/log.rb +28 -0
- data/lib/nswtopo/map.rb +296 -0
- data/lib/nswtopo/os.rb +75 -0
- data/lib/nswtopo/safely.rb +13 -0
- data/lib/nswtopo/version.rb +4 -0
- data/lib/nswtopo/zip.rb +15 -0
- data/lib/nswtopo.rb +249 -0
- 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,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'
|