nswtopo 3.0.1 → 3.1.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/bin/nswtopo +20 -4
- data/docs/contours.md +2 -0
- data/docs/relief.md +2 -3
- data/docs/spot-heights.md +2 -0
- data/lib/nswtopo/archive.rb +6 -3
- data/lib/nswtopo/chrome.rb +9 -6
- data/lib/nswtopo/commands/layers.rb +2 -2
- data/lib/nswtopo/config.rb +1 -0
- data/lib/nswtopo/formats/gemf.rb +1 -0
- data/lib/nswtopo/formats/kmz.rb +16 -10
- data/lib/nswtopo/formats/mbtiles.rb +1 -0
- data/lib/nswtopo/formats/pdf.rb +4 -3
- data/lib/nswtopo/formats/svg.rb +5 -13
- data/lib/nswtopo/formats/svgz.rb +1 -0
- data/lib/nswtopo/formats/zip.rb +5 -4
- data/lib/nswtopo/formats.rb +35 -36
- data/lib/nswtopo/geometry/r_tree.rb +24 -23
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
- data/lib/nswtopo/geometry/vector.rb +55 -49
- data/lib/nswtopo/geometry.rb +0 -5
- data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
- data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
- data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
- data/lib/nswtopo/gis/dem.rb +3 -2
- data/lib/nswtopo/gis/gdal_glob.rb +3 -3
- data/lib/nswtopo/gis/geojson/collection.rb +60 -14
- data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
- data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
- data/lib/nswtopo/gis/geojson/point.rb +16 -1
- data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
- data/lib/nswtopo/gis/geojson.rb +92 -46
- data/lib/nswtopo/gis/projection.rb +5 -1
- data/lib/nswtopo/helpers/thread_pool.rb +39 -0
- data/lib/nswtopo/helpers.rb +44 -5
- data/lib/nswtopo/layer/arcgis_raster.rb +4 -6
- data/lib/nswtopo/layer/contour.rb +24 -26
- data/lib/nswtopo/layer/control.rb +5 -3
- data/lib/nswtopo/layer/declination.rb +14 -10
- data/lib/nswtopo/layer/feature.rb +5 -5
- data/lib/nswtopo/layer/grid.rb +19 -18
- data/lib/nswtopo/layer/labels/barriers.rb +23 -0
- data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
- data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
- data/lib/nswtopo/layer/labels/label.rb +63 -0
- data/lib/nswtopo/layer/labels.rb +192 -315
- data/lib/nswtopo/layer/overlay.rb +11 -12
- data/lib/nswtopo/layer/raster.rb +1 -0
- data/lib/nswtopo/layer/relief.rb +6 -4
- data/lib/nswtopo/layer/spot.rb +11 -17
- data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
- data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
- data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
- data/lib/nswtopo/layer.rb +2 -1
- data/lib/nswtopo/map.rb +70 -56
- data/lib/nswtopo/svg.rb +5 -0
- data/lib/nswtopo/tiled_web_map.rb +3 -3
- data/lib/nswtopo/tree_indenter.rb +2 -2
- data/lib/nswtopo/version.rb +1 -1
- data/lib/nswtopo.rb +4 -0
- metadata +15 -17
- data/lib/nswtopo/geometry/overlap.rb +0 -47
- data/lib/nswtopo/geometry/segment.rb +0 -27
- data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
- data/lib/nswtopo/helpers/array.rb +0 -19
- data/lib/nswtopo/helpers/concurrently.rb +0 -27
- data/lib/nswtopo/helpers/dir.rb +0 -7
- data/lib/nswtopo/helpers/hash.rb +0 -15
- data/lib/nswtopo/helpers/tar_writer.rb +0 -11
- data/lib/nswtopo/layer/labels/barrier.rb +0 -39
@@ -9,20 +9,30 @@ module StraightSkeleton
|
|
9
9
|
|
10
10
|
def initialize(data)
|
11
11
|
@active, @indices = Set[], Hash.new.compare_by_identity
|
12
|
-
data.
|
12
|
+
data.map do |points|
|
13
|
+
points.map(&:to_d)
|
14
|
+
end.map do |points|
|
13
15
|
next points unless points.length > 2
|
14
16
|
points.each.with_object [] do |point, points|
|
15
17
|
points << point unless points.last == point
|
16
18
|
end
|
17
|
-
end.
|
18
|
-
points.first ==
|
19
|
-
|
20
|
-
|
19
|
+
end.each.with_index do |points, index|
|
20
|
+
closed = points.first == points.last
|
21
|
+
points.each_cons(2).sum do |p0, p1|
|
22
|
+
p0.cross(p1)
|
23
|
+
end.then do |signed_area_x2|
|
24
|
+
index = nil if signed_area_x2 < 0
|
25
|
+
end if closed
|
26
|
+
normals = points.each_cons(2).map do |p0, p1|
|
27
|
+
(p1 - p0).normalised.perp
|
28
|
+
end
|
21
29
|
points.map do |point|
|
22
30
|
Vertex.new self, point
|
23
|
-
end.
|
24
|
-
|
25
|
-
|
31
|
+
end.tap do |nodes|
|
32
|
+
nodes.pop if closed
|
33
|
+
nodes.inject(@active, &:<<)
|
34
|
+
nodes << nodes.first if closed
|
35
|
+
end.each_cons(2).zip(normals) do |edge, normal|
|
26
36
|
Nodes.stitch normal, *edge
|
27
37
|
@indices[normal] = index if index
|
28
38
|
end
|
@@ -40,8 +50,8 @@ module StraightSkeleton
|
|
40
50
|
det = n2.cross(n1) + n1.cross(n0) + n0.cross(n2)
|
41
51
|
return if det.zero?
|
42
52
|
travel = (x0 * n1.cross(n2) + x1 * n2.cross(n0) + x2 * n0.cross(n1)) / det
|
43
|
-
point =
|
44
|
-
|
53
|
+
point = ((n1 - n2).perp * x0 + (n2 - n0).perp * x1 + (n0 - n1).perp * x2) / det
|
54
|
+
return point, travel
|
45
55
|
end
|
46
56
|
|
47
57
|
# #################################
|
@@ -52,11 +62,11 @@ module StraightSkeleton
|
|
52
62
|
# #################################
|
53
63
|
|
54
64
|
def self.solve_asym(n0, n1, n2, x0, x1, x2)
|
55
|
-
det = n0
|
65
|
+
det = (n0 - n1).dot(n2)
|
56
66
|
return if det.zero?
|
57
67
|
travel = (x0 * n1.dot(n2) - x1 * n2.dot(n0) + x2 * n0.cross(n1)) / det
|
58
|
-
point = (n2
|
59
|
-
|
68
|
+
point = (n2 * (x0 - x1) + (n0 - n1).perp * x2) / det
|
69
|
+
return point, travel
|
60
70
|
end
|
61
71
|
|
62
72
|
def collapse(edge)
|
@@ -79,14 +89,14 @@ module StraightSkeleton
|
|
79
89
|
bounds = node.project(@limit).zip(node.point).map do |centre, coord|
|
80
90
|
[coord, centre - @limit, centre + @limit].minmax
|
81
91
|
end if @limit
|
82
|
-
@index.search(bounds).
|
92
|
+
@index.search(bounds).filter_map do |edge|
|
83
93
|
p0, p1, p2 = [*edge, node].map(&:point)
|
84
94
|
t0, t1, t2 = [*edge, node].map(&:travel)
|
85
95
|
(n00, n01), (n10, n11), (n20, n21) = [*edge, node].map(&:normals)
|
86
96
|
next if p0 == p2 || p1 == p2
|
87
97
|
next if node.terminal? and Split === node and node.source.normals[0].equal? n01
|
88
98
|
next if node.terminal? and Split === node and node.source.normals[1].equal? n01
|
89
|
-
next unless node.terminal? || [n20, n21].compact.inject(
|
99
|
+
next unless node.terminal? || [n20, n21].compact.inject(&:+).dot(n01) < 0
|
90
100
|
point, travel = case
|
91
101
|
when n20 && n21 then Nodes::solve(n20, n21, n01, n20.dot(p2) - t2, n21.dot(p2) - t2, n01.dot(p0) - t0)
|
92
102
|
when n20 then Nodes::solve_asym(n01, n20, n20, n01.dot(p0) - t0, n20.dot(p2) - t2, n20.cross(p2))
|
@@ -94,9 +104,9 @@ module StraightSkeleton
|
|
94
104
|
end || next
|
95
105
|
next if travel * @direction < node.travel
|
96
106
|
next if @limit && travel.abs > @limit.abs
|
97
|
-
next if point
|
107
|
+
next if (point - p0).dot(n01) * @direction < 0
|
98
108
|
Split.new self, point, travel, node, edge[0]
|
99
|
-
end.
|
109
|
+
end.each do |split|
|
100
110
|
@candidates << split
|
101
111
|
end
|
102
112
|
end
|
@@ -110,7 +120,7 @@ module StraightSkeleton
|
|
110
120
|
@track[node.normals[1]] << node if node.normals[1]
|
111
121
|
2.times.inject [node] do |nodes|
|
112
122
|
[nodes.first.prev, *nodes, nodes.last.next].compact
|
113
|
-
end.
|
123
|
+
end.each_cons(2).uniq.each do |edge|
|
114
124
|
collapse edge
|
115
125
|
end
|
116
126
|
split node if node.splits?
|
@@ -123,7 +133,7 @@ module StraightSkeleton
|
|
123
133
|
end
|
124
134
|
|
125
135
|
def nodeset
|
126
|
-
|
136
|
+
Enumerator.new do |yielder|
|
127
137
|
pending, processed = @active.dup, Set[]
|
128
138
|
while pending.any?
|
129
139
|
nodes = pending.take 1
|
@@ -137,35 +147,36 @@ module StraightSkeleton
|
|
137
147
|
end
|
138
148
|
pending.subtract nodes
|
139
149
|
nodes << nodes.first if nodes.first == nodes.last.next
|
140
|
-
|
150
|
+
yielder << nodes unless nodes.one?
|
141
151
|
end
|
142
|
-
end
|
152
|
+
end.entries
|
143
153
|
end
|
144
154
|
|
145
155
|
attr_reader :direction
|
146
156
|
|
147
157
|
def progress(limit: nil, rounding_angle: DEFAULT_ROUNDING_ANGLE, cutoff_angle: nil, interval: nil, splits: true, &block)
|
148
|
-
return self if limit
|
158
|
+
return self if limit&.zero?
|
149
159
|
|
150
160
|
nodeset.tap do
|
151
161
|
@active.clear
|
152
162
|
@indices = nil
|
153
|
-
end.
|
154
|
-
nodes.first ==
|
155
|
-
|
156
|
-
|
157
|
-
edge[0].normals[1]
|
163
|
+
end.each do |nodes|
|
164
|
+
closed = nodes.first == nodes.last
|
165
|
+
normals = nodes.map do |node|
|
166
|
+
node.normals[1]
|
158
167
|
end
|
159
168
|
nodes.map do |node|
|
160
169
|
Vertex.new self, node.project(@limit)
|
161
|
-
end.
|
162
|
-
|
163
|
-
|
170
|
+
end.tap do |nodes|
|
171
|
+
nodes.pop if closed
|
172
|
+
nodes.inject(@active, &:<<)
|
173
|
+
nodes << nodes.first if closed
|
174
|
+
end.each_cons(2).zip(normals).each do |edge, normal|
|
164
175
|
Nodes.stitch normal, *edge
|
165
176
|
end
|
166
177
|
end if @limit
|
167
178
|
|
168
|
-
@candidates, @travel, @limit, @direction = AVLTree.new, 0, limit
|
179
|
+
@candidates, @travel, @limit, @direction = AVLTree.new, 0, limit&.to_d, limit ? limit <=> 0 : 1
|
169
180
|
|
170
181
|
rounding_angle *= Math::PI / 180
|
171
182
|
cutoff_angle *= Math::PI / 180 if cutoff_angle
|
@@ -189,10 +200,12 @@ module StraightSkeleton
|
|
189
200
|
events << [:outgoing, node.next] if node.next
|
190
201
|
end.sort_by do |event, node|
|
191
202
|
case event
|
192
|
-
when :incoming then [-@direction *
|
193
|
-
when :outgoing then [-@direction * node.normals[0].
|
203
|
+
when :incoming then [-@direction * node.normals[1].angle, 1]
|
204
|
+
when :outgoing then [-@direction * (-node.normals[0]).angle, 0]
|
194
205
|
end
|
195
|
-
end.
|
206
|
+
end.then do |events|
|
207
|
+
events.zip events.rotate
|
208
|
+
end.map(&:transpose).each do |events, neighbours|
|
196
209
|
node = Vertex.new self, point
|
197
210
|
case events
|
198
211
|
when [:outgoing, :incoming] then next
|
@@ -236,9 +249,9 @@ module StraightSkeleton
|
|
236
249
|
end.each do |extra_node|
|
237
250
|
@active << extra_node
|
238
251
|
end
|
239
|
-
edges = [node.neighbours[0], node, *extra_nodes, node.neighbours[1]].
|
252
|
+
edges = [node.neighbours[0], node, *extra_nodes, node.neighbours[1]].each_cons(2)
|
240
253
|
normals = [node.normals[0], *extra_normals, node.normals[1]]
|
241
|
-
edges.zip(normals)
|
254
|
+
edges.zip(normals) do |edge, normal|
|
242
255
|
Nodes.stitch normal, *edge
|
243
256
|
end
|
244
257
|
end
|
@@ -251,7 +264,7 @@ module StraightSkeleton
|
|
251
264
|
end.map do |edge|
|
252
265
|
[edge.map(&:point).transpose.map(&:minmax), edge]
|
253
266
|
end.tap do |bounds_edges|
|
254
|
-
@index = RTree.load bounds_edges
|
267
|
+
@index = RTree.load! bounds_edges
|
255
268
|
end
|
256
269
|
|
257
270
|
@active.select(&:splits?).each do |node|
|
@@ -281,9 +294,7 @@ module StraightSkeleton
|
|
281
294
|
node.project(travel).to_f
|
282
295
|
end
|
283
296
|
end.map do |points|
|
284
|
-
points.
|
285
|
-
segment.inject(&:==)
|
286
|
-
end.map(&:last).unshift(points.first)
|
297
|
+
points.chunk(&:itself).map(&:first)
|
287
298
|
end.reject(&:one?)
|
288
299
|
end
|
289
300
|
|
@@ -13,8 +13,8 @@ module StraightSkeleton
|
|
13
13
|
@edge = @nodes.track(@normal).find do |edge|
|
14
14
|
(n00, n01), (n10, n11) = edge.map(&:normals)
|
15
15
|
p0, p1 = edge.map(&:point)
|
16
|
-
next if point
|
17
|
-
next if point
|
16
|
+
next if (point - p0).cross(n00 ? n00 + n01 : n01) < 0
|
17
|
+
next if (point - p1).cross(n11 ? n11 + n10 : n10) > 0
|
18
18
|
true
|
19
19
|
end
|
20
20
|
end
|
@@ -1,56 +1,86 @@
|
|
1
|
-
|
1
|
+
class Vector
|
2
|
+
def initialize(x, y)
|
3
|
+
@x, @y = x, y
|
4
|
+
freeze
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.[](x, y)
|
8
|
+
new x, y
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :x, :y
|
12
|
+
|
13
|
+
def to_ary
|
14
|
+
[@x, @y]
|
15
|
+
end
|
16
|
+
|
17
|
+
extend Forwardable
|
18
|
+
delegate %i[to_json hash each join <=>] => :to_ary
|
19
|
+
|
20
|
+
include Enumerable
|
21
|
+
include Comparable
|
22
|
+
alias eql? ==
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"{%s, %s}" % [@x, @y]
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_d
|
29
|
+
Vector[@x.to_d, @y.to_d]
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_f
|
33
|
+
Vector[@x.to_f, @y.to_f]
|
34
|
+
end
|
35
|
+
|
2
36
|
def rotate_by(angle)
|
3
37
|
cos = Math::cos(angle)
|
4
38
|
sin = Math::sin(angle)
|
5
|
-
[
|
6
|
-
end
|
7
|
-
|
8
|
-
def rotate_by!(angle)
|
9
|
-
replace rotate_by(angle)
|
39
|
+
Vector[@x * cos - @y * sin, @x * sin + @y * cos]
|
10
40
|
end
|
11
41
|
|
12
42
|
def rotate_by_degrees(angle)
|
13
43
|
rotate_by(angle * Math::PI / 180.0)
|
14
44
|
end
|
15
45
|
|
16
|
-
def
|
17
|
-
|
46
|
+
def +(other)
|
47
|
+
Vector[@x + other.x, @y + other.y]
|
18
48
|
end
|
19
49
|
|
20
|
-
def
|
21
|
-
[
|
50
|
+
def -(other)
|
51
|
+
Vector[@x - other.x, @y - other.y]
|
22
52
|
end
|
23
53
|
|
24
|
-
def
|
25
|
-
[
|
54
|
+
def *(scalar)
|
55
|
+
Vector[@x * scalar, @y * scalar]
|
26
56
|
end
|
27
57
|
|
28
|
-
def
|
29
|
-
[
|
58
|
+
def /(scalar)
|
59
|
+
Vector[@x / scalar, @y / scalar]
|
30
60
|
end
|
31
61
|
|
32
|
-
def
|
33
|
-
|
62
|
+
def +@
|
63
|
+
self
|
34
64
|
end
|
35
65
|
|
36
|
-
def
|
37
|
-
|
66
|
+
def -@
|
67
|
+
self * -1
|
38
68
|
end
|
39
69
|
|
40
|
-
def
|
41
|
-
|
70
|
+
def dot(other)
|
71
|
+
@x * other.x + @y * other.y
|
42
72
|
end
|
43
73
|
|
44
|
-
def
|
45
|
-
|
74
|
+
def perp
|
75
|
+
Vector[-@y, @x]
|
46
76
|
end
|
47
77
|
|
48
|
-
def
|
49
|
-
|
78
|
+
def cross(other)
|
79
|
+
perp.dot other
|
50
80
|
end
|
51
81
|
|
52
82
|
def angle
|
53
|
-
Math::atan2
|
83
|
+
Math::atan2 @y, @x
|
54
84
|
end
|
55
85
|
|
56
86
|
def norm
|
@@ -64,28 +94,4 @@ module Vector
|
|
64
94
|
def proj(other)
|
65
95
|
dot(other) / other.norm
|
66
96
|
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
97
|
end
|
90
|
-
|
91
|
-
Array.send :include, Vector
|
data/lib/nswtopo/geometry.rb
CHANGED
@@ -1,8 +1,3 @@
|
|
1
|
-
require 'bigdecimal'
|
2
|
-
require 'bigdecimal/util'
|
3
1
|
require_relative 'geometry/vector'
|
4
|
-
require_relative 'geometry/segment'
|
5
|
-
require_relative 'geometry/vector_sequence'
|
6
|
-
require_relative 'geometry/overlap'
|
7
2
|
require_relative 'geometry/r_tree'
|
8
3
|
require_relative 'geometry/straight_skeleton'
|
@@ -116,10 +116,13 @@ module NSWTopo
|
|
116
116
|
curves = [[coords.last.last, *points]]
|
117
117
|
while curve = curves.shift
|
118
118
|
next if curve.first == curve.last
|
119
|
-
|
119
|
+
curve_length = curve.each_cons(2).sum do |p0, p1|
|
120
|
+
(p1 - p0).norm
|
121
|
+
end
|
122
|
+
if (curve.first - curve.last).norm < 0.99 * curve_length
|
120
123
|
reduced = 3.times.inject [ curve ] do |reduced|
|
121
|
-
reduced << reduced.last.each_cons(2).map do |
|
122
|
-
|
124
|
+
reduced << reduced.last.each_cons(2).map do |p0, p1|
|
125
|
+
(p0 + p1) / 2
|
123
126
|
end
|
124
127
|
end
|
125
128
|
curves.unshift reduced.map(&:last).reverse
|
@@ -141,15 +144,13 @@ module NSWTopo
|
|
141
144
|
when "esriGeometryPoint"
|
142
145
|
raise "unexpected SVG response (bad point symbol)" unless coords.map(&:length) == [ 4 ]
|
143
146
|
point = coords[0].transpose.map { |coords| coords.sum / coords.length }
|
144
|
-
next GeoJSON::Point
|
147
|
+
next GeoJSON::Point[point, properties]
|
145
148
|
when "esriGeometryPolyline"
|
146
|
-
next GeoJSON::LineString
|
147
|
-
next GeoJSON::MultiLineString
|
149
|
+
next GeoJSON::LineString[coords[0], properties] if @mixed && coords.one?
|
150
|
+
next GeoJSON::MultiLineString[coords, properties]
|
148
151
|
when "esriGeometryPolygon"
|
149
|
-
coords.
|
150
|
-
|
151
|
-
next GeoJSON::Polygon.new polys.first, properties if @mixed && polys.one?
|
152
|
-
next GeoJSON::MultiPolygon.new polys, properties
|
152
|
+
polys = GeoJSON::MultiLineString[coords.map(&:reverse), properties].to_multipolygon
|
153
|
+
next @mixed && polys.one? ? polys.first : polys
|
153
154
|
end
|
154
155
|
end.tap do |features|
|
155
156
|
yielder << GeoJSON::Collection.new(projection: projection, name: @name, features: features)
|
@@ -45,7 +45,7 @@ module NSWTopo
|
|
45
45
|
get_json "#{@id}/query", outFields: out_fields, objectIds: objectids.take(per_page).join(?,)
|
46
46
|
rescue Connection::Error
|
47
47
|
(per_page /= 2) > 0 ? retry : raise
|
48
|
-
end.fetch("features", []).
|
48
|
+
end.fetch("features", []).filter_map do |feature|
|
49
49
|
next unless geometry = feature["geometry"]
|
50
50
|
properties = feature.fetch("attributes", {})
|
51
51
|
|
@@ -53,29 +53,27 @@ module NSWTopo
|
|
53
53
|
when "esriGeometryPoint"
|
54
54
|
point = geometry.values_at "x", "y"
|
55
55
|
next unless point.all?
|
56
|
-
next GeoJSON::Point
|
56
|
+
next GeoJSON::Point[point, properties]
|
57
57
|
when "esriGeometryMultipoint"
|
58
58
|
points = geometry["points"]
|
59
59
|
next unless points&.any?
|
60
|
-
next GeoJSON::MultiPoint
|
60
|
+
next GeoJSON::MultiPoint[points.transpose.take(2).transpose, properties]
|
61
61
|
when "esriGeometryPolyline"
|
62
62
|
raise "ArcGIS curve geometries not supported" if geometry.key? "curvePaths"
|
63
63
|
paths = geometry["paths"]
|
64
64
|
next unless paths&.any?
|
65
|
-
next GeoJSON::LineString
|
66
|
-
next GeoJSON::MultiLineString
|
65
|
+
next GeoJSON::LineString[paths[0], properties] if @mixed && paths.one?
|
66
|
+
next GeoJSON::MultiLineString[paths, properties]
|
67
67
|
when "esriGeometryPolygon"
|
68
68
|
raise "ArcGIS curve geometries not supported" if geometry.key? "curveRings"
|
69
69
|
rings = geometry["rings"]
|
70
70
|
next unless rings&.any?
|
71
|
-
rings.
|
72
|
-
|
73
|
-
next GeoJSON::Polygon.new polys.first, properties if @mixed && polys.one?
|
74
|
-
next GeoJSON::MultiPolygon.new polys, properties
|
71
|
+
polys = GeoJSON::MultiLineString[rings.map(&:reverse), properties].to_multipolygon
|
72
|
+
next @mixed && polys.one? ? polys.first : polys
|
75
73
|
else
|
76
74
|
raise "unsupported ArcGIS geometry type: #{@geometry_type}"
|
77
75
|
end
|
78
|
-
end.
|
76
|
+
end.tap do |features|
|
79
77
|
yielder << GeoJSON::Collection.new(projection: projection, features: features, name: @name)
|
80
78
|
end
|
81
79
|
objectids.shift per_page
|
@@ -133,20 +133,16 @@ module NSWTopo
|
|
133
133
|
def decode(attributes)
|
134
134
|
attributes.map do |name, value|
|
135
135
|
[name, @revalue[name, value, attributes]]
|
136
|
-
end.to_h.slice(*@fields)
|
137
|
-
attributes.replace decoded
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def transform(feature)
|
142
|
-
decode(feature.properties).transform_keys!(&@rename)
|
136
|
+
end.to_h.slice(*@fields)
|
143
137
|
end
|
144
138
|
|
145
139
|
def paged(per_page: nil)
|
146
140
|
per_page = [*per_page, *@layer["maxRecordCount"], 500].min
|
147
141
|
Enumerator::Lazy.new pages(per_page) do |yielder, page|
|
148
|
-
page.
|
149
|
-
|
142
|
+
page.map! do |feature|
|
143
|
+
decoded = decode(feature.properties).transform_keys!(&@rename)
|
144
|
+
feature.with_properties decoded
|
145
|
+
end.then(&yielder)
|
150
146
|
end
|
151
147
|
end
|
152
148
|
|
@@ -173,9 +169,9 @@ module NSWTopo
|
|
173
169
|
end
|
174
170
|
|
175
171
|
def counts
|
176
|
-
classify(*@fields, *extra_field).
|
172
|
+
classify(*@fields, *extra_field).group_by do |attributes, count|
|
177
173
|
decode attributes
|
178
|
-
end.
|
174
|
+
end.map do |attributes, attributes_counts|
|
179
175
|
[attributes, attributes_counts.sum(&:last)]
|
180
176
|
end
|
181
177
|
end
|
data/lib/nswtopo/gis/dem.rb
CHANGED
@@ -16,12 +16,13 @@ module NSWTopo
|
|
16
16
|
raise "no elevation data found at specified path" if rasters.none?
|
17
17
|
log_update "%s: extracting DEM raster" % @name
|
18
18
|
end.group_by do |path, info|
|
19
|
-
Projection.new
|
19
|
+
@epsg ? Projection.epsg(@epsg) : Projection.new(info.dig("coordinateSystem", "wkt"))
|
20
20
|
end.map.with_index do |(projection, rasters), index|
|
21
21
|
raise "DEM data not in planar projection with metre units" unless projection.metres?
|
22
22
|
|
23
23
|
paths, resolutions = rasters.map do |path, info|
|
24
|
-
|
24
|
+
rx, ry = info["geoTransform"].values_at(1, 2)
|
25
|
+
next path, Vector[rx, ry].norm
|
25
26
|
end.sort_by(&:last).transpose
|
26
27
|
|
27
28
|
txt_path.write paths.reverse.join(?\n)
|
@@ -28,14 +28,14 @@ module NSWTopo
|
|
28
28
|
end
|
29
29
|
end.entries.tap do |paths|
|
30
30
|
total = paths.length
|
31
|
-
end.
|
31
|
+
end.filter_map.with_index do |path, index|
|
32
32
|
yield [index + 1, total] if block_given?
|
33
33
|
info = JSON.parse OS.gdalinfo("-json", path)
|
34
34
|
next unless info["geoTransform"]
|
35
|
-
next unless
|
35
|
+
next unless @epsg || info.dig("coordinateSystem", "wkt")
|
36
36
|
next path, info
|
37
37
|
rescue JSON::ParserError
|
38
|
-
end
|
38
|
+
end
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -19,7 +19,7 @@ module NSWTopo
|
|
19
19
|
geometry, properties = feature.values_at "geometry", "properties"
|
20
20
|
type, coordinates = geometry.values_at "type", "coordinates"
|
21
21
|
raise Error, "unsupported geometry type: #{type}" unless TYPES === type
|
22
|
-
GeoJSON.const_get(type)
|
22
|
+
GeoJSON.const_get(type)[coordinates, properties]
|
23
23
|
end.then do |features|
|
24
24
|
new projection: projection, features: features, name: name
|
25
25
|
end
|
@@ -37,6 +37,14 @@ module NSWTopo
|
|
37
37
|
block_given? ? tap { @features.each(&block) } : @features.each
|
38
38
|
end
|
39
39
|
|
40
|
+
def map!(&block)
|
41
|
+
tap { @features.map!(&block) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def reject!(&block)
|
45
|
+
tap { @features.reject!(&block) }
|
46
|
+
end
|
47
|
+
|
40
48
|
def reproject_to(projection)
|
41
49
|
return self if self.projection == projection
|
42
50
|
json = OS.ogr2ogr "-t_srs", projection, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
|
@@ -52,32 +60,48 @@ module NSWTopo
|
|
52
60
|
def to_h
|
53
61
|
{
|
54
62
|
"type" => "FeatureCollection",
|
63
|
+
"name" => @name,
|
55
64
|
"crs" => { "type" => "name", "properties" => { "name" => @projection } },
|
56
65
|
"features" => map(&:to_h)
|
57
|
-
}.
|
58
|
-
hash["name"] = @name if @name
|
59
|
-
end
|
66
|
+
}.compact
|
60
67
|
end
|
61
68
|
|
62
69
|
extend Forwardable
|
63
|
-
delegate %i[coordinates properties wkt area] => :first
|
64
|
-
delegate %i[
|
70
|
+
delegate %i[coordinates properties wkt area svg_path_data] => :first
|
71
|
+
delegate %i[length] => :@features
|
65
72
|
|
66
73
|
def to_json(**extras)
|
67
74
|
to_h.merge(extras).to_json
|
68
75
|
end
|
69
76
|
|
77
|
+
def with_features(features)
|
78
|
+
Collection.new projection: @projection, name: @name, features: features
|
79
|
+
end
|
80
|
+
|
81
|
+
def with_name(name)
|
82
|
+
Collection.new projection: @projection, name: name, features: @features
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_sql(sql, name: @name)
|
86
|
+
json = OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/ GeoJSON:/vsistdin/ -dialect SQLite -sql], sql do |stdin|
|
87
|
+
stdin.puts to_json
|
88
|
+
end
|
89
|
+
Collection.load(json, projection: @projection).with_name(name)
|
90
|
+
rescue OS::Error
|
91
|
+
raise "GDAL with SQLite support required"
|
92
|
+
end
|
93
|
+
|
70
94
|
def explode
|
71
|
-
|
95
|
+
with_features flat_map(&:explode)
|
72
96
|
end
|
73
97
|
|
74
98
|
def multi
|
75
|
-
|
99
|
+
with_features map(&:multi)
|
76
100
|
end
|
77
101
|
|
78
102
|
def merge(other)
|
79
103
|
raise Error, "can't merge different projections" unless @projection == other.projection
|
80
|
-
|
104
|
+
with_features @features + other.features
|
81
105
|
end
|
82
106
|
|
83
107
|
def merge!(other)
|
@@ -85,6 +109,19 @@ module NSWTopo
|
|
85
109
|
tap { @features.concat other.features }
|
86
110
|
end
|
87
111
|
|
112
|
+
def dissolve_points
|
113
|
+
with_features map(&:dissolve_points)
|
114
|
+
end
|
115
|
+
|
116
|
+
def union
|
117
|
+
return self if none?
|
118
|
+
with_features [inject(&:+)]
|
119
|
+
end
|
120
|
+
|
121
|
+
def rotate_by_degrees!(angle)
|
122
|
+
map! { |feature| feature.rotate_by_degrees(angle) }
|
123
|
+
end
|
124
|
+
|
88
125
|
def clip(polygon)
|
89
126
|
OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "-clipsrc", polygon.wkt, "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
|
90
127
|
stdin.puts to_json
|
@@ -97,14 +134,10 @@ module NSWTopo
|
|
97
134
|
map do |feature|
|
98
135
|
feature.buffer(*margins, **options)
|
99
136
|
end.then do |features|
|
100
|
-
|
137
|
+
with_features features
|
101
138
|
end
|
102
139
|
end
|
103
140
|
|
104
|
-
def rename(name = nil)
|
105
|
-
tap { @name = name }
|
106
|
-
end
|
107
|
-
|
108
141
|
# TODO: what about empty collections?
|
109
142
|
def bounds
|
110
143
|
map(&:bounds).transpose.map(&:flatten).map(&:minmax)
|
@@ -113,6 +146,19 @@ module NSWTopo
|
|
113
146
|
def bbox
|
114
147
|
GeoJSON.polygon [bounds.inject(&:product).values_at(0,2,3,1,0)], projection: @projection
|
115
148
|
end
|
149
|
+
|
150
|
+
def bbox_centre
|
151
|
+
midpoint = bounds.map { |min, max| (max + min) / 2 }
|
152
|
+
GeoJSON.point midpoint, projection: @projection
|
153
|
+
end
|
154
|
+
|
155
|
+
def bbox_extents
|
156
|
+
bounds.map { |min, max| max - min }
|
157
|
+
end
|
158
|
+
|
159
|
+
def minimum_bbox_angle(*margins)
|
160
|
+
dissolve_points.union.first.minimum_bbox_angle(*margins)
|
161
|
+
end
|
116
162
|
end
|
117
163
|
end
|
118
164
|
end
|