nswtopo 3.0.1 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|