nswtopo 3.0.1 → 3.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 +19 -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 +4 -4
- 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 +59 -13
- 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 +3 -3
- 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 +56 -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
@@ -1,11 +1,152 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module GeoJSON
|
3
3
|
class LineString
|
4
|
-
|
4
|
+
include SVG
|
5
|
+
|
6
|
+
delegate %i[length offset buffer smooth samples subdivide to_polygon] => :multi
|
7
|
+
|
8
|
+
def self.[](coordinates, properties = nil, &block)
|
9
|
+
new(coordinates, properties) do
|
10
|
+
sanitised = @coordinates.map do |point|
|
11
|
+
Vector === point ? point : Vector[*point]
|
12
|
+
end.chunk(&:itself).map(&:first)
|
13
|
+
@coordinates.replace sanitised
|
14
|
+
block.call self if block_given?
|
15
|
+
end
|
16
|
+
end
|
5
17
|
|
6
18
|
def bounds
|
7
19
|
@coordinates.transpose.map(&:minmax)
|
8
20
|
end
|
21
|
+
|
22
|
+
def reverse
|
23
|
+
LineString.new @coordinates.reverse, @properties
|
24
|
+
end
|
25
|
+
|
26
|
+
def path_length
|
27
|
+
each_cons(2).sum { |p0, p1| (p1 - p0).norm }
|
28
|
+
end
|
29
|
+
|
30
|
+
def closed?
|
31
|
+
@coordinates.last == @coordinates.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def signed_area
|
35
|
+
each_cons(2).sum { |p0, p1| p0.cross(p1) } / 2
|
36
|
+
end
|
37
|
+
|
38
|
+
def clockwise?
|
39
|
+
signed_area < 0
|
40
|
+
end
|
41
|
+
alias interior? clockwise?
|
42
|
+
|
43
|
+
def anticlockwise?
|
44
|
+
signed_area >= 0
|
45
|
+
end
|
46
|
+
alias exterior? anticlockwise?
|
47
|
+
|
48
|
+
def simplify(tolerance)
|
49
|
+
chunks, simplified = [@coordinates], []
|
50
|
+
while chunk = chunks.pop
|
51
|
+
direction = (chunk.last - chunk.first).normalised
|
52
|
+
delta, index = chunk.map do |point|
|
53
|
+
(point - chunk.first).cross(direction).abs
|
54
|
+
end.each.with_index.max_by(&:first)
|
55
|
+
if delta < tolerance
|
56
|
+
simplified.prepend chunk.first
|
57
|
+
else
|
58
|
+
chunks << chunk[0..index] << chunk[index..-1]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
simplified << @coordinates.last
|
62
|
+
LineString.new simplified, @properties
|
63
|
+
end
|
64
|
+
|
65
|
+
def sample_at(interval, offset: 0, &block)
|
66
|
+
Enumerator.new do |yielder|
|
67
|
+
alpha = (0.5 + Float(offset || 0) / interval) % 1.0
|
68
|
+
each_cons(2).inject [alpha, 0] do |(alpha, along), (p0, p1)|
|
69
|
+
angle = (p1 - p0).angle
|
70
|
+
loop do
|
71
|
+
distance = (p1 - p0).norm
|
72
|
+
fraction = alpha * interval / distance
|
73
|
+
break unless fraction < 1
|
74
|
+
p0 = p1 * fraction + p0 * (1 - fraction)
|
75
|
+
along += alpha * interval
|
76
|
+
block_given? ? yielder << block.call(p0, along, angle) : yielder << p0
|
77
|
+
alpha = 1.0
|
78
|
+
end
|
79
|
+
distance = (p1 - p0).norm
|
80
|
+
next alpha - distance / interval, along + distance
|
81
|
+
end
|
82
|
+
end.entries
|
83
|
+
end
|
84
|
+
|
85
|
+
def segmentise(interval)
|
86
|
+
LineString.new sample_at(interval).push(@coordinates.last), @properties
|
87
|
+
end
|
88
|
+
|
89
|
+
def smooth_window(window)
|
90
|
+
[@coordinates.take(1)*(window-1), @coordinates, @coordinates.last(1)*(window-1)].flatten(1).each_cons(window).map do |points|
|
91
|
+
points.inject(&:+) / window
|
92
|
+
end.then do |smoothed|
|
93
|
+
LineString.new smoothed, @properties
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def trim(amount)
|
98
|
+
return self unless amount > 0
|
99
|
+
ending, total = path_length - amount, 0
|
100
|
+
trimmed = each_cons(2).with_object [] do |(p0, p1), trimmed|
|
101
|
+
delta = (p1 - p0).norm
|
102
|
+
case
|
103
|
+
when total >= ending then break trimmed
|
104
|
+
when total <= amount - delta
|
105
|
+
when total <= amount
|
106
|
+
trimmed << (p0 * (delta + total - amount) + p1 * (amount - total)) / delta
|
107
|
+
trimmed << (p0 * (delta + total - ending) + p1 * (ending - total)) / delta if total + delta >= ending
|
108
|
+
else
|
109
|
+
trimmed << p0
|
110
|
+
trimmed << (p0 * (delta + total - ending) + p1 * (ending - total)) / delta if total + delta >= ending
|
111
|
+
end
|
112
|
+
total += delta
|
113
|
+
end
|
114
|
+
LineString.new trimmed, @properties
|
115
|
+
end
|
116
|
+
|
117
|
+
def crop(length)
|
118
|
+
trim((path_length - length) / 2)
|
119
|
+
end
|
120
|
+
|
121
|
+
def svg_path_data(bezier: false)
|
122
|
+
if bezier
|
123
|
+
fraction = Numeric === bezier ? bezier.clamp(0, 1) : 1
|
124
|
+
extras = closed? ? [@coordinates[-2], *@coordinates, @coordinates[2]] : [@coordinates.first, *@coordinates, @coordinates.last]
|
125
|
+
midpoints = extras.each_cons(2).map do |p0, p1|
|
126
|
+
(p0 + p1) / 2
|
127
|
+
end
|
128
|
+
distances = extras.each_cons(2).map do |p0, p1|
|
129
|
+
(p1 - p0).norm
|
130
|
+
end
|
131
|
+
offsets = midpoints.zip(distances).each_cons(2).map do |(m0, d0), (m1, d1)|
|
132
|
+
(m0 * d1 + m1 * d0) / (d0 + d1)
|
133
|
+
end.zip(@coordinates).map do |p0, p1|
|
134
|
+
p1 - p0
|
135
|
+
end
|
136
|
+
controls = midpoints.each_cons(2).zip(offsets).flat_map do |(m0, m1), offset|
|
137
|
+
next m0 + offset * fraction, m1 + offset * fraction
|
138
|
+
end.drop(1).each_slice(2).entries.prepend(nil)
|
139
|
+
zip(controls).map do |point, controls|
|
140
|
+
controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point
|
141
|
+
end.join(" ")
|
142
|
+
else
|
143
|
+
map do |point|
|
144
|
+
POINT % point
|
145
|
+
end.join(" L ").tap do |string|
|
146
|
+
string.concat(" Z") if closed?
|
147
|
+
end.prepend("M ")
|
148
|
+
end
|
149
|
+
end
|
9
150
|
end
|
10
151
|
end
|
11
152
|
end
|
@@ -3,12 +3,21 @@ module NSWTopo
|
|
3
3
|
class MultiLineString
|
4
4
|
include StraightSkeleton
|
5
5
|
|
6
|
-
def
|
7
|
-
|
6
|
+
def freeze!
|
7
|
+
each { }
|
8
|
+
freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
def path_length
|
12
|
+
sum(&:path_length)
|
13
|
+
end
|
14
|
+
|
15
|
+
def nodes
|
16
|
+
Nodes.new self
|
8
17
|
end
|
9
18
|
|
10
19
|
def offset(*margins, **options)
|
11
|
-
linestrings = margins.inject
|
20
|
+
linestrings = margins.inject nodes do |nodes, margin|
|
12
21
|
nodes.progress limit: margin, **options.slice(:rounding_angle, :cutoff_angle)
|
13
22
|
end.readout
|
14
23
|
MultiLineString.new linestrings, @properties
|
@@ -19,7 +28,7 @@ module NSWTopo
|
|
19
28
|
end
|
20
29
|
|
21
30
|
def smooth(margin, **options)
|
22
|
-
linestrings =
|
31
|
+
linestrings = nodes.tap do |nodes|
|
23
32
|
nodes.progress **options.slice(:rounding_angle).merge(limit: margin)
|
24
33
|
nodes.progress **options.slice(:rounding_angle, :cutoff_angle).merge(limit: -2 * margin)
|
25
34
|
nodes.progress **options.slice(:rounding_angle, :cutoff_angle).merge(limit: margin)
|
@@ -28,13 +37,46 @@ module NSWTopo
|
|
28
37
|
end
|
29
38
|
|
30
39
|
def samples(interval)
|
31
|
-
|
40
|
+
sampled = flat_map do |linestring|
|
32
41
|
distance = linestring.path_length
|
33
|
-
linestring.sample_at(interval
|
42
|
+
linestring.sample_at(interval) do |point, along, angle|
|
34
43
|
[point, (2 * along - distance).abs - distance]
|
35
44
|
end
|
36
45
|
end.sort_by(&:last).map(&:first)
|
37
|
-
MultiPoint.new
|
46
|
+
MultiPoint.new sampled, @properties
|
47
|
+
end
|
48
|
+
|
49
|
+
def dissolve_points
|
50
|
+
MultiPoint.new @coordinates.flatten(1), @properties
|
51
|
+
end
|
52
|
+
|
53
|
+
def subdivide(count)
|
54
|
+
subdivided = flat_map do |linestring|
|
55
|
+
linestring.each_cons(2).each_slice(count).map do |pairs|
|
56
|
+
pairs.inject { |part, (p0, p1)| part << p1 }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
MultiLineString.new subdivided, @properties
|
60
|
+
end
|
61
|
+
|
62
|
+
def trim(amount)
|
63
|
+
map do |feature|
|
64
|
+
feature.trim amount
|
65
|
+
end.reject(&:empty?).inject(empty_linestrings, &:+)
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_polygon
|
69
|
+
Polygon.new @coordinates, @properties
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_multipolygon
|
73
|
+
unclaimed, exterior_rings = partition(&:interior?)
|
74
|
+
exterior_rings.sort_by(&:signed_area).map(&:to_polygon).map do |polygon|
|
75
|
+
interior_rings, unclaimed = unclaimed.partition do |ring|
|
76
|
+
polygon.contains? ring.first
|
77
|
+
end
|
78
|
+
interior_rings.inject(polygon, &:add_ring)
|
79
|
+
end.inject(empty_polygons, &:+)
|
38
80
|
end
|
39
81
|
end
|
40
82
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module GeoJSON
|
3
|
+
class MultiPoint
|
4
|
+
def self.[](coordinates, properties = nil, &block)
|
5
|
+
new(coordinates, properties) do
|
6
|
+
@coordinates.map! do |point|
|
7
|
+
Vector === point ? point : Vector[*point]
|
8
|
+
end
|
9
|
+
block.call self if block_given?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def bounds
|
14
|
+
@coordinates.transpose.map(&:minmax)
|
15
|
+
end
|
16
|
+
|
17
|
+
alias dissolve_points itself
|
18
|
+
|
19
|
+
def rotate_by_degrees(angle)
|
20
|
+
rotated = @coordinates.map do |point|
|
21
|
+
point.rotate_by_degrees(angle)
|
22
|
+
end
|
23
|
+
MultiPoint.new rotated, @properties
|
24
|
+
end
|
25
|
+
|
26
|
+
def convex_hull
|
27
|
+
start = @coordinates.min
|
28
|
+
points, remaining = @coordinates.partition { |point| point == start }
|
29
|
+
remaining.sort_by do |point|
|
30
|
+
next (point - start).angle, (point - start).norm
|
31
|
+
end.inject(points) do |points, p2|
|
32
|
+
while points.length > 1 do
|
33
|
+
p0, p1 = points.last(2)
|
34
|
+
(p2 - p0).cross(p1 - p0) < 0 ? break : points.pop
|
35
|
+
end
|
36
|
+
points << p2
|
37
|
+
end.then do |points|
|
38
|
+
LineString.new points, @properties
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def minimum_bbox_angle(*margins)
|
43
|
+
ring = convex_hull.coordinates
|
44
|
+
return 0 if ring.one?
|
45
|
+
indices = [%i[min_by max_by], %i[x y]].inject(:product).map do |min, coord|
|
46
|
+
ring.map(&coord).each.with_index.send(min, &:first).last
|
47
|
+
end
|
48
|
+
calipers = [Vector[0, -1], Vector[1, 0], Vector[0, 1], Vector[-1, 0]]
|
49
|
+
rotation = 0.0
|
50
|
+
candidates = []
|
51
|
+
|
52
|
+
while rotation < Math::PI / 2
|
53
|
+
edges = indices.map do |index|
|
54
|
+
ring[(index + 1) % ring.length] - ring[index]
|
55
|
+
end
|
56
|
+
angle, which = [edges, calipers].transpose.map do |edge, caliper|
|
57
|
+
Math::acos caliper.proj(edge).clamp(-1, 1)
|
58
|
+
end.map.with_index.min_by(&:first)
|
59
|
+
|
60
|
+
calipers.map! { |caliper| caliper.rotate_by(angle) }
|
61
|
+
rotation += angle
|
62
|
+
|
63
|
+
break if rotation >= Math::PI / 2
|
64
|
+
|
65
|
+
dimensions = [0, 1].map do |offset|
|
66
|
+
(ring[indices[offset + 2]] - ring[indices[offset]]).proj(calipers[offset + 1])
|
67
|
+
end
|
68
|
+
|
69
|
+
if rotation < Math::PI / 4
|
70
|
+
candidates << [dimensions, rotation]
|
71
|
+
else
|
72
|
+
candidates << [dimensions.reverse, rotation - Math::PI / 2]
|
73
|
+
end
|
74
|
+
|
75
|
+
indices[which] += 1
|
76
|
+
indices[which] %= ring.length
|
77
|
+
end
|
78
|
+
|
79
|
+
candidates.min_by do |dimensions, rotation|
|
80
|
+
dimensions.zip(margins).map do |dimension, margin|
|
81
|
+
margin ? dimension + 2 * margin : dimension
|
82
|
+
end.inject(:*)
|
83
|
+
end.last
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -3,14 +3,27 @@ module NSWTopo
|
|
3
3
|
class MultiPolygon
|
4
4
|
include StraightSkeleton
|
5
5
|
|
6
|
+
def freeze!
|
7
|
+
each { }
|
8
|
+
freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
def rings
|
12
|
+
MultiLineString.new @coordinates.flatten(1), @properties
|
13
|
+
end
|
14
|
+
|
6
15
|
def area
|
7
|
-
|
16
|
+
rings.sum(&:signed_area)
|
17
|
+
end
|
18
|
+
|
19
|
+
def nodes
|
20
|
+
Nodes.new rings
|
8
21
|
end
|
9
22
|
|
10
23
|
def skeleton
|
11
24
|
segments = []
|
12
|
-
|
13
|
-
segments << [node0.point, node1.point
|
25
|
+
nodes.progress do |event, node0, node1|
|
26
|
+
segments << [node0.point.to_f, node1.point.to_f]
|
14
27
|
end
|
15
28
|
MultiLineString.new segments, @properties
|
16
29
|
end
|
@@ -19,7 +32,7 @@ module NSWTopo
|
|
19
32
|
neighbours = Hash.new { |neighbours, node| neighbours[node] = [] }
|
20
33
|
samples, tails, node1 = {}, {}, nil
|
21
34
|
|
22
|
-
|
35
|
+
nodes.progress(interval: interval) do |event, *args|
|
23
36
|
case event
|
24
37
|
when :nodes
|
25
38
|
node0, node1 = *args
|
@@ -28,7 +41,7 @@ module NSWTopo
|
|
28
41
|
when :interval
|
29
42
|
travel, rings = *args
|
30
43
|
samples[travel] = rings.flat_map do |ring|
|
31
|
-
ring.sample_at
|
44
|
+
LineString.new(ring).sample_at(interval)
|
32
45
|
end
|
33
46
|
end
|
34
47
|
end
|
@@ -52,7 +65,7 @@ module NSWTopo
|
|
52
65
|
neighbours.delete node
|
53
66
|
neighbours[neighbour].delete node
|
54
67
|
nodes, length = tails.delete(node) || [[node], 0]
|
55
|
-
candidate = [nodes << neighbour, length +
|
68
|
+
candidate = [nodes << neighbour, length + (node.point - neighbour.point).norm]
|
56
69
|
tails[neighbour] = [tails[neighbour], candidate].compact.max_by(&:last)
|
57
70
|
end.any?
|
58
71
|
end
|
@@ -61,7 +74,7 @@ module NSWTopo
|
|
61
74
|
while candidates.any?
|
62
75
|
(*nodes, node), length = candidates.pop
|
63
76
|
next if (neighbours[node] - nodes).each do |neighbour|
|
64
|
-
candidates << [[*nodes, node, neighbour], length +
|
77
|
+
candidates << [[*nodes, node, neighbour], length + (node.point - neighbour.point).norm]
|
65
78
|
end.any?
|
66
79
|
index = nodes.find(&:index).index
|
67
80
|
tail_nodes, tail_length = tails[node] || [[node], 0]
|
@@ -72,7 +85,7 @@ module NSWTopo
|
|
72
85
|
nodes.chunk do |node|
|
73
86
|
node.travel >= min_travel
|
74
87
|
end.select(&:first).map(&:last).reject(&:one?).map do |nodes|
|
75
|
-
nodes.map(&:point).to_f
|
88
|
+
nodes.map(&:point).map(&:to_f)
|
76
89
|
end
|
77
90
|
end
|
78
91
|
features.prepend MultiLineString.new(linestrings, @properties)
|
@@ -87,30 +100,29 @@ module NSWTopo
|
|
87
100
|
end
|
88
101
|
|
89
102
|
def buffer(*margins, **options)
|
90
|
-
|
91
|
-
margins.each do |margin|
|
92
|
-
nodes.progress limit: -margin, **options.slice(:rounding_angle, :cutoff_angle)
|
93
|
-
end
|
94
|
-
interior_rings, exterior_rings = nodes.readout.partition(&:hole?)
|
95
|
-
polygons, foo = exterior_rings.sort_by(&:signed_area).inject [[], interior_rings] do |(polygons, interior_rings), exterior_ring|
|
96
|
-
claimed, unclaimed = interior_rings.partition do |interior_ring|
|
97
|
-
interior_ring.first.within? exterior_ring
|
98
|
-
end
|
99
|
-
[polygons << [exterior_ring, *claimed], unclaimed]
|
100
|
-
end
|
101
|
-
MultiPolygon.new polygons.entries, @properties
|
103
|
+
rings.offset(*margins.map(&:-@), **options).to_multipolygon
|
102
104
|
end
|
103
105
|
|
104
106
|
def centroids
|
105
|
-
|
107
|
+
map(&:centroid).inject(empty_points, &:+)
|
106
108
|
end
|
107
109
|
|
108
110
|
def samples(interval)
|
109
|
-
points =
|
110
|
-
|
111
|
+
points = rings.flat_map do |coordinates|
|
112
|
+
linestring.sample_at(interval)
|
111
113
|
end
|
112
114
|
MultiPoint.new points, @properties
|
113
115
|
end
|
116
|
+
|
117
|
+
def dissolve_points
|
118
|
+
MultiPoint.new @coordinates.flatten(2), @properties
|
119
|
+
end
|
120
|
+
|
121
|
+
def remove_holes(&block)
|
122
|
+
map do |polygon|
|
123
|
+
polygon.remove_holes(&block)
|
124
|
+
end.inject(empty_polygons, &:+)
|
125
|
+
end
|
114
126
|
end
|
115
127
|
end
|
116
128
|
end
|
@@ -1,8 +1,23 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module GeoJSON
|
3
3
|
class Point
|
4
|
+
def self.[](coordinates, properties = nil, &block)
|
5
|
+
new(coordinates, properties) do
|
6
|
+
@coordinates = Vector[*@coordinates] unless Vector === @coordinates
|
7
|
+
block.call self if block_given?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
4
11
|
def bounds
|
5
|
-
|
12
|
+
zip.map(&:minmax)
|
13
|
+
end
|
14
|
+
|
15
|
+
def empty?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def rotate_by_degrees(angle)
|
20
|
+
Point.new @coordinates.rotate_by_degrees(angle), @properties
|
6
21
|
end
|
7
22
|
end
|
8
23
|
end
|
@@ -1,26 +1,88 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module GeoJSON
|
3
3
|
class Polygon
|
4
|
-
|
4
|
+
include SVG
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
def self.[](coordinates, properties = nil, &block)
|
7
|
+
new(coordinates, properties) do
|
8
|
+
@coordinates.each.with_index do |coordinates, index|
|
9
|
+
LineString[coordinates] do |ring|
|
10
|
+
ring.coordinates << ring.first unless ring.closed?
|
11
|
+
ring.coordinates.reverse! if index.zero? ^ ring.exterior?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
block.call self if block_given?
|
10
15
|
end
|
11
16
|
end
|
12
17
|
|
18
|
+
def freeze!
|
19
|
+
@coordinates.each(&:freeze)
|
20
|
+
freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
delegate %i[skeleton centres centrepoints centrelines buffer samples] => :multi
|
24
|
+
|
13
25
|
def bounds
|
14
|
-
|
26
|
+
first.transpose.map(&:minmax)
|
15
27
|
end
|
16
28
|
|
17
29
|
def wkt
|
18
|
-
|
30
|
+
map do |ring|
|
19
31
|
ring.map do |point|
|
20
32
|
point.join(" ")
|
21
33
|
end.join(", ").prepend("(").concat(")")
|
22
34
|
end.join(", ").prepend("POLYGON (").concat(")")
|
23
35
|
end
|
36
|
+
|
37
|
+
def centroid
|
38
|
+
flat_map do |ring|
|
39
|
+
ring.each_cons(2).map do |p0, p1|
|
40
|
+
next (p0 + p1) * p0.cross(p1), 3 * p0.cross(p1)
|
41
|
+
end
|
42
|
+
end.transpose.then do |centroids_x6, signed_areas_x6|
|
43
|
+
point = centroids_x6.inject(&:+) / signed_areas_x6.inject(&:+)
|
44
|
+
Point.new point, @properties
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def rings
|
49
|
+
MultiLineString.new @coordinates, @properties
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_ring(ring)
|
53
|
+
Polygon.new [*@coordinates, ring.coordinates], @properties
|
54
|
+
end
|
55
|
+
|
56
|
+
def area
|
57
|
+
rings.sum(&:signed_area)
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_holes(&block)
|
61
|
+
rings.reject_linestrings do |ring|
|
62
|
+
ring.interior? && (block_given? ? block.call(ring) : true)
|
63
|
+
end.to_polygon
|
64
|
+
end
|
65
|
+
|
66
|
+
def contains?(geometry)
|
67
|
+
geometry = Point.new(geometry) if Vector === geometry
|
68
|
+
geometry.dissolve_points.coordinates.all? do |point|
|
69
|
+
sum do |ring|
|
70
|
+
ring.each_cons(2).inject(0) do |winding, (p0, p1)|
|
71
|
+
case
|
72
|
+
when p1.y > point.y && p0.y <= point.y && (p0 - p1).cross(p0 - point) >= 0 then winding + 1
|
73
|
+
when p0.y > point.y && p1.y <= point.y && (p1 - p0).cross(p0 - point) >= 0 then winding - 1
|
74
|
+
when p0.y == point.y && p1.y == point.y && p0.x >= point.x && p1.x < point.x then winding + 1
|
75
|
+
when p0.y == point.y && p1.y == point.y && p1.x >= point.x && p0.x < point.x then winding - 1
|
76
|
+
else winding
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end.nonzero?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def svg_path_data
|
84
|
+
rings.map(&:svg_path_data).join(?\s)
|
85
|
+
end
|
24
86
|
end
|
25
87
|
end
|
26
88
|
end
|