"""
Install instructions for traits 2.0
   # blow away old enthought
   rm -rf ~/dev/lib/python2.4/site-packages/enthought.*
   # get easy_install, if necessary
   wget  http://peak.telecommunity.com/dist/ez_setup.py
   sudo python sez_setup.py
    sudo rm -rf /usr/local/lib/python2.5/site-packages/enthought*
    sudo easy_install \
      -f http://code.enthought.com/enstaller/eggs/source/unstable \
      "enthought.traits < 3.0a"
    
"""
# see install instructions for enthrought traits2 in mtraits
import enthought.traits.api as traits
from enthought.traits.api import HasTraits, Instance, Trait, Float, Int, \
     Array, Tuple
from enthought.traits.trait_numeric import TraitArray
from matplotlib import agg
from matplotlib import colors as mcolors
from matplotlib import cbook
import numpy as npy
is_string_like = cbook.is_string_like
## begin core infrastructure
                    
class Affine(HasTraits):
    """
    An affine 3x3 matrix that supports matrix multiplication with
    other Affine instances or numpy arrays.
    a = Affine()
    a.translate = 10,20
    a.scale = 20, 40
    Be careful not to do *inplace* operations on the array components
    or the update callbacks will not be triggered, eg DO NOT 
      a.translate += 10, 20
    rather DO
      a.translate_delta(10, 20)
    Multiplication works as expected:
      a1 = Affine()
      a1.scale = 10, 20
      a2 = Affine()
      a2.scale = 4, 5
      print a1*a2
      x = numpy.random(3, 10)
      print a1*x
    All of the translate, scale, xlim, ylim and vec6 properties are
    simply views into the data matrix, and are updated by reference
    """
    # connect to the data_modified event if you want a callback
    data = Array('d', (3,3))
    translate = traits.Property(Array('d', (2,)))
    scale = traits.Property(Array('d', (2,)))    
    vec6 = traits.Property(Array('d', (6,)))
    xlim = traits.Property(Array('d', (2,)))
    ylim = traits.Property(Array('d', (2,)))            
    #data_modified = traits.Event 
    def _data_default(self):
        return npy.array([[1,0,0],[0,1,0],[0,0,1]], npy.float_)
    def _get_xlim(self):
        sx, b, tx = self.data[0]
        return self._get_lim(sx, tx)
    def _set_xlim(self, xlim):
        xmin, xmax = xlim
        oldsx, oldb, oldtx = self.data[0]
        
        sx = 1./(xmax-xmin)
        tx = -xmin*sx
        forward = oldsx!=sx or oldtx!=tx
        if forward:
            old = self.data.copy()
            self.data[0][0] = sx
            self.data[0][-1] = tx
            self._data_changed(old, self.data)
    def _get_ylim(self):
        c, sy, ty = self.data[1]
        return self._get_lim(sy, ty)
    def _set_ylim(self, ylim):
        ymin, ymax = ylim
        oldc, oldsy, oldty = self.data[1]
        
        sy = 1./(ymax-ymin)
        ty = -ymin*sy
        forward = oldsy!=sy or oldty!=ty
        if forward:
            old = self.data.copy()
            self.data[1][1] = sy
            self.data[1][-1] = ty
            self._data_changed(old, self.data)
            
    def _get_translate(self):
        return [self.data[0][-1], self.data[1][-1]]
    def _set_translate(self, s):
        oldtx = self.data[0][-1]
        oldty = self.data[1][-1]        
        
        tx, ty = s
        forward = tx!=oldtx or ty!=oldty
        if forward:
            old = self.data.copy()
            self.data[0][-1] = tx
            self.data[1][-1] = ty
            self._data_changed(old, self.data)
    def _get_scale(self):
        return [self.data[0][0], self.data[1][1]]
    def _set_scale(self, s):
        oldsx = self.data[0][0]
        oldsy = self.data[1][1]        
        
        sx, sy = s
        forward = sx!=oldsx or sy!=oldsy
        if forward:
            old = self.data.copy()
            self.data[0][0] = sx
            self.data[1][1] = sy
            self._data_changed(old, self.data)
            
    def _get_vec6(self):
        a,b,tx = self.data[0]
        c,d,ty = self.data[1]
        return [a,b,c,d,tx,ty]
    def _set_vec6(self, v):
        a,b,c,d,tx,ty = v
        olda, oldb, oldtx = self.data[0]
        oldc, oldd, oldty = self.data[1]
        forward = a!=olda or b!=oldb or c!=oldc or d!=oldd or tx!=oldtx or ty!=oldty
        if forward:
            old = self.data.copy()
            self.data[0] = a,b,tx
            self.data[1] = c,d,ty
            self._data_changed(old, self.data)
            
    def _get_lim(self, s, t):
        lmin = -t/s
        lmax = 1./s + lmin
        return lmin, lmax
    
    def _data_changed(self, old, new):
        # Make it known if the translate changed
        oldtx, oldty = old[0][-1], old[1][-1]
        tx, ty = new[0][-1], new[1][-1]
        oldsx, oldsy = old[0][0], old[1][1]
        sx, sy = new[0][0], new[1][1]
        oldb, oldc = old[0][1], old[1][0]
        b, c = new[0][1], new[1][0]
        tchanged = False
        schanged = False
        vchanged = False
        tchanged = oldtx!=tx or oldty!=ty
        schanged = oldsx!=sx or oldsy!=sy
        vchanged = tchanged or schanged or b!=oldb or c!=oldc
        xchanged = oldtx!=tx or oldsx!=sx
        ychanged = oldty!=ty or oldsy!=sy        
        
        if tchanged:
            self.trait_property_changed('translate', [oldtx, oldty], [tx, ty])
        if schanged:
            self.trait_property_changed('scale', [oldsx, oldsy], [sx, sy])
        if xchanged:
            oldxmin, oldxmax = self._get_lim(oldsx, oldtx)
            xmin, xmax = self._get_lim(sx, tx)            
            self.trait_property_changed('xlim', [oldxmin, oldxmax], [xmin, xmax])
        if ychanged:
            oldymin, oldymax = self._get_lim(oldsy, oldty)
            ymin, ymax = self._get_lim(sy, ty)            
            self.trait_property_changed('ylim', [oldymin, oldymax], [ymin, ymax])
        if vchanged:
            self.trait_property_changed(
                'vec6',
                [oldsx, oldb, oldc, oldsy, oldtx, oldty],
                [sx, b, c, sy, tx, ty])
            
        if tchanged or schanged or vchanged:
            #self._data_modified = True            
            self.trait_property_changed('data_modified', old, new)
        
    def follow(self, othervec6):
        self.vec6 = othervec6
        
    def __mul__(self, other):
        if isinstance(other, Affine):
            new = Affine()
            new.data = npy.dot(self.data, other.data)
            return new
        elif isinstance(other, npy.ndarray):
            return npy.dot(self.data, other)
        raise TypeError('Do not know how to multiply Affine by %s'%type(other))
                        
    def __repr__(self):
        return 'AFFINE: %s'%', '.join([str(val) for val in self.vec6])
        #return 'AFFINE:\n%s'%self.data
