1 from __future__ 
import division
 
   4     from Numeric 
import array
,asarray
,Float
,cos
, sin
, pi
,sum,minimum
,maximum
,Int32
,zeros
, ones
, concatenate
, sqrt
, argmin
, power
, absolute
, matrixmultiply
, transpose
, sometrue
, arange
, hypot
 
   7         from numarray 
import array
, asarray
, Float
, cos
, sin
, pi
, sum, minimum
, maximum
, Int32
, zeros
, concatenate
, matrixmultiply
, transpose
, sometrue
, arange
, hypot
 
   9         raise ImportError("I could not import either Numeric or numarray") 
  11 from time 
import clock
, sleep
 
  13 import Resources 
# A file with icons, etc for FloatCanvas 
  20 ## A global variable to hold the Pixels per inch that wxWindows thinks is in use 
  21 ## This is used for scaling fonts. 
  22 ## This can't be computed on module __init__, because a wx.App might not have initialized yet. 
  25 ## a custom Exceptions: 
  27 class FloatCanvasError(Exception): 
  30 ## Create all the mouse events 
  31 # I don't see a need for these two, but maybe some day! 
  32 #EVT_FC_ENTER_WINDOW = wx.NewEventType() 
  33 #EVT_FC_LEAVE_WINDOW = wx.NewEventType() 
  34 EVT_FC_LEFT_DOWN 
= wx
.NewEventType()  
  35 EVT_FC_LEFT_UP  
= wx
.NewEventType() 
  36 EVT_FC_LEFT_DCLICK 
= wx
.NewEventType()  
  37 EVT_FC_MIDDLE_DOWN 
= wx
.NewEventType()  
  38 EVT_FC_MIDDLE_UP 
= wx
.NewEventType()  
  39 EVT_FC_MIDDLE_DCLICK 
= wx
.NewEventType()  
  40 EVT_FC_RIGHT_DOWN 
= wx
.NewEventType()  
  41 EVT_FC_RIGHT_UP 
= wx
.NewEventType()  
  42 EVT_FC_RIGHT_DCLICK 
= wx
.NewEventType()  
  43 EVT_FC_MOTION 
= wx
.NewEventType()  
  44 EVT_FC_MOUSEWHEEL 
= wx
.NewEventType()  
  45 ## these two are for the hit-test stuff, I never make them real Events 
  46 EVT_FC_ENTER_OBJECT 
= wx
.NewEventType() 
  47 EVT_FC_LEAVE_OBJECT 
= wx
.NewEventType() 
  49 ##Create all mouse event binding functions 
  50 #def EVT_ENTER_WINDOW( window, function ): 
  51 #    window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function )  
  52 #def EVT_LEAVE_WINDOW( window, function ): 
  53 #    window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function )  
  54 def EVT_LEFT_DOWN( window
, function 
):   
  55     window
.Connect( -1, -1,EVT_FC_LEFT_DOWN 
, function 
) 
  56 def EVT_LEFT_UP( window
, function 
): 
  57     window
.Connect( -1, -1,EVT_FC_LEFT_UP 
, function 
) 
  58 def EVT_LEFT_DCLICK  ( window
, function 
): 
  59     window
.Connect( -1, -1,EVT_FC_LEFT_DCLICK 
, function 
) 
  60 def EVT_MIDDLE_DOWN  ( window
, function 
): 
  61     window
.Connect( -1, -1,EVT_FC_MIDDLE_DOWN 
, function 
) 
  62 def EVT_MIDDLE_UP  ( window
, function 
): 
  63     window
.Connect( -1, -1,EVT_FC_MIDDLE_UP 
, function 
) 
  64 def EVT_MIDDLE_DCLICK  ( window
, function 
): 
  65     window
.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK 
, function 
) 
  66 def EVT_RIGHT_DOWN  ( window
, function 
): 
  67     window
.Connect( -1, -1,EVT_FC_RIGHT_DOWN 
, function 
) 
  68 def EVT_RIGHT_UP( window
, function 
): 
  69     window
.Connect( -1, -1,EVT_FC_RIGHT_UP 
, function 
) 
  70 def EVT_RIGHT_DCLICK( window
, function 
): 
  71     window
.Connect( -1, -1,EVT_FC_RIGHT_DCLICK 
, function 
) 
  72 def EVT_MOTION( window
, function 
): 
  73     window
.Connect( -1, -1,EVT_FC_MOTION 
, function 
) 
  74 def EVT_MOUSEWHEEL( window
, function 
): 
  75     window
.Connect( -1, -1,EVT_FC_MOUSEWHEEL 
, function 
) 
  77 class _MouseEvent(wx
.PyCommandEvent
): 
  81     This event class takes a regular wxWindows mouse event as a parameter, 
  82     and wraps it so that there is access to all the original methods. This 
  83     is similar to subclassing, but you can't subclass a wxWindows event 
  85     The goal is to be able to it just like a regular mouse event. 
  89     GetCoords() , which returns and (x,y) tuple in world coordinates. 
  91     Another difference is that it is a CommandEvent, which propagates up 
  92     the window hierarchy until it is handled. 
  96     def __init__(self
, EventType
, NativeEvent
, WinID
, Coords 
= None): 
  97         wx
.PyCommandEvent
.__init
__(self
) 
  99         self
.SetEventType( EventType 
) 
 100         self
._NativeEvent 
= NativeEvent
 
 103 # I don't think this is used. 
 104 #    def SetCoords(self,Coords): 
 105 #        self.Coords = Coords 
 110     def __getattr__(self
, name
): 
 111         #return eval(self.NativeEvent.__getattr__(name) ) 
 112         return getattr(self
._NativeEvent
, name
) 
 114 def _cycleidxs(indexcount
, maxvalue
, step
): 
 117     Utility function used by _colorGenerator 
 123         for idx 
in xrange(0, maxvalue
, step
): 
 124             for tail 
in _cycleidxs(indexcount 
- 1, maxvalue
, step
): 
 127 def _colorGenerator(): 
 131     Generates a seris of unique colors used to do hit-tests with the HIt 
 136     if sys
.platform 
== 'darwin': 
 139         b 
= wx
.EmptyBitmap(1,1) 
 146         raise "ColorGenerator does not work with depth = %s" % depth
 
 147     return _cycleidxs(indexcount
=3, maxvalue
=256, step
=step
) 
 150 #### I don't know if the Set objects are useful, beyond the pointset 
 151 #### object. The problem is that when zoomed in, the BB is checked to see 
 152 #### whether to draw the object.  A Set object can defeat this. One day 
 153 #### I plan to write some custon C++ code to draw sets of objects 
 155 ##class ObjectSetMixin: 
 157 ##    A mix-in class for draw objects that are sets of objects 
 159 ##    It contains methods for setting lists of pens and brushes 
 162 ##    def SetPens(self,LineColors,LineStyles,LineWidths): 
 164 ##        This method used when an object could have a list of pens, rather than just one 
 165 ##        It is used for LineSet, and perhaps others in the future. 
 167 ##        fixme: this should be in a mixin 
 169 ##        fixme: this is really kludgy, there has got to be a better way! 
 174 ##        if type(LineColors) == types.ListType: 
 175 ##            length = len(LineColors) 
 177 ##            LineColors = [LineColors] 
 179 ##        if type(LineStyles) == types.ListType: 
 180 ##            length = len(LineStyles) 
 182 ##            LineStyles = [LineStyles] 
 184 ##        if type(LineWidths) == types.ListType: 
 185 ##            length = len(LineWidths) 
 187 ##            LineWidths = [LineWidths] 
 190 ##            if len(LineColors) == 1: 
 191 ##                LineColors = LineColors*length 
 192 ##            if len(LineStyles) == 1: 
 193 ##                LineStyles = LineStyles*length 
 194 ##            if len(LineWidths) == 1: 
 195 ##                LineWidths = LineWidths*length 
 198 ##        for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths): 
 199 ##            if LineColor is None or LineStyle is None: 
 200 ##                self.Pens.append(wx.TRANSPARENT_PEN) 
 201 ##                # what's this for?> self.LineStyle = 'Transparent' 
 202 ##            if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): 
 203 ##                Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) 
 204 ##                self.Pens.append(Pen) 
 206 ##                self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)]) 
 208 ##            self.Pens = self.Pens[0] 
 212     This is the base class for all the objects that can be drawn. 
 214     One must subclass from this (and an assortment of Mixins) to create 
 219     def __init__(self
, InForeground  
= False, IsVisible 
= True): 
 220         self
.InForeground 
= InForeground
 
 225         self
.CallBackFuncs 
= {} 
 227         ## these are the defaults 
 231         self
.MinHitLineWidth 
= 3 
 232         self
.HitLineWidth 
= 3 ## this gets re-set by the subclasses if necessary 
 237         self
.FillStyle 
= "Solid" 
 239         self
.Visible 
= IsVisible
 
 241     # I pre-define all these as class variables to provide an easier 
 242     # interface, and perhaps speed things up by caching all the Pens 
 243     # and Brushes, although that may not help, as I think wx now 
 244     # does that on it's own. Send me a note if you know! 
 247             ( None,"Transparent")  : wx
.TRANSPARENT_BRUSH
, 
 248             ("Blue","Solid")       : wx
.BLUE_BRUSH
, 
 249             ("Green","Solid")      : wx
.GREEN_BRUSH
, 
 250             ("White","Solid")      : wx
.WHITE_BRUSH
, 
 251             ("Black","Solid")      : wx
.BLACK_BRUSH
, 
 252             ("Grey","Solid")       : wx
.GREY_BRUSH
, 
 253             ("MediumGrey","Solid") : wx
.MEDIUM_GREY_BRUSH
, 
 254             ("LightGrey","Solid")  : wx
.LIGHT_GREY_BRUSH
, 
 255             ("Cyan","Solid")       : wx
.CYAN_BRUSH
, 
 256             ("Red","Solid")        : wx
.RED_BRUSH
 
 259             (None,"Transparent",1)   : wx
.TRANSPARENT_PEN
, 
 260             ("Green","Solid",1)      : wx
.GREEN_PEN
, 
 261             ("White","Solid",1)      : wx
.WHITE_PEN
, 
 262             ("Black","Solid",1)      : wx
.BLACK_PEN
, 
 263             ("Grey","Solid",1)       : wx
.GREY_PEN
, 
 264             ("MediumGrey","Solid",1) : wx
.MEDIUM_GREY_PEN
, 
 265             ("LightGrey","Solid",1)  : wx
.LIGHT_GREY_PEN
, 
 266             ("Cyan","Solid",1)       : wx
.CYAN_PEN
, 
 267             ("Red","Solid",1)        : wx
.RED_PEN
 
 271             "Transparent"    : wx
.TRANSPARENT
, 
 273             "BiDiagonalHatch": wx
.BDIAGONAL_HATCH
, 
 274             "CrossDiagHatch" : wx
.CROSSDIAG_HATCH
, 
 275             "FDiagonal_Hatch": wx
.FDIAGONAL_HATCH
, 
 276             "CrossHatch"     : wx
.CROSS_HATCH
, 
 277             "HorizontalHatch": wx
.HORIZONTAL_HATCH
, 
 278             "VerticalHatch"  : wx
.VERTICAL_HATCH
 
 283             "Transparent": wx
.TRANSPARENT
, 
 285             "LongDash"   : wx
.LONG_DASH
, 
 286             "ShortDash"  : wx
.SHORT_DASH
, 
 287             "DotDash"    : wx
.DOT_DASH
, 
 290     def Bind(self
, Event
, CallBackFun
): 
 291         self
.CallBackFuncs
[Event
] = CallBackFun
 
 293         self
._Canvas
.UseHitTest 
= True 
 294         if not self
._Canvas
._HTdc
: 
 295             self
._Canvas
.MakeNewHTdc() 
 296         if not self
.HitColor
: 
 297             if not self
._Canvas
.HitColorGenerator
: 
 298                 self
._Canvas
.HitColorGenerator 
= _colorGenerator() 
 299                 self
._Canvas
.HitColorGenerator
.next() # first call to prevent the background color from being used. 
 300             self
.HitColor 
= self
._Canvas
.HitColorGenerator
.next() 
 301             self
.SetHitPen(self
.HitColor
,self
.HitLineWidth
) 
 302             self
.SetHitBrush(self
.HitColor
) 
 303         # put the object in the hit dict, indexed by it's color 
 304         if not self
._Canvas
.HitDict
: 
 305             self
._Canvas
.MakeHitDict() 
 306         self
._Canvas
.HitDict
[Event
][self
.HitColor
] = (self
) # put the object in the hit dict, indexed by it's color 
 309         ## fixme: this only removes one from each list, there could be more. 
 310         if self
._Canvas
.HitDict
: 
 311             for List 
in self
._Canvas
.HitDict
.itervalues(): 
 319     def SetBrush(self
,FillColor
,FillStyle
): 
 320         if FillColor 
is None or FillStyle 
is None: 
 321             self
.Brush 
= wx
.TRANSPARENT_BRUSH
 
 322             ##fixme: should I really re-set the style? 
 323             self
.FillStyle 
= "Transparent" 
 325             self
.Brush 
= self
.BrushList
.setdefault( (FillColor
,FillStyle
),  wx
.Brush(FillColor
,self
.FillStyleList
[FillStyle
] ) ) 
 327     def SetPen(self
,LineColor
,LineStyle
,LineWidth
): 
 328         if (LineColor 
is None) or (LineStyle 
is None): 
 329             self
.Pen 
= wx
.TRANSPARENT_PEN
 
 330             self
.LineStyle 
= 'Transparent' 
 332              self
