diff options
Diffstat (limited to 'graph.py')
-rwxr-xr-x | graph.py | 1447 |
1 files changed, 1447 insertions, 0 deletions
diff --git a/graph.py b/graph.py new file mode 100755 index 0000000..d0d60bb --- /dev/null +++ b/graph.py @@ -0,0 +1,1447 @@ +#!/usr/bin/env python +""" + graph.py + Activity that plots 1st and 2nd degree polynomials + Part of the olpc.gr project + Copyright (C) 2009 Xenofon Papadopoulos <xpapad@gmail.com> +""" + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import logging +from gettext import gettext as _ +import os +import copy +import time +import math + +import cairo +import pango +import gtk +import gtk.glade +import gtk.gdk +import gobject +from gobject import GObject +from math import sqrt, pow +from gtk.gdk import Color +from gtk import ListStore +from gtk import TreeViewColumn + +# Check for equality between floats +SIGMA = 1e-6 +def equals( x, val ): + """Check for equality between floats, using a Sigma of 1e-6""" + return ( x == val or ( abs( x - val ) <= SIGMA ) ) + +def markup_func( column, cell, model, iter ): + cell.props.markup = model.get_value( iter, 0 ).markup + cell.props.foreground = model.get_value( iter, 0 ).color + +def toggle_visible_func( column, cell, model, iter ): + cell.props.active = model.get_value( iter, 0 ).visible + +def toggle_info_func( column, cell, model, iter ): + cell.props.active = model.get_value( iter, 0 ).info + +def rounded_number( x, dec ): + """ + Return a text representation of a number with a specified number of + decimal points. + + x -- the number to convert to text + dex -- the number of decimal points + """ + x = float( str( x ) ) + fmt = '%%.%df' % ( dec ) + return fmt % ( x ) + +def translate_x( x, x_min, x_max, width ): + return int( ( width * ( x - x_min ) ) / ( x_max - x_min ) ) + +def translate_y( y, y_min, y_max, height ): + return int( height - ( height * ( y - y_min ) ) / ( y_max - y_min ) ) + +class Polynomial( GObject ): + """ + A class to handle polynomials of 1st and 2nd degree. It can be expanded + to support polynomials of higher degree. + It subclasses gobject.GObject so that it can be used in TreeView models, + such as a ListStore. + """ + def __init__( self, degree ): + """Class constructor.""" + self.degree = degree + self.coefficient = { 0: 0, 1: 0, 2: 0 } + self.solved = False + self.D = None + self.x1 = None + self.x2 = None + self.limit = ( None, None ) + self.has_min = None + self.cmap = gtk.gdk.colormap_get_system() + self.fg_color = None + self.is_visible = True + self.show_marks = True + + def copy( self, obj ): + """ + Make a deep copy of the object. The copy.deepcopy() function will + not work for GObjects. + """ + self.degree = obj.degree + self.coefficient = copy.deepcopy( obj.coefficient ) + self.solved = obj.solved + self.D = obj.D + self.x1 = obj.x1 + self.x2 = obj.x2 + self.limit = obj.limit + self.has_min = obj.has_min + self.fg_color = obj.fg_color + self.is_visible = obj.is_visible + + def equals( self, obj ): + """ + Check for equality between polynomials. Two polynomials are equal if + their degree and all their coefficients are equal. + """ + if self.degree != obj.degree: + return False + degree = self.degree + while degree >= 0: + if not equals(self.coefficient[degree], obj.coefficient[degree]): + return False + degree = degree - 1 + return True + + def get_coefficient_name( self, degree ): + """ + Get a text representation of a coefficient's name. We assume that the + polynomial's form is a1x^n + a2x^(n-1) + ... + a(n+1). We want to + display a1 as 'a', a2 as 'b' etc. The exact representation is specified + in the localization files. + """ + index = self.degree - degree + 1 + name = 'a' + str( index ) + return _( name ) + + def is_empty( self ): + """Check if all coefficients are zero.""" + degree = self.degree + while degree >= 0: + if not equals( self.coefficient[ degree ], 0 ): + return False + degree = degree - 1 + return True + + def get_template_markup( self, use_f_notation = False ): + """ + Return a notation of the polynomial in Pango markup form. Do not + use coefficient values; use coefficient names instead. + + use_f_notation -- if True, then use f(x) = ... in the notation, + otherwise use y = ... + """ + msg = '' + if use_f_notation: + msg = 'f( x ) = ' + else: + msg = 'y = ' + degree = self.degree + while degree >= 0: + if degree != self.degree: + msg = msg + ' + ' + msg = msg + self.get_coefficient_name( degree ) + if degree >= 2: + msg = msg + 'x<sup>%d</sup>' % ( degree ) + elif degree == 1: + msg = msg + 'x' + degree = degree - 1 + return msg + + def get_expression_markup( self, use_f_notation = False ): + """ + Return a notation of the polynomial in Pango markup form, by using + coefficient values. + + use_f_notation -- if True, then use f(x) = ... in the notation, + otherwise use y = ... + """ + msg = '' + added = False + degree = self.degree + if use_f_notation: + msg = 'f( x ) = ' + else: + msg = 'y = ' + while degree >= 0: + val = self.coefficient[ degree ] + if equals( val, 0 ): + degree = degree - 1 + continue + + # We do not display a coefficient of 1, unless it's last one + if equals( abs( val ), 1.0 ) and degree > 0: + strval = '' + else: + strval = '%.3g' % ( abs( val ) ) + + # Display the sign seperated from the coefficient value + # Only the first factor will be signed normally + if not added: + if val < 0: + msg = msg + '-' + strval + else: + msg = msg + strval + else: + if val < 0: + msg = msg + ' - ' + strval + else: + msg = msg + ' + ' + strval + added = True + + # Add the factor notation + if degree >= 2: + msg = msg + 'x<sup>%d</sup>' % ( degree ) + elif degree == 1: + msg = msg + 'x' + degree = degree - 1 + + # Handle the special case where all coefficients are zero + if not added: + msg = msg + '0' + return msg + + def set_expression_markup( self, txt ): + None + + def set_coefficient( self, degree, value ): + """Set the value of the specified coefficient.""" + val = float( str( value ) ) + if equals( val, 0 ): + val = 0 # For the special case of -0.0 + self.coefficient[ degree ] = float( str( value ) ) + self.solved = False + + def get_coefficients( self ): + """Get the values of all coefficients, in order of descending degree.""" + degree = self.degree + ret = [] + while degree >= 0: + ret.append( self.coefficient[ degree ] ) + degree = degree - 1 + return ret + + def calculate( self, x ): + """Calculate the value of the polynomial for a given x.""" + degree = 0 + y = 0 + # Add in order of increasing degree to keep precision for high x values + while degree <= self.degree: + coef = self.coefficient[ degree ] + val = pow( x, degree ) + y = coef * val + y + degree = degree + 1 + return y + + def solve( self ): + """ + Solve the polynomial. When the polynomial is solved, x1 and x2 are set + to the solutions, and D specified the number of solutions: + + D = 0 : one solution (double, in case of a 2nd degree polynomial) + D > 0 : two solutions (only in the case of a 2nd degree polynomial) + D < 0 : no solutions + """ + a = self.coefficient[ 2 ] + b = self.coefficient[ 1 ] + c = self.coefficient[ 0 ] + + if equals( a, 0 ): + if equals( b, 0 ): + self.D = -1 + self.x1 = None + self.x2 = None + self.solved = True + return + self.D = 0 + self.x1 = -c / b + self.x2 = self.x1 + self.solved = True + return + + # Solution + D = b * b - 4 * a * c + self.D = D + + if equals( D, 0 ): + x = -b / ( 2 * a ) + self.x1 = x + self.x2 = x + elif D > 0: + self.x1 = ( -b - sqrt( D ) ) / ( 2 * a ) + self.x2 = ( -b + sqrt( D ) ) / ( 2 * a ) + + else: + self.x1 = None + self.x2 = None + + # Akrotata + x = -b / ( 2 * a ) + y = -D / ( 4 * a ) + if equals( x, 0 ): + x = 0 + if equals( y, 0 ): + y = 0 + self.limit = ( x, y ) + if a > 0: + self.has_min = True + else: + self.has_min = False + + self.solved = True + + def get_points_d1( self ): + """ + Return a list of points of interest for a 1st degree polynomial. + Each list element is a tuple of point coordinations. + Points of interest are the points of intersection of the graph with + the axis (x=0, y=0) + """ + a, b = self.coefficient[ 1 ], self.coefficient[ 0 ] + ret = [] + + # x = 0 + x = 0 + y = self.calculate( x ) + ret.append( ( x, y ) ) + + # y = 0 + y = 0 + if not equals( a, 0 ): + x = -b/a + ret.append( ( x, y ) ) + return ret + + def get_points_d2( self ): + """ + Return a list of points of interest for a 2nd degree polynomial. + Each list element is a tuple of point coordinations. + Points of interest are the points of intersection of the graph with + the axis (x=0, y=0) + """ + ret = [] + + # x = 0 + x = 0 + y = self.calculate( x ) + ret.append( ( x, y ) ) + + # y = 0 + self.solve() + if equals( self.D, 0 ): + x = self.x1 + ret.append( ( x, 0 ) ) + elif self.D > 0: + for x in [ self.x1, self.x2 ]: + ret.append( ( x, 0 ) ) + + return ret + + def get_points( self ): + """ + Return a list of points of interest for a polynomial. + Each list element is a tuple of point coordinations. + Points of interest are the points of intersection of the graph with + the axis (x=0, y=0) + """ + if self.degree == 1: + return self.get_points_d1() + elif self.degree == 2: + return self.get_points_d2() + else: + return [] + + def get_second_degree( self ): + """ + Return a 2nd degree polynomial with the same coefficients as a 1st + degree one. This is used to solve a system of polynomials of 1st and + 2nd degree by converting them all to 2nd degree. + """ + poly = Polynomial( 2 ) + if self.degree == 2: + poly.copy( self ) + else: + poly.coefficient[ 1 ] = self.coefficient[ 1 ] + poly.coefficient[ 0 ] = self.coefficient[ 0 ] + poly.solved = False + return poly + + def get_common_points( self, poly ): + """ + Return a list of intersection points between self and poly. + Each list item is a tuple of point coordinations. + """ + ret = [] + + # Get some 2nd degree polynomials + if self.degree == 1: + p1 = self.get_second_degree() + else: + p1 = self + if poly.degree == 1: + p2 = poly.get_second_degree() + else: + p2 = poly + + # We need the common solutions of p1, p2 + p = Polynomial( 2 ) + for i in [ 2, 1, 0 ]: + p.coefficient[ i ] = p1.coefficient[ i ] - p2.coefficient[ i ] + p.solve() + if equals( p.D, 0 ): + x = p.x1 + y = p1.calculate( x ) + ret.append( ( x, y ) ) + elif p.D > 0: + for x in [ p.x1, p.x2 ]: + y = p1.calculate( x ) + ret.append( ( x, y ) ) + return ret + + markup = property( get_expression_markup ) + """The markup property.""" + + def get_color( self ): + """color property getter.""" + return self.fg_color + + def set_color( self, val ): + """color property setter.""" + cmap = gtk.gdk.colormap_get_system() + self.fg_color = cmap.alloc_color( val ) + + color = property( get_color ) + """The color property.""" + + def get_visible( self ): + """visible property getter.""" + return self.is_visible + + def set_visible( self, state ): + """visible property setter.""" + self.is_visible = state + + def toggle_visible( self ): + """Toggles the visible property.""" + self.is_visible = not self.is_visible + + visible = property( get_visible, set_visible ) + """The visible property.""" + + def set_show_marks( self, state ): + """info property setter.""" + self.show_marks = state + + def get_show_marks( self ): + """info property getter.""" + return self.show_marks + + def toggle_info( self ): + """Toggles the info property.""" + self.show_marks = not self.show_marks + + info = property( get_show_marks, set_show_marks ) + """The info property.""" + + def get_x_bounds( self, axis, width, height, real_coords = False ): + """ + Return a tuble of x coordinates that restict the visible area of the + polynomial on the specified screen width. + + real_coords -- If True, then coordinates are real. + """ + axis_x_min, axis_x_max, axis_y_min, axis_y_max = axis + step = ( axis_x_max - axis_x_min ) / width + x_min = x_max = None + xi_min = xi_max = None + + x = axis_x_min + while x <= axis_x_max: + y = self.calculate( x ) + xi = translate_x( x, axis_x_min, axis_x_max, width ) + yi = translate_y( y, axis_y_min, axis_y_max, height ) + if ( xi >= 0 and xi <= width and yi >= 0 and yi <= height ): + if xi_min == None: + xi_min = xi_max = xi + x_min = x_max = x + x = x + step + continue + if xi_max < xi: + xi_max = xi + x_max = x + x = x + step + + if real_coords: + return ( xi_min, xi_max ) + else: + return ( x_min, x_max ) + + def get_animator( self, wnd, gc, pixmap, axis ): + """Get the polygon's animator""" + if self.degree == 1: + ani = Animator_Degree_1( self, wnd, gc, pixmap, axis ) + else: + ani = None + return ani + +class Point: + """ + A helper class to handle points of interest of polynomials on the x-y + axis. It contains the coordinates, and some text to display when the user + clicks on (or near) them. + Each point has an associated rectangle of specified size that constitutes + the 'active' area around the point. The user may click on the rectangle + to get info about the point. + Point.x and Point.y are REAL, screen coordinates. + """ + def __init__(self, x, y, size = 8): + """Class constructor.""" + self.x = x + self.y = y + self.size = size + self.txt = '' + self.color = None + + def set_text( self, txt ): + """Set the text displayed when the user clicks near the points.""" + self.txt = txt + + def set_poly_coords( self, x, y ): + """Set the coordinates of the rectangle displayed at this point.""" + self.poly_x = x + self.poly_y = y + + def draw( self, wnd, gc, size = 8 ): + """Draw a rectangle around this point, with te specified size.""" + old_fg = None + if size != self.size: + self.size = size + if self.color != None: + old_fg = gc.foreground + gc.set_foreground( self.color ) + s = self.size + x,y = self.x,self.y + wnd.draw_rectangle( gc, True, x - s / 2, y - s / 2, s + 1, s + 1 ) + if old_fg != None: + gc.set_foreground( old_fg ) + + def set_color( self, color ): + """Set the color of this point.""" + self.color = color + + def in_window( self, xi, yi ): + """Return True if xi, yi reside inside the rectangle of this point.""" + s = self.size + x,y = self.x, self.y + if xi < x - s/2 or xi > x + s/2: + return False + if yi < y - s/2 or yi > y + s/2: + return False + return True + + def get_markup( self ): + """Return a formatted string of the point's text.""" + x,y = self.poly_x, self.poly_y + msg = "x=%s\ny=%s" % ( rounded_number( x, 2 ), rounded_number( y, 2 ) ) + return msg + +class Animator: + """A helper class to handle animation effects.""" + def __init__(self, poly, wnd, gc, pixmap, axis ): + self.round = 0 + self.poly = poly + self.wnd = wnd + self.gc = gc + self.pixmap = pixmap + self.axis = axis + self.width, self.height = pixmap.get_size() + self.x_min, self.x_max = poly.get_x_bounds( axis, self.width, self.height ) + self.xi_min = self.translate_x( self.x_min ) + self.xi_max = self.translate_x( self.x_max ) + + def translate_x( self, x ): + axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis + return translate_x( x, axis_x_min, axis_x_max, self.width ) + + def translate_y( self, y ): + axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis + return translate_y( y, axis_y_min, axis_y_max, self.height ) + + def animate(self): + """ + This is called whenever pixmap needs an update. The method should just + draw on the pixmap, updates are handled elsewhere. + + Return False to end the animation. + """ + self.round = self.round + 1 + return self.round != 5 + + def next_round(self): + """Next animator round.""" + self.round = self.round + 1 + +class Animator_Degree_1( Animator ): + def __init__(self, poly, wnd, gc, pixmap, axis ): + Animator.__init__(self, poly, wnd, gc, pixmap, axis) + self.a = poly.coefficient[ 1 ] + + self.pixbufs = [ + self.pixbuf_from_img( "images/bike_rider_1.png", self.a ), + self.pixbuf_from_img( "images/bike_rider_2.png", self.a ), + self.pixbuf_from_img( "images/bike_rider_3.png", self.a ) ] + + def pixbuf_from_img( self, img, theta ): + # Read the bike image + image = cairo.ImageSurface.create_from_png( img ) + # Calculate the size of the rotated image + w, h = image.get_width(), image.get_height() + r = abs( math.atan( theta ) ) + sinf = math.sin( r ) + cosf = math.cos( r ) + new_w = int( w * cosf + h * sinf ) + new_h = int( w * sinf + h * cosf ) + + # Create a rotated image of the appropriate size + surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, new_w, new_h ) + context = cairo.Context( surface ) + + # Test rectangle + """ + context.set_line_width( 1 ) + context.set_source_rgba( 1, 0, 0 ) + context.rectangle( 0, 0, new_w, new_h ) + context.stroke() + """ + + if theta >= 0: + context.translate( 0, w * sinf ) + context.rotate( -r ) + else: + context.translate( h * sinf, 0 ) + context.rotate( r ) + context.set_source_surface( image ) + context.paint() + + if theta >= 0: + hypo = h + w * sinf * cosf + dx = - int( sinf * hypo ) + dy = - int( cosf * hypo ) + else: + hypo = h * cosf * cosf + dx = int( sinf * hypo ) + dy = - int( cosf * hypo ) + + data = surface.get_data() + width, height = surface.get_width(), surface.get_height() + stride = surface.get_stride() + + pixbuf = gtk.gdk.pixbuf_new_from_data( data, gtk.gdk.COLORSPACE_RGB, True, 8, width, height, stride ) + return ( pixbuf, dx, dy ) + + def animate(self): + # Get image info + width, height = self.pixmap.get_size() + pb, dx, dy = self.pixbufs[ self.round % len( self.pixbufs ) ] + iw,ih = pb.get_width(), pb.get_height() + + axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis + x_start = axis_x_min + x_end = axis_x_max + + if self.x_min == None: + return False + + step = ( self.x_max - self.x_min ) / 45 + x = self.x_min + step * self.round + self.round = self.round + 1 + if x >= self.x_max: + return False + + y = self.poly.calculate( x ) + xi, yi = self.translate_x( x ), self.translate_y( y ) + + posx = xi + dx + posy = yi + dy + self.pixmap.draw_pixbuf( self.gc, pb, 0, 0, posx, posy, iw, ih ) + return True + +class Animator_Degree_2( Animator ): + def __init__(self, poly, wnd, gc, pixmap, axis ): + Animator.__init__(self, poly, wnd, gc, pixmap, axis) + self.a = poly.coefficient[ 1 ] + + self.pixbufs = [ + self.pixbuf_from_img( "images/bike_rider_1.png", self.a ), + self.pixbuf_from_img( "images/bike_rider_2.png", self.a ), + self.pixbuf_from_img( "images/bike_rider_3.png", self.a ) ] + + def pixbuf_from_img( self, img, theta ): + # Read the bike image + image = cairo.ImageSurface.create_from_png( img ) + # Calculate the size of the rotated image + w, h = image.get_width(), image.get_height() + r = abs( math.atan( theta ) ) + sinf = math.sin( r ) + cosf = math.cos( r ) + new_w = int( w * cosf + h * sinf ) + new_h = int( w * sinf + h * cosf ) + + # Create a rotated image of the appropriate size + surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, new_w, new_h ) + context = cairo.Context( surface ) + + # Test rectangle + """ + context.set_line_width( 1 ) + context.set_source_rgba( 1, 0, 0 ) + context.rectangle( 0, 0, new_w, new_h ) + context.stroke() + """ + + if theta >= 0: + context.translate( 0, w * sinf ) + context.rotate( -r ) + else: + context.translate( h * sinf, 0 ) + context.rotate( r ) + context.set_source_surface( image ) + context.paint() + + if theta >= 0: + hypo = h + w * sinf * cosf + dx = - int( sinf * hypo ) + dy = - int( cosf * hypo ) + else: + hypo = h * cosf * cosf + dx = int( sinf * hypo ) + dy = - int( cosf * hypo ) + + data = surface.get_data() + width, height = surface.get_width(), surface.get_height() + stride = surface.get_stride() + + pixbuf = gtk.gdk.pixbuf_new_from_data( data, gtk.gdk.COLORSPACE_RGB, True, 8, width, height, stride ) + return ( pixbuf, dx, dy ) + + def animate(self): + # Get image info + width, height = self.pixmap.get_size() + pb, dx, dy = self.pixbufs[ self.round % len( self.pixbufs ) ] + iw,ih = pb.get_width(), pb.get_height() + + axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis + x_start = axis_x_min + x_end = axis_x_max + + if self.x_min == None: + return False + + step = ( self.x_max - self.x_min ) / 45 + x = self.x_min + step * self.round + self.round = self.round + 1 + if x >= self.x_max: + return False + + y = self.poly.calculate( x ) + xi, yi = self.translate_x( x ), self.translate_y( y ) + + posx = xi + dx + posy = yi + dy + self.pixmap.draw_pixbuf( self.gc, pb, 0, 0, posx, posy, iw, ih ) + return True + +# The main module class +class Graph: + """ + The main application class. + """ + def __init__(self, runaslib=True): + """Class constructor.""" + # Load Glade XML + self.xml = gtk.glade.XML( "graph.glade" ) + + # Make sure application shuts down if main window closes + self.w = self.xml.get_widget( 'window_graph' ) + self.w.connect( 'delete_event', gtk.main_quit ) + + # Get Windows child + self.w_child = self.w.get_child() + + # Get a 2nd degree polynomial + self.poly = None + + # Get widgets + self.label_info = self.xml.get_widget( 'label_info' ) + self.txt_func_a_1 = self.xml.get_widget( 'txt_func_a_1' ) + self.txt_func_b_1 = self.xml.get_widget( 'txt_func_b_1' ) + self.txt_func_a_2 = self.xml.get_widget( 'txt_func_a_2' ) + self.txt_func_b_2 = self.xml.get_widget( 'txt_func_b_2' ) + self.txt_func_c_2 = self.xml.get_widget( 'txt_func_c_2' ) + + # Axis widgets + self.txt_axis_x_min = self.xml.get_widget( 'txt_axis_x_min' ) + self.txt_axis_x_max = self.xml.get_widget( 'txt_axis_x_max' ) + self.txt_axis_x_step = self.xml.get_widget( 'txt_axis_x_step' ) + self.txt_axis_y_min = self.xml.get_widget( 'txt_axis_y_min' ) + self.txt_axis_y_max = self.xml.get_widget( 'txt_axis_y_max' ) + self.txt_axis_y_step = self.xml.get_widget( 'txt_axis_y_step' ) + + # Main tab buttons + btn_draw = self.xml.get_widget( 'btn_draw' ) + btn_draw.connect( 'clicked', self.on_draw_clicked ) + self.set_button_icon( btn_draw, 'draw.svg' ) + + btn_store = self.xml.get_widget( 'btn_store' ) + btn_store.connect( 'clicked', self.on_store_clicked ) + self.set_button_icon( btn_store, 'archive.svg' ) + + self.btn_delete = self.xml.get_widget( 'btn_delete' ) + self.btn_delete.connect( 'clicked', self.on_delete_clicked ) + self.set_button_icon( self.btn_delete, 'delete.svg' ) + + self.btn_color = self.xml.get_widget( 'btn_color' ) + self.btn_color.connect( 'clicked', self.on_color_clicked ) + self.set_button_icon( self.btn_color, 'color.svg' ) + self.color_dlg = None + + self.btn_animate = self.xml.get_widget( 'btn_animate' ) + self.btn_animate.connect( 'clicked', self.on_animate_clicked ) + self.set_button_icon( self.btn_animate, 'animate.svg' ) + + # Config tab buttons + btn_reset = self.xml.get_widget( 'btn_reset' ) + btn_reset.connect( 'clicked', self.on_reset_clicked ) + self.set_button_icon( btn_reset, 'undo.svg' ) + + self.toggle_show_axis_numbers = self.xml.get_widget( "toggle_show_axis_numbers" ) + self.set_button_icon( self.toggle_show_axis_numbers, 'ruller.svg' ) + + self.toggle_show_grid = self.xml.get_widget( "toggle_show_grid" ) + self.set_button_icon( self.toggle_show_grid, 'grid.svg' ) + + # Setup draw area + self.draw = self.xml.get_widget( 'drawing_area' ) + self.draw.connect( 'motion_notify_event', self.update_coords ) + self.draw.connect( 'button_press_event', self.on_draw_button_pressed ) + self.draw.connect( 'configure_event', self.on_draw_configure ) + self.draw.connect( "expose_event", self.on_draw_expose ) + self.draw.get_pango_context().set_font_description( pango.FontDescription( "fixed 6" ) ) + self.pixmap = None + self.old_pixmap = None + self.gc = None + self.width = self.height = 0 + + # More event handlers + self.notebook = self.xml.get_widget( 'notebook_app' ) + self.notebook.connect( 'switch_page', self.on_tab_switched ) + + # Template labels + self.notebook_degree = self.xml.get_widget( 'notebook_degree' ) + self.list_functions = self.xml.get_widget( 'list_functions' ) + self.list_functions.connect( "cursor-changed", self.on_functions_updated ) + # setup the view ... + cellr = gtk.CellRendererText() + col = TreeViewColumn( _( 'Functions' ), cellr ) + col.set_cell_data_func( cellr, markup_func ) + col.set_expand( True ) + self.list_functions.append_column( col ) + + cellr = gtk.CellRendererToggle() + cellr.connect( "toggled", self.on_visible_toggled ) + pb = gtk.gdk.pixbuf_new_from_file( "icons/eye.png" ) + img = gtk.Image() + img.set_from_pixbuf( pb ) + img.show() + col = TreeViewColumn( None, cellr ) + col.set_widget( img ) + col.set_cell_data_func( cellr, toggle_visible_func ) + col.set_expand( False ) + self.list_functions.append_column( col ) + + cellr = gtk.CellRendererToggle() + cellr.connect( "toggled", self.on_info_toggled ) + pb = gtk.gdk.pixbuf_new_from_file( "icons/information.png" ) + img = gtk.Image() + img.set_from_pixbuf( pb ) + img.show() + col = TreeViewColumn( None, cellr ) + col.set_widget( img ) + col.set_cell_data_func( cellr, toggle_info_func ) + col.set_expand( False ) + self.list_functions.append_column( col ) + + # ... and the model + self.list_functions.set_model( ListStore( gobject.TYPE_PYOBJECT ) ) + + # Labels + self.label_function_1_template = self.xml.get_widget( 'label_function_1_template' ) + self.label_function_1_template.set_markup( Polynomial( 1 ).get_template_markup() ) + self.label_function_2_template = self.xml.get_widget( 'label_function_2_template' ) + self.label_function_2_template.set_markup( Polynomial( 2 ).get_template_markup() ) + + # Info label + self.label_info = self.xml.get_widget( 'label_info' ) + self.label_info.set_text( '' ) + #self.label_info.modify_font( pango.FontDescription( "sans 14" ) ) + + self.label_coords = self.xml.get_widget( 'label_coords' ) + self.label_coords.set_text( '' ) + + # a[n] labels + self.xml.get_widget( "label_a_1" ).set_text( _( 'a1' ) + ' = ' ) + self.xml.get_widget( "label_b_1" ).set_text( _( 'a2' ) + ' = ' ) + self.xml.get_widget( "label_a_2" ).set_text( _( 'a1' ) + ' = ' ) + self.xml.get_widget( "label_b_2" ).set_text( _( 'a2' ) + ' = ' ) + self.xml.get_widget( "label_c_2" ).set_text( _( 'a3' ) + ' = ' ) + + # More labels + self.xml.get_widget( "label_from_1" ).set_text( _( 'From' ) ) + self.xml.get_widget( "label_from_2" ).set_text( _( 'From' ) ) + self.xml.get_widget( "label_until_1" ).set_text( _( 'To' ) ) + self.xml.get_widget( "label_until_2" ).set_text( _( 'To' ) ) + self.xml.get_widget( "label_step_1" ).set_text( _( 'Step' ) ) + self.xml.get_widget( "label_step_2" ).set_text( _( 'Step' ) ) + self.xml.get_widget( "label_axis" ).set_text( _( 'Axis' ) ) + self.xml.get_widget( "label_dec_1" ).set_text( _( 'Decimals' ) ) + self.xml.get_widget( "label_dec_2" ).set_text( _( 'Decimals' ) ) + + # Spin + self.spin_axis_x_dec = self.xml.get_widget( "spin_axis_x_dec" ) + self.spin_axis_y_dec = self.xml.get_widget( "spin_axis_y_dec" ) + self.spin_axis_x_dec.set_editable( False ) + self.spin_axis_y_dec.set_editable( False ) + + # Tab labels + self.xml.get_widget( "label_tab_graph" ).set_text( _( 'Graph Tab' ) ) + self.xml.get_widget( "label_tab_config" ).set_text( _( 'Config Tab' ) ) + + # Get parameters + self.txt_func_a_1 = self.xml.get_widget( 'txt_func_a_1' ) + self.txt_func_b_1 = self.xml.get_widget( 'txt_func_b_1' ) + self.txt_func_a_2 = self.xml.get_widget( 'txt_func_a_2' ) + self.txt_func_b_2 = self.xml.get_widget( 'txt_func_b_2' ) + self.txt_func_c_2 = self.xml.get_widget( 'txt_func_c_2' ) + + # Setup colors + cmap = gtk.gdk.colormap_get_system() + self.color_bg = cmap.alloc_color( '#FFFFCC' ) + self.marked_point_color = cmap.alloc_color( '#00FF00' ) + + # Setup initial axis values etc + self.validate_parameters( None ) + self.update_buttons_status() + + # self.widget will be attached to the Activity + # This can be any GTK widget except a window + self.widget = self.w_child + if not runaslib: + self.w.show_all() + gtk.main() + + def set_button_icon( self, btn, file, width = 40, height = 40 ): + fname = "icons/" + file + pb = gtk.gdk.pixbuf_new_from_file( fname ) + pb = pb.scale_simple( width, height, gtk.gdk.INTERP_BILINEAR ) + img = gtk.Image() + img.set_from_pixbuf( pb ) + btn.set_label( '' ) + btn.set_image( img ) + + def validate_parameters( self, poly ): + # Set default function factors + for txt in ( self.txt_func_a_1, self.txt_func_b_1, self.txt_func_a_2, self.txt_func_b_2, self.txt_func_c_2 ): + if ( txt.get_text() == '' ): + txt.set_text( '0' ) + + # Override function factors + try: + a1 = float( self.txt_func_a_1.get_text() ) + b1 = float( self.txt_func_b_1.get_text() ) + a2 = float( self.txt_func_a_2.get_text() ) + b2 = float( self.txt_func_b_2.get_text() ) + c2 = float( self.txt_func_c_2.get_text() ) + except: + return False + + # Set default axis values + if self.txt_axis_x_min.get_text() == '': + self.txt_axis_x_min.set_text( '-10' ) + if self.txt_axis_x_max.get_text() == '': + self.txt_axis_x_max.set_text( '10' ) + if self.txt_axis_x_step.get_text() == '': + self.txt_axis_x_step.set_text( '1' ) + + if self.txt_axis_y_min.get_text() == '': + self.txt_axis_y_min.set_text( '-10' ) + if self.txt_axis_y_max.get_text() == '': + self.txt_axis_y_max.set_text( '10' ) + if self.txt_axis_y_step.get_text() == '': + self.txt_axis_y_step.set_text( '1' ) + + # Override axis values + try: + self.axis_x_min = float( self.txt_axis_x_min.get_text() ); + self.axis_x_max = float( self.txt_axis_x_max.get_text() ); + self.axis_x_step = float( self.txt_axis_x_step.get_text() ); + + self.axis_y_min = float( self.txt_axis_y_min.get_text() ); + self.axis_y_max = float( self.txt_axis_y_max.get_text() ); + self.axis_y_step = float( self.txt_axis_y_step.get_text() ); + except: + return False + + # Update the poly coefficients + if poly != None: + if poly.degree == 2: + poly.set_coefficient( 2, a2 ) + poly.set_coefficient( 1, b2 ) + poly.set_coefficient( 0, c2 ) + elif poly.degree == 1: + poly.set_coefficient( 1, a1 ) + poly.set_coefficient( 0, b1 ) + else: + return False + + # Eveything is fine + return True + + def get_selected_function_iter( self ): + tree = self.list_functions + select = tree.get_selection() + if select == None: + return + tree, iter = select.get_selected() + return iter + + def on_visible_toggled( self, renderer, path, *args ): + model = self.list_functions.get_model() + iter = model.get_iter( path ) + if iter == None: + return + poly = model.get_value( iter, 0 ) + poly.toggle_visible() + self.refresh() + + def on_info_toggled( self, renderer, path, *args ): + model = self.list_functions.get_model() + iter = model.get_iter( path ) + if iter == None: + return + poly = model.get_value( iter, 0 ) + poly.toggle_info() + self.refresh() + + def update_buttons_status( self ): + iter = self.get_selected_function_iter() + if iter == None: + self.btn_delete.set_sensitive( False ) + self.btn_color.set_sensitive( False ) + self.btn_animate.set_sensitive( False ) + else: + self.btn_delete.set_sensitive( True ) + self.btn_color.set_sensitive( True ) + self.btn_animate.set_sensitive( True ) + + def update_all_buttons( self, state ): + self.notebook.set_sensitive( state ) + + def on_functions_updated( self, tree, *args ): + self.update_buttons_status( ) + + def on_animate_clicked( self, *args ): + self.animate_selected_function() + return + + def on_color_clicked( self, *args ): + iter = self.get_selected_function_iter() + if iter == None: + return + poly = self.list_functions.get_model().get_value( iter, 0 ) + old_color = poly.get_color() + dlg = gtk.ColorSelectionDialog( _( 'Select a color' ) ) + ret = dlg.run() + if ret == gtk.RESPONSE_OK: + color = dlg.colorsel.get_current_color() + if color != old_color: + poly.set_color( color ) + self.refresh() + dlg.destroy() + + def on_tab_switched( self, notebook, page, page_num ): + if page_num == 0: + self.refresh() + + def on_delete_clicked( self, *args ): + iter = self.get_selected_function_iter() + if iter == None: + return + model = self.list_functions.get_model() + model.remove( iter ) + self.update_buttons_status() + self.refresh() + + def on_draw_button_pressed( self, widget, event ): + if ( event.button != 1 ): + return + wnd = self.pixmap + gc = self.gc + for pt in self.points: + if pt.in_window( event.x, event.y ): + self.label_info.set_markup( pt.get_markup() ) + for pto in self.points: + pto.set_color( None ) + pto.draw( wnd, self.gc ) + pt.set_color( self.marked_point_color ) + pt.draw( wnd, gc ) + self.draw.queue_draw() + return + + def on_reset_clicked( self, *args ): + self.txt_axis_x_min.set_text( '-10' ) + self.txt_axis_x_max.set_text( '10' ) + self.txt_axis_x_step.set_text( '1' ) + self.txt_axis_y_min.set_text( '-10' ) + self.txt_axis_y_max.set_text( '10' ) + self.txt_axis_y_step.set_text( '1' ) + + def refresh( self ): + wnd = self.pixmap + gc = self.gc + visible_polys = [] + points = [] + + # Get current window dimensions + self.width, self.height = self.pixmap.get_size() + self.draw.set_size_request( self.width, self.height ) + + wnd.draw_rectangle( self.draw.get_style().white_gc, True, 0, 0, self.width, self.height ) + gc.set_background( self.color_bg ) + self.points = [ ] + self.draw_axis( wnd, gc ) + + # Get a list of all visible polynomials + model = self.list_functions.get_model() + iter = model.get_iter_first() + while iter != None: + poly = model.get_value( iter, 0 ) + if poly.get_visible(): + visible_polys.append( poly ) + iter = model.iter_next( iter ) + if self.poly != None: + visible_polys.append( self.poly ) + + # Get a list of points of interest + m = len( visible_polys ) + i = 0 + for poly in visible_polys: + self.draw_function( wnd, gc, poly ) + if not poly.info: + continue + + points = points + poly.get_points() + t = i + 1 + while t < m: + p2 = visible_polys[ t ] + if not p2.info: + t = t + 1 + continue + points = points + poly.get_common_points( p2 ) + t = t + 1 + i = i + 1 + + # Now we have the points. Create the data + for p in points: + x,y = p + xi, yi = self.translate_x( x ), self.translate_y( y ) + pt = Point( xi, yi ) + pt.set_poly_coords( x, y ) + pt.draw( wnd, gc ) + self.points.append( pt ) + + self.draw.queue_draw() + + def copy_pixmap( self, pixmap ): + width, height = pixmap.get_size() + cp = gtk.gdk.Pixmap( pixmap, width, height, -1 ) + cp.draw_drawable( self.gc, pixmap, 0, 0, 0, 0, width, height ) + return cp + + def animate( self, animator ): + w, h = self.pixmap.get_size() + self.pixmap.draw_drawable( self.gc, self.old_pixmap, 0, 0, 0, 0, w, h ) + self.draw.queue_draw() + ret = animator.animate() + self.draw.queue_draw() + if ret == False: + self.pixmap.draw_drawable( self.gc, self.old_pixmap, 0, 0, 0, 0, w, h ) + self.old_pixmap = None + return False + return True + + # Some animations + def animate_selected_function( self ): + # Get the selected polynomial + iter = self.get_selected_function_iter() + if iter == None: + return + poly = self.list_functions.get_model().get_value( iter, 0 ) + + # Store the pixmap + self.old_pixmap = self.copy_pixmap( self.pixmap ) + + # Queue the animation + axis = ( self.axis_x_min, self.axis_x_max, self.axis_y_min, self.axis_y_max ) + ani = poly.get_animator( self.draw.window, self.gc, self.pixmap, axis ) + if ani != None: + gobject.timeout_add( 100, self.animate, ani ) + + # This is called when the drawing area is created, and every time its + # size changes + def on_draw_configure( self, widget, event ): + x,y,width,height = widget.get_allocation() + self.pixmap = gtk.gdk.Pixmap( widget.get_window(), width, height, -1 ) + self.pixmap.draw_rectangle( widget.get_style().white_gc, True, 0, 0, width, height ) + if self.gc == None: + self.gc = self.pixmap.new_gc( ) + self.refresh() + + def on_draw_expose( self, widget, event ): + x, y, width, height = event.area + wnd = widget.window + wnd.draw_drawable( self.gc, self.pixmap, x, y, x, y, width, height ) + return False + + def poly_is_stored( self, poly ): + model = self.list_functions.get_model() + iter = model.get_iter_first() + while iter != None: + p = model.get_value( iter, 0 ) + if p.equals( poly ): + return True + iter = model.iter_next( iter ) + return False + + def on_store_clicked( self, *args ): + if self.notebook_degree.get_current_page() == 0: + poly = Polynomial( 1 ) + else: + poly = Polynomial( 2 ) + if not self.validate_parameters( poly ): + # TODO: Warn about error + return + + # Ignore stored polynomials + if poly.is_empty() or self.poly_is_stored( poly ): + return + self.poly = None + + # add to model + model = self.list_functions.get_model() + model.append( [ poly ] ) + self.refresh() + + def on_draw_clicked( self, *args ): + if self.notebook_degree.get_current_page() == 0: + poly = Polynomial( 1 ) + else: + poly = Polynomial( 2 ) + + # Get and validate function arguments + if not self.validate_parameters( poly ): + # TODO: Warn about error + return + + if poly.is_empty(): + return + self.poly = poly + self.refresh() + + # Draw the axes of the graph + def draw_axis( self, wnd, gc ): + width = self.width + height = self.height + x0 = self.translate_x( 0 ) + y0 = self.translate_y( 0 ) + w = gc.line_width + gc.line_width = 2 + # y-axis (x=0) + wnd.draw_line( gc, x0, 0, x0, height ) + # x-axis (y=0) + wnd.draw_line( gc, 0, y0, width, y0 ) + gc.line_width = w + self.draw_marks( wnd, gc ) + + def draw_marks( self, wnd, gc ): + x0 = self.translate_x( 0 ) + y0 = self.translate_y( 0 ) + width = self.width + height = self.height + show_numbers = self.toggle_show_axis_numbers.get_active() + show_grid = self.toggle_show_grid.get_active() + x = 0 + dec_x = self.spin_axis_x_dec.get_value_as_int() + dec_y = self.spin_axis_y_dec.get_value_as_int() + + while x < self.axis_x_max: + x = x + self.axis_x_step + if x >= self.axis_x_max: + break + tx = self.translate_x( x ) + wnd.draw_line( gc, tx, y0 - 2, tx, y0 + 2 ) + if show_numbers: + self.add_axis_number( wnd, gc, x, dec_x, tx - 6, y0 + 4 ) + if show_grid: + wnd.draw_line( gc, tx, 0, tx, height ) + x = 0 + while x > self.axis_x_min: + x = x - self.axis_x_step + if x <= self.axis_x_min: + break + tx = self.translate_x( x ) + wnd.draw_line( gc, tx, y0 - 2, tx, y0 + 2 ) + if show_numbers: + self.add_axis_number( wnd, gc, x, dec_x, tx - 6, y0 + 4 ) + if show_grid: + wnd.draw_line( gc, tx, 0, tx, height ) + y = 0 + while y < self.axis_y_max: + y = y + self.axis_y_step + if y >= self.axis_y_max: + break + ty = self.translate_y( y ) + wnd.draw_line( gc, x0 - 2, ty, x0 + 2, ty ) + if show_numbers: + self.add_axis_number( wnd, gc, y, dec_y, x0 - 25, ty - 6 ) + if show_grid: + wnd.draw_line( gc, 0, ty, width, ty ) + y = 0 + while y > self.axis_y_min: + y = y - self.axis_y_step + if y <= self.axis_y_min: + break + ty = self.translate_y( y ) + wnd.draw_line( gc, x0 - 2, ty, x0 + 2, ty ) + if show_numbers: + self.add_axis_number( wnd, gc, y, dec_y, x0 - 25, ty - 6 ) + if show_grid: + wnd.draw_line( gc, 0, ty, width, ty ) + + def add_axis_number( self, wnd, gc, val, dec, x, y ): + pl = pango.Layout( self.draw.get_pango_context() ) + msg = rounded_number( val, dec ) + pl.set_text( msg ) + wnd.draw_layout( gc, x, y, pl ) + + # Draw the graph of the function + def draw_function( self, wnd, gc, poly ): + # Setup some graph parameters + width = self.width + height = self.height + x_start = self.axis_x_min + x_end = self.axis_x_max + step = ( x_end - x_start ) / width + + # Set the line color + old_fg = gc.foreground + fg = poly.get_color() + if fg != None: + gc.set_foreground( fg ) + + # Erase old point + old_x, old_y = ( None, None ) + + x = x_start + while x < x_end: + y = poly.calculate( x ) + x = x + step + self.draw_line( wnd, gc, old_x, old_y, x, y ) + old_x = x + old_y = y + + gc.set_foreground( old_fg ) + + def translate_x( self, x ): + width = self.width + x_min = self.axis_x_min + x_max = self.axis_x_max + return int( ( width * ( x - x_min ) ) / ( x_max - x_min ) ) + + def rev_translate_x( self, x ): + width = self.width + x_min = self.axis_x_min + x_max = self.axis_x_max + return x_min + ( x * ( x_max - x_min ) ) / width + + def translate_y( self, y ): + height = self.height + y_min = self.axis_y_min + y_max = self.axis_y_max + return int( height - ( height * ( y - y_min ) ) / ( y_max - y_min ) ) + + def rev_translate_y( self, y ): + height = self.height + y_min = self.axis_y_min + y_max = self.axis_y_max + return y_min + ( ( height - y ) * ( y_max - y_min ) ) / height + + def draw_line( self, wnd, gc, old_x, old_y, x, y ): + if old_x == None: + wnd.draw_point( gc, self.translate_x( x), self.translate_y( y ) ) + return + + x0 = self.translate_x( old_x ) + y0 = self.translate_y( old_y ) + x1 = self.translate_x( x ) + y1 = self.translate_y( y ) + if ( x0 < 0 or x0 > self.width ): + return + if ( x1 < 0 or x1 > self.width ): + return + if ( y0 < 0 or y0 > self.height ): + return + if ( y1 < 0 or y1 > self.height ): + return + + wnd.draw_line( gc, x0, y0, x1, y1 ) + + def update_coords( self, widget, event ): + if self.width == 0 or self.height == 0: + return + if event.is_hint: + x, y, = event.window.pointer + state = event.window.pointer_state + else: + x = event.x + y = event.y + state = event.state + self.draw.get_pointer() + x = self.rev_translate_x( x ) + y = self.rev_translate_y( y ) + msg = '(%4.2f,%4.2f)' % ( x, y ) + self.label_coords.set_text( msg ) + +# Uncomment the following line for stand-alone application +#Graph( False ) |