class Box(HasTraits):
    # left, bottom, width, height
    bounds = traits.Array('d', (4,))
    left = traits.Property(Float)
    bottom = traits.Property(Float)
    width = traits.Property(Float)  
    height = traits.Property(Float) 
    right = traits.Property(Float) # read only
    top = traits.Property(Float)   # read only
    def _bounds_default(self):
        return  [0.0, 0.0, 1.0, 1.0]
    def _get_left(self):
        return self.bounds[0]
    def _set_left(self, left):
        oldbounds = self.bounds[:]        
        self.bounds[0] = left
        self.trait_property_changed('bounds', oldbounds, self.bounds)
    def _get_bottom(self):
        return self.bounds[1]
    def _set_bottom(self, bottom):
        oldbounds = self.bounds[:]        
        self.bounds[1] = bottom
        self.trait_property_changed('bounds', oldbounds, self.bounds)
    def _get_width(self):
        return self.bounds[2]
    def _set_width(self, width):
        oldbounds = self.bounds[:]        
        self.bounds[2] = width
        self.trait_property_changed('bounds', oldbounds, self.bounds)
    def _get_height(self):
        return self.bounds[2]
    def _set_height(self, height):
        oldbounds = self.bounds[:]        
        self.bounds[2] = height
        self.trait_property_changed('bounds', oldbounds, self.bounds)
    def _get_right(self):
        return self.left + self.width
    def _get_top(self):
        return self.bottom + self.height
    def _bounds_changed(self, old, new):
        pass
## begin custom trait handlers
    
class TraitVertexArray(TraitArray):
    def __init__ ( self, typecode = None, shape = None, coerce = False ):
        TraitArray.__init__(self, typecode, shape, coerce)
    def validate(self, object, name, value):
        orig = value
        value = TraitArray.validate(self, object, name, value)
        if len(value.shape)!=2 or value.shape[1]!=2:
            return self.error(object, name, orig)
        return value
    def info(self):
        return 'an Nx2 array of doubles which are x,y vertices'
VertexArray = Trait(npy.array([[0,0], [1,1]], npy.float_),
                    TraitVertexArray('d'))
    
class ColorHandler(traits.TraitHandler):
    """
    This is a clever little traits mechanism -- users can specify the
    color as any mpl color, and the traited object will keep the
    original color, but will add a new attribute with a '_' postfix
    which is the color rgba tuple.
    Eg
    class C(HasTraits):
        facecolor = Trait('black', ColorHandler())
    c = C()
    c.facecolor = 'red'
    print c.facecolor    # prints red
    print c.facecolor_   # print (1,0,0,1)
    """
    is_mapped = True
    def post_setattr(self, object, name, value):
        object.__dict__[ name + '_' ] = self.mapped_value( value )
    def mapped_value(self, value ):
        if value is None: return None
        if is_string_like(value): value = value.lower()
        return mcolors.colorConverter.to_rgba(value)
       
    def validate(self, object, name, value):
        try:
            self.mapped_value(value)
        except ValueError:
            return self.error(object, name, value)
        else:            
            return value
    def info(self):
        return """\
any valid matplotlib color, eg an abbreviation like 'r' for red, a full
name like 'orange', a hex color like '#efefef', a grayscale intensity
like '0.5', or an RGBA tuple (1,0,0,1)"""
    
class MTraitsNamespace:
    DPI         = Float(72.)
    Alpha       = traits.Range(0., 1., 0.)
    Affine      = Trait(Affine())
    AntiAliased = traits.true
    Color       = Trait('black', ColorHandler())
    DPI         = Float(72.)
    Interval    = Array('d', (2,), npy.array([0.0, 1.0], npy.float_))
    LineStyle   = Trait('-', '--', '-.', ':', 'steps', None)
    LineWidth   = Float(1.0)
    Marker      = Trait(None, '.', ',', 'o', '^', 'v', '<', '>', 's',
                 '+', 'x', 'd', 'D', '|', '_', 'h', 'H',
                 'p', '1', '2', '3', '4')
    MarkerSize  = Float(6)
    Visible     = traits.true