.Pen 
= self
.PenList
.setdefault( (LineColor
,LineStyle
,LineWidth
),  wx
.Pen(LineColor
,LineWidth
,self
.LineStyleList
[LineStyle
]) ) 
 334     def SetHitBrush(self
,HitColor
): 
 336             self
.HitBrush 
= wx
.TRANSPARENT_BRUSH
 
 338             self
.HitBrush 
= self
.BrushList
.setdefault( (HitColor
,"solid"),  wx
.Brush(HitColor
,self
.FillStyleList
["Solid"] ) ) 
 340     def SetHitPen(self
,HitColor
,LineWidth
): 
 342             self
.HitPen 
= wx
.TRANSPARENT_PEN
 
 344             self
.HitPen 
= self
.PenList
.setdefault( (HitColor
, "solid", self
.HitLineWidth
),  wx
.Pen(HitColor
, self
.HitLineWidth
, self
.LineStyleList
["Solid"]) ) 
 346     def PutInBackground(self
): 
 347         if self
._Canvas 
and self
.InForeground
: 
 348             self
._Canvas
._ForeDrawList
.remove(self
) 
 349             self
._Canvas
._DrawList
.append(self
) 
 350             self
._Canvas
._BackgroundDirty 
= True 
 351             self
.InForeground 
= False 
 353     def PutInForeground(self
): 
 354         if self
._Canvas 
and (not self
.InForeground
): 
 355             self
._Canvas
._ForeDrawList
.append(self
) 
 356             self
._Canvas
._DrawList
.remove(self
) 
 357             self
._Canvas
._BackgroundDirty 
= True 
 358             self
.InForeground 
= True 
 366 class ColorOnlyMixin
: 
 369     Mixin class for objects that have just one color, rather than a fill 
 374     def SetColor(self
, Color
): 
 375         self
.SetPen(Color
,"Solid",1) 
 376         self
.SetBrush(Color
,"Solid") 
 378     SetFillColor 
= SetColor 
# Just to provide a consistant interface  
 383     Mixin class for objects that have just one color, rather than a fill 
 388     def SetLineColor(self
, LineColor
): 
 389         self
.LineColor 
= LineColor
 
 390         self
.SetPen(LineColor
,self
.LineStyle
,self
.LineWidth
) 
 392     def SetLineStyle(self
, LineStyle
): 
 393         self
.LineStyle 
= LineStyle
 
 394         self
.SetPen(self
.LineColor
,LineStyle
,self
.LineWidth
) 
 396     def SetLineWidth(self
, LineWidth
): 
 397         self
.LineWidth 
= LineWidth
 
 398         self
.SetPen(self
.LineColor
,self
.LineStyle
,LineWidth
) 
 400 class LineAndFillMixin(LineOnlyMixin
): 
 403     Mixin class for objects that have both a line and a fill color and 
 407     def SetFillColor(self
, FillColor
): 
 408         self
.FillColor 
= FillColor
 
 409         self
.SetBrush(FillColor
, self
.FillStyle
) 
 411     def SetFillStyle(self
, FillStyle
): 
 412         self
.FillStyle 
= FillStyle
 
 413         self
.SetBrush(self
.FillColor
,FillStyle
) 
 418     This is a mixin class that provides some methods suitable for use 
 419     with objects that have a single (x,y) coordinate pair. 
 423     def Move(self
, Delta 
): 
 426         Move(Delta): moves the object by delta, where delta is a 
 427         (dx,dy) pair. Ideally a Numpy array of shape (2,) 
 431         Delta 
= asarray(Delta
, Float
) 
 433         self
.BoundingBox 
= self
.BoundingBox 
+ Delta
 
 436             self
._Canvas
.BoundingBoxDirty 
= True       
 438     def CalcBoundingBox(self
): 
 439         ## This may get overwritten in some subclasses 
 440         self
.BoundingBox 
= array( (self
.XY
, self
.XY
), Float 
) 
 442     def SetPoint(self
, xy
): 
 443         xy 
= array( xy
, Float
) 
 448         self
.BoundingBox 
= self
.BoundingBox 
+ Delta
 
 450         #self.CalcBoundingBox() 
 452             self
._Canvas
.BoundingBoxDirty 
= True      
 454 class PointsObjectMixin
: 
 457     This is a mixin class that provides some methods suitable for use 
 458     with objects that have a set of (x,y) coordinate pairs. 
 463 ## This is code for the PointsObjectMixin object, it needs to be adapted and tested. 
 464 ## Is the neccesary at all: you can always do: 
 465 ##    Object.SetPoints( Object.Points + delta, copy = False)     
 466 ##    def Move(self, Delta ): 
 469 ##        Move(Delta): moves the object by delta, where delta is an (dx, 
 470 ##        dy) pair. Ideally a Numpy array of shape (2,) 
 474 ##        Delta = array(Delta, Float) 
 476 ##        self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float) 
 478 ##            self._Canvas.BoundingBoxDirty = True       
 480     def CalcBoundingBox(self
): 
 481         self
.BoundingBox 
= array(((min(self
.Points
[:,0]), 
 482                                    min(self
.Points
[:,1]) ), 
 483                                   (max(self
.Points
[:,0]), 
 484                                    max(self
.Points
[:,1]) ) ), Float 
) 
 486             self
._Canvas
.BoundingBoxDirty 
= True 
 488     def SetPoints(self
, Points
, copy 
= True): 
 490         Sets the coordinates of the points of the object to Points (NX2 array). 
 492         By default, a copy is made, if copy is set to False, a reference 
 493         is used, iff Points is a NumPy array of Floats. This allows you 
 494         to change some or all of the points without making any copies. 
 498         Points = Object.Points 
 499         Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir. 
 500         Object.SetPoints(Points, False) # Sets the points to the same array as it was 
 504             self
.Points 
= array(Points
, Float
) 
 505             self
.Points
.shape 
= (-1,2) # Make sure it is a NX2 array, even if there is only one point 
 507             self
.Points 
= asarray(Points
, Float
) 
 508         self
.CalcBoundingBox() 
 511 class Polygon(DrawObject
,PointsObjectMixin
,LineAndFillMixin
): 
 515     The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of 
 516     point coordinates.  so that Points[N][0] is the x-coordinate of 
 517     point N and Points[N][1] is the y-coordinate or Points[N,0] is the 
 518     x-coordinate of point N and Points[N,1] is the y-coordinate for 
 521     The other parameters specify various properties of the Polygon, and 
 522     should be self explanatory. 
 532                  InForeground 
= False): 
 533         DrawObject
.__init
__(self
,InForeground
) 
 534         self
.Points 
= array(Points
,Float
) # this DOES need to make a copy 
 535         self
.CalcBoundingBox() 
 537         self
.LineColor 
= LineColor
 
 538         self
.LineStyle 
= LineStyle
 
 539         self
.LineWidth 
= LineWidth
 
 540         self
.FillColor 
= FillColor
 
 541         self
.FillStyle 
= FillStyle
 
 543         self
.HitLineWidth 
= max(LineWidth
,self
.MinHitLineWidth
) 
 545         self
.SetPen(LineColor
,LineStyle
,LineWidth
) 
 546         self
.SetBrush(FillColor
,FillStyle
) 
 548     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel 
= None, HTdc
=None): 
 549         Points 
= WorldToPixel(self
.Points
)#.tolist() 
 551         dc
.SetBrush(self
.Brush
) 
 552         dc
.DrawPolygon(Points
) 
 553         if HTdc 
and self
.HitAble
: 
 554             HTdc
.SetPen(self
.HitPen
) 
 555             HTdc
.SetBrush(self
.HitBrush
) 
 556             HTdc
.DrawPolygon(Points
) 
 558 ##class PolygonSet(DrawObject): 
 560 ##    The PolygonSet class takes a Geometry.Polygon object. 
 561 ##    so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! 
 563 ##    it creates a set of line segments, from (x1,y1) to (x2,y2) 
 567 ##    def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False): 
 568 ##        DrawObject.__init__(self, InForeground) 
 570 ##        ##fixme: there should be some error checking for everything being the right length. 
 573 ##        self.Points = array(Points,Float) 
 574 ##        self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) 
 576 ##        self.LineColors = LineColors 
 577 ##        self.LineStyles = LineStyles 
 578 ##        self.LineWidths = LineWidths 
 579 ##        self.FillColors = FillColors 
 580 ##        self.FillStyles = FillStyles 
 582 ##        self.SetPens(LineColors,LineStyles,LineWidths) 
 584 ##    #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): 
 585 ##    def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): 
 586 ##        Points = WorldToPixel(self.Points) 
 587 ##        Points.shape = (-1,4) 
 588 ##        dc.DrawLineList(Points,self.Pens) 
 591 class Line(DrawObject
,PointsObjectMixin
,LineOnlyMixin
): 
 594     The Line class takes a list of 2-tuples, or a NX2 NumPy Float array 
 595     of point coordinates. 
 597     It will draw a straight line if there are two points, and a polyline 
 598     if there are more than two. 
 601     def __init__(self
,Points
, 
 605                  InForeground 
= False): 
 606         DrawObject
.__init
__(self
, InForeground
) 
 609         self
.Points 
= array(Points
,Float
) 
 610         self
.CalcBoundingBox() 
 612         self
.LineColor 
= LineColor
 
 613         self
.LineStyle 
= LineStyle
 
 614         self
.LineWidth 
= LineWidth
 
 616         self
.SetPen(LineColor
,LineStyle
,LineWidth
) 
 618         self
.HitLineWidth 
= max(LineWidth
,self
.MinHitLineWidth
) 
 621     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 622         Points 
= WorldToPixel(self
.Points
) 
 625         if HTdc 
and self
.HitAble
: 
 626             HTdc
.SetPen(self
.HitPen
) 
 627             HTdc
.DrawLines(Points
) 
 629 class Arrow(DrawObject
,XYObjectMixin
,LineOnlyMixin
): 
 632     Arrow(XY, # coords of origin of arrow (x,y) 
 633           Length, # length of arrow in pixels 
 634           theta, # angle of arrow in degrees: zero is straight up 
 635                  # angle is to the right 
 641           InForeground = False): 
 643     It will draw an arrow , starting at the point, (X,Y) pointing in 
 654                  LineWidth    
= 2, # pixels 
 655                  ArrowHeadSize 
= 8, # pixels 
 656                  ArrowHeadAngle 
= 30, # degrees 
 657                  InForeground 
= False): 
 659         DrawObject
.__init
__(self
, InForeground
) 
 661         self
.XY 
= array(XY
, Float
) 
 662         self
.XY
.shape 
= (2,) # Make sure it is a 1X2 array, even if there is only one point 
 664         self
.Direction 
= float(Direction
) 
 665         self
.ArrowHeadSize 
= ArrowHeadSize 
 
 666         self
.ArrowHeadAngle 
= float(ArrowHeadAngle
)         
 668         self
.CalcArrowPoints() 
 669         self
.CalcBoundingBox() 
 671         self
.LineColor 
= LineColor
 
 672         self
.LineStyle 
= LineStyle
 
 673         self
.LineWidth 
= LineWidth
 
 675         self
.SetPen(LineColor
,LineStyle
,LineWidth
) 
 677         ##fixme: How should the HitTest be drawn? 
 678         self
.HitLineWidth 
= max(LineWidth
,self
.MinHitLineWidth
) 
 680     def SetDirection(self
, Direction
): 
 681         self
.Direction 
= float(Direction
) 
 682         self
.CalcArrowPoints() 
 684     def SetLength(self
, Length
): 
 686         self
.CalcArrowPoints() 
 688     def SetLengthDirection(self
, Length
, Direction
): 
 689         self
.Direction 
= float(Direction
) 
 691         self
.CalcArrowPoints() 
 693     def SetLength(self
, Length
): 
 695         self
.CalcArrowPoints() 
 697     ## fixme: cache this? 
 698     def CalcArrowPoints(self
): 
 700         S 
= self
.ArrowHeadSize
 
 701         phi 
= self
.ArrowHeadAngle 
* pi 
/ 360 
 702         theta 
= (self
.Direction
-90.0) * pi 
/ 180 
 703         ArrowPoints 
