]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/floatcanvas/FloatCanvas.py
2 from __future__
import division
7 raise ImportError("I could not import numpy")
12 from Utilities
import BBox
15 ## A global variable to hold the Pixels per inch that wxWindows thinks is in use
16 ## This is used for scaling fonts.
17 ## This can't be computed on module __init__, because a wx.App might not have initialized yet.
22 class FloatCanvasError(Exception):
25 ## Create all the mouse events
26 EVT_FC_ENTER_WINDOW
= wx
.NewEventType()
27 EVT_FC_LEAVE_WINDOW
= wx
.NewEventType()
28 EVT_FC_LEFT_DOWN
= wx
.NewEventType()
29 EVT_FC_LEFT_UP
= wx
.NewEventType()
30 EVT_FC_LEFT_DCLICK
= wx
.NewEventType()
31 EVT_FC_MIDDLE_DOWN
= wx
.NewEventType()
32 EVT_FC_MIDDLE_UP
= wx
.NewEventType()
33 EVT_FC_MIDDLE_DCLICK
= wx
.NewEventType()
34 EVT_FC_RIGHT_DOWN
= wx
.NewEventType()
35 EVT_FC_RIGHT_UP
= wx
.NewEventType()
36 EVT_FC_RIGHT_DCLICK
= wx
.NewEventType()
37 EVT_FC_MOTION
= wx
.NewEventType()
38 EVT_FC_MOUSEWHEEL
= wx
.NewEventType()
39 ## these two are for the hit-test stuff, I never make them real Events
40 ## fixme: could I use the PyEventBinder for the Object events too?
41 EVT_FC_ENTER_OBJECT
= wx
.NewEventType()
42 EVT_FC_LEAVE_OBJECT
= wx
.NewEventType()
44 ##Create all mouse event binding objects
45 EVT_LEFT_DOWN
= wx
.PyEventBinder(EVT_FC_LEFT_DOWN
)
46 EVT_LEFT_UP
= wx
.PyEventBinder(EVT_FC_LEFT_UP
)
47 EVT_LEFT_DCLICK
= wx
.PyEventBinder(EVT_FC_LEFT_DCLICK
)
48 EVT_MIDDLE_DOWN
= wx
.PyEventBinder(EVT_FC_MIDDLE_DOWN
)
49 EVT_MIDDLE_UP
= wx
.PyEventBinder(EVT_FC_MIDDLE_UP
)
50 EVT_MIDDLE_DCLICK
= wx
.PyEventBinder(EVT_FC_MIDDLE_DCLICK
)
51 EVT_RIGHT_DOWN
= wx
.PyEventBinder(EVT_FC_RIGHT_DOWN
)
52 EVT_RIGHT_UP
= wx
.PyEventBinder(EVT_FC_RIGHT_UP
)
53 EVT_RIGHT_DCLICK
= wx
.PyEventBinder(EVT_FC_RIGHT_DCLICK
)
54 EVT_MOTION
= wx
.PyEventBinder(EVT_FC_MOTION
)
55 EVT_ENTER_WINDOW
= wx
.PyEventBinder(EVT_FC_ENTER_WINDOW
)
56 EVT_LEAVE_WINDOW
= wx
.PyEventBinder(EVT_FC_LEAVE_WINDOW
)
57 EVT_MOUSEWHEEL
= wx
.PyEventBinder(EVT_FC_MOUSEWHEEL
)
59 class _MouseEvent(wx
.PyCommandEvent
):
63 This event class takes a regular wxWindows mouse event as a parameter,
64 and wraps it so that there is access to all the original methods. This
65 is similar to subclassing, but you can't subclass a wxWindows event
67 The goal is to be able to it just like a regular mouse event.
71 GetCoords() , which returns and (x,y) tuple in world coordinates.
73 Another difference is that it is a CommandEvent, which propagates up
74 the window hierarchy until it is handled.
78 def __init__(self
, EventType
, NativeEvent
, WinID
, Coords
= None):
79 wx
.PyCommandEvent
.__init
__(self
)
81 self
.SetEventType( EventType
)
82 self
._NativeEvent
= NativeEvent
88 def __getattr__(self
, name
):
89 return getattr(self
._NativeEvent
, name
)
91 def _cycleidxs(indexcount
, maxvalue
, step
):
94 Utility function used by _colorGenerator
101 for idx
in xrange(0, maxvalue
, step
):
102 for tail
in _cycleidxs(indexcount
- 1, maxvalue
, step
):
105 def _colorGenerator():
109 Generates a series of unique colors used to do hit-tests with the Hit
113 depth
= wx
.GetDisplayDepth()
114 ## ##there have been problems with 16 bbp displays, to I'm disabling this for now.
116 ## print "Warning: There have been problems with hit-testing on 16bbp displays"
121 msg
= ["ColorGenerator does not work with depth = %s" % depth
]
122 msg
.append("It is required for hit testing -- binding events to mouse")
123 msg
.append("actions on objects on the Canvas.")
124 msg
.append("Please set your display to 24bit")
125 msg
.append("Alternatively, the code could be adapted to 16 bit if that's required")
126 raise FloatCanvasError(msg
)
127 return _cycleidxs(indexcount
=3, maxvalue
=256, step
=step
)
131 This is the base class for all the objects that can be drawn.
133 One must subclass from this (and an assortment of Mixins) to create
136 \note This class contain a series of static dictionaries:
143 Is this still necessary?
147 def __init__(self
, InForeground
= False, IsVisible
= True):
148 """! \param InForeground (bool)
149 \param IsVisible (Bool)
151 self
.InForeground
= InForeground
156 self
.CallBackFuncs
= {}
158 ## these are the defaults
162 self
.MinHitLineWidth
= 3
163 self
.HitLineWidth
= 3 ## this gets re-set by the subclasses if necessary
168 self
.FillStyle
= "Solid"
170 self
.Visible
= IsVisible
172 # I pre-define all these as class variables to provide an easier
173 # interface, and perhaps speed things up by caching all the Pens
174 # and Brushes, although that may not help, as I think wx now
175 # does that on it's own. Send me a note if you know!
178 ( None,"Transparent") : wx
.TRANSPARENT_BRUSH
,
179 ("Blue","Solid") : wx
.BLUE_BRUSH
,
180 ("Green","Solid") : wx
.GREEN_BRUSH
,
181 ("White","Solid") : wx
.WHITE_BRUSH
,
182 ("Black","Solid") : wx
.BLACK_BRUSH
,
183 ("Grey","Solid") : wx
.GREY_BRUSH
,
184 ("MediumGrey","Solid") : wx
.MEDIUM_GREY_BRUSH
,
185 ("LightGrey","Solid") : wx
.LIGHT_GREY_BRUSH
,
186 ("Cyan","Solid") : wx
.CYAN_BRUSH
,
187 ("Red","Solid") : wx
.RED_BRUSH
190 (None,"Transparent",1) : wx
.TRANSPARENT_PEN
,
191 ("Green","Solid",1) : wx
.GREEN_PEN
,
192 ("White","Solid",1) : wx
.WHITE_PEN
,
193 ("Black","Solid",1) : wx
.BLACK_PEN
,
194 ("Grey","Solid",1) : wx
.GREY_PEN
,
195 ("MediumGrey","Solid",1) : wx
.MEDIUM_GREY_PEN
,
196 ("LightGrey","Solid",1) : wx
.LIGHT_GREY_PEN
,
197 ("Cyan","Solid",1) : wx
.CYAN_PEN
,
198 ("Red","Solid",1) : wx
.RED_PEN
202 "Transparent" : wx
.TRANSPARENT
,
204 "BiDiagonalHatch": wx
.BDIAGONAL_HATCH
,
205 "CrossDiagHatch" : wx
.CROSSDIAG_HATCH
,
206 "FDiagonal_Hatch": wx
.FDIAGONAL_HATCH
,
207 "CrossHatch" : wx
.CROSS_HATCH
,
208 "HorizontalHatch": wx
.HORIZONTAL_HATCH
,
209 "VerticalHatch" : wx
.VERTICAL_HATCH
214 "Transparent": wx
.TRANSPARENT
,
216 "LongDash" : wx
.LONG_DASH
,
217 "ShortDash" : wx
.SHORT_DASH
,
218 "DotDash" : wx
.DOT_DASH
,
221 # def BBFromPoints(self, Points):
223 # Calculates a Bounding box from a set of points (NX2 array of coordinates)
224 # \param Points (array?)
227 # ## fixme: this could be done with array.min() and vstack() in numpy.
228 # ## This could use the Utilities.BBox module now.
229 # #return N.array( (N.minimum.reduce(Points),
230 # # N.maximum.reduce(Points) ),
232 # return BBox.fromPoints(Points)
234 def Bind(self
, Event
, CallBackFun
):
235 self
.CallBackFuncs
[Event
] = CallBackFun
237 self
._Canvas
.UseHitTest
= True
238 if self
.InForeground
and self
._Canvas
._ForegroundHTBitmap
is None:
239 self
._Canvas
.MakeNewForegroundHTBitmap()
240 elif self
._Canvas
._HTBitmap
is None:
241 self
._Canvas
.MakeNewHTBitmap()
242 if not self
.HitColor
:
243 if not self
._Canvas
.HitColorGenerator
:
244 self
._Canvas
.HitColorGenerator
= _colorGenerator()
245 self
._Canvas
.HitColorGenerator
.next() # first call to prevent the background color from being used.
246 self
.HitColor
= self
._Canvas
.HitColorGenerator
.next()
247 self
.SetHitPen(self
.HitColor
,self
.HitLineWidth
)
248 self
.SetHitBrush(self
.HitColor
)
249 # put the object in the hit dict, indexed by it's color
250 if not self
._Canvas
.HitDict
:
251 self
._Canvas
.MakeHitDict()
252 self
._Canvas
.HitDict
[Event
][self
.HitColor
] = (self
) # put the object in the hit dict, indexed by it's color
255 ## fixme: this only removes one from each list, there could be more.
256 if self
._Canvas
.HitDict
:
257 for List
in self
._Canvas
.HitDict
.itervalues():
265 def SetBrush(self
,FillColor
,FillStyle
):
266 if FillColor
is None or FillStyle
is None:
267 self
.Brush
= wx
.TRANSPARENT_BRUSH
268 ##fixme: should I really re-set the style?
269 self
.FillStyle
= "Transparent"
271 self
.Brush
= self
.BrushList
.setdefault( (FillColor
,FillStyle
), wx
.Brush(FillColor
,self
.FillStyleList
[FillStyle
] ) )
273 def SetPen(self
,LineColor
,LineStyle
,LineWidth
):
274 if (LineColor
is None) or (LineStyle
is None):
275 self
.Pen
= wx
.TRANSPARENT_PEN
276 self
.LineStyle
= 'Transparent'
278 self
.Pen
= self
.PenList
.setdefault( (LineColor
,LineStyle
,LineWidth
), wx
.Pen(LineColor
,LineWidth
,self
.LineStyleList
[LineStyle
]) )
280 def SetHitBrush(self
,HitColor
):
282 self
.HitBrush
= wx
.TRANSPARENT_BRUSH
284 self
.HitBrush
= self
.BrushList
.setdefault( (HitColor
,"solid"), wx
.Brush(HitColor
,self
.FillStyleList
["Solid"] ) )
286 def SetHitPen(self
,HitColor
,LineWidth
):
288 self
.HitPen
= wx
.TRANSPARENT_PEN
290 self
.HitPen
= self
.PenList
.setdefault( (HitColor
, "solid", self
.HitLineWidth
), wx
.Pen(HitColor
, self
.HitLineWidth
, self
.LineStyleList
["Solid"]) )
292 ## Just to make sure that they will always be there
293 ## the appropriate ones should be overridden in the subclasses
294 def SetColor(self
, Color
):
296 def SetLineColor(self
, LineColor
):
298 def SetLineStyle(self
, LineStyle
):
300 def SetLineWidth(self
, LineWidth
):
302 def SetFillColor(self
, FillColor
):
304 def SetFillStyle(self
, FillStyle
):
307 def PutInBackground(self
):
308 if self
._Canvas
and self
.InForeground
:
309 self
._Canvas
._ForeDrawList
.remove(self
)
310 self
._Canvas
._DrawList
.append(self
)
311 self
._Canvas
._BackgroundDirty
= True
312 self
.InForeground
= False
314 def PutInForeground(self
):
315 if self
._Canvas
and (not self
.InForeground
):
316 self
._Canvas
._ForeDrawList
.append(self
)
317 self
._Canvas
._DrawList
.remove(self
)
318 self
._Canvas
._BackgroundDirty
= True
319 self
.InForeground
= True
322 """! \brief Make an object hidden.
327 """! \brief Make an object visible on the canvas.
331 class Group(DrawObject
):
333 A group of other FloatCanvas Objects
335 Not all DrawObject methods may apply here. In particular, you can't Bind events to a group.
337 Note that if an object is in more than one group, it will get drawn more than once.
341 def __init__(self
, ObjectList
=[], InForeground
= False, IsVisible
= True):
342 self
.ObjectList
= list(ObjectList
)
343 DrawObject
.__init
__(self
, InForeground
, IsVisible
)
344 self
.CalcBoundingBox()
346 def AddObject(self
, obj
):
347 self
.ObjectList
.append(obj
)
348 self
.BoundingBox
.Merge(obj
.BoundingBox
)
350 def AddObjects(self
, Objects
):
354 def CalcBoundingBox(self
):
356 BB
= BBox
.asBBox(self
.ObjectList
[0].BoundingBox
)
357 for obj
in self
.ObjectList
[1:]:
358 BB
.Merge(obj
.BoundingBox
)
361 self
.BoundingBox
= BB
363 def SetColor(self
, Color
):
364 for o
in self
.ObjectList
:
366 def SetLineColor(self
, Color
):
367 for o
in self
.ObjectList
:
368 o
.SetLineColor(Color
)
369 def SetLineStyle(self
, LineStyle
):
370 for o
in self
.ObjectList
:
371 o
.SetLineStyle(LineStyle
)
372 def SetLineWidth(self
, LineWidth
):
373 for o
in self
.ObjectList
:
374 o
.SetLineWidth(LineWidth
)
375 def SetFillColor(self
, Color
):
376 for o
in self
.ObjectList
:
377 o
.SetFillColor(Color
)
378 def SetFillStyle(self
, FillStyle
):
379 for o
in self
.ObjectList
:
380 o
.SetFillStyle(FillStyle
)
381 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
= None, HTdc
=None):
382 for obj
in self
.ObjectList
:
383 obj
._Draw
(dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
)
386 class ColorOnlyMixin
:
389 Mixin class for objects that have just one color, rather than a fill
394 def SetColor(self
, Color
):
395 self
.SetPen(Color
,"Solid",1)
396 self
.SetBrush(Color
,"Solid")
398 SetFillColor
= SetColor
# Just to provide a consistant interface
403 Mixin class for objects that have just one color, rather than a fill
408 def SetLineColor(self
, LineColor
):
409 self
.LineColor
= LineColor
410 self
.SetPen(LineColor
,self
.LineStyle
,self
.LineWidth
)
411 SetColor
= SetLineColor
# so that it will do somethign reasonable
413 def SetLineStyle(self
, LineStyle
):
414 self
.LineStyle
= LineStyle
415 self
.SetPen(self
.LineColor
,LineStyle
,self
.LineWidth
)
417 def SetLineWidth(self
, LineWidth
):
418 self
.LineWidth
= LineWidth
419 self
.SetPen(self
.LineColor
,self
.LineStyle
,LineWidth
)
421 class LineAndFillMixin(LineOnlyMixin
):
424 Mixin class for objects that have both a line and a fill color and
428 def SetFillColor(self
, FillColor
):
429 self
.FillColor
= FillColor
430 self
.SetBrush(FillColor
, self
.FillStyle
)
432 def SetFillStyle(self
, FillStyle
):
433 self
.FillStyle
= FillStyle
434 self
.SetBrush(self
.FillColor
,FillStyle
)
436 def SetUpDraw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
):
438 dc
.SetBrush(self
.Brush
)
439 if HTdc
and self
.HitAble
:
440 HTdc
.SetPen(self
.HitPen
)
441 HTdc
.SetBrush(self
.HitBrush
)
442 return ( WorldToPixel(self
.XY
),
443 ScaleWorldToPixel(self
.WH
) )
448 This is a mixin class that provides some methods suitable for use
449 with objects that have a single (x,y) coordinate pair.
453 def Move(self
, Delta
):
456 Move(Delta): moves the object by delta, where delta is a
457 (dx,dy) pair. Ideally a Numpy array of shape (2,)
461 Delta
= N
.asarray(Delta
, N
.float)
463 self
.BoundingBox
+= Delta
466 self
._Canvas
.BoundingBoxDirty
= True
468 def CalcBoundingBox(self
):
469 ## This may get overwritten in some subclasses
470 self
.BoundingBox
= N
.array( (self
.XY
, self
.XY
), N
.float )
471 self
.BoundingBox
= BBox
.asBBox((self
.XY
, self
.XY
))
473 def SetPoint(self
, xy
):
474 xy
= N
.array(xy
, N
.float)
478 self
.CalcBoundingBox()
481 self
._Canvas
.BoundingBoxDirty
= True
483 class PointsObjectMixin
:
486 This is a mixin class that provides some methods suitable for use
487 with objects that have a set of (x,y) coordinate pairs.
491 def Move(self
, Delta
):
493 Move(Delta): moves the object by delta, where delta is an (dx,
494 dy) pair. Ideally a Numpy array of shape (2,)
497 Delta
= N
.asarray(Delta
, N
.float)
500 self
.BoundingBox
+= Delta
502 self
._Canvas
.BoundingBoxDirty
= True
504 def CalcBoundingBox(self
):
505 self
.BoundingBox
= BBox
.fromPoints(self
.Points
)
507 self
._Canvas
.BoundingBoxDirty
= True
509 def SetPoints(self
, Points
, copy
= True):
511 Sets the coordinates of the points of the object to Points (NX2 array).
513 By default, a copy is made, if copy is set to False, a reference
514 is used, iff Points is a NumPy array of Floats. This allows you
515 to change some or all of the points without making any copies.
519 Points = Object.Points
520 Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir.
521 Object.SetPoints(Points, False) # Sets the points to the same array as it was
525 self
.Points
= N
.array(Points
, N
.float)
526 self
.Points
.shape
= (-1,2) # Make sure it is a NX2 array, even if there is only one point
528 self
.Points
= N
.asarray(Points
, N
.float)
529 self
.CalcBoundingBox()
532 class Polygon(PointsObjectMixin
, LineAndFillMixin
, DrawObject
):
536 The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of
537 point coordinates. so that Points[N][0] is the x-coordinate of
538 point N and Points[N][1] is the y-coordinate or Points[N,0] is the
539 x-coordinate of point N and Points[N,1] is the y-coordinate for
542 The other parameters specify various properties of the Polygon, and
543 should be self explanatory.
553 InForeground
= False):
554 DrawObject
.__init
__(self
, InForeground
)
555 self
.Points
= N
.array(Points
,N
.float) # this DOES need to make a copy
556 self
.CalcBoundingBox()
558 self
.LineColor
= LineColor
559 self
.LineStyle
= LineStyle
560 self
.LineWidth
= LineWidth
561 self
.FillColor
= FillColor
562 self
.FillStyle
= FillStyle
564 self
.HitLineWidth
= max(LineWidth
,self
.MinHitLineWidth
)
566 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
567 self
.SetBrush(FillColor
,FillStyle
)
569 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
= None, HTdc
=None):
570 Points
= WorldToPixel(self
.Points
)#.tolist()
572 dc
.SetBrush(self
.Brush
)
573 dc
.DrawPolygon(Points
)
574 if HTdc
and self
.HitAble
:
575 HTdc
.SetPen(self
.HitPen
)
576 HTdc
.SetBrush(self
.HitBrush
)
577 HTdc
.DrawPolygon(Points
)
579 class Line(PointsObjectMixin
, LineOnlyMixin
, DrawObject
,):
582 The Line class takes a list of 2-tuples, or a NX2 NumPy Float array
583 of point coordinates.
585 It will draw a straight line if there are two points, and a polyline
586 if there are more than two.
589 def __init__(self
,Points
,
593 InForeground
= False):
594 DrawObject
.__init
__(self
, InForeground
)
597 self
.Points
= N
.array(Points
,N
.float)
598 self
.CalcBoundingBox()
600 self
.LineColor
= LineColor
601 self
.LineStyle
= LineStyle
602 self
.LineWidth
= LineWidth
604 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
606 self
.HitLineWidth
= max(LineWidth
,self
.MinHitLineWidth
)
609 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
610 Points
= WorldToPixel(self
.Points
)
613 if HTdc
and self
.HitAble
:
614 HTdc
.SetPen(self
.HitPen
)
615 HTdc
.DrawLines(Points
)
618 def __init__(self
, *args
, **kwargs
):
619 Line
.__init
__(self
, *args
, **kwargs
)
621 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
622 Points
= WorldToPixel(self
.Points
)
624 dc
.DrawSpline(Points
)
625 if HTdc
and self
.HitAble
:
626 HTdc
.SetPen(self
.HitPen
)
627 HTdc
.DrawSpline(Points
)
630 class Arrow(XYObjectMixin
, LineOnlyMixin
, DrawObject
):
633 Arrow(XY, # coords of origin of arrow (x,y)
634 Length, # length of arrow in pixels
635 theta, # angle of arrow in degrees: zero is straight up
636 # +angle is to the right
640 ArrowHeadSize = 4, # size of arrowhead in pixels
641 ArrowHeadAngle = 45, # angle of arrow head in degrees
642 InForeground = False):
644 It will draw an arrow , starting at the point, (X,Y) pointing in
655 LineWidth
= 2, # pixels
656 ArrowHeadSize
= 8, # pixels
657 ArrowHeadAngle
= 30, # degrees
658 InForeground
= False):
660 DrawObject
.__init
__(self
, InForeground
)
662 self
.XY
= N
.array(XY
, N
.float)
663 self
.XY
.shape
= (2,) # Make sure it is a length 2 vector
665 self
.Direction
= float(Direction
)
666 self
.ArrowHeadSize
= ArrowHeadSize
667 self
.ArrowHeadAngle
= float(ArrowHeadAngle
)
669 self
.CalcArrowPoints()
670 self
.CalcBoundingBox()
672 self
.LineColor
= LineColor
673 self
.LineStyle
= LineStyle
674 self
.LineWidth
= LineWidth
676 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
678 ##fixme: How should the HitTest be drawn?
679 self
.HitLineWidth
= max(LineWidth
,self
.MinHitLineWidth
)
681 def SetDirection(self
, Direction
):
682 self
.Direction
= float(Direction
)
683 self
.CalcArrowPoints()
685 def SetLength(self
, Length
):
687 self
.CalcArrowPoints()
689 def SetLengthDirection(self
, Length
, Direction
):
690 self
.Direction
= float(Direction
)
692 self
.CalcArrowPoints()
694 ## def CalcArrowPoints(self):
696 ## S = self.ArrowHeadSize
697 ## phi = self.ArrowHeadAngle * N.pi / 360
698 ## theta = (self.Direction-90.0) * N.pi / 180
699 ## ArrowPoints = N.array( ( (0, L, L - S*N.cos(phi),L, L - S*N.cos(phi) ),
700 ## (0, 0, S*N.sin(phi), 0, -S*N.sin(phi) ) ),
702 ## RotationMatrix = N.array( ( ( N.cos(theta), -N.sin(theta) ),
703 ## ( N.sin(theta), N.cos(theta) ) ),
706 ## ArrowPoints = N.matrixmultiply(RotationMatrix, ArrowPoints)
707 ## self.ArrowPoints = N.transpose(ArrowPoints)
709 def CalcArrowPoints(self
):
711 S
= self
.ArrowHeadSize
712 phi
= self
.ArrowHeadAngle
* N
.pi
/ 360
713 theta
= (270 - self
.Direction
) * N
.pi
/ 180
714 AP
= N
.array( ( (0,0),
716 (N
.cos(theta
- phi
), -N
.sin(theta
- phi
) ),
718 (N
.cos(theta
+ phi
), -N
.sin(theta
+ phi
) ),
721 shift
= (-L
*N
.cos(theta
), L
*N
.sin(theta
) )
723 self
.ArrowPoints
= AP
725 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
727 xy
= WorldToPixel(self
.XY
)
728 ArrowPoints
= xy
+ self
.ArrowPoints
729 dc
.DrawLines(ArrowPoints
)
730 if HTdc
and self
.HitAble
:
731 HTdc
.SetPen(self
.HitPen
)
732 HTdc
.DrawLines(ArrowPoints
)
735 class ArrowLine(PointsObjectMixin
, LineOnlyMixin
, DrawObject
):
738 ArrowLine(Points, # coords of points
742 ArrowHeadSize = 4, # in pixels
744 InForeground = False):
746 It will draw a set of arrows from point to point.
748 It takes a list of 2-tuples, or a NX2 NumPy Float array
749 of point coordinates.
758 LineWidth
= 1, # pixels
759 ArrowHeadSize
= 8, # pixels
760 ArrowHeadAngle
= 30, # degrees
761 InForeground
= False):
763 DrawObject
.__init
__(self
, InForeground
)
765 self
.Points
= N
.asarray(Points
,N
.float)
766 self
.Points
.shape
= (-1,2) # Make sure it is a NX2 array, even if there is only one point
767 self
.ArrowHeadSize
= ArrowHeadSize
768 self
.ArrowHeadAngle
= float(ArrowHeadAngle
)
770 self
.CalcArrowPoints()
771 self
.CalcBoundingBox()
773 self
.LineColor
= LineColor
774 self
.LineStyle
= LineStyle
775 self
.LineWidth
= LineWidth
777 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
779 self
.HitLineWidth
= max(LineWidth
,self
.MinHitLineWidth
)
781 def CalcArrowPoints(self
):
782 S
= self
.ArrowHeadSize
783 phi
= self
.ArrowHeadAngle
* N
.pi
/ 360
786 self
.ArrowPoints
= N
.zeros((n
-1, 3, 2), N
.float)
787 for i
in xrange(n
-1):
788 dx
, dy
= self
.Points
[i
] - self
.Points
[i
+1]
789 theta
= N
.arctan2(dy
, dx
)
791 (N
.cos(theta
- phi
), -N
.sin(theta
-phi
)),
793 (N
.cos(theta
+ phi
), -N
.sin(theta
+ phi
))
796 self
.ArrowPoints
[i
,:,:] = AP
797 self
.ArrowPoints
*= S
799 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
800 Points
= WorldToPixel(self
.Points
)
801 ArrowPoints
= Points
[1:,N
.newaxis
,:] + self
.ArrowPoints
804 for arrow
in ArrowPoints
:
806 if HTdc
and self
.HitAble
:
807 HTdc
.SetPen(self
.HitPen
)
808 HTdc
.DrawLines(Points
)
809 for arrow
in ArrowPoints
:
810 HTdc
.DrawLines(arrow
)
813 class PointSet(PointsObjectMixin
, ColorOnlyMixin
, DrawObject
):
816 The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of
819 If Points is a sequence of tuples: Points[N][0] is the x-coordinate of
820 point N and Points[N][1] is the y-coordinate.
822 If Points is a NumPy array: Points[N,0] is the x-coordinate of point
823 N and Points[N,1] is the y-coordinate for arrays.
825 Each point will be drawn the same color and Diameter. The Diameter
826 is in screen pixels, not world coordinates.
828 The hit-test code does not distingish between the points, you will
829 only know that one of the points got hit, not which one. You can use
830 PointSet.FindClosestPoint(WorldPoint) to find out which one
832 In the case of points, the HitLineWidth is used as diameter.
835 def __init__(self
, Points
, Color
= "Black", Diameter
= 1, InForeground
= False):
836 DrawObject
.__init
__(self
,InForeground
)
838 self
.Points
= N
.array(Points
,N
.float)
839 self
.Points
.shape
= (-1,2) # Make sure it is a NX2 array, even if there is only one point
840 self
.CalcBoundingBox()
841 self
.Diameter
= Diameter
843 self
.HitLineWidth
= min(self
.MinHitLineWidth
, Diameter
)
846 def SetDiameter(self
,Diameter
):
847 self
.Diameter
= Diameter
849 def FindClosestPoint(self
, XY
):
852 Returns the index of the closest point to the point, XY, given
853 in World coordinates. It's essentially random which you get if
854 there are more than one that are the same.
856 This can be used to figure out which point got hit in a mouse
857 binding callback, for instance. It's a lot faster that using a
858 lot of separate points.
862 return N
.argmin(N
.hypot(d
[:,0],d
[:,1]))
865 def DrawD2(self
, dc
, Points
):
866 # A Little optimization for a diameter2 - point
867 dc
.DrawPointList(Points
)
868 dc
.DrawPointList(Points
+ (1,0))
869 dc
.DrawPointList(Points
+ (0,1))
870 dc
.DrawPointList(Points
+ (1,1))
872 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
874 Points
= WorldToPixel(self
.Points
)
875 if self
.Diameter
<= 1:
876 dc
.DrawPointList(Points
)
877 elif self
.Diameter
<= 2:
878 self
.DrawD2(dc
, Points
)
880 dc
.SetBrush(self
.Brush
)
881 radius
= int(round(self
.Diameter
/2))
882 ##fixme: I really should add a DrawCircleList to wxPython
883 if len(Points
) > 100:
885 xywh
= N
.concatenate((xy
-radius
, N
.ones(xy
.shape
) * self
.Diameter
), 1 )
886 dc
.DrawEllipseList(xywh
)
889 dc
.DrawCircle(xy
[0],xy
[1], radius
)
890 if HTdc
and self
.HitAble
:
891 HTdc
.SetPen(self
.HitPen
)
892 HTdc
.SetBrush(self
.HitBrush
)
893 if self
.Diameter
<= 1:
894 HTdc
.DrawPointList(Points
)
895 elif self
.Diameter
<= 2:
896 self
.DrawD2(HTdc
, Points
)
898 if len(Points
) > 100:
900 xywh
= N
.concatenate((xy
-radius
, N
.ones(xy
.shape
) * self
.Diameter
), 1 )
901 HTdc
.DrawEllipseList(xywh
)
904 HTdc
.DrawCircle(xy
[0],xy
[1], radius
)
906 class Point(XYObjectMixin
, ColorOnlyMixin
, DrawObject
):
909 The Point class takes a 2-tuple, or a (2,) NumPy array of point
912 The Diameter is in screen points, not world coordinates, So the
913 Bounding box is just the point, and doesn't include the Diameter.
915 The HitLineWidth is used as diameter for the
919 def __init__(self
, XY
, Color
= "Black", Diameter
= 1, InForeground
= False):
920 DrawObject
.__init
__(self
, InForeground
)
922 self
.XY
= N
.array(XY
, N
.float)
923 self
.XY
.shape
= (2,) # Make sure it is a length 2 vector
924 self
.CalcBoundingBox()
926 self
.Diameter
= Diameter
928 self
.HitLineWidth
= self
.MinHitLineWidth
930 def SetDiameter(self
,Diameter
):
931 self
.Diameter
= Diameter
934 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
936 xy
= WorldToPixel(self
.XY
)
937 if self
.Diameter
<= 1:
938 dc
.DrawPoint(xy
[0], xy
[1])
940 dc
.SetBrush(self
.Brush
)
941 radius
= int(round(self
.Diameter
/2))
942 dc
.DrawCircle(xy
[0],xy
[1], radius
)
943 if HTdc
and self
.HitAble
:
944 HTdc
.SetPen(self
.HitPen
)
945 if self
.Diameter
<= 1:
946 HTdc
.DrawPoint(xy
[0], xy
[1])
948 HTdc
.SetBrush(self
.HitBrush
)
949 HTdc
.DrawCircle(xy
[0],xy
[1], radius
)
951 class SquarePoint(XYObjectMixin
, ColorOnlyMixin
, DrawObject
):
954 The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point
955 coordinates. It produces a square dot, centered on Point
957 The Size is in screen points, not world coordinates, so the
958 Bounding box is just the point, and doesn't include the Size.
960 The HitLineWidth is used as diameter for the
964 def __init__(self
, Point
, Color
= "Black", Size
= 4, InForeground
= False):
965 DrawObject
.__init
__(self
, InForeground
)
967 self
.XY
= N
.array(Point
, N
.float)
968 self
.XY
.shape
= (2,) # Make sure it is a length 2 vector
969 self
.CalcBoundingBox()
973 self
.HitLineWidth
= self
.MinHitLineWidth
975 def SetSize(self
,Size
):
978 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
981 xc
,yc
= WorldToPixel(self
.XY
)
988 dc
.SetBrush(self
.Brush
)
989 dc
.DrawRectangle(x
, y
, Size
, Size
)
990 if HTdc
and self
.HitAble
:
991 HTdc
.SetPen(self
.HitPen
)
993 HTdc
.DrawPoint(xc
, xc
)
995 HTdc
.SetBrush(self
.HitBrush
)
996 HTdc
.DrawRectangle(x
, y
, Size
, Size
)
998 class RectEllipse(XYObjectMixin
, LineAndFillMixin
, DrawObject
):
999 def __init__(self
, XY
, WH
,
1000 LineColor
= "Black",
1001 LineStyle
= "Solid",
1004 FillStyle
= "Solid",
1005 InForeground
= False):
1007 DrawObject
.__init
__(self
,InForeground
)
1009 self
.SetShape(XY
, WH
)
1010 self
.LineColor
= LineColor
1011 self
.LineStyle
= LineStyle
1012 self
.LineWidth
= LineWidth
1013 self
.FillColor
= FillColor
1014 self
.FillStyle
= FillStyle
1016 self
.HitLineWidth
= max(LineWidth
,self
.MinHitLineWidth
)
1018 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
1019 self
.SetBrush(FillColor
,FillStyle
)
1021 def SetShape(self
, XY
, WH
):
1022 self
.XY
= N
.array( XY
, N
.float)
1023 self
.XY
.shape
= (2,)
1024 self
.WH
= N
.array( WH
, N
.float)
1025 self
.WH
.shape
= (2,)
1026 self
.CalcBoundingBox()
1029 def CalcBoundingBox(self
):
1030 # you need this in case Width or Height are negative
1031 corners
= N
.array((self
.XY
, (self
.XY
+ self
.WH
) ), N
.float)
1032 self
.BoundingBox
= BBox
.fromPoints(corners
)
1034 self
._Canvas
.BoundingBoxDirty
= True
1037 class Rectangle(RectEllipse
):
1039 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1040 ( XY
, WH
) = self
.SetUpDraw(dc
,
1044 dc
.DrawRectanglePointSize(XY
, WH
)
1045 if HTdc
and self
.HitAble
:
1046 HTdc
.DrawRectanglePointSize(XY
, WH
)
1050 class Ellipse(RectEllipse
):
1052 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1053 ( XY
, WH
) = self
.SetUpDraw(dc
,
1057 dc
.DrawEllipsePointSize(XY
, WH
)
1058 if HTdc
and self
.HitAble
:
1059 HTdc
.DrawEllipsePointSize(XY
, WH
)
1061 class Circle(Ellipse
):
1062 ## fixme: this should probably be use the DC.DrawCircle!
1063 def __init__(self
, XY
, Diameter
, **kwargs
):
1064 self
.Center
= N
.array(XY
, N
.float)
1065 Diameter
= float(Diameter
)
1066 RectEllipse
.__init
__(self
,
1067 self
.Center
- Diameter
/2.0,
1068 (Diameter
, Diameter
),
1071 def SetDiameter(self
, Diameter
):
1072 Diameter
= float(Diameter
)
1073 XY
= self
.Center
- (Diameter
/2.0)
1075 (Diameter
, Diameter
)
1078 class TextObjectMixin(XYObjectMixin
):
1081 A mix in class that holds attributes and methods that are needed by
1086 ## I'm caching fonts, because on GTK, getting a new font can take a
1087 ## while. However, it gets cleared after every full draw as hanging
1088 ## on to a bunch of large fonts takes a massive amount of memory.
1092 LayoutFontSize
= 16 # font size used for calculating layout
1094 def SetFont(self
, Size
, Family
, Style
, Weight
, Underlined
, FaceName
):
1095 self
.Font
= self
.FontList
.setdefault( (Size
,
1101 #wx.FontFromPixelSize((0.45*Size,Size), # this seemed to give a decent height/width ratio on Windows
1109 def SetColor(self
, Color
):
1112 def SetBackgroundColor(self
, BackgroundColor
):
1113 self
.BackgroundColor
= BackgroundColor
1115 def SetText(self
, String
):
1117 Re-sets the text displayed by the object
1119 In the case of the ScaledTextBox, it will re-do the layout as appropriate
1121 Note: only tested with the ScaledTextBox
1125 self
.String
= String
1128 def LayoutText(self
):
1130 A dummy method to re-do the layout of the text.
1132 A derived object needs to override this if required.
1137 ## store the function that shift the coords for drawing text. The
1138 ## "c" parameter is the correction for world coordinates, rather
1139 ## than pixel coords as the y axis is reversed
1140 ## pad is the extra space around the text
1141 ## if world = 1, the vertical shift is done in y-up coordinates
1142 ShiftFunDict
= {'tl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
+ pad
, y
+ pad
- 2*world
*pad
),
1143 'tc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
/2, y
+ pad
- 2*world
*pad
),
1144 'tr': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
- pad
, y
+ pad
- 2*world
*pad
),
1145 'cl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
+ pad
, y
- h
/2 + world
*h
),
1146 'cc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
/2, y
- h
/2 + world
*h
),
1147 'cr': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
- pad
, y
- h
/2 + world
*h
),
1148 'bl': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
+ pad
, y
- h
+ 2*world
*h
- pad
+ world
*2*pad
) ,
1149 'bc': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
/2, y
- h
+ 2*world
*h
- pad
+ world
*2*pad
) ,
1150 'br': lambda x
, y
, w
, h
, world
=0, pad
=0: (x
- w
- pad
, y
- h
+ 2*world
*h
- pad
+ world
*2*pad
)}
1152 class Text(TextObjectMixin
, DrawObject
, ):
1154 This class creates a text object, placed at the coordinates,
1155 x,y. the "Position" argument is a two charactor string, indicating
1156 where in relation to the coordinates the string should be oriented.
1158 The first letter is: t, c, or b, for top, center and bottom The
1159 second letter is: l, c, or r, for left, center and right The
1160 position refers to the position relative to the text itself. It
1161 defaults to "tl" (top left).
1163 Size is the size of the font in pixels, or in points for printing
1164 (if it ever gets implimented). Those will be the same, If you assume
1168 Font family, a generic way of referring to fonts without
1169 specifying actual facename. One of:
1170 wx.DEFAULT: Chooses a default font.
1171 wx.DECORATIVE: A decorative font.
1172 wx.ROMAN: A formal, serif font.
1173 wx.SCRIPT: A handwriting font.
1174 wx.SWISS: A sans-serif font.
1175 wx.MODERN: A fixed pitch font.
1176 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1178 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1180 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1182 The value can be True or False. At present this may have an an
1183 effect on Windows only.
1185 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1186 above will be ignored.
1188 The size is fixed, and does not scale with the drawing.
1190 The hit-test is done on the entire text extent
1194 def __init__(self
,String
, xy
,
1197 BackgroundColor
= None,
1203 InForeground
= False,
1206 DrawObject
.__init
__(self
,InForeground
)
1208 self
.String
= String
1209 # Input size in in Pixels, compute points size from FontScaleinfo.
1210 # fixme: for printing, we'll have to do something a little different
1211 self
.Size
= Size
* FontScale
1214 self
.BackgroundColor
= BackgroundColor
1219 FaceName
= Font
.GetFaceName()
1220 Family
= Font
.GetFamily()
1221 Size
= Font
.GetPointSize()
1222 Style
= Font
.GetStyle()
1223 Underlined
= Font
.GetUnderlined()
1224 Weight
= Font
.GetWeight()
1225 self
.SetFont(Size
, Family
, Style
, Weight
, Underlined
, FaceName
)
1227 self
.BoundingBox
= BBox
.asBBox((xy
, xy
))
1229 self
.XY
= N
.asarray(xy
)
1230 self
.XY
.shape
= (2,)
1232 (self
.TextWidth
, self
.TextHeight
) = (None, None)
1233 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1235 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1236 XY
= WorldToPixel(self
.XY
)
1237 dc
.SetFont(self
.Font
)
1238 dc
.SetTextForeground(self
.Color
)
1239 if self
.BackgroundColor
:
1240 dc
.SetBackgroundMode(wx
.SOLID
)
1241 dc
.SetTextBackground(self
.BackgroundColor
)
1243 dc
.SetBackgroundMode(wx
.TRANSPARENT
)
1244 if self
.TextWidth
is None or self
.TextHeight
is None:
1245 (self
.TextWidth
, self
.TextHeight
) = dc
.GetTextExtent(self
.String
)
1246 XY
= self
.ShiftFun(XY
[0], XY
[1], self
.TextWidth
, self
.TextHeight
)
1247 dc
.DrawTextPoint(self
.String
, XY
)
1248 if HTdc
and self
.HitAble
:
1249 HTdc
.SetPen(self
.HitPen
)
1250 HTdc
.SetBrush(self
.HitBrush
)
1251 HTdc
.DrawRectanglePointSize(XY
, (self
.TextWidth
, self
.TextHeight
) )
1253 class ScaledText(TextObjectMixin
, DrawObject
, ):
1255 This class creates a text object that is scaled when zoomed. It is
1256 placed at the coordinates, x,y. the "Position" argument is a two
1257 charactor string, indicating where in relation to the coordinates
1258 the string should be oriented.
1260 The first letter is: t, c, or b, for top, center and bottom The
1261 second letter is: l, c, or r, for left, center and right The
1262 position refers to the position relative to the text itself. It
1263 defaults to "tl" (top left).
1265 Size is the size of the font in world coordinates.
1268 Font family, a generic way of referring to fonts without
1269 specifying actual facename. One of:
1270 wx.DEFAULT: Chooses a default font.
1271 wx.DECORATI: A decorative font.
1272 wx.ROMAN: A formal, serif font.
1273 wx.SCRIPT: A handwriting font.
1274 wx.SWISS: A sans-serif font.
1275 wx.MODERN: A fixed pitch font.
1276 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1278 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1280 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1282 The value can be True or False. At present this may have an an
1283 effect on Windows only.
1285 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1286 above will be ignored. The size of the font you specify will be
1287 ignored, but the rest of its attributes will be preserved.
1289 The size will scale as the drawing is zoomed.
1293 As fonts are scaled, the do end up a little different, so you don't
1294 get exactly the same picture as you scale up and doen, but it's
1297 On wxGTK1 on my Linux system, at least, using a font of over about
1298 3000 pts. brings the system to a halt. It's the Font Server using
1299 huge amounts of memory. My work around is to max the font size to
1300 3000 points, so it won't scale past there. GTK2 uses smarter font
1301 drawing, so that may not be an issue in future versions, so feel
1302 free to test. Another smarter way to do it would be to set a global
1303 zoom limit at that point.
1305 The hit-test is done on the entire text extent. This could be made
1306 optional, but I haven't gotten around to it.
1315 BackgroundColor
= None,
1322 InForeground
= False):
1324 DrawObject
.__init
__(self
,InForeground
)
1326 self
.String
= String
1327 self
.XY
= N
.array( XY
, N
.float)
1328 self
.XY
.shape
= (2,)
1331 self
.BackgroundColor
= BackgroundColor
1332 self
.Family
= Family
1334 self
.Weight
= Weight
1335 self
.Underlined
= Underlined
1339 self
.FaceName
= Font
.GetFaceName()
1340 self
.Family
= Font
.GetFamily()
1341 self
.Style
= Font
.GetStyle()
1342 self
.Underlined
= Font
.GetUnderlined()
1343 self
.Weight
= Font
.GetWeight()
1345 # Experimental max font size value on wxGTK2: this works OK on
1346 # my system. If it's a lot larger, there is a crash, with the
1349 # The application 'FloatCanvasDemo.py' lost its
1350 # connection to the display :0.0; most likely the X server was
1351 # shut down or you killed/destroyed the application.
1353 # Windows and OS-X seem to be better behaved in this regard.
1354 # They may not draw it, but they don't crash either!
1355 self
.MaxFontSize
= 1000
1357 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1359 self
.CalcBoundingBox()
1361 def LayoutText(self
):
1362 # This will be called when the text is re-set
1363 # nothing much to be done here
1364 self
.CalcBoundingBox()
1366 def CalcBoundingBox(self
):
1367 ## this isn't exact, as fonts don't scale exactly.
1369 bitmap
= wx
.EmptyBitmap(1, 1)
1370 dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work.
1371 DrawingSize
= 40 # pts This effectively determines the resolution that the BB is computed to.
1372 ScaleFactor
= float(self
.Size
) / DrawingSize
1373 self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underlined
, self
.FaceName
)
1374 dc
.SetFont(self
.Font
)
1375 (w
,h
) = dc
.GetTextExtent(self
.String
)
1378 x
, y
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world
= 1)
1379 self
.BoundingBox
= BBox
.asBBox(((x
, y
-h
),(x
+ w
, y
)))
1381 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1382 (X
,Y
) = WorldToPixel( (self
.XY
) )
1384 # compute the font size:
1385 Size
= abs( ScaleWorldToPixel( (self
.Size
, self
.Size
) )[1] ) # only need a y coordinate length
1386 ## Check to see if the font size is large enough to blow up the X font server
1387 ## If so, limit it. Would it be better just to not draw it?
1388 ## note that this limit is dependent on how much memory you have, etc.
1389 Size
= min(Size
, self
.MaxFontSize
)
1390 self
.SetFont(Size
, self
.Family
, self
.Style
, self
.Weight
, self
.Underlined
, self
.FaceName
)
1391 dc
.SetFont(self
.Font
)
1392 dc
.SetTextForeground(self
.Color
)
1393 if self
.BackgroundColor
:
1394 dc
.SetBackgroundMode(wx
.SOLID
)
1395 dc
.SetTextBackground(self
.BackgroundColor
)
1397 dc
.SetBackgroundMode(wx
.TRANSPARENT
)
1398 (w
,h
) = dc
.GetTextExtent(self
.String
)
1399 # compute the shift, and adjust the coordinates, if neccesary
1400 # This had to be put in here, because it changes with Zoom, as
1401 # fonts don't scale exactly.
1402 xy
= self
.ShiftFun(X
, Y
, w
, h
)
1404 dc
.DrawTextPoint(self
.String
, xy
)
1405 if HTdc
and self
.HitAble
:
1406 HTdc
.SetPen(self
.HitPen
)
1407 HTdc
.SetBrush(self
.HitBrush
)
1408 HTdc
.DrawRectanglePointSize(xy
, (w
, h
) )
1410 class ScaledTextBox(TextObjectMixin
, DrawObject
):
1412 This class creates a TextBox object that is scaled when zoomed. It is
1413 placed at the coordinates, x,y.
1415 If the Width parameter is defined, the text will be wrapped to the width given.
1417 A Box can be drawn around the text, be specifying:
1418 LineWidth and/or FillColor
1420 A space(margin) can be put all the way around the text, be specifying:
1421 the PadSize argument in world coordinates.
1423 The spacing between lines can be adjusted with the:
1424 LineSpacing argument.
1426 The "Position" argument is a two character string, indicating where
1427 in relation to the coordinates the Box should be oriented.
1428 -The first letter is: t, c, or b, for top, center and bottom.
1429 -The second letter is: l, c, or r, for left, center and right The
1430 position refers to the position relative to the text itself. It
1431 defaults to "tl" (top left).
1433 Size is the size of the font in world coordinates.
1436 Font family, a generic way of referring to fonts without
1437 specifying actual facename. One of:
1438 wx.DEFAULT: Chooses a default font.
1439 wx.DECORATIVE: A decorative font.
1440 wx.ROMAN: A formal, serif font.
1441 wx.SCRIPT: A handwriting font.
1442 wx.SWISS: A sans-serif font.
1443 wx.MODERN: A fixed pitch font.
1444 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1446 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1448 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1450 The value can be True or False. At present this may have an an
1451 effect on Windows only.
1453 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1454 above will be ignored. The size of the font you specify will be
1455 ignored, but the rest of its attributes will be preserved.
1457 The size will scale as the drawing is zoomed.
1461 As fonts are scaled, they do end up a little different, so you don't
1462 get exactly the same picture as you scale up and down, but it's
1465 On wxGTK1 on my Linux system, at least, using a font of over about
1466 1000 pts. brings the system to a halt. It's the Font Server using
1467 huge amounts of memory. My work around is to max the font size to
1468 1000 points, so it won't scale past there. GTK2 uses smarter font
1469 drawing, so that may not be an issue in future versions, so feel
1470 free to test. Another smarter way to do it would be to set a global
1471 zoom limit at that point.
1473 The hit-test is done on the entire box. This could be made
1474 optional, but I haven't gotten around to it.
1478 def __init__(self
, String
,
1482 BackgroundColor
= None,
1483 LineColor
= 'Black',
1484 LineStyle
= 'Solid',
1496 InForeground
= False):
1498 DrawObject
.__init
__(self
,InForeground
)
1500 self
.XY
= N
.array(Point
, N
.float)
1503 self
.BackgroundColor
= BackgroundColor
1504 self
.LineColor
= LineColor
1505 self
.LineStyle
= LineStyle
1506 self
.LineWidth
= LineWidth
1508 if PadSize
is None: # the default is just a little bit of padding
1509 self
.PadSize
= Size
/10.0
1511 self
.PadSize
= float(PadSize
)
1512 self
.Family
= Family
1514 self
.Weight
= Weight
1515 self
.Underlined
= Underlined
1516 self
.Alignment
= Alignment
.lower()
1517 self
.LineSpacing
= float(LineSpacing
)
1518 self
.Position
= Position
1523 self
.FaceName
= Font
.GetFaceName()
1524 self
.Family
= Font
.GetFamily()
1525 self
.Style
= Font
.GetStyle()
1526 self
.Underlined
= Font
.GetUnderlined()
1527 self
.Weight
= Font
.GetWeight()
1529 # Experimental max font size value on wxGTK2: this works OK on
1530 # my system. If it's a lot larger, there is a crash, with the
1533 # The application 'FloatCanvasDemo.py' lost its
1534 # connection to the display :0.0; most likely the X server was
1535 # shut down or you killed/destroyed the application.
1537 # Windows and OS-X seem to be better behaved in this regard.
1538 # They may not draw it, but they don't crash either!
1540 self
.MaxFontSize
= 1000
1541 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1543 self
.String
= String
1545 self
.CalcBoundingBox()
1547 self
.SetPen(LineColor
,LineStyle
,LineWidth
)
1548 self
.SetBrush(BackgroundColor
, "Solid")
1551 def WrapToWidth(self
):
1553 bitmap
= wx
.EmptyBitmap(1, 1)
1554 dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work.
1555 DrawingSize
= self
.LayoutFontSize
# pts This effectively determines the resolution that the BB is computed to.
1556 ScaleFactor
= float(self
.Size
) / DrawingSize
1557 Width
= (self
.Width
- 2*self
.PadSize
) / ScaleFactor
#Width to wrap to
1558 self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underlined
, self
.FaceName
)
1559 dc
.SetFont(self
.Font
)
1561 for s
in self
.Strings
:
1569 w
= dc
.GetTextExtent(' ' + text
[-1])[0]
1570 if LineLength
+ w
<= Width
:
1573 LineLength
= dc
.GetTextExtent(NewText
)[0]
1575 NewStrings
.append(NewText
)
1577 LineLength
= dc
.GetTextExtent(text
[-1])[0]
1579 NewStrings
.append(NewText
)
1580 self
.Strings
= NewStrings
1582 def ReWrap(self
, Width
):
1586 def LayoutText(self
):
1589 Calculates the positions of the words of text.
1591 This isn't exact, as fonts don't scale exactly.
1592 To help this, the position of each individual word
1593 is stored separately, so that the general layout stays
1594 the same in world coordinates, as the fonts scale.
1597 self
.Strings
= self
.String
.split("\n")
1602 bitmap
= wx
.EmptyBitmap(1, 1)
1603 dc
.SelectObject(bitmap
) #wxMac needs a Bitmap selected for GetTextExtent to work.
1605 DrawingSize
= self
.LayoutFontSize
# pts This effectively determines the resolution that the BB is computed to.
1606 ScaleFactor
= float(self
.Size
) / DrawingSize
1608 self
.SetFont(DrawingSize
, self
.Family
, self
.Style
, self
.Weight
, self
.Underlined
, self
.FaceName
)
1609 dc
.SetFont(self
.Font
)
1610 TextHeight
= dc
.GetTextExtent("X")[1]
1611 SpaceWidth
= dc
.GetTextExtent(" ")[0]
1612 LineHeight
= TextHeight
* self
.LineSpacing
1614 LineWidths
= N
.zeros((len(self
.Strings
),), N
.float)
1619 for i
, s
in enumerate(self
.Strings
):
1621 LineWords
= s
.split(" ")
1622 LinePoints
= N
.zeros((len(LineWords
),2), N
.float)
1623 for j
, word
in enumerate(LineWords
):
1625 LineWidths
[i
] += SpaceWidth
1627 LinePoints
[j
] = (LineWidths
[i
], y
)
1628 w
= dc
.GetTextExtent(word
)[0]
1631 AllLinePoints
.append(LinePoints
)
1632 TextWidth
= N
.maximum
.reduce(LineWidths
)
1635 if self
.Width
is None:
1636 BoxWidth
= TextWidth
* ScaleFactor
+ 2*self
.PadSize
1637 else: # use the defined Width
1638 BoxWidth
= self
.Width
1639 Points
= N
.zeros((0,2), N
.float)
1641 for i
, LinePoints
in enumerate(AllLinePoints
):
1642 ## Scale to World Coords.
1643 LinePoints
*= (ScaleFactor
, ScaleFactor
)
1644 if self
.Alignment
== 'left':
1645 LinePoints
[:,0] += self
.PadSize
1646 elif self
.Alignment
== 'center':
1647 LinePoints
[:,0] += (BoxWidth
- LineWidths
[i
]*ScaleFactor
)/2.0
1648 elif self
.Alignment
== 'right':
1649 LinePoints
[:,0] += (BoxWidth
- LineWidths
[i
]*ScaleFactor
-self
.PadSize
)
1650 Points
= N
.concatenate((Points
, LinePoints
))
1652 BoxHeight
= -(Points
[-1,1] - (TextHeight
* ScaleFactor
)) + 2*self
.PadSize
1653 #(x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1)
1654 Points
+= (0, -self
.PadSize
)
1655 self
.Points
= Points
1656 self
.BoxWidth
= BoxWidth
1657 self
.BoxHeight
= BoxHeight
1658 self
.CalcBoundingBox()
1660 def CalcBoundingBox(self
):
1664 Calculates the Bounding Box
1668 w
, h
= self
.BoxWidth
, self
.BoxHeight
1669 x
, y
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world
=1)
1670 self
.BoundingBox
= BBox
.asBBox(((x
, y
-h
),(x
+ w
, y
)))
1672 def GetBoxRect(self
):
1673 wh
= (self
.BoxWidth
, self
.BoxHeight
)
1674 xy
= (self
.BoundingBox
[0,0], self
.BoundingBox
[1,1])
1678 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1679 xy
, wh
= self
.GetBoxRect()
1681 Points
= self
.Points
+ xy
1682 Points
= WorldToPixel(Points
)
1683 xy
= WorldToPixel(xy
)
1684 wh
= ScaleWorldToPixel(wh
) * (1,-1)
1686 # compute the font size:
1687 Size
= abs( ScaleWorldToPixel( (self
.Size
, self
.Size
) )[1] ) # only need a y coordinate length
1688 ## Check to see if the font size is large enough to blow up the X font server
1689 ## If so, limit it. Would it be better just to not draw it?
1690 ## note that this limit is dependent on how much memory you have, etc.
1691 Size
= min(Size
, self
.MaxFontSize
)
1693 self
.SetFont(Size
, self
.Family
, self
.Style
, self
.Weight
, self
.Underlined
, self
.FaceName
)
1694 dc
.SetFont(self
.Font
)
1695 dc
.SetTextForeground(self
.Color
)
1696 dc
.SetBackgroundMode(wx
.TRANSPARENT
)
1699 if (self
.LineStyle
and self
.LineColor
) or self
.BackgroundColor
:
1700 dc
.SetBrush(self
.Brush
)
1702 dc
.DrawRectanglePointSize(xy
, wh
)
1705 dc
.DrawTextList(self
.Words
, Points
)
1708 if HTdc
and self
.HitAble
:
1709 HTdc
.SetPen(self
.HitPen
)
1710 HTdc
.SetBrush(self
.HitBrush
)
1711 HTdc
.DrawRectanglePointSize(xy
, wh
)
1713 class Bitmap(TextObjectMixin
, DrawObject
, ):
1715 This class creates a bitmap object, placed at the coordinates,
1716 x,y. the "Position" argument is a two charactor string, indicating
1717 where in relation to the coordinates the bitmap should be oriented.
1719 The first letter is: t, c, or b, for top, center and bottom The
1720 second letter is: l, c, or r, for left, center and right The
1721 position refers to the position relative to the text itself. It
1722 defaults to "tl" (top left).
1724 The size is fixed, and does not scale with the drawing.
1728 def __init__(self
,Bitmap
,XY
,
1730 InForeground
= False):
1732 DrawObject
.__init
__(self
,InForeground
)
1734 if type(Bitmap
) == wx
._gdi
.Bitmap
:
1735 self
.Bitmap
= Bitmap
1736 elif type(Bitmap
) == wx
._core
.Image
:
1737 self
.Bitmap
= wx
.BitmapFromImage(Bitmap
)
1739 # Note the BB is just the point, as the size in World coordinates is not fixed
1740 self
.BoundingBox
= BBox
.asBBox( (XY
,XY
) )
1744 (self
.Width
, self
.Height
) = self
.Bitmap
.GetWidth(), self
.Bitmap
.GetHeight()
1745 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1747 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1748 XY
= WorldToPixel(self
.XY
)
1749 XY
= self
.ShiftFun(XY
[0], XY
[1], self
.Width
, self
.Height
)
1750 dc
.DrawBitmapPoint(self
.Bitmap
, XY
, True)
1751 if HTdc
and self
.HitAble
:
1752 HTdc
.SetPen(self
.HitPen
)
1753 HTdc
.SetBrush(self
.HitBrush
)
1754 HTdc
.DrawRectanglePointSize(XY
, (self
.Width
, self
.Height
) )
1756 class ScaledBitmap(TextObjectMixin
, DrawObject
, ):
1759 This class creates a bitmap object, placed at the coordinates, XY,
1760 of Height, H, in World coorsinates. The width is calculated from the
1761 aspect ratio of the bitmap.
1763 the "Position" argument is a two charactor string, indicating
1764 where in relation to the coordinates the bitmap should be oriented.
1766 The first letter is: t, c, or b, for top, center and bottom The
1767 second letter is: l, c, or r, for left, center and right The
1768 position refers to the position relative to the text itself. It
1769 defaults to "tl" (top left).
1771 The size scales with the drawing
1780 InForeground
= False):
1782 DrawObject
.__init
__(self
,InForeground
)
1784 if type(Bitmap
) == wx
._gdi
.Bitmap
:
1785 self
.Image
= Bitmap
.ConvertToImage()
1786 elif type(Bitmap
) == wx
._core
.Image
:
1790 self
.Height
= Height
1791 (self
.bmpWidth
, self
.bmpHeight
) = self
.Image
.GetWidth(), self
.Image
.GetHeight()
1792 self
.Width
= self
.bmpWidth
/ self
.bmpHeight
* Height
1793 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1794 self
.CalcBoundingBox()
1795 self
.ScaledBitmap
= None
1796 self
.ScaledHeight
= None
1798 def CalcBoundingBox(self
):
1799 ## this isn't exact, as fonts don't scale exactly.
1800 w
, h
= self
.Width
, self
.Height
1801 x
, y
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world
= 1)
1802 self
.BoundingBox
= BBox
.asBBox( ( (x
, y
-h
), (x
+ w
, y
) ) )
1805 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1806 XY
= WorldToPixel(self
.XY
)
1807 H
= ScaleWorldToPixel(self
.Height
)[0]
1808 W
= H
* (self
.bmpWidth
/ self
.bmpHeight
)
1809 if (self
.ScaledBitmap
is None) or (H
<> self
.ScaledHeight
) :
1810 self
.ScaledHeight
= H
1811 Img
= self
.Image
.Scale(W
, H
)
1812 self
.ScaledBitmap
= wx
.BitmapFromImage(Img
)
1814 XY
= self
.ShiftFun(XY
[0], XY
[1], W
, H
)
1815 dc
.DrawBitmapPoint(self
.ScaledBitmap
, XY
, True)
1816 if HTdc
and self
.HitAble
:
1817 HTdc
.SetPen(self
.HitPen
)
1818 HTdc
.SetBrush(self
.HitBrush
)
1819 HTdc
.DrawRectanglePointSize(XY
, (W
, H
) )
1821 class ScaledBitmap2(TextObjectMixin
, DrawObject
, ):
1824 An alternative scaled bitmap that only scaled the required amount of
1825 the main bitmap when zoomed in: EXPERIMENTAL!
1835 InForeground
= False):
1837 DrawObject
.__init
__(self
,InForeground
)
1839 if type(Bitmap
) == wx
._gdi
.Bitmap
:
1840 self
.Image
= Bitmap
.ConvertToImage()
1841 elif type(Bitmap
) == wx
._core
.Image
:
1844 self
.XY
= N
.array(XY
, N
.float)
1845 self
.Height
= Height
1846 (self
.bmpWidth
, self
.bmpHeight
) = self
.Image
.GetWidth(), self
.Image
.GetHeight()
1847 self
.bmpWH
= N
.array((self
.bmpWidth
, self
.bmpHeight
), N
.int32
)
1848 ## fixme: this should all accommodate different scales for X and Y
1850 self
.BmpScale
= float(self
.bmpHeight
) / Height
1851 self
.Width
= self
.bmpWidth
/ self
.BmpScale
1852 self
.WH
= N
.array((self
.Width
, Height
), N
.float)
1853 ##fixme: should this have a y = -1 to shift to y-up?
1854 self
.BmpScale
= self
.bmpWH
/ self
.WH
1856 print "bmpWH:", self
.bmpWH
1857 print "Width, Height:", self
.WH
1858 print "self.BmpScale", self
.BmpScale
1859 self
.ShiftFun
= self
.ShiftFunDict
[Position
]
1860 self
.CalcBoundingBox()
1861 self
.ScaledBitmap
= None # cache of the last existing scaled bitmap
1863 def CalcBoundingBox(self
):
1864 ## this isn't exact, as fonts don't scale exactly.
1865 w
,h
= self
.Width
, self
.Height
1866 x
, y
= self
.ShiftFun(self
.XY
[0], self
.XY
[1], w
, h
, world
= 1)
1867 self
.BoundingBox
= BBox
.asBBox( ((x
, y
-h
), (x
+ w
, y
)) )
1869 def WorldToBitmap(self
, Pw
):
1871 computes bitmap coords from World coords
1873 delta
= Pw
- self
.XY
1874 Pb
= delta
* self
.BmpScale
1875 Pb
*= (1, -1) ##fixme: this may only works for Yup projection!
1876 ## and may only work for top left position
1878 return Pb
.astype(N
.int_
)
1880 def _DrawEntireBitmap(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
):
1882 this is pretty much the old code
1884 Scales and Draws the entire bitmap.
1887 XY
= WorldToPixel(self
.XY
)
1888 H
= ScaleWorldToPixel(self
.Height
)[0]
1889 W
= H
* (self
.bmpWidth
/ self
.bmpHeight
)
1890 if (self
.ScaledBitmap
is None) or (self
.ScaledBitmap
[0] != (0, 0, self
.bmpWidth
, self
.bmpHeight
, W
, H
) ):
1891 #if True: #fixme: (self.ScaledBitmap is None) or (H <> self.ScaledHeight) :
1892 self
.ScaledHeight
= H
1893 print "Scaling to:", W
, H
1894 Img
= self
.Image
.Scale(W
, H
)
1895 bmp
= wx
.BitmapFromImage(Img
)
1896 self
.ScaledBitmap
= ((0, 0, self
.bmpWidth
, self
.bmpHeight
, W
, H
), bmp
)# this defines the cached bitmap
1898 print "Using Cached bitmap"
1899 bmp
= self
.ScaledBitmap
[1]
1900 XY
= self
.ShiftFun(XY
[0], XY
[1], W
, H
)
1901 dc
.DrawBitmapPoint(bmp
, XY
, True)
1902 if HTdc
and self
.HitAble
:
1903 HTdc
.SetPen(self
.HitPen
)
1904 HTdc
.SetBrush(self
.HitBrush
)
1905 HTdc
.DrawRectanglePointSize(XY
, (W
, H
) )
1907 def _DrawSubBitmap(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
):
1909 Subsets just the part of the bitmap that is visible
1910 then scales and draws that.
1913 BBworld
= BBox
.asBBox(self
._Canvas
.ViewPortBB
)
1914 BBbitmap
= BBox
.fromPoints(self
.WorldToBitmap(BBworld
))
1916 XYs
= WorldToPixel(self
.XY
)
1917 # figure out subimage:
1918 # fixme: this should be able to be done more succinctly!
1920 if BBbitmap
[0,0] < 0:
1922 elif BBbitmap
[0,0] > self
.bmpWH
[0]: # off the bitmap
1926 XYs
[0] = 0 # draw at origin
1928 if BBbitmap
[0,1] < 0:
1930 elif BBbitmap
[0,1] > self
.bmpWH
[1]: # off the bitmap
1935 XYs
[1] = 0 # draw at origin
1937 if BBbitmap
[1,0] < 0:
1938 #off the screen -- This should never happen!
1940 elif BBbitmap
[1,0] > self
.bmpWH
[0]:
1941 Wb
= self
.bmpWH
[0] - Xb
1943 Wb
= BBbitmap
[1,0] - Xb
1945 if BBbitmap
[1,1] < 0:
1946 # off the screen -- This should never happen!
1949 elif BBbitmap
[1,1] > self
.bmpWH
[1]:
1950 Hb
= self
.bmpWH
[1] - Yb
1952 Hb
= BBbitmap
[1,1] - Yb
1954 FullHeight
= ScaleWorldToPixel(self
.Height
)[0]
1955 scale
= FullHeight
/ self
.bmpWH
[1]
1956 Ws
= int(scale
* Wb
+ 0.5) # add the 0.5 to round
1957 Hs
= int(scale
* Hb
+ 0.5)
1958 if (self
.ScaledBitmap
is None) or (self
.ScaledBitmap
[0] != (Xb
, Yb
, Wb
, Hb
, Ws
, Ws
) ):
1959 Img
= self
.Image
.GetSubImage(wx
.Rect(Xb
, Yb
, Wb
, Hb
))
1961 bmp
= wx
.BitmapFromImage(Img
)
1962 self
.ScaledBitmap
= ((Xb
, Yb
, Wb
, Hb
, Ws
, Ws
), bmp
)# this defines the cached bitmap
1963 #XY = self.ShiftFun(XY[0], XY[1], W, H)
1964 #fixme: get the shiftfun working!
1966 print "Using cached bitmap"
1967 ##fixme: The cached bitmap could be used if the one needed is the same scale, but
1968 ## a subset of the cached one.
1969 bmp
= self
.ScaledBitmap
[1]
1970 dc
.DrawBitmapPoint(bmp
, XYs
, True)
1972 if HTdc
and self
.HitAble
:
1973 HTdc
.SetPen(self
.HitPen
)
1974 HTdc
.SetBrush(self
.HitBrush
)
1975 HTdc
.DrawRectanglePointSize(XYs
, (Ws
, Hs
) )
1977 def _Draw(self
, dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
=None):
1978 BBworld
= BBox
.asBBox(self
._Canvas
.ViewPortBB
)
1979 ## first see if entire bitmap is displayed:
1980 if BBworld
.Inside(self
.BoundingBox
):
1981 print "Drawing entire bitmap with old code"
1982 self
._DrawEntireBitmap
(dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
)
1984 elif BBworld
.Overlaps(self
.BoundingBox
):
1985 #BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld))
1986 print "Drawing a sub-bitmap"
1987 self
._DrawSubBitmap
(dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
)
1989 print "Not Drawing -- no part of image is showing"
1993 An example of a Grid Object -- it is set on teh FloatCAnvas with one of:
1995 FloatCanvas.GridUnder = Grid
1996 FloatCanvas.GridOver = Grid
1998 It will be drawn every time, regardless of the viewport.
2000 In its _Draw method, it computes what to draw, given the ViewPortBB
2001 of the Canvas it's being drawn on.
2004 def __init__(self
, Spacing
, Size
= 2, Color
= "Black", Cross
=False, CrossThickness
= 1):
2006 self
.Spacing
= N
.array(Spacing
, N
.float)
2007 self
.Spacing
.shape
= (2,)
2011 self
.CrossThickness
= CrossThickness
2013 def CalcPoints(self
, Canvas
):
2014 ViewPortBB
= Canvas
.ViewPortBB
2016 Spacing
= self
.Spacing
2018 minx
, miny
= N
.floor(ViewPortBB
[0] / Spacing
) * Spacing
2019 maxx
, maxy
= N
.ceil(ViewPortBB
[1] / Spacing
) * Spacing
2021 ##fixme: this could use vstack or something with numpy
2022 x
= N
.arange(minx
, maxx
+Spacing
[0], Spacing
[0]) # making sure to get the last point
2023 y
= N
.arange(miny
, maxy
+Spacing
[1], Spacing
[1]) # an extra is OK
2024 Points
= N
.zeros((len(y
), len(x
), 2), N
.float)
2029 Points
.shape
= (-1,2)
2033 def _Draw(self
, dc
, Canvas
):
2034 Points
= self
.CalcPoints(Canvas
)
2036 Points
= Canvas
.WorldToPixel(Points
)
2038 dc
.SetPen(wx
.Pen(self
.Color
,self
.CrossThickness
))
2040 if self
.Cross
: # Use cross shaped markers
2042 LinePoints
= N
.concatenate((Points
+ (self
.Size
,0),Points
+ (-self
.Size
,0)),1)
2043 dc
.DrawLineList(LinePoints
)
2045 LinePoints
= N
.concatenate((Points
+ (0,self
.Size
),Points
+ (0,-self
.Size
)),1)
2046 dc
.DrawLineList(LinePoints
)
2049 ## Note: this code borrowed from Pointset -- itreally shouldn't be repeated here!.
2051 dc
.DrawPointList(Points
)
2052 elif self
.Size
<= 2:
2053 dc
.DrawPointList(Points
+ (0,-1))
2054 dc
.DrawPointList(Points
+ (0, 1))
2055 dc
.DrawPointList(Points
+ (1, 0))
2056 dc
.DrawPointList(Points
+ (-1,0))
2058 dc
.SetBrush(wx
.Brush(self
.Color
))
2059 radius
= int(round(self
.Size
/2))
2060 ##fixme: I really should add a DrawCircleList to wxPython
2061 if len(Points
) > 100:
2063 xywh
= N
.concatenate((xy
-radius
, N
.ones(xy
.shape
) * self
.Size
), 1 )
2064 dc
.DrawEllipseList(xywh
)
2067 dc
.DrawCircle(xy
[0],xy
[1], radius
)
2071 #---------------------------------------------------------------------------
2072 class FloatCanvas(wx
.Panel
):
2076 This is a high level window for drawing maps and anything else in an
2077 arbitrary coordinate system.
2079 The goal is to provide a convenient way to draw stuff on the screen
2080 without having to deal with handling OnPaint events, converting to pixel
2081 coordinates, knowing about wxWindows brushes, pens, and colors, etc. It
2082 also provides virtually unlimited zooming and scrolling
2084 I am using it for two things:
2085 1) general purpose drawing in floating point coordinates
2086 2) displaying map data in Lat-long coordinates
2088 If the projection is set to None, it will draw in general purpose
2089 floating point coordinates. If the projection is set to 'FlatEarth', it
2090 will draw a FlatEarth projection, centered on the part of the map that
2091 you are viewing. You can also pass in your own projection function.
2093 It is double buffered, so re-draws after the window is uncovered by something
2094 else are very quick.
2096 It relies on NumPy, which is needed for speed (maybe, I havn't profiled it)
2098 Bugs and Limitations:
2099 Lots: patches, fixes welcome
2101 For Map drawing: It ignores the fact that the world is, in fact, a
2102 sphere, so it will do strange things if you are looking at stuff near
2103 the poles or the date line. so far I don't have a need to do that, so I
2104 havn't bothered to add any checks for that yet.
2107 I have set no zoom limits. What this means is that if you zoom in really
2108 far, you can get integer overflows, and get wierd results. It
2109 doesn't seem to actually cause any problems other than wierd output, at
2110 least when I have run it.
2113 I have done a couple of things to improve speed in this app. The one
2114 thing I have done is used NumPy Arrays to store the coordinates of the
2115 points of the objects. This allowed me to use array oriented functions
2116 when doing transformations, and should provide some speed improvement
2117 for objects with a lot of points (big polygons, polylines, pointsets).
2119 The real slowdown comes when you have to draw a lot of objects, because
2120 you have to call the wx.DC.DrawSomething call each time. This is plenty
2121 fast for tens of objects, OK for hundreds of objects, but pretty darn
2122 slow for thousands of objects.
2124 The solution is to be able to pass some sort of object set to the DC
2125 directly. I've used DC.DrawPointList(Points), and it helped a lot with
2126 drawing lots of points. I havn't got a LineSet type object, so I havn't
2127 used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList()
2128 methods implimented, and then I'd also have a full set of Object sets
2129 that could take advantage of them. I hope to get to it some day.
2133 At this point, there are a full set of custom mouse events. They are
2134 just like the regular mouse events, but include an extra attribute:
2135 Event.GetCoords(), that returns the (x,y) position in world
2136 coordinates, as a length-2 NumPy vector of Floats.
2138 Copyright: Christopher Barker
2140 License: Same as the version of wxPython you are using it with
2142 Please let me know if you're using this!!!
2146 Chris.Barker@noaa.gov
2150 def __init__(self
, parent
, id = -1,
2151 size
= wx
.DefaultSize
,
2152 ProjectionFun
= None,
2153 BackgroundColor
= "WHITE",
2156 wx
.Panel
.__init
__( self
, parent
, id, wx
.DefaultPosition
, size
)
2158 self
.ComputeFontScale()
2161 self
.BackgroundBrush
= wx
.Brush(BackgroundColor
,wx
.SOLID
)
2165 wx
.EVT_PAINT(self
, self
.OnPaint
)
2166 wx
.EVT_SIZE(self
, self
.OnSize
)
2168 wx
.EVT_LEFT_DOWN(self
, self
.LeftDownEvent
)
2169 wx
.EVT_LEFT_UP(self
, self
.LeftUpEvent
)
2170 wx
.EVT_LEFT_DCLICK(self
, self
.LeftDoubleClickEvent
)
2171 wx
.EVT_MIDDLE_DOWN(self
, self
.MiddleDownEvent
)
2172 wx
.EVT_MIDDLE_UP(self
, self
.MiddleUpEvent
)
2173 wx
.EVT_MIDDLE_DCLICK(self
, self
.MiddleDoubleClickEvent
)
2174 wx
.EVT_RIGHT_DOWN(self
, self
.RightDownEvent
)
2175 wx
.EVT_RIGHT_UP(self
, self
.RightUpEvent
)
2176 wx
.EVT_RIGHT_DCLICK(self
, self
.RightDoubleCLickEvent
)
2177 wx
.EVT_MOTION(self
, self
.MotionEvent
)
2178 wx
.EVT_MOUSEWHEEL(self
, self
.WheelEvent
)
2180 ## CHB: I'm leaving these out for now.
2181 #wx.EVT_ENTER_WINDOW(self, self. )
2182 #wx.EVT_LEAVE_WINDOW(self, self. )
2184 self
.SetProjectionFun(ProjectionFun
)
2187 # timer to give a delay when re-sizing so that buffers aren't re-built too many times.
2188 self
.SizeTimer
= wx
.PyTimer(self
.OnSizeTimer
)
2190 self
.InitializePanel()
2191 self
.MakeNewBuffers()
2193 # self.CreateCursors()
2195 def ComputeFontScale(self
):
2196 ## A global variable to hold the scaling from pixel size to point size.
2199 dc
.SetFont(wx
.Font(16, wx
.ROMAN
, wx
.NORMAL
, wx
.NORMAL
))
2200 E
= dc
.GetTextExtent("X")
2206 InitAll() sets everything in the Canvas to default state.
2208 It can be used to reset the Canvas
2212 self
.HitColorGenerator
= None
2213 self
.UseHitTest
= False
2215 self
.NumBetweenBlits
= 500
2217 ## create the Hit Test Dicts:
2222 self
._ForeDrawList
= []
2223 self
._ForegroundBuffer
= None
2224 self
.BoundingBox
= None
2225 self
.BoundingBoxDirty
= False
2226 self
.MinScale
= None
2227 self
.MaxScale
= None
2228 self
.ViewPortCenter
= N
.array( (0,0), N
.float)
2230 self
.SetProjectionFun(None)
2232 self
.MapProjectionVector
= N
.array( (1,1), N
.float) # No Projection to start!
2233 self
.TransformVector
= N
.array( (1,-1), N
.float) # default Transformation
2236 self
.ObjectUnderMouse
= None
2238 self
.GridUnder
= None
2239 self
.GridOver
= None
2241 self
._BackgroundDirty
= True
2243 def SetProjectionFun(self
,ProjectionFun
):
2244 if ProjectionFun
== 'FlatEarth':
2245 self
.ProjectionFun
= self
.FlatEarthProjection
2246 elif callable(ProjectionFun
):
2247 self
.ProjectionFun
= ProjectionFun
2248 elif ProjectionFun
is None:
2249 self
.ProjectionFun
= lambda x
=None: N
.array( (1,1), N
.float)
2251 raise FloatCanvasError('Projectionfun must be either:'
2252 ' "FlatEarth", None, or a callable object '
2253 '(function, for instance) that takes the '
2254 'ViewPortCenter and returns a MapProjectionVector')
2256 def FlatEarthProjection(self
, CenterPoint
):
2257 MaxLatitude
= 75 # these were determined essentially arbitrarily
2259 Lat
= min(CenterPoint
[1],MaxLatitude
)
2260 Lat
= max(Lat
,MinLatitude
)
2261 return N
.array((N
.cos(N
.pi
*Lat
/180),1),N
.float)
2263 def SetMode(self
, Mode
):
2265 Set the GUImode to any of the availble mode.
2269 #self.GUIMode.SetCursor()
2270 self
.SetCursor(self
.GUIMode
.Cursor
)
2272 def MakeHitDict(self
):
2273 ##fixme: Should this just be None if nothing has been bound?
2274 self
.HitDict
= {EVT_FC_LEFT_DOWN: {}
,
2276 EVT_FC_LEFT_DCLICK
: {},
2277 EVT_FC_MIDDLE_DOWN
: {},
2278 EVT_FC_MIDDLE_UP
: {},
2279 EVT_FC_MIDDLE_DCLICK
: {},
2280 EVT_FC_RIGHT_DOWN
: {},
2281 EVT_FC_RIGHT_UP
: {},
2282 EVT_FC_RIGHT_DCLICK
: {},
2283 EVT_FC_ENTER_OBJECT
: {},
2284 EVT_FC_LEAVE_OBJECT
: {},
2287 def _RaiseMouseEvent(self
, Event
, EventType
):
2289 This is called in various other places to raise a Mouse Event
2291 pt
= self
.PixelToWorld( Event
.GetPosition() )
2292 evt
= _MouseEvent(EventType
, Event
, self
.GetId(), pt
)
2293 self
.GetEventHandler().ProcessEvent(evt
)
2295 if wx
.__version
__ >= "2.8":
2296 HitTestBitmapDepth
= 32
2297 #print "Using hit test code for 2.8"
2298 def GetHitTestColor(self
, xy
):
2299 if self
._ForegroundHTBitmap
:
2300 pdata
= wx
.AlphaPixelData(self
._ForegroundHTBitmap
)
2302 pdata
= wx
.AlphaPixelData(self
._HTBitmap
)
2304 raise RuntimeError("Trouble Accessing Hit Test bitmap")
2305 pacc
= pdata
.GetPixels()
2306 pacc
.MoveTo(pdata
, xy
[0], xy
[1])
2307 return pacc
.Get()[:3]
2309 HitTestBitmapDepth
= 24
2310 #print "using pre-2.8 hit test code"
2311 def GetHitTestColor(self
, xy
):
2313 if self
._ForegroundHTBitmap
:
2314 dc
.SelectObject(self
._ForegroundHTBitmap
)
2316 dc
.SelectObject(self
._HTBitmap
)
2317 hitcolor
= dc
.GetPixelPoint( xy
)
2318 return hitcolor
.Get()
2320 def HitTest(self
, event
, HitEvent
):
2322 # check if there are any objects in the dict for this event
2323 if self
.HitDict
[ HitEvent
]:
2324 xy
= event
.GetPosition()
2325 color
= self
.GetHitTestColor( xy
)
2326 if color
in self
.HitDict
[ HitEvent
]:
2327 Object
= self
.HitDict
[ HitEvent
][color
]
2328 ## Add the hit coords to the Object
2329 Object
.HitCoords
= self
.PixelToWorld( xy
)
2330 Object
.HitCoordsPixel
= xy
2331 Object
.CallBackFuncs
[HitEvent
](Object
)
2335 def MouseOverTest(self
, event
):
2336 ##fixme: Can this be cleaned up?
2337 if (self
.HitDict
and
2339 (self
.HitDict
[EVT_FC_ENTER_OBJECT
] or
2340 self
.HitDict
[EVT_FC_LEAVE_OBJECT
] )
2342 xy
= event
.GetPosition()
2343 color
= self
.GetHitTestColor( xy
)
2344 OldObject
= self
.ObjectUnderMouse
2345 ObjectCallbackCalled
= False
2346 if color
in self
.HitDict
[ EVT_FC_ENTER_OBJECT
]:
2347 Object
= self
.HitDict
[ EVT_FC_ENTER_OBJECT
][color
]
2348 if (OldObject
is None):
2350 Object
.CallBackFuncs
[EVT_FC_ENTER_OBJECT
](Object
)
2351 ObjectCallbackCalled
= True
2353 pass # this means the enter event isn't bound for that object
2354 elif OldObject
== Object
: # the mouse is still on the same object
2356 ## Is the mouse on a differnt object as it was...
2357 elif not (Object
== OldObject
):
2358 # call the leave object callback
2360 OldObject
.CallBackFuncs
[EVT_FC_LEAVE_OBJECT
](OldObject
)
2361 ObjectCallbackCalled
= True
2363 pass # this means the leave event isn't bound for that object
2365 Object
.CallBackFuncs
[EVT_FC_ENTER_OBJECT
](Object
)
2366 ObjectCallbackCalled
= True
2368 pass # this means the enter event isn't bound for that object
2369 ## set the new object under mouse
2370 self
.ObjectUnderMouse
= Object
2371 elif color
in self
.HitDict
[ EVT_FC_LEAVE_OBJECT
]:
2372 Object
= self
.HitDict
[ EVT_FC_LEAVE_OBJECT
][color
]
2373 self
.ObjectUnderMouse
= Object
2375 # no objects under mouse bound to mouse-over events
2376 self
.ObjectUnderMouse
= None
2379 OldObject
.CallBackFuncs
[EVT_FC_LEAVE_OBJECT
](OldObject
)
2380 ObjectCallbackCalled
= True
2382 pass # this means the leave event isn't bound for that object
2383 return ObjectCallbackCalled
2386 ## fixme: There is a lot of repeated code here
2387 ## Is there a better way?
2388 def LeftDoubleClickEvent(self
, event
):
2390 self
.GUIMode
.OnLeftDouble(event
)
2393 def MiddleDownEvent(self
, event
):
2395 self
.GUIMode
.OnMiddleDown(event
)
2398 def MiddleUpEvent(self
, event
):
2400 self
.GUIMode
.OnMiddleUp(event
)
2403 def MiddleDoubleClickEvent(self
, event
):
2405 self
.GUIMode
.OnMiddleDouble(event
)
2408 def RightDoubleCLickEvent(self
, event
):
2410 self
.GUIMode
.OnRightDouble(event
)
2413 def WheelEvent(self
, event
):
2415 self
.GUIMode
.OnWheel(event
)
2418 def LeftDownEvent(self
, event
):
2420 self
.GUIMode
.OnLeftDown(event
)
2423 def LeftUpEvent(self
, event
):
2424 if self
.HasCapture():
2427 self
.GUIMode
.OnLeftUp(event
)
2430 def MotionEvent(self
, event
):
2432 self
.GUIMode
.OnMove(event
)
2435 def RightDownEvent(self
, event
):
2437 self
.GUIMode
.OnRightDown(event
)
2440 def RightUpEvent(self
, event
):
2442 self
.GUIMode
.OnRightUp(event
)
2445 def MakeNewBuffers(self
):
2446 self
._BackgroundDirty
= True
2447 # Make new offscreen bitmap:
2448 self
._Buffer
= wx
.EmptyBitmap(*self
.PanelSize
)
2449 if self
._ForeDrawList
:
2450 self
._ForegroundBuffer
= wx
.EmptyBitmap(*self
.PanelSize
)
2452 self
.MakeNewHTBitmap()
2454 self
._ForegroundHTBitmap
= None
2456 self
._ForegroundBuffer
= None
2457 self
._ForegroundHTBitmap
= None
2460 self
.MakeNewHTBitmap()
2462 self
._HTBitmap
= None
2463 self
._ForegroundHTBitmap
= None
2465 def MakeNewHTBitmap(self
):
2467 Off screen Bitmap used for Hit tests on background objects
2470 self
._HTBitmap
= wx
.EmptyBitmap(self
.PanelSize
[0],
2474 depth
=self
.HitTestBitmapDepth
)
2476 def MakeNewForegroundHTBitmap(self
):
2477 ## Note: the foreground and backround HT bitmaps are in separate functions
2478 ## so that they can be created separate --i.e. when a foreground is
2479 ## added after the backgound is drawn
2481 Off screen Bitmap used for Hit tests on foreground objects
2484 self
._ForegroundHTBitmap
= wx
.EmptyBitmap(self
.PanelSize
[0],
2488 depth
=self
.HitTestBitmapDepth
)
2490 def OnSize(self
, event
=None):
2491 self
.InitializePanel()
2492 self
.SizeTimer
.Start(50, oneShot
=True)
2494 def OnSizeTimer(self
, event
=None):
2495 self
.MakeNewBuffers()
2498 def InitializePanel(self
):
2499 self
.PanelSize
= self
.GetClientSizeTuple()
2500 if self
.PanelSize
== (0,0):
2501 ## OS-X sometimes gives a Size event when the panel is size (0,0)
2502 self
.PanelSize
= (2,2)
2503 self
.PanelSize
= N
.array(self
.PanelSize
, N
.int32
)
2504 self
.HalfPanelSize
= self
.PanelSize
/ 2 # lrk: added for speed in WorldToPixel
2505 if self
.PanelSize
[0] == 0 or self
.PanelSize
[1] == 0:
2506 self
.AspectRatio
= 1.0
2508 self
.AspectRatio
= float(self
.PanelSize
[0]) / self
.PanelSize
[1]
2510 def OnPaint(self
, event
):
2511 dc
= wx
.PaintDC(self
)
2512 if self
._ForegroundBuffer
:
2513 dc
.DrawBitmap(self
._ForegroundBuffer
,0,0)
2515 dc
.DrawBitmap(self
._Buffer
,0,0)
2517 def Draw(self
, Force
=False):
2520 Canvas.Draw(Force=False)
2522 Re-draws the canvas.
2524 Note that the buffer will not be re-drawn unless something has
2525 changed. If you change a DrawObject directly, then the canvas
2526 will not know anything has changed. In this case, you can force
2527 a re-draw by passing int True for the Force flag:
2529 Canvas.Draw(Force=True)
2531 There is a main buffer set up to double buffer the screen, so
2532 you can get quick re-draws when the window gets uncovered.
2534 If there are any objects in self._ForeDrawList, then the
2535 background gets drawn to a new buffer, and the foreground
2536 objects get drawn on top of it. The final result if blitted to
2537 the screen, and stored for future Paint events. This is done so
2538 that you can have a complicated background, but have something
2539 changing on the foreground, without having to wait for the
2540 background to get re-drawn. This can be used to support simple
2541 animation, for instance.
2545 if N
.sometrue(self
.PanelSize
<= 2 ):
2546 # it's possible for this to get called before being properly initialized.
2548 if self
.Debug
: start
= clock()
2549 ScreenDC
= wx
.ClientDC(self
)
2550 ViewPortWorld
= N
.array(( self
.PixelToWorld((0,0)),
2551 self
.PixelToWorld(self
.PanelSize
) )
2553 self
.ViewPortBB
= N
.array( ( N
.minimum
.reduce(ViewPortWorld
),
2554 N
.maximum
.reduce(ViewPortWorld
) ) )
2555 #self.ViewPortWorld = ViewPortWorld
2558 dc
.SelectObject(self
._Buffer
)
2559 if self
._BackgroundDirty
or Force
:
2560 dc
.SetBackground(self
.BackgroundBrush
)
2562 if self
._HTBitmap
is not None:
2563 HTdc
= wx
.MemoryDC()
2564 HTdc
.SelectObject(self
._HTBitmap
)
2568 if self
.GridUnder
is not None:
2569 self
.GridUnder
._Draw
(dc
, self
)
2570 self
._DrawObjects
(dc
, self
._DrawList
, ScreenDC
, self
.ViewPortBB
, HTdc
)
2571 self
._BackgroundDirty
= False
2574 if self
._ForeDrawList
:
2575 ## If an object was just added to the Foreground, there might not yet be a buffer
2576 if self
._ForegroundBuffer
is None:
2577 self
._ForegroundBuffer
= wx
.EmptyBitmap(self
.PanelSize
[0],
2580 dc
= wx
.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here
2581 dc
.SelectObject(self
._ForegroundBuffer
)
2582 dc
.DrawBitmap(self
._Buffer
,0,0)
2583 if self
._ForegroundHTBitmap
is not None:
2584 ForegroundHTdc
= wx
.MemoryDC()
2585 ForegroundHTdc
.SelectObject( self
._ForegroundHTBitmap
)
2586 ForegroundHTdc
.Clear()
2587 if self
._HTBitmap
is not None:
2588 #Draw the background HT buffer to the foreground HT buffer
2589 ForegroundHTdc
.DrawBitmap(self
._HTBitmap
, 0, 0)
2591 ForegroundHTdc
= None
2592 self
._DrawObjects
(dc
,
2597 if self
.GridOver
is not None:
2598 self
.GridOver
._Draw
(dc
, self
)
2599 ScreenDC
.Blit(0, 0, self
.PanelSize
[0],self
.PanelSize
[1], dc
, 0, 0)
2600 # If the canvas is in the middle of a zoom or move,
2601 # the Rubber Band box needs to be re-drawn
2602 ##fixme: maybe GUIModes should never be None, and rather have a Do-nothing GUI-Mode.
2603 if self
.GUIMode
is not None:
2604 self
.GUIMode
.UpdateScreen()
2606 if self
.Debug
: print "Drawing took %f seconds of CPU time"%(clock()-start
)
2608 ## Clear the font cache. If you don't do this, the X font server
2609 ## starts to take up Massive amounts of memory This is mostly a
2610 ## problem with very large fonts, that you get with scaled text
2612 DrawObject
.FontList
= {}
2614 def _ShouldRedraw(DrawList
, ViewPortBB
): # lrk: adapted code from BBCheck
2615 # lrk: Returns the objects that should be redrawn
2617 ## fixme: should this check be moved into the object?
2618 ## also: a BB object would make this cleaner too
2621 for Object
in DrawList
:
2622 BB1
= Object
.BoundingBox
2623 ## note: this could use the Utilities.BBCheck function
2624 ## butthis saves a function call
2625 if (BB1
[1,0] > BB2
[0,0] and BB1
[0,0] < BB2
[1,0] and
2626 BB1
[1,1] > BB2
[0,1] and BB1
[0,1] < BB2
[1,1]):
2627 redrawlist
.append(Object
)
2629 ##fixme: disabled this!!!!
2631 _ShouldRedraw
= staticmethod(_ShouldRedraw
)
2633 def MoveImage(self
,shift
,CoordType
):
2635 move the image in the window.
2637 shift is an (x,y) tuple, specifying the amount to shift in each direction
2639 It can be in any of three coordinates: Panel, Pixel, World,
2640 specified by the CoordType parameter
2642 Panel coordinates means you want to shift the image by some
2643 fraction of the size of the displaed image
2645 Pixel coordinates means you want to shift the image by some number of pixels
2647 World coordinates mean you want to shift the image by an amount
2648 in Floating point world coordinates
2651 shift
= N
.asarray(shift
,N
.float)
2652 if CoordType
== 'Panel':# convert from panel coordinates
2653 shift
= shift
* N
.array((-1,1),N
.float) *self
.PanelSize
/self
.TransformVector
2654 elif CoordType
== 'Pixel': # convert from pixel coordinates
2655 shift
= shift
/self
.TransformVector
2656 elif CoordType
== 'World': # No conversion
2659 raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"')
2661 self
.ViewPortCenter
= self
.ViewPortCenter
+ shift
2662 self
.MapProjectionVector
= self
.ProjectionFun(self
.ViewPortCenter
)
2663 self
.TransformVector
= N
.array((self
.Scale
,-self
.Scale
),N
.float) * self
.MapProjectionVector
2664 self
._BackgroundDirty
= True
2667 def Zoom(self
, factor
, center
= None, centerCoords
="world"):
2670 Zoom(factor, center) changes the amount of zoom of the image by factor.
2671 If factor is greater than one, the image gets larger.
2672 If factor is less than one, the image gets smaller.
2674 center is a tuple of (x,y) coordinates of the center of the viewport, after zooming.
2675 If center is not given, the center will stay the same.
2677 centerCoords is a flag indicating whether the center given is in pixel or world
2678 coords. Options are: "world" or "pixel"
2681 self
.Scale
= self
.Scale
*factor
2682 if not center
is None:
2683 if centerCoords
== "pixel":
2684 center
= self
.PixelToWorld( center
)
2686 center
= N
.array(center
,N
.float)
2687 self
.ViewPortCenter
= center
2688 self
.SetToNewScale()
2690 def ZoomToBB(self
, NewBB
=None, DrawFlag
=True):
2694 Zooms the image to the bounding box given, or to the bounding
2695 box of all the objects on the canvas, if none is given.
2699 if NewBB
is not None:
2702 if self
.BoundingBoxDirty
:
2703 self
._ResetBoundingBox
()
2704 BoundingBox
= self
.BoundingBox
2705 if BoundingBox
is not None:
2706 self
.ViewPortCenter
= N
.array(((BoundingBox
[0,0]+BoundingBox
[1,0])/2,
2707 (BoundingBox
[0,1]+BoundingBox
[1,1])/2 ),N
.float_
)
2708 self
.MapProjectionVector
= self
.ProjectionFun(self
.ViewPortCenter
)
2709 # Compute the new Scale
2710 BoundingBox
= BoundingBox
*self
.MapProjectionVector
# this does need to make a copy!
2712 self
.Scale
= min(abs(self
.PanelSize
[0] / (BoundingBox
[1,0]-BoundingBox
[0,0])),
2713 abs(self
.PanelSize
[1] / (BoundingBox
[1,1]-BoundingBox
[0,1])) )*0.95
2714 except ZeroDivisionError: # this will happen if the BB has zero width or height
2716 self
.Scale
= (self
.PanelSize
[0] / (BoundingBox
[1,0]-BoundingBox
[0,0]))*0.95
2717 except ZeroDivisionError:
2719 self
.Scale
= (self
.PanelSize
[1] / (BoundingBox
[1,1]-BoundingBox
[0,1]))*0.95
2720 except ZeroDivisionError: #zero size! (must be a single point)
2724 self
._BackgroundDirty
= True
2726 # Reset the shifting and scaling to defaults when there is no BB
2727 self
.ViewPortCenter
= N
.array( (0,0), N
.float)
2729 self
.SetToNewScale(DrawFlag
=DrawFlag
)
2731 def SetToNewScale(self
, DrawFlag
=True):
2733 if self
.MinScale
is not None:
2734 Scale
= max(Scale
, self
.MinScale
)
2735 if self
.MaxScale
is not None:
2736 Scale
= min(Scale
, self
.MaxScale
)
2737 self
.MapProjectionVector
= self
.ProjectionFun(self
.ViewPortCenter
)
2738 self
.TransformVector
= N
.array((Scale
,-Scale
),N
.float) * self
.MapProjectionVector
2740 self
._BackgroundDirty
= True
2744 def RemoveObjects(self
, Objects
):
2745 for Object
in Objects
:
2746 self
.RemoveObject(Object
, ResetBB
=False)
2747 self
.BoundingBoxDirty
= True
2749 def RemoveObject(self
, Object
, ResetBB
= True):
2750 ##fixme: Using the list.remove method is kind of slow
2751 if Object
.InForeground
:
2752 self
._ForeDrawList
.remove(Object
)
2753 if not self
._ForeDrawList
:
2754 self
._ForegroundBuffer
= None
2755 self
._ForegroundHTdc
= None
2757 self
._DrawList
.remove(Object
)
2758 self
._BackgroundDirty
= True
2760 self
.BoundingBoxDirty
= True
2762 def ClearAll(self
, ResetBB
=True):
2764 ClearAll(ResetBB=True)
2766 Removes all DrawObjects from the Canvas
2768 If ResetBB is set to False, the original bounding box will remain
2772 self
._ForeDrawList
= []
2773 self
._BackgroundDirty
= True
2774 self
.HitColorGenerator
= None
2775 self
.UseHitTest
= False
2777 self
._ResetBoundingBox
()
2778 self
.MakeNewBuffers()
2781 def _ResetBoundingBox(self
):
2782 if self
._DrawList
or self
._ForeDrawList
:
2784 for (i
, obj
) in enumerate(self
._DrawList
):
2785 bblist
.append(obj
.BoundingBox
)
2786 for (j
, obj
) in enumerate(self
._ForeDrawList
):
2787 bblist
.append(obj
.BoundingBox
)
2788 self
.BoundingBox
= BBox
.fromBBArray(bblist
)
2790 self
.BoundingBox
= None
2791 self
.ViewPortCenter
= N
.array( (0,0), N
.float)
2792 self
.TransformVector
= N
.array( (1,-1), N
.float)
2793 self
.MapProjectionVector
= N
.array( (1,1), N
.float)
2795 self
.BoundingBoxDirty
= False
2797 def PixelToWorld(self
, Points
):
2799 Converts coordinates from Pixel coordinates to world coordinates.
2801 Points is a tuple of (x,y) coordinates, or a list of such tuples,
2802 or a NX2 Numpy array of x,y coordinates.
2805 return (((N
.asarray(Points
, N
.float) -
2806 (self
.PanelSize
/2))/self
.TransformVector
) +
2807 self
.ViewPortCenter
)
2809 def WorldToPixel(self
,Coordinates
):
2811 This function will get passed to the drawing functions of the objects,
2812 to transform from world to pixel coordinates.
2813 Coordinates should be a NX2 array of (x,y) coordinates, or
2814 a 2-tuple, or sequence of 2-tuples.
2816 #Note: this can be called by users code for various reasons, so N.asarray is needed.
2817 return (((N
.asarray(Coordinates
,N
.float) -
2818 self
.ViewPortCenter
)*self
.TransformVector
)+
2819 (self
.HalfPanelSize
)).astype('i')
2821 def ScaleWorldToPixel(self
,Lengths
):
2823 This function will get passed to the drawing functions of the objects,
2824 to Change a length from world to pixel coordinates.
2826 Lengths should be a NX2 array of (x,y) coordinates, or
2827 a 2-tuple, or sequence of 2-tuples.
2829 return ( (N
.asarray(Lengths
, N
.float)*self
.TransformVector
) ).astype('i')
2831 def ScalePixelToWorld(self
,Lengths
):
2833 This function computes a pair of x.y lengths,
2834 to change then from pixel to world coordinates.
2836 Lengths should be a NX2 array of (x,y) coordinates, or
2837 a 2-tuple, or sequence of 2-tuples.
2840 return (N
.asarray(Lengths
,N
.float) / self
.TransformVector
)
2842 def AddObject(self
, obj
):
2843 # put in a reference to the Canvas, so remove and other stuff can work
2845 if obj
.InForeground
:
2846 self
._ForeDrawList
.append(obj
)
2847 self
.UseForeground
= True
2849 self
._DrawList
.append(obj
)
2850 self
._BackgroundDirty
= True
2851 self
.BoundingBoxDirty
= True
2854 def AddObjects(self
, Objects
):
2855 for Object
in Objects
:
2856 self
.AddObject(Object
)
2858 def _DrawObjects(self
, dc
, DrawList
, ScreenDC
, ViewPortBB
, HTdc
= None):
2860 This is a convenience function;
2861 This function takes the list of objects and draws them to specified
2864 dc
.SetBackground(self
.BackgroundBrush
)
2867 PanelSize0
, PanelSize1
= self
.PanelSize
# for speed
2868 WorldToPixel
= self
.WorldToPixel
# for speed
2869 ScaleWorldToPixel
= self
.ScaleWorldToPixel
# for speed
2870 Blit
= ScreenDC
.Blit
# for speed
2871 NumBetweenBlits
= self
.NumBetweenBlits
# for speed
2872 for i
, Object
in enumerate(self
._ShouldRedraw
(DrawList
, ViewPortBB
)):
2874 Object
._Draw
(dc
, WorldToPixel
, ScaleWorldToPixel
, HTdc
)
2875 if (i
+1) % NumBetweenBlits
== 0:
2876 Blit(0, 0, PanelSize0
, PanelSize1
, dc
, 0, 0)
2879 def SaveAsImage(self
, filename
, ImageType
=wx
.BITMAP_TYPE_PNG
):
2882 Saves the current image as an image file. The default is in the
2883 PNG format. Other formats can be specified using the wx flags:
2890 etc. (see the wx docs for the complete list)
2894 self
._Buffer
.SaveFile(filename
, ImageType
)
2897 def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__
2898 classnames
= ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon",
2899 "Line", "Text", "PointSet","Point", "Arrow", "ArrowLine", "ScaledTextBox",
2900 "SquarePoint","Bitmap", "ScaledBitmap", "Spline", "Group"]
2901 for classname
in classnames
:
2902 klass
= globals()[classname
]
2903 def getaddshapemethod(klass
=klass
):
2904 def addshape(self
, *args
, **kwargs
):
2905 Object
= klass(*args
, **kwargs
)
2906 self
.AddObject(Object
)
2909 addshapemethod
= getaddshapemethod()
2910 methodname
= "Add" + classname
2911 setattr(FloatCanvas
, methodname
, addshapemethod
)
2912 docstring
= "Creates %s and adds its reference to the canvas.\n" % classname
2913 docstring
+= "Argument protocol same as %s class" % classname
2915 docstring
+= ", whose docstring is:\n%s" % klass
.__doc
__
2916 FloatCanvas
.__dict
__[methodname
].__doc
__ = docstring
2918 _makeFloatCanvasAddMethods()