]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/floatcanvas/FloatCanvas.py
reSWIGged
[wxWidgets.git] / wxPython / wx / lib / floatcanvas / FloatCanvas.py
CommitLineData
42463de2
RD
1
2try:
3 from Numeric import array,asarray,Float,cos,pi,sum,minimum,maximum,Int32,zeros
4except ImportError:
5 from numarray import array, asarray, Float, cos, pi, sum, minimum, maximum, Int32, zeros
6
7from time import clock, sleep
8
9import wx
10
11import types
12import os
13
14import Resources
15
16## A global variable to hold the Pixels per inch that wxWindows thinks is in use
17## This is used for scaling fonts.
18## This can't be computed on module __init__, because a wx.App might not have iniitalized yet.
19global ScreenPPI
20
21## a custom Exceptions:
22
23class FloatCanvasException(Exception):
24 pass
25
26## All the mouse events
27#EVT_FC_ENTER_WINDOW = wx.NewEventType()
28#EVT_FC_LEAVE_WINDOW = wx.NewEventType()
29EVT_FC_LEFT_DOWN = wx.NewEventType()
30EVT_FC_LEFT_UP = wx.NewEventType()
31EVT_FC_LEFT_DCLICK = wx.NewEventType()
32EVT_FC_MIDDLE_DOWN = wx.NewEventType()
33EVT_FC_MIDDLE_UP = wx.NewEventType()
34EVT_FC_MIDDLE_DCLICK = wx.NewEventType()
35EVT_FC_RIGHT_DOWN = wx.NewEventType()
36EVT_FC_RIGHT_UP = wx.NewEventType()
37EVT_FC_RIGHT_DCLICK = wx.NewEventType()
38EVT_FC_MOTION = wx.NewEventType()
39EVT_FC_MOUSEWHEEL = wx.NewEventType()
40## these two are for the hit-test stuff, I never make them real Events
41EVT_FC_ENTER_OBJECT = wx.NewEventType()
42EVT_FC_LEAVE_OBJECT = wx.NewEventType()
43
44#def EVT_ENTER_WINDOW( window, function ):
45# window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function )
46#def EVT_LEAVE_WINDOW( window, function ):
47# window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function )
48def EVT_LEFT_DOWN( window, function ):
49 window.Connect( -1, -1,EVT_FC_LEFT_DOWN , function )
50def EVT_LEFT_UP( window, function ):
51 window.Connect( -1, -1,EVT_FC_LEFT_UP , function )
52def EVT_LEFT_DCLICK ( window, function ):
53 window.Connect( -1, -1,EVT_FC_LEFT_DCLICK , function )
54def EVT_MIDDLE_DOWN ( window, function ):
55 window.Connect( -1, -1,EVT_FC_MIDDLE_DOWN , function )
56def EVT_MIDDLE_UP ( window, function ):
57 window.Connect( -1, -1,EVT_FC_MIDDLE_UP , function )
58def EVT_MIDDLE_DCLICK ( window, function ):
59 window.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK , function )
60def EVT_RIGHT_DOWN ( window, function ):
61 window.Connect( -1, -1,EVT_FC_RIGHT_DOWN , function )
62def EVT_RIGHT_UP( window, function ):
63 window.Connect( -1, -1,EVT_FC_RIGHT_UP , function )
64def EVT_RIGHT_DCLICK( window, function ):
65 window.Connect( -1, -1,EVT_FC_RIGHT_DCLICK , function )
66def EVT_MOTION( window, function ):
67 window.Connect( -1, -1,EVT_FC_MOTION , function )
68def EVT_MOUSEWHEEL( window, function ):
69 window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function )
70
71class MouseEvent(wx.PyCommandEvent):
72
73 """
74
75 This event class takes a regular wxWindows mouse event as a parameter,
76 and wraps it so that there is access to all the original methods. This
77 is similar to subclassing, but you can't subclass a wxWindows event
78
79 The goal is to be able to it just like a regular mouse event.
80
81 It adds the method:
82
83 GetCoords() , which returns and (x,y) tuple in world coordinates.
84
85 Another differnce is that it is a CommandEvent, which propagates up
86 the window hierarchy until it is handled.
87
88 """
89
90 def __init__(self, EventType, NativeEvent, WinID, Coords = None):
91 wx.PyCommandEvent.__init__(self)
92
93 self.SetEventType( EventType )
94 self._NativeEvent = NativeEvent
95 self.Coords = Coords
96
97 def SetCoords(self,Coords):
98 self.Coords = Coords
99
100 def GetCoords(self):
101 return self.Coords
102
103 def __getattr__(self, name):
104 #return eval(self.NativeEvent.__getattr__(name) )
105 return getattr(self._NativeEvent, name)
106
107#### ColorGEnerator class is now obsolete. I'm using a python generator function instead.
108##class ColorGenerator:
109
110## """
111
112## An instance of this class generates a unique color each time
113## GetNextColor() is called. Someday I will use a proper Python
114## generator for this class.
115
116## The point of this generator is for the hit-test bitmap, each object
117## needs to be a unique color. Also, each system can be running a
118## different number of colors, and it doesn't appear to be possible to
119## have a wxMemDC with a different colordepth as the screen so this
120## generates colors far enough apart that they can be distinguished on
121## a 16bit screen. Anything less than 16bits won't work. It could, but
122## I havn't written the code that way. You also wouldn't get many
123## distict colors
124
125## """
126
127## def __init__(self):
128## import sys
129## ## figure out the color depth of the screen
130## ## for some bizare reason, thisdoesn't work on OS-X
131## if sys.platform == 'darwin':
132## depth = 24
133## else:
134## b = wx.EmptyBitmap(1,1)
135## depth = b.GetDepth()
136## self.r = 0
137## self.g = 0
138## self.b = 0
139## if depth == 16:
140## self.step = 8
141## elif depth >= 24:
142## self.step = 1
143## else:
144## raise FloatCanvasException("ColorGenerator does not work with depth = %s"%depth )
145
146## def GetNextColor(self):
147## step = self.step
148## ##r,g,b = self.r,self.g,self.b
149## self.r += step
150## if self.r > 255:
151## self.r = step
152## self.g += step
153## if self.g > 255:
154## self.g = step
155## self.b += step
156## if self.b > 255:
157## ## fixme: this should be a derived exception
158## raise FloatCanvasException("Too many objects in colorgenerator for HitTest")
159## return (self.r,self.g,self.b)
160
161## def Reset(self):
162## self.r = 0
163## self.g = 0
164## self.b = 0
165
166def cycleidxs(indexcount, maxvalue, step):
167 if indexcount == 0:
168 yield ()
169 else:
170 for idx in xrange(0, maxvalue, step):
171 for tail in cycleidxs(indexcount - 1, maxvalue, step):
172 yield (idx, ) + tail
173
174def colorGenerator():
175 import sys
176 if sys.platform == 'darwin':
177 depth = 24
178 else:
179 b = wx.EmptyBitmap(1,1)
180 depth = b.GetDepth()
181 if depth == 16:
182 step = 8
183 elif depth >= 24:
184 step = 1
185 else:
186 raise "ColorGenerator does not work with depth = %s" % depth
187 return cycleidxs(indexcount=3, maxvalue=256, step=step)
188
189
190#### I don't know if the Set objects are useful, beyond the pointset object
191#### The problem is that when zoomed in, the BB is checked to see whether to draw the object.
192#### A Set object can defeat this
193
194##class ObjectSetMixin:
195## """
196## A mix-in class for draw objects that are sets of objects
197
198## It contains methods for setting lists of pens and brushes
199
200## """
201## def SetPens(self,LineColors,LineStyles,LineWidths):
202## """
203## This method used when an object could have a list of pens, rather than just one
204## It is used for LineSet, and perhaps others in the future.
205
206## fixme: this should be in a mixin
207
208## fixme: this is really kludgy, there has got to be a better way!
209
210## """
211
212## length = 1
213## if type(LineColors) == types.ListType:
214## length = len(LineColors)
215## else:
216## LineColors = [LineColors]
217
218## if type(LineStyles) == types.ListType:
219## length = len(LineStyles)
220## else:
221## LineStyles = [LineStyles]
222
223## if type(LineWidths) == types.ListType:
224## length = len(LineWidths)
225## else:
226## LineWidths = [LineWidths]
227
228## if length > 1:
229## if len(LineColors) == 1:
230## LineColors = LineColors*length
231## if len(LineStyles) == 1:
232## LineStyles = LineStyles*length
233## if len(LineWidths) == 1:
234## LineWidths = LineWidths*length
235
236## self.Pens = []
237## for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths):
238## if LineColor is None or LineStyle is None:
239## self.Pens.append(wx.TRANSPARENT_PEN)
240## # what's this for?> self.LineStyle = 'Transparent'
241## if not self.PenList.has_key((LineColor,LineStyle,LineWidth)):
242## Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle])
243## self.Pens.append(Pen)
244## else:
245## self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)])
246## if length == 1:
247## self.Pens = self.Pens[0]
248
249
250
251class DrawObject:
252 """
253 This is the base class for all the objects that can be drawn.
254
255 """
256
257 def __init__(self,InForeground = False):
258 self.InForeground = InForeground
259
260 self._Canvas = None
261
262 self.HitColor = None
263 self.CallBackFuncs = {}
264
265 ## these are the defaults
266 self.HitAble = False
267 self.HitLine = True
268 self.HitFill = True
269 self.MinHitLineWidth = 3
270 self.HitLineWidth = 3 ## this gets re-set by the subclasses if necessary
271
272 # I pre-define all these as class variables to provide an easier
273 # interface, and perhaps speed things up by caching all the Pens
274 # and Brushes, although that may not help, as I think wx now
275 # does that on it's own. Send me a note if you know!
276
277 BrushList = {
278 ( None,"Transparent") : wx.TRANSPARENT_BRUSH,
279 ("Blue","Solid") : wx.BLUE_BRUSH,
280 ("Green","Solid") : wx.GREEN_BRUSH,
281 ("White","Solid") : wx.WHITE_BRUSH,
282 ("Black","Solid") : wx.BLACK_BRUSH,
283 ("Grey","Solid") : wx.GREY_BRUSH,
284 ("MediumGrey","Solid") : wx.MEDIUM_GREY_BRUSH,
285 ("LightGrey","Solid") : wx.LIGHT_GREY_BRUSH,
286 ("Cyan","Solid") : wx.CYAN_BRUSH,
287 ("Red","Solid") : wx.RED_BRUSH
288 }
289 PenList = {
290 (None,"Transparent",1) : wx.TRANSPARENT_PEN,
291 ("Green","Solid",1) : wx.GREEN_PEN,
292 ("White","Solid",1) : wx.WHITE_PEN,
293 ("Black","Solid",1) : wx.BLACK_PEN,
294 ("Grey","Solid",1) : wx.GREY_PEN,
295 ("MediumGrey","Solid",1) : wx.MEDIUM_GREY_PEN,
296 ("LightGrey","Solid",1) : wx.LIGHT_GREY_PEN,
297 ("Cyan","Solid",1) : wx.CYAN_PEN,
298 ("Red","Solid",1) : wx.RED_PEN
299 }
300
301 FillStyleList = {
302 "Transparent" : wx.TRANSPARENT,
303 "Solid" : wx.SOLID,
304 "BiDiagonalHatch": wx.BDIAGONAL_HATCH,
305 "CrossDiagHatch" : wx.CROSSDIAG_HATCH,
306 "FDiagonal_Hatch": wx.FDIAGONAL_HATCH,
307 "CrossHatch" : wx.CROSS_HATCH,
308 "HorizontalHatch": wx.HORIZONTAL_HATCH,
309 "VerticalHatch" : wx.VERTICAL_HATCH
310 }
311
312 LineStyleList = {
313 "Solid" : wx.SOLID,
314 "Transparent": wx.TRANSPARENT,
315 "Dot" : wx.DOT,
316 "LongDash" : wx.LONG_DASH,
317 "ShortDash" : wx.SHORT_DASH,
318 "DotDash" : wx.DOT_DASH,
319 }
320
321 def Bind(self, Event, CallBackFun):
322 self.CallBackFuncs[Event] = CallBackFun
323 self.HitAble = True
324 self._Canvas.UseHitTest = True
325 if not self._Canvas._HTdc:
326 self._Canvas.MakeNewHTdc()
327 if not self.HitColor:
328 if not self._Canvas.HitColorGenerator:
329 self._Canvas.HitColorGenerator = colorGenerator()
330 self._Canvas.HitColorGenerator.next() # first call to prevent the background color from being used.
331 self.HitColor = self._Canvas.HitColorGenerator.next()
332 self.SetHitPen(self.HitColor,self.HitLineWidth)
333 self.SetHitBrush(self.HitColor)
334 # put the object in the hit dict, indexed by it's color
335 if not self._Canvas.HitDict:
336 self._Canvas.MakeHitDict()
337 self._Canvas.HitDict[Event][self.HitColor] = (self) # put the object in the hit dict, indexed by it's color
338
339
340 def UnBindAll(self):
341 ## fixme: this only removes one from each list, there could be more.
342 if self._Canvas.HitDict:
343 for List in self._Canvas.HitDict.itervalues():
344 try:
345 List.remove(self)
346 except ValueError:
347 pass
348 self.HitAble = False
349
350 def SetBrush(self,FillColor,FillStyle):
351 if FillColor is None or FillStyle is None:
352 self.Brush = wx.TRANSPARENT_BRUSH
353 self.FillStyle = "Transparent"
354 else:
355 self.Brush = self.BrushList.setdefault( (FillColor,FillStyle), wx.Brush(FillColor,self.FillStyleList[FillStyle] ) )
356
357 def SetPen(self,LineColor,LineStyle,LineWidth):
358 if (LineColor is None) or (LineStyle is None):
359 self.Pen = wx.TRANSPARENT_PEN
360 self.LineStyle = 'Transparent'
361 else:
362 self.Pen = self.PenList.setdefault( (LineColor,LineStyle,LineWidth), wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) )
363
364 def SetHitBrush(self,HitColor):
365 if not self.HitFill:
366 self.HitBrush = wx.TRANSPARENT_BRUSH
367 else:
368 self.HitBrush = self.BrushList.setdefault( (HitColor,"solid"), wx.Brush(HitColor,self.FillStyleList["Solid"] ) )
369
370 def SetHitPen(self,HitColor,LineWidth):
371 if not self.HitLine:
372 self.HitPen = wx.TRANSPARENT_PEN
373 else:
374 self.HitPen = self.PenList.setdefault( (HitColor, "solid", LineWidth), wx.Pen(HitColor, LineWidth, self.LineStyleList["Solid"]) )
375
376 def PutInBackground(self):
377 if self._Canvas and self.InForeground:
378 self._Canvas._ForeDrawList.remove(self)
379 self._Canvas._DrawList.append(self)
380 self._Canvas._BackgroundDirty = True
381 self.InForeground = False
382
383 def PutInForeground(self):
384 if self._Canvas and (not self.InForeground):
385 self._Canvas._ForeDrawList.append(self)
386 self._Canvas._DrawList.remove(self)
387 self._Canvas._BackgroundDirty = True
388 self.InForeground = True
389
390class XYObjectMixin:
391 """
392
393 This is a mixin class that provides some methods suitable for use
394 with objects that have a single (x,y) coordinate pair.
395
396 """
397
398 def Move(self, Delta ):
399 """
400
401 Move(Delta): moves the object by delta, where delta is a
402 (dx,dy) pair. Ideally a Numpy array of shape (2,)
403
404 """
405
406 Delta = asarray(Delta, Float)
407 self.XY += Delta
408 self.BoundingBox = self.BoundingBox + Delta
409 if self._Canvas:
410 self._Canvas.BoundingBoxDirty = True
411
412class PointsObjectMixin:
413 """
414
415 This is a mixin class that provides some methods suitable for use
416 with objects that have a set of (x,y) coordinate pairs.
417
418 """
419
420## This is code for the XYMixin object, it needs to be adapeted and tested.
421## def Move(self, Delta ):
422## """
423
424## Move(Delta): moves the object by delta, where delta is an (dx,
425## dy) pair. Ideally a Numpy array or shape (2,)
426
427## """
428
429## Delta = array(Delta, Float)
430## self.XY += Delta
431## self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float)
432## if self._Canvas:
433## self._Canvas.BoundingBoxDirty = True
434
435 def SetPoints(self,Points):
436 self.Points = Points
437 self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
438 if self._Canvas:
439 self._Canvas.BoundingBoxDirty = True
440
441
442
443class Polygon(DrawObject,PointsObjectMixin):
444
445 """
446
447 The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of
448 point coordinates. so that Points[N][0] is the x-coordinate of
449 point N and Points[N][1] is the y-coordinate or Points[N,0] is the
450 x-coordinate of point N and Points[N,1] is the y-coordinate for
451 arrays.
452
453 """
454 def __init__(self,
455 Points,
456 LineColor = "Black",
457 LineStyle = "Solid",
458 LineWidth = 1,
459 FillColor = None,
460 FillStyle = "Solid",
461 InForeground = False):
462 DrawObject.__init__(self,InForeground)
463 self.Points = array(Points,Float) # this DOES need to make a copy
464 self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
465
466 self.LineColor = LineColor
467 self.LineStyle = LineStyle
468 self.LineWidth = LineWidth
469 self.FillColor = FillColor
470 self.FillStyle = FillStyle
471
472 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
473
474 self.SetPen(LineColor,LineStyle,LineWidth)
475 self.SetBrush(FillColor,FillStyle)
476
477 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None):
478 Points = WorldToPixel(self.Points)
479 dc.SetPen(self.Pen)
480 dc.SetBrush(self.Brush)
481 dc.DrawPolygon(Points)
482 if HTdc and self.HitAble:
483 HTdc.SetPen(self.HitPen)
484 HTdc.SetBrush(self.HitBrush)
485 HTdc.DrawPolygon(Points)
486
487##class PolygonSet(DrawObject):
488## """
489## The PolygonSet class takes a Geometry.Polygon object.
490## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
491
492## it creates a set of line segments, from (x1,y1) to (x2,y2)
493
494## """
495
496## def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False):
497## DrawObject.__init__(self, InForeground)
498
499## ##fixme: there should be some error checking for everything being the right length.
500
501
502## self.Points = array(Points,Float)
503## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
504
505## self.LineColors = LineColors
506## self.LineStyles = LineStyles
507## self.LineWidths = LineWidths
508## self.FillColors = FillColors
509## self.FillStyles = FillStyles
510
511## self.SetPens(LineColors,LineStyles,LineWidths)
512
513## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
514## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
515## Points = WorldToPixel(self.Points)
516## Points.shape = (-1,4)
517## dc.DrawLineList(Points,self.Pens)
518
519
520class Line(DrawObject,PointsObjectMixin):
521 """
522 The Line class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates.
523 so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate
524 or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays.
525
526 It will draw a straight line if there are two points, and a polyline if there are more than two.
527
528 """
529 def __init__(self,Points,
530 LineColor = "Black",
531 LineStyle = "Solid",
532 LineWidth = 1,
533 InForeground = False):
534 DrawObject.__init__(self, InForeground)
535
536
537 self.Points = array(Points,Float)
538 self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
539
540 self.LineColor = LineColor
541 self.LineStyle = LineStyle
542 self.LineWidth = LineWidth
543
544 self.SetPen(LineColor,LineStyle,LineWidth)
545
546 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
547
548
549 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
550 Points = WorldToPixel(self.Points)
551 dc.SetPen(self.Pen)
552 dc.DrawLines(Points)
553 if HTdc and self.HitAble:
554 HTdc.SetPen(self.HitPen)
555 HTdc.DrawLines(Points)
556
557##class LineSet(DrawObject, ObjectSetMixin):
558## """
559## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates.
560## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
561
562## it creates a set of line segments, from (x1,y1) to (x2,y2)
563
564## """
565
566## def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False):
567## DrawObject.__init__(self, InForeground)
568
569## NumLines = len(Points) / 2
570## ##fixme: there should be some error checking for everything being the right length.
571
572
573## self.Points = array(Points,Float)
574## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
575
576## self.LineColors = LineColors
577## self.LineStyles = LineStyles
578## self.LineWidths = LineWidths
579
580## self.SetPens(LineColors,LineStyles,LineWidths)
581
582## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
583## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
584## Points = WorldToPixel(self.Points)
585## Points.shape = (-1,4)
586## dc.DrawLineList(Points,self.Pens)
587
588class PointSet(DrawObject):
589 """
590 The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates.
591 so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate
592 or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays.
593
594 Each point will be drawn the same color and Diameter. The Diameter is in screen points,
595 not world coordinates.
596
597 At this point, the hit-test code does not distingish between the
598 points, you will only know that one of the poins got hit, not which
599 one.
600
601 In the case of points, the HitLineWidth is used as diameter.
602
603 """
604 def __init__(self, Points, Color = "Black", Diameter = 1, InForeground = False):
605 DrawObject.__init__(self,InForeground)
606
607 self.Points = array(Points,Float)
608 self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point
609 self.BoundingBox = array(((min(self.Points[:,0]),
610 min(self.Points[:,1])),
611 (max(self.Points[:,0]),
612 max(self.Points[:,1]))),Float)
613
614 self.Color = Color
615 self.Diameter = Diameter
616
617 self.HitLineWidth = self.MinHitLineWidth
618 self.SetPen(Color,"Solid",1)
619 self.SetBrush(Color,"Solid")
620
621 def SetPoints(self,Points):
622 self.Points = array(Points, Float)
623 self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point
624 self.BoundingBox = array(((min(self.Points[:,0]),
625 min(self.Points[:,1]) ),
626 (max(self.Points[:,0]),
627 max(self.Points[:,1]) ) ) )
628 if self._Canvas:
629 self._Canvas.BoundingBoxDirty = True
630
631 def DrawD2(self, dc, Points):
632 # A Little optimization for a diameter2 - point
633 dc.DrawPointList(Points)
634 dc.DrawPointList(Points + (1,0))
635 dc.DrawPointList(Points + (0,1))
636 dc.DrawPointList(Points + (1,1))
637
638 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
639 dc.SetPen(self.Pen)
640 Points = WorldToPixel(self.Points)
641 if self.Diameter <= 1:
642 dc.DrawPointList(Points)
643 elif self.Diameter <= 2:
644 self.DrawD2(dc, Points)
645 else:
646 dc.SetBrush(self.Brush)
647 radius = int(round(self.Diameter/2))
648 for xy in Points:
649 dc.DrawEllipsePointSize( (xy - radius), (self.Diameter, self.Diameter) )
650 if HTdc and self.HitAble:
651 HTdc.SetPen(self.HitPen)
652 if self.Diameter <= 1:
653 HTdc.DrawPointList(Points)
654 elif self.Diameter <= 2:
655 self.DrawD2(HTdc, Points)
656 else:
657 HTdc.SetBrush(self.HitBrush)
658 radius = int(round(self.Diameter/2))
659 for xy in Points:
660 HTdc.DrawEllipsePointSize( (xy - radius), (self.Diameter, self.Diameter) )
661
662#### Does anyone need this?
663##class Dot(DrawObject):
664## """
665## The Dot class takes an x.y coordinate pair, and the Diameter of the circle.
666## The Diameter is in pixels, so it won't change with zoom.
667
668## Also Fill and line data
669
670## """
671## def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,InForeground = False):
672## DrawObject.__init__(self,InForeground)
673
674## self.X = x
675## self.Y = y
676## self.Diameter = Diameter
677## # NOTE: the bounding box does not include the diameter of the dot, as that is in pixel coords.
678## # If this is a problem, perhaps you should use a circle, instead!
679## self.BoundingBox = array(((x,y),(x,y)),Float)
680
681## self.LineColor = LineColor
682## self.LineStyle = LineStyle
683## self.LineWidth = LineWidth
684## self.FillColor = FillColor
685## self.FillStyle = FillStyle
686
687## self.SetPen(LineColor,LineStyle,LineWidth)
688## self.SetBrush(FillColor,FillStyle)
689
690## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
691## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
692## dc.SetPen(self.Pen)
693## dc.SetBrush(self.Brush)
694## radius = int(round(self.Diameter/2))
695## (X,Y) = WorldToPixel((self.X,self.Y))
696## dc.DrawEllipse((X - radius), (Y - radius), self.Diameter, self.Diameter)
697
698class RectEllipse(DrawObject, XYObjectMixin):
699 def __init__(self,x,y,width,height,
700 LineColor = "Black",
701 LineStyle = "Solid",
702 LineWidth = 1,
703 FillColor = None,
704 FillStyle = "Solid",
705 InForeground = False):
706
707 DrawObject.__init__(self,InForeground)
708
709 self.XY = array( (x, y), Float)
710 self.WH = array( (width, height), Float )
711 self.BoundingBox = array(((x,y), (self.XY + self.WH)), Float)
712 self.LineColor = LineColor
713 self.LineStyle = LineStyle
714 self.LineWidth = LineWidth
715 self.FillColor = FillColor
716 self.FillStyle = FillStyle
717
718 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
719
720 self.SetPen(LineColor,LineStyle,LineWidth)
721 self.SetBrush(FillColor,FillStyle)
722
723
724 def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
725 dc.SetPen(self.Pen)
726 dc.SetBrush(self.Brush)
727 if HTdc and self.HitAble:
728 HTdc.SetPen(self.HitPen)
729 HTdc.SetBrush(self.HitBrush)
730 return ( WorldToPixel(self.XY),
731 ScaleWorldToPixel(self.WH) )
732
733 def SetXY(self, x, y):
734 self.XY = array( (x, y), Float)
735 self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float)
736 if self._Canvas:
737 self._Canvas.BoundingBoxDirty = True
738
739
740class Rectangle(RectEllipse):
741# def __init__(*args, **kwargs):
742# RectEllipse.__init__(*args, **kwargs)
743# raise "an error"
744
745 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
746 ( XY, WH ) = self.SetUpDraw(dc,
747 WorldToPixel,
748 ScaleWorldToPixel,
749 HTdc)
750 dc.DrawRectanglePointSize(XY, WH)
751 if HTdc and self.HitAble:
752 HTdc.DrawRectanglePointSize(XY, WH)
753
754class Ellipse(RectEllipse):
755# def __init__(*args, **kwargs):
756# RectEllipse.__init__(*args, **kwargs)
757
758 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
759 ( XY, WH ) = self.SetUpDraw(dc,
760 WorldToPixel,
761 ScaleWorldToPixel,
762 HTdc)
763 dc.DrawEllipsePointSize(XY, WH)
764 if HTdc and self.HitAble:
765 HTdc.DrawEllipsePointSize(XY, WH)
766
767class Circle(Ellipse):
768 def __init__(self, x ,y, Diameter, **kwargs):
769 RectEllipse.__init__(self ,
770 x-Diameter/2.,
771 y-Diameter/2.,
772 Diameter,
773 Diameter,
774 **kwargs)
775
776class TextObjectMixin:
777 """
778
779 A mix in class that holds attributes and methods that are needed by
780 the Text objects
781
782 """
783
784 ## I'm caching fonts, because on GTK, getting a new font can take a
785 ## while. However, it gets cleared after every full draw as hanging
786 ## on to a bunch of large fonts takes a massive amount of memory.
787
788 FontList = {}
789
790 def SetFont(self, Size, Family, Style, Weight, Underline, FaceName):
791 self.Font = self.FontList.setdefault( (Size,
792 Family,
793 Style,
794 Weight,
795 Underline,
796 FaceName),
797 wx.Font(Size,
798 Family,
799 Style,
800 Weight,
801 Underline,
802 FaceName) )
803 return self.Font
804
805 ## store the function that shift the coords for drawing text. The
806 ## "c" parameter is the correction for world coordinates, rather
807 ## than pixel coords as the y axis is reversed
808 ShiftFunDict = {'tl': lambda x, y, w, h, world=0: (x, y) ,
809 'tc': lambda x, y, w, h, world=0: (x - w/2, y) ,
810 'tr': lambda x, y, w, h, world=0: (x - w, y) ,
811 'cl': lambda x, y, w, h, world=0: (x, y - h/2 + world*h) ,
812 'cc': lambda x, y, w, h, world=0: (x - w/2, y - h/2 + world*h) ,
813 'cr': lambda x, y, w, h, world=0: (x - w, y - h/2 + world*h) ,
814 'bl': lambda x, y, w, h, world=0: (x, y - h + 2*world*h) ,
815 'bc': lambda x, y, w, h, world=0: (x - w/2, y - h + 2*world*h) ,
816 'br': lambda x, y, w, h, world=0: (x - w, y - h + 2*world*h)}
817
818class Text(DrawObject, TextObjectMixin):
819 """
820 This class creates a text object, placed at the coordinates,
821 x,y. the "Position" argument is a two charactor string, indicating
822 where in relation to the coordinates the string should be oriented.
823
824 The first letter is: t, c, or b, for top, center and bottom The
825 second letter is: l, c, or r, for left, center and right The
826 position refers to the position relative to the text itself. It
827 defaults to "tl" (top left).
828
829 Size is the size of the font in pixels, or in points for printing
830 (if it ever gets implimented). Those will be the same, If you assume
831 72 PPI.
832
833 Family:
834 Font family, a generic way of referring to fonts without
835 specifying actual facename. One of:
836 wx.DEFAULT: Chooses a default font.
837 wx.DECORATIVE: A decorative font.
838 wx.ROMAN: A formal, serif font.
839 wx.SCRIPT: A handwriting font.
840 wx.SWISS: A sans-serif font.
841 wx.MODERN: A fixed pitch font.
842 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
843 Style:
844 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
845 Weight:
846 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
847 Underline:
848 The value can be True or False. At present this may have an an
849 effect on Windows only.
850
851 Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored.
852
853 The size is fixed, and does not scale with the drawing.
854
855 The hit-test is done on the entire text extent
856
857 """
858
859 def __init__(self,String,x,y,
860 Size = 12,
861 Color = "Black",
862 BackgroundColor = None,
863 Family = wx.MODERN,
864 Style = wx.NORMAL,
865 Weight = wx.NORMAL,
866 Underline = False,
867 Position = 'tl',
868 InForeground = False,
869 Font = None):
870
871 DrawObject.__init__(self,InForeground)
872
873 self.String = String
874 # Input size in in Pixels, compute points size from PPI info.
875 # fixme: for printing, we'll have to do something a little different
876 self.Size = int(round(72.0 * Size / ScreenPPI))
877
878 self.Color = Color
879 self.BackgroundColor = BackgroundColor
880
881 if not Font:
882 FaceName = ''
883 else:
884 FaceName = Font.GetFaceName()
885 Family = Font.GetFamily()
886 Size = Font.GetPointSize()
887 Style = Font.GetStyle()
888 Underlined = Font.GetUnderlined()
889 Weight = Font.GetWeight()
890 self.SetFont(Size, Family, Style, Weight, Underline, FaceName)
891
892 self.BoundingBox = array(((x,y),(x,y)),Float)
893
894 self.XY = ( x,y )
895
896 # use a memDC -- ScreenDC doesn't work with 2.5.1 and GTK2
897 #dc = wx.MemoryDC()
898 #bitmap = wx.EmptyBitmap(1, 1)
899 #dc.SelectObject(bitmap)
900 #dc.SetFont(self.Font)
901 #(self.TextWidth, self.TextHeight) = dc.GetTextExtent(self.String)
902 (self.TextWidth, self.TextHeight) = (None, None)
903 self.ShiftFun = self.ShiftFunDict[Position]
904
905 def SetXY(self, x, y):
906 self.XY = ( x,y )
907 self.BoundingBox = array((self.XY, self.XY),Float)
908 if self._Canvas:
909 self._Canvas.BoundingBoxDirty = True
910
911 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
912 XY = WorldToPixel(self.XY)
913 dc.SetFont(self.Font)
914 dc.SetTextForeground(self.Color)
915 if self.BackgroundColor:
916 dc.SetBackgroundMode(wx.SOLID)
917 dc.SetTextBackground(self.BackgroundColor)
918 else:
919 dc.SetBackgroundMode(wx.TRANSPARENT)
920 if self.TextWidth is None or self.TextHeight is None:
921 (self.TextWidth, self.TextHeight) = dc.GetTextExtent(self.String)
922 XY = self.ShiftFun(XY[0], XY[1], self.TextWidth, self.TextHeight)
923 dc.DrawTextPoint(self.String, XY)
924 if HTdc and self.HitAble:
925 HTdc.SetPen(self.HitPen)
926 HTdc.SetBrush(self.HitBrush)
927 HTdc.DrawRectanglePointSize(XY, (self.TextWidth, self.TextHeight) )
928
929class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin):
930 """
931 This class creates a text object that is scaled when zoomed. It is
932 placed at the coordinates, x,y. the "Position" argument is a two
933 charactor string, indicating where in relation to the coordinates
934 the string should be oriented.
935
936 The first letter is: t, c, or b, for top, center and bottom The
937 second letter is: l, c, or r, for left, center and right The
938 position refers to the position relative to the text itself. It
939 defaults to "tl" (top left).
940
941 Size is the size of the font in world coordinates.
942
943 Family:
944 Font family, a generic way of referring to fonts without
945 specifying actual facename. One of:
946 wx.DEFAULT: Chooses a default font.
947 wx.DECORATI: A decorative font.
948 wx.ROMAN: A formal, serif font.
949 wx.SCRIPT: A handwriting font.
950 wx.SWISS: A sans-serif font.
951 wx.MODERN: A fixed pitch font.
952 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
953 Style:
954 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
955 Weight:
956 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
957 Underline:
958 The value can be True or False. At present this may have an an
959 effect on Windows only.
960
961 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
962 above will be ignored. The size of the font you specify will be
963 ignored, but the rest of it's attributes will be preserved.
964
965 The size will scale as the drawing is zoomed.
966
967 Bugs/Limitations:
968
969 As fonts are scaled, the do end up a little different, so you don't
970 get exactly the same picture as you scale up and doen, but it's
971 pretty darn close.
972
973 On wxGTK1 on my Linux system, at least, using a font of over about
974 3000 pts. brings the system to a halt. It's the Font Server using
975 huge amounts of memory. My work around is to max the font size to
976 3000 points, so it won't scale past there. GTK2 uses smarter font
977 drawing, so that may not be an issue in future versions, so feel
978 free to test. Another smarter way to do it would be to set a global
979 zoom limit at that point.
980
981 The hit-test is done on the entire text extent. This could be made
982 optional, but I havn't gotten around to it.
983
984 """
985
986 def __init__(self, String, x, y , Size,
987 Color = "Black",
988 BackgroundColor = None,
989 Family = wx.MODERN,
990 Style = wx.NORMAL,
991 Weight = wx.NORMAL,
992 Underline = False,
993 Position = 'tl',
994 Font = None,
995 InForeground = False):
996
997 DrawObject.__init__(self,InForeground)
998
999 self.String = String
1000 self.XY = array( (x, y), Float)
1001 self.Size = Size
1002 self.Color = Color
1003 self.BackgroundColor = BackgroundColor
1004 self.Family = Family
1005 self.Style = Style
1006 self.Weight = Weight
1007 self.Underline = Underline
1008 if not Font:
1009 self.FaceName = ''
1010 else:
1011 self.FaceName = Font.GetFaceName()
1012 self.Family = Font.GetFamily()
1013 self.Style = Font.GetStyle()
1014 self.Underlined = Font.GetUnderlined()
1015 self.Weight = Font.GetWeight()
1016
1017 # Experimental max font size value on wxGTK2: this works OK on
1018 # my system If it's any larger, there is a crash, with the
1019 # message: The application 'FloatCanvasDemo.py' lost its
1020 # connection to the display :0.0; most likely the X server was
1021 # shut down or you killed/destroyed the application.
1022 self.MaxSize = 2750
1023
1024 self.ShiftFun = self.ShiftFunDict[Position]
1025
1026 ## Compute the BB
1027 ## this isn't exact, as fonts don't scale exactly.
1028 dc = wx.MemoryDC()
1029 bitmap = wx.EmptyBitmap(1, 1)
1030 dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
1031 DrawingSize = 40 # pts This effectively determines the resolution that the BB is computed to.
1032 ScaleFactor = float(Size) / DrawingSize
1033 dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
1034 (w,h) = dc.GetTextExtent(self.String)
1035 w = w * ScaleFactor
1036 h = h * ScaleFactor
1037 x, y = self.ShiftFun(x, y, w, h, world = 1)
1038 self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
1039
1040 # the new coords are set to the corner of the BB:
1041 #self.X = self.BoundingBox[0,0]
1042 #self.Y = self.BoundingBox[1,1]
1043 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1044 (X,Y) = WorldToPixel( (self.XY) )
1045
1046 # compute the font size:
1047 Size = abs( ScaleWorldToPixel( (self.Size, self.Size) )[1] ) # only need a y coordinate length
1048 ## Check to see if the font size is large enough to blow up the X font server
1049 ## If so, limit it. Would it be better just to not draw it?
1050 ## note that this limit is dependent on how much memory you have, etc.
1051 if Size > self.MaxSize:
1052 Size = self.MaxSize
1053 dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName))
1054 dc.SetTextForeground(self.Color)
1055 if self.BackgroundColor:
1056 dc.SetBackgroundMode(wx.SOLID)
1057 dc.SetTextBackground(self.BackgroundColor)
1058 else:
1059 dc.SetBackgroundMode(wx.TRANSPARENT)
1060 (w,h) = dc.GetTextExtent(self.String)
1061 # compute the shift, and adjust the coordinates, if neccesary
1062 # This had to be put in here, because it changes with Zoom, as
1063 # fonts don't scale exactly.
1064 xy = self.ShiftFun(X, Y, w, h)
1065
1066 dc.DrawTextPoint(self.String, xy)
1067 if HTdc and self.HitAble:
1068 HTdc.SetPen(self.HitPen)
1069 HTdc.SetBrush(self.HitBrush)
1070 HTdc.DrawRectanglePointSize(xy, (w, h) )
1071
1072
1073#---------------------------------------------------------------------------
1074class FloatCanvas(wx.Panel):
1075 """
1076 FloatCanvas.py
1077
1078 This is a high level window for drawing maps and anything else in an
1079 arbitrary coordinate system.
1080
1081 The goal is to provide a convenient way to draw stuff on the screen
1082 without having to deal with handling OnPaint events, converting to pixel
1083 coordinates, knowing about wxWindows brushes, pens, and colors, etc. It
1084 also provides virtually unlimited zooming and scrolling
1085
1086 I am using it for two things:
1087 1) general purpose drawing in floating point coordinates
1088 2) displaying map data in Lat-long coordinates
1089
1090 If the projection is set to None, it will draw in general purpose
1091 floating point coordinates. If the projection is set to 'FlatEarth', it
1092 will draw a FlatEarth projection, centered on the part of the map that
1093 you are viewing. You can also pass in your own projection function.
1094
1095 It is double buffered, so re-draws after the window is uncovered by something
1096 else are very quick.
1097
1098 It relies on NumPy, which is needed for speed (maybe, I havn't profiled it)
1099
1100 Bugs and Limitations:
1101 Lots: patches, fixes welcome
1102
1103 For Map drawing: It ignores the fact that the world is, in fact, a
1104 sphere, so it will do strange things if you are looking at stuff near
1105 the poles or the date line. so far I don't have a need to do that, so I
1106 havn't bothered to add any checks for that yet.
1107
1108 Zooming:
1109 I have set no zoom limits. What this means is that if you zoom in really
1110 far, you can get integer overflows, and get wierd results. It
1111 doesn't seem to actually cause any problems other than wierd output, at
1112 least when I have run it.
1113
1114 Speed:
1115 I have done a couple of things to improve speed in this app. The one
1116 thing I have done is used NumPy Arrays to store the coordinates of the
1117 points of the objects. This allowed me to use array oriented functions
1118 when doing transformations, and should provide some speed improvement
1119 for objects with a lot of points (big polygons, polylines, pointsets).
1120
1121 The real slowdown comes when you have to draw a lot of objects, because
1122 you have to call the wx.DC.DrawSomething call each time. This is plenty
1123 fast for tens of objects, OK for hundreds of objects, but pretty darn
1124 slow for thousands of objects.
1125
1126 The solution is to be able to pass some sort of object set to the DC
1127 directly. I've used DC.DrawPointList(Points), and it helped a lot with
1128 drawing lots of points. I havn't got a LineSet type object, so I havn't
1129 used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList()
1130 methods implimented, and then I'd also have a full set of Object sets
1131 that could take advantage of them. I hope to get to it some day.
1132
1133 Mouse Events:
1134
1135 At this point, there are a full set of custom mouse events. They are
1136 just like the rebulsr mouse events, but include an extra attribute:
1137 Event.GetCoords(), that returns the (x,y) position in world
1138 coordinates, as a length-2 NumPy vector of Floats.
1139
1140 Copyright: Christopher Barker
1141
1142 License: Same as the version of wxPython you are using it with
1143
1144 Please let me know if you're using this!!!
1145
1146 Contact me at:
1147
1148 Chris.Barker@noaa.gov
1149
1150 """
1151
1152 def __init__(self, parent, id = -1,
1153 size = wx.DefaultSize,
1154 ProjectionFun = None,
1155 BackgroundColor = "WHITE",
1156 Debug = False):
1157
1158 wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size)
1159
1160 global ScreenPPI ## A global variable to hold the Pixels per inch that wxWindows thinks is in use.
1161 dc = wx.ScreenDC()
1162 ScreenPPI = dc.GetPPI()[0] # Assume square pixels
1163 del dc
1164
1165 self.HitColorGenerator = None
1166 self.UseHitTest = None
1167
1168 self.NumBetweenBlits = 500
1169
1170 self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID)
1171
1172 self.Debug = Debug
1173
1174 wx.EVT_PAINT(self, self.OnPaint)
1175 wx.EVT_SIZE(self, self.OnSize)
1176
1177 wx.EVT_LEFT_DOWN(self, self.LeftDownEvent )
1178 wx.EVT_LEFT_UP(self, self.LeftUpEvent )
1179 wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent )
1180 wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent )
1181 wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent )
1182 wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent )
1183 wx.EVT_RIGHT_DOWN(self, self.RightDownEvent)
1184 wx.EVT_RIGHT_UP(self, self.RightUpEvent )
1185 wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent )
1186 wx.EVT_MOTION(self, self.MotionEvent )
1187 wx.EVT_MOUSEWHEEL(self, self.WheelEvent )
1188
1189 ## CHB: I'm leaving these out for now.
1190 #wx.EVT_ENTER_WINDOW(self, self. )
1191 #wx.EVT_LEAVE_WINDOW(self, self. )
1192
1193 ## create the Hit Test Dicts:
1194 self.HitDict = None
1195
1196
1197 self._DrawList = []
1198 self._ForeDrawList = []
1199 self._ForegroundBuffer = None
1200 self.BoundingBox = None
1201 self.BoundingBoxDirty = False
1202 self.ViewPortCenter= array( (0,0), Float)
1203
1204 self.SetProjectionFun(ProjectionFun)
1205
1206 self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
1207 self.TransformVector = array( (1,-1), Float) # default Transformation
1208
1209 self.Scale = 1
1210
1211 self.GUIMode = None
1212 self.StartRBBox = None
1213 self.PrevRBBox = None
1214 self.StartMove = None
1215 self.PrevMoveXY = None
1216 self.ObjectUnderMouse = None
1217
1218 # called just to make sure everything is initialized
1219 self.OnSize(None)
1220
1221 self.InHereNum = 0
1222
1223 def SetProjectionFun(self,ProjectionFun):
1224 if ProjectionFun == 'FlatEarth':
1225 self.ProjectionFun = self.FlatEarthProjection
1226 elif type(ProjectionFun) == types.FunctionType:
1227 self.ProjectionFun = ProjectionFun
1228 elif ProjectionFun is None:
1229 self.ProjectionFun = lambda x=None: array( (1,1), Float)
1230 else:
1231 raise FloatCanvasException('Projectionfun must be either: "FlatEarth", None, or a function that takes the ViewPortCenter and returns a MapProjectionVector')
1232
1233 def FlatEarthProjection(self,CenterPoint):
1234 return array((cos(pi*CenterPoint[1]/180),1),Float)
1235
1236 def SetMode(self,Mode):
1237 if Mode in ["ZoomIn","ZoomOut","Move","Mouse",None]:
1238 self.GUIMode = Mode
1239 else:
1240 raise FloatCanvasException('"%s" is Not a valid Mode'%Mode)
1241
1242 def MakeHitDict(self):
1243 ##fixme: Should this just be None if nothing has been bound?
1244 self.HitDict = {EVT_FC_LEFT_DOWN: {},
1245 EVT_FC_LEFT_UP: {},
1246 EVT_FC_LEFT_DCLICK: {},
1247 EVT_FC_MIDDLE_DOWN: {},
1248 EVT_FC_MIDDLE_UP: {},
1249 EVT_FC_MIDDLE_DCLICK: {},
1250 EVT_FC_RIGHT_DOWN: {},
1251 EVT_FC_RIGHT_UP: {},
1252 EVT_FC_RIGHT_DCLICK: {},
1253 EVT_FC_ENTER_OBJECT: {},
1254 EVT_FC_LEAVE_OBJECT: {},
1255 }
1256
1257 def RaiseMouseEvent(self, Event, EventType):
1258 """
1259 This is called in various other places to raise a Mouse Event
1260 """
1261 #print "in Raise Mouse Event", Event
1262 pt = self.PixelToWorld( Event.GetPosition() )
1263 evt = MouseEvent(EventType, Event, self.GetId(), pt)
1264 self.GetEventHandler().ProcessEvent(evt)
1265
1266 def HitTest(self, event, HitEvent):
1267 if self.HitDict:
1268 # check if there are any objects in the dict for this event
1269 if self.HitDict[ HitEvent ]:
1270 xy = event.GetPosition()
1271 if self._ForegroundHTdc:
1272 hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
1273 else:
1274 hitcolor = self._HTdc.GetPixelPoint( xy )
1275 color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
1276 if color in self.HitDict[ HitEvent ]:
1277 Object = self.HitDict[ HitEvent ][color]
1278 ## Add the hit coords to the Object
1279 Object.HitCoords = self.PixelToWorld( xy )
1280 Object.CallBackFuncs[HitEvent](Object)
1281 return True
1282 return False
1283
1284 def MouseOverTest(self, event):
1285 ##fixme: Can this be cleaned up?
1286 if self.HitDict:
1287 xy = event.GetPosition()
1288 if self._ForegroundHTdc:
1289 hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
1290 else:
1291 hitcolor = self._HTdc.GetPixelPoint( xy )
1292 color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
1293 OldObject = self.ObjectUnderMouse
1294 ObjectCallbackCalled = False
1295 if color in self.HitDict[ EVT_FC_ENTER_OBJECT ]:
1296 Object = self.HitDict[ EVT_FC_ENTER_OBJECT][color]
1297 if (OldObject is None):
1298 try:
1299 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
1300 ObjectCallbackCalled = True
1301 except KeyError:
1302 pass # this means the enter event isn't bound for that object
1303 elif OldObject == Object: # the mouse is still on the same object
1304 pass
1305 ## Is the mouse on a differnt object as it was...
1306 elif not (Object == OldObject):
1307 # call the leave object callback
1308 try:
1309 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
1310 ObjectCallbackCalled = True
1311 except KeyError:
1312 pass # this means the leave event isn't bound for that object
1313 try:
1314 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
1315 ObjectCallbackCalled = True
1316 except KeyError:
1317 pass # this means the enter event isn't bound for that object
1318 ## set the new object under mouse
1319 self.ObjectUnderMouse = Object
1320 elif color in self.HitDict[ EVT_FC_LEAVE_OBJECT ]:
1321 Object = self.HitDict[ EVT_FC_LEAVE_OBJECT][color]
1322 self.ObjectUnderMouse = Object
1323 else:
1324 # no objects under mouse bound to mouse-over events
1325 self.ObjectUnderMouse = None
1326 if OldObject:
1327 try:
1328 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
1329 ObjectCallbackCalled = True
1330 except KeyError:
1331 pass # this means the leave event isn't bound for that object
1332 return ObjectCallbackCalled
1333
1334
1335 ## fixme: There is a lot of repeated code here
1336 ## Is there a better way?
1337 def LeftDoubleClickEvent(self,event):
1338 if self.GUIMode == "Mouse":
1339 EventType = EVT_FC_LEFT_DCLICK
1340 if not self.HitTest(event, EventType):
1341 self.RaiseMouseEvent(event, EventType)
1342
1343
1344 def MiddleDownEvent(self,event):
1345 if self.GUIMode == "Mouse":
1346 EventType = EVT_FC_MIDDLE_DOWN
1347 if not self.HitTest(event, EventType):
1348 self.RaiseMouseEvent(event, EventType)
1349
1350 def MiddleUpEvent(self,event):
1351 if self.GUIMode == "Mouse":
1352 EventType = EVT_FC_MIDDLE_UP
1353 if not self.HitTest(event, EventType):
1354 self.RaiseMouseEvent(event, EventType)
1355
1356 def MiddleDoubleClickEvent(self,event):
1357 if self.GUIMode == "Mouse":
1358 EventType = EVT_FC_MIDDLE_DCLICK
1359 if not self.HitTest(event, EventType):
1360 self.RaiseMouseEvent(event, EventType)
1361
1362 def RightUpEvent(self,event):
1363 if self.GUIMode == "Mouse":
1364 EventType = EVT_FC_RIGHT_UP
1365 if not self.HitTest(event, EventType):
1366 self.RaiseMouseEvent(event, EventType)
1367
1368 def RightDoubleCLickEvent(self,event):
1369 if self.GUIMode == "Mouse":
1370 EventType = EVT_FC_RIGHT_DCLICK
1371 if not self.HitTest(event, EventType):
1372 self.RaiseMouseEvent(event, EventType)
1373
1374 def WheelEvent(self,event):
1375 if self.GUIMode == "Mouse":
1376 self.RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL)
1377
1378
1379 def LeftDownEvent(self,event):
1380 if self.GUIMode:
1381 if self.GUIMode == "ZoomIn":
1382 self.StartRBBox = array( event.GetPosition() )
1383 self.PrevRBBox = None
1384 self.CaptureMouse()
1385 elif self.GUIMode == "ZoomOut":
1386 Center = self.PixelToWorld( event.GetPosition() )
1387 self.Zoom(1/1.5,Center)
1388 elif self.GUIMode == "Move":
1389 self.StartMove = array( event.GetPosition() )
1390 self.PrevMoveXY = (0,0)
1391 elif self.GUIMode == "Mouse":
1392 ## check for a hit
1393 if not self.HitTest(event, EVT_FC_LEFT_DOWN):
1394 self.RaiseMouseEvent(event,EVT_FC_LEFT_DOWN)
1395 else:
1396 pass
1397
1398 def LeftUpEvent(self,event):
1399 if self.HasCapture():
1400 self.ReleaseMouse()
1401 if self.GUIMode:
1402 if self.GUIMode == "ZoomIn":
1403 if event.LeftUp() and not self.StartRBBox is None:
1404 self.PrevRBBox = None
1405 EndRBBox = event.GetPosition()
1406 StartRBBox = self.StartRBBox
1407 # if mouse has moved less that ten pixels, don't use the box.
1408 if ( abs(StartRBBox[0] - EndRBBox[0]) > 10
1409 and abs(StartRBBox[1] - EndRBBox[1]) > 10 ):
1410 EndRBBox = self.PixelToWorld(EndRBBox)
1411 StartRBBox = self.PixelToWorld(StartRBBox)
1412 BB = array(((min(EndRBBox[0],StartRBBox[0]),
1413 min(EndRBBox[1],StartRBBox[1])),
1414 (max(EndRBBox[0],StartRBBox[0]),
1415 max(EndRBBox[1],StartRBBox[1]))),Float)
1416 self.ZoomToBB(BB)
1417 else:
1418 Center = self.PixelToWorld(StartRBBox)
1419 self.Zoom(1.5,Center)
1420 self.StartRBBox = None
1421 elif self.GUIMode == "Move":
1422 if not self.StartMove is None:
1423 StartMove = self.StartMove
1424 EndMove = array((event.GetX(),event.GetY()))
1425 if sum((StartMove-EndMove)**2) > 16:
1426 self.Move(StartMove-EndMove,'Pixel')
1427 self.StartMove = None
1428 elif self.GUIMode == "Mouse":
1429 EventType = EVT_FC_LEFT_UP
1430 if not self.HitTest(event, EventType):
1431 self.RaiseMouseEvent(event, EventType)
1432 else:
1433 pass
1434
1435 def MotionEvent(self,event):
1436 if self.GUIMode:
1437 if self.GUIMode == "ZoomIn":
1438 if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None):
1439 xy0 = self.StartRBBox
1440 xy1 = array( event.GetPosition() )
1441 wh = abs(xy1 - xy0)
1442 wh[0] = max(wh[0], int(wh[1]*self.AspectRatio))
1443 wh[1] = int(wh[0] / self.AspectRatio)
1444 xy_c = (xy0 + xy1) / 2
1445 dc = wx.ClientDC(self)
1446 dc.BeginDrawing()
1447 dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
1448 dc.SetBrush(wx.TRANSPARENT_BRUSH)
1449 dc.SetLogicalFunction(wx.XOR)
1450 if self.PrevRBBox:
1451 dc.DrawRectanglePointSize(*self.PrevRBBox)
1452 self.PrevRBBox = ( xy_c - wh/2, wh )
1453 dc.DrawRectanglePointSize( *self.PrevRBBox )
1454 dc.EndDrawing()
1455 elif self.GUIMode == "Move":
1456 if event.Dragging() and event.LeftIsDown() and not self.StartMove is None:
1457 xy1 = array( event.GetPosition() )
1458 wh = self.PanelSize
1459 xy_tl = xy1 - self.StartMove
1460 dc = wx.ClientDC(self)
1461 dc.BeginDrawing()
1462 x1,y1 = self.PrevMoveXY
1463 x2,y2 = xy_tl
1464 w,h = self.PanelSize
1465 if x2 > x1 and y2 > y1:
1466 xa = xb = x1
1467 ya = yb = y1
1468 wa = w
1469 ha = y2 - y1
1470 wb = x2- x1
1471 hb = h
1472 elif x2 > x1 and y2 <= y1:
1473 xa = x1
1474 ya = y1
1475 wa = x2 - x1
1476 ha = h
1477 xb = x1
1478 yb = y2 + h
1479 wb = w
1480 hb = y1 - y2
1481 elif x2 <= x1 and y2 > y1:
1482 xa = x1
1483 ya = y1
1484 wa = w
1485 ha = y2 - y1
1486 xb = x2 + w
1487 yb = y1
1488 wb = x1 - x2
1489 hb = h - y2 + y1
1490 elif x2 <= x1 and y2 <= y1:
1491 xa = x2 + w
1492 ya = y1
1493 wa = x1 - x2
1494 ha = h
1495 xb = x1
1496 yb = y2 + h
1497 wb = w
1498 hb = y1 - y2
1499
1500 dc.SetPen(wx.TRANSPARENT_PEN)
1501 dc.SetBrush(self.BackgroundBrush)
1502 dc.DrawRectangle(xa, ya, wa, ha)
1503 dc.DrawRectangle(xb, yb, wb, hb)
1504 self.PrevMoveXY = xy_tl
1505 if self._ForegroundBuffer:
1506 dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl)
1507 else:
1508 dc.DrawBitmapPoint(self._Buffer,xy_tl)
1509 dc.EndDrawing()
1510 elif self.GUIMode == "Mouse":
1511 ## Only do something if there are mouse over events bound
1512 if self.HitDict and (self.HitDict[ EVT_FC_ENTER_OBJECT ] or self.HitDict[ EVT_FC_LEAVE_OBJECT ] ):
1513 if not self.MouseOverTest(event):
1514 self.RaiseMouseEvent(event,EVT_FC_MOTION)
1515 else:
1516 pass
1517 self.RaiseMouseEvent(event,EVT_FC_MOTION)
1518 else:
1519 pass
1520
1521 def RightDownEvent(self,event):
1522 if self.GUIMode:
1523 if self.GUIMode == "ZoomIn":
1524 Center = self.PixelToWorld((event.GetX(),event.GetY()))
1525 self.Zoom(1/1.5,Center)
1526 elif self.GUIMode == "ZoomOut":
1527 Center = self.PixelToWorld((event.GetX(),event.GetY()))
1528 self.Zoom(1.5,Center)
1529 elif self.GUIMode == "Mouse":
1530 EventType = EVT_FC_RIGHT_DOWN
1531 if not self.HitTest(event, EventType):
1532 self.RaiseMouseEvent(event, EventType)
1533 else:
1534 pass
1535
1536 def MakeNewBuffers(self):
1537 self._BackgroundDirty = True
1538 # Make new offscreen bitmap:
1539 self._Buffer = wx.EmptyBitmap(*self.PanelSize)
1540 #dc = wx.MemoryDC()
1541 #dc.SelectObject(self._Buffer)
1542 #dc.Clear()
1543 if self._ForeDrawList:
1544 self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize)
1545 else:
1546 self._ForegroundBuffer = None
1547 if self.UseHitTest:
1548 self.MakeNewHTdc()
1549 else:
1550 self._HTdc = None
1551 self._ForegroundHTdc = None
1552
1553 def MakeNewHTdc(self):
1554 ## Note: While it's considered a "bad idea" to keep a
1555 ## MemoryDC around I'm doing it here because a wx.Bitmap
1556 ## doesn't have a GetPixel method so a DC is needed to do
1557 ## the hit-test. It didn't seem like a good idea to re-create
1558 ## a wx.MemoryDC on every single mouse event, so I keep it
1559 ## around instead
1560 self._HTdc = wx.MemoryDC()
1561 self._HTBitmap = wx.EmptyBitmap(*self.PanelSize)
1562 self._HTdc.SelectObject( self._HTBitmap )
1563 self._HTdc.SetBackground(wx.BLACK_BRUSH)
1564 if self._ForeDrawList:
1565 self._ForegroundHTdc = wx.MemoryDC()
1566 self._ForegroundHTBitmap = wx.EmptyBitmap(*self.PanelSize)
1567 self._ForegroundHTdc.SelectObject( self._ForegroundHTBitmap )
1568 self._ForegroundHTdc.SetBackground(wx.BLACK_BRUSH)
1569 else:
1570 self._ForegroundHTdc = None
1571
1572 def OnSize(self,event):
1573 self.PanelSize = array(self.GetClientSizeTuple(),Int32)
1574 self.HalfPanelSize = self.PanelSize / 2 # lrk: added for speed in WorldToPixel
1575 if self.PanelSize[0] == 0 or self.PanelSize[1] == 0:
1576 self.AspectRatio = 1.0
1577 else:
1578 self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1]
1579 self.MakeNewBuffers()
1580 self.Draw()
1581
1582 def OnPaint(self, event):
1583 dc = wx.PaintDC(self)
1584 if self._ForegroundBuffer:
1585 dc.DrawBitmap(self._ForegroundBuffer,0,0)
1586 else:
1587 dc.DrawBitmap(self._Buffer,0,0)
1588
1589 def Draw(self, Force=False):
1590 """
1591 There is a main buffer set up to double buffer the screen, so
1592 you can get quick re-draws when the window gets uncovered.
1593
1594 If there are any objects in self._ForeDrawList, then the
1595 background gets drawn to a new buffer, and the foreground
1596 objects get drawn on top of it. The final result if blitted to
1597 the screen, and stored for future Paint events. This is done so
1598 that you can have a complicated background, but have something
1599 changing on the foreground, without having to wait for the
1600 background to get re-drawn. This can be used to support simple
1601 animation, for instance.
1602
1603 """
1604 #print "In Draw"
1605 if self.Debug: start = clock()
1606 ScreenDC = wx.ClientDC(self)
1607 ViewPortWorld = ( self.PixelToWorld((0,0)),
1608 self.PixelToWorld(self.PanelSize) )
1609 ViewPortBB = array( ( minimum.reduce(ViewPortWorld),
1610 maximum.reduce(ViewPortWorld) ) )
1611 dc = wx.MemoryDC()
1612 dc.SelectObject(self._Buffer)
1613 if self._BackgroundDirty or Force:
1614 #print "Background is Dirty"
1615 dc.SetBackground(self.BackgroundBrush)
1616 dc.Clear()
1617 if self._HTdc:
1618 self._HTdc.Clear()
1619 self._DrawObjects(dc, self._DrawList, ScreenDC, ViewPortBB, self._HTdc)
1620 self._BackgroundDirty = False
1621
1622 if self._ForeDrawList:
1623 ## If an object was just added to the Foreground, there might not yet be a buffer
1624 if self._ForegroundBuffer is None:
1625 self._ForegroundBuffer = wx.EmptyBitmap(self.PanelSize[0],
1626 self.PanelSize[1])
1627
1628 dc = wx.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here
1629 dc.SelectObject(self._ForegroundBuffer)
1630 dc.DrawBitmap(self._Buffer,0,0)
1631 if self._ForegroundHTdc is None:
1632 self._ForegroundHTdc = wx.MemoryDC()
1633 self._ForegroundHTdc.SelectObject( wx.EmptyBitmap(
1634 self.PanelSize[0],
1635 self.PanelSize[1]) )
1636 if self._HTdc:
1637 ## blit the background HT buffer to the foreground HT buffer
1638 self._ForegroundHTdc.Blit(0, 0,
1639 self.PanelSize[0], self.PanelSize[1],
1640 self._HTdc, 0, 0)
1641 self._DrawObjects(dc,
1642 self._ForeDrawList,
1643 ScreenDC,
1644 ViewPortBB,
1645 self._ForegroundHTdc)
1646 ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0)
1647## wx.GetApp().Yield(True)
1648 # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn
1649 # This seeems out of place, but it works.
1650 if self.PrevRBBox:
1651 ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH))
1652 ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH)
1653 ScreenDC.SetLogicalFunction(wx.XOR)
1654 ScreenDC.DrawRectanglePointSize(*self.PrevRBBox)
1655 if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start)
1656
1657 ## Clear the font cache
1658 ## IF you don't do this, the X font server starts to take up Massive amounts of memory
1659 ## This is mostly a problem with very large fonts, that you get with scaled text when zoomed in.
1660 DrawObject.FontList = {}
1661
1662 def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck
1663 # lrk: Returns the objects that should be redrawn
1664
1665 BB2 = ViewPortBB
1666 redrawlist = []
1667 for Object in DrawList:
1668 BB1 = Object.BoundingBox
1669 if (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and
1670 BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1]):
1671 redrawlist.append(Object)
1672 return redrawlist
1673 _ShouldRedraw = staticmethod(_ShouldRedraw)
1674
1675
1676## def BBCheck(self, BB1, BB2):
1677## """
1678
1679## BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise
1680
1681## """
1682## if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and
1683## (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ):
1684## return True
1685## else:
1686## return False
1687
1688 def Move(self,shift,CoordType):
1689 """
1690 move the image in the window.
1691
1692 shift is an (x,y) tuple, specifying the amount to shift in each direction
1693
1694 It can be in any of three coordinates: Panel, Pixel, World,
1695 specified by the CoordType parameter
1696
1697 Panel coordinates means you want to shift the image by some
1698 fraction of the size of the displaed image
1699
1700 Pixel coordinates means you want to shift the image by some number of pixels
1701
1702 World coordinates mean you want to shift the image by an amount
1703 in Floating point world coordinates
1704
1705 """
1706
1707 shift = array(shift,Float)
1708 if CoordType == 'Panel':# convert from panel coordinates
1709 shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector
1710 elif CoordType == 'Pixel': # convert from pixel coordinates
1711 shift = shift/self.TransformVector
1712 elif CoordType == 'World': # No conversion
1713 pass
1714 else:
1715 raise FloatCanvasException('CoordType must be either "Panel", "Pixel", or "World"')
1716
1717 self.ViewPortCenter = self.ViewPortCenter + shift
1718 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
1719 self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
1720 self._BackgroundDirty = True
1721 self.Draw()
1722
1723 def Zoom(self,factor,center = None):
1724
1725 """
1726 Zoom(factor, center) changes the amount of zoom of the image by factor.
1727 If factor is greater than one, the image gets larger.
1728 If factor is less than one, the image gets smaller.
1729
1730 Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming.
1731 If center is not given, the center will stay the same.
1732
1733 """
1734 self.Scale = self.Scale*factor
1735 if not center is None:
1736 self.ViewPortCenter = array(center,Float)
1737 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
1738 self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
1739 self._BackgroundDirty = True
1740 self.Draw()
1741
1742 def ZoomToBB(self, NewBB = None, DrawFlag = True):
1743
1744 """
1745
1746 Zooms the image to the bounding box given, or to the bounding
1747 box of all the objects on the canvas, if none is given.
1748
1749 """
1750
1751 if not NewBB is None:
1752 BoundingBox = NewBB
1753 else:
1754 if self.BoundingBoxDirty:
1755 self._ResetBoundingBox()
1756 BoundingBox = self.BoundingBox
1757 if not BoundingBox is None:
1758 self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2,
1759 (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float)
1760 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
1761 # Compute the new Scale
1762 BoundingBox = BoundingBox * self.MapProjectionVector
1763 try:
1764 self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])),
1765 abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )*0.95
1766 except ZeroDivisionError: # this will happen if the BB has zero width or height
1767 try: #width == 0
1768 self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95
1769 except ZeroDivisionError:
1770 try: # height == 0
1771 self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95
1772 except ZeroDivisionError: #zero size! (must be a single point)
1773 self.Scale = 1
1774
1775 self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector
1776 if DrawFlag:
1777 self._BackgroundDirty = True
1778 self.Draw()
1779 else:
1780 # Reset the shifting and scaling to defaults when there is no BB
1781 self.ViewPortCenter= array( (0,0), Float)
1782 self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
1783 self.TransformVector = array( (1,-1), Float) # default Transformation
1784 self.Scale = 1
1785
1786 def RemoveObjects(self, Objects):
1787 for Object in Objects:
1788 self.RemoveObject(Object, ResetBB = False)
1789 self.BoundingBoxDirty = True
1790
1791 def RemoveObject(self, Object, ResetBB = True):
1792 ##fixme: Using the list.remove method is kind of slow
1793 if Object.InForeground:
1794 self._ForeDrawList.remove(Object)
1795 else:
1796 self._DrawList.remove(Object)
1797 self._BackgroundDirty = True
1798 if ResetBB:
1799 self.BoundingBoxDirty = True
1800
1801 def ClearAll(self, ResetBB = True):
1802 self._DrawList = []
1803 self._ForeDrawList = []
1804 self._BackgroundDirty = True
1805 self.HitColorGenerator = None
1806 self.UseHitTest = False
1807 if ResetBB:
1808 self._ResetBoundingBox()
1809 self.MakeNewBuffers()
1810 self.HitDict = None
1811
1812## No longer called
1813## def _AddBoundingBox(self,NewBB):
1814## if self.BoundingBox is None:
1815## self.BoundingBox = NewBB
1816## self.ZoomToBB(NewBB,DrawFlag = False)
1817## else:
1818## self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]),
1819## min(self.BoundingBox[0,1],NewBB[0,1])),
1820## (max(self.BoundingBox[1,0],NewBB[1,0]),
1821## max(self.BoundingBox[1,1],NewBB[1,1]))),
1822## Float)
1823
1824 def _getboundingbox(bboxarray): # lrk: added this
1825
1826 upperleft = minimum.reduce(bboxarray[:,0])
1827 lowerright = maximum.reduce(bboxarray[:,1])
1828 return array((upperleft, lowerright), Float)
1829
1830 _getboundingbox = staticmethod(_getboundingbox)
1831
1832 def _ResetBoundingBox(self):
1833 if self._DrawList or self._ForeDrawList:
1834 bboxarray = zeros((len(self._DrawList)+len(self._ForeDrawList), 2, 2),Float)
1835 i = -1 # just in case _DrawList is empty
1836 for (i, BB) in enumerate(self._DrawList):
1837 bboxarray[i] = BB.BoundingBox
1838 for (j, BB) in enumerate(self._ForeDrawList):
1839 bboxarray[i+j+1] = BB.BoundingBox
1840 self.BoundingBox = self._getboundingbox(bboxarray)
1841 else:
1842 self.BoundingBox = None
1843 self.ViewPortCenter= array( (0,0), Float)
1844 self.TransformVector = array( (1,-1), Float)
1845 self.MapProjectionVector = array( (1,1), Float)
1846 self.Scale = 1
1847 self.BoundingBoxDirty = False
1848
1849 def PixelToWorld(self,Points):
1850 """
1851 Converts coordinates from Pixel coordinates to world coordinates.
1852
1853 Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates.
1854
1855 """
1856 return (((asarray(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter)
1857
1858 def WorldToPixel(self,Coordinates):
1859 """
1860 This function will get passed to the drawing functions of the objects,
1861 to transform from world to pixel coordinates.
1862 Coordinates should be a NX2 array of (x,y) coordinates, or
1863 a 2-tuple, or sequence of 2-tuples.
1864 """
1865 #Note: this can be called by users code for various reasons, so asarray is needed.
1866 return (((asarray(Coordinates,Float) - self.ViewPortCenter)*self.TransformVector)+(self.HalfPanelSize)).astype('i')
1867
1868 def ScaleWorldToPixel(self,Lengths):
1869 """
1870 This function will get passed to the drawing functions of the objects,
1871 to Change a length from world to pixel coordinates.
1872
1873 Lengths should be a NX2 array of (x,y) coordinates, or
1874 a 2-tuple, or sequence of 2-tuples.
1875 """
1876 return ( (asarray(Lengths,Float)*self.TransformVector) ).astype('i')
1877
1878 def ScalePixelToWorld(self,Lengths):
1879 """
1880 This function computes a pair of x.y lengths,
1881 to change then from pixel to world coordinates.
1882
1883 Lengths should be a NX2 array of (x,y) coordinates, or
1884 a 2-tuple, or sequence of 2-tuples.
1885 """
1886
1887 return (asarray(Lengths,Float) / self.TransformVector)
1888
1889 def AddObject(self,obj):
1890 # put in a reference to the Canvas, so remove and other stuff can work
1891 obj._Canvas = self
1892 if obj.InForeground:
1893 self._ForeDrawList.append(obj)
1894 self.UseForeground = True
1895 else:
1896 self._DrawList.append(obj)
1897 self._BackgroundDirty = True
1898 self.BoundingBoxDirty = True
1899 return True
1900
1901 def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None):
1902 """
1903 This is a convenience function;
1904 This function takes the list of objects and draws them to specified
1905 device context.
1906 """
1907 dc.SetBackground(self.BackgroundBrush)
1908 dc.BeginDrawing()
1909 #i = 0
1910 PanelSize0, PanelSize1 = self.PanelSize # for speed
1911 WorldToPixel = self.WorldToPixel # for speed
1912 ScaleWorldToPixel = self.ScaleWorldToPixel # for speed
1913 Blit = ScreenDC.Blit # for speed
1914 NumBetweenBlits = self.NumBetweenBlits # for speed
1915 for i, Object in enumerate(self._ShouldRedraw(DrawList, ViewPortBB)):
1916 Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc)
1917 if i % NumBetweenBlits == 0:
1918 Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0)
1919 dc.EndDrawing()
1920
1921## ## This is a way to automatically add a AddObject method for each
1922## ## object type This code has been replaced by Leo's code above, so
1923## ## that it happens at module init, rather than as needed. The
1924## ## primary advantage of this is that dir(FloatCanvas) will have
1925## ## them, and docstrings are preserved. Probably more useful
1926## ## exceptions if there is a problem, as well.
1927## def __getattr__(self, name):
1928## if name[:3] == "Add":
1929## func=globals()[name[3:]]
1930## def AddFun(*args, **kwargs):
1931## Object = func(*args, **kwargs)
1932## self.AddObject(Object)
1933## return Object
1934## ## add it to FloatCanvas' dict for future calls.
1935## self.__dict__[name] = AddFun
1936## return AddFun
1937## else:
1938## raise AttributeError("FloatCanvas has no attribute '%s'"%name)
1939
1940def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__
1941 classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon",
1942 "Line", "Text", "PointSet"]
1943 for classname in classnames:
1944 klass = globals()[classname]
1945 def getaddshapemethod(klass=klass):
1946 def addshape(self, *args, **kwargs):
1947 Object = klass(*args, **kwargs)
1948 self.AddObject(Object)
1949 return Object
1950 return addshape
1951 addshapemethod = getaddshapemethod()
1952 methodname = "Add" + classname
1953 setattr(FloatCanvas, methodname, addshapemethod)
1954 docstring = "Creates %s and adds its reference to the canvas.\n" % classname
1955 docstring += "Argument protocol same as %s class" % classname
1956 if klass.__doc__:
1957 docstring += ", whose docstring is:\n%s" % klass.__doc__
1958 FloatCanvas.__dict__[methodname].__doc__ = docstring
1959
1960_makeFloatCanvasAddMethods()
1961
1962