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()