# cs1graphics module
#
# Copyright David Letscher and Michael Goldwasser, 2007
#
# Ad Hoc Version (Nov 2007)
#     This is a preliminary version that does not yet support events.

# License:
#
#    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 2 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, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
"""
Library for drawing and manipulating basic shapes.
"""
_debug = 0    # no debugging
#_debug = 3   # can be 1 or 2 or 3

import copy as _copy
import math as _math
import random as _random

_ourRandom = _random.Random()
_ourRandom.seed(1234) # initialize the random seed so that treatment of equal depths is reproducible

_underlyingLibrary = None
try:
    import Tkinter as _Tkinter

    _tkroot = _Tkinter.Tk()
    _tkroot.withdraw()

    _underlyingLibrary = 'Tkinter'

    class _RenderedCanvas(object):
        def __init__(self, canvas, w, h, background, title, refresh):
            self._parent = canvas
            
            self._tkWin = _Tkinter.Toplevel()
            self._tkWin.protocol("WM_DELETE_WINDOW", self.close)
            self._tkWin.title(title)
            self._canvas = _Tkinter.Canvas(self._tkWin,width=w,height=h,background=getTkColor(background))
            self._canvas.pack(expand=False,side=_Tkinter.TOP)
            self._tkWin.resizable(0,0)
            self.refresh()
    
        def setBackgroundColor(self, color, doRefresh=True):
            self._canvas.config(background=getTkColor(color))
            if doRefresh:
                self.refresh()
    
        def setWidth(self, w, doRefresh=True):
            self._canvas.config(width=w)
            if doRefresh:
                self.refresh()
    
        def setHeight(self, h, doRefresh=True):
            self._canvas.config(height=h)
            if doRefresh:
                self.refresh()
    
        def setTitle(self, title, doRefresh=True):
            self._tkWin.title(title)
            if doRefresh:
                self.refresh()

        def refresh(self):
            if _debug>=3:
                import inspect
                s = inspect.stack()
                s.reverse()
                for i in s[:-1]:
                    if i[4]:   # avoid interpreter command
                        print 'line %4d, method %-10s, command: %s'%(i[2],i[3],i[4][0].strip())
            if _debug>=1: print "TkCanvas being refreshed"
            self._canvas.update()    # unsafe if invoked by event handler?