= array( ( (0, L
, L 
- S
*cos(phi
),L
, L 
- S
*cos(phi
) ), 
 704                                (0, 0, S
*sin(phi
),    0, -S
*sin(phi
)    ) ), 
 706         RotationMatrix 
= array( ( ( cos(theta
), -sin(theta
) ), 
 707                                   ( sin(theta
), cos(theta
) ) ), 
 710         ArrowPoints 
= matrixmultiply(RotationMatrix
, ArrowPoints
) 
 711         self
.ArrowPoints 
= transpose(ArrowPoints
) 
 713     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 715         xy 
= WorldToPixel(self
.XY
) 
 716         ArrowPoints 
= xy 
+ self
.ArrowPoints
 
 717         dc
.DrawLines(ArrowPoints
) 
 718         if HTdc 
and self
.HitAble
: 
 719             HTdc
.SetPen(self
.HitPen
) 
 720             HTdc
.DrawLines(ArrowPoints
) 
 722 ##class LineSet(DrawObject, ObjectSetMixin): 
 724 ##    The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. 
 725 ##    so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! 
 727 ##    it creates a set of line segments, from (x1,y1) to (x2,y2) 
 731 ##    def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False): 
 732 ##        DrawObject.__init__(self, InForeground) 
 734 ##        NumLines = len(Points) / 2 
 735 ##        ##fixme: there should be some error checking for everything being the right length. 
 738 ##        self.Points = array(Points,Float) 
 739 ##        self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) 
 741 ##        self.LineColors = LineColors 
 742 ##        self.LineStyles = LineStyles 
 743 ##        self.LineWidths = LineWidths 
 745 ##        self.SetPens(LineColors,LineStyles,LineWidths) 
 747 ##    #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): 
 748 ##    def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): 
 749 ##        Points = WorldToPixel(self.Points) 
 750 ##        Points.shape = (-1,4) 
 751 ##        dc.DrawLineList(Points,self.Pens) 
 753 class PointSet(DrawObject
,PointsObjectMixin
, ColorOnlyMixin
): 
 756     The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of 
 759     If Points is a sequence of tuples: Points[N][0] is the x-coordinate of 
 760     point N and Points[N][1] is the y-coordinate. 
 762     If Points is a NumPy array: Points[N,0] is the x-coordinate of point 
 763     N and Points[N,1] is the y-coordinate for arrays. 
 765     Each point will be drawn the same color and Diameter. The Diameter 
 766     is in screen pixels, not world coordinates. 
 768     The hit-test code does not distingish between the points, you will 
 769     only know that one of the points got hit, not which one. You can use 
 770     PointSet.FindClosestPoint(WorldPoint) to find out which one 
 772     In the case of points, the HitLineWidth is used as diameter. 
 775     def __init__(self
, Points
, Color 
= "Black", Diameter 
=  1, InForeground 
= False): 
 776         DrawObject
.__init
__(self
,InForeground
) 
 778         self
.Points 
= array(Points
,Float
) 
 779         self
.Points
.shape 
= (-1,2) # Make sure it is a NX2 array, even if there is only one point 
 780         self
.CalcBoundingBox() 
 781         self
.Diameter 
= Diameter
 
 783         self
.HitLineWidth 
= self
.MinHitLineWidth
 
 786     def SetDiameter(self
,Diameter
): 
 787             self
.Diameter 
= Diameter
 
 789     def FindClosestPoint(self
, XY
): 
 792         Returns the index of the closest point to the point, XY, given 
 793         in World coordinates. It's essentially random which you get if 
 794         there are more than one that are the same. 
 796         This can be used to figure out which point got hit in a mouse 
 797         binding callback, for instance. It's a lot faster that using a 
 798         lot of separate points. 
 802         return argmin(hypot(d
[:,0],d
[:,1])) 
 805     def DrawD2(self
, dc
, Points
): 
 806         # A Little optimization for a diameter2 - point 
 807         dc
.DrawPointList(Points
) 
 808         dc
.DrawPointList(Points 
+ (1,0)) 
 809         dc
.DrawPointList(Points 
+ (0,1)) 
 810         dc
.DrawPointList(Points 
+ (1,1)) 
 812     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 814         Points 
= WorldToPixel(self
.Points
) 
 815         if self
.Diameter 
<= 1: 
 816             dc
.DrawPointList(Points
) 
 817         elif self
.Diameter 
<= 2: 
 818             self
.DrawD2(dc
, Points
) 
 820             dc
.SetBrush(self
.Brush
) 
 821             radius 
= int(round(self
.Diameter
/2)) 
 822             ##fixme: I really should add a DrawCircleList to wxPython 
 823             if len(Points
) > 100: 
 825                 xywh 
= concatenate((xy
-radius
, ones(xy
.shape
) * self
.Diameter 
), 1 ) 
 826                 dc
.DrawEllipseList(xywh
) 
 829                     dc
.DrawCircle(xy
[0],xy
[1], radius
) 
 830         if HTdc 
and self
.HitAble
: 
 831             HTdc
.SetPen(self
.HitPen
) 
 832             HTdc
.SetBrush(self
.HitBrush
) 
 833             if self
.Diameter 
<= 1: 
 834                 HTdc
.DrawPointList(Points
) 
 835             elif self
.Diameter 
<= 2: 
 836                 self
.DrawD2(HTdc
, Points
) 
 838                 if len(Points
) > 100: 
 840                     xywh 
= concatenate((xy
-radius
, ones(xy
.shape
) * self
.Diameter 
), 1 ) 
 841                     HTdc
.DrawEllipseList(xywh
) 
 844                         HTdc
.DrawCircle(xy
[0],xy
[1], radius
) 
 846 class Point(DrawObject
,XYObjectMixin
,ColorOnlyMixin
): 
 849     The Point class takes a 2-tuple, or a (2,) NumPy array of point 
 852     The Diameter is in screen points, not world coordinates, So the 
 853     Bounding box is just the point, and doesn't include the Diameter. 
 855     The HitLineWidth is used as diameter for the 
 859     def __init__(self
, XY
, Color 
= "Black", Diameter 
=  1, InForeground 
= False): 
 860         DrawObject
.__init
__(self
, InForeground
) 
 862         self
.XY 
= array(XY
, Float
) 
 863         self
.XY
.shape 
= (2,) # Make sure it is a 1X2 array, even if there is only one point 
 864         self
.CalcBoundingBox() 
 866         self
.Diameter 
= Diameter
 
 868         self
.HitLineWidth 
= self
.MinHitLineWidth
 
 870     def SetDiameter(self
,Diameter
): 
 871             self
.Diameter 
= Diameter
 
 874     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 876         xy 
= WorldToPixel(self
.XY
) 
 877         if self
.Diameter 
<= 1: 
 878             dc
.DrawPoint(xy
[0], xy
[1]) 
 880             dc
.SetBrush(self
.Brush
) 
 881             radius 
= int(round(self
.Diameter
/2)) 
 882             dc
.DrawCircle(xy
[0],xy
[1], radius
) 
 883         if HTdc 
and self
.HitAble
: 
 884             HTdc
.SetPen(self
.HitPen
) 
 885             if self
.Diameter 
<= 1: 
 886                 HTdc
.DrawPoint(xy
[0], xy
[1]) 
 888                 HTdc
.SetBrush(self
.HitBrush
) 
 889                 HTdc
.DrawCircle(xy
[0],xy
[1], radius
) 
 891 class SquarePoint(DrawObject
,XYObjectMixin
,ColorOnlyMixin
): 
 894     The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point 
 895     coordinates. It produces a square dot, centered on Point 
 897     The Size is in screen points, not world coordinates, so the 
 898     Bounding box is just the point, and doesn't include the Size. 
 900     The HitLineWidth is used as diameter for the 
 904     def __init__(self
, Point
, Color 
= "Black", Size 
=  4, InForeground 
= False): 
 905         DrawObject
.__init
__(self
, InForeground
) 
 907         self
.XY 
= array(Point
, Float
) 
 908         self
.XY
.shape 
= (2,) # Make sure it is a 1X2 array, even if there is only one point 
 909         self
.CalcBoundingBox() 
 913         self
.HitLineWidth 
= self
.MinHitLineWidth
 
 915     def SetSize(self
,Size
): 
 918     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 921         xc
,yc 
= WorldToPixel(self
.XY
) 
 928             dc
.SetBrush(self
.Brush
) 
 929             dc
.DrawRectangle(x
, y
, Size
, Size
) 
 930         if HTdc 
and self
.HitAble
: 
 931             HTdc
.SetPen(self
.HitPen
) 
 933                 HTdc
.DrawPoint(xc
, xc
) 
 935                 HTdc
.SetBrush(self
.HitBrush
) 
 936                 HTdc
.DrawRectangle(x
, y
, Size
, Size
) 
 938 class RectEllipse(DrawObject
, XYObjectMixin
, LineAndFillMixin
): 
 939     def __init__(self
, XY
, WH
, 
 945                  InForeground 
= False): 
 947         DrawObject
.__init
__(self
,InForeground
) 
 949         self
.XY 
= array( XY
, Float
) 
 951         self
.WH 
= array( WH
, Float 
) 
 953         self
.BoundingBox 
= array((self
.XY
, (self
.XY 
+ self
.WH
)), Float
) 
 954         self
.LineColor 
= LineColor
 
 955         self
.LineStyle 
= LineStyle
 
 956         self
.LineWidth 
= LineWidth
 
 957         self
.FillColor 
= FillColor
 
 958         self
.FillStyle 
= FillStyle
 
 960         self
.HitLineWidth 
= max(LineWidth
,self
.MinHitLineWidth
) 
 962         self
.SetPen(LineColor
,LineStyle
,LineWidth
) 
 963         self
.SetBrush(FillColor
,FillStyle
) 
 965     def SetShape(self
, XY
, WH
): 
 966         self
.XY 
= array( XY
, Float
) 
 967         self
.WH 
= array( WH
, Float 
) 
 968         self
.CalcBoundingBox() 
 971     def SetUpDraw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
): 
 973         dc
.SetBrush(self
.Brush
) 
 974         if HTdc 
and self
.HitAble
: 
 975             HTdc
.SetPen(self
.HitPen
) 
 976             HTdc
.SetBrush(self
.HitBrush
) 
 977         return ( WorldToPixel(self
.XY
), 
 978                  ScaleWorldToPixel(self
.WH
) ) 
 980     def CalcBoundingBox(self
): 
 981         self
.BoundingBox 
= array((self
.XY
, (self
.XY 
+ self
.WH
) ), Float
) 
 982         self
._Canvas
.BoundingBoxDirty 
= True 
 985 class Rectangle(RectEllipse
): 
 987     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 988         ( XY
, WH 
) = self
.SetUpDraw(dc
, 
 992         dc
.DrawRectanglePointSize(XY
, WH
) 
 993         if HTdc 
and self
.HitAble
: 
 994             HTdc
.DrawRectanglePointSize(XY
, WH
) 
 996 class Ellipse(RectEllipse
): 
 998     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
 999         ( XY
, WH 
) = self
.SetUpDraw(dc
, 
1003         dc
.DrawEllipsePointSize(XY
, WH
) 
1004         if HTdc 
and self
.HitAble
: 
1005             HTdc
.DrawEllipsePointSize(XY
, WH
) 
1007 class Circle(Ellipse
): 
1009     def __init__(self
, XY
, Diameter
, **kwargs
): 
1010         self
.Center 
= array(XY
, Float
) 
1011         Diameter 
= float(Diameter
) 
1012         RectEllipse
.__init
__(self 
, 
1013                              self
.Center 
- Diameter
/2.0, 
1014                              (Diameter
, Diameter
), 
1017     def SetDiameter(self
, Diameter
): 
1018         Diameter 
= float(Diameter
) 
1019         XY 
= self
.Center 
- (Diameter
/2.0) 
1021                       (Diameter
, Diameter
) 
1024 class TextObjectMixin(XYObjectMixin
): 
1027     A mix in class that holds attributes and methods that are needed by 
1032     ## I'm caching fonts, because on GTK, getting a new font can take a 
1033     ## while. However, it gets cleared after every full draw as hanging 
1034     ## on to a bunch of large fonts takes a massive amount of memory. 
1038     LayoutFontSize 
= 12 # font size used for calculating layout 
1040     def SetFont(self
, Size
, Family
, Style
, Weight
, Underline
, FaceName
): 
1041         self
.Font 
= self
.FontList
.setdefault( (Size
, 
1055     def SetColor(self
, Color
): 
1058     def SetBackgroundColor(self
, BackgroundColor
): 
1059         self
.BackgroundColor 
= BackgroundColor
 
1061     def SetText(self
, String
): 
1063         Re-sets the text displayed by the object 
1065         In the case of the ScaledTextBox, it will re-do the layout as appropriate 
1067         Note: only tested with the ScaledTextBox 
1071         self
.String 
= String
 
1074     def LayoutText(self
): 
1076         A dummy method to re-do the layout of the text. 
1078         A derived object needs to override this if required. 
1083     ## store the function that shift the coords for drawing text. The 
1084     ## "c" parameter is the correction for world coordinates, rather 
1085     ## than pixel coords as the y axis is reversed 
1086     ## pad is the extra space around the text 
1087     ## if world = 1, the vertical shift is done in y-up coordinates 
1088     ShiftFunDict 
= {'tl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
+ pad
,     y 
+ pad 
- 2*world
*pad
), 
1089                     'tc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w
/2,     y 
+ pad 
- 2*world
*pad
),  
1090                     'tr': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w 
- pad
, y 
+ pad 
- 2*world
*pad
),  
1091                     'cl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
+ pad
,     y 
- h
/2 + world
*h
),  
1092                     'cc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w
/2,     y 
- h
/2 + world
*h
),  
1093                     'cr': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w 
- pad
, y 
- h
/2 + world
*h
), 
1094                     'bl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
+ pad
,     y 
- h 
+ 2*world
*h 
- pad 
+ world
*2*pad
) , 
1095                     'bc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w
/2,     y 
- h 
+ 2*world
*h 
- pad 
+ world
*2*pad
) ,  
1096                     'br': lambda x
, y
, w
, h
, world
=0, pad
=0: (x 
- w 
- pad
, y 
- h 
+ 2*world
*h 
- pad 
+ world
*2*pad
)} 
1098 class Text(DrawObject
, TextObjectMixin
): 
1100     This class creates a text object, placed at the coordinates, 
1101     x,y. the "Position" argument is a two charactor string, indicating 
1102     where in relation to the coordinates the string should be oriented. 
1104     The first letter is: t, c, or b, for top, center and bottom The 
1105     second letter is: l, c, or r, for left, center and right The 
1106     position refers to the position relative to the text itself. It 
1107     defaults to "tl" (top left). 
1109     Size is the size of the font in pixels, or in points for printing 
1110     (if it ever gets implimented). Those will be the same, If you assume 
1114         Font family, a generic way of referring to fonts without 
1115         specifying actual facename. One of: 
1116             wx.DEFAULT:  Chooses a default font.  
1117             wx.DECORATIVE: A decorative font.  
1118             wx.ROMAN: A formal, serif font.  
1119             wx.SCRIPT: A handwriting font.  
1120             wx.SWISS: A sans-serif font.  
1121             wx.MODERN: A fixed pitch font. 
1122         NOTE: these are only as good as the wxWindows defaults, which aren't so good. 
1124         One of wx.NORMAL, wx.SLANT and wx.ITALIC. 
1126         One of wx.NORMAL, wx.LIGHT and wx.BOLD. 
1128         The value can be True or False. At present this may have an an 
1129         effect on Windows only. 
1131     Alternatively, you can set the kw arg: Font, to a wx.Font, and the 
1132     above will be ignored. 
1134     The size is fixed, and does not scale with the drawing. 
1136     The hit-test is done on the entire text extent 
1140     def __init__(self
,String
, xy
, 
1143                  BackgroundColor 
= None, 
1149                  InForeground 
= False, 
1152         DrawObject
.__init
__(self
,InForeground
) 
1154         self
.String 
= String
 
1155         # Input size in in Pixels, compute points size from PPI info. 
1156         # fixme: for printing, we'll have to do something a little different 
1157         self
.Size 
= int(round(72.0 * Size 
/ ScreenPPI
)) 
1160         self
.BackgroundColor 
= BackgroundColor
 
1165             FaceName           
=  Font
.GetFaceName()            
1166             Family             
=  Font
.GetFamily() 
1167             Size               
=  Font
.GetPointSize()           
1168             Style              
=  Font
.GetStyle() 
1169             Underlined         
=  Font
.GetUnderlined()          
1170             Weight             
=  Font
.GetWeight() 
1171         self
.SetFont(Size
, Family
, Style
, Weight
, Underline
, FaceName
) 
1173         self
.BoundingBox 
= array((xy
, xy
),Float
) 
1175         self
.XY 
= asarray(xy
) 
1176         self
.XY
.shape 
= (2,) 
1178         (self
.TextWidth
, self
.TextHeight
) = (None, None) 
1179         self
.ShiftFun 
= self
.ShiftFunDict
[Position
] 
1181     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
1182         XY 
= WorldToPixel(self
.XY
) 
1183         dc
.SetFont(self
.Font
) 
1184         dc
.SetTextForeground(self
.Color
) 
1185         if self
.BackgroundColor
: 
1186             dc
.SetBackgroundMode(wx
.SOLID
) 
1187             dc
.SetTextBackground(self
.BackgroundColor
) 
1189             dc
.SetBackgroundMode(wx
.TRANSPARENT
) 
1190         if self
.TextWidth 
is None or self
.TextHeight 
is None: 
1191             (self
.TextWidth
, self
.TextHeight
) = dc
.GetTextExtent(self
.String
) 
1192         XY 
= self
.ShiftFun(XY
[0], XY
[1], self
.TextWidth
, self
.TextHeight
) 
1193         dc
.DrawTextPoint(self
.String
, XY
) 
1194         if HTdc 
and self
.HitAble
: 
1195             HTdc
.SetPen(self
.HitPen
) 
1196             HTdc
.SetBrush(self
.HitBrush
) 
1197             HTdc
.DrawRectanglePointSize(XY
, (self
.TextWidth
, self
.TextHeight
) ) 
1199 class ScaledText(DrawObject
, TextObjectMixin
): 
1201     This class creates a text object that is scaled when zoomed.  It is 
1202     placed at the coordinates, x,y. the "Position" argument is a two 
1203     charactor string, indicating where in relation to the coordinates 
1204     the string should be oriented. 
1206     The first letter is: t, c, or b, for top, center and bottom The 
1207     second letter is: l, c, or r, for left, center and right The 
1208     position refers to the position relative to the text itself. It 
1209     defaults to "tl" (top left). 
1211     Size is the size of the font in world coordinates. 
1214         Font family, a generic way of referring to fonts without 
1215         specifying actual facename. One of: 
1216             wx.DEFAULT:  Chooses a default font.  
1217             wx.DECORATI: A decorative font.  
1218             wx.ROMAN: A formal, serif font.  
1219             wx.SCRIPT: A handwriting font.  
1220             wx.SWISS: A sans-serif font.  
1221             wx.MODERN: A fixed pitch font. 
1222         NOTE: these are only as good as the wxWindows defaults, which aren't so good. 
1224         One of wx.NORMAL, wx.SLANT and wx.ITALIC. 
1226         One of wx.NORMAL, wx.LIGHT and wx.BOLD. 
1228         The value can be True or False. At present this may have an an 
1229         effect on Windows only. 
1231     Alternatively, you can set the kw arg: Font, to a wx.Font, and the 
1232     above will be ignored. The size of the font you specify will be 
1233     ignored, but the rest of its attributes will be preserved. 
1235     The size will scale as the drawing is zoomed. 
1239     As fonts are scaled, the do end up a little different, so you don't 
1240     get exactly the same picture as you scale up and doen, but it's 
1243     On wxGTK1 on my Linux system, at least, using a font of over about 
1244     3000 pts. brings the system to a halt. It's the Font Server using 
1245     huge amounts of memory. My work around is to max the font size to 
1246     3000 points, so it won't scale past there. GTK2 uses smarter font 
1247     drawing, so that may not be an issue in future versions, so feel 
1248     free to test. Another smarter way to do it would be to set a global 
1249     zoom limit at that point. 
1251     The hit-test is done on the entire text extent. This could be made 
1252     optional, but I haven't gotten around to it. 
1256     def __init__(self
, String
, XY 
, Size
, 
1258                  BackgroundColor 
= None, 
1265                  InForeground 
= False): 
1267         DrawObject
.__init
__(self
,InForeground
) 
1269         self
.String 
= String
 
1270         self
.XY 
= array( XY
, Float
) 
1271         self
.XY
.shape 
= (2,) 
1274         self
.BackgroundColor 
= BackgroundColor
 
1275         self
.Family 
= Family   
 
1277         self
.Weight 
= Weight   
 
1278         self
.Underline 
= Underline
 
1282             self
.FaceName           
=  Font
.GetFaceName()            
1283             self
.Family             
=  Font
.GetFamily()     
1284             self
.Style              
=  Font
.GetStyle()      
1285             self
.Underlined         
=  Font
.GetUnderlined()          
1286             self
.Weight             
=  Font
.GetWeight()     
1288         # Experimental max font size value on wxGTK2: this works OK on 
1289         # my system. If it's a lot  larger, there is a crash, with the 
1292         # The application 'FloatCanvasDemo.py' lost its 
1293         # connection to the display :0.0; most likely the X server was 
1294         # shut down or you killed/destroyed the application. 
1296         # Windows and OS-X seem to be better behaved in this regard. 
1297         # They may not draw it, but they don't crash either! 
1298         self
.MaxFontSize 
= 1000 
1300         self
.ShiftFun 
= self
.ShiftFunDict
[Position
] 
1302         self
.CalcBoundingBox() 
1304     def LayoutText(self
): 
1305         # This will be called when the text is re-set 
1306         # nothing much to be done here 
1307         self
.CalcBoundingBox() 
1309     def CalcBoundingBox(self
): 
1310         ## this isn't exact, as fonts don't scale exactly. 
1312         bitmap 
= wx
.EmptyBitmap(1, 1) 
1313         dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work. 
1314         DrawingSize 
= 40 # pts This effectively determines the resolution that the BB is computed to. 
1315         ScaleFactor 
= float(self
.Size
) / DrawingSize
 
1316         dc
.SetFont(self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underline
, self
.FaceName
) ) 
1317         (w
,h
) = dc
.GetTextExtent(self
.String
) 
1320         x
, y 
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world 
= 1) 
1321         self
.BoundingBox 
= array(((x
, y
-h 
),(x 
+ w
, y
)),Float
) 
1323     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
1324         (X
,Y
) = WorldToPixel( (self
.XY
) ) 
1326         # compute the font size: 
1327         Size 
= abs( ScaleWorldToPixel( (self
.Size
, self
.Size
) )[1] ) # only need a y coordinate length 
1328         ## Check to see if the font size is large enough to blow up the X font server 
1329         ## If so, limit it. Would it be better just to not draw it? 
1330         ## note that this limit is dependent on how much memory you have, etc. 
1331         Size 
= min(Size
, self
.MaxFontSize
) 
1332         dc
.SetFont(self
.SetFont(Size
, self
.Family
, self
.Style
, self
.Weight
, self
.Underline
, self
.FaceName
)) 
1333         dc
.SetTextForeground(self
.Color
) 
1334         if self
.BackgroundColor
: 
1335             dc
.SetBackgroundMode(wx
.SOLID
) 
1336             dc
.SetTextBackground(self
.BackgroundColor
) 
1338             dc
.SetBackgroundMode(wx
.TRANSPARENT
) 
1339         (w
,h
) = dc
.GetTextExtent(self
.String
) 
1340         # compute the shift, and adjust the coordinates, if neccesary 
1341         # This had to be put in here, because it changes with Zoom, as 
1342         # fonts don't scale exactly. 
1343         xy 
= self
.ShiftFun(X
, Y
, w
, h
) 
1345         dc
.DrawTextPoint(self
.String
, xy
) 
1346         if HTdc 
and self
.HitAble
: 
1347             HTdc
.SetPen(self
.HitPen
) 
1348             HTdc
.SetBrush(self
.HitBrush
) 
1349             HTdc
.DrawRectanglePointSize(xy
, (w
, h
) ) 
1351 class ScaledTextBox(DrawObject
, TextObjectMixin
): 
1353     This class creates a TextBox object that is scaled when zoomed.  It is 
1354     placed at the coordinates, x,y. 
1356     If the Width parameter is defined, the text will be wrapped to the width given. 
1358     A Box can be drawn around the text, be specifying: 
1359     LineWidth and/or  FillColor  
1361     A space(margin) can be put all the way around the text, be specifying: 
1362     the PadSize argument in world coordinates. 
1364     The spacing between lines can be adjusted with the: 
1365     LineSpacing argument. 
1367     The "Position" argument is a two character string, indicating where 
1368     in relation to the coordinates the Box should be oriented. 
1369     -The first letter is: t, c, or b, for top, center and bottom. 
1370     -The second letter is: l, c, or r, for left, center and right The 
1371     position refers to the position relative to the text itself. It 
1372     defaults to "tl" (top left). 
1374     Size is the size of the font in world coordinates. 
1377         Font family, a generic way of referring to fonts without 
1378         specifying actual facename. One of: 
1379             wx.DEFAULT:  Chooses a default font.  
1380             wx.DECORATIVE: A decorative font.  
1381             wx.ROMAN: A formal, serif font.  
1382             wx.SCRIPT: A handwriting font.  
1383             wx.SWISS: A sans-serif font.  
1384             wx.MODERN: A fixed pitch font. 
1385         NOTE: these are only as good as the wxWindows defaults, which aren't so good. 
1387         One of wx.NORMAL, wx.SLANT and wx.ITALIC. 
1389         One of wx.NORMAL, wx.LIGHT and wx.BOLD. 
1391         The value can be True or False. At present this may have an an 
1392         effect on Windows only. 
1394     Alternatively, you can set the kw arg: Font, to a wx.Font, and the 
1395     above will be ignored. The size of the font you specify will be 
1396     ignored, but the rest of its attributes will be preserved. 
1398     The size will scale as the drawing is zoomed. 
1402     As fonts are scaled, they do end up a little different, so you don't 
1403     get exactly the same picture as you scale up and down, but it's 
1406     On wxGTK1 on my Linux system, at least, using a font of over about 
1407     1000 pts. brings the system to a halt. It's the Font Server using 
1408     huge amounts of memory. My work around is to max the font size to 
1409     1000 points, so it won't scale past there. GTK2 uses smarter font 
1410     drawing, so that may not be an issue in future versions, so feel 
1411     free to test. Another smarter way to do it would be to set a global 
1412     zoom limit at that point. 
1414     The hit-test is done on the entire box. This could be made 
1415     optional, but I haven't gotten around to it. 
1419     def __init__(self
, String
, 
1423                  BackgroundColor 
= None, 
1424                  LineColor 
= 'Black', 
1425                  LineStyle 
= 'Solid', 
1437                  InForeground 
= False): 
1439         DrawObject
.__init
__(self
,InForeground
) 
1441         self
.XY 
= array(Point
, Float
) 
1444         self
.BackgroundColor 
= BackgroundColor
 