mtraits = MTraitsNamespace()
def Alias(name):
    return Property(lambda obj: getattr(obj, name),
                    lambda obj, val: setattr(obj, name, val))
class IDGenerator:
    def __init__(self):
        self._id = 0
    def __call__(self):
        _id = self._id
        self._id += 1
        return _id
## begin backend API
# PATH CODES
STOP      = 0
MOVETO    = 1
LINETO    = 2
CURVE3    = 3
CURVE4    = 4
CURVEN    = 5
CATROM    = 6
UBSPLINE  = 7
CLOSEPOLY = 0x0F
class PathPrimitive(HasTraits):
    """
    The path is an object that talks to the backends, and is an
    intermediary between the high level path artists like Line and
    Polygon, and the backend renderer
    """
    
    color       = mtraits.Color('black')
    facecolor   = mtraits.Color('blue')
    alpha       = mtraits.Alpha(1.0)
    linewidth   = mtraits.LineWidth(1.0)
    antialiased = mtraits.AntiAliased
    pathdata     =Tuple(Array('b'), VertexArray)
    affine      = Instance(Affine, ())
    def _pathdata_default(self):
        return (npy.array([0,0], dtype=npy.uint8),
                npy.array([[0,0],[0,0]], npy.float_))
        
class MarkerPrimitive(HasTraits):
    locs  = Array('d')
    path   = Instance(PathPrimitive, ())       # marker path in points
    affine = Instance(Affine, ()) # transformation for the verts
    def _locs_default(self):
        return npy.array([[0,0],[0,0]], npy.float_)
class Renderer(HasTraits):
    dpi = mtraits.DPI
    size = traits.Tuple(Int(600), Int(400))
    adisplay = Instance(Affine, ()) 
    pathd = traits.Dict(Int, PathPrimitive)
    markerd = traits.Dict(Int, MarkerPrimitive)
    
    def __init__(self, size=(600,400)):
        self.pathd = dict()
        self.markerd = dict()
        self._size_changed(None, size)
        
    def _size_changed(self, old, new):
        width, height = new
        # almost all renderers assume 0,0 is left, upper, so we'll flip y here by default
        self.adisplay.translate = 0, height
        self.adisplay.scale = width, -height
        
        
    def render_path(self, pathid):
        pass
            
    def new_path_primitive(self):
        """
        return a PathPrimitive (or derived); these instances will be
        added and removed later through add_path and remove path
        """
        return PathPrimitive()
    def new_marker_primitive(self):
        """
        return a MarkerPrimitive (or derived); these instances will be
        added and removed later through add_maker and remove_marker
        """
        return MarkerPrimitive()
    
## begin backend agg    
class PathPrimitiveAgg(PathPrimitive):
    def __init__(self):
        self._pathdata_changed(None, self.pathdata)
        self._facecolor_changed(None, self.facecolor)
        self._color_changed(None, self.color)                
        
    @staticmethod
    def make_agg_path(pathdata):
        agg_path = agg.path_storage()
        codes, xy = pathdata
        Ncodes = len(codes)
        for i in range(Ncodes):
            x, y = xy[i]
            code = codes[i]
            #XXX handle other path codes here
            if code==MOVETO:
                agg_path.move_to(x, y)
            elif code==LINETO:
                agg_path.line_to(x, y)                
            elif code==CLOSEPOLY:
                agg_path.close_polygon()
        return agg_path
    def _pathdata_changed(self, olddata, newdata):        
        self.agg_path = PathPrimitiveAgg.make_agg_path(newdata)
        
    def _facecolor_changed(self, oldcolor, newcolor):        
        self.agg_facecolor = self.color_to_rgba8(self.facecolor_)
    def _color_changed(self, oldcolor, newcolor):                
        #print 'stroke color changed', newcolor
        c = self.color_to_rgba8(self.color_)
        self.agg_color = c
    def color_to_rgba8(self, color):
        if color is None: return None
        rgba = [int(255*c) for c in color]
        return agg.rgba8(*rgba)
class MarkerPrimitiveAgg(MarkerPrimitive):
    path   = Instance(PathPrimitiveAgg, ())
