]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/floatcanvas/FloatCanvas.py
Remove const bool
[wxWidgets.git] / wxPython / wx / lib / floatcanvas / FloatCanvas.py
1
2 from __future__ import division
3
4 try:
5 import numpy as N
6 except ImportError:
7 raise ImportError("I could not import numpy")
8
9 from time import clock
10 import wx
11
12 from Utilities import BBox
13
14
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.
18 global FontScale
19
20 ## Custom Exceptions:
21
22 class FloatCanvasError(Exception):
23 pass
24
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()
43
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)
58
59 class _MouseEvent(wx.PyCommandEvent):
60
61 """!
62
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
66
67 The goal is to be able to it just like a regular mouse event.
68
69 It adds the method:
70
71 GetCoords() , which returns and (x,y) tuple in world coordinates.
72
73 Another difference is that it is a CommandEvent, which propagates up
74 the window hierarchy until it is handled.
75
76 """
77
78 def __init__(self, EventType, NativeEvent, WinID, Coords = None):
79 wx.PyCommandEvent.__init__(self)
80
81 self.SetEventType( EventType )
82 self._NativeEvent = NativeEvent
83 self.Coords = Coords
84
85 def GetCoords(self):
86 return self.Coords
87
88 def __getattr__(self, name):
89 return getattr(self._NativeEvent, name)
90
91 def _cycleidxs(indexcount, maxvalue, step):
92
93 """!
94 Utility function used by _colorGenerator
95
96 """
97
98 if indexcount == 0:
99 yield ()
100 else:
101 for idx in xrange(0, maxvalue, step):
102 for tail in _cycleidxs(indexcount - 1, maxvalue, step):
103 yield (idx, ) + tail
104
105 def _colorGenerator():
106
107 """!
108
109 Generates a series of unique colors used to do hit-tests with the Hit
110 Test bitmap
111 """
112
113 depth = wx.GetDisplayDepth()
114 ## ##there have been problems with 16 bbp displays, to I'm disabling this for now.
115 ## if depth == 16:
116 ## print "Warning: There have been problems with hit-testing on 16bbp displays"
117 ## step = 8
118 if depth >= 24:
119 step = 1
120 else:
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)
128
129 class DrawObject:
130 """!
131 This is the base class for all the objects that can be drawn.
132
133 One must subclass from this (and an assortment of Mixins) to create
134 a new DrawObject.
135
136 \note This class contain a series of static dictionaries:
137
138 * BrushList
139 * PenList
140 * FillStyleList
141 * LineStyleList
142
143 Is this still necessary?
144
145 """
146
147 def __init__(self, InForeground = False, IsVisible = True):
148 """! \param InForeground (bool)
149 \param IsVisible (Bool)
150 """
151 self.InForeground = InForeground
152
153 self._Canvas = None
154
155 self.HitColor = None
156 self.CallBackFuncs = {}
157
158 ## these are the defaults
159 self.HitAble = False
160 self.HitLine = True
161 self.HitFill = True
162 self.MinHitLineWidth = 3
163 self.HitLineWidth = 3 ## this gets re-set by the subclasses if necessary
164
165 self.Brush = None
166 self.Pen = None
167
168 self.FillStyle = "Solid"
169
170 self.Visible = IsVisible
171
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!
176
177 BrushList = {
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
188 }
189 PenList = {
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
199 }
200
201 FillStyleList = {
202 "Transparent" : wx.TRANSPARENT,
203 "Solid" : wx.SOLID,
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
210 }
211
212 LineStyleList = {
213 "Solid" : wx.SOLID,
214 "Transparent": wx.TRANSPARENT,
215 "Dot" : wx.DOT,
216 "LongDash" : wx.LONG_DASH,
217 "ShortDash" : wx.SHORT_DASH,
218 "DotDash" : wx.DOT_DASH,
219 }
220
221 # def BBFromPoints(self, Points):
222 # """!
223 # Calculates a Bounding box from a set of points (NX2 array of coordinates)
224 # \param Points (array?)
225 # """
226 #
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) ),
231 # # )
232 # return BBox.fromPoints(Points)
233
234 def Bind(self, Event, CallBackFun):
235 self.CallBackFuncs[Event] = CallBackFun
236 self.HitAble = True
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
253
254 def UnBindAll(self):
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():
258 try:
259 List.remove(self)
260 except ValueError:
261 pass
262 self.HitAble = False
263
264
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"
270 else:
271 self.Brush = self.BrushList.setdefault( (FillColor,FillStyle), wx.Brush(FillColor,self.FillStyleList[FillStyle] ) )
272
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'
277 else:
278 self.Pen = self.PenList.setdefault( (LineColor,LineStyle,LineWidth), wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) )
279
280 def SetHitBrush(self,HitColor):
281 if not self.HitFill:
282 self.HitBrush = wx.TRANSPARENT_BRUSH
283 else:
284 self.HitBrush = self.BrushList.setdefault( (HitColor,"solid"), wx.Brush(HitColor,self.FillStyleList["Solid"] ) )
285
286 def SetHitPen(self,HitColor,LineWidth):
287 if not self.HitLine:
288 self.HitPen = wx.TRANSPARENT_PEN
289 else:
290 self.HitPen = self.PenList.setdefault( (HitColor, "solid", self.HitLineWidth), wx.Pen(HitColor, self.HitLineWidth, self.LineStyleList["Solid"]) )
291
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):
295 pass
296 def SetLineColor(self, LineColor):
297 pass
298 def SetLineStyle(self, LineStyle):
299 pass
300 def SetLineWidth(self, LineWidth):
301 pass
302 def SetFillColor(self, FillColor):
303 pass
304 def SetFillStyle(self, FillStyle):
305 pass
306
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
313
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
320
321 def Hide(self):
322 """! \brief Make an object hidden.
323 """
324 self.Visible = False
325
326 def Show(self):
327 """! \brief Make an object visible on the canvas.
328 """
329 self.Visible = True
330
331 class Group(DrawObject):
332 """
333 A group of other FloatCanvas Objects
334
335 Not all DrawObject methods may apply here. In particular, you can't Bind events to a group.
336
337 Note that if an object is in more than one group, it will get drawn more than once.
338
339 """
340
341 def __init__(self, ObjectList=[], InForeground = False, IsVisible = True):
342 self.ObjectList = list(ObjectList)
343 DrawObject.__init__(self, InForeground, IsVisible)
344 self.CalcBoundingBox()
345
346 def AddObject(self, obj):
347 self.ObjectList.append(obj)
348 self.BoundingBox.Merge(obj.BoundingBox)
349
350 def AddObjects(self, Objects):
351 for o in Objects:
352 self.AddObject(o)
353
354 def CalcBoundingBox(self):
355 if self.ObjectList:
356 BB = BBox.asBBox(self.ObjectList[0].BoundingBox)
357 for obj in self.ObjectList[1:]:
358 BB.Merge(obj.BoundingBox)
359 else:
360 BB = None
361 self.BoundingBox = BB
362
363 def SetColor(self, Color):
364 for o in self.ObjectList:
365 o.SetColor(Color)
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)
384
385
386 class ColorOnlyMixin:
387 """
388
389 Mixin class for objects that have just one color, rather than a fill
390 color and line color
391
392 """
393
394 def SetColor(self, Color):
395 self.SetPen(Color,"Solid",1)
396 self.SetBrush(Color,"Solid")
397
398 SetFillColor = SetColor # Just to provide a consistant interface
399
400 class LineOnlyMixin:
401 """
402
403 Mixin class for objects that have just one color, rather than a fill
404 color and line color
405
406 """
407
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
412
413 def SetLineStyle(self, LineStyle):
414 self.LineStyle = LineStyle
415 self.SetPen(self.LineColor,LineStyle,self.LineWidth)
416
417 def SetLineWidth(self, LineWidth):
418 self.LineWidth = LineWidth
419 self.SetPen(self.LineColor,self.LineStyle,LineWidth)
420
421 class LineAndFillMixin(LineOnlyMixin):
422 """
423
424 Mixin class for objects that have both a line and a fill color and
425 style.
426
427 """
428 def SetFillColor(self, FillColor):
429 self.FillColor = FillColor
430 self.SetBrush(FillColor, self.FillStyle)
431
432 def SetFillStyle(self, FillStyle):
433 self.FillStyle = FillStyle
434 self.SetBrush(self.FillColor,FillStyle)
435
436 def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
437 dc.SetPen(self.Pen)
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) )
444
445 class XYObjectMixin:
446 """
447
448 This is a mixin class that provides some methods suitable for use
449 with objects that have a single (x,y) coordinate pair.
450
451 """
452
453 def Move(self, Delta ):
454 """
455
456 Move(Delta): moves the object by delta, where delta is a
457 (dx,dy) pair. Ideally a Numpy array of shape (2,)
458
459 """
460
461 Delta = N.asarray(Delta, N.float)
462 self.XY += Delta
463 self.BoundingBox += Delta
464
465 if self._Canvas:
466 self._Canvas.BoundingBoxDirty = True
467
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))
472
473 def SetPoint(self, xy):
474 xy = N.array(xy, N.float)
475 xy.shape = (2,)
476
477 self.XY = xy
478 self.CalcBoundingBox()
479
480 if self._Canvas:
481 self._Canvas.BoundingBoxDirty = True
482
483 class PointsObjectMixin:
484 """
485
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.
488
489 """
490
491 def Move(self, Delta):
492 """
493 Move(Delta): moves the object by delta, where delta is an (dx,
494 dy) pair. Ideally a Numpy array of shape (2,)
495 """
496
497 Delta = N.asarray(Delta, N.float)
498 Delta.shape = (2,)
499 self.Points += Delta
500 self.BoundingBox += Delta
501 if self._Canvas:
502 self._Canvas.BoundingBoxDirty = True
503
504 def CalcBoundingBox(self):
505 self.BoundingBox = BBox.fromPoints(self.Points)
506 if self._Canvas:
507 self._Canvas.BoundingBoxDirty = True
508
509 def SetPoints(self, Points, copy = True):
510 """
511 Sets the coordinates of the points of the object to Points (NX2 array).
512
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.
516
517 For example:
518
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
522
523 """
524 if copy:
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
527 else:
528 self.Points = N.asarray(Points, N.float)
529 self.CalcBoundingBox()
530
531
532 class Polygon(PointsObjectMixin, LineAndFillMixin, DrawObject):
533
534 """
535
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
540 arrays.
541
542 The other parameters specify various properties of the Polygon, and
543 should be self explanatory.
544
545 """
546 def __init__(self,
547 Points,
548 LineColor = "Black",
549 LineStyle = "Solid",
550 LineWidth = 1,
551 FillColor = None,
552 FillStyle = "Solid",
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()
557
558 self.LineColor = LineColor
559 self.LineStyle = LineStyle
560 self.LineWidth = LineWidth
561 self.FillColor = FillColor
562 self.FillStyle = FillStyle
563
564 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
565
566 self.SetPen(LineColor,LineStyle,LineWidth)
567 self.SetBrush(FillColor,FillStyle)
568
569 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None):
570 Points = WorldToPixel(self.Points)#.tolist()
571 dc.SetPen(self.Pen)
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)
578
579 class Line(PointsObjectMixin, LineOnlyMixin, DrawObject,):
580 """
581
582 The Line class takes a list of 2-tuples, or a NX2 NumPy Float array
583 of point coordinates.
584
585 It will draw a straight line if there are two points, and a polyline
586 if there are more than two.
587
588 """
589 def __init__(self,Points,
590 LineColor = "Black",
591 LineStyle = "Solid",
592 LineWidth = 1,
593 InForeground = False):
594 DrawObject.__init__(self, InForeground)
595
596
597 self.Points = N.array(Points,N.float)
598 self.CalcBoundingBox()
599
600 self.LineColor = LineColor
601 self.LineStyle = LineStyle
602 self.LineWidth = LineWidth
603
604 self.SetPen(LineColor,LineStyle,LineWidth)
605
606 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
607
608
609 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
610 Points = WorldToPixel(self.Points)
611 dc.SetPen(self.Pen)
612 dc.DrawLines(Points)
613 if HTdc and self.HitAble:
614 HTdc.SetPen(self.HitPen)
615 HTdc.DrawLines(Points)
616
617 class Spline(Line):
618 def __init__(self, *args, **kwargs):
619 Line.__init__(self, *args, **kwargs)
620
621 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
622 Points = WorldToPixel(self.Points)
623 dc.SetPen(self.Pen)
624 dc.DrawSpline(Points)
625 if HTdc and self.HitAble:
626 HTdc.SetPen(self.HitPen)
627 HTdc.DrawSpline(Points)
628
629
630 class Arrow(XYObjectMixin, LineOnlyMixin, DrawObject):
631 """
632
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
637 LineColor = "Black",
638 LineStyle = "Solid",
639 LineWidth = 1,
640 ArrowHeadSize = 4, # size of arrowhead in pixels
641 ArrowHeadAngle = 45, # angle of arrow head in degrees
642 InForeground = False):
643
644 It will draw an arrow , starting at the point, (X,Y) pointing in
645 direction, theta.
646
647
648 """
649 def __init__(self,
650 XY,
651 Length,
652 Direction,
653 LineColor = "Black",
654 LineStyle = "Solid",
655 LineWidth = 2, # pixels
656 ArrowHeadSize = 8, # pixels
657 ArrowHeadAngle = 30, # degrees
658 InForeground = False):
659
660 DrawObject.__init__(self, InForeground)
661
662 self.XY = N.array(XY, N.float)
663 self.XY.shape = (2,) # Make sure it is a length 2 vector
664 self.Length = Length
665 self.Direction = float(Direction)
666 self.ArrowHeadSize = ArrowHeadSize
667 self.ArrowHeadAngle = float(ArrowHeadAngle)
668
669 self.CalcArrowPoints()
670 self.CalcBoundingBox()
671
672 self.LineColor = LineColor
673 self.LineStyle = LineStyle
674 self.LineWidth = LineWidth
675
676 self.SetPen(LineColor,LineStyle,LineWidth)
677
678 ##fixme: How should the HitTest be drawn?
679 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
680
681 def SetDirection(self, Direction):
682 self.Direction = float(Direction)
683 self.CalcArrowPoints()
684
685 def SetLength(self, Length):
686 self.Length = Length
687 self.CalcArrowPoints()
688
689 def SetLengthDirection(self, Length, Direction):
690 self.Direction = float(Direction)
691 self.Length = Length
692 self.CalcArrowPoints()
693
694 ## def CalcArrowPoints(self):
695 ## L = self.Length
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) ) ),
701 ## N.float )
702 ## RotationMatrix = N.array( ( ( N.cos(theta), -N.sin(theta) ),
703 ## ( N.sin(theta), N.cos(theta) ) ),
704 ## N.float
705 ## )
706 ## ArrowPoints = N.matrixmultiply(RotationMatrix, ArrowPoints)
707 ## self.ArrowPoints = N.transpose(ArrowPoints)
708
709 def CalcArrowPoints(self):
710 L = self.Length
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),
715 (0,0),
716 (N.cos(theta - phi), -N.sin(theta - phi) ),
717 (0,0),
718 (N.cos(theta + phi), -N.sin(theta + phi) ),
719 ), N.float )
720 AP *= S
721 shift = (-L*N.cos(theta), L*N.sin(theta) )
722 AP[1:,:] += shift
723 self.ArrowPoints = AP
724
725 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
726 dc.SetPen(self.Pen)
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)
733
734
735 class ArrowLine(PointsObjectMixin, LineOnlyMixin, DrawObject):
736 """
737
738 ArrowLine(Points, # coords of points
739 LineColor = "Black",
740 LineStyle = "Solid",
741 LineWidth = 1,
742 ArrowHeadSize = 4, # in pixels
743 ArrowHeadAngle = 45,
744 InForeground = False):
745
746 It will draw a set of arrows from point to point.
747
748 It takes a list of 2-tuples, or a NX2 NumPy Float array
749 of point coordinates.
750
751
752 """
753
754 def __init__(self,
755 Points,
756 LineColor = "Black",
757 LineStyle = "Solid",
758 LineWidth = 1, # pixels
759 ArrowHeadSize = 8, # pixels
760 ArrowHeadAngle = 30, # degrees
761 InForeground = False):
762
763 DrawObject.__init__(self, InForeground)
764
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)
769
770 self.CalcArrowPoints()
771 self.CalcBoundingBox()
772
773 self.LineColor = LineColor
774 self.LineStyle = LineStyle
775 self.LineWidth = LineWidth
776
777 self.SetPen(LineColor,LineStyle,LineWidth)
778
779 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
780
781 def CalcArrowPoints(self):
782 S = self.ArrowHeadSize
783 phi = self.ArrowHeadAngle * N.pi / 360
784 Points = self.Points
785 n = Points.shape[0]
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)
790 AP = N.array( (
791 (N.cos(theta - phi), -N.sin(theta-phi)),
792 (0,0),
793 (N.cos(theta + phi), -N.sin(theta + phi))
794 ),
795 N.float )
796 self.ArrowPoints[i,:,:] = AP
797 self.ArrowPoints *= S
798
799 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
800 Points = WorldToPixel(self.Points)
801 ArrowPoints = Points[1:,N.newaxis,:] + self.ArrowPoints
802 dc.SetPen(self.Pen)
803 dc.DrawLines(Points)
804 for arrow in ArrowPoints:
805 dc.DrawLines(arrow)
806 if HTdc and self.HitAble:
807 HTdc.SetPen(self.HitPen)
808 HTdc.DrawLines(Points)
809 for arrow in ArrowPoints:
810 HTdc.DrawLines(arrow)
811
812
813 class PointSet(PointsObjectMixin, ColorOnlyMixin, DrawObject):
814 """
815
816 The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of
817 point coordinates.
818
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.
821
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.
824
825 Each point will be drawn the same color and Diameter. The Diameter
826 is in screen pixels, not world coordinates.
827
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
831
832 In the case of points, the HitLineWidth is used as diameter.
833
834 """
835 def __init__(self, Points, Color = "Black", Diameter = 1, InForeground = False):
836 DrawObject.__init__(self,InForeground)
837
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
842
843 self.HitLineWidth = min(self.MinHitLineWidth, Diameter)
844 self.SetColor(Color)
845
846 def SetDiameter(self,Diameter):
847 self.Diameter = Diameter
848
849 def FindClosestPoint(self, XY):
850 """
851
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.
855
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.
859
860 """
861 d = self.Points - XY
862 return N.argmin(N.hypot(d[:,0],d[:,1]))
863
864
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))
871
872 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
873 dc.SetPen(self.Pen)
874 Points = WorldToPixel(self.Points)
875 if self.Diameter <= 1:
876 dc.DrawPointList(Points)
877 elif self.Diameter <= 2:
878 self.DrawD2(dc, Points)
879 else:
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:
884 xy = Points
885 xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Diameter ), 1 )
886 dc.DrawEllipseList(xywh)
887 else:
888 for xy in Points:
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)
897 else:
898 if len(Points) > 100:
899 xy = Points
900 xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Diameter ), 1 )
901 HTdc.DrawEllipseList(xywh)
902 else:
903 for xy in Points:
904 HTdc.DrawCircle(xy[0],xy[1], radius)
905
906 class Point(XYObjectMixin, ColorOnlyMixin, DrawObject):
907 """
908
909 The Point class takes a 2-tuple, or a (2,) NumPy array of point
910 coordinates.
911
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.
914
915 The HitLineWidth is used as diameter for the
916 Hit Test.
917
918 """
919 def __init__(self, XY, Color = "Black", Diameter = 1, InForeground = False):
920 DrawObject.__init__(self, InForeground)
921
922 self.XY = N.array(XY, N.float)
923 self.XY.shape = (2,) # Make sure it is a length 2 vector
924 self.CalcBoundingBox()
925 self.SetColor(Color)
926 self.Diameter = Diameter
927
928 self.HitLineWidth = self.MinHitLineWidth
929
930 def SetDiameter(self,Diameter):
931 self.Diameter = Diameter
932
933
934 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
935 dc.SetPen(self.Pen)
936 xy = WorldToPixel(self.XY)
937 if self.Diameter <= 1:
938 dc.DrawPoint(xy[0], xy[1])
939 else:
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])
947 else:
948 HTdc.SetBrush(self.HitBrush)
949 HTdc.DrawCircle(xy[0],xy[1], radius)
950
951 class SquarePoint(XYObjectMixin, ColorOnlyMixin, DrawObject):
952 """
953
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
956
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.
959
960 The HitLineWidth is used as diameter for the
961 Hit Test.
962
963 """
964 def __init__(self, Point, Color = "Black", Size = 4, InForeground = False):
965 DrawObject.__init__(self, InForeground)
966
967 self.XY = N.array(Point, N.float)
968 self.XY.shape = (2,) # Make sure it is a length 2 vector
969 self.CalcBoundingBox()
970 self.SetColor(Color)
971 self.Size = Size
972
973 self.HitLineWidth = self.MinHitLineWidth
974
975 def SetSize(self,Size):
976 self.Size = Size
977
978 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
979 Size = self.Size
980 dc.SetPen(self.Pen)
981 xc,yc = WorldToPixel(self.XY)
982
983 if self.Size <= 1:
984 dc.DrawPoint(xc, yc)
985 else:
986 x = xc - Size/2.0
987 y = yc - Size/2.0
988 dc.SetBrush(self.Brush)
989 dc.DrawRectangle(x, y, Size, Size)
990 if HTdc and self.HitAble:
991 HTdc.SetPen(self.HitPen)
992 if self.Size <= 1:
993 HTdc.DrawPoint(xc, xc)
994 else:
995 HTdc.SetBrush(self.HitBrush)
996 HTdc.DrawRectangle(x, y, Size, Size)
997
998 class RectEllipse(XYObjectMixin, LineAndFillMixin, DrawObject):
999 def __init__(self, XY, WH,
1000 LineColor = "Black",
1001 LineStyle = "Solid",
1002 LineWidth = 1,
1003 FillColor = None,
1004 FillStyle = "Solid",
1005 InForeground = False):
1006
1007 DrawObject.__init__(self,InForeground)
1008
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
1015
1016 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
1017
1018 self.SetPen(LineColor,LineStyle,LineWidth)
1019 self.SetBrush(FillColor,FillStyle)
1020
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()
1027
1028
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)
1033 if self._Canvas:
1034 self._Canvas.BoundingBoxDirty = True
1035
1036
1037 class Rectangle(RectEllipse):
1038
1039 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1040 ( XY, WH ) = self.SetUpDraw(dc,
1041 WorldToPixel,
1042 ScaleWorldToPixel,
1043 HTdc)
1044 dc.DrawRectanglePointSize(XY, WH)
1045 if HTdc and self.HitAble:
1046 HTdc.DrawRectanglePointSize(XY, WH)
1047
1048
1049
1050 class Ellipse(RectEllipse):
1051
1052 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1053 ( XY, WH ) = self.SetUpDraw(dc,
1054 WorldToPixel,
1055 ScaleWorldToPixel,
1056 HTdc)
1057 dc.DrawEllipsePointSize(XY, WH)
1058 if HTdc and self.HitAble:
1059 HTdc.DrawEllipsePointSize(XY, WH)
1060
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),
1069 **kwargs)
1070
1071 def SetDiameter(self, Diameter):
1072 Diameter = float(Diameter)
1073 XY = self.Center - (Diameter/2.0)
1074 self.SetShape(XY,
1075 (Diameter, Diameter)
1076 )
1077
1078 class TextObjectMixin(XYObjectMixin):
1079 """
1080
1081 A mix in class that holds attributes and methods that are needed by
1082 the Text objects
1083
1084 """
1085
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.
1089
1090 FontList = {}
1091
1092 LayoutFontSize = 16 # font size used for calculating layout
1093
1094 def SetFont(self, Size, Family, Style, Weight, Underlined, FaceName):
1095 self.Font = self.FontList.setdefault( (Size,
1096 Family,
1097 Style,
1098 Weight,
1099 Underlined,
1100 FaceName),
1101 #wx.FontFromPixelSize((0.45*Size,Size), # this seemed to give a decent height/width ratio on Windows
1102 wx.Font(Size,
1103 Family,
1104 Style,
1105 Weight,
1106 Underlined,
1107 FaceName) )
1108
1109 def SetColor(self, Color):
1110 self.Color = Color
1111
1112 def SetBackgroundColor(self, BackgroundColor):
1113 self.BackgroundColor = BackgroundColor
1114
1115 def SetText(self, String):
1116 """
1117 Re-sets the text displayed by the object
1118
1119 In the case of the ScaledTextBox, it will re-do the layout as appropriate
1120
1121 Note: only tested with the ScaledTextBox
1122
1123 """
1124
1125 self.String = String
1126 self.LayoutText()
1127
1128 def LayoutText(self):
1129 """
1130 A dummy method to re-do the layout of the text.
1131
1132 A derived object needs to override this if required.
1133
1134 """
1135 pass
1136
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)}
1151
1152 class Text(TextObjectMixin, DrawObject, ):
1153 """
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.
1157
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).
1162
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
1165 72 PPI.
1166
1167 Family:
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.
1177 Style:
1178 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1179 Weight:
1180 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1181 Underlined:
1182 The value can be True or False. At present this may have an an
1183 effect on Windows only.
1184
1185 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1186 above will be ignored.
1187
1188 The size is fixed, and does not scale with the drawing.
1189
1190 The hit-test is done on the entire text extent
1191
1192 """
1193
1194 def __init__(self,String, xy,
1195 Size = 14,
1196 Color = "Black",
1197 BackgroundColor = None,
1198 Family = wx.MODERN,
1199 Style = wx.NORMAL,
1200 Weight = wx.NORMAL,
1201 Underlined = False,
1202 Position = 'tl',
1203 InForeground = False,
1204 Font = None):
1205
1206 DrawObject.__init__(self,InForeground)
1207
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
1212
1213 self.Color = Color
1214 self.BackgroundColor = BackgroundColor
1215
1216 if not Font:
1217 FaceName = ''
1218 else:
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)
1226
1227 self.BoundingBox = BBox.asBBox((xy, xy))
1228
1229 self.XY = N.asarray(xy)
1230 self.XY.shape = (2,)
1231
1232 (self.TextWidth, self.TextHeight) = (None, None)
1233 self.ShiftFun = self.ShiftFunDict[Position]
1234
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)
1242 else:
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) )
1252
1253 class ScaledText(TextObjectMixin, DrawObject, ):
1254 """
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.
1259
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).
1264
1265 Size is the size of the font in world coordinates.
1266
1267 Family:
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.
1277 Style:
1278 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1279 Weight:
1280 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1281 Underlined:
1282 The value can be True or False. At present this may have an an
1283 effect on Windows only.
1284
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.
1288
1289 The size will scale as the drawing is zoomed.
1290
1291 Bugs/Limitations:
1292
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
1295 pretty darn close.
1296
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.
1304
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.
1307
1308 """
1309
1310 def __init__(self,
1311 String,
1312 XY,
1313 Size,
1314 Color = "Black",
1315 BackgroundColor = None,
1316 Family = wx.MODERN,
1317 Style = wx.NORMAL,
1318 Weight = wx.NORMAL,
1319 Underlined = False,
1320 Position = 'tl',
1321 Font = None,
1322 InForeground = False):
1323
1324 DrawObject.__init__(self,InForeground)
1325
1326 self.String = String
1327 self.XY = N.array( XY, N.float)
1328 self.XY.shape = (2,)
1329 self.Size = Size
1330 self.Color = Color
1331 self.BackgroundColor = BackgroundColor
1332 self.Family = Family
1333 self.Style = Style
1334 self.Weight = Weight
1335 self.Underlined = Underlined
1336 if not Font:
1337 self.FaceName = ''
1338 else:
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()
1344
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
1347 # message:
1348 #
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.
1352 #
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
1356
1357 self.ShiftFun = self.ShiftFunDict[Position]
1358
1359 self.CalcBoundingBox()
1360
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()
1365
1366 def CalcBoundingBox(self):
1367 ## this isn't exact, as fonts don't scale exactly.
1368 dc = wx.MemoryDC()
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)
1376 w = w * ScaleFactor
1377 h = h * ScaleFactor
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)))
1380
1381 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1382 (X,Y) = WorldToPixel( (self.XY) )
1383
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)
1396 else:
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)
1403
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) )
1409
1410 class ScaledTextBox(TextObjectMixin, DrawObject):
1411 """
1412 This class creates a TextBox object that is scaled when zoomed. It is
1413 placed at the coordinates, x,y.
1414
1415 If the Width parameter is defined, the text will be wrapped to the width given.
1416
1417 A Box can be drawn around the text, be specifying:
1418 LineWidth and/or FillColor
1419
1420 A space(margin) can be put all the way around the text, be specifying:
1421 the PadSize argument in world coordinates.
1422
1423 The spacing between lines can be adjusted with the:
1424 LineSpacing argument.
1425
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).
1432
1433 Size is the size of the font in world coordinates.
1434
1435 Family:
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.
1445 Style:
1446 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1447 Weight:
1448 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1449 Underlined:
1450 The value can be True or False. At present this may have an an
1451 effect on Windows only.
1452
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.
1456
1457 The size will scale as the drawing is zoomed.
1458
1459 Bugs/Limitations:
1460
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
1463 pretty darn close.
1464
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.
1472
1473 The hit-test is done on the entire box. This could be made
1474 optional, but I haven't gotten around to it.
1475
1476 """
1477
1478 def __init__(self, String,
1479 Point,
1480 Size,
1481 Color = "Black",
1482 BackgroundColor = None,
1483 LineColor = 'Black',
1484 LineStyle = 'Solid',
1485 LineWidth = 1,
1486 Width = None,
1487 PadSize = None,
1488 Family = wx.MODERN,
1489 Style = wx.NORMAL,
1490 Weight = wx.NORMAL,
1491 Underlined = False,
1492 Position = 'tl',
1493 Alignment = "left",
1494 Font = None,
1495 LineSpacing = 1.0,
1496 InForeground = False):
1497
1498 DrawObject.__init__(self,InForeground)
1499
1500 self.XY = N.array(Point, N.float)
1501 self.Size = Size
1502 self.Color = Color
1503 self.BackgroundColor = BackgroundColor
1504 self.LineColor = LineColor
1505 self.LineStyle = LineStyle
1506 self.LineWidth = LineWidth
1507 self.Width = Width
1508 if PadSize is None: # the default is just a little bit of padding
1509 self.PadSize = Size/10.0
1510 else:
1511 self.PadSize = float(PadSize)
1512 self.Family = Family
1513 self.Style = Style
1514 self.Weight = Weight
1515 self.Underlined = Underlined
1516 self.Alignment = Alignment.lower()
1517 self.LineSpacing = float(LineSpacing)
1518 self.Position = Position
1519
1520 if not Font:
1521 self.FaceName = ''
1522 else:
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()
1528
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
1531 # message:
1532 #
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.
1536 #
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!
1539
1540 self.MaxFontSize = 1000
1541 self.ShiftFun = self.ShiftFunDict[Position]
1542
1543 self.String = String
1544 self.LayoutText()
1545 self.CalcBoundingBox()
1546
1547 self.SetPen(LineColor,LineStyle,LineWidth)
1548 self.SetBrush(BackgroundColor, "Solid")
1549
1550
1551 def WrapToWidth(self):
1552 dc = wx.MemoryDC()
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)
1560 NewStrings = []
1561 for s in self.Strings:
1562 #beginning = True
1563 text = s.split(" ")
1564 text.reverse()
1565 LineLength = 0
1566 NewText = text[-1]
1567 del text[-1]
1568 while text:
1569 w = dc.GetTextExtent(' ' + text[-1])[0]
1570 if LineLength + w <= Width:
1571 NewText += ' '
1572 NewText += text[-1]
1573 LineLength = dc.GetTextExtent(NewText)[0]
1574 else:
1575 NewStrings.append(NewText)
1576 NewText = text[-1]
1577 LineLength = dc.GetTextExtent(text[-1])[0]
1578 del text[-1]
1579 NewStrings.append(NewText)
1580 self.Strings = NewStrings
1581
1582 def ReWrap(self, Width):
1583 self.Width = Width
1584 self.LayoutText()
1585
1586 def LayoutText(self):
1587 """
1588
1589 Calculates the positions of the words of text.
1590
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.
1595
1596 """
1597 self.Strings = self.String.split("\n")
1598 if self.Width:
1599 self.WrapToWidth()
1600
1601 dc = wx.MemoryDC()
1602 bitmap = wx.EmptyBitmap(1, 1)
1603 dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
1604
1605 DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to.
1606 ScaleFactor = float(self.Size) / DrawingSize
1607
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
1613
1614 LineWidths = N.zeros((len(self.Strings),), N.float)
1615 y = 0
1616 Words = []
1617 AllLinePoints = []
1618
1619 for i, s in enumerate(self.Strings):
1620 LineWidths[i] = 0
1621 LineWords = s.split(" ")
1622 LinePoints = N.zeros((len(LineWords),2), N.float)
1623 for j, word in enumerate(LineWords):
1624 if j > 0:
1625 LineWidths[i] += SpaceWidth
1626 Words.append(word)
1627 LinePoints[j] = (LineWidths[i], y)
1628 w = dc.GetTextExtent(word)[0]
1629 LineWidths[i] += w
1630 y -= LineHeight
1631 AllLinePoints.append(LinePoints)
1632 TextWidth = N.maximum.reduce(LineWidths)
1633 self.Words = Words
1634
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)
1640
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))
1651
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()
1659
1660 def CalcBoundingBox(self):
1661
1662 """
1663
1664 Calculates the Bounding Box
1665
1666 """
1667
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)))
1671
1672 def GetBoxRect(self):
1673 wh = (self.BoxWidth, self.BoxHeight)
1674 xy = (self.BoundingBox[0,0], self.BoundingBox[1,1])
1675
1676 return (xy, wh)
1677
1678 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1679 xy, wh = self.GetBoxRect()
1680
1681 Points = self.Points + xy
1682 Points = WorldToPixel(Points)
1683 xy = WorldToPixel(xy)
1684 wh = ScaleWorldToPixel(wh) * (1,-1)
1685
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)
1692
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)
1697
1698 # Draw The Box
1699 if (self.LineStyle and self.LineColor) or self.BackgroundColor:
1700 dc.SetBrush(self.Brush)
1701 dc.SetPen(self.Pen)
1702 dc.DrawRectanglePointSize(xy , wh)
1703
1704 # Draw the Text
1705 dc.DrawTextList(self.Words, Points)
1706
1707 # Draw the hit box.
1708 if HTdc and self.HitAble:
1709 HTdc.SetPen(self.HitPen)
1710 HTdc.SetBrush(self.HitBrush)
1711 HTdc.DrawRectanglePointSize(xy, wh)
1712
1713 class Bitmap(TextObjectMixin, DrawObject, ):
1714 """
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.
1718
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).
1723
1724 The size is fixed, and does not scale with the drawing.
1725
1726 """
1727
1728 def __init__(self,Bitmap,XY,
1729 Position = 'tl',
1730 InForeground = False):
1731
1732 DrawObject.__init__(self,InForeground)
1733
1734 if type(Bitmap) == wx._gdi.Bitmap:
1735 self.Bitmap = Bitmap
1736 elif type(Bitmap) == wx._core.Image:
1737 self.Bitmap = wx.BitmapFromImage(Bitmap)
1738
1739 # Note the BB is just the point, as the size in World coordinates is not fixed
1740 self.BoundingBox = BBox.asBBox( (XY,XY) )
1741
1742 self.XY = XY
1743
1744 (self.Width, self.Height) = self.Bitmap.GetWidth(), self.Bitmap.GetHeight()
1745 self.ShiftFun = self.ShiftFunDict[Position]
1746
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) )
1755
1756 class ScaledBitmap(TextObjectMixin, DrawObject, ):
1757 """
1758
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.
1762
1763 the "Position" argument is a two charactor string, indicating
1764 where in relation to the coordinates the bitmap should be oriented.
1765
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).
1770
1771 The size scales with the drawing
1772
1773 """
1774
1775 def __init__(self,
1776 Bitmap,
1777 XY,
1778 Height,
1779 Position = 'tl',
1780 InForeground = False):
1781
1782 DrawObject.__init__(self,InForeground)
1783
1784 if type(Bitmap) == wx._gdi.Bitmap:
1785 self.Image = Bitmap.ConvertToImage()
1786 elif type(Bitmap) == wx._core.Image:
1787 self.Image = Bitmap
1788
1789 self.XY = XY
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
1797
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) ) )
1803
1804
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)
1813
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) )
1820
1821 class ScaledBitmap2(TextObjectMixin, DrawObject, ):
1822 """
1823
1824 An alternative scaled bitmap that only scaled the required amount of
1825 the main bitmap when zoomed in: EXPERIMENTAL!
1826
1827 """
1828
1829 def __init__(self,
1830 Bitmap,
1831 XY,
1832 Height,
1833 Width=None,
1834 Position = 'tl',
1835 InForeground = False):
1836
1837 DrawObject.__init__(self,InForeground)
1838
1839 if type(Bitmap) == wx._gdi.Bitmap:
1840 self.Image = Bitmap.ConvertToImage()
1841 elif type(Bitmap) == wx._core.Image:
1842 self.Image = Bitmap
1843
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
1849 if Width is None:
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
1855
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
1862
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)) )
1868
1869 def WorldToBitmap(self, Pw):
1870 """
1871 computes bitmap coords from World coords
1872 """
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
1877
1878 return Pb.astype(N.int_)
1879
1880 def _DrawEntireBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
1881 """
1882 this is pretty much the old code
1883
1884 Scales and Draws the entire bitmap.
1885
1886 """
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
1897 else:
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) )
1906
1907 def _DrawSubBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
1908 """
1909 Subsets just the part of the bitmap that is visible
1910 then scales and draws that.
1911
1912 """
1913 BBworld = BBox.asBBox(self._Canvas.ViewPortBB)
1914 BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld))
1915
1916 XYs = WorldToPixel(self.XY)
1917 # figure out subimage:
1918 # fixme: this should be able to be done more succinctly!
1919
1920 if BBbitmap[0,0] < 0:
1921 Xb = 0
1922 elif BBbitmap[0,0] > self.bmpWH[0]: # off the bitmap
1923 Xb = 0
1924 else:
1925 Xb = BBbitmap[0,0]
1926 XYs[0] = 0 # draw at origin
1927
1928 if BBbitmap[0,1] < 0:
1929 Yb = 0
1930 elif BBbitmap[0,1] > self.bmpWH[1]: # off the bitmap
1931 Yb = 0
1932 ShouldDraw = False
1933 else:
1934 Yb = BBbitmap[0,1]
1935 XYs[1] = 0 # draw at origin
1936
1937 if BBbitmap[1,0] < 0:
1938 #off the screen -- This should never happen!
1939 Wb = 0
1940 elif BBbitmap[1,0] > self.bmpWH[0]:
1941 Wb = self.bmpWH[0] - Xb
1942 else:
1943 Wb = BBbitmap[1,0] - Xb
1944
1945 if BBbitmap[1,1] < 0:
1946 # off the screen -- This should never happen!
1947 Hb = 0
1948 ShouldDraw = False
1949 elif BBbitmap[1,1] > self.bmpWH[1]:
1950 Hb = self.bmpWH[1] - Yb
1951 else:
1952 Hb = BBbitmap[1,1] - Yb
1953
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))
1960 Img.Rescale(Ws, Hs)
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!
1965 else:
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)
1971
1972 if HTdc and self.HitAble:
1973 HTdc.SetPen(self.HitPen)
1974 HTdc.SetBrush(self.HitBrush)
1975 HTdc.DrawRectanglePointSize(XYs, (Ws, Hs) )
1976
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)
1983 return None
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)
1988 else:
1989 print "Not Drawing -- no part of image is showing"
1990
1991 class DotGrid:
1992 """
1993 An example of a Grid Object -- it is set on teh FloatCAnvas with one of:
1994
1995 FloatCanvas.GridUnder = Grid
1996 FloatCanvas.GridOver = Grid
1997
1998 It will be drawn every time, regardless of the viewport.
1999
2000 In its _Draw method, it computes what to draw, given the ViewPortBB
2001 of the Canvas it's being drawn on.
2002
2003 """
2004 def __init__(self, Spacing, Size = 2, Color = "Black", Cross=False, CrossThickness = 1):
2005
2006 self.Spacing = N.array(Spacing, N.float)
2007 self.Spacing.shape = (2,)
2008 self.Size = Size
2009 self.Color = Color
2010 self.Cross = Cross
2011 self.CrossThickness = CrossThickness
2012
2013 def CalcPoints(self, Canvas):
2014 ViewPortBB = Canvas.ViewPortBB
2015
2016 Spacing = self.Spacing
2017
2018 minx, miny = N.floor(ViewPortBB[0] / Spacing) * Spacing
2019 maxx, maxy = N.ceil(ViewPortBB[1] / Spacing) * Spacing
2020
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)
2025 x.shape = (1,-1)
2026 y.shape = (-1,1)
2027 Points[:,:,0] += x
2028 Points[:,:,1] += y
2029 Points.shape = (-1,2)
2030
2031 return Points
2032
2033 def _Draw(self, dc, Canvas):
2034 Points = self.CalcPoints(Canvas)
2035
2036 Points = Canvas.WorldToPixel(Points)
2037
2038 dc.SetPen(wx.Pen(self.Color,self.CrossThickness))
2039
2040 if self.Cross: # Use cross shaped markers
2041 #Horizontal lines
2042 LinePoints = N.concatenate((Points + (self.Size,0),Points + (-self.Size,0)),1)
2043 dc.DrawLineList(LinePoints)
2044 # Vertical Lines
2045 LinePoints = N.concatenate((Points + (0,self.Size),Points + (0,-self.Size)),1)
2046 dc.DrawLineList(LinePoints)
2047 pass
2048 else: # use dots
2049 ## Note: this code borrowed from Pointset -- itreally shouldn't be repeated here!.
2050 if self.Size <= 1:
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))
2057 else:
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:
2062 xy = Points
2063 xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Size ), 1 )
2064 dc.DrawEllipseList(xywh)
2065 else:
2066 for xy in Points:
2067 dc.DrawCircle(xy[0],xy[1], radius)
2068
2069
2070
2071 #---------------------------------------------------------------------------
2072 class FloatCanvas(wx.Panel):
2073 """
2074 FloatCanvas.py
2075
2076 This is a high level window for drawing maps and anything else in an
2077 arbitrary coordinate system.
2078
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
2083
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
2087
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.
2092
2093 It is double buffered, so re-draws after the window is uncovered by something
2094 else are very quick.
2095
2096 It relies on NumPy, which is needed for speed (maybe, I havn't profiled it)
2097
2098 Bugs and Limitations:
2099 Lots: patches, fixes welcome
2100
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.
2105
2106 Zooming:
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.
2111
2112 Speed:
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).
2118
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.
2123
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.
2130
2131 Mouse Events:
2132
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.
2137
2138 Copyright: Christopher Barker
2139
2140 License: Same as the version of wxPython you are using it with
2141
2142 Please let me know if you're using this!!!
2143
2144 Contact me at:
2145
2146 Chris.Barker@noaa.gov
2147
2148 """
2149
2150 def __init__(self, parent, id = -1,
2151 size = wx.DefaultSize,
2152 ProjectionFun = None,
2153 BackgroundColor = "WHITE",
2154 Debug = False):
2155
2156 wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size)
2157
2158 self.ComputeFontScale()
2159 self.InitAll()
2160
2161 self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID)
2162
2163 self.Debug = Debug
2164
2165 wx.EVT_PAINT(self, self.OnPaint)
2166 wx.EVT_SIZE(self, self.OnSize)
2167
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)
2179
2180 ## CHB: I'm leaving these out for now.
2181 #wx.EVT_ENTER_WINDOW(self, self. )
2182 #wx.EVT_LEAVE_WINDOW(self, self. )
2183
2184 self.SetProjectionFun(ProjectionFun)
2185 self.GUIMode = None
2186
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)
2189
2190 self.InitializePanel()
2191 self.MakeNewBuffers()
2192
2193 # self.CreateCursors()
2194
2195 def ComputeFontScale(self):
2196 ## A global variable to hold the scaling from pixel size to point size.
2197 global FontScale
2198 dc = wx.ScreenDC()
2199 dc.SetFont(wx.Font(16, wx.ROMAN, wx.NORMAL, wx.NORMAL))
2200 E = dc.GetTextExtent("X")
2201 FontScale = 16/E[1]
2202 del dc
2203
2204 def InitAll(self):
2205 """
2206 InitAll() sets everything in the Canvas to default state.
2207
2208 It can be used to reset the Canvas
2209
2210 """
2211
2212 self.HitColorGenerator = None
2213 self.UseHitTest = False
2214
2215 self.NumBetweenBlits = 500
2216
2217 ## create the Hit Test Dicts:
2218 self.HitDict = None
2219 self._HTdc = None
2220
2221 self._DrawList = []
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)
2229
2230 self.SetProjectionFun(None)
2231
2232 self.MapProjectionVector = N.array( (1,1), N.float) # No Projection to start!
2233 self.TransformVector = N.array( (1,-1), N.float) # default Transformation
2234
2235 self.Scale = 1
2236 self.ObjectUnderMouse = None
2237
2238 self.GridUnder = None
2239 self.GridOver = None
2240
2241 self._BackgroundDirty = True
2242
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)
2250 else:
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')
2255
2256 def FlatEarthProjection(self, CenterPoint):
2257 MaxLatitude = 75 # these were determined essentially arbitrarily
2258 MinLatitude = -75
2259 Lat = min(CenterPoint[1],MaxLatitude)
2260 Lat = max(Lat,MinLatitude)
2261 return N.array((N.cos(N.pi*Lat/180),1),N.float)
2262
2263 def SetMode(self, Mode):
2264 '''
2265 Set the GUImode to any of the availble mode.
2266 '''
2267 # Set mode
2268 self.GUIMode = Mode
2269 #self.GUIMode.SetCursor()
2270 self.SetCursor(self.GUIMode.Cursor)
2271
2272 def MakeHitDict(self):
2273 ##fixme: Should this just be None if nothing has been bound?
2274 self.HitDict = {EVT_FC_LEFT_DOWN: {},
2275 EVT_FC_LEFT_UP: {},
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: {},
2285 }
2286
2287 def _RaiseMouseEvent(self, Event, EventType):
2288 """
2289 This is called in various other places to raise a Mouse Event
2290 """
2291 pt = self.PixelToWorld( Event.GetPosition() )
2292 evt = _MouseEvent(EventType, Event, self.GetId(), pt)
2293 self.GetEventHandler().ProcessEvent(evt)
2294
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)
2301 else:
2302 pdata = wx.AlphaPixelData(self._HTBitmap)
2303 if not pdata:
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]
2308 else:
2309 HitTestBitmapDepth = 24
2310 #print "using pre-2.8 hit test code"
2311 def GetHitTestColor(self, xy ):
2312 dc = wx.MemoryDC()
2313 if self._ForegroundHTBitmap:
2314 dc.SelectObject(self._ForegroundHTBitmap)
2315 else:
2316 dc.SelectObject(self._HTBitmap)
2317 hitcolor = dc.GetPixelPoint( xy )
2318 return hitcolor.Get()
2319
2320 def HitTest(self, event, HitEvent):
2321 if self.HitDict:
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)
2332 return True
2333 return False
2334
2335 def MouseOverTest(self, event):
2336 ##fixme: Can this be cleaned up?
2337 if (self.HitDict and
2338
2339 (self.HitDict[EVT_FC_ENTER_OBJECT ] or
2340 self.HitDict[EVT_FC_LEAVE_OBJECT ] )
2341 ):
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):
2349 try:
2350 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
2351 ObjectCallbackCalled = True
2352 except KeyError:
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
2355 pass
2356 ## Is the mouse on a differnt object as it was...
2357 elif not (Object == OldObject):
2358 # call the leave object callback
2359 try:
2360 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
2361 ObjectCallbackCalled = True
2362 except KeyError:
2363 pass # this means the leave event isn't bound for that object
2364 try:
2365 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
2366 ObjectCallbackCalled = True
2367 except KeyError:
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
2374 else:
2375 # no objects under mouse bound to mouse-over events
2376 self.ObjectUnderMouse = None
2377 if OldObject:
2378 try:
2379 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
2380 ObjectCallbackCalled = True
2381 except KeyError:
2382 pass # this means the leave event isn't bound for that object
2383 return ObjectCallbackCalled
2384 return False
2385
2386 ## fixme: There is a lot of repeated code here
2387 ## Is there a better way?
2388 def LeftDoubleClickEvent(self, event):
2389 if self.GUIMode:
2390 self.GUIMode.OnLeftDouble(event)
2391 event.Skip()
2392
2393 def MiddleDownEvent(self, event):
2394 if self.GUIMode:
2395 self.GUIMode.OnMiddleDown(event)
2396 event.Skip()
2397
2398 def MiddleUpEvent(self, event):
2399 if self.GUIMode:
2400 self.GUIMode.OnMiddleUp(event)
2401 event.Skip()
2402
2403 def MiddleDoubleClickEvent(self, event):
2404 if self.GUIMode:
2405 self.GUIMode.OnMiddleDouble(event)
2406 event.Skip()
2407
2408 def RightDoubleCLickEvent(self, event):
2409 if self.GUIMode:
2410 self.GUIMode.OnRightDouble(event)
2411 event.Skip()
2412
2413 def WheelEvent(self, event):
2414 if self.GUIMode:
2415 self.GUIMode.OnWheel(event)
2416 event.Skip()
2417
2418 def LeftDownEvent(self, event):
2419 if self.GUIMode:
2420 self.GUIMode.OnLeftDown(event)
2421 event.Skip()
2422
2423 def LeftUpEvent(self, event):
2424 if self.HasCapture():
2425 self.ReleaseMouse()
2426 if self.GUIMode:
2427 self.GUIMode.OnLeftUp(event)
2428 event.Skip()
2429
2430 def MotionEvent(self, event):
2431 if self.GUIMode:
2432 self.GUIMode.OnMove(event)
2433 event.Skip()
2434
2435 def RightDownEvent(self, event):
2436 if self.GUIMode:
2437 self.GUIMode.OnRightDown(event)
2438 event.Skip()
2439
2440 def RightUpEvent(self, event):
2441 if self.GUIMode:
2442 self.GUIMode.OnRightUp(event)
2443 event.Skip()
2444
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)
2451 if self.UseHitTest:
2452 self.MakeNewHTBitmap()
2453 else:
2454 self._ForegroundHTBitmap = None
2455 else:
2456 self._ForegroundBuffer = None
2457 self._ForegroundHTBitmap = None
2458
2459 if self.UseHitTest:
2460 self.MakeNewHTBitmap()
2461 else:
2462 self._HTBitmap = None
2463 self._ForegroundHTBitmap = None
2464
2465 def MakeNewHTBitmap(self):
2466 """
2467 Off screen Bitmap used for Hit tests on background objects
2468
2469 """
2470 self._HTBitmap = wx.EmptyBitmap(self.PanelSize[0],
2471
2472 self.PanelSize[1],
2473
2474 depth=self.HitTestBitmapDepth)
2475
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
2480 """
2481 Off screen Bitmap used for Hit tests on foreground objects
2482
2483 """
2484 self._ForegroundHTBitmap = wx.EmptyBitmap(self.PanelSize[0],
2485
2486 self.PanelSize[1],
2487
2488 depth=self.HitTestBitmapDepth)
2489
2490 def OnSize(self, event=None):
2491 self.InitializePanel()
2492 self.SizeTimer.Start(50, oneShot=True)
2493
2494 def OnSizeTimer(self, event=None):
2495 self.MakeNewBuffers()
2496 self.Draw()
2497
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
2507 else:
2508 self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1]
2509
2510 def OnPaint(self, event):
2511 dc = wx.PaintDC(self)
2512 if self._ForegroundBuffer:
2513 dc.DrawBitmap(self._ForegroundBuffer,0,0)
2514 else:
2515 dc.DrawBitmap(self._Buffer,0,0)
2516
2517 def Draw(self, Force=False):
2518 """
2519
2520 Canvas.Draw(Force=False)
2521
2522 Re-draws the canvas.
2523
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:
2528
2529 Canvas.Draw(Force=True)
2530
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.
2533
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.
2542
2543 """
2544
2545 if N.sometrue(self.PanelSize <= 2 ):
2546 # it's possible for this to get called before being properly initialized.
2547 return
2548 if self.Debug: start = clock()
2549 ScreenDC = wx.ClientDC(self)
2550 ViewPortWorld = N.array(( self.PixelToWorld((0,0)),
2551 self.PixelToWorld(self.PanelSize) )
2552 )
2553 self.ViewPortBB = N.array( ( N.minimum.reduce(ViewPortWorld),
2554 N.maximum.reduce(ViewPortWorld) ) )
2555 #self.ViewPortWorld = ViewPortWorld
2556
2557 dc = wx.MemoryDC()
2558 dc.SelectObject(self._Buffer)
2559 if self._BackgroundDirty or Force:
2560 dc.SetBackground(self.BackgroundBrush)
2561 dc.Clear()
2562 if self._HTBitmap is not None:
2563 HTdc = wx.MemoryDC()
2564 HTdc.SelectObject(self._HTBitmap)
2565 HTdc.Clear()
2566 else:
2567 HTdc = None
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
2572 del HTdc
2573
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],
2578 self.PanelSize[1])
2579
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)
2590 else:
2591 ForegroundHTdc = None
2592 self._DrawObjects(dc,
2593 self._ForeDrawList,
2594 ScreenDC,
2595 self.ViewPortBB,
2596 ForegroundHTdc)
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()
2605
2606 if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start)
2607
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
2611 ## when zoomed in.
2612 DrawObject.FontList = {}
2613
2614 def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck
2615 # lrk: Returns the objects that should be redrawn
2616
2617 ## fixme: should this check be moved into the object?
2618 ## also: a BB object would make this cleaner too
2619 BB2 = ViewPortBB
2620 redrawlist = []
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)
2628 #return redrawlist
2629 ##fixme: disabled this!!!!
2630 return redrawlist
2631 _ShouldRedraw = staticmethod(_ShouldRedraw)
2632
2633 def MoveImage(self,shift,CoordType):
2634 """
2635 move the image in the window.
2636
2637 shift is an (x,y) tuple, specifying the amount to shift in each direction
2638
2639 It can be in any of three coordinates: Panel, Pixel, World,
2640 specified by the CoordType parameter
2641
2642 Panel coordinates means you want to shift the image by some
2643 fraction of the size of the displaed image
2644
2645 Pixel coordinates means you want to shift the image by some number of pixels
2646
2647 World coordinates mean you want to shift the image by an amount
2648 in Floating point world coordinates
2649
2650 """
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
2657 pass
2658 else:
2659 raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"')
2660
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
2665 self.Draw()
2666
2667 def Zoom(self, factor, center = None, centerCoords="world"):
2668
2669 """
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.
2673
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.
2676
2677 centerCoords is a flag indicating whether the center given is in pixel or world
2678 coords. Options are: "world" or "pixel"
2679
2680 """
2681 self.Scale = self.Scale*factor
2682 if not center is None:
2683 if centerCoords == "pixel":
2684 center = self.PixelToWorld( center )
2685 else:
2686 center = N.array(center,N.float)
2687 self.ViewPortCenter = center
2688 self.SetToNewScale()
2689
2690 def ZoomToBB(self, NewBB=None, DrawFlag=True):
2691
2692 """
2693
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.
2696
2697 """
2698
2699 if NewBB is not None:
2700 BoundingBox = NewBB
2701 else:
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!
2711 try:
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
2715 try: #width == 0
2716 self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95
2717 except ZeroDivisionError:
2718 try: # height == 0
2719 self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95
2720 except ZeroDivisionError: #zero size! (must be a single point)
2721 self.Scale = 1
2722
2723 if DrawFlag:
2724 self._BackgroundDirty = True
2725 else:
2726 # Reset the shifting and scaling to defaults when there is no BB
2727 self.ViewPortCenter= N.array( (0,0), N.float)
2728 self.Scale= 1
2729 self.SetToNewScale(DrawFlag=DrawFlag)
2730
2731 def SetToNewScale(self, DrawFlag=True):
2732 Scale = self.Scale
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
2739 self.Scale = Scale
2740 self._BackgroundDirty = True
2741 if DrawFlag:
2742 self.Draw()
2743
2744 def RemoveObjects(self, Objects):
2745 for Object in Objects:
2746 self.RemoveObject(Object, ResetBB=False)
2747 self.BoundingBoxDirty = True
2748
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
2756 else:
2757 self._DrawList.remove(Object)
2758 self._BackgroundDirty = True
2759 if ResetBB:
2760 self.BoundingBoxDirty = True
2761
2762 def ClearAll(self, ResetBB=True):
2763 """
2764 ClearAll(ResetBB=True)
2765
2766 Removes all DrawObjects from the Canvas
2767
2768 If ResetBB is set to False, the original bounding box will remain
2769
2770 """
2771 self._DrawList = []
2772 self._ForeDrawList = []
2773 self._BackgroundDirty = True
2774 self.HitColorGenerator = None
2775 self.UseHitTest = False
2776 if ResetBB:
2777 self._ResetBoundingBox()
2778 self.MakeNewBuffers()
2779 self.HitDict = None
2780
2781 def _ResetBoundingBox(self):
2782 if self._DrawList or self._ForeDrawList:
2783 bblist = []
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)
2789 else:
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)
2794 self.Scale = 1
2795 self.BoundingBoxDirty = False
2796
2797 def PixelToWorld(self, Points):
2798 """
2799 Converts coordinates from Pixel coordinates to world coordinates.
2800
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.
2803
2804 """
2805 return (((N.asarray(Points, N.float) -
2806 (self.PanelSize/2))/self.TransformVector) +
2807 self.ViewPortCenter)
2808
2809 def WorldToPixel(self,Coordinates):
2810 """
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.
2815 """
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')
2820
2821 def ScaleWorldToPixel(self,Lengths):
2822 """
2823 This function will get passed to the drawing functions of the objects,
2824 to Change a length from world to pixel coordinates.
2825
2826 Lengths should be a NX2 array of (x,y) coordinates, or
2827 a 2-tuple, or sequence of 2-tuples.
2828 """
2829 return ( (N.asarray(Lengths, N.float)*self.TransformVector) ).astype('i')
2830
2831 def ScalePixelToWorld(self,Lengths):
2832 """
2833 This function computes a pair of x.y lengths,
2834 to change then from pixel to world coordinates.
2835
2836 Lengths should be a NX2 array of (x,y) coordinates, or
2837 a 2-tuple, or sequence of 2-tuples.
2838 """
2839
2840 return (N.asarray(Lengths,N.float) / self.TransformVector)
2841
2842 def AddObject(self, obj):
2843 # put in a reference to the Canvas, so remove and other stuff can work
2844 obj._Canvas = self
2845 if obj.InForeground:
2846 self._ForeDrawList.append(obj)
2847 self.UseForeground = True
2848 else:
2849 self._DrawList.append(obj)
2850 self._BackgroundDirty = True
2851 self.BoundingBoxDirty = True
2852 return True
2853
2854 def AddObjects(self, Objects):
2855 for Object in Objects:
2856 self.AddObject(Object)
2857
2858 def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None):
2859 """
2860 This is a convenience function;
2861 This function takes the list of objects and draws them to specified
2862 device context.
2863 """
2864 dc.SetBackground(self.BackgroundBrush)
2865 dc.BeginDrawing()
2866 #i = 0
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)):
2873 if Object.Visible:
2874 Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc)
2875 if (i+1) % NumBetweenBlits == 0:
2876 Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0)
2877 dc.EndDrawing()
2878
2879 def SaveAsImage(self, filename, ImageType=wx.BITMAP_TYPE_PNG):
2880 """
2881
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:
2884
2885 wx.BITMAP_TYPE_PNG
2886 wx.BITMAP_TYPE_JPG
2887 wx.BITMAP_TYPE_BMP
2888 wx.BITMAP_TYPE_XBM
2889 wx.BITMAP_TYPE_XPM
2890 etc. (see the wx docs for the complete list)
2891
2892 """
2893
2894 self._Buffer.SaveFile(filename, ImageType)
2895
2896
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)
2907 return Object
2908 return addshape
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
2914 if klass.__doc__:
2915 docstring += ", whose docstring is:\n%s" % klass.__doc__
2916 FloatCanvas.__dict__[methodname].__doc__ = docstring
2917
2918 _makeFloatCanvasAddMethods()
2919
2920