1445         self
.LineColor 
= LineColor
 
1446         self
.LineStyle 
= LineStyle
 
1447         self
.LineWidth 
= LineWidth
 
1449         if PadSize 
is None: # the default is just a little bit of padding 
1450             self
.PadSize 
= Size
/10.0 
1452             self
.PadSize 
= float(PadSize
) 
1453         self
.Family 
= Family   
 
1455         self
.Weight 
= Weight   
 
1456         self
.Underline 
= Underline
 
1457         self
.Alignment 
= Alignment
.lower() 
1458         self
.LineSpacing 
= float(LineSpacing
) 
1459         self
.Position 
= Position
 
1464             self
.FaceName           
=  Font
.GetFaceName()            
1465             self
.Family             
=  Font
.GetFamily()     
1466             self
.Style              
=  Font
.GetStyle()      
1467             self
.Underlined         
=  Font
.GetUnderlined()          
1468             self
.Weight             
=  Font
.GetWeight()     
1470         # Experimental max font size value on wxGTK2: this works OK on 
1471         # my system. If it's a lot  larger, there is a crash, with the 
1474         # The application 'FloatCanvasDemo.py' lost its 
1475         # connection to the display :0.0; most likely the X server was 
1476         # shut down or you killed/destroyed the application. 
1478         # Windows and OS-X seem to be better behaved in this regard. 
1479         # They may not draw it, but they don't crash either! 
1481         self
.MaxFontSize 
= 1000 
1482         self
.ShiftFun 
= self
.ShiftFunDict
[Position
] 
1484         self
.String 
= String
 
