class NSWTopo::GeoJSON::LineString
def self.[](coordinates, properties = nil, &block)
def self.[](coordinates, properties = nil, &block) new(coordinates, properties) do sanitised = @coordinates.map do |point| Vector === point ? point : Vector[*point] end.chunk(&:itself).map(&:first) @coordinates.replace sanitised block.call self if block_given? end end
def anticlockwise?
def anticlockwise? signed_area >= 0 end
def bounds
def bounds @coordinates.transpose.map(&:minmax) end
def clockwise?
def clockwise? signed_area < 0 end
def closed?
def closed? @coordinates.last == @coordinates.first end
def crop(length)
def crop(length) trim((path_length - length) / 2) end
def path_length
def path_length each_cons(2).sum { |p0, p1| (p1 - p0).norm } end
def reverse
def reverse LineString.new @coordinates.reverse, @properties end
def sample_at(interval, offset: 0, &block)
def sample_at(interval, offset: 0, &block) Enumerator.new do |yielder| alpha = (0.5 + Float(offset || 0) / interval) % 1.0 each_cons(2).inject [alpha, 0] do |(alpha, along), (p0, p1)| angle = (p1 - p0).angle loop do distance = (p1 - p0).norm fraction = alpha * interval / distance break unless fraction < 1 p0 = p1 * fraction + p0 * (1 - fraction) along += alpha * interval block_given? ? yielder << block.call(p0, along, angle) : yielder << p0 alpha = 1.0 end distance = (p1 - p0).norm next alpha - distance / interval, along + distance end end.entries end
def segmentise(interval)
def segmentise(interval) LineString.new sample_at(interval).push(@coordinates.last), @properties end
def signed_area
def signed_area each_cons(2).sum { |p0, p1| p0.cross(p1) } / 2 end
def simplify(tolerance)
def simplify(tolerance) chunks, simplified = [@coordinates], [] while chunk = chunks.pop direction = (chunk.last - chunk.first).normalised delta, index = chunk.map do |point| (point - chunk.first).cross(direction).abs end.each.with_index.max_by(&:first) if delta < tolerance simplified.prepend chunk.first else chunks << chunk[0..index] << chunk[index..-1] end end simplified << @coordinates.last LineString.new simplified, @properties end
def smooth_window(window)
def smooth_window(window) [@coordinates.take(1)*(window-1), @coordinates, @coordinates.last(1)*(window-1)].flatten(1).each_cons(window).map do |points| points.inject(&:+) / window end.then do |smoothed| LineString.new smoothed, @properties end end
def svg_path_data(bezier: false)
def svg_path_data(bezier: false) if bezier fraction = Numeric === bezier ? bezier.clamp(0, 1) : 1 extras = closed? ? [@coordinates[-2], *@coordinates, @coordinates[2]] : [@coordinates.first, *@coordinates, @coordinates.last] midpoints = extras.each_cons(2).map do |p0, p1| (p0 + p1) / 2 end distances = extras.each_cons(2).map do |p0, p1| (p1 - p0).norm end offsets = midpoints.zip(distances).each_cons(2).map do |(m0, d0), (m1, d1)| (m0 * d1 + m1 * d0) / (d0 + d1) end.zip(@coordinates).map do |p0, p1| p1 - p0 end controls = midpoints.each_cons(2).zip(offsets).flat_map do |(m0, m1), offset| next m0 + offset * fraction, m1 + offset * fraction end.drop(1).each_slice(2).entries.prepend(nil) zip(controls).map do |point, controls| controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point end.join(" ") else map do |point| POINT % point end.join(" L ").tap do |string| string.concat(" Z") if closed? end.prepend("M ") end end
def trim(amount)
def trim(amount) return self unless amount > 0 ending, total = path_length - amount, 0 trimmed = each_cons(2).with_object [] do |(p0, p1), trimmed| delta = (p1 - p0).norm case when total >= ending then break trimmed when total <= amount - delta when total <= amount trimmed << (p0 * (delta + total - amount) + p1 * (amount - total)) / delta trimmed << (p0 * (delta + total - ending) + p1 * (ending - total)) / delta if total + delta >= ending else trimmed << p0 trimmed << (p0 * (delta + total - ending) + p1 * (ending - total)) / delta if total + delta >= ending end total += delta end LineString.new trimmed, @properties end