Module: VectorSequence

Defined in:
lib/nswtopo/geometry/vector_sequence.rb

Instance Method Summary collapse

Instance Method Details

#anticlockwise?Boolean

Returns:

  • (Boolean)


15
16
17
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 15

def anticlockwise?
  signed_area >= 0
end

#centroidObject



19
20
21
22
23
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 19

def centroid
  ring.map do |p1, p2|
    (p1.plus p2).times(p1.cross p2)
  end.inject(&:plus) / (6.0 * signed_area)
end

#clockwise?Boolean Also known as: hole?

Returns:

  • (Boolean)


10
11
12
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 10

def clockwise?
  signed_area < 0
end

#convex?Boolean

Returns:

  • (Boolean)


25
26
27
28
29
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 25

def convex?
  ring.map(&:diff).ring.all? do |directions|
    directions.inject(&:cross) >= 0
  end
end

#convex_hullObject



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 37

def convex_hull
  start = min_by(&:reverse)
  hull, remaining = uniq.partition { |point| point == start }
  remaining.sort_by do |point|
    [point.minus(start).angle, point.minus(start).norm]
  end.inject(hull) do |memo, p3|
    while memo.many? do
      p1, p2 = memo.last(2)
      (p3.minus p1).cross(p2.minus p1) < 0 ? break : memo.pop
    end
    memo << p3
  end
end

#crop(length) ⇒ Object



129
130
131
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 129

def crop(length)
  trim(0.5 * (path_length - length))
end

#douglas_peucker(tolerance) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 162

def douglas_peucker(tolerance)
  chunks, simplified = [self], []
  while chunk = chunks.pop
    direction = chunk.last.minus(chunk.first).normalised
    deltas = chunk.map do |point|
      point.minus(chunk.first).cross(direction).abs
    end
    delta, index = deltas.each.with_index.max_by(&:first)
    if delta < tolerance
      simplified.prepend chunk.first
    else
      chunks << chunk[0..index] << chunk[index..-1]
    end
  end
  simplified << last
end

#in_sections(count) ⇒ Object



154
155
156
157
158
159
160
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 154

def in_sections(count)
  segments.each_slice(count).map do |segments|
    segments.inject do |section, segment|
      section << segment[1]
    end
  end
end

#minimum_bounding_box(*margins) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 51

def minimum_bounding_box(*margins)
  polygon = convex_hull
  return polygon[0], [0, 0], 0 if polygon.one?
  indices = [%i[min_by max_by], [0, 1]].inject(:product).map do |min, axis|
    polygon.map.with_index.send(min) { |point, index| point[axis] }.last
  end
  calipers = [[0, -1], [1, 0], [0, 1], [-1, 0]]
  rotation = 0.0
  candidates = []

  while rotation < Math::PI / 2
    edges = indices.map do |index|
      polygon[(index + 1) % polygon.length].minus polygon[index]
    end
    angle, which = [edges, calipers].transpose.map do |edge, caliper|
      Math::acos caliper.proj(edge).clamp(-1, 1)
    end.map.with_index.min_by { |angle, index| angle }

    calipers.each { |caliper| caliper.rotate_by!(angle) }
    rotation += angle

    break if rotation >= Math::PI / 2

    dimensions = [0, 1].map do |offset|
      polygon[indices[offset + 2]].minus(polygon[indices[offset]]).proj(calipers[offset + 1])
    end

    centre = polygon.values_at(*indices).map do |point|
      point.rotate_by(-rotation)
    end.partition.with_index do |point, index|
      index.even?
    end.map.with_index do |pair, index|
      0.5 * pair.map { |point| point[index] }.inject(:+)
    end.rotate_by(rotation)

    if rotation < Math::PI / 4
      candidates << [centre, dimensions, rotation]
    else
      candidates << [centre, dimensions.reverse, rotation - Math::PI / 2]
    end

    indices[which] += 1
    indices[which] %= polygon.length
  end

  candidates.min_by do |centre, dimensions, rotation|
    dimensions.zip(margins).map do |dimension, margin|
      margin ? dimension + 2 * margin : dimension
    end.inject(:*)
  end
end

#path_lengthObject



103
104
105
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 103

def path_length
  segments.map(&:diff).sum(&:norm)
end

#perpsObject



2
3
4
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 2

def perps
  ring.map(&:diff).map(&:perp)
end

#sample_at(interval, along: false, angle: false, offset: nil) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 133

def sample_at(interval, along: false, angle: false, offset: nil)
  Enumerator.new do |yielder|
    alpha = (0.5 + Float(offset || 0) / interval) % 1.0
    segments.inject [alpha, 0] do |(alpha, sum), segment|
      loop do
        fraction = alpha * interval / segment.distance
        break unless fraction < 1
        segment[0] = segment.along(fraction)
        sum += alpha * interval
        yielder << case
        when along then [segment[0], sum]
        when angle then [segment[0], segment.diff.angle]
        else segment[0]
        end
        alpha = 1.0
      end
      [alpha - segment.distance / interval, sum + segment.distance]
    end
  end.entries
end

#signed_areaObject



6
7
8
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 6

def signed_area
  0.5 * ring.map { |p1, p2| p1.cross p2 }.inject(&:+)
end

#surrounds?(points) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
34
35
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 31

def surrounds?(points)
  points.all? do |point|
    point.within? self
  end
end

#trim(margin) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/nswtopo/geometry/vector_sequence.rb', line 107

def trim(margin)
  start = [margin, 0].max
  stop = path_length - start
  return [] unless start < stop
  points, total = [], 0
  segments.each do |segment|
    distance = segment.distance
    case
    when total + distance <= start
    when total <= start
      points << segment.along((start - total) / distance)
      points << segment.along((stop  - total) / distance) if total + distance >= stop
    else
      points << segment[0]
      points << segment.along((stop  - total) / distance) if total + distance >= stop
    end
    total += distance
    break if total >= stop
  end
  points
end