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
@@ -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
|