[back]

class Point < Struct.new(:x, :y)
  # Generates a random point around (0,0) with radius = 1
  def self.random
    radius = rand
    theta = 360 * rand
    Point.new(radius*Math.cos(theta), radius*Math.sin(theta))
  end

  # Returns true if x is close to y as defined by eps
  def self.close_to(x, y, eps = 1e-9)
    return (x-y).abs < eps
  end

  def self.rad2deg(a)
    return (a * 180.0) / Math::PI
  end

  def normal_left
    Point.new(self.y, -self.x)
  end

  # Returns the point at the center of this point
  # and the other_point
  def center(other_point)
    Point.new((self.x + other_point.x)/2.0, (self.y + other_point.y)/2.0)
  end

  # Computes the distance between self and other_point
  def distance(other_point)
    ((self.x - other_point.x) ** 2 + (self.y - other_point.y) ** 2) ** 0.5
  end

  def self.inf_line_intersection(base1, v1, base2, v2)
    if (Point.close_to(v1.x, 0.0) && Point.close_to(v2.y, 0.0))
      return Point.new(base1.x, base2.y)
    end
    if (Point.close_to(v1.y, 0.0) && Point.close_to(v2.x, 0.0))
      return Point.new(base2.x, base1.y)
    end

    m1 = !Point.close_to(v1.x, 0.0) ? v1.y / v1.x : 0.0
    m2 = !Point.close_to(v2.x, 0.0) ? v2.y / v2.x : 0.0

    if Point.close_to(m1, m2)
      return nil
    end

    c1 = base1.y - m1 * base1.x
    c2 = base2.y - m2 * base2.x
    ix = (c2 - c1) / (m1 - m2)
    iy = m1 * ix + c1
    if Point.close_to(v1.x, 0.0)
      return Point.new(base1.x, base1.x * m2 + c2)
    end
    if Point.close_to(v2.x, 0.0)
      return Point.new(base2.x, base2.x * m1 + c1)
    end
    return Point.new(ix, iy)
  end

  # compute the polar angle of this point
  def polar
    theta = 0.0
    if Point.close_to(self.x, 0)
      if self.y > 0
        theta = 90.0
      elsif self.y < 0
        theta = 270.0
      end
    else
      theta = Point.rad2deg(Math.atan(self.y/self.x))
      if self.x < 0.0
        theta += 180.0
      end
      if theta < 0.0
        theta += 360.0
      end
    end
    theta
  end

  # Computes the angle formed by p1 - self - p2
  def angle_between(p1, p2)
    vect_p1 = p1 - self
    vect_p2 = p2 - self

    theta = vect_p1.polar - vect_p2.polar
    theta += 360.0 if theta < 0.0
    theta = 360.0 - theta if theta > 180.0
    return theta
  end

  def ==(other_point)
    return Point.close_to(self.x, other_point.x) && Point.close_to(self.y, other_point.y)
  end

  # We define sort order of Points based on x coordinate, then y coordinate
  def <=>(other_point)
    ret = self.x <=> other_point.x
    if ret == 0
      ret = self.y <=> other_point.y
    end
    ret
  end

  # Subtract other_point from self
  def -(other_point)
    Point.new(self.x - other_point.x, self.y - other_point.y)
  end

  # Add other_point to self
  def +(other_point)
    Point.new(self.x + other_point.x, self.y + other_point.y)
  end

  # Scale this point by the provided factor
  def scale(factor)
    Point.new(self.x * factor, self.y * factor)
  end

  # Pretty print a point
  def to_s
    "(%5.4f, %5.4f)" % [x, y]
  end
end

class Circle < Struct.new(:center, :radius)
  # Returns true if this Circle contains the given Point
  def contains?(point)
    point.distance(self.center) < self.radius || (point.distance(self.center) - self.radius).abs < 1e-9
  end
  def to_s
    "{center:%s, radius:%5.4f}}" % [center.to_s, radius]
  end
end

