#!/usr/pkg/bin/ruby27

# wxRuby2 Sample Code. Copyright (c) 2004-2009 wxRuby development team
# Freely reusable code: see SAMPLES-LICENSE.TXT for details

require 'wx'
include Wx
include Math

# This sample was originally written by Alex Fenton as an answer to Ruby
# Quiz #191, which challenged entrants to create an application which
# could draw images based on mathematical functions:
#
# http://rubyquiz.strd6.com/quizzes/191/
#
# To use the application, enter functions which take input values of x
# and y from 0 to 1, and return intensities of red, green and blue. If a
# bad function is entered, a cross is displayed; hover over this to get
# a hint on the problem.
#
# The sample demonstrates some uses of the Wx::Image class, a
# platform-independent representation of an image which can be
# manipulated (for example, resizing) and written to files in various
# formats. It also shows how an image's rgb data can be written directly, by
# using Array#pack.

# A canvas that draws and displays a mathematically generated image
class MathsDrawing < Window
  # The functions which return the colour components at each pixel
  attr_writer :red, :green, :blue
  # The time taken to render, whether re-rendering is needed, and the
  # source image
  attr_reader :render_time, :done, :img
  
  def initialize(parent)
    super(parent)
    # Create a dummy image 
    @default_image = Image.new(1, 1)
    @default_image.rgb_data = [255, 255, 255].pack('CCC')
    @img = @default_image

    @red   = lambda { | x, y | 1 }
    @green = lambda { | x, y | 1 }
    @blue  = lambda { | x, y | 1 }

    @done = true

    evt_size :on_size
    evt_paint :on_paint
    evt_idle :on_idle
  end

  # Paint the image on the screen. The actual image rendering is done in
  # idle time, so that the GUI is responsive whilst redrawing - eg, when
  # resized. Painting is done by quickly rescaling the cached image.
  def on_paint
    paint do | dc |
      draw_img = @img.scale(client_size.x, client_size.y)
      dc.draw_bitmap(draw_img.convert_to_bitmap, 0, 0, true)
    end
  end

  # Regenerate the image if needed, then do a refresh
  def on_idle
    if not @done
      @img = make_image
      refresh
    end
    @done = true
  end

  # Note to regenerate the image if the canvas has been resized
  def on_size(event)
    @done = false
    event.skip
  end

  # Call this to force a re-render - eg if the functions have changed
  def redraw
    @done = false
  end

  # Actually make the image
  def make_image
    size_x, size_y = client_size.x, client_size.y
    if size_x < 1 or size_y < 1
      return @default_image
    end

    start_time = Time.now
    # The string holding raw rgb data
    data = ''
    x_factor = size_x.to_f
    y_factor = size_y.to_f

    # Input values from the range 0 to 1, with origin in the bottom left
    (size_y - 1).downto(0) do | y |
      the_y = y.to_f / y_factor
      0.upto(size_x - 1) do | x |
        the_x = x.to_f / x_factor
        red   = @red.call(the_x, the_y) * 255
        green = @green.call(the_x, the_y) * 255
        blue  = @blue.call(the_x, the_y) * 255
        data << [red, green, blue].pack("CCC")
      end
    end
    img = Image.new(size_x, size_y)
    img.rgb_data = data
    @render_time = Time.now - start_time
    img
  end
end

# A helper dialog for saving the image to a file
class SaveImageDialog < FileDialog
  # The image file formats on offer
  TYPES = [ [ "PNG file (*.png)|*.png", BITMAP_TYPE_PNG ],
            [ "TIF file (*.tif)|*.tif", BITMAP_TYPE_TIF ],
            [ "BMP file (*.bmp)|*.bmp", BITMAP_TYPE_BMP ] ]
  
  WILDCARD = TYPES.map { | type | type.first }.join("|")
  
  def initialize(parent)
    super(parent, :wildcard => WILDCARD,
                  :message => 'Save Image',
                  :style => FD_SAVE|FD_OVERWRITE_PROMPT)
  end

  # Returns the Wx identifier for the selected image type. 
  def image_type
    TYPES[filter_index].last
  end
end

