module VectorSequence

Public Instance Methods

anticlockwise?() click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 15
def anticlockwise?
  signed_area >= 0
end
centroid() click to toggle source
# 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?() click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 10
def clockwise?
  signed_area < 0
end
Also aliased as: hole?
convex?() click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 25
def convex?
  ring.map(&:difference).ring.all? do |directions|
    directions.inject(&:cross) >= 0
  end
end
convex_hull() click to toggle source
# 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) click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 129
def crop(length)
  trim(0.5 * (path_length - length))
end
douglas_peucker(tolerance) click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 161
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
hole?()
Alias for: clockwise?
in_sections(count) click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 153
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) click to toggle source
# 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_length() click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 103
def path_length
  segments.map(&:difference).sum(&:norm)
end
perps() click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 2
def perps
  ring.map(&:difference).map(&:perp)
end
sample_at(interval, along: false, angle: false) click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 133
def sample_at(interval, along: false, angle: false)
  Enumerator.new do |yielder|
    segments.inject [0.5, 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.difference.angle]
        else segment[0]
        end
        alpha = 1.0
      end
      [alpha - segment.distance / interval, sum + segment.distance]
    end
  end.entries
end
signed_area() click to toggle source
# 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) click to toggle source
# File lib/nswtopo/geometry/vector_sequence.rb, line 31
def surrounds?(points)
  points.all? do |point|
    point.within? self
  end
end
trim(margin) click to toggle source
# 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