1486         self
.CalcBoundingBox() 
1488         self
.SetPen(LineColor
,LineStyle
,LineWidth
) 
1489         self
.SetBrush(BackgroundColor
, "Solid") 
1492     def WrapToWidth(self
): 
1494         bitmap 
= wx
.EmptyBitmap(1, 1) 
1495         dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work. 
1496         DrawingSize 
= self
.LayoutFontSize 
# pts This effectively determines the resolution that the BB is computed to. 
1497         ScaleFactor 
= float(self
.Size
) / DrawingSize
 
1498         Width 
= (self
.Width 
- 2*self
.PadSize
) / ScaleFactor 
#Width to wrap to 
1499         dc
.SetFont(self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underline
, self
.FaceName
) ) 
1502         for s 
in self
.Strings
: 
1510                 w  
= dc
.GetTextExtent(' ' + text
[-1])[0] 
1511                 if LineLength 
+ w 
<= Width
: 
1514                     LineLength 
= dc
.GetTextExtent(NewText
)[0] 
1516                     NewStrings
.append(NewText
) 
1518                     LineLength 
= dc
.GetTextExtent(text
[-1])[0] 
1520             NewStrings
.append(NewText
) 
1521         self
.Strings 
= NewStrings
 
1523     def ReWrap(self
, Width
): 
1527     def LayoutText(self
): 
1530         Calculates the positions of the words of text. 
1532         This isn't exact, as fonts don't scale exactly. 
1533         To help this, the position of each individual word 
1534         is stored separately, so that the general layout stays 
1535         the same in world coordinates, as the fonts scale. 
1538         self
.Strings 
= self
.String
.split("\n") 
1543         bitmap 
= wx
.EmptyBitmap(1, 1) 
1544         dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work. 
1546         DrawingSize 
= self
.LayoutFontSize 
# pts This effectively determines the resolution that the BB is computed to. 
1547         ScaleFactor 
= float(self
.Size
) / DrawingSize
 
1549         dc
.SetFont(self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underline
, self
.FaceName
) ) 
1551         TextHeight 
= dc
.GetTextExtent("X")[1] 
1552         SpaceWidth 
= dc
.GetTextExtent(" ")[0] 
1553         LineHeight 
= TextHeight 
* self
.LineSpacing
 
1555         LineWidths 
= zeros((len(self
.Strings
),), Float
) 
1560         for i
, s 
in enumerate(self
.Strings
): 
1562             LineWords 
= s
.split(" ") 
1563             LinePoints 
= zeros((len(LineWords
),2), Float
) 
1564             for j
, word 
in enumerate(LineWords
): 
1566                     LineWidths
[i
] += SpaceWidth
 
1568                 LinePoints
[j
] = (LineWidths
[i
], y
) 
1569                 w 
= dc
.GetTextExtent(word
)[0] 
1572             AllLinePoints
.append(LinePoints
) 
1573         TextWidth 
= maximum
.reduce(LineWidths
) 
1576         if self
.Width 
is None: 
1577             BoxWidth 
= TextWidth 
* ScaleFactor 
+ 2*self
.PadSize
 
1578         else: # use the defined Width 
1579             BoxWidth 
= self
.Width
 
1580         Points 
= zeros((0,2), Float
) 
1582         for i
, LinePoints 
in enumerate(AllLinePoints
): 
1583             ## Scale to World Coords. 
1584             LinePoints 
*= (ScaleFactor
, ScaleFactor
) 
1585             if self
.Alignment 
== 'left': 
1586                 LinePoints
[:,0] += self
.PadSize
 
1587             elif self
.Alignment 
== 'center': 
1588                 LinePoints
[:,0] += (BoxWidth 
- LineWidths
[i
]*ScaleFactor
)/2.0 
1589             elif self
.Alignment 
== 'right': 
1590                 LinePoints
[:,0] += (BoxWidth 
- LineWidths
[i
]*ScaleFactor
-self
.PadSize
) 
1591             Points 
= concatenate((Points
, LinePoints
)) 
1593         BoxHeight 
= -(Points
[-1,1] - (TextHeight 
* ScaleFactor
)) + 2*self
.PadSize
 
1594         (x
,y
) = self
.ShiftFun(self
.XY
[0], self
.XY
[1], BoxWidth
, BoxHeight
, world
=1) 
1595         Points 
+= (0, -self
.PadSize
) 
1596         self
.Points 
= Points
 
1597         self
.BoxWidth 
= BoxWidth
 
1598         self
.BoxHeight 
= BoxHeight
 
1599         self
.CalcBoundingBox() 
1601     def CalcBoundingBox(self
): 
1605         Calculates the Bounding Box 
1609         w
, h 
= self
.BoxWidth
, self
.BoxHeight
 
1610         x
, y 
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world
=1) 
1611         self
.BoundingBox 
= array(((x
, y
-h 
),(x 
+ w
, y
)),Float
) 
1613     def GetBoxRect(self
): 
1614         wh 
= (self
.BoxWidth
, self
.BoxHeight
) 
1615         xy 
= (self
.BoundingBox
[0,0], self
.BoundingBox
[1,1]) 
1619     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
1620         xy
, wh 
= self
.GetBoxRect() 
1622         Points 
= self
.Points 
+ xy 
 
1623         Points 
= WorldToPixel(Points
)  
1624         xy 
= WorldToPixel(xy
) 
1625         wh 
= ScaleWorldToPixel(wh
) * (1,-1) 
1627         # compute the font size: 
1628         Size 
= abs( ScaleWorldToPixel( (self
.Size
, self
.Size
) )[1] ) # only need a y coordinate length 
1629         ## Check to see if the font size is large enough to blow up the X font server 
1630         ## If so, limit it. Would it be better just to not draw it? 
1631         ## note that this limit is dependent on how much memory you have, etc. 
1632         Size 
= min(Size
, self
.MaxFontSize
) 
1634         font 
= self
.SetFont(Size
, self
.Family
, self
.Style
, self
.Weight
, self
.Underline
, self
.FaceName
) 
1636         dc
.SetTextForeground(self
.Color
) 
1637         dc
.SetBackgroundMode(wx
.TRANSPARENT
) 
1640         if (self
.LineStyle 
and self
.LineColor
) or self
.BackgroundColor
: 
1641             dc
.SetBrush(self
.Brush
) 
1643             dc
.DrawRectanglePointSize(xy 
, wh
) 
1646         dc
.DrawTextList(self
.Words
, Points
) 
1649         if HTdc 
and self
.HitAble
: 
1650             HTdc
.SetPen(self
.HitPen
) 
1651             HTdc
.SetBrush(self
.HitBrush
) 
1652             HTdc
.DrawRectanglePointSize(xy
, wh
) 
1654 class Bitmap(DrawObject
, TextObjectMixin
): 
1656     This class creates a bitmap object, placed at the coordinates, 
1657     x,y. the "Position" argument is a two charactor string, indicating 
1658     where in relation to the coordinates the bitmap should be oriented. 
1660     The first letter is: t, c, or b, for top, center and bottom The 
1661     second letter is: l, c, or r, for left, center and right The 
1662     position refers to the position relative to the text itself. It 
1663     defaults to "tl" (top left). 
1665     The size is fixed, and does not scale with the drawing. 
1669     def __init__(self
,Bitmap
,XY
, 
1671                  InForeground 
= False): 
1673         DrawObject
.__init
__(self
,InForeground
) 
1675         if type(Bitmap
) == wx
._gdi
.Bitmap
: 
1676             self
.Bitmap 
= Bitmap
 
1677         elif type(Bitmap
) == wx
._core
.Image
: 
1678             self
.Bitmap 
= wx
.BitmapFromImage(Bitmap
) 
1680         # Note the BB is just the point, as the size in World coordinates is not fixed 
1681         self
.BoundingBox 
= array((XY
,XY
),Float
) 
1685         (self
.Width
, self
.Height
) = self
.Bitmap
.GetWidth(), self
.Bitmap
.GetHeight() 
1686         self
.ShiftFun 
= self
.ShiftFunDict
[Position
] 
1688     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
1689         XY 
= WorldToPixel(self
.XY
) 
1690         XY 
= self
.ShiftFun(XY
[0], XY
[1], self
.Width
, self
.Height
) 
1691         dc
.DrawBitmapPoint(self
.Bitmap
, XY
, True) 
1692         if HTdc 
and self
.HitAble
: 
1693             HTdc
.SetPen(self
.HitPen
) 
1694             HTdc
.SetBrush(self
.HitBrush
) 
1695             HTdc
.DrawRectanglePointSize(XY
, (self
.Width
, self
.Height
) ) 
1697 class ScaledBitmap(DrawObject
, TextObjectMixin
): 
1700     This class creates a bitmap object, placed at the coordinates, XY, 
1701     of Height, H, in World coorsinates. The width is calculated from the 
1702     aspect ratio of the bitmap. 
1704     the "Position" argument is a two charactor string, indicating 
1705     where in relation to the coordinates the bitmap should be oriented. 
1707     The first letter is: t, c, or b, for top, center and bottom The 
1708     second letter is: l, c, or r, for left, center and right The 
1709     position refers to the position relative to the text itself. It 
1710     defaults to "tl" (top left). 
1712     The size scales with the drawing 
1721                  InForeground 
= False): 
1723         DrawObject
.__init
__(self
,InForeground
) 
1725         if type(Bitmap
) == wx
._gdi
.Bitmap
: 
1726             self
.Image 
= Bitmap
.ConvertToImage() 
1727         elif type(Bitmap
) == wx
._core
.Image
: 
1731         self
.Height 
= Height 
 
1732         (self
.bmpWidth
, self
.bmpHeight
) = self
.Image
.GetWidth(), self
.Image
.GetHeight() 
1733         self
.Width 
= self
.bmpWidth 
/ self
.bmpHeight 
* Height
 
1734         self
.ShiftFun 
= self
.ShiftFunDict
[Position
] 
1735         self
.CalcBoundingBox() 
1736         self
.ScaledBitmap 
= None 
1737         self
.ScaledHeight 
= None 
1739     def CalcBoundingBox(self
): 
1740         ## this isn't exact, as fonts don't scale exactly. 
1741         w
,h 
= self
.Width
, self
.Height
 
1742         x
, y 
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world 
= 1) 
1743         self
.BoundingBox 
= array(((x
, y
-h 
),(x 
+ w
, y
)),Float
) 
1745     def _Draw(self
, dc 
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None): 
1746         XY 
= WorldToPixel(self
.XY
) 
1747         H 
= ScaleWorldToPixel(self
.Height
)[0] 
1748         W 
= H 
* (self
.bmpWidth 
/ self
.bmpHeight
) 
1749         if (self
.ScaledBitmap 
is None) or (H 
<> self
.ScaledHeight
) : 
1750             self
.ScaledHeight 
= H
 
