nswtopo 3.0 → 3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +19 -4
  3. data/docs/contours.md +2 -0
  4. data/docs/relief.md +2 -3
  5. data/docs/spot-heights.md +2 -0
  6. data/lib/nswtopo/archive.rb +6 -3
  7. data/lib/nswtopo/chrome.rb +16 -8
  8. data/lib/nswtopo/commands/layers.rb +2 -2
  9. data/lib/nswtopo/config.rb +1 -0
  10. data/lib/nswtopo/formats/gemf.rb +1 -0
  11. data/lib/nswtopo/formats/kmz.rb +16 -10
  12. data/lib/nswtopo/formats/mbtiles.rb +1 -0
  13. data/lib/nswtopo/formats/pdf.rb +34 -4
  14. data/lib/nswtopo/formats/svg.rb +4 -4
  15. data/lib/nswtopo/formats/svgz.rb +1 -0
  16. data/lib/nswtopo/formats/zip.rb +5 -4
  17. data/lib/nswtopo/formats.rb +35 -36
  18. data/lib/nswtopo/geometry/r_tree.rb +24 -23
  19. data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
  20. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
  21. data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
  22. data/lib/nswtopo/geometry/vector.rb +55 -49
  23. data/lib/nswtopo/geometry.rb +0 -5
  24. data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
  25. data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
  26. data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
  27. data/lib/nswtopo/gis/dem.rb +3 -2
  28. data/lib/nswtopo/gis/gdal_glob.rb +3 -3
  29. data/lib/nswtopo/gis/geojson/collection.rb +59 -13
  30. data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
  31. data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
  32. data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
  33. data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
  34. data/lib/nswtopo/gis/geojson/point.rb +16 -1
  35. data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
  36. data/lib/nswtopo/gis/geojson.rb +92 -46
  37. data/lib/nswtopo/gis/projection.rb +5 -1
  38. data/lib/nswtopo/helpers/thread_pool.rb +39 -0
  39. data/lib/nswtopo/helpers.rb +44 -5
  40. data/lib/nswtopo/layer/arcgis_raster.rb +3 -3
  41. data/lib/nswtopo/layer/contour.rb +24 -26
  42. data/lib/nswtopo/layer/control.rb +5 -3
  43. data/lib/nswtopo/layer/declination.rb +14 -10
  44. data/lib/nswtopo/layer/feature.rb +5 -5
  45. data/lib/nswtopo/layer/grid.rb +19 -18
  46. data/lib/nswtopo/layer/labels/barriers.rb +23 -0
  47. data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
  48. data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
  49. data/lib/nswtopo/layer/labels/label.rb +63 -0
  50. data/lib/nswtopo/layer/labels.rb +192 -315
  51. data/lib/nswtopo/layer/overlay.rb +11 -12
  52. data/lib/nswtopo/layer/raster.rb +1 -0
  53. data/lib/nswtopo/layer/relief.rb +6 -4
  54. data/lib/nswtopo/layer/spot.rb +11 -17
  55. data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
  56. data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
  57. data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
  58. data/lib/nswtopo/layer.rb +2 -1
  59. data/lib/nswtopo/map.rb +62 -60
  60. data/lib/nswtopo/svg.rb +5 -0
  61. data/lib/nswtopo/tiled_web_map.rb +3 -3
  62. data/lib/nswtopo/tree_indenter.rb +2 -2
  63. data/lib/nswtopo/version.rb +1 -1
  64. data/lib/nswtopo.rb +8 -0
  65. metadata +15 -17
  66. data/lib/nswtopo/geometry/overlap.rb +0 -47
  67. data/lib/nswtopo/geometry/segment.rb +0 -27
  68. data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
  69. data/lib/nswtopo/helpers/array.rb +0 -19
  70. data/lib/nswtopo/helpers/concurrently.rb +0 -27
  71. data/lib/nswtopo/helpers/dir.rb +0 -7
  72. data/lib/nswtopo/helpers/hash.rb +0 -15
  73. data/lib/nswtopo/helpers/tar_writer.rb +0 -11
  74. 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
- delegate %i[length offset buffer smooth samples] => :multi
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 length
7
- @coordinates.sum(&:path_length)
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 Nodes.new(@coordinates) do |nodes, margin|
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 = Nodes.new(@coordinates).tap do |nodes|
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
- points = @coordinates.flat_map do |linestring|
40
+ sampled = flat_map do |linestring|
32
41
  distance = linestring.path_length
33
- linestring.sample_at(interval, along: true).map do |point, along|
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 points, @properties
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
- @coordinates.flatten(1).sum(&:signed_area)
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
- Nodes.new(@coordinates.flatten(1)).progress do |event, node0, node1|
13
- segments << [node0.point, node1.point].to_f
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
- Nodes.new(@coordinates.flatten(1)).progress(interval: interval) do |event, *args|
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 interval
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 + [node.point, neighbour.point].distance]
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 + [node.point, neighbour.point].distance]
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
- nodes = Nodes.new @coordinates.flatten(1)
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
- MultiPoint.new @coordinates.map(&:first).map(&:centroid), @properties
107
+ map(&:centroid).inject(empty_points, &:+)
106
108
  end
107
109
 
108
110
  def samples(interval)
109
- points = @coordinates.flatten(1).flat_map do |ring|
110
- ring.sample_at interval
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
- @coordinates.zip.map(&:minmax)
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
- delegate %i[area skeleton centres centrepoints centrelines buffer centroids samples] => :multi
4
+ include SVG
5
5
 
6
- def validate!
7
- @coordinates.inject(false) do |hole, ring|
8
- ring.reverse! if hole ^ ring.hole?
9
- true
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
- @coordinates.first.transpose.map(&:minmax)
26
+ first.transpose.map(&:minmax)
15
27
  end
16
28
 
17
29
  def wkt
18
- @coordinates.map do |ring|
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