require 'set'
class Solver < Struct.new(:mec)
  def initialize(debug)
    @points = SortedSet.new
    @debug = debug
  end

  # add a point to the solver and recalcluate the minimum enclosing
  # circle if the point lies outise the current MEC.
  def add_point(point)
    puts "adding p: #{point}" if @debug
    @points << point
    if self.mec && self.mec.center
      unless self.mec.contains?(point)
        iterate
      end
    else
      iterate
    end
  end

  # Tests if a point is left|on|right of a infinite line
  # return >0 for p2 left of line through p0 and p1
  # return = 0 for p2 on the line
  # return <0 for p2 right of the line
  def direction(p0, p1, p2)
    return (p0.x-p1.x)*(p2.y-p1.y) - (p2.x-p1.x)*(p0.y-p1.y)
  end

  # Computes the set of points that represents the
  # convex hull of the Solver's @points
  def convex_hull
    a = @points.to_a
    left = a.first
    a = a[1..-1]
    right = a.pop
    puts "left = #{left} right = #{right} a = #{a.map{|p| p.to_s}.join(", ")}" if @debug
    upper = []
    lower = []
    # Partition remaining points into upper and lower buckets
    a.each do |p|
      dir = direction(left, right, p)
      puts "p = #{p} dir = #{dir}" if @debug
      if dir < 0
        upper << p
      else
        lower << p
      end
    end
    puts "upper = #{upper.map{|p| p.to_s}.join(', ')}" if @debug
    puts "lower = #{lower.map{|p| p.to_s}.join(', ')}" if @debug
    upper_hull = half_hull(left, right, upper, -1)
    lower_hull = half_hull(left, right, lower, 1)
    puts "upper_hull = #{upper_hull.map{|p| p.to_s}.join(', ')}" if @debug
    puts "lower_hull = #{lower_hull.map{|p| p.to_s}.join(', ')}" if @debug
    (upper_hull + lower_hull).uniq
  end

  # Computs a half hull
  def half_hull(left, right, input, factor)
    input = input.dup
    input << right
    half = []
    half << left
    puts "input: #{input.map{|p| p.to_s}.join(', ')}" if @debug
    input.each do |p|
      half << p
      puts "half: #{half.map{|p| p.to_s}.join(', ')}" if @debug
      while half.size >= 3
        dir = factor * direction(half[-3], half[-1], half[-2])
        if dir <= 0
          half.delete_at(half.size-2)
        else
          break
        end
      end
    end
    half
  end

  # Recalculates the MEC after the addition of a new point
  def iterate
    self.mec ||= Circle.new
    # Handle degenerate cases first
    if @points.size == 1
      self.mec.center = @points.to_a[0]
      self.mec.radius = 0
    elsif @points.size == 2
      a = @points.to_a
      self.mec.center = a[0].center(a[1])
      self.mec.radius = a[0].distance(self.mec.center)
    else
      puts "points = #{@points.map{|p| p.to_s}.join(", ")}" if @debug
      a = convex_hull
      puts "convex hull = #{a.map{|p| p.to_s}.join(', ')}" if @debug
      point_a = a[0]
      point_c = a[1]
      while (true)
        point_b = nil
        best_theta = 180.0;
        for point in @points
          if point != point_a && point != point_c
            theta_abc = point.angle_between(point_a, point_c)
            puts "a: #{point_a} c: #{point_c} p: #{point} best_theta: #{best_theta} theta_abc: #{theta_abc}" if @debug
            if theta_abc < best_theta
              point_b = point
              best_theta = theta_abc
            end
          end
        end
        puts "Found best theta: #{best_theta}, a = #{point_a}, b = #{point_b}, c = #{point_c}" if @debug
        if best_theta >= 90.0 || point_b.nil?
          self.mec.center = point_a.center(point_c)
          self.mec.radius = point_a.distance(self.mec.center)
          return self.mec
        end
        ang_bca = point_c.angle_between(point_b, point_a)
        ang_cab = point_a.angle_between(point_c, point_b)
        puts "ang_bca = #{ang_bca}, ang_cab = #{ang_cab}" if @debug
        if ang_bca > 90.0
          point_c = point_b
        elsif ang_cab <= 90.0
          puts "breaking ..." if @debug
          break
        else
          point_a = point_b
        end
      end
      ch1 = (point_b - point_a).scale(0.5)
      ch2 = (point_c - point_a).scale(0.5)
      n1 = ch1.normal_left
      n2 = ch2.normal_left
      ch1 = point_a + ch1
      ch2 = point_a + ch2
      self.mec.center = Point.inf_line_intersection(ch1, n1, ch2, n2)
      self.mec.radius = self.mec.center.distance(point_a)
    end
    self.mec
  end
end

def distance(p1, p2)
  return p1.distance(p2)
end

# Use algorithm invented by Pr. Chrystal (1885)
# Port of code in a Java Applet found at
# http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/CG-Applets/Center/centercli.htm
# MEC Is determined by the Convex Hull of the set of points
# takes array of Point objects
# returns a Circle object
def encircle(points, debug = false)
  solver = Solver.new(debug)
  points.each { |p| solver.add_point(p) }
  puts "SOLVED: mec: #{solver.mec}" if debug
  return solver.mec
end

# Generate an array of random Point objects in the unit circle
# with center (0,0)
def generate_samples(n)
  (1..n).map { Point.random }
end

# Creates an image using rmagick showing the points and the MEC and
# saves it as filename
def draw(points, circle, filename)
  require 'rubygems'
  require 'RMagick'

  # Change these to control the output
  dim_x = 500                 # width of the box to draw the circle in
  dim_y = 500                 # height of the box to draw the circle in
  o_x = dim_x / 2             # x-origin on the graph
  o_y = dim_y / 2             # y-origin on the graph

  # Create a canvas and drawing context using rmagick
  canvas = Magick::Image.new(dim_x, dim_y, Magick::HatchFill.new('white', 'lightcyan2', dim_y / 20))
  gc = Magick::Draw.new
  gc.stroke_width(1)

  # Draw axes
  gc.stroke('black')
  gc.line(0, o_x, dim_y, o_x)
  gc.line(o_y, 0, o_y, dim_x)

  # Draw the MEC
  gc.stroke('red')
  gc.fill_opacity(0)
  cx = o_x + circle.center.x * dim_x / 2
  cy = o_y - circle.center.y * dim_y / 2
  cy_edge = cy - circle.radius * dim_y / 2
  gc.circle(cx, cy, cx, cy_edge)
  gc.fill_opacity(1)
  gc.fill('red')
  gc.circle(cx, cy, cx, cy+3)
  gc.fill_opacity(0)

  # Draw the MEC radius
  gc.stroke('green')
  cx_edge = cx - circle.radius * dim_x / 2 * (1 / (2 ** 0.5))
  cy_edge = cy + circle.radius * dim_y / 2 * (1 / (2 ** 0.5))
  gc.line(cx, cy, cx_edge, cy_edge)

  # Annotate the radius
  gc.stroke('darkgreen').fill('darkgreen')
  gc.text(cx_edge - 5, cy_edge + 5, "R:%5.4f" % circle.radius)

  # Annotate the center
  gc.stroke('darkgreen').fill('darkgreen')
  gc.text(cx + 5, cy, "(%3.2f, %3.2f)" % [circle.center.x, circle.center.y])

  # Draw the points
  points.each do |point|
    gc.stroke('blue')
    gc.fill('blue')
    gc.fill_opacity(1)
    px = o_x + point.x * dim_x / 2
    py = o_y - point.y * dim_y / 2
    gc.circle(px, py, px, py+3)
    # Annotate the point
    gc.stroke('black').fill('black')
    gc.text(px + 5, py, "(%2.2f, %2.2f)" % [point.x, point.y])
  end

  # draw the graphics context on the canvas and dump it to a file
  gc.draw(canvas)
  canvas.write(filename)
end

# For debugging, checks the solution and if it doesn't work, creates the image
# and reruns encircle in debug mode for the failed solution
def check_solution(points, circle)
  points.each do |point|
    if !circle.contains?(point)
      encircle(points, true)
      draw(points, circle, 'circle.png')
      puts "point #{point} (#{point.distance(circle.center)}) is outside of circle #{circle}"
      raise "BAD SOLUTION: point #{point} (#{point.distance(circle.center)}) is outside of circle #{circle}"
    end
  end
  return true
end

if __FILE__ == $0
  srand

  if ARGV[0] && ARGV[0] == 'draw'
    n = ARGV[1] ? ARGV[1].to_i : 15
    points = generate_samples(n)
    circle = encircle(points)
    draw(points, circle, 'circle.png')
    puts "SOLUTION: #{circle}"
  elsif ARGV[0] && ARGV[0] == 'benchmark'
    require 'benchmark'

    Benchmark.bm(15) do |x|
      5.times do |i|
        n = 10 ** i
        x.report("points: #{n}") do
          25.times do
            points = generate_samples(n)
            circle = encircle(points)
            check_solution(points, circle)
          end
        end
      end
    end
  elsif ARGV[0] && ARGV[0] == 'once'
    n = ARGV[1] ? ARGV[1].to_i : 15
    points = generate_samples(n)
    puts encircle(points)
  else
    puts "Usage: ruby mec.rb [draw [num_points]] | [benchmark] | [once [num_points]]"
    puts "     once: Runs the solution once and puts the circle"
    puts "        num_points defaults to 15 if not specified."
    puts "     draw: draws num_points points and the minimum enclosing circle to the file circle.png"
    puts "        num_points defaults to 15 if not specified."
    puts "benchmark: benchmarks the algorithm"
  end
end