1751             Img 
= self
.Image
.Scale(W
, H
) 
1752             self
.ScaledBitmap 
= wx
.BitmapFromImage(Img
) 
1754         XY 
= self
.ShiftFun(XY
[0], XY
[1], W
, H
) 
1755         dc
.DrawBitmapPoint(self
.ScaledBitmap
, XY
, True) 
1756         if HTdc 
and self
.HitAble
: 
1757             HTdc
.SetPen(self
.HitPen
) 
1758             HTdc
.SetBrush(self
.HitBrush
) 
1759             HTdc
.DrawRectanglePointSize(XY
, (W
, H
) ) 
1762 #--------------------------------------------------------------------------- 
1763 class FloatCanvas(wx
.Panel
): 
1764     ## fixme: could this be a wx.Window? 
1768     This is a high level window for drawing maps and anything else in an 
1769     arbitrary coordinate system. 
1771     The goal is to provide a convenient way to draw stuff on the screen 
1772     without having to deal with handling OnPaint events, converting to pixel 
1773     coordinates, knowing about wxWindows brushes, pens, and colors, etc. It 
1774     also provides virtually unlimited zooming and scrolling 
1776     I am using it for two things: 
1777     1) general purpose drawing in floating point coordinates 
1778     2) displaying map data in Lat-long coordinates 
1780     If the projection is set to None, it will draw in general purpose 
1781     floating point coordinates. If the projection is set to 'FlatEarth', it 
1782     will draw a FlatEarth projection, centered on the part of the map that 
1783     you are viewing. You can also pass in your own projection function. 
1785     It is double buffered, so re-draws after the window is uncovered by something 
1786     else are very quick. 
1788     It relies on NumPy, which is needed for speed (maybe, I havn't profiled it) 
1790     Bugs and Limitations: 
1791         Lots: patches, fixes welcome 
1793     For Map drawing: It ignores the fact that the world is, in fact, a 
1794     sphere, so it will do strange things if you are looking at stuff near 
1795     the poles or the date line. so far I don't have a need to do that, so I 
1796     havn't bothered to add any checks for that yet. 
1799     I have set no zoom limits. What this means is that if you zoom in really  
1800     far, you can get integer overflows, and get wierd results. It 
1801     doesn't seem to actually cause any problems other than wierd output, at 
1802     least when I have run it. 
1805     I have done a couple of things to improve speed in this app. The one 
1806     thing I have done is used NumPy Arrays to store the coordinates of the 
1807     points of the objects. This allowed me to use array oriented functions 
1808     when doing transformations, and should provide some speed improvement 
1809     for objects with a lot of points (big polygons, polylines, pointsets). 
1811     The real slowdown comes when you have to draw a lot of objects, because 
1812     you have to call the wx.DC.DrawSomething call each time. This is plenty 
1813     fast for tens of objects, OK for hundreds of objects, but pretty darn 
1814     slow for thousands of objects. 
1816     The solution is to be able to pass some sort of object set to the DC 
1817     directly. I've used DC.DrawPointList(Points), and it helped a lot with 
1818     drawing lots of points. I havn't got a LineSet type object, so I havn't 
1819     used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList() 
1820     methods implimented, and then I'd also have a full set of Object sets 
1821     that could take advantage of them. I hope to get to it some day. 
1825     At this point, there are a full set of custom mouse events. They are 
1826     just like the rebulsr mouse events, but include an extra attribute: 
1827     Event.GetCoords(), that returns the (x,y) position in world 
1828     coordinates, as a length-2 NumPy vector of Floats. 
1830     Copyright: Christopher Barker 
1832     License: Same as the version of wxPython you are using it with 
1834     Please let me know if you're using this!!! 
1838     Chris.Barker@noaa.gov 
1842     def __init__(self
, parent
, id = -1, 
1843                  size 
= wx
.DefaultSize
, 
1844                  ProjectionFun 
= None, 
1845                  BackgroundColor 
= "WHITE", 
1848         wx
.Panel
.__init
__( self
, parent
, id, wx
.DefaultPosition
, size
) 
1850         global ScreenPPI 
## A global variable to hold the Pixels per inch that wxWindows thinks is in use. 
1852         ScreenPPI 
= dc
.GetPPI()[1] # Pixel height 
1855         self
.HitColorGenerator 
= None 
1856         self
.UseHitTest 
= None 
1858         self
.NumBetweenBlits 
= 500 
1860         self
.BackgroundBrush 
= wx
.Brush(BackgroundColor
,wx
.SOLID
) 
1864         wx
.EVT_PAINT(self
, self
.OnPaint
) 
1865         wx
.EVT_SIZE(self
, self
.OnSize
) 
1867         wx
.EVT_LEFT_DOWN(self
, self
.LeftDownEvent 
)  
1868         wx
.EVT_LEFT_UP(self
, self
.LeftUpEvent 
)  
1869         wx
.EVT_LEFT_DCLICK(self
, self
.LeftDoubleClickEvent 
)  
1870         wx
.EVT_MIDDLE_DOWN(self
, self
.MiddleDownEvent 
)  
1871         wx
.EVT_MIDDLE_UP(self
, self
.MiddleUpEvent 
)  
1872         wx
.EVT_MIDDLE_DCLICK(self
, self
.MiddleDoubleClickEvent 
)  
1873         wx
.EVT_RIGHT_DOWN(self
, self
.RightDownEvent
) 
1874         wx
.EVT_RIGHT_UP(self
, self
.RightUpEvent 
)  
1875         wx
.EVT_RIGHT_DCLICK(self
, self
.RightDoubleCLickEvent 
)  
1876         wx
.EVT_MOTION(self
, self
.MotionEvent 
)  
1877         wx
.EVT_MOUSEWHEEL(self
, self
.WheelEvent 
)  
1879         ## CHB: I'm leaving these out for now. 
1880         #wx.EVT_ENTER_WINDOW(self, self. )  
1881         #wx.EVT_LEAVE_WINDOW(self, self. )  
1883         ## create the Hit Test Dicts: 
1888         self
._ForeDrawList 
= [] 
1889         self
._ForegroundBuffer 
= None 
1890         self
.BoundingBox 
= None 
1891         self
.BoundingBoxDirty 
= False 
1892         self
.ViewPortCenter
= array( (0,0), Float
) 
1894         self
.SetProjectionFun(ProjectionFun
) 
1896         self
.MapProjectionVector 
= array( (1,1), Float
) # No Projection to start! 
1897         self
.TransformVector 
= array( (1,-1), Float
) # default Transformation 
1902         self
.StartRBBox 
= None 
1903         self
.PrevRBBox 
= None 
1904         self
.StartMove 
= None 
1905         self
.PrevMoveXY 
= None 
1906         self
.ObjectUnderMouse 
= None 
1908         # called just to make sure everything is initialized 
1909         # this is a bug on OS-X, maybe it's not required? 
1910         self
.SizeTimer 
= wx
.PyTimer(self
.OnSizeTimer
) # timer to give a delay when re-sizing so that bufferes aren't re-built too many times. 
1912         self
.InitializePanel() 
1913         self
.MakeNewBuffers() 
1917         self
.CreateCursors() 
1919     def CreateCursors(self
): 
1921         ## create all the Cursors, so they don't need to be created each time. 
1923         if "wxMac" in wx
.PlatformInfo
: # use 16X16 cursors for wxMac 
1924             self
.HandCursor 
= wx
.CursorFromImage(Resources
.getHand16Image()) 
1925             self
.GrabHandCursor 
= wx
.CursorFromImage(Resources
.getGrabHand16Image()) 
1927             img 
= Resources
.getMagPlus16Image() 
1928             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_X
, 6) 
1929             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_Y
, 6) 
1930             self
.MagPlusCursor 
= wx
.CursorFromImage(img
) 
1932             img 
= Resources
.getMagMinus16Image() 
1933             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_X
, 6) 
1934             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_Y
, 6) 
1935             self
.MagMinusCursor 
= wx
.CursorFromImage(img
) 
1936         else: # use 24X24 cursors for GTK and Windows 
1937             self
.HandCursor 
= wx
.CursorFromImage(Resources
.getHandImage()) 
1938             self
.GrabHandCursor 
= wx
.CursorFromImage(Resources
.getGrabHandImage()) 
1940             img 
= Resources
.getMagPlusImage() 
1941             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_X
, 9) 
1942             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_Y
, 9) 
1943             self
.MagPlusCursor 
= wx
.CursorFromImage(img
) 
1945             img 
= Resources
.getMagMinusImage() 
1946             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_X
, 9) 
1947             img
.SetOptionInt(wx
.IMAGE_OPTION_CUR_HOTSPOT_Y
, 9) 
1948             self
.MagMinusCursor 
= wx
.CursorFromImage(img
) 
1950     def SetProjectionFun(self
,ProjectionFun
): 
1951         if ProjectionFun 
== 'FlatEarth': 
1952             self
.ProjectionFun 
= self
.FlatEarthProjection 
 
1953         elif callable(ProjectionFun
): 
1954             self
.ProjectionFun 
= ProjectionFun 
 