#            self._canvas.update_idletasks()
        
        def open(self):
            """
            Open the canvas window (if not already open).
            """
            self._tkWin.deiconify()

        def close(self):
            """
            Close the canvas window (if not already closed).
            """
            self._tkWin.withdraw()

        def remove(self, obj, doRefresh=True):
            self._parent._objectDepths.remove(obj)
            self._canvas.delete(obj._object)
            if doRefresh:
                self.refresh()

        def saveToFile(self, filename):
            parent = self._parent
            background = Rectangle(parent.getWidth()+2, parent.getHeight()+2)
            background.move( float(parent.getWidth())/2, float(parent.getHeight())/2 )
            background.setFillColor(parent.getBackgroundColor())
            background.setBorderColor(parent.getBackgroundColor())

            maxDepth = 0
            for o in parent._contents:
                if o.getDepth() > maxDepth:
                    maxDepth = o.getDepth()

            background.setDepth(maxDepth+1)
            parent.add(background)
            parent.refresh()
            self._canvas.postscript(file=filename)
            parent.remove(background)
            parent.refresh()
            

        def _moveToDepth(self, obj, depth, doRefresh=True):
            self._parent._objectDepths.remove(obj)
            self._parent._objectDepths.add(obj, depth)
            above = self._parent._objectDepths.getPred(obj)
            if above:
                self._canvas.lower(obj._object, above._object)
            else:
                self._canvas.lift(obj._object)

            if _debug>=3:
                print "Moving", obj, "below", above
                print "Depths", self._parent._objectDepths.depth(obj),
                if above:
                    print "below", self._parent._objectDepths.depth(above),
                print
            if doRefresh:
                self.refresh()
                
    def getTkColor(color):
        if color._transparent:
            return ""
        return "#%04X%04X%04X" % (256*color.getColorValue()[0], 256*color.getColorValue()[1], 256*color.getColorValue()[2])

    class _RenderedDrawable(object):
        def __init__(self, drawable, canvas, doRefresh=True):
            self._drawable = drawable
            self._canvas = canvas
            self._object = None
    
        def update(self, transform, doRefresh=True):
            pass

        def remove(self, doRefresh=True):
            self._canvas._canvas.remove(self, doRefresh)
    
    class _RenderedShape(_RenderedDrawable):
        def __init__(self, drawable, canvas, doRefresh=True):
            _RenderedDrawable.__init__(self, drawable, canvas, doRefresh)

        def update(self, transform, doRefresh=True):
            _RenderedDrawable.update(self, transform, False)
            # cannot update border color because it is called 'outline' for fillables, yet 'fill' for path/line
            self._canvas._canvas._canvas.itemconfigure(self._object, width=self._drawable._borderWidth)
            if doRefresh:
                self._canvas._canvas.refresh()
    
    class _RenderedFillableShape(_RenderedShape):
        def __init__(self, drawable, canvas, doRefresh=True):
            _RenderedShape.__init__(self, drawable, canvas, doRefresh)
    
        def update(self, transform, doRefresh=True):
            _RenderedShape.update(self, transform, False)
            self._canvas._canvas._canvas.itemconfigure(self._object, fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor))
            if doRefresh:
                self._canvas._canvas.refresh()
    
    class _RenderedImage(_RenderedDrawable):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedDrawable.__init__(self, drawable, canvas)
            self._img = _Tkinter.PhotoImage(file=self._drawable._filename, master=self._canvas._canvas._canvas)
            self._object = self._canvas._canvas._canvas.create_image(transform.image(Point(0,0)).get(),
                                                              image=self._img,
                                                              anchor="nw")
            self._canvas._canvas._moveToDepth(self, transform._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()

        def update(self, transform, doRefresh=True):
            # Update size and position
            composed = transform * self._drawable._transform

            self._canvas._canvas._canvas.coords(self._object, composed.image(Point(0,0)).getX(),  composed.image(Point(0,0)).getY())
            self._canvas._canvas._moveToDepth(self, composed._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()
        

    class _RenderedText(_RenderedDrawable):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedDrawable.__init__(self, drawable, canvas)
            self._object = canvas._canvas._canvas.create_text(transform.image(Point(0,0)).get(),
                                                              text=self._drawable._text,
                                                              fill=getTkColor(self._drawable._color),
                                                              font=('Helvetica', self._drawable._size, 'normal'),
                                                              anchor="nw")
            self._canvas._canvas._moveToDepth(self, transform._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()
    
        def update(self, transform, doRefresh=True):
            # Update size and position
            composed = transform * self._drawable._transform

            self._canvas._canvas._canvas.coords(self._object, composed.image(Point(0,0)).getX(),  composed.image(Point(0,0)).getY())
            self._canvas._canvas._canvas.itemconfigure(self._object, text=self._drawable._text,
                                                       fill=getTkColor(self._drawable._color),
                                                       font=('Helvetica', self._drawable._size, 'normal'))
            self._canvas._canvas._moveToDepth(self, composed._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()


    class _RenderedCircle(_RenderedFillableShape):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedFillableShape.__init__(self, drawable, canvas, False)
            center = transform.image(Point(0.,0.))
            radius = _math.sqrt(abs(transform.det()))
            self._object = canvas._canvas._canvas.create_oval(center.getX() - radius, center.getY() - radius,
                                                              center.getX() + radius, center.getY() + radius,
                                                              fill=getTkColor(self._drawable._fillColor),
                                                              outline=getTkColor(self._drawable._borderColor),
                                                              width=self._drawable._borderWidth)
            self._canvas._canvas._moveToDepth(self, transform._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()
    
        def update(self, transform, doRefresh=True):
            # Update interior and border
            _RenderedFillableShape.update(self, transform, False)
            
            # Update size and position
            composed = transform * self._drawable._transform
            center = composed.image(Point(0.,0.))
            radius = _math.sqrt(abs(composed.det()))

            self._canvas._canvas._canvas.coords(self._object,center.getX() - radius, center.getY() - radius, center.getX() + radius, center.getY() + radius)

            self._canvas._canvas._moveToDepth(self, composed._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()
    
    class _RenderedRectangle(_RenderedFillableShape):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedFillableShape.__init__(self, drawable, canvas, False)
            self._object = canvas._canvas._canvas.create_polygon(transform.image(Point(-.5,-.5)).get(),
                                                                 transform.image(Point(-.5,.5)).get(),
                                                                 transform.image(Point(.5,.5)).get(),
                                                                 transform.image(Point(.5,-.5)).get(),
                                                                 fill=getTkColor(self._drawable._fillColor),
                                                                 outline=getTkColor(self._drawable._borderColor),
                                                                 width=self._drawable._borderWidth)
            self._canvas._canvas._moveToDepth(self, transform._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()

        def update(self, transform, doRefresh=True):
            # Update interior and border
            _RenderedFillableShape.update(self, transform, False)
            
            # Update size and position
            composed = transform * self._drawable._transform

            self._canvas._canvas._canvas.coords(self._object,
                                                composed.image(Point(-.5,-.5)).getX(),
                                                composed.image(Point(-.5,-.5)).getY(),
                                                composed.image(Point(-.5,.5)).getX(),
                                                composed.image(Point(-.5,.5)).getY(),
                                                composed.image(Point(.5,.5)).getX(),
                                                composed.image(Point(.5,.5)).getY(),
                                                composed.image(Point(.5,-.5)).getX(),
                                                composed.image(Point(.5,-.5)).getY())
            self._canvas._canvas._moveToDepth(self, composed._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()

    class _RenderedPath(_RenderedShape):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedShape.__init__(self, drawable, canvas, False)

            if len(self._drawable._points)>1:   # path must have two points in TK
                self._object = self._createObj(transform)
                self._canvas._canvas._moveToDepth(self, transform._depth, False)
                if doRefresh:
                    self._canvas._canvas.refresh()
            else:
                self._object = None
    
        def update(self, transform, doRefresh=True):
            # Update border properties
            _RenderedShape.update(self, transform, False)
            
            # Update size and position
            composed = transform * self._drawable._transform

            statement = "self._canvas._canvas._canvas.coords(self._object"
            for p in self._drawable._points:
                image = composed.image(p)
                statement += ", %d, %d" % (image.getX(), image.getY())
            statement += ")"
            exec statement
            self._canvas._canvas._moveToDepth(self, composed._depth, False)
            if doRefresh:
                self._canvas._canvas.refresh()

        def update(self, transform, doRefresh=True):
            # Update size and position
            composed = transform * self._drawable._transform

            if self._object:
                if len(self._drawable._points)>1:  # existing non-trivial path
                    # Update border properties
                    _RenderedShape.update(self, composed, False)
                    statement = "self._canvas._canvas._canvas.coords(self._object"
                    for p in self._drawable._points:
                        image = composed.image(p)
                        statement += ", %d, %d" % (image.getX(), image.getY())
                    statement += ")"
                    exec statement
                    self._canvas._canvas._moveToDepth(self, composed._depth, False)
                    if doRefresh:
                        self._canvas._canvas.refresh()
                else:   # existing path must be remove
                    self.remove(doRefresh)
                    self._object=None
            else:  # no previously existing path image
                if len(self._drawable._points)>1:
                    self._object = self._createObj(composed)
                    self._canvas._canvas._moveToDepth(self, composed._depth, doRefresh)
    
        def _createObj(self, transform):
            """
            Actually creates a path (assumed to be non-trivial).
            """
            statement = "newObj = self._canvas._canvas._canvas.create_line("
            for p in self._drawable._points:
                image = transform.image(p)
                statement += "%d, %d, " % (image.getX(), image.getY())
            statement += "fill=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth)"
            exec statement
            return newObj
            
    class _RenderedPolygon(_RenderedFillableShape):
        def __init__(self, drawable, canvas, transform, doRefresh=True):
            _RenderedFillableShape.__init__(self, drawable, canvas, False)

            if len(self._drawable._points)>0:
                self._object = self._createObj(transform)
                self._canvas._canvas._moveToDepth(self, transform._depth, False)
                if doRefresh:
                    self._canvas._canvas.refresh()
            else:
                self._object = None

        def update(self, transform, doRefresh=True):
            # Update size and position
            composed = transform * self._drawable._transform

            if self._object:
                if len(self._drawable._points)>0:  # existing non-trivial polygon
                    # Update border properties
                    _RenderedFillableShape.update(self, composed, False)
                    statement = "self._canvas._canvas._canvas.coords(self._object"
                    for p in self._drawable._points:
                        image = composed.image(p)
                        statement += ", %d, %d" % (image.getX(), image.getY())
                    statement += ")"
                    exec statement
                    self._canvas._canvas._moveToDepth(self, composed._depth, False)
                    if doRefresh:
                        self._canvas._canvas.refresh()
                else:   # existing polygon must be remove
                    self.remove(doRefresh)
                    self._object=None
            else:  # no previously existing polygon image
                if len(self._drawable._points)>0:
                    self._object = self._createObj(composed)
                    self._canvas._canvas._moveToDepth(self, composed._depth, doRefresh)

        def _createObj(self, transform):
            """
            Actually creates a polygon (assumed to be non-trivial).
            """
            statement = "newObj = self._canvas._canvas._canvas.create_polygon("
            for p in self._drawable._points:
                image = transform.image(p)
                statement += "%d, %d, " % (image.getX(), image.getY())
            statement += "fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth)"
            exec statement
            return newObj
            
    

except:
    pass
 
try:
    from java.awt import Color
    from javax.swing import JFrame
    _underlyingLibrary = 'AWT'
    
    class object:
        pass
    
    True = 1
    False = 0
    bool = int

    _isinstance = isinstance
    def isinstance(obj,classes):
        answer = False
        # convert to tuple of classes, if not already
        if type(classes)!=type((0,0)):
            classes = (classes,)

        for cls in classes:
            if cls==str:
                if type(obj)==type(''):
                   answer = True
                   break
            elif cls==int:
                if type(obj)==type(1):
                    answer = True
                    break
            elif cls==float:
                if type(obj)==type(1.):
                    answer = True
                    break
            elif cls==list:
                if type(obj)==type([]):
                    answer = True
                    break
            elif cls==dict:
                if type(obj)==type({}):
                    answer = True
                    break
            elif cls==tuple:
                if type(obj)==type(()):
                    answer = True
                    break
            elif cls==bool:
                if obj==True or obj==False:
                    answer = True
                    break
            else:
                answer = _isinstance(obj,classes)
        return answer

    
    class _RenderedCanvas(JFrame):
        def __init__(self, canvas, w, h, background, title, refresh):
            JFrame.__init__(self, title, size=(w,h), visible=1)
            self.background = background.getColorValue()[0], background.getColorValue()[1], background.getColorValue()[2]
            self._canvas = canvas
            self._graphics = self.contentPane.graphics
            self._autoRefresh = refresh
    
        def paint(self,graphics):
            pass
    
        def setBackgroundColor(self, color):
            self.background = color.getColorValue()[0], color.getColorValue()[1], color.getColorValue()[2]
            if self._autoRefresh:
                self.refresh()
    
        def close(self):
            """
            Close the canvas window (if not already closed).
            """
            pass
    
        def refresh(self):
            pass
            
    
    class _RenderedShape(object):
        def __init__(self, drawable, canvas):
            self._drawable = drawable
            self._canvas = canvas

        def draw(self, transform):
            pass
    
        def update(self, transform):
            pass
    
    class _RenderedFillableShape(_RenderedShape):
        def __init__(self, drawable, canvas):
            _RenderedShape.__init__(self, drawable, canvas)
    
        def update(self, transform):
            pass
            
    
    class _RenderedCircle(_RenderedFillableShape):
        def __init__(self, drawable, canvas, transform):
            _RenderedFillableShape.__init__(self, drawable, canvas)
            self._transform = transform
            self._object = None
    
        def draw(self, canvas, transform):
            canvas.refresh()
    
        def update(self, transform):
            canvas.refresh()

except:
    pass

if _underlyingLibrary == None:
    _underlyingLibrary = 'Null'
    
    class _RenderedCanvas:
        pass

class Point(object):
    """
    Stores a two-dimensional point using cartesian coordinates.
    """
    
    def __init__(self, initialX=0, initialY=0):
        """
        Create a new point instance.

        initialX   x-coordinate of the point (defaults to 0)
        initialY   y-coordinate of the point (defaults to 0)
        """
        if not isinstance(initialX, (int,float)):
            raise TypeError, 'numeric value expected for x-coodinate'

        if not isinstance(initialY, (int,float)):
            raise TypeError, 'numeric value expected for y-coodinate'

        self._x = initialX
        self._y = initialY

    def getX(self):
        """
        Returns the x-coordinate.
        """
        return self._x

    def setX(self, val):
        """
        Set the x-coordinate to val.
        """
        self._x = val

    def getY(self):
        """
        Returns the y-coordinate.
        """
        return self._y

    def setY(self, val):
        """
        Set the y-coordinate to val.
        """
        self._y = val

    def get(self):
        """
        Returns an (x,y) tuple.
        """
        return self._x, self._y

    def scale(self, factor):
        self._x *= factor
        self._y *= factor
      
    def distance(self, other):
        dx = self._x - other._x
        dy = self._y - other._y
        return _math.sqrt(dx * dx + dy * dy)

    def normalize(self):
        mag = self.distance( Point() )
        if mag > 0:
            self.scale(1/mag)

    def __str__(self):
        return '<' + str(self._x) + ',' + str(self._y) + '>'

    def __add__(self, other):
        return Point(self._x + other._x, self._y + other._y)

    def __mul__(self, operand):
        if isinstance(operand, (int,float)):                 # multiply by constant
            return Point(self._x * operand, self._y * operand)
        elif isinstance(operand, Point):                     # dot-product
            return self._x * operand._x + self._y * operand._y

    def __rmul__(self, operand):
        return self * operand

    def __xor__(self, angle):
        """
        Returns a new point instance representing the original, rotated about the origin.

        angle    number of degrees of rotation (clockwise)
        """
        angle = -_math.pi*angle/180.
        mag = _math.sqrt(self._x * self._x + self._y * self._y)
        return Point(self._x * _math.cos(angle) - self._y * _math.sin(angle), self._x * _math.sin(angle) + self._y * _math.cos(angle))

class _TraceToCanvas(object):
    """
    Represents a single nested path leading from a Drawable up to a Canvas upon which it is Rendered
    """
    def __init__(self, drawable, parent):
        """
        parent should either be a Canvas or else a relative _TraceToCanvas instance
        """
        self._drawable = drawable
        if isinstance(parent,Canvas):
            self._chain = (parent,)
            self._chainTrans = Transformation()
        else:
            self._chain = parent._chain + (parent._drawable,)
            self._chainTrans = parent._chainTrans * parent._drawable._transform

        if _debug>=3:
            print "In _TraceToCanvas constructor for",hex(id(self))
            print "  drawable =", drawable
            print "  parent =", parent
            print "  self._chain =", self._chain
            print "  self._chainTrans._depth =", self._chainTrans._depth

        self.render()

    def __getitem__(self,y):
        return self._chain.__getitem__(y)

    def getBase(self):
        return self._drawable

    def getTrans(self):
        return self._chainTrans

    def setTrans(self,newtrans):
        self._chainTrans = newtrans

    def __contains(self,value):
        return value in self._chain
    
    def getChain(self):
        return self._chain

    def getRendered(self):
        return self._rendered

    def startswith(self, othertrace):
        mylen = len(self._chain)
        otherlen = len(othertrace._chain)
        return mylen>= otherlen and self[ :otherlen] == othertrace[ : ]

    def render(self, doRefresh=True):
        # create actual rendered objects, if possible
        self._rendered = None

        # determine underlying class to use
        # (better way: have each class maintain Class._renderClass)
        renderClass = None
        if isinstance(self._drawable, Polygon):
            renderClass = _RenderedPolygon
        elif isinstance(self._drawable, (Path,Segment)):
            renderClass = _RenderedPath
        elif isinstance(self._drawable, Circle):
            renderClass = _RenderedCircle
        elif isinstance(self._drawable, Rectangle):
            renderClass = _RenderedRectangle
        elif isinstance(self._drawable, Text):
            renderClass = _RenderedText
        elif isinstance(self._drawable, Image):
            renderClass = _RenderedImage

        if renderClass:
            canv = self[0]
            if canv._autoRefresh:   # make low-level change now
                self._rendered = renderClass(self._drawable, canv, self._chainTrans*self._drawable._transform, doRefresh)
            else:
                canv._addToUpdateQueue(self.render)  # really should pack False as extra parameter.
        
    def update(self, doRefresh=True):
        """
        Underlying mutation possible for the rendered object associated with this trace.

        Should refresh that rendering, depending upon canvas refresh mode
        """
        if self._rendered:
            canv = self[0]
            if canv._autoRefresh:  # make low-level change now
                self._rendered.update(self._chainTrans, doRefresh)
            else:
                canv._addToUpdateQueue(self.update)

    def destroy(self, doRefresh=True):
        """
        Get rid of underlying object (if there)
        """
        if self._rendered:
            canv = self[0]
            if canv._autoRefresh:  # make low-level change now
                self._rendered.remove(doRefresh)
            else:
                canv._addToUpdateQueue(self.destroy)

    def __repr__(self):
        return '\n   _TraceToCanvas '+str(hex(id(self)))+':\n   chain='+repr(self._chain)+'\n   drawable='+repr(self._drawable)+'\n   rendered='+repr(self._rendered)+'\n   chainTrans='+repr(self._chainTrans)

class Transformation(object):
    def __init__(self, value=None, depth=None):
        if value:
            self._matrix = value[:4]
            self._translation = value[4:]
        else:
            self._matrix = (1.,0.,0.,1.)
            self._translation = (0.,0.)
        if depth:
            self._depth = depth
        else:
            self._depth = []

    def __repr__(self):
        return '\n     Transformation '+str(hex(id(self)))+':\n     matrix = %s\n     translation = %s\n     depth = %s\n'%(repr(self._matrix),
                                                                               repr(self._translation),
                                                                               repr(self._depth))
        
    def image(self, point):
        return Point( self._matrix[0]*point._x + self._matrix[1]*point._y + self._translation[0],
            self._matrix[2]*point._x + self._matrix[3]*point._y + self._translation[1])

    def inv(self):
        detinv = 1. / self.det()
        m = ( self._matrix[3] * detinv, -self._matrix[1] * detinv, -self._matrix[2] * detinv, self._matrix[0] * detinv )
        t = ( -m[0]*self._translation[0] - m[1]*self._translation[1], -m[2]*self._translation[0] - m[3]*self._translation[1])

        return Transformation(m+t, self._depth)

    def __mul__(self, other):
        m = ( self._matrix[0] * other._matrix[0] + self._matrix[1] * other._matrix[2],
            self._matrix[0] * other._matrix[1] + self._matrix[1] * other._matrix[3],
            self._matrix[2] * other._matrix[0] + self._matrix[3] * other._matrix[2],
            self._matrix[2] * other._matrix[1] + self._matrix[3] * other._matrix[3])

        p = self.image( Point(other._translation[0], other._translation[1]) )

        return Transformation(m + (p.getX(), p.getY()), self._depth + other._depth)

    def det(self):
        return (self._matrix[0] * self._matrix[3] - self._matrix[1] * self._matrix[2])

class Color(object):
    """
    Represents a color.

    Color can be specified by name or RGB value, and can be made transparent.
    """
    _colorValues = {   # more available from /usr/lib/X11/rgb.txt
        'black'            : (  0,  0,  0),
        'blue'             : (  0,  0,255),
        'brown'            : (165, 42, 42),
        'burlywood'        : (222, 184, 135),
        'cyan'             : (  0,255,255),
        'darkblue'         : (  0,  0,100),
        'darkgray'         : (170,170,170),
        'darkgreen'        : (  0,100,  0),
        'gray'             : (128,128,128),
        'gray10'           : (230,230,230),
        'gray30'           : (179,179,179),
        'gray50'           : (128,128,128),
        'green'            : (  0,255,  0),
        'grey'             : (128,128,128),
        'lightblue'        : (174,217,231),
        'lightgray'        : (211,211,211),
        'lightgreen'       : (145,239,145),
        'lightgrey'        : (211,211,211),
        'lightpink'        : (255,183,194),
        'midnightblue'     : ( 25, 25,112),
        'orange'           : (255,128,  0),
        'purple'           : (128,  0,128),
        'red'              : (255,  0,  0),
        'saddle brown'     : (139, 69, 19),
        'skyblue'          : (136,207,236),
        'sky blue'         : (136,207,236),
        'tan'              : (211,181,141),
        'turquoise'        : ( 64,225,209),
        'white'            : (255,255,255),
        'yellow'           : (255,255,  0)
    }

    def __init__(self, colorChoice='White'):
        """
        Create a new instance of Color.

        The parameter can be either:
           - a string with the name of the color
           - an (r,g,b) tuple
           - an existing Color instance (which will be cloned)

        It defaults to 'White'
        """
        # we intentionally have Drawable objects using a color
        # register with the color instance, so that when the color is
        # muated, the object can be informed that it has changed
        self._drawables = []
        
        if isinstance(colorChoice,str):
            try:
                self.setByName(colorChoice)
            except ValueError, ve:
                raise ValueError, str(ve)
        elif isinstance(colorChoice,tuple):
            try:
                self.setByValue(colorChoice)
            except ValueError, ve:
                raise ValueError, str(ve)
        elif isinstance(colorChoice,Color):
            self._colorName = colorChoice._colorName
            self._transparent = colorChoice._transparent
            self._colorValue = colorChoice._colorValue
        else:
            raise TypeError, 'Invalid color specification'

    def setByName(self, colorName):
        """
        Set the color to colorName.

        colorName   a string representing a valid name
        
        It colorName is 'Transparent' the resulting color will not
        show up on a canvas.
        """
        if not isinstance(colorName,str):
            raise TypeError, 'string expected as color name'

        self._colorName = colorName
        if self._colorName.lower() == 'transparent':
            self._transparent = True
            self._colorValue = (0,0,0)
        else:
            try:
                self._transparent = False
                self._colorValue = Color._colorValues[self._colorName.lower()]
            except KeyError:
                raise ValueError, '%s is not a valid color name' % colorName
        self._informDrawables()

    def getColorName(self):
        """
        Returns the name of the color.

        If the color was set by RGB value, it returns 'Custom'.
        """
        return self._colorName

    def setByValue(self, rgbTuple):
        """
        Sets the color to the given tuple of (red, green, blue) values.
        """
        if not isinstance(rgbTuple,tuple):
            raise TypeError, '(r,g,b) tuple expected'
        if len(rgbTuple)!=3:
            raise ValueError, '(r,g,b) tuple must have three components'
        self._transparent = False
        self._colorName = 'Custom'
        self._colorValue = rgbTuple
        self._informDrawables()

    def getColorValue(self):
        """
        Returns a tuple of the (red, green, blue) color components.
        """
        return (self._colorValue[0], self._colorValue[1], self._colorValue[2])

    def isTransparent(self):
        """
        Returns a boolean variable indicating if the current color is transparent.

        A return value of True indicates the color is not visibile.
        """
        return self._transparent

    def __repr__(self):
        """
        Returns the name of the color, if named.  Otherwise returns the (r,g,b) value.
        """
        if self._colorName == 'Custom':
            return self._colorValue.__repr__()
        else:
            return self._colorName

    def _register(self, drawable):
        """
        Called to register a drawable with this color instance
        """
        if drawable not in self._drawables:
            self._drawables.append(drawable)
    
    def _unregister(self, drawable):
        """
        Called to unregister a drawable with this color instance
        """
        if drawable in self._drawables:
            self._drawables.remove(drawable)
    
    def _informDrawables(self):
        """
        When the color instance has been mutated, we must inform those registered drawables.
        """
        for drawable in self._drawables:
            drawable._objectChanged()

class _SortedSet(list):
    def __init__(self, initial=None):
        list.__init__(self)  # calls the parent class constructor
        if initial:
            self.extend(initial)

    def indexAfter(self, value):
        index = 0
        while index < len(self) and value >= self[index]:
            index += 1
        return index

    def insert(self, value):
        if value not in self:
            index = self.indexAfter(value)
            list.insert(self, index, value)   # the parent's method
            
    def append(self, object):
        self.insert(object)

    def extend(self, other):
        for element in other:
            self.insert(element)
        
    def __add__(self, other):
        result = _SortedSet(self)    # creates new copy of self
        result.extend(other)
        return result

    def sort(self):
        pass

class _DepthManager(object):
    def __init__(self):
        self._set = _SortedSet()

    def add(self, obj, depth):
        self._set.insert((depth,obj))

    def remove(self, obj):
        index = 0
        while index < len(self._set):
            if self._set[index][1] == obj:
                self._set.remove(self._set[index])
                return
            index += 1

    def getPred(self, obj):
        index = 0
        while index < len(self._set):
            if index + 1 < len(self._set) and self._set[index+1][1] == obj:
                return self._set[index][1]
            index += 1
        return None

    def depth(self, obj):
        index = 0
        while index < len(self._set):
            if self._set[index][1] == obj:
                return self._set[index][0]
            index += 1

class _Container(object):
    def __init__(self):
        self._contents = []
        self._lastUserDrawn = [None,0]   # for tracking objects from user-defined classes

    def __contains__(self, obj):
        return obj in self._contents

    def add(self, drawable):
        """
        Add the drawable object to the container.
        """
        # not doing error checking here, as we want tailored messages for Canvas and Layer
        self._contents.append(drawable)

    def remove(self, drawable):
        """
        Removes the drawable object from the container.
        """
        # not doing error checking here, as we want tailored messages for Canvas and Layer
        self._contents.remove(drawable)

    def clear(self):
        """
        Removes all objects from the container.
        """
        contents = list(self._contents)  # intentional clone
        for drawable in contents:
            self.remove(drawable)


class Canvas(_Container):
    """
    Represents a window which can be drawn upon.
    """

    def __init__(self, w=200, h=200, background=None, title="Graphics canvas", autoRefresh=True):
        """
        Create a new drawing canvas.

        A new canvas will be created.
            w               width of drawing area (defaults to 200)
            h               height of drawing area (defaults to 200)
            background      color of the background (defaults to White)
            title           window title (defaults to "Graphics Canvas")
            autoRefresh     whether auto-refresh mode is used (default to True)
        """
        _Container.__init__(self)

        if not background:
            background = 'white'

        if not isinstance(w, (int,float)):
            raise TypeError, 'numeric value expected for width'
        if not isinstance(h, (int,float)):
            raise TypeError, 'numeric value expected for height'
        if not isinstance(title,str):
            raise TypeError, 'string expected as title'
        if not isinstance(autoRefresh,bool):
            raise TypeError, 'autoRefresh flag should be a bool'
        
        if isinstance(background,Color):
            self._backgroundColor = background
        else:
            try:
                self._backgroundColor = Color(background)
            except TypeError,te:
                raise TypeError,str(te)
            except ValueError,ve:
                raise ValueError,str(ve)

        self._width = w
        self._height = h
        self._title = title
        self._autoRefresh = autoRefresh
        
        self._canvas = _RenderedCanvas(self, w, h, self._backgroundColor, title, autoRefresh)
        self._updateQueue = []  # Tuples: (trace, callbackFunction)
        self._objectDepths = _DepthManager()
            
    def setBackgroundColor(self, backgroundColor):
        """
        Set the background color.

        The parameter can be either:
           - a string with the name of the color
           - an (r,g,b) tuple
           - an existing Color instance
        """
        if isinstance(backgroundColor,Color):
            self._backgroundColor = backgroundColor
        else:
            try:
                self._backgroundColor = Color(backgroundColor)
            except TypeError,te:
                raise TypeError,str(te)
            except ValueError,ve:
                raise ValueError,str(ve)
        if self._autoRefresh:
            self._canvas.setBackgroundColor(self._backgroundColor)

    def getBackgroundColor(self):
        """
        Returns the background color as an instance of Color.
        """
        return self._backgroundColor
        
    def setWidth(self, w):
        """
        Resets the canvas width to w.
        """
        if not isinstance(w, (int,float)):
            raise TypeError, 'numeric value expected for width'
        if w<=0:
            raise ValueError, 'width must be positive'
        self._width = w
        if self._autoRefresh:
            self._canvas.setWidth(w)

    def getWidth(self):
        """
        Get the width of the canvas.
        """
        return self._width

    def setHeight(self, h):
        """
        Resets the canvas height to h.
        """
        if not isinstance(h, (int,float)):
            raise TypeError, 'numeric value expected for height'
        if h<=0:
            raise ValueError, 'height must be positive'
        self._height = h
        if self._autoRefresh:
            self._canvas.setHeight(h)

    def getHeight(self):
        """
        Get the height of the canvas.
        """
        return self._height

    def setTitle(self, title):
        """
        Set the title for the canvas window.
        """
        if not isinstance(title,str):
            raise TypeError, 'string expected as title'
        self._title = title
        if self._autoRefresh:
            self._canvas.setTitle(title)

    def getTitle(self):
        """
        Get the title of the window.
        """
        return self._title

    def close(self):
        """
        Close the canvas window (if not already closed).
        """
        self._canvas.close()

    def open(self):
        """
        Opens a graphic window (if not already open).
        """
        self._canvas.open()

    def add(self, drawable):
        """
        Add the drawable object to the canvas.
        """
        if not isinstance(drawable,Drawable):
            raise TypeError, 'can only add Drawable objects to a Canvas'
        if drawable in self._contents:
            raise ValueError, 'object already on the Canvas'
        _Container.add(self, drawable)
        if _debug>=2: print "Adding drawable to canvas, beginning draw cycle, drawable =", drawable
        drawable._draw(self)

    def remove(self, drawable):
        """
        Removes the drawable object from the canvas.
        """
        if drawable not in self._contents:
            raise ValueError, 'object not currently on the Canvas'
        _Container.remove(self, drawable)
        drawable._undraw(self)

    def setAutoRefresh(self, autoRefresh=True):
        """
        Change the auto-refresh mode to either True or False.

        When True (the default), every change to the canvas or to an
           object drawn upon the canvas will be immediately rendered to
           the screen.

        When False, all changes are recorded internally, yet not shown
        on the screen until the next subsequent call to the refresh()
        method of this canvas.  This allows multiple changes to be
        buffered and rendered all at once.
        """
        if not isinstance(autoRefresh,bool):
            raise TypeError, 'autoRefresh flag should be a bool'
        if autoRefresh and not self._autoRefresh:
            self.refresh()    # flush the current queue
        self._autoRefresh = autoRefresh

    def refresh(self):
        """
        Forces all internal changes to be rendered to the screen.

        This method is only necessary when the auto-refresh property
        of the canvas has previously been turned off.
        """
        if not self._autoRefresh:
            self._canvas.setHeight(self._height, False)  # delay refresh
            self._canvas.setWidth(self._width, False)
            self._canvas.setBackgroundColor(self._backgroundColor, False)
            self._canvas.setTitle(self._title, False)

            self._autoRefresh = True   # temporarily, to force rendering in following callbacks
            for update in self._updateQueue:
                update(False)  # delay all refreshing until the end
            self._autoRefresh = False  # undo temporary toggle
            self._updateQueue = []
            self._canvas.refresh()

    def saveToFile(self, filename):
        """
        Saves a picture of the current canvas to a file.
        """
        self._canvas.saveToFile(filename)

    def _addToUpdateQueue(self, func):
        self._updateQueue.append(func)

class Drawable(object):
    """
    An object that can be drawn to a graphics canvas.
    """
    
    def __init__(self, reference=None):
        """
        Creates an instance of Drawable.

        referencePoint  local reference point for scaling and rotating
                        (Defaults to Point(0,0) if None specified)
        """
        if reference:
            if not isinstance(reference,Point):
                raise TypeError, 'reference point must be a Point instance'
            self._reference = reference
        else:
            self._reference = Point()
        self._transform = Transformation()
        self._transform._depth = [50, _ourRandom.random()]
        self._traces = {}   # dict of _TraceToCanvas objects for each rendered instance of this object
        self._drones = []   # these are objects which are detected within a UserDefined object

    def move(self, dx, dy):
        """
        Move the object dx units to the right and dy units down.

        Negative values move the object left or up.
        """
        if not isinstance(dx, (int,float)):
            raise TypeError, 'dx must be numeric'
        if not isinstance(dy, (int,float)):
            raise TypeError, 'dy must be numeric'
        self._transform = Transformation( (1.,0.,0.,1.,dx,dy)) * self._transform
        self._objectChanged()

    def moveTo(self, x, y):
        """
        Move the object to align its reference point with (x,y)
        """
        if not isinstance(x, (int,float)):
            raise TypeError, 'x must be numeric'
        if not isinstance(y, (int,float)):
            raise TypeError, 'y must be numeric'
        curRef = self.getReferencePoint()
        self.move(x-curRef.getX(), y-curRef.getY())

    def rotate(self, angle):
        """
        Rotates the object around its current reference point.

        angle    number of degrees of rotation (clockwise)
        """
        if not isinstance(angle, (int,float)):
            raise TypeError, 'angle must be specified numerically'
        angle = -_math.pi*angle/180.
        p = self._localToGlobal(self._reference)
        trans = Transformation((1.,0.,0.,1.)+p.get())
        rot = Transformation((_math.cos(angle),_math.sin(angle),
                              -_math.sin(angle),_math.cos(angle),0.,0.))
        
        self._transform = trans*(rot*(trans.inv()*self._transform))
        self._objectChanged()

    def scale(self, factor):
        """
        Scales the object relative to its current reference point.

        factor    scale is multiplied by this number (must be positive)
        """
        if not isinstance(factor, (int,float)):
            raise TypeError, 'scaling factor must be a positive number'
        if factor<=0:
            raise ValueError, 'scaling factor must be a positive number'
        p = self._localToGlobal(self._reference)
        trans = Transformation((1.,0.,0.,1.)+p.get())
        sca = Transformation((factor,0.,0.,factor,0.,0.))

        self._transform = trans*(sca*(trans.inv()*self._transform))
        self._objectChanged()

    def flip(self, angle=0):
        
        if not isinstance(angle, (int,float)):
          raise TypeError, 'scaling factor must be a positive number'
        
        angle = -_math.pi*angle/180.
        p = self._localToGlobal(self._reference)
        trans = Transformation((1.,0.,0.,1.)+p.get())
        rot = Transformation((_math.cos(angle),_math.sin(angle),
                    -_math.sin(angle),_math.cos(angle),0.,0.))
        rotinv = rot.inv()
        invert = Transformation((-1.,0.,0.,1.,0.,0.))
        
        self._transform = trans*(rotinv*(invert*(rot*(trans.inv()*self._transform))))
        self._objectChanged()
    
    def getReferencePoint(self):
        """
        Return a copy of the current reference point placement.

        Note that mutating that copy has no effect on the drawable object.
        """
        return self._localToGlobal(self._reference)

    def adjustReference(self, dx, dy):
        """
        Move the local reference point relative to its current position.

        Note that the object is not moved at all.
        """
        if not isinstance(dx, (int,float)):
            raise TypeError, 'dx must be numeric'
        if not isinstance(dy, (int,float)):
            raise TypeError, 'dy must be numeric'
        p = self._localToGlobal(self._reference)
        p = Point(p.getX()+dx, p.getY()+dy)
        self._reference = self._globalToLocal(p)

    def setDepth(self, depth):
        """
        Sets the depth of the object.

        Objects with a higher depth will be rendered behind those with lower depths.
        """
        if not isinstance(depth, (int,float)):
            raise TypeError, 'depth must be numeric'
        self._transform._depth[-2] = depth
        self._objectChanged()

    def getDepth(self):
        """
        Returns the depth of the object.
        """
        return self._transform._depth[-2]

    def clone(self):
        """
        Return an exact duplicate of the drawable object.
        """
        return _copy.deepcopy(self)

    def _localToGlobal(self, point):
        if not isinstance(point,Point):
            raise TypeError, 'parameter must be a Point instance'
        return self._transform.image(point)

    def _globalToLocal(self, point):
        if not isinstance(point,Point):
            raise TypeError, 'parameter must be a Point instance'
        return self._transform.inv().image(point)

    def _draw(self, parent):
        """
        Causes the object to be drawn (typically, the method is not called directly).
        
        Parent should either be a Canvas to be drawn on or else a
        TraceToCanvas instance which will be used as relative offset.
        """
        if _debug>=2:
            print "within Drawable._draw for self=",self,"parent=",parent

        if isinstance(parent,Canvas):
            parentContainer = parent
        elif isinstance(parent,_TraceToCanvas):
            parentContainer = parent._drawable
        else:
            raise TypeError, 'Drawable._draw called with unknown parent'

        if self not in parentContainer:
            # figure out the "effective" parent, which is a userDefined object
            # and then adjust our trace accordingly
            owner = parentContainer._lastUserDrawn[0]
            if self not in owner._drones:
                owner._drones.append(self)
            parent = parentContainer._lastUserDrawn[1]
            if _debug>=3:
                print '*****object being drawn on misdirected container.\n  drawable=',self,'container=',parentContainer
                print '     reassigning perceived parent to',parentContainer._lastUserDrawn[1]

            # adjust random component of depth to be serialized
            self._transform._depth[-1] = parentContainer._lastUserDrawn[2]
            parentContainer._lastUserDrawn[2] -= 1

            
        if _debug>=2:
            print "about to create a new (composed) trace"

        newTrace = _TraceToCanvas(self,parent)
        newKey = newTrace.getChain()
        if newKey in self._traces:
            raise StandardError, 'cs1graphics -- Unexpected Error:  duplicated trace'
        self._traces[newKey] = newTrace

        if self._isUserDefined():
            # record information about this object being drawn on parent, in case
            # it's draw ends up causing additional objects to be drawn.
            if _debug>=3: print 'Recording _draw of UserDefined object.\n  drawable=',self,'container=',parentContainer
            parentContainer._lastUserDrawn = [self,newTrace,0]



    def _undraw(self, parent, doRefresh=True):
        """
        Get rid of all renderings of this object relative to the parent container.
        """
        if _debug>=1: print 'within _undraw for self=',self,'parent=',parent
        effectedCanvases = []
        for (key,trace) in self._traces.items():
            can = trace._chain[0]
            if can not in effectedCanvases:
                effectedCanvases.append(can)
            
            if parent == trace.getChain()[-1]:
                trace.destroy(False)
                self._traces.pop(key)

        for drone in self._drones:
            drone._undraw(self,False)

        if doRefresh:
            for can in effectedCanvases:
                can._canvas.refresh()


    def _objectChanged(self, filter=None, doRefresh=True):
        """
        Some trait of this object has been mutated and so all of its
        rendered images may need to be updated.

        filter  a _TraceToCanvas instance.

        If filter given, instead of updated all rendered images, updates will
        only be performed for those which fall under the umbrella of that filter.
        
        """
        if _debug>=2: print 'Drawable._objectChanged called with self=',self,' filter=',filter
        effectedCanvases = []
        for trace in self._traces.values():
            if _debug>=2: print "  considering trace =", trace
            if not filter or filter in trace:
                if _debug>=2: print "  matches filter"

                can = trace._chain[0]
                if can not in effectedCanvases:
                    effectedCanvases.append(can)
                trace.update(False)  # wait a bit before refreshing canvases

                for drawable in self._drones:
                    try:
                        match = drawable._traces[trace.getChain()+(self,)]
                    except KeyError:
                        if _debug>=3:
                            print 'KeyError looking for trace=',trace.getChain()+(self,)
                            print 'drawable=',drawable
                            print 'keys() is ',drawable._traces.keys()
                        raise StandardError, 'cs1graphics -- Unexpected Error:  trace not found'
                    if _debug>=2: print 'found match with trans = ',match.getTrans()
                    match.setTrans(trace.getTrans()*self._transform)
                    if _debug>=2: print 'changing to = ',match.getTrans()

        for drone in self._drones:
            drone._objectChanged(self,False)

        if doRefresh:
            for can in effectedCanvases:
                can._canvas.refresh()

    def _isUserDefined(self):
        """
        Determines if this object has base class outside of our own primitives.

        Typically, this is here for examples of user defined classes
        which inherit directly from Drawable and then implement their
        own _draw method.  We need to watch for hidden components that
        are rendered but not ever directly added to a layer/canvas.
        """
        return not isinstance(self,(Image,Text,Circle,Rectangle,Segment,Path,Layer))

class Shape(Drawable):
    """
    Represents objects that are drawable and have a border.
    """
    
    def __init__(self, reference=None):
        """
        Construct an instance of Shape.

        reference  the initial placement of the shape's reference point.
                     (Defaults to Point(0,0) if None specified)
        """
        if reference and not isinstance(reference,Point):
            raise TypeError, 'reference point must be a Point instance'
        Drawable.__init__(self, reference)
        self._borderColor = Color("Black")
        self._borderWidth = 1

    def setBorderColor(self, borderColor):
        """
        Set the border color to a copy of borderColor.

        The parameter can be either:
           - a string with the name of the color
           - an (r,g,b) tuple
           - an existing Color instance
        """
        old = self._borderColor
        if isinstance(borderColor,Color):
            self._borderColor = borderColor
        else:
            try:
                self._borderColor = Color(borderColor)
            except TypeError,te:
                raise TypeError,str(te)
            except ValueError,ve:
                raise ValueError,str(ve)
        self._objectChanged()

        if self._borderColor is not old:
            self._borderColor._register(self)
            if not isinstance(self,FillableShape) or old is not self._fillColor:
                # this shape no longer using the old color
                old._unregister(self)

    def getBorderColor(self):
        """
        Return the color of the object's border.
        """
        return self._borderColor

    def setBorderWidth(self, borderWidth):
        """
        Set the width of the border to borderWidth.
        """
        if not isinstance(borderWidth, (int,float)):
            raise TypeError, 'Border width must be non-negative number'
        if borderWidth < 0:
            raise ValueError, "A shape's border width cannot be negative."
        self._borderWidth = borderWidth
        self._objectChanged()

    def getBorderWidth(self):
        """
        Returns the width of the border.
        """
        return self._borderWidth

        
class FillableShape(Shape):
    """
    A shape that can be filled in.
    """
    def __init__(self, reference=None):
        """
        Construct a new instance of Shape.

        The interior color defaults to 'transparent'.

        reference  the initial placement of the shape's reference point.
                     (Defaults to Point(0,0) if None specified)
        """
        if reference and not isinstance(reference,Point):
            raise TypeError, 'reference point must be a Point instance'
        Shape.__init__(self, reference)
        self._fillColor = Color("Transparent")

    def setFillColor(self, color):
        """
        Set the interior color of the shape to the color.

        The parameter can be either:
           - a string with the name of the color
           - an (r,g,b) tuple
           - an existing Color instance
        """
        old = self._fillColor
        if isinstance(color,Color):
            self._fillColor = color
        else:
            try:
                self._fillColor = Color(color)
            except TypeError,te:
                raise TypeError,str(te)
            except ValueError,ve:
                raise ValueError,str(ve)
        self._objectChanged()

        if self._fillColor is not old:
            self._fillColor._register(self)
            if self._borderColor is not old:
                # no longer using the old color
                old._unregister(self)


    def getFillColor(self):
        """
        Return the color of the shape's interior.
        """
        return self._fillColor


class Image(Drawable):
    def __init__(self, filename):
        Drawable.__init__(self)
        if not isinstance(filename,str):
            raise TypeError, 'filename must be a string'
        self._filename = filename   # of course, we won't know if this is legitimate until it is added to a canvas
                 
    def rotate(self,angle):
        """
        Not yet implemented.
        """
        raise NotImplementedError,'rotating image is not yet implemented'
        
    def scale(self,factor):
        """
        Not yet implemented.
        """
        raise NotImplementedError,'scaling image is not yet implemented'


class Text(Drawable):
    """
    A piece of text that can be drawn to a canvas.
    """
    def __init__(self, message='', size=12):
        """
        Construct a new Text instance.

        The text color is black, though this can be changed by setColor.
        The reference point for text is the upper-left hand corner of the text.

        message   a string which is to be displayed (empty string by default)
        size      the font size (12 by default)
        """
        if not isinstance(message,str):
            raise TypeError, 'message must be a string'
        if not isinstance(size,int):
            raise TypeError, 'size must be an integer'

        Drawable.__init__(self)
        self._text = message
        self._size = size
        self._color = Color("black")

    def setMessage(self, message):
        """
        Set the string to be displayed.

        message  a string
        """
        if not isinstance(message,str):
            raise TypeError, 'message must be a string'
        self._text = message
        self._objectChanged()

    def getMessage(self):
        """
        Returns the current string.
        """
        return self._text

    def setFontColor(self, color):
        """
        Set the color of the font.

        The parameter can be either:
           - a string with the name of the color
           - an (r,g,b) tuple
           - an existing Color instance
        """
        if isinstance(color,Color):
            self._color = color
        else:
            try:
                self._color = Color(color)
            except TypeError,te:
                raise TypeError,str(te)
            except ValueError,ve:
                raise ValueError,str(ve)
        self._objectChanged()

    def getFontColor(self):
        """
        Returns the current font color.
        """
        return self._color

    def setFontSize(self, fontsize):
        """
        Set the font size.
        """
        if not isinstance(fontsize,int):
            raise TypeError, 'fontsize must be an integer'
        if fontsize<=0:
            raise ValueError, 'fontsize must be positive'
        self._size = fontsize
        self._objectChanged()

    def getFontSize(self):
        """
        Returns the current font size.
        """
        return size

    def rotate(self,angle):
        """
        Not yet implemented.
        """
        raise NotImplementedError,'rotating text is not yet implemented'
        
    def scale(self,factor):
        """
        Not yet implemented.
        """
        raise NotImplementedError,'scaling text is not yet implemented.  Use setSize to change font'

    # Compatibilities to previous version
    setSize = setFontSize
    getSize = getFontSize
    setColor = setFontColor
    getColor = getFontColor
    setText = setMessage
    getText = getMessage

class Circle(FillableShape):
    """
    A circle that can be drawn to a canvas.
    """
    def __init__(self, radius=10, center=None):
        """
        Construct a new instance of Circle.

        radius  the circle's radius.  (Defaults to 10)
        center  a Point representing the placement of the circle's center
                (Defaults to Point(0,0) )

        The reference point for a circle is originally its center.
        """
        if not isinstance(radius, (int,float)):
            raise TypeError, 'Radius must be a number'
        if radius <= 0:
            raise ValueError, "The circle's radius must be positive."
        if center and not isinstance(center,Point):
            raise TypeError, 'center must be specified as a Point'

        FillableShape.__init__(self) # intentionally not sending center
        if not center:
            center = Point()
        self._transform = Transformation( (radius,0.,0.,radius,center.getX(),center.getY()), self._transform._depth )

    def setRadius(self, radius):
        """
        Set the radius of the circle to radius.
        """
        if not isinstance(radius, (int,float)):
            raise TypeError, 'Radius must be a number'
        if radius <= 0:
            raise ValueError, "The circle's radius must be positive."

        factor = float(radius)/self.getRadius()
        self._transform = self._transform * Transformation((factor,0.,0.,factor,0.,0.))
        self._objectChanged()

    def getRadius(self):
        """
        Returns the radius of the circle.
        """
        return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[1]**2)



class Rectangle(FillableShape):
    """
    A rectangle that can be drawn to the canvas.
    """
    def __init__(self, width=20, height=10, center=None):
        """
        Construct a new instance of a Rectangle.

        The reference point for a rectangle is its center.

        width       the width of the rectangle.  (Defaults to 20)
        height      the height of the rectangle.  (Defaults to 10)
        center      a Point representing the placement of the rectangle's center
                    (Defaults to Point(0,0) )
        """
        if not isinstance(width, (int,float)):
            raise TypeError, 'Width must be a number'
        if width <= 0:
            raise ValueError, "The width must be positive."
        if not isinstance(height, (int,float)):
            raise TypeError, 'Height must be a number'
        if height <= 0:
            raise ValueError, "The height must be positive."
        if center and not isinstance(center,Point):
            raise TypeError, 'center must be specified as a Point'

        FillableShape.__init__(self)  # intentionally not sending center point

        if not center:
            center = Point(0,0)

        self._transform = Transformation( (width, 0., 0., height, center.getX(), center.getY()), self._transform._depth )

    def getWidth(self):
        """
        Returns the width of the rectangle.
        """
        return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[2]**2)
    
    def getHeight(self):
        """
        Returns the height of the rectangle.
        """
        return _math.sqrt(self._transform._matrix[1]**2 + self._transform._matrix[3]**2)

    def setWidth(self, w):
        """
        Sets the width of the rectangle to w.
        """
        if not isinstance(w, (int,float)):
            raise TypeError, 'Width must be a positive number'
        if w <= 0:
            raise ValueError, "The rectangle's width must be positive"
        factor = float(w) / self.getWidth()
        p = self._localToGlobal(self._reference)
        trans = Transformation((1.,0.,0.,1.)+p.get())
        sca = Transformation((factor,0.,0.,1.,0.,0.))
        
#        self._transform = self._transform * Transformation((factor,0.,0.,1.,0.,0.))
        self._transform = trans*(sca*(trans.inv()*self._transform))
        self._objectChanged()
        
    def setHeight(self, h):
        """
        Sets the height of the rectangle to h.
        """
        if not isinstance(h, (int,float)):
            raise TypeError, 'Height must be a positive number'
        if h <= 0:
            raise ValueError, "The rectangle's height must be positive"
        factor = float(h) / self.getHeight()
        p = self._localToGlobal(self._reference)
        trans = Transformation((1.,0.,0.,1.)+p.get())
        sca = Transformation((1.,0.,0.,factor,0.,0.))
        
        self._transform = trans*(sca*(trans.inv()*self._transform))
        self._objectChanged()


class Square(Rectangle):
    """
    A square that can be drawn to the canvas.
    """
    def __init__(self, size=10, center=None):
        """
        Construct a new instance of a Square.

        The reference point for a square is its center.

        size        the dimension of the square.  (Defaults to 10)
        center      a Point representing the placement of the rectangle's center
                    (Defaults to Point(0,0) )
        """
        if not isinstance(size, (int,float)):
            raise TypeError, 'size must be a number'
        if size <= 0:
            raise ValueError, "The size must be positive."
        if center and not isinstance(center,Point):
            raise TypeError, 'center must be specified as a Point'

        Rectangle.__init__(self, size, size, center)


    def getSize(self):
        """
        Returns the length of a side of the square.
        """
        return self.getWidth()

    def setSize(self, size):
        """
        Set the length and width of the square to size.
        """
        if not isinstance(size, (int,float)):
            raise TypeError, 'size must be a number'
        if size <= 0:
            raise ValueError, "The size must be positive."
        
        Rectangle.setWidth(self, size)
        Rectangle.setHeight(self, size)

    def setWidth(self, width):
        """
        Sets the width and height of the square to given width.
        """
        if not isinstance(width, (int,float)):
            raise TypeError, 'width must be a positive number'
        if width <= 0:
            raise ValueError, "The square's width must be positive"
        self.setSize(width)

    def setHeight(self, height):
        """
        Sets the width and height of the square to given height.
        """
        if not isinstance(height, (int,float)):
            raise TypeError, 'height must be a positive number'
        if height <= 0:
            raise ValueError, "The square's height must be positive"
        self.setSize(height)


class Segment(Shape):
    """
    A line segment that can be drawn to a canvas.
    """
    def __init__(self, start=None, end=None):
        """
        Construct a new instance of a Segment.

        The two endpoints are specified as parameters.

        start  the first endpoint (by default, set to Point(10,0) )
        end    the second endpoint (by default, set to Point(0,0) )

        The reference point for a segment is originally its start point.
        """
        Shape.__init__(self)

        if not start:
            start = Point(10,0)
        if not end:
            end = Point(0,0)

        if not isinstance(start,Point):
            raise TypeError, 'start must be specified as a Point'
        if not isinstance(end,Point):
            raise TypeError, 'end must be specified as a Point'

        self._points = [start,end]
        self.adjustReference(self._points[0].getX(), self._points[0].getY())

    def getStart(self):
        """
        Return a copy of the segment's starting point.

        Subsequently mutating that copy has no effect on segment.
        """
        return Point(self._points[0].getX(), self._points[0].getY())
    
    def getEnd(self):
        """
        Return a copy of the segment's ending point.

        Subsequently mutating that copy has no effect on segment.
        """
        return Point(self._points[1].getX(), self._points[1].getY())
    
    def setPoints(self, startPoint, endPoint):
        """
        Set the two endpoints of the segment.

        startPoint    a Point instance
        endPoint      a Point instance
        """
        if not isinstance(startPoint,Point):
            raise TypeError, 'start must be specified as a Point'
        if not isinstance(endPoint,Point):
            raise TypeError, 'end must be specified as a Point'
        self._points = [startPoint,endPoint]
        self._objectChanged()

class Path(Shape):
    """
    A path that can be drawn to a canvas.
    """
    def __init__(self, *points):
        """
        Construct a new instance of a Path.

        The path is described as a series of points which are connected in order.

        These points can be initialized either be sending a single
        tuple of Point instances, or by sending each individual Point
        as a separate parameter.  (by default, path will have zero
        points)

        By default, the reference point for a path is aligned with the
        first point of the path.
        """
        Shape.__init__(self)

        if len(points)==1 and isinstance(points[0],tuple):
            points = points[0]
        for p in points:
            if not isinstance(p,Point):
                raise TypeError, 'all parameters must be Point instances'
        self._points = list(points)
        if len(self._points)>=1:
            self.adjustReference(self._points[0].getX(), self._points[0].getY())

    def addPoint(self, point, index=None):
        """
        Add a new point to the end of a Path.
        """
        if not isinstance(point,Point):
            raise TypeError, 'parameter must be a Point instance'
        if index == None:
            index = len(self._points)
        if not isinstance(index,int):
            raise TypeError, 'index must be an integer'
        self._points.insert(index, point)
        if len(self._points)==1:  # first point added
            self._reference = Point(point.getX(), point.getY())
        self._objectChanged()

    def deletePoint(self, index):
        """
        Remove the Point at the given index.
        """
        if not isinstance(index,int):
            raise TypeError, 'index must be an integer'
        try:
            self._points.pop(index)
        except:
            raise IndexError, 'index out of range'
        self._objectChanged()

    def clearPoints(self):
        """
        Remove all points, setting this back to an empty Path.
        """
        self._points = list()
        self._objectChanged()

    def getNumberOfPoints(self):
        """
        Return the current number of points.
        """
        return len(self._points)

    def getPoint(self, index):
        """
        Return a copy of the Point at the given index.

        Subsequently mutating that copy has no effect on path.
        """
        if not isinstance(index,int):
            raise TypeError, 'index must be an integer'
        try:
            p = self._points[index]
        except:
            raise IndexError, 'index out of range'
        return Point(p.getX(), p.getY())

    def setPoint(self, index, point):
        """
        Change the Point at the given index to a new value.
        """
        if not isinstance(index,int):
            raise TypeError, 'index must be an integer'
        if not isinstance(point,Point):
            raise TypeError, 'parameter must be a Point instance'
        try:
            self._points[index] = point
        except:
            raise IndexError, 'index out of range'
        self._objectChanged()

class Polygon(Path,FillableShape):
    """
    A polygon that can be drawn to a canvas.
    """
    def __init__(self, *points):
        """
        Construct a new instance of a Polygon.

        The polygon is described as a series of points which are connected in order.
        The last point is automatically connected back to the first to close the polygon.
        
        These points can be initialized either be sending a single
        tuple of Point instances, or by sending each individual Point
        as a separate parameter.  (by default, polygon will have zero
        points)

        By default, the reference point for a polygon is aligned with
        the first point of the polygon.
        """
        FillableShape.__init__(self)
        Path.__init__(self, points)

class Layer(Drawable, _Container):
    """Stores a group of shapes that act as one drawable object.

    Objects are placed onto the layer relative to the coordinate
    system of the layer itself.  The layer can then be placed onto a
    canvas (or even onto another layer).
    """
    def __init__(self):
        """
        Construct a new instance of Layer.

        The layer is initially empty.
        
        The reference point of that layer is initially the origin in
        its own coordinate system, (0,0).
        """
        Drawable.__init__(self)
        _Container.__init__(self)
            

    def add(self, drawable):
        """
        Add the drawable object to the layer.
        """
        if _debug>=2: print 'Call to Layer.add with self=',self,' drawable=',drawable
        if not isinstance(drawable,Drawable):
            raise TypeError, 'can only add Drawable objects to a Layer'
        if drawable in self._contents:
            raise ValueError, 'object already on the Layer'
        _Container.add(self, drawable)

        for trace in self._traces.values():
            if _debug>=2: print "Adding drawable to layer, begging draw cycle, trace = ",trace
            drawable._draw(trace)

    def remove(self, drawable):
        """
        Removes the drawable object from the layer.
        """
        if drawable not in self._contents:
            raise ValueError, 'object not currently on the Layer'
        _Container.remove(self,drawable)
        drawable._undraw(self)

    def _draw(self, parent):
        Drawable._draw(self, parent)
        if _debug>=2: print 'Call to Layer._draw with self=',self,' parent=',parent
        if self._contents:
            composedTrace = _TraceToCanvas(self,parent)
        for drawable in self._contents:
            drawable._draw(composedTrace)
                    
    def _undraw(self, parent, doRefresh=True):
        # record effected canvases BEFORE calling parent method
        effectedCanvases = []
        for trace in self._traces.values():
            can = trace._chain[0]
            if can not in effectedCanvases:
                effectedCanvases.append(can)

        Drawable._undraw(self, parent, False)
        if _debug>=2: print 'Call to Layer._undraw with self=',self,' parent=',parent
        for drawable in self._contents:
            drawable._undraw(self,False)

        if doRefresh:
            for can in effectedCanvases:
                can._canvas.refresh()

                    
    def _objectChanged(self, filter=None, doRefresh=True):
        if _debug>=2: print 'Layer._objectChanged called with self=',self,' filter=',filter

        if _debug>=2: print self._traces.values()

        effectedCanvases = []
        for trace in self._traces.values():
            if _debug>=2: print "  considering trace =", trace
            if not filter or filter in trace:
                if _debug>=2: print "  matches filter"

                can = trace._chain[0]
                if can not in effectedCanvases:
                    effectedCanvases.append(can)
                trace.update(False)  # wait a bit before refreshing canvases

                for drawable in self._contents:
                    try:
                        match = drawable._traces[trace.getChain()+(self,)]
                    except KeyError:
                        if _debug>=3:
                            print 'KeyError looking for trace=',trace.getChain()+(self,)
                            print 'drawable=',drawable
                            print 'keys() is ',drawable._traces.keys()
                        raise StandardError, 'cs1graphics -- Unexpected Error:  trace not found'
                    if _debug>=2: print 'found match with trans = ',match.getTrans()
                    match.setTrans(trace.getTrans()*self._transform)
                    if _debug>=2: print 'changing to = ',match.getTrans()

        for drawable in self._contents:
            drawable._objectChanged(self,False)

        if doRefresh:
            for can in effectedCanvases:
                can._canvas.refresh()
            
                    
    def _remove(self, canvas, transform, force=False):
        if _debug>=2: print 'Call to Layer._remove with self=',self,' canvas=',canvas, ' transform=',transform
#         if transform:
#             transform = transform * self._transform
#         else:
#             transform = self._transform
#             transform._container = (canvas,)
        if not transform:
            transform = Transformation()
            transform._container = (canvas,)
           
        self._objLookup.removeTransform(transform)
        
        for drawable in self._contents:
            trans = transform * Transformation()
            trans._container += (self,)
            drawable._remove(canvas, trans, force)


def _RectangleTest():
    paper = Canvas(400,400)
    r = Rectangle(50,200)
    paper.add(r)
    r.move(200,200)
    r.rotate(45)