# A Panel for displaying the image and controls to manipulate it
class MathsPanel < Panel
  # Set functions to some nice initial values
  RED_INITIAL   = "cos(x)"
  GREEN_INITIAL = "cos(y ** x)"
  BLUE_INITIAL  = "(x ** 4) + ( y ** 3 ) - (4.5 * x ** 2 ) + ( y * 2)"

  # Symbols to show correct and incorrect functions
  TICK  = "\xE2\x9C\x94"
  CROSS = "\xE2\x9C\x98"

  attr_reader :drawing

  def initialize(parent)
    super(parent)
    self.sizer = VBoxSizer.new
    # The canvas
    @drawing = MathsDrawing.new(self) 
    sizer.add @drawing, 1, GROW

    sizer.add Wx::StaticLine.new(self)
    
    # The text controls for entering functions
    grid_sz = FlexGridSizer.new(3, 8, 8)
    grid_sz.add_growable_col(1, 1)

    grid_sz.add StaticText.new(self, :label => "Red")
    @red_tx = TextCtrl.new(self, :value => RED_INITIAL)
    grid_sz.add @red_tx, 0, GROW
    @red_err = StaticText.new(self, :label => TICK)
    grid_sz.add @red_err, 0, ALIGN_CENTRE

    grid_sz.add StaticText.new(self, :label => "Green")
    @green_tx = TextCtrl.new(self, :value => GREEN_INITIAL)
    grid_sz.add @green_tx, 0, GROW
    @green_err = StaticText.new(self, :label => TICK)
    grid_sz.add @green_err, 0, ALIGN_CENTRE

    grid_sz.add StaticText.new(self, :label => "Blue")
    @blue_tx = TextCtrl.new(self, :value => BLUE_INITIAL)
    grid_sz.add @blue_tx, 0, GROW
    @blue_err = StaticText.new(self, :label => TICK)
    grid_sz.add @blue_err, 0, ALIGN_CENTRE

    # Buttons to save and render
    grid_sz.add nil
    butt_sz = HBoxSizer.new
    render_bt = Button.new(self, :label => "Render")
    butt_sz.add render_bt, 0, Wx::RIGHT, 8
    evt_button render_bt, :on_render

    save_bt = Button.new(self, :label => "Save Image")
    butt_sz.add save_bt, 0, Wx::RIGHT, 8
    evt_button save_bt, :on_save

    # Disable the buttons whilst redrawing
    evt_update_ui(render_bt) { | evt | evt.enable(@drawing.done) }
    evt_update_ui(save_bt) { | evt | evt.enable(@drawing.done) }
    grid_sz.add butt_sz

    # Add the controls sizer to the whole thing
    sizer.add grid_sz, 0, GROW|ALL, 10

    on_render
  end
  
  # Update the functions that generate the image, then re-render it
  def on_render
    @drawing.red   = make_a_function(@red_tx.value, @red_err)
    @drawing.green = make_a_function(@green_tx.value, @green_err)
    @drawing.blue  = make_a_function(@blue_tx.value, @blue_err)
    @drawing.redraw
  end

  # Display a dialog to save the image to a file
  def on_save
    dlg = SaveImageDialog.new(parent)
    if dlg.show_modal == ID_OK
      @drawing.img.save_file(dlg.path, dlg.image_type)
    end
  end

  # A function which doesn't do anything
  NULL_FUNC = lambda { | x, y | 1 }

  # Takes a string source +source+, returns a lambda. If the string
  # source isn't valid, flag this in the GUI static text +error_outlet+
  def make_a_function(source, error_outlet)
    return NULL_FUNC if source.empty?
    func = nil
    begin
      # Create the function and test it, to check for wrong names
      func = eval "lambda { | x, y | #{source} }"
      func.call(0, 0)
    rescue Exception => e
      error_outlet.label = CROSS
      error_outlet.tool_tip = e.class.name + ":\n" +
                              e.message.sub(/^\(eval\):\d+: /, '')        
      return NULL_FUNC
    end
    
    # Things are good, note this and return the function
    error_outlet.label = TICK
    error_outlet.tool_tip = ''
    func
  end
end

class MathsFrame < Frame
  def initialize
    super(nil, :title => 'Maths drawing', 
               :size => [400, 500], 
               :pos => [50, 50])
    sb = create_status_bar(1)
    evt_update_ui sb, :on_update_status
    @panel = MathsPanel.new(self)
  end

  def on_update_status
    if @panel.drawing.done
      pixels = @panel.drawing.client_size
      msg = "[#{pixels.x} x #{pixels.y}] drawing completed in " +
            "#{@panel.drawing.render_time}s"
      status_bar.status_text = msg
    end
  end
end

App.run do
  MathsFrame.new.show
end