class RendererAgg(Renderer):
    gray = agg.rgba8(128,128,128,255)    
    white = agg.rgba8(255,255,255,255)
    blue = agg.rgba8(0,0,255,255)
    black = agg.rgba8(0,0,0,0)
    pathd = traits.Dict(Int, PathPrimitiveAgg)
    markerd = traits.Dict(Int, MarkerPrimitiveAgg)
    def _size_changed(self, old, new):
        Renderer._size_changed(self, old, new)
        width, height = self.size
        stride = width*4
        self.buf = buf = agg.buffer(width, height, stride)
        self.rbuf = rbuf = agg.rendering_buffer()
        rbuf.attachb(buf)
        self.pf = pf = agg.pixel_format_rgba(rbuf)
        self.rbase = rbase = agg.renderer_base_rgba(pf)
        rbase.clear_rgba8(self.white)
        # the antialiased renderers
        self.renderer =  agg.renderer_scanline_aa_solid_rgba(rbase);        
        self.rasterizer = agg.rasterizer_scanline_aa()
        self.scanline = agg.scanline_p8()
        self.trans = None
        # the aliased renderers
        self.rendererbin =  agg.renderer_scanline_bin_solid_rgba(rbase);
        self.scanlinebin = agg.scanline_bin()
    def new_path_primitive(self):
        'return a PathPrimitive (or derived)'
        return PathPrimitiveAgg()
    def new_marker_primitive(self):
        'return a MarkerPrimitive (or derived)'
        return MarkerPrimitiveAgg()
    def render_path(self, pathid):
        path = self.pathd[pathid]
        if path.antialiased:
            renderer = self.renderer
            scanline = self.scanline
            render_scanlines = agg.render_scanlines_rgba
        else:
            renderer = self.rendererbin
            scanline = self.scanlinebin
            render_scanlines = agg.render_scanlines_bin_rgba
        affine = self.adisplay * path.affine
        #print 'display affine:\n', self.adisplay
        #print 'path affine:\n', path.affine
        #print 'product affine:\n', affine
        aggaffine = agg.trans_affine(*affine.vec6)
        transpath = agg.conv_transform_path(path.agg_path, aggaffine)
        if path.facecolor is not None:
            #print 'render path', path.facecolor, path.agg_facecolor
            self.rasterizer.add_path(transpath)
            renderer.color_rgba8( path.agg_facecolor )
            render_scanlines(self.rasterizer, scanline, renderer);
        if path.color is not None:
            stroke = agg.conv_stroke_transpath(transpath)
            stroke.width(path.linewidth)
            self.rasterizer.add_path(stroke)
            renderer.color_rgba8( path.agg_color )        
            render_scanlines(self.rasterizer, scanline, renderer);
    def render_marker(self, markerid):
        marker = self.markerd[markerid]
        path = marker.path
        
        if path.antialiased:
            renderer = self.renderer
            scanline = self.scanline
            render_scanlines = agg.render_scanlines_rgba
        else:
            renderer = self.rendererbin
            scanline = self.scanlinebin
            render_scanlines = agg.render_scanlines_bin_rgba
        affinelocs = self.adisplay * marker.affine
        Nmarkers = marker.locs.shape[0]
        Locs = npy.ones((3, Nmarkers))
        Locs[0] = marker.locs[:,0]
        Locs[1] = marker.locs[:,1]        
        Locs = affinelocs * Locs
        
        dpiscale = self.dpi/72. # for some reason this is broken 
        # this will need to be highly optimized and hooked into some
        # extension code using cached marker rasters as we now do in
        # _backend_agg
        pathcodes, pathxy = marker.path.pathdata
        pathx = dpiscale*pathxy[:,0] 
        pathy = dpiscale*pathxy[:,1]
        Npath = len(pathcodes)
        XY = npy.ones((Npath, 2))
        for xv,yv,tmp in Locs.T:
            XY[:,0] = (pathx + xv).astype(int) + 0.5
            XY[:,1] = (pathy + yv).astype(int) + 0.5
            pathdata = pathcodes, XY
            aggpath = PathPrimitiveAgg.make_agg_path(pathdata)
            if path.facecolor is not None:
                self.rasterizer.add_path(aggpath)
                renderer.color_rgba8( path.agg_facecolor )
                render_scanlines(self.rasterizer, scanline, renderer);
            if path.color is not None:
                stroke = agg.conv_stroke_path(aggpath)
                stroke.width(path.linewidth)
                self.rasterizer.add_path(stroke)
                renderer.color_rgba8( path.agg_color )        
                render_scanlines(self.rasterizer, scanline, renderer);
    def show(self):
        # we'll cheat a little and use pylab for display
        X = npy.fromstring(self.buf.to_string(), npy.uint8)
        width, height = self.size
        X.shape = height, width, 4
        if 1:
            import pylab
            fig = pylab.figure()
            ax = fig.add_axes([0,0,1,1], xticks=[], yticks=[],
                              frameon=False, aspect='auto')
            ax.imshow(X, aspect='auto')
            pylab.show()
class Func(HasTraits):
    def __call__(self, X):
        'transform the numpy array with shape N,2'
        return X
    
    def invert(self, x, y):
        'invert the point x, y'
        return x, y
    def point(self, x, y):
        'transform the point x, y'
        return x, y
        
class Identity(Func):
    def __call__(self, X):
        'transform the numpy array with shape N,2'
        return X
    
    def invert(self, x, y):
        'invert the point x, y'
        return x, y
    def point(self, x, y):
        'transform the point x, y'
        return x, y
class Polar(Func):
    def __call__(self, X):
        'transform the numpy array with shape N,2'
        r = X[:,0]
        theta = X[:,1]
        x = r*npy.cos(theta)
        y = r*npy.sin(theta)
        return npy.array([x,y]).T
    
    def invert(self, x, y):
        'invert the point x, y'
        raise NotImplementedError
    def point(self, x, y):
        'transform the point x, y'
        raise NotImplementedError
mtraits.Model = Instance(Func, ())
    