1955         elif ProjectionFun 
is None: 
1956             self
.ProjectionFun 
= lambda x
=None: array( (1,1), Float
) 
1958             raise FloatCanvasError('Projectionfun must be either: "FlatEarth", None, or a callable object (function, for instance) that takes the ViewPortCenter and returns a MapProjectionVector') 
1960     def FlatEarthProjection(self
, CenterPoint
): 
1961         return array((cos(pi
*CenterPoint
[1]/180),1),Float
) 
1963     def SetMode(self
,Mode
): 
1964         if Mode 
in ["ZoomIn","ZoomOut","Move","Mouse", None]: 
1966                 self
.SetCursor(self
.HandCursor
) 
1967             elif Mode 
== "ZoomIn": 
1968                 self
.SetCursor(self
.MagPlusCursor
)  
1969             elif Mode 
== "ZoomOut": 
1970                 self
.SetCursor(self
.MagMinusCursor
)  
1972                 self
.SetCursor(wx
.NullCursor
) 
1977             raise FloatCanvasError('"%s" is Not a valid Mode'%Mode
) 
1979     def MakeHitDict(self
): 
1980         ##fixme: Should this just be None if nothing has been bound?  
1981         self
.HitDict 
= {EVT_FC_LEFT_DOWN: {}
, 
1983                         EVT_FC_LEFT_DCLICK
: {}, 
1984                         EVT_FC_MIDDLE_DOWN
: {}, 
1985                         EVT_FC_MIDDLE_UP
: {}, 
1986                         EVT_FC_MIDDLE_DCLICK
: {}, 
1987                         EVT_FC_RIGHT_DOWN
: {}, 
1988                         EVT_FC_RIGHT_UP
: {}, 
1989                         EVT_FC_RIGHT_DCLICK
: {}, 
1990                         EVT_FC_ENTER_OBJECT
: {}, 
1991                         EVT_FC_LEAVE_OBJECT
: {}, 
1994     def _RaiseMouseEvent(self
, Event
, EventType
): 
1996         This is called in various other places to raise a Mouse Event 
1998         #print "in Raise Mouse Event", Event 
1999         pt 
= self
.PixelToWorld( Event
.GetPosition() ) 
2000         evt 
= _MouseEvent(EventType
, Event
, self
.GetId(), pt
) 
2001         self
.GetEventHandler().ProcessEvent(evt
)        
2003     def HitTest(self
, event
, HitEvent
): 
2005             # check if there are any objects in the dict for this event 
2006             if self
.HitDict
[ HitEvent 
]: 
2007                 xy 
= event
.GetPosition() 
2008                 if self
._ForegroundHTdc
: 
2009                     hitcolor 
= self
._ForegroundHTdc
.GetPixelPoint( xy 
) 
2011                     hitcolor 
= self
._HTdc
.GetPixelPoint( xy 
) 
2012                 color 
= ( hitcolor
.Red(), hitcolor
.Green(), hitcolor
.Blue() ) 
2013                 if color 
in self
.HitDict
[ HitEvent 
]: 
2014                     Object 
= self
.HitDict
[ HitEvent 
][color
] 
2015                     ## Add the hit coords to the Object 
2016                     Object
.HitCoords 
= self
.PixelToWorld( xy 
) 
2017                     Object
.HitCoordsPixel 
= xy
 
2018                     Object
.CallBackFuncs
[HitEvent
](Object
) 
2022     def MouseOverTest(self
, event
): 
2023         ##fixme: Can this be cleaned up? 
2025             xy 
= event
.GetPosition() 
2026             if self
._ForegroundHTdc
: 
2027                 hitcolor 
= self
._ForegroundHTdc
.GetPixelPoint( xy 
) 
2029                 hitcolor 
= self
._HTdc
.GetPixelPoint( xy 
) 
2030             color 
= ( hitcolor
.Red(), hitcolor
.Green(), hitcolor
.Blue() ) 
2031             OldObject 
= self
.ObjectUnderMouse
 
2032             ObjectCallbackCalled 
= False 
2033             if color 
in self
.HitDict
[ EVT_FC_ENTER_OBJECT 
]: 
2034                 Object 
= self
.HitDict
[ EVT_FC_ENTER_OBJECT
][color
] 
2035                 if (OldObject 
is None): 
2037                         Object
.CallBackFuncs
[EVT_FC_ENTER_OBJECT
](Object
) 
2038                         ObjectCallbackCalled 
=  True 
2040                         pass # this means the enter event isn't bound for that object 
2041                 elif OldObject 
== Object
: # the mouse is still on the same object 
2043                     ## Is the mouse on a differnt object as it was... 
2044                 elif not (Object 
== OldObject
): 
2045                     # call the leave object callback 
2047                         OldObject
.CallBackFuncs
[EVT_FC_LEAVE_OBJECT
](OldObject
) 
2048                         ObjectCallbackCalled 
=  True 
2050                         pass # this means the leave event isn't bound for that object 
2052                         Object
.CallBackFuncs
[EVT_FC_ENTER_OBJECT
](Object
) 
2053                         ObjectCallbackCalled 
=  True 
2055                         pass # this means the enter event isn't bound for that object 
2056                     ## set the new object under mouse 
2057                 self
.ObjectUnderMouse 
= Object
 
2058             elif color 
in self
.HitDict
[ EVT_FC_LEAVE_OBJECT 
]: 
2059                 Object 
= self
.HitDict
[ EVT_FC_LEAVE_OBJECT
][color
] 
2060                 self
.ObjectUnderMouse 
= Object
 
2062                 # no objects under mouse bound to mouse-over events 
2063                 self
.ObjectUnderMouse 
= None 
2066                         OldObject
.CallBackFuncs
[EVT_FC_LEAVE_OBJECT
](OldObject
) 
2067                         ObjectCallbackCalled 
=  True 
2069                         pass # this means the leave event isn't bound for that object 
2070             return ObjectCallbackCalled
 
2073     ## fixme: There is a lot of repeated code here 
2074     ##        Is there a better way?             
2075     def LeftDoubleClickEvent(self
,event
): 
2076         if self
.GUIMode 
== "Mouse": 
2077             EventType 
= EVT_FC_LEFT_DCLICK
 
2078             if not self
.HitTest(event
, EventType
): 
2079                 self
._RaiseMouseEvent
(event
, EventType
) 
2081     def MiddleDownEvent(self
,event
): 
2082         if self
.GUIMode 
== "Mouse": 
2083             EventType 
= EVT_FC_MIDDLE_DOWN
 
2084             if not self
.HitTest(event
, EventType
): 
2085                 self
._RaiseMouseEvent
(event
, EventType
) 
2087     def MiddleUpEvent(self
,event
): 
2088         if self
.GUIMode 
== "Mouse": 
2089             EventType 
= EVT_FC_MIDDLE_UP
 
2090             if not self
.HitTest(event
, EventType
): 
2091                 self
._RaiseMouseEvent
(event
, EventType
) 
2093     def MiddleDoubleClickEvent(self
,event
): 
2094         if self
.GUIMode 
== "Mouse": 
2095             EventType 
= EVT_FC_MIDDLE_DCLICK
 
2096             if not self
.HitTest(event
, EventType
): 
2097                 self
._RaiseMouseEvent
(event
, EventType
) 
2099     def RightUpEvent(self
,event
): 
2100         if self
.GUIMode 
== "Mouse": 
2101             EventType 
= EVT_FC_RIGHT_UP
 
2102             if not self
.HitTest(event
, EventType
): 
2103                 self
._RaiseMouseEvent
(event
, EventType
) 
2105     def RightDoubleCLickEvent(self
,event
): 
2106         if self
.GUIMode 
== "Mouse": 
2107             EventType 
= EVT_FC_RIGHT_DCLICK
 
2108             if not self
.HitTest(event
, EventType
): 
2109                 self
._RaiseMouseEvent
(event
, EventType
) 
2111     def WheelEvent(self
,event
): 
2112         ##if self.GUIMode == "Mouse": 
2113         ## Why not always raise this? 
2114             self
._RaiseMouseEvent
(event
, EVT_FC_MOUSEWHEEL
) 
2117     def LeftDownEvent(self
,event
): 
2119             if self
.GUIMode 
== "ZoomIn": 
2120                 self
.StartRBBox 
= array( event
.GetPosition() ) 
2121                 self
.PrevRBBox 
= None 
2123             elif self
.GUIMode 
== "ZoomOut": 
2124                 Center 
= self
.PixelToWorld( event
.GetPosition() ) 
2125                 self
.Zoom(1/1.5,Center
) 
2126             elif self
.GUIMode 
== "Move": 
2127                 self
.SetCursor(self
.GrabHandCursor
) 
2128                 self
.StartMove 
= array( event
.GetPosition() ) 
2129                 self
.PrevMoveXY 
= (0,0) 
2130             elif self
.GUIMode 
== "Mouse": 
2132                 if not self
.HitTest(event
, EVT_FC_LEFT_DOWN
): 
2133                    self
._RaiseMouseEvent
(event
,EVT_FC_LEFT_DOWN
) 
2137     def LeftUpEvent(self
,event
): 
2138         if self
.HasCapture(): 
2141             if self
.GUIMode 
== "ZoomIn": 
2142                 if event
.LeftUp() and not self
.StartRBBox 
is None: 
2143                     self
.PrevRBBox 
= None 
2144                     EndRBBox 
= event
.GetPosition() 
2145                     StartRBBox 
= self
.StartRBBox
 
2146                     # if mouse has moved less that ten pixels, don't use the box. 
2147                     if ( abs(StartRBBox
[0] - EndRBBox
[0]) > 10 
2148                          and abs(StartRBBox
[1] - EndRBBox
[1]) > 10 ): 
2149                         EndRBBox 
= self
.PixelToWorld(EndRBBox
) 
2150                         StartRBBox 
= self
.PixelToWorld(StartRBBox
) 
2151                         BB 
= array(((min(EndRBBox
[0],StartRBBox
[0]), 
2152                                      min(EndRBBox
[1],StartRBBox
[1])), 
2153                                     (max(EndRBBox
[0],StartRBBox
[0]), 
2154                                      max(EndRBBox
[1],StartRBBox
[1]))),Float
) 
2157                         Center 
= self
.PixelToWorld(StartRBBox
) 
2158                         self
.Zoom(1.5,Center
) 
2159                     self
.StartRBBox 
= None 
2160             elif self
.GUIMode 
== "Move": 
2161                 self
.SetCursor(self
.HandCursor
) 
2162                 if self
.StartMove 
is not None: 
2163                     StartMove 
= self
.StartMove
 
2164                     EndMove 
= array((event
.GetX(),event
.GetY())) 
2165                     if sum((StartMove
-EndMove
)**2) > 16: 
2166                         self
.MoveImage(StartMove
-EndMove
,'Pixel') 
2167                     self
.StartMove 
= None 
2168             elif self
.GUIMode 
== "Mouse": 
2169                 EventType 
= EVT_FC_LEFT_UP
 
2170                 if not self
.HitTest(event
, EventType
): 
2171                    self
._RaiseMouseEvent
(event
, EventType
) 
2175     def MotionEvent(self
,event
): 
2177             if self
.GUIMode 
== "ZoomIn": 
2178                 if event
.Dragging() and event
.LeftIsDown() and not (self
.StartRBBox 
is None): 
2179                     xy0 
= self
.StartRBBox
 
2180                     xy1 
= array( event
.GetPosition() ) 
2182                     wh
[0] = max(wh
[0], int(wh
[1]*self
.AspectRatio
)) 
2183                     wh
[1] = int(wh
[0] / self
.AspectRatio
) 
2184                     xy_c 
= (xy0 
+ xy1
) / 2 
2185                     dc 
= wx
.ClientDC(self
) 
2187                     dc
.SetPen(wx
.Pen('WHITE', 2, wx
.SHORT_DASH
)) 
2188                     dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
2189                     dc
.SetLogicalFunction(wx
.XOR
) 
2191                         dc
.DrawRectanglePointSize(*self
.PrevRBBox
) 
2192                     self
.PrevRBBox 
= ( xy_c 
- wh
/2, wh 
) 
2193                     dc
.DrawRectanglePointSize( *self
.PrevRBBox 
) 
2195             elif self
.GUIMode 
== "Move": 
2196                 if event
.Dragging() and event
.LeftIsDown() and not self
.StartMove 
is None: 
2197                     xy1 
= array( event
.GetPosition() ) 
2199                     xy_tl 
= xy1 
- self
.StartMove
 
2200                     dc 
= wx
.ClientDC(self
) 
2202                     x1
,y1 
= self
.PrevMoveXY
 
2204                     w
,h 
= self
.PanelSize
 
2205                     ##fixme: This sure could be cleaner! 
2206                     if x2 
> x1 
and y2 
> y1
: 
2213                     elif x2 
> x1 
and y2 
<= y1
: 
2222                     elif x2 
<= x1 
and y2 
> y1
: 
2231                     elif x2 
<= x1 
and y2 
<= y1
: 
2241                     dc
.SetPen(wx
.TRANSPARENT_PEN
) 
2242                     dc
.SetBrush(self
.BackgroundBrush
) 
2243                     dc
.DrawRectangle(xa
, ya
, wa
, ha
) 
2244                     dc
.DrawRectangle(xb
, yb
, wb
, hb
) 
2245                     self
.PrevMoveXY 
= xy_tl
 
2246                     if self
._ForeDrawList
: 
2247                     ##if self._ForegroundBuffer: 
2248                         dc
.DrawBitmapPoint(self
._ForegroundBuffer
,xy_tl
) 
2250                         dc
.DrawBitmapPoint(self
._Buffer
,xy_tl
) 
2252             elif self
.GUIMode 
== "Mouse": 
2253                 ## Only do something if there are mouse over events bound 
2254                 if self
.HitDict 
and (self
.HitDict
[ EVT_FC_ENTER_OBJECT 
] or self
.HitDict
[ EVT_FC_LEAVE_OBJECT 
] ): 
2255                     if not self
.MouseOverTest(event
): 
2256                         self
._RaiseMouseEvent
(event
,EVT_FC_MOTION
) 
2259             self
._RaiseMouseEvent
(event
,EVT_FC_MOTION
) 
2263     def RightDownEvent(self
,event
): 
2265             if self
.GUIMode 
== "ZoomIn": 
2266                 Center 
= self
.PixelToWorld((event
.GetX(),event
.GetY())) 
2267                 self
.Zoom(1/1.5,Center
) 
2268             elif self
.GUIMode 
== "ZoomOut": 
2269                 Center 
= self
.PixelToWorld((event
.GetX(),event
.GetY())) 
2270                 self
.Zoom(1.5,Center
) 
2271             elif self
.GUIMode 
== "Mouse": 
2272                 EventType 
= EVT_FC_RIGHT_DOWN
 
2273                 if not self
.HitTest(event
, EventType
): 
2274                    self
._RaiseMouseEvent
(event
, EventType
) 
2278     def MakeNewBuffers(self
): 
2279         self
._BackgroundDirty 
= True 
2280         # Make new offscreen bitmap: 
2281         self
._Buffer 
= wx
.EmptyBitmap(*self
.PanelSize
) 
2283         #dc.SelectObject(self._Buffer) 
2285         if self
._ForeDrawList
: 
2286             self
._ForegroundBuffer 
= wx
.EmptyBitmap(*self
.PanelSize
) 
2288             self
._ForegroundBuffer 
= None 
2293             self
._ForegroundHTdc 
= None 
2295     def MakeNewHTdc(self
): 
2296         ## Note: While it's considered a "bad idea" to keep a 
2297         ## MemoryDC around I'm doing it here because a wx.Bitmap 
2298         ## doesn't have a GetPixel method so a DC is needed to do 
2299         ## the hit-test. It didn't seem like a good idea to re-create 
2300         ## a wx.MemoryDC on every single mouse event, so I keep it 
2302         self
._HTdc 
= wx
.MemoryDC() 
2303         self
._HTBitmap 
= wx
.EmptyBitmap(*self
.PanelSize
)  
2304         self
._HTdc
.SelectObject( self
._HTBitmap 
) 
2305         self
._HTdc
.SetBackground(wx
.BLACK_BRUSH
) 
2306         if self
._ForeDrawList
: 
2307             self
._ForegroundHTdc 
= wx
.MemoryDC() 
2308             self
._ForegroundHTBitmap 
= wx
.EmptyBitmap(*self
.PanelSize
)  
2309             self
._ForegroundHTdc
.SelectObject( self
._ForegroundHTBitmap 
) 
2310             self
._ForegroundHTdc
.SetBackground(wx
.BLACK_BRUSH
) 
2312            self
._ForegroundHTdc 
= None  
2314     def OnSize(self
, event
=None): 
2315         self
.InitializePanel() 
2316         self
.SizeTimer
.Start(50, oneShot
=True) 
2318     def OnSizeTimer(self
, event
=None): 
2319         self
.MakeNewBuffers() 
2322     def InitializePanel(self
): 
2323         self
.PanelSize 
= self
.GetClientSizeTuple() 
2324         if self
.PanelSize 
== (0,0): 
2325             ## OS-X sometimes gives a Size event when the panel is size (0,0) 
2326             self
.PanelSize 
= (2,2) 
2327         self
.PanelSize  
= array(self
.PanelSize
,  Int32
) 
2328         self
.HalfPanelSize 
= self
.PanelSize 
/ 2 # lrk: added for speed in WorldToPixel 
2329         if self
.PanelSize
[0] == 0 or self
.PanelSize
[1] == 0: 
2330             self
.AspectRatio 
= 1.0 
2332             self
.AspectRatio 
= float(self
.PanelSize
[0]) / self
.PanelSize
[1] 
2334     def OnPaint(self
, event
): 
2335         dc 
= wx
.PaintDC(self
) 
2336         if self
._ForegroundBuffer
: 
2337             dc
.DrawBitmap(self
._ForegroundBuffer
,0,0) 
2339             dc
.DrawBitmap(self
._Buffer
,0,0) 
2341     def Draw(self
, Force
=False): 
2343         There is a main buffer set up to double buffer the screen, so 
2344         you can get quick re-draws when the window gets uncovered. 
2346         If there are any objects in self._ForeDrawList, then the 
2347         background gets drawn to a new buffer, and the foreground 
2348         objects get drawn on top of it. The final result if blitted to 
2349         the screen, and stored for future Paint events.  This is done so 
2350         that you can have a complicated background, but have something 
2351         changing on the foreground, without having to wait for the 
2352         background to get re-drawn. This can be used to support simple 
2353         animation, for instance. 
2356         if sometrue(self
.PanelSize 
<= 2 ): # it's possible for this to get called before being properly initialized. 
2358         if self
.Debug
: start 
= clock() 
2359         ScreenDC 
=  wx
.ClientDC(self
) 
2360         ViewPortWorld 
= ( self
.PixelToWorld((0,0)), 
2361                           self
.PixelToWorld(self
.PanelSize
) ) 
2362         ViewPortBB 
= array( ( minimum
.reduce(ViewPortWorld
), 
2363                               maximum
.reduce(ViewPortWorld
) ) ) 
2365         dc
.SelectObject(self
._Buffer
) 
2366         if self
._BackgroundDirty 
or Force
: 
2367             #print "Background is Dirty" 
2368             dc
.SetBackground(self
.BackgroundBrush
) 
2372             self
._DrawObjects
(dc
, self
._DrawList
, ScreenDC
, ViewPortBB
, self
._HTdc
) 
2373             self
._BackgroundDirty 
= False 
2375         if self
._ForeDrawList
: 
2376             ## If an object was just added to the Foreground, there might not yet be a buffer 
2377             if self
._ForegroundBuffer 
is None: 
2378                 self
._ForegroundBuffer 
= wx
.EmptyBitmap(self
.PanelSize
[0], 
2381             dc 
= wx
.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here 
2382             dc
.SelectObject(self
._ForegroundBuffer
) 
2383             dc
.DrawBitmap(self
._Buffer
,0,0) 
2384             if self
._ForegroundHTdc 
is None: 
2385                 self
._ForegroundHTdc 
= wx
.MemoryDC() 
2386                 self
._ForegroundHTdc
.SelectObject( wx
.EmptyBitmap( 
2388                                                    self
.PanelSize
[1]) ) 
2390                 ## blit the background HT buffer to the foreground HT buffer 
2391                 self
._ForegroundHTdc
.Blit(0, 0, 
2392                                           self
.PanelSize
[0], self
.PanelSize
[1], 
2394             self
._DrawObjects
(dc
, 
2398                               self
._ForegroundHTdc
) 
2399         ScreenDC
.Blit(0, 0, self
.PanelSize
[0],self
.PanelSize
[1], dc
, 0, 0) 
2400         # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn 
2401         # This seeems out of place, but it works. 
2403             ScreenDC
.SetPen(wx
.Pen('WHITE', 2,wx
.SHORT_DASH
)) 
2404             ScreenDC
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
2405             ScreenDC
.SetLogicalFunction(wx
.XOR
) 
2406             ScreenDC
.DrawRectanglePointSize(*self
.PrevRBBox
) 
2407         if self
.Debug
: print "Drawing took %f seconds of CPU time"%(clock()-start
) 
2409         ## Clear the font cache 
2410         ## IF you don't do this, the X font server starts to take up Massive amounts of memory 
2411         ## This is mostly a problem with very large fonts, that you get with scaled text when zoomed in. 
2412         DrawObject
.FontList 
= {} 
2414     def _ShouldRedraw(DrawList
, ViewPortBB
): # lrk: adapted code from BBCheck 
2415         # lrk: Returns the objects that should be redrawn 
2419         for Object 
in DrawList
: 
2420             BB1 
= Object
.BoundingBox
 
2421             if (BB1
[1,0] > BB2
[0,0] and BB1
[0,0] < BB2
[1,0] and 
2422                  BB1
[1,1] > BB2
[0,1] and BB1
[0,1] < BB2
[1,1]): 
2423                 redrawlist
.append(Object
) 
2425     _ShouldRedraw 
= staticmethod(_ShouldRedraw
) 
2428 ##    def BBCheck(self, BB1, BB2): 
2431 ##        BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise 
2434 ##        if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and 
2435 ##             (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ): 
2440     def MoveImage(self
,shift
,CoordType
): 
2442         move the image in the window. 
2444         shift is an (x,y) tuple, specifying the amount to shift in each direction 
2446         It can be in any of three coordinates: Panel, Pixel, World, 
2447         specified by the CoordType parameter 
2449         Panel coordinates means you want to shift the image by some 
2450         fraction of the size of the displaed image 
2452         Pixel coordinates means you want to shift the image by some number of pixels 
2454         World coordinates mean you want to shift the image by an amount 
2455         in Floating point world coordinates 
2459         shift 
= asarray(shift
,Float
) 
2460         #print "shifting by:", shift 
2461         if CoordType 
== 'Panel':# convert from panel coordinates 
2462             shift 
= shift 
* array((-1,1),Float
) *self
.PanelSize
/self
.TransformVector
 
2463         elif CoordType 
== 'Pixel': # convert from pixel coordinates 
2464             shift 
= shift
/self
.TransformVector
 
2465         elif CoordType 
== 'World': # No conversion 
2468             raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"') 
2470         self
.ViewPortCenter 
= self
.ViewPortCenter 
+ shift 
 
2471         self
.MapProjectionVector 
= self
.ProjectionFun(self
.ViewPortCenter
) 
2472         self
.TransformVector 
= array((self
.Scale
,-self
.Scale
),Float
) * self
.MapProjectionVector
 
2473         self
._BackgroundDirty 
= True 
2476     def Zoom(self
,factor
,center 
= None): 
2479         Zoom(factor, center) changes the amount of zoom of the image by factor. 
2480         If factor is greater than one, the image gets larger. 
2481         If factor is less than one, the image gets smaller. 
2483         Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. 
2484         If center is not given, the center will stay the same. 
2487         self
.Scale 
= self
.Scale
*factor
 
2488         if not center 
is None: 
2489             self
.ViewPortCenter 
= array(center
,Float
) 
2490         self
.MapProjectionVector 
= self
.ProjectionFun(self
.ViewPortCenter
) 
2491         self
.TransformVector 
= array((self
.Scale
,-self
.Scale
),Float
) * self
.MapProjectionVector
 
2492         self
._BackgroundDirty 
= True 
2495     def ZoomToBB(self
, NewBB 
= None, DrawFlag 
= True): 
2499         Zooms the image to the bounding box given, or to the bounding 
2500         box of all the objects on the canvas, if none is given. 
2504         if not  NewBB 
is None: 
2507             if self
.BoundingBoxDirty
: 
2508                 self
._ResetBoundingBox
() 
2509             BoundingBox 
= self
.BoundingBox
 
2510         if not BoundingBox 
is None: 
2511             self
.ViewPortCenter 
= array(((BoundingBox
[0,0]+BoundingBox
[1,0])/2, 
2512                                          (BoundingBox
[0,1]+BoundingBox
[1,1])/2 ),Float
) 
2513             self
.MapProjectionVector 
= self
.ProjectionFun(self
.ViewPortCenter
) 
2514             # Compute the new Scale 
2515             BoundingBox 
= BoundingBox 
* self
.MapProjectionVector
 
2517                 self
.Scale 
= min(abs(self
.PanelSize
[0] / (BoundingBox
[1,0]-BoundingBox
[0,0])), 
2518                                  abs(self
.PanelSize
[1] / (BoundingBox
[1,1]-BoundingBox
[0,1])) )*0.95 
2519             except ZeroDivisionError: # this will happen if the BB has zero width or height 
2521                     self
.Scale 
= (self
.PanelSize
[0]  / (BoundingBox
[1,0]-BoundingBox
[0,0]))*0.95 
2522                 except ZeroDivisionError: 
2524                         self
.Scale 
= (self
.PanelSize
[1]  / (BoundingBox
[1,1]-BoundingBox
[0,1]))*0.95 
2525                     except ZeroDivisionError: #zero size! (must be a single point) 
2528             self
.TransformVector 
= array((self
.Scale
,-self
.Scale
),Float
)* self
.MapProjectionVector
 
2530                 self
._BackgroundDirty 
= True 
2533             # Reset the shifting and scaling to defaults when there is no BB 
2534             self
.ViewPortCenter
= array( (0,0), Float
) 
2535             self
.MapProjectionVector 
= array( (1,1), Float
) # No Projection to start! 
2536             self
.TransformVector 
= array( (1,-1), Float
) # default Transformation 
2539     def RemoveObjects(self
, Objects
): 
2540         for Object 
in Objects
: 
2541             self
.RemoveObject(Object
, ResetBB 
= False) 
2542         self
.BoundingBoxDirty 
= True 
2544     def RemoveObject(self
, Object
, ResetBB 
= True): 
2545         ##fixme: Using the list.remove method is kind of slow 
2546         if Object
.InForeground
: 
2547             self
._ForeDrawList
.remove(Object
) 
2548             if not self
._ForeDrawList
: 
2549                 self
._ForegroundBuffer 
= None 
2550                 self
._ForegroundHTdc 
= None 
2552             self
._DrawList
.remove(Object
) 
2553             self
._BackgroundDirty 
= True 
2555             self
.BoundingBoxDirty 
= True 
2557     def ClearAll(self
, ResetBB 
= True): 
2559         self
._ForeDrawList 
= [] 
2560         self
._BackgroundDirty 
= True 
2561         self
.HitColorGenerator 
= None 
2562         self
.UseHitTest 
= False 
2564             self
._ResetBoundingBox
() 
2565         self
.MakeNewBuffers() 
2569 ##    def _AddBoundingBox(self,NewBB): 
2570 ##        if self.BoundingBox is None: 
2571 ##            self.BoundingBox = NewBB 
2572 ##            self.ZoomToBB(NewBB,DrawFlag = False) 
2574 ##            self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]), 
2575 ##                                         min(self.BoundingBox[0,1],NewBB[0,1])), 
2576 ##                                        (max(self.BoundingBox[1,0],NewBB[1,0]), 
2577 ##                                         max(self.BoundingBox[1,1],NewBB[1,1]))), 
2580     def _getboundingbox(bboxarray
): # lrk: added this 
2582         upperleft 
= minimum
.reduce(bboxarray
[:,0]) 
2583         lowerright 
= maximum
.reduce(bboxarray
[:,1]) 
2584         return array((upperleft
, lowerright
), Float
) 
2586     _getboundingbox 
= staticmethod(_getboundingbox
) 
2588     def _ResetBoundingBox(self
): 
2589         if self
._DrawList 
or self
._ForeDrawList
: 
2590             bboxarray 
= zeros((len(self
._DrawList
)+len(self
._ForeDrawList
), 2, 2),Float
)  
2591             i 
= -1 # just in case _DrawList is empty 
2592             for (i
, BB
) in enumerate(self
._DrawList
): 
2593                 bboxarray
[i
] = BB
.BoundingBox
 
2594             for (j
, BB
) in enumerate(self
._ForeDrawList
): 
2595                 bboxarray
[i
+j
+1] = BB
.BoundingBox
 
2596             self
.BoundingBox 
= self
._getboundingbox
(bboxarray
) 
2598             self
.BoundingBox 
= None 
2599             self
.ViewPortCenter
= array( (0,0), Float
) 
2600             self
.TransformVector 
= array( (1,-1), Float
) 
2601             self
.MapProjectionVector 
= array( (1,1), Float
)                      
2603         self
.BoundingBoxDirty 
= False 
2605     def PixelToWorld(self
,Points
): 
2607         Converts coordinates from Pixel coordinates to world coordinates. 
2609         Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates. 
2612         return  (((asarray(Points
,Float
) - (self
.PanelSize
/2))/self
.TransformVector
) + self
.ViewPortCenter
) 
2614     def WorldToPixel(self
,Coordinates
): 
2616         This function will get passed to the drawing functions of the objects, 
2617         to transform from world to pixel coordinates. 
2618         Coordinates should be a NX2 array of (x,y) coordinates, or 
2619         a 2-tuple, or sequence of 2-tuples. 
2621         #Note: this can be called by users code for various reasons, so asarray is needed. 
2622         return  (((asarray(Coordinates
,Float
) - 
2623                    self
.ViewPortCenter
)*self
.TransformVector
)+ 
2624                  (self
.HalfPanelSize
)).astype('i') 
2626     def ScaleWorldToPixel(self
,Lengths
): 
2628         This function will get passed to the drawing functions of the objects, 
2629         to Change a length from world to pixel coordinates. 
2631         Lengths should be a NX2 array of (x,y) coordinates, or 
2632         a 2-tuple, or sequence of 2-tuples. 
2634         return  ( (asarray(Lengths
,Float
)*self
.TransformVector
) ).astype('i') 
2636     def ScalePixelToWorld(self
,Lengths
): 
2638         This function computes a pair of x.y lengths, 
2639         to change then from pixel to world coordinates. 
2641         Lengths should be a NX2 array of (x,y) coordinates, or 
2642         a 2-tuple, or sequence of 2-tuples. 
2645         return  (asarray(Lengths
,Float
) / self
.TransformVector
) 
2647     def AddObject(self
,obj
): 
2648         # put in a reference to the Canvas, so remove and other stuff can work 
2650         if  obj
.InForeground
: 
2651             self
._ForeDrawList
.append(obj
) 
2652             self
.UseForeground 
= True 
2654             self
._DrawList
.append(obj
) 
2655             self
._BackgroundDirty 
= True 
2656         self
.BoundingBoxDirty 
= True 
2659     def _DrawObjects(self
, dc
, DrawList
, ScreenDC
, ViewPortBB
, HTdc 
= None): 
2661         This is a convenience function; 
2662         This function takes the list of objects and draws them to specified 
2665         dc
.SetBackground(self
.BackgroundBrush
) 
2668         PanelSize0
, PanelSize1 
= self
.PanelSize 
# for speed 
2669         WorldToPixel 
= self
.WorldToPixel 
# for speed 
2670         ScaleWorldToPixel 
= self
.ScaleWorldToPixel 
# for speed 
2671         Blit 
= ScreenDC
.Blit 
# for speed 
2672         NumBetweenBlits 
= self
.NumBetweenBlits 
# for speed 
2673         for i
, Object 
in enumerate(self
._ShouldRedraw
(DrawList
, ViewPortBB
)): 
2675                 Object
._Draw
(dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
) 
2676                 if (i
+1) % NumBetweenBlits 
== 0: 
2677                     Blit(0, 0, PanelSize0
, PanelSize1
, dc
, 0, 0) 
2680     def SaveAsImage(self
, filename
, ImageType
=wx
.BITMAP_TYPE_PNG
): 
2683         Saves the current image as an image file. The default is in the 
2684         PNG format. Other formats can be spcified using the wx flags: 
2689         etc. (see the wx docs for the complete list) 
2693         self
._Buffer
.SaveFile(filename
, ImageType
) 
2696 def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__ 
2697     classnames 
= ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon", 
2698                   "Line", "Text", "PointSet","Point", "Arrow","ScaledTextBox", 
2699                   "SquarePoint","Bitmap", "ScaledBitmap"] 
2700     for classname 
in classnames
: 
2701         klass 
= globals()[classname
] 
2702         def getaddshapemethod(klass
=klass
): 
2703             def addshape(self
, *args
, **kwargs
): 
2704                 Object 
= klass(*args
, **kwargs
) 
2705                 self
.AddObject(Object
) 
2708         addshapemethod 
= getaddshapemethod() 
2709         methodname 
= "Add" + classname
 
2710         setattr(FloatCanvas
, methodname
, addshapemethod
) 
2711         docstring 
= "Creates %s and adds its reference to the canvas.\n" % classname
 
2712         docstring 
+= "Argument protocol same as %s class" % classname
 
2714             docstring 
+= ", whose docstring is:\n%s" % klass
.__doc
__ 
2715         FloatCanvas
.__dict
__[methodname
].__doc
__ = docstring
 
2717 _makeFloatCanvasAddMethods()