# RubyLabs gem, top level module. SCRIPT_LINES__ = Hash.new unless defined? SCRIPT_LINES__ autoload :IntroLab, "introlab.rb" autoload :SieveLab, "sievelab.rb" autoload :IterationLab, "iterationlab.rb" autoload :RecursionLab, "recursionlab.rb" autoload :HashLab, "hashlab.rb" autoload :BitLab, "bitlab.rb" autoload :MARSLab, "marslab.rb" autoload :RandomLab, "randomlab.rb" autoload :EncryptionLab, "encryptionlab.rb" autoload :ElizaLab, "elizalab.rb" autoload :SphereLab, "spherelab.rb" autoload :TSPLab, "tsplab.rb" autoload :Demos, "demos.rb" include Math =begin rdoc == RubyLabs RubyLabs is a collection of modules for projects described in Explorations in Computing: An Introduction to Computer Science. There is one submodule for each project, e.g. RubyLabs::SieveLab has methods used in the Sieve of Eratosthenes project in Chapter 3. The top level RubyLabs module also has common methods used in several projects. =end module RubyLabs # A simple 'hello world' method to test the installation. def hello "Hello, you have successfully installed RubyLabs!" begin Canvas.init(200, 200, "RubyLabs::Hello") Canvas::Text.new("Hello from RubyLabs!",40,90) rescue Exception => e puts e puts "Did you install Tk, the software module used to display graphics? See your Lab Manual for installtion instructions..." end return nil end # Return the larger of +a+ and +b+ (determined by calling a < b). # # :call-seq: # max(a,b) => Object # def max(a,b) a < b ? b : a end # Return the smaller of +a+ and +b+ (determined by calling a < b). # # :call-seq: # min(a,b) => Object # def min(a,b) a < b ? a : b end # Compute the logarithm (base 2) of a number +x+. # # :call-seq: # log2(x) => Float # def log2(x) log(x) / log(2.0) end # Measure the execution time of a block of code, which can be any Ruby expression # enclosed in braces after the method name. The return value is the number of seconds # required to evaluate the block. # Any output produced by the block is discarded. # # Example: # >> time { sieve(1000) } # => 0.014921 # # :call-seq: # time { f } => Float # def time(&f) tstart = Time.now f.call return Time.now - tstart end # Monitor the execution of a block of code, which can be any Ruby expression enclosed in braces. Sets up # a callback method which checks to see if a probe has been attached to a line in # a method called by the block. See Source#probe for information about attaching # probes. If a probe is attached, the expression associated with # the probe is evaluated. The value returned from trace is the value returned # by the block. # # See also RubyLabs#count. # # Example -- call a method named +brackets+ to print brackets around array # elements as the arry is being sorted by a call to +isort+: # >> Source.probe( "isort", 5, "puts brackets(a,i)" ) # => true # >> trace { isort(cars) } # mazda [ford bmw saab chrysler] # ford mazda [bmw saab chrysler] # bmw ford mazda [saab chrysler] # ... # => ["bmw", "chrysler", "ford", "mazda", "saab"] # # :call-seq: # trace { f } => Object # #-- # Debugging aid: put this line at the front of the trace_func: # p [event, file, line, id, binding, classname] def trace(&f) set_trace_func proc { |event, file, line, id, binding, classname| if expr = Source.probing(file, id, line) eval(expr, binding) if expr != :count end } res = f.call set_trace_func nil res end # Monitor the execution of a block of code, which can be any Ruby expression enclosed in braces. # If a counting probe (see Source#probe) is attached to any lines of code evaluated by the block # this method will return the total number of times those lines are executed. # # See also RubyLabs#trace. # # Example -- count the number of times line 2 in the method named +less+ is executed # during a call to +isort+: # >> Source.probe("less", 2, :count) # => true # >> count { isort(cars) } # => 8 # # :call-seq: # count { f } => Fixnum # def count(&f) counter = 0 set_trace_func proc { |event, file, line, id, binding, classname| if expr = Source.probing(file, id, line) counter += 1 if expr == :count end } f.call set_trace_func nil return counter end =begin rdoc == TestArray A TestArray is an array of random values that can be used to test searching and sorting algorithms. A method named +random+ will return a random element to use as a search target. If +a+ is a TestArray object, call a.random(:success) to get a value that is in the array +a+, or call a.random(:fail) to get a number that is not in the array. #-- The constructor uses a hash to create unique numbers -- it draws random numbers and uses them as keys to insert into the hash, and returns when the hash has n items. The hash is saved so it can be reused by a call to random(:fail) -- this time draw random numbers until one is not a key in the hash. A lot of machinery to keep around for very few calls, but it's efficient enough -- making an array of 100K items takes less than a second. An earlier version used a method named test_array to make a regular Array object and augment it with the location method, but the singleton's methods were not passed on to copies made by a call to sort: >> x = test_array(3) => [16, 13, 4] >> x.sort.random(:fail) NoMethodError: undefined method `random' for [4, 13, 16]:Array =end class TestArray < Array data = File.join(File.dirname(__FILE__), '..', 'data', 'arrays') @@sources = { :cars => "#{data}/cars.txt", :colors => "#{data}/colors.txt", :elements => "#{data}/elements.txt", :fruits => "#{data}/fruit.txt", :fish => "#{data}/fish.txt", :languages => "#{data}/languages.txt", :words => "#{data}/wordlist.txt", } # Create a new TestArray of size +n+ containing items of the specified +type+. # If a type is not supplied, create an array of integers. Types are identified # by symbols, e.g. :cars tells the constructor to return an array of # car names (see TestArray.sources). If a type is specified, the symbol :all # can be given instead of an array size, in which case the constructor returns # all items of that type. # # Examples: # >> TestArray.new(5) # => [3, 28, 48, 64, 4] # >> TestArray.new(5, :cars) # => ["lamborghini", "lincoln", "chrysler", "toyota", "rolls-royce"] # >> TestArray.new(:all, :colors) # => ["almond", "antique white", ... "yellow green"] # # :call-seq: # TestArray.new(n, type) => Array # def initialize(size, src = nil) if src.nil? || src.class == Fixnum raise "TestArray: array size must be an integer" unless size.class == Fixnum if src.nil? @max = (size < 50) ? 100 : (10 * size) else @max = src raise "TestArray: max must be at least 2x larger than size" unless @max >= 2 * size end else raise "TestArray: array size must be an integer or :all" unless size.class == Fixnum || size == :all end @h = Hash.new # if @max is defined make an array of integers, otherwise src defines the type of data; # size might be :all, in which case return the whole file, and set @all to true so random # doesn't try to make a random value not in the array. if @max while @h.size < size @h[ rand( @max ) ] = 1 end else fn = @@sources[src] or raise "TestArray: undefined source: #{src}" @words = File.open(fn).readlines if size != :all max = @words.length raise "TestArray: size must be less than #{max} for an array of #{src}" unless size < max while @h.size < size @h[ @words[ rand(max) ].chomp ] = 1 end end end if size == :all self.concat @words.map { |s| s.chomp! } @all = true else self.concat @h.keys for i in 0..length-2 r = rand(length-i) + i # i <= r < length self[i],self[r] = self[r],self[i] end end end # Return a value that is guaranteed to be in the array or not in the array, # depending on the value of +outcome+. Pass :success to get a random # value in the array, or pass :fail to get an item of the same type as the # items in the array but which is not itself in the array. Call a.random(:fail) # to get a value that will cause a search algorithm to do the maximum number of comparisons. # # Example: # >> a = TestArray.new(10).sort # => [13, 23, 24, 26, 47, 49, 86, 88, 92, 95] # >> x = a.random(:fail) # => 22 # >> search(a, x) # => nil # # :call-seq: # a.random(outcome) => Object # def random(outcome) if outcome == :success return self[ rand(self.length) ] elsif outcome == :fail raise "TestArray#random: array is universal set" if @all loop do if @max x = rand( @max ) else x = @words[ rand( @words.length ) ].chomp end return x if @h[x] == nil end else return nil end end # Return a list of types of items that can be passed as arguments # to TestArray.new # # :call-seq: # TestArray.sources() => Array # def TestArray.sources return @@sources.keys.sort { |a,b| a.to_s <=> b.to_s } end end # class TestArray # Equivalent to calling TestArray.new. # # Examples: # >> TestArray(5) # => [65, 79, 60, 88, 30] # >> TestArray(5, :cars) # => ["mini", "opel", "chevrolet", "isuzu", "cadillac"] # # :call-seq: # TestArray(n, type) => Array # def TestArray(n, type = nil) TestArray.new(n, type) end =begin rdoc == Source The Source module provides access the source code for a lab project. When IRB reads the modules from a file, the source is saved in a global array named SCRIPT_LINES__. The methods defined in this module scan the source code to look for tags that mark the first and last lines of methods described in the book. A method name can be passed either as a String or a Symbol. For example, to print a listing of the +isort+ method a user can call Source.listing("isort") or Source.listing(:isort) #-- Code that will be accessed by methods in this module should be delimited by :begin and :end tags. See the definition of isort in iterationlab.rb for an example of how to name a method and its helper methods. =end module Source @@probes = Hash.new @@file = Hash.new @@size = Hash.new @@base = Hash.new @@helpers = Hash.new @@line = nil # Print a listing (source code along with line numbers) for method +name+. # # :call-seq: # Source.listing(name) # def Source.listing(name) begin id = Source.find(name) for i in @@base[id]..(@@base[id]+@@size[id]-1) line = SCRIPT_LINES__[@@file[id]][i-1].chomp printf "%3d: %s\n", i - @@base[id] + 1, line.gsub(/\t/," ") end rescue Exception => e puts e end return true end # Save a copy of the source code for method +name+ in a file. If a file name is not # specified, the output file name is the name of the method with ".rb" appended. # Prompts the user before overwriting an existing file. # # Example -- Write the source for the method +isort+ to "isort.rb": # Source.checkout("isort") # # Example -- Write the source for +isort+ to "mysort.rb" # Source.checkout("isort", "mysort.rb") # # :call-seq: # Source.checkout(name) # def Source.checkout(name, newfilename = nil) begin id = Source.find(name) if newfilename.nil? newfilename = id.to_s end if newfilename[-1] == ?? || newfilename[1] == ?! newfilename.chop! end if newfilename !~ /\.rb$/ newfilename += ".rb" end if File.exists?(newfilename) print "Replace existing #{newfilename}? [yn] " if STDIN.gets[0] != ?y puts "File not written" return false end end File.open(newfilename, "w") do |f| f.puts "# #{name} method exported from #{File.basename(@@file[id])}" f.puts Source.print_source(f, id) @@helpers[id].each do |m| f.puts xid = Source.find(m) Source.print_source(f, xid) end end rescue Exception => e puts e end puts "Saved a copy of source in #{newfilename}" return true end # Helper method called from Source.checkout -- print the code for # method +id+ in the file +f+ def Source.print_source(f, id) # :nodoc: for i in @@base[id]..(@@base[id]+@@size[id]-1) line = SCRIPT_LINES__[@@file[id]][i-1].chomp f.puts line.gsub(/\t/," ") end end # Attach a software probe to a line in method +name+. The line can be specified # by line number relative to the start of the method or a string the specifies a pattern. The third argument # is either a string or the symbol :count. # If the third argument is a string, it should be a Ruby expression that will be # evaluated whenever the probe is activated via a call to +trace+ (see RubyLabs#trace). # If the third argument is :count, a call to +count+ (see RubyLabs#count) # will count the number of times the specified line is executed. # # Example: attach a probe to the +sieve+ method, so that when +sieve+ is called # via +trace+ the statement "p worklist" will be evaluated whenever line 6 is executed: # >> Source.probe("sieve", 6, "p worksheet") # => true # # Example: attach a counting probe, so +count+ can record how many times line 6 is executed: # >> Source.probe("sieve", 6, :count) # => true # # If the second argument to Source.probe is a string, the probe is attached to every line that # contains that string. For example, to attach a counting probe to every line in +merge+ that # has a call to the << operator: # >> Source.probe("merge", '<<', :count) # => true # # :call-seq: # Source.probe(name, line, spec) # def Source.probe(name, spec, expr = :count) begin id = Source.find(name) Source.lines(spec, id).each do |n| n += @@base[id] - 1 @@probes[id][n] = expr end rescue Exception => e puts e end return true end # -- Method for internal use only -- # Return probes (if any) attached to the specified file, method, and line number. # Intended to be called from a trace func callback (which is why the file is one # of the parameters). def Source.probing(filename, method, line) # :nodoc: return nil if line == @@line @@line = line return nil unless @@probes[method] && @@file[method] == filename return @@probes[method][line] end # Print a description of all the currently defined probes. # # :call-seq: # Source.probes() # def Source.probes @@probes.each do |name, plist| plist.each do |line, exprs| n = line - @@base[name] + 1 printf "%s %2d: %s\n", name, n, exprs end end return true end # Remove all the probes on a designated method, or if no method name is passed, # remove all probes from all methods. # # :call-seq: # Source.clear(name) # def Source.clear(name = nil) @@probes.each do |id, plist| next if ! name.nil? && id != name plist.clear end return true end # Internal use only -- locate the filename, starting line number, and length # of method +name+, record the # information for any methods that need it. This information only needs to # be found once, so it is recorded in a set of class variables. Revisit this # decision if monitoring user-defined methods.... def Source.find(name) # :nodoc: id = name.to_sym return id if @@file[id] # return if we looked for this source previously filename, base, size, helpers = nil, nil, nil, nil catch (:found) do SCRIPT_LINES__.each do |file, lines| line_num = 0 lines.each do |s| line_num += 1 if match = s.match(/:(begin|end)\s+(.*)/) verb = match[1] names = match[2].split.collect{|x| eval(x)} if names[0] == id if verb == "begin" filename = file base = line_num + 1 helpers = names[1..-1] else size = line_num - base throw :found end end end end end end raise "Can't find method named '#{name}'" if size.nil? @@file[id] = filename @@size[id] = size @@base[id] = base @@probes[id] = Hash.new @@helpers[id] = helpers return id end # Internal use only -- make an array of line numbers to use for probing method +name+. # Argument can be a single line number, an array of line numbers, or a pattern. Checks # to make sure line numbers are valid. def Source.lines(spec, id) # :nodoc: if spec.class == Fixnum && Source.range_check(spec, id) return [spec] elsif spec.class == Array res = Array.new spec.each do |line| raise "line number must be an integer" unless line.class == Fixnum res << line if Source.range_check(line, id) end return res elsif spec.class == String res = Array.new for i in @@base[id]..(@@base[id]+@@size[id]-1) line = SCRIPT_LINES__[@@file[id]][i-1].chomp res << i - @@base[id] + 1 if line.index(spec) end return res else raise "invalid spec: '#{spec}' (must be an integer, array of integers, or a pattern)" end end def Source.range_check(n, id) # :nodoc: max = @@size[id] raise "line number must be between 1 and #{max}" unless n >= 1 && n <= max return true end # Internal use only -- show info about method to verify it's being found by Source.lines def Source.info(name) # :nodoc: unless id = Source.find(name) puts "Can't find method named '#{name}'" return end printf "file: %s\n", @@file[id] printf "size: %d\n", @@size[id] printf "base: %d\n", @@base[id] printf "helpers: %s\n", @@helpers[id].inspect printf "probes: %s\n", @@probes[id].inspect end end # Source =begin rdoc == Canvas The Canvas module defines a graphics window that can be used for interactive visualizations. Classes in this module describe objects that are drawn in the window; for example, objects of type Canvas::Circle are circles on the canvas. In the current implementation all drawing objects are derived from a base class named TkObject, which provides an interface to the Tk library of ActiveTcl. Instances of TkObject are proxies for the actual objects defined in Tk. When the user calls a method that opens the RubyLabs Canvas, the method initializes a pipe to a new process running the Tcl shell (wish). If a TkObject is updated, it sends a Tcl command over the pipe, and the wish process updates the window. =end module Canvas =begin rdoc == TkObject Base class of all objects defined for RubyLabs::Canvas. Objects derived from this class are proxies for Tk objects in a wish shell, opened when the canvas is initialized. Applications should not try to instantiate a TkObject directly, but instead should call a constructor of one of the derived classes. For example, to draw a circle on the canvas: c = Canvas::Circle.new(x, y, r) Public instance methods defined here are inherited by all objects derived from TkObject, and can be used to manipulate the object. For example, to change the color of the circle: c.fill = 'green' The current implementation is very sparse: the only operations defined are the ones needs for the visualizations described in the textbook, which are just the basic methods that create an object or move it around on the screen. In addition to the usual attributes, these objects have a "pen point", which is the location of an imaginary pen relative to the object's base coordinate. The pen point is used by methods that draw a track as an object is moved (e.g. see the methods that control the motion of the robot in SphereLab) =end class TkObject attr_accessor :name, :coords, :penpoint # Initialization: set the object ID to 0 and save the file descriptor # for the connection to the wish shell. # # :call-seq: # TkObject.reset(p) # def TkObject.reset(p) @@pipe = p @@id = 0 end # Return a unique ID number for a new proxy object. # # :call-seq: # TkObject.nextId() # def TkObject.nextId @@id += 1 return "obj" + @@id.to_s end # Translate a Ruby hash +h+ into a Tk option string. Example: # >> Canvas::TkObject.options( { :outline => :black, :fill => :red } ) # => "-fill red -outline black" # Note the ordering of the options in the Tk string may not be the same as # the order they are listed when the hash object is created. # # :call-seq: # TkObject.options(h) => String # def TkObject.options(h) a = [] h.each do |k,v| a << "-#{k} #{v}" end return a.join(" ") end # Bring an object to the foreground (on top of all other objects on the canvas). # # :call-seq: # x.raise() # def raise @@pipe.puts ".canvas raise $#{@name}" end # Put an object in the background (below all other objects on the canvas). # # :call-seq: # x.lower() # def lower @@pipe.puts ".canvas lower $#{@name}" end # Remove an object from the canvas. # # :call-seq: # x.erase() # def erase @@pipe.puts ".canvas delete $#{@name}" end # Assign new coordinates for an object. Can be used to move or resize an # object, but applications should use the more abstract interface defined by class methods in Canvas # (e.g. Canvas#move). # # Example: if an object x is a proxy for a rectangle # that has an upper left corner at (10,10) and # a lower right corner at (20,50), this call will move it down and to the right # by 10 pixels: # >> x.coords = [20, 20, 30, 60] # => [20, 20, 30, 60] # # :call-seq: # x.coords = [...] # def coords=(a) @coords = a @@pipe.puts ".canvas coords $#{@name} #{a.join(' ')}" end # Set the fill color for an object. Example: # >> x.fill = "green" # => "green" # # :call-seq: # x.fill = String # def fill=(x) @@pipe.puts ".canvas itemconfigure $#{name} -fill #{x}" end end =begin rdoc == Line A Line object is a proxy for a line segment defined by a pair of (x,y) coordinates. There are no instance methods for Lines beyond those defined in the TkObject base class. =end class Line < TkObject # Create a new line segment that runs from (x0,y0) to (x1,y1). Attributes # of the line can be passed in a hash object at the end of the argument list. # # Example -- create a gray line one pixel wide from (0,0) to (100,100): # >> z = Line.new(0, 0, 100, 100, :fill => :gray, :width => 1) # => # # # :call-seq: # x = Line.new(x0, y0, x1, y1, args = {}) # def initialize(x0, y0, x1, y1, args = {}) raise "No canvas" unless @@pipe @name = TkObject.nextId @coords = [ x0, y0, x1, y1 ] @penpoint = nil cmnd = "set #{@name} [.canvas create line #{@coords.join(" ")} #{TkObject.options(args)}]" @@pipe.puts cmnd end # Erase all Line objects that are tagged with the ID +tag+. Tags are optional # arguments defined by passing -tag x to Line.new (see the # visualization methods in tsplab.rb, which uses tags to erase all # edges from a graph in a single call). # # :call-seq: # Line.erase_all(tag) # def Line.erase_all(tag) @@pipe.puts ".canvas delete withtag #{tag}" end end =begin rdoc == Circle A Circle object is a proxy for a Tk oval. There are no instance methods for Circles beyond those defined in the TkObject base class. =end class Circle < TkObject # Create a new circle with center at (x,y) and radius r. Attributes # of the circle can be passed in a hash object at the end of the argument list. # # Example -- create a green cicle with radius 5 at location (20,20): # >> x = Canvas::Circle.new(20, 20, 5, :fill => "green") # => # # # :call-seq: # x = Circle.new(x, y, r, args = {}) # def initialize(x, y, r, args = {}) raise "No canvas" unless @@pipe @name = TkObject.nextId @coords = [ x-r, y-r, x+r, y+r ] @penpoint = [ r, r ] cmnd = "set #{@name} [.canvas create oval #{@coords.join(" ")} #{TkObject.options(args)}]" @@pipe.puts cmnd end end =begin rdoc == Rectangle A Rectangle object is a proxy for a Tk rectangle. There are no instance methods for Rectangles beyond those defined in the TkObject base class. =end class Rectangle < TkObject # Create a new rectangle with its upper left corner at (x0,y0) # lower right corner at (x1,y1). Attributes # of the rectangle can be passed in a hash object at the end of the argument list. # # Example -- create a 20x20 square with upper left corner at (10,10), with a dark green # outline and yellow interior: # >> x = Canvas::Rectangle.new(10, 10, 30, 30, :fill => "yellow", :outline => "darkgreen") # => # # # :call-seq: # x = Rectangle.new(x0, y0, x1, y1, args = {}) # def initialize(x0, y0, x1, y1, args = {}) raise "No canvas" unless @@pipe @name = TkObject.nextId @coords = [ x0, y0, x1, y1 ] @penpoint = [ (x1-x0)/2, (y1-y0)/2 ] cmnd = "set #{@name} [.canvas create rectangle #{@coords.join(" ")} #{TkObject.options(args)}]" @@pipe.puts cmnd end end =begin rdoc == Polygon A Polygon object is a proxy for a Tk polygon. In addition to the methods defined in the TkObject base class, a method named +rotate+ will rotate a Polygon by a specified angle. =end class Polygon < TkObject # Create a new polygon. The first argument is an array of vertex coordinates. If # the polygon has +n+ vertices, the argument should be a flat array of 2*n points. # Attributes of the polygon can be passed in a hash object at the end of the argument list. # # Example -- create a small triangle in the upper left corner of the canvas, with a black # outline and green interior: # >> x = Canvas::Polygon.new([10,10,20,20,30,10], :fill => "green", :outline => "black") # => # # # :call-seq: # x = Polygon.new(a, args = {}) # def initialize(a, args = {}) raise "No canvas" unless @@pipe @name = TkObject.nextId @coords = a @penpoint = [0,0] cmnd = "set #{@name} [.canvas create polygon #{@coords.join(" ")} #{TkObject.options(args)}]" @@pipe.puts cmnd end # Rotate the polygon by an angle +theta+ (expressed in degrees). The object is # rotated about the point defined by the first pair of (x,y) coordinates. The # return value is an array with the new coordinates. # # Example: Suppose +x+ is a square with coordinates (10,10), (20,10), (20,20), and (10,20). # This call will rotate it clockwise by 45 degrees and return the new vertices: # >> x.rotate(45) # => [10.0, 10.0, 17.07, 17.07, 10.0, 24.14, 2.93, 17.07] def rotate(theta) theta = Canvas.radians(theta) a = self.coords x0 = a[0] y0 = a[1] (0...a.length).step(2) do |i| x = a[i] - x0 y = a[i+1] - y0 a[i] = x0 + x * cos(theta) - y * sin(theta) a[i+1] = y0 + x * sin(theta) + y * cos(theta) end self.coords = a return a end end =begin rdoc == Text A Text object is a string displayed on the RubyLabs canvas. =end class Text < TkObject # Display string +s+ at location (+x+,+y+) on the canvas. By default the coordinates # specify the location of the top left of the first character in the string (in Tk # terminology, the anchor point for the text is "northwest"). A different anchor # position and other attributes of the text can be passed in a hash object at the # end of the argument list. # # Example -- display a message in dark green letters: # >> x = Canvas::Text.new("hello", 50, 50, :fill => 'darkgreen') # => # # # :call-seq: # x = Text.new(s, x, y, args = {}) # def initialize(s, x, y, args = {}) raise "No canvas" unless @@pipe @name = TkObject.nextId @coords = [ x, y ] @penpoint = nil args[:anchor] = :nw unless args.has_key?(:anchor) args[:text] = "\"#{s}\"" cmnd = "set #{@name} [.canvas create text #{@coords.join(" ")} #{TkObject.options(args)}]" @@pipe.puts cmnd end # Replace the string displayed by a text object. Example: erase the current string # for object +x+ and replace it with "hello, world": # >> x.update("hello, world") # => nil # The new string is displayed at the same location and with the same attributes # as the old string. def update(s) @@pipe.puts ".canvas itemconfigure $#{name} -text \"#{s}\"" end end =begin rdoc == Font A Font object defines the appearance of strings displayed by a Text object. A Font object is a proxy for a Tk font object. The proxy is never used by an application; instead, when text is created, it is passed the name of the Tk font (see the example in the description of Font.new). =end class Font < TkObject @@fonts = [] # Create a font named +name+ with attributes defined in +args+. When creating # a new Text object, +name+ can be passed as an argument to Text.new so the # string will be displayed in the specified font. # # Example -- create a new font for displaying fixed width text, and then display # a message using this font: # >> f = Canvas::Font.new('code', :family => 'Courier', :size => 20) # => # # >> t = Canvas::Text.new("Go Ducks!", 100, 100, :font => 'code') # => # # # :call-seq: # x = Font.new(name,args) # def initialize(name, args) @name = name if @@fonts.index(name).nil? cmnd = "font create #{name} #{TkObject.options(args)}" @@pipe.puts cmnd @@fonts << name end end end # Initialize a drawing canvas with the specified width and height. If this is the first # call in an IRB session, open a connection to a wish shell and send it Tcl commands to # make a window with a single widget, a canvas centered in the middle. The +title+ argument # passed to Canvas.init becomes part of the new window. # # If this is not the first call to Canvas.init, the existing Tk canvas is resized according # to the new width and height arguments and the window is renamed using the new name. # # An optional fourth argument can be the symbol :debug, in which case the return # value is the Ruby Pipe object used to communicate with Tk. The Pipe can be useful for # developing new TkObject objects, since it can be used to see how Tcl commands are # processed. Example: # # >> p = Canvas.init(200, 200, "Test", :debug) # => # # >> p.puts ".canvas create text 30 30 -text hello" # => nil # # :call-seq: # Canvas.init(width, height, title, opts = nil) # #-- # TODO Read the path to wish from a configuration file, so users can alter it # TODO there are probably other configuration options that can go there, too # TODO use popen3 on OSX, capture stderr def Canvas.init(width, height, title, *opts) @@title = "RubyLabs::#{title}" @@width = width @@height = height pad = 12 if @@tkpipe.nil? if Canvas.OS == "Windows" @@tkpipe = IO.popen("wish", "w") elsif Canvas.OS == "Linux" @@tkpipe = IO.popen("/opt/ActiveTcl-8.5/bin/wish", "w") else @@tkpipe = IO.popen("/usr/local/bin/wish", "w") end at_exit { Canvas.close } @@tkpipe.puts "tk::canvas .canvas -bg white -width #{width} -height #{height}" @@tkpipe.puts ". configure -bg gray" @@tkpipe.puts "pack .canvas -padx #{pad} -pady #{pad}" TkObject.reset(@@tkpipe) else @@tkpipe.puts ".canvas delete all" @@tkpipe.puts ".canvas configure -width #{width} -height #{height}" end @@tkpipe.puts "wm geometry . #{width+2*pad}x#{height+2*pad}+30+50" @@tkpipe.puts "wm title . #{@@title}" return opts[0] == :debug ? @@tkpipe : true end # Send an +exit+ command to the wish shell, which closes the drawing window and # terminates the shell. # # :call-seq: # Canvas.close() # def Canvas.close if @@tkpipe @@tkpipe.puts "exit" @@tkpipe = nil TkObject.reset(nil) end end # Return the current width of the canvas. # # :call-seq: # Canvas.width() => Fixnum # def Canvas.width @@width end # Return the current height of the canvas. # # :call-seq: # Canvas.height() => Fixnum # def Canvas.height @@height end # Return a reference to the Pipe object used to communicate with the wish shell. # # :call-seq: # Canvas.pipe() => IO # def Canvas.pipe @@tkpipe end #-- # Idea for the future, abandoned for now: allow applications to define a coordinate # system, e.g. cartesian with the origin in the middle of the canvas, and map coords # pass to line, rectangle, etc from user-specified coordinates to Tk coordinates. # Lots of complications, though -- e.g. if an application calls the coords method to # get object coordinates (e.g. RandomLab#add_tick gets y coordinate of number line) # it will have to map back from Tk to the selected mapping..... # def Canvas.origin(option) # case option # when :tk # @@map = lambda { |x,y| return x, y } # when :quadrant # @@map = lambda { |x,y| return x, @@height - y } # when :cartesian # @@map = lambda { |x,y| return x + @@width/2, @@height/2 - y } # end # end # # def Canvas.map(a) # (0...a.length).step(2) do |i| # a[i], a[i+1] = @@map.call( a[i], a[i+1] ) # end # end #++ # Return a string that uses the RUBY_PLATFORM environment variable to # determine the host operating system type. Possible return values are "Mac OS X", # "Linux", or "Windows". # # :call-seq: # Canvas.OS() => String # #-- # TODO: find out what the platform string is for the legacy windows one-click installer; # the new installer uses the "mingw" compiler. def Canvas.OS if RUBY_PLATFORM =~ %r{darwin} return "Mac OS X" elsif RUBY_PLATFORM =~ %r{linux} return "Linux" elsif RUBY_PLATFORM =~ %r{mingw} return "Windows" else return "Unknown" end end # Move an object by a distance +dx+ vertically and a distance +dy+ horizontally. # If the fourth argument is the symbol :track a line segment is drawn # connecting the pen position of the object's previous location with the # pen position of its new location. The return value is an array with the new # coordinates. # # Example: Suppose +x+ is a Polygon with coordinates (10,10), (20,20), and (30,10). # This call moves it down and to the right by 10 pixels and returns the new location: # >> Canvas.move(x, 10, 10) # => [20, 20, 30, 30, 40, 20] # # :call-seq: # Canvas.move(obj, dx, dy, track = nil) # def Canvas.move(obj, dx, dy, option = nil) a = obj.coords if option == :track x0 = a[0] + obj.penpoint[0] y0 = a[1] + obj.penpoint[1] end (0...a.length).step(2) do |i| a[i] += dx a[i+1] += dy end obj.coords = a if option == :track x1 = a[0] + obj.penpoint[0] y1 = a[1] + obj.penpoint[1] Canvas::Line.new( x0, y0, x1, y1, :width => 1, :fill => '#777777' ) obj.raise end return a end # Convert an angle from degrees to radians. Example: # >> Math::PI / 4 # => 0.785398163397448 # >> Canvas.radians(45) # => 0.785398163397448 # # :call-seq: # Canvas.radians(deg) => Float # def Canvas.radians(deg) deg * Math::PI / 180 end # Convert an angle from radians to degrees. Example: # >> Canvas.degrees( Math::PI / 2 ) # => 90.0 # # :call-seq: # Canvas.degrees(rad) => Float # def Canvas.degrees(rad) 180 * rad / Math::PI end # Make a range of colors starting from +first+ and going to +last+ in +n+ steps. # Color arguments should be be 3-tuples of integer RGB values. The # result is an array that starts with +first+, has +n+-1 intermediate colors, # and ends with +last+. # # Example: # >> Canvas.palette( [255,0,0], [0,0,0], 10) # => ["#FF0000", "#E60000", "#CD0000", ... "#1E0000", "#000000"] # The return value is an array of 11 colors starting with red and ending with black. # # :call-seq: # Canvas.palette(first, last, n) => Array # def Canvas.palette(first, last, n) d = Array.new(3) 3.times { |i| d[i] = (first[i] - last[i]) / n } a = [first] (n-1).times do |i| a << a.last.clone 3.times { |j| a.last[j] -= d[j] } end a << last a.map { |c| sprintf("#%02X%02X%02X",c[0],c[1],c[2]) } end @@tkpipe = nil @@title = "" @@height = 0 @@width = 0 end # Canvas =begin rdoc == PriorityQueue A PriorityQueue is a collection of objects that is always maintained in order. Any kind of object can be in the queue as long as it can be compared with the < operator. More precisely, if an object +x+ responds to the < operator, and +x+ < +y+ is defined for every object +y+ already in the queue, then +x+ can be inserted. The methods that insert and remove items check to see if an instance variable named @on_canvas is +true+. If so, a method named +update+ is called. +update+ is not defined here, but is added to the class by modules that use the queue during visualizations (see the definition of the priority queue used in the Huffman tree project in bitlab.rb for an example). Several methods of the Array class are defined dynamically in the PriorityQueue class when this module is loaded. These methods have the same meaning for PriorityQueue objects as they do for Array objects: +length+:: return the number of items in the queue +first+:: return a reference to the first item in the queue +last+:: return a reference to the last item in the queue +to_s+:: generate a String representation of the queue (by calling Array#to_s) +inspect+:: generate a String representation of the queue (by calling Array#inspect) +clear+:: remove all items from the queue empty?:: return +true+ if the queue has no items =end class PriorityQueue # Create a new, initially empty, priority queue. def initialize @q = Array.new end # Insert an item into the queue. The item's location is determined by how # it compares with other items, according to the < operator. Specifically, # when item +x+ is added to a queue, it it put before the first item +y+ # where +x+ < +y+. # # Example: suppose object +q+ has three strings: # >> q # => ["Au", "He", "O"] # This expression adds the string "Fe" to the queue: # >> q << "Fe" # => ["Au", "Fe", "He", "O"] # The new string went into the second position because "Fe" < "Au" is false but "Fe" < "He" is true. def <<(obj) raise "Object cannot be inserted into priority queue" unless obj.respond_to?(:<) i = 0 while (i < @q.length) break if obj < @q[i] i += 1 end @q.insert(i, obj) update(:insert, obj) if @on_canvas return @q end # Remove the first item from the queue, returning a reference to that item. # # Example: suppose object +q+ has three strings: # >> q # => ["Au", "He", "O"] # Then a call to shift removes the string "Au" and leaves the remaining items in order in the queue: # >> x = q.shift # => "Au" # >> q # => ["He", "O"] # def shift res = @q.shift update(:shift, res) if @on_canvas return res end # Return the item at location +i+ in the queue. *Note*: unlike Array objects, an # index expression cannot be used on the left side of an assignment. If # +q+ is a PriorityQueue object, # x = q[i] # is valid, but # q[i] = x # is undefined. def [](i) @q[i] end # Call +f+ for every item in the queue, and return an array with # the result of each call (essentially the same as the +map+ operation # defined for Ruby's Enumeration interface). # # Example: # >> q # => ["avocado", "boysenberry", "clementine", "elderberry", "loquat"] # >> q.collect { |x| x.length } # => [7, 11, 10, 10, 6] def collect(&f) @q.map { |x| f.call(x) } end # Evaluate block +b+ for every item in the queue (equivalent to Array#each) # # Example: # >> q # => ["Au", "He", "O"] # >> q.each { |x| puts x } # Ar # He # O # => ["Au", "He", "O"] def each(&b) @q.each &b end # Evaluate block +b+ for every item in the queue, also passing the item's # location in the queue to the block (equivalent to Array#each_with_index) # # Example: # >> q # => ["Au", "He", "O"] # >> q.each_with_index { |x,i| puts "#{i}: #{x}" } # 0: Ar # 1: He # 2: O # => ["Au", "He", "O"] def each_with_index(&b) @q.each_with_index &b end %w{length first last to_s inspect clear empty?}.each do |name| eval "def #{name}() @q.#{name} end" end end # PriorityQueue end # RubyLabs =begin rdoc == Fixnum When the RubyLabs module is loaded it defines a new method named +ord+ to the Fixnum class. In Ruby 1.8, using the [] operator to access items in a String object returns the ASCII value of a character. The +ord+ method defined here (and used by hash functions defined in hashlab.rb) maps the ASCII value of a letter to a number between 0 and 25. The BitLab module also extends Fixnum by defining a method named +code+ that returns a Code object containing the binary or hexadecimal representation of an integer. #-- NOTE: +ord+ is built in to Ruby 1.9, so this method will have to be renamed or reimplemented when RubyLabs is ported to 1.9. =end class Fixnum # If a number is the ASCII code for a letter from the Roman alphabet (upper or lower case, # in the range 'A' to 'Z') map it to a number between 0 and 25, otherwise just return the # value of the number. # # Example: # >> "Ducks!".each_byte { |x| puts x.ord } # 3 # 20 # 2 # 10 # 18 # 33 def ord if self >= ?a && self <= ?z self - ?a elsif self >= ?A && self <= ?Z self - ?A else self end end end # Fixnum include RubyLabs