## begin Artist layer
# coordinates:
#
#   artist model : a possibly nonlinear transformation (Func instance)
#     to a separable cartesian coordinate, eg for polar is takes r,
#     theta -> r*cos(theta), r*sin(theta)
#
#   AxesCoords.adata : an affine 3x3 matrix that takes model output and
#     transforms it to axes 0,1.  We are kind of stuck with the
#     mpl/matlab convention that 0,0 is the bottom left of the axes,
#     even though it contradicts pretty much every GUI layout in the
#     world
#
#   AxesCoords.aview: an affine 3x3 that transforms an axesview into figure
#     0,1 
#
#   Renderer.adisplay : takes an affine 3x3 and puts figure view into display.  0,
#      0 is left, top, which is the typical coordinate system of most
#      graphics formats
primitiveID = IDGenerator()
artistID = IDGenerator()
class Artist(HasTraits):
    zorder  = Float(1.0)
    alpha   = mtraits.Alpha()
    visible = mtraits.Visible()
    adata = Instance(Affine, ())  # the data affine
    aview = Instance(Affine, ())  # the view affine
    affine  = Instance(Affine, ()) # the product of the data and view affine
    renderer = Trait(None, Renderer)
    # every artist defines a string which is the name of the attr that
    # containers should put it into when added.  Eg, an Axes is an
    # Aritst container, and when you place a Line in to an Axes, the
    # Axes will store a reference to it in the sequence ax.lines where
    # Line.sequence = 'lines'
    sequence = 'artists'
    def __init__(self):
        self.artistid = artistID()
        # track affine as the product of the view and the data affines
        # -- this should be a property, but I had trouble making a
        # property on my custom class affine so this is a workaround
        def product(ignore):
            # modify in place
            self.affine.follow((self.aview * self.adata).vec6)
        product(None)  # force an affine product updated
        self.adata.on_trait_change(product, 'vec6')
        self.aview.on_trait_change(product, 'vec6')        
    def _get_affine(self):
        return self.aview * self.adata
    
    def draw(self):
        pass
class ArtistContainer(Artist):
    
    artistd = traits.Dict(Int, Artist)
    sequence = 'containers'
    def __init__(self):
        Artist.__init__(self)
        self.artistd = dict()
    
    def add_artist(self, artist, followdata=True, followview=True):        
        # this is a very interesting change from matplotlib -- every
        # artist acts as a container that can hold other artists, and
        # respects zorder drawing internally.  This makes zordering
        # much more flexibel
        self.artistd[artist.artistid] = artist
        self.__dict__.setdefault(artist.sequence, []).append(artist)
        
        artist.renderer = self.renderer
        self.sync_trait('renderer', artist, mutual=False)
        artist.followdata = followdata
        artist.followview = followview
        if followdata:
            # set the data affines to be the same
            artist.adata.follow(self.adata.vec6)
            self.adata.on_trait_change(artist.adata.follow, 'vec6')
        if followview:
            # set the view affines to be the same
            artist.aview.follow(self.aview.vec6)
            self.aview.on_trait_change(artist.aview.follow, 'vec6')
    def remove_artist(self, artist):
        if artist.followview:
            self.aview.on_trait_change(artist.aview.follow, 'vec6', remove=True)
            del artist.followview
            
        if artist.followdata:
            self.adata.on_trait_change(artist.adata.follow, 'vec6', remove=True)
            del artist.followdata
        self.sync_trait('renderer', artist, remove=True)
        del self.artistd[artist.artistid]
        self.__dict__[artist.sequence].remove(artist)
    def draw(self):
        if self.renderer is None or not self.visible: return
        
        dsu = [(artist.zorder, artist.artistid, artist) for artist in self.artistd.values()]
        dsu.sort()
        for zorder, artistid, artist in dsu:
            #print 'artist draw', self, artist, zorder
            artist.draw()
class Path(Artist):
    """
    An interface class between the higher level artists and the path
    primitive that needs to talk to the renderers
    """
    _path       = traits.Instance(PathPrimitive, ())
    antialiased = mtraits.AntiAliased()
    color       = mtraits.Color('blue')
    facecolor   = mtraits.Color('yellow')    
    linestyle   = mtraits.LineStyle('-')
    linewidth   = mtraits.LineWidth(1.0) 
    model       = mtraits.Model
    pathdata    = traits.Tuple(Array('b'), VertexArray)
    sequence    = 'paths'
    zorder      = Float(1.0)
    # why have an extra layer separating the PathPrimitive from the
    # Path artist?  The reasons are severalfold, but it is still not
    # clear if this is the better solution.  Doing it this way enables
    # the backends to create their own derived primitves (eg
    # RendererAgg creates PathPrimitiveAgg, and in that class sets up
    # trait listeners to create agg colors and agg paths when the
    # PathPrimitive traits change.  Another reason is that it allows
    # us to handle nonlinear transformation (the "model") at the top
    # layer w/o making the backends understand them.  The current
    # design is create a mapping between backend primitives and
    # primitive artists (Path, Text, Image, etc...) and all of the
    # higher level Artists (Line, Polygon, Axis) will use the
    # primitive artitsts. So only a few artists will need to know how
    # to talk to the backend.  The alternative is to make the backends
    # track and understand the primitive artists themselves.
    def __init__(self):
        """
        The model is a function taking Nx2->Nx2.  This is where the
        nonlinear transformation can be used
        """
        Artist.__init__(self)
        self._pathid = primitiveID()
    def _pathdata_default(self):
        return (npy.array([0,0], dtype=npy.uint8),
                npy.array([[0,0],[0,0]], npy.float_))
        
    def _update_path(self):
        'sync the Path traits with the path primitive'
        self.sync_trait('linewidth', self._path, mutual=False)
        self.sync_trait('color', self._path, mutual=False)
        self.sync_trait('facecolor', self._path, mutual=False)
        self.sync_trait('antialiased', self._path, mutual=False)
        # sync up the path affine
        self._path.affine.follow(self.affine.vec6)
        self.affine.on_trait_change(self._path.affine.follow, 'vec6')
        self._update_pathdata()
    def _update_pathdata(self):
        #print 'PATH: update pathdata'
        codes, xy = self.pathdata
        #print '    PATH: shapes', codes.shape, xy.shape
        if self.model is not None:
            xy = self.model(xy)
        pathdata = codes, xy
        self._path.pathdata = pathdata
        
    def draw(self):
        if self.renderer is None or not self.visible: return
        Artist.draw(self)
        self.renderer.render_path(self._pathid)
        
    def _renderer_changed(self, old, new):
        if old is not None:
            del old.pathd[self._pathid]
            
        if new is None: return 
        #print 'PATH renderer_changed; updating' 
        self._path = renderer.new_path_primitive()
        new.pathd[self._pathid] = self._path
        self._update_path()        
    
    def _model_changed(self, old, new):
        self._update_pathdata()
    def _pathdata_changed(self, old, new):
        #print 'PATH: pathdata changed'
        self._update_pathdata()
class Marker(Artist):
    """
    An interface class between the higher level artists and the marker
    primitive that needs to talk to the renderers
    """
    _marker       = traits.Instance(MarkerPrimitive, ())
    locs        = Array('d')
    path        = Instance(Path, ())    
    model       = mtraits.Model
    sequence    = 'markers'
    size        = Float(1.0) # size of the marker in points
    def __init__(self):
        """
        The model is a function taking Nx2->Nx2.  This is where the
        nonlinear transformation can be used
        """
        Artist.__init__(self)
        self._markerid = primitiveID()
    def _locs_default(self):
        return npy.array([[0,1],[0,1]], npy.float_)
        
    def _path_default(self):
        bounds = npy.array([-0.5, -0.5, 1, 1])*self.size
        return Rectangle().set(bounds=bounds)
    def _path_changed(self, old, new):
        if self.renderer is None:
            # we can't sync up to the underlying path yet
            return 
        print 'MARKER _path_changed', self.path._path.pathdata, self._marker.path.pathdata
        old.sync_trait('_path', self._marker, 'path', remove=True)
        new.sync_trait('_path', self._marker, 'path', mutual=False)
        
    def _update_marker(self):
        'sync the Marker traits with the marker primitive'
        if self.renderer is None:
            # we can't sync up to the underlying path yet
            return 
        # sync up the marker affine
        self.path.sync_trait('_path', self._marker, 'path', mutual=False)
        self._marker.affine.follow(self.affine.vec6)
        self.affine.on_trait_change(self._marker.affine.follow, 'vec6')
        self._update_locs()
        print 'MARKER _update_marker', self.path._path.pathdata, self._marker.path.pathdata
    def _update_locs(self):
        print 'MARKER: update markerdata'
        xy = self.locs
        if self.model is not None:
            xy = self.model(xy)
        self._marker.locs = xy
        
    def draw(self):
        if self.renderer is None or not self.visible: return
        Artist.draw(self)
        self.renderer.render_marker(self._markerid)
        
    def _renderer_changed(self, old, new):
        # we must make sure the contained artist gets the callback
        # first so we can update the path primitives properly
        self.path._renderer_changed(old, new)
        if old is not None:
            del old.markerd[self._markerid]
            
        if new is None: return 
        print 'MARKER renderer_changed; updating' 
        self._marker = renderer.new_marker_primitive()
        new.markerd[self._markerid] = self._marker
        self._update_marker()        
    
    def _model_changed(self, old, new):
        self._update_locs()
    def _locs_changed(self, old, new):
        if len(new.shape)!=2:
            raise ValueError('new must be nx2 array')
        self._update_locs()
class Line(Path):
    XY = Array('d')
    sequence        = 'lines'
    def _facecolor_default(self):
        return None
    
    def _XY_default(self):
        return npy.array([[0,1],[0,1]], npy.float_)
    
    def _XY_changed(self):
        #print 'LINE: XY changed'
        codes = LINETO*npy.ones(len(self.XY), npy.uint8)
        codes[0] = MOVETO
        #print 'LINE shapes', codes.shape, self.XY.shape
        self.pathdata = codes, self.XY
class Polygon(Path):
    zorder    = Float(2.0)
    vertices  = Array('d')
    sequence  = 'polygons'
    def _vertices_default(self):
        return npy.array([[-1,0], [0,1], [1,0], [0,0]], npy.float_)
    def _vertices_changed(self, old, new):
        #print 'POLY: verts changed'
        N = len(new)
        
        codes = LINETO*npy.ones(N, npy.uint8)
        codes[0] = MOVETO
        codes[-1] = CLOSEPOLY
        pathdata = codes, new
        self.pathdata = pathdata
        self._pathdata_changed(None, pathdata)
        
class Rectangle(Polygon, Box):
    sequence = 'rectangles'
    def __init__(self):
        Polygon.__init__(self)
        self._bounds_changed(None, self.bounds)
        
    def _bounds_changed(self, old, new):
        Box._bounds_changed(self, old, new)
        #print 'RECT: bounds changed'
        l,b,w,h = new
        t = b+h
        r = l+w
        self.vertices = npy.array([(l,b), (l,t), (r, t), (r, b), (0,0)], npy.float_)
        #XXX: do we need to otify traits change
        self._vertices_changed(None, self.vertices)
class RegularPolygon(Polygon):
    center = Tuple(Float, Float) 
    sides = Int(6)
    radius = Float(1.0)
    theta = Float(0.)  # orientation in radians
    sequence = 'rectangles'
    def __init__(self):
        self._update_vertices()
    
    def _sides_changed(self, old, new):
        self._update_verts()
    def _theta_changed(self, old, new):
        self._update_verts()
    def _radius_changed(self, old, new):
        self._update_verts()
        
    def _update_verts(self):
        theta = 2*npy.pi/self.sides*npy.arange(self.sides) + self.theta
        x, y = self.center
        xy = npy.zeros((self.sides,2))
        xy[:,0] = x + self.radius*npy.cos(theta)
        xy[:,1] = y + self.radius*npy.sin(theta)
        self.vertices = xy
class Figure(ArtistContainer):
    rectangle = Instance(Rectangle, ())
    sequence = None  # figure is top level container
    def __init__(self):
        ArtistContainer.__init__(self)
        self.rectangle.zorder = 0
        self.rectangle.facecolor = '0.75'
        self.rectangle.bounds = [0,0,1,1]
        self.add_artist(self.rectangle)
class Axis(ArtistContainer):
    zorder = Float(1.5)
    tickmarker  = Instance(Marker, ())
    line = Instance(Line, ())
    ticklocs = Array('d')
    ticksize = Float(7.0)
    
    loc  = Float(0.)          # the y location of the x-axis
    tickoffset = Float(0)  # -1 for outer, -0.5 for centered, 0 for inner
    sequence  = 'axes'
    def __init__(self):
        ArtistContainer.__init__(self)
        self.affine.on_trait_change(self._update_blended_affine, 'vec6')
        self.tickmarker.antialiased = False
        self.line.antialiased = False
        self.add_artist(self.line, followdata=False)
        self.add_artist(self.tickmarker, followdata=False)
        # XXX, do we have to manually call these or will they get
        # calle dautomagically in init
        self._update_tick_path()
        self._update_marker_locations()
        self._update_blended_affine()
        self._update_linepath()
    
    def _ticklocs_changed(self, old, new):
        self._update_marker_locations()
    def _loc_changed(self, old, new):
        self._update_blended_affine()
    
    def _ticksize_changed(self, old, new):
        self._update_tick_path()
    def _tickoffset_changed(self, old, new):
        self._update_tick_path()
    def _update_blended_affine(self):
        'blend of xdata and y axis affine'
        raise NotImplementedError
        
    def _update_marker_locations(self):
        raise NotImplementedError
    
    def _update_tick_path(self):
        raise NotImplementedError
    def _update_linepath(self):
        raise NotImplementedError
class XAxis(Axis):
    sequence = 'xaxes'
    def _update_blended_affine(self):
        'blend of xdata and y axis affine'
        sx, b, tx = self.adata.data[0]
        a = Affine()
        a.vec6 = sx, b, 0, 1, tx, self.loc
        self.tickmarker.affine.vec6 = (self.aview * a).vec6
        
        a = Affine()
        a.translate = 0, self.loc
        self.line.affine.vec6 = (self.aview * a).vec6        
    def _update_marker_locations(self):
        Nticks = len(self.ticklocs)
        locs = self.loc*npy.ones((Nticks,2))
        locs[:,0] = self.ticklocs
        self.tickmarker.locs = locs
    def _update_tick_path(self):
        codes = MOVETO, LINETO
        verts = npy.array([[0., self.tickoffset], [0, self.tickoffset-1]])*self.ticksize
        pathdata = codes, verts
        self.tickmarker.path.pathdata = pathdata    
    def _update_linepath(self):
        
        codes = MOVETO, LINETO
        X = npy.array([[0, 1], [0, 0]], npy.float_).T
        pathdata = codes, X
        self.line.pathdata = pathdata
class YAxis(Axis):
    sequence = 'yaxes'
    def _update_blended_affine(self):
        'blend of xdata and y axis affine'
        c, sy, ty = self.adata.data[1]
        a = Affine()
        a.vec6 = 1, 0, 0, sy, self.loc, ty
        self.tickmarker.affine.vec6 = (self.aview * a).vec6
        
        a = Affine()
        a.translate = self.loc, 0
        self.line.affine.vec6 = (self.aview * a).vec6        
    def _update_marker_locations(self):
        Nticks = len(self.ticklocs)
        locs = self.loc*npy.ones((Nticks,2))
        locs[:,1] = self.ticklocs
        self.tickmarker.locs = locs
    def _update_tick_path(self):
        codes = MOVETO, LINETO
        verts = npy.array([[self.tickoffset,0], [self.tickoffset+1,0]])*self.ticksize
        pathdata = codes, verts
        self.tickmarker.path.pathdata = pathdata
    def _update_linepath(self):
        codes = MOVETO, LINETO
        X = npy.array([[0, 0], [0, 1]], npy.float_).T
        pathdata = codes, X
        self.line.pathdata = pathdata
class FigurePane(ArtistContainer, Box):
    """
    The figure pane conceptually like the matplotlib Axes, but now
    almost all of it's functionality is modular into the Axis and
    Affine instances.  It is a shell of it's former self: it has a
    rectangle and a default x and y axis instance
    """
    rectangle = Instance(Rectangle, ())
    #gridabove = traits.false # TODO handle me
    xaxis     = Instance(XAxis, ())
    yaxis     = Instance(YAxis, ())
    sequence  = 'panes'
    
    def __init__(self):
        ArtistContainer.__init__(self)
        self.rectangle.zorder = 0
        self.rectangle.facecolor = 'white'
        self.rectangle.edgecolor = 'white'        
        self.rectangle.linewidth = 0
        
        self.rectangle.bounds = [0,0,1,1]
        self.add_artist(self.rectangle, followdata=False)
        self.add_artist(self.xaxis)
        self.add_artist(self.yaxis)                
    def _bounds_changed(self, old, new):
        Box._bounds_changed(self, old, new)
        l,b,w,h = self.bounds
        self.aview.scale = w, h
        self.aview.translate = l, b
## begin examples
        
def classic(fig):
    pane = FigurePane().set(bounds=[0.1, 0.1, 0.8, 0.8])
    fig.add_artist(pane, followdata=False, followview=False)
    # update the view limits, all the affines should be automagically updated
    x = npy.arange(0, 10., 0.01)
    y = npy.sin(2*npy.pi*x)
    y = npy.exp(-x/2.)
    line1 = Line().set(XY=npy.array([x,y]).T,
                       color='blue', linewidth=2.0, 
                       )
    pane.add_artist(line1)
    pane.adata.xlim = 0, 10
    pane.adata.ylim = -0.1, 1.1
    # axis placement is still broken
    xax, yax = pane.xaxis, pane.yaxis
    xax.ticklocs = npy.arange(0., 11., 1)
    xax.tickoffset = 0.5
    xax.loc = -0.05
    xax.line.color = 'black'
    xax.tickmarker.path.color = 'black'
    yax.ticklocs = npy.arange(-1.0, 1.1, 0.2)
    yax.tickoffset = -0.5
    yax.loc = -0.05
    yax.line.color = 'black'
    yax.tickmarker.path.color = 'black'
    if 0:
        x = npy.arange(0, 10., 0.1)
        y = npy.sin(2*npy.pi*x)
        marker = Marker().set(
            locs=npy.array([x,y]).T,  color='ref', linewidth=1.0,
            size=10)
        pane.add_artist(marker)
    if 0:
        xax, yax = pane.xaxis, pane.yaxis
        xax.ticklocs = npy.arange(0., 11., 1)
        xax.ticksize = 8
        xax.line.color = 'black'
        xax.line.linewidth = 2.0
        xax.tickoffset = .5
        xax.tickmarker.path.color = 'black'
        xax.tickmarker.path.linewidth = 2
        xax.loc = 0.5
        xax.zorder = 10  
        yax.ticklocs = npy.arange(-1.0, 1.1, 0.2)
        yax.line.color = 'black'
        yax.line.linewidth = 2.0
        yax.tickmarker.path.color = 'black'
        yax.ticksize = 8
        yax.tickoffset = -0.5
        yax.tickmarker.path.linewidth = 2
        yax.loc = 0.5
        yax.zorder = 10
    if 0:
        # add a right and top axis; the markers are getting the loc
        # but the line path isn't...  It appears all the line paths
        # are getting 0
        xaxis2 = XAxis()
        xaxis2.loc = 0.6
        xaxis2.tickoffset = -1
        xaxis2.ticklocs = npy.arange(0., 10.1, 0.5)
        yaxis2 = YAxis().set(loc=0.6, tickoffset=-1)    
        yaxis2.tickmarker.path.color = 'green'
        yaxis2.loc = 0.5
        yaxis2.ticksize = 10
        yaxis2.tickmarker.path.linewidth = 1
        yaxis2.line.color = 'green'
        yaxis2.tickmarker.path.color = 'green'
        yaxis2.ticklocs = npy.arange(-1.0, 1.1, 0.1)
        pane.add_artist(xaxis2)
        pane.add_artist(yaxis2)
        # uncomment to change Axes wwidth
        #pane.width = 0.8
    if 0:
        # XXX: axes lines and tick markes are placed in vastly
        # different locations depending on whether this is commented
        # or uncommented, suggesting that the problem is caused by
        # events not propogating unless lim are changed.  If we set
        # these lim to be the same as the lim above (note they are
        # almost identical) then the graphs are the same regardless of
        # whether the lim are set
        pane.adata.xlim = 0.01, 10.01
        pane.adata.ylim = -0.101, 1.101
def make_subplot_ll(fig):
    x1 = npy.arange(0, 10., 0.05)
    x2 = npy.arange(0, 10., 0.1)
    y1 = npy.cos(2*npy.pi*x1)
    y2 = 10*npy.exp(-x1)
    pane = FigurePane().set(bounds=[0.1, 0.1, 0.4, 0.4])
    fig.add_artist(pane, followdata=False, followview=False)
    line1 = Line().set(X=npy.array([x1,y1]).T,
                       color='blue', linewidth=2.0)
    pane.add_artist(line1)
    # update the view limits, all the affines should be automagically updated
    pane.adata.xlim = 0, 10
    pane.adata.ylim = -1.1, 1.1
    pane.xaxis.ticklocs = npy.arange(0., 11., 1.)
    pane.xaxis.loc = -0.1
    pane.xaxis.tickoffset = -0.5
    pane.xaxis.line.color = 'red'
    Pane.yaxis.ticklocs = npy.arange(-1.0, 1.1, 0.2)
    pane.yaxis.loc = -0.1
    pane.xaxis.tickoffset = -0.5    
    
    pane.yaxis.line.color = 'blue'
    pane.yaxis.tickmarker.color = 'blue'
    # uncomment to change Axes wwidth
    #pane.width = 0.8
def make_subplot_ur(fig):
    axes2 = Axes()
    axes2.aview.scale = 0.4, 0.4
    axes2.aview.translate = 0.55, 0.55
    
    fig.add_artist(axes2, followdata=False, followview=False)
    r = npy.arange(0.0, 1.0, 0.01)
    theta = r*4*npy.pi
    
    line2 = Line().set(X=npy.array([r, theta]).T, model=Polar(), color='#ee8d18', linewidth=2.0)
    axes2.add_artist(line2)
    rect2 = Rectangle().set(bounds=[0,0,1,1], facecolor='#d5de9c')
    axes2.add_artist(rect2, followdata=False)
    axes2.adata.xlim = -1.1, 1.1
    axes2.adata.ylim = -1.1, 1.1    
if __name__=='__main__':
    renderer = RendererAgg()
    fig = Figure()
    fig.renderer = renderer
    classic(fig)
    #make_subplot_ll(fig)
    #make_subplot_ur(fig)
    fig.draw()
    renderer.show()