]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/floatcanvas/FloatCanvas.py
eea063af650a969aed550d3d6949fd223a3d3c6f
[wxWidgets.git] / wxPython / wx / lib / floatcanvas / FloatCanvas.py
1 from __future__ import division
2
3 try:
4 from Numeric import array,asarray,Float,cos, sin, pi,sum,minimum,maximum,Int32,zeros, ones, concatenate, sqrt, argmin, power, absolute, matrixmultiply, transpose, sometrue, arange, hypot
5 except ImportError:
6 try:
7 from numarray import array, asarray, Float, cos, sin, pi, sum, minimum, maximum, Int32, zeros, concatenate, matrixmultiply, transpose, sometrue, arange, hypot
8 except ImportError:
9 raise ImportError("I could not import either Numeric or numarray")
10
11 from time import clock, sleep
12
13 import Resources # A file with icons, etc for FloatCanvas
14
15 import wx
16
17 import types
18 import os
19
20 ## A global variable to hold the Pixels per inch that wxWindows thinks is in use
21 ## This is used for scaling fonts.
22 ## This can't be computed on module __init__, because a wx.App might not have initialized yet.
23 global ScreenPPI
24
25 ## a custom Exceptions:
26
27 class FloatCanvasError(Exception):
28 pass
29
30 ## Create all the mouse events
31 # I don't see a need for these two, but maybe some day!
32 #EVT_FC_ENTER_WINDOW = wx.NewEventType()
33 #EVT_FC_LEAVE_WINDOW = wx.NewEventType()
34 EVT_FC_LEFT_DOWN = wx.NewEventType()
35 EVT_FC_LEFT_UP = wx.NewEventType()
36 EVT_FC_LEFT_DCLICK = wx.NewEventType()
37 EVT_FC_MIDDLE_DOWN = wx.NewEventType()
38 EVT_FC_MIDDLE_UP = wx.NewEventType()
39 EVT_FC_MIDDLE_DCLICK = wx.NewEventType()
40 EVT_FC_RIGHT_DOWN = wx.NewEventType()
41 EVT_FC_RIGHT_UP = wx.NewEventType()
42 EVT_FC_RIGHT_DCLICK = wx.NewEventType()
43 EVT_FC_MOTION = wx.NewEventType()
44 EVT_FC_MOUSEWHEEL = wx.NewEventType()
45 ## these two are for the hit-test stuff, I never make them real Events
46 EVT_FC_ENTER_OBJECT = wx.NewEventType()
47 EVT_FC_LEAVE_OBJECT = wx.NewEventType()
48
49 ##Create all mouse event binding functions
50 #def EVT_ENTER_WINDOW( window, function ):
51 # window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function )
52 #def EVT_LEAVE_WINDOW( window, function ):
53 # window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function )
54 def EVT_LEFT_DOWN( window, function ):
55 window.Connect( -1, -1,EVT_FC_LEFT_DOWN , function )
56 def EVT_LEFT_UP( window, function ):
57 window.Connect( -1, -1,EVT_FC_LEFT_UP , function )
58 def EVT_LEFT_DCLICK ( window, function ):
59 window.Connect( -1, -1,EVT_FC_LEFT_DCLICK , function )
60 def EVT_MIDDLE_DOWN ( window, function ):
61 window.Connect( -1, -1,EVT_FC_MIDDLE_DOWN , function )
62 def EVT_MIDDLE_UP ( window, function ):
63 window.Connect( -1, -1,EVT_FC_MIDDLE_UP , function )
64 def EVT_MIDDLE_DCLICK ( window, function ):
65 window.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK , function )
66 def EVT_RIGHT_DOWN ( window, function ):
67 window.Connect( -1, -1,EVT_FC_RIGHT_DOWN , function )
68 def EVT_RIGHT_UP( window, function ):
69 window.Connect( -1, -1,EVT_FC_RIGHT_UP , function )
70 def EVT_RIGHT_DCLICK( window, function ):
71 window.Connect( -1, -1,EVT_FC_RIGHT_DCLICK , function )
72 def EVT_MOTION( window, function ):
73 window.Connect( -1, -1,EVT_FC_MOTION , function )
74 def EVT_MOUSEWHEEL( window, function ):
75 window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function )
76
77 class _MouseEvent(wx.PyCommandEvent):
78
79 """
80
81 This event class takes a regular wxWindows mouse event as a parameter,
82 and wraps it so that there is access to all the original methods. This
83 is similar to subclassing, but you can't subclass a wxWindows event
84
85 The goal is to be able to it just like a regular mouse event.
86
87 It adds the method:
88
89 GetCoords() , which returns and (x,y) tuple in world coordinates.
90
91 Another difference is that it is a CommandEvent, which propagates up
92 the window hierarchy until it is handled.
93
94 """
95
96 def __init__(self, EventType, NativeEvent, WinID, Coords = None):
97 wx.PyCommandEvent.__init__(self)
98
99 self.SetEventType( EventType )
100 self._NativeEvent = NativeEvent
101 self.Coords = Coords
102
103 # I don't think this is used.
104 # def SetCoords(self,Coords):
105 # self.Coords = Coords
106
107 def GetCoords(self):
108 return self.Coords
109
110 def __getattr__(self, name):
111 #return eval(self.NativeEvent.__getattr__(name) )
112 return getattr(self._NativeEvent, name)
113
114 def _cycleidxs(indexcount, maxvalue, step):
115
116 """
117 Utility function used by _colorGenerator
118
119 """
120 if indexcount == 0:
121 yield ()
122 else:
123 for idx in xrange(0, maxvalue, step):
124 for tail in _cycleidxs(indexcount - 1, maxvalue, step):
125 yield (idx, ) + tail
126
127 def _colorGenerator():
128
129 """
130
131 Generates a seris of unique colors used to do hit-tests with the HIt
132 Test bitmap
133
134 """
135 import sys
136 if sys.platform == 'darwin':
137 depth = 24
138 else:
139 b = wx.EmptyBitmap(1,1)
140 depth = b.GetDepth()
141 if depth == 16:
142 step = 8
143 elif depth >= 24:
144 step = 1
145 else:
146 raise "ColorGenerator does not work with depth = %s" % depth
147 return _cycleidxs(indexcount=3, maxvalue=256, step=step)
148
149
150 #### I don't know if the Set objects are useful, beyond the pointset
151 #### object. The problem is that when zoomed in, the BB is checked to see
152 #### whether to draw the object. A Set object can defeat this. One day
153 #### I plan to write some custon C++ code to draw sets of objects
154
155 ##class ObjectSetMixin:
156 ## """
157 ## A mix-in class for draw objects that are sets of objects
158
159 ## It contains methods for setting lists of pens and brushes
160
161 ## """
162 ## def SetPens(self,LineColors,LineStyles,LineWidths):
163 ## """
164 ## This method used when an object could have a list of pens, rather than just one
165 ## It is used for LineSet, and perhaps others in the future.
166
167 ## fixme: this should be in a mixin
168
169 ## fixme: this is really kludgy, there has got to be a better way!
170
171 ## """
172
173 ## length = 1
174 ## if type(LineColors) == types.ListType:
175 ## length = len(LineColors)
176 ## else:
177 ## LineColors = [LineColors]
178
179 ## if type(LineStyles) == types.ListType:
180 ## length = len(LineStyles)
181 ## else:
182 ## LineStyles = [LineStyles]
183
184 ## if type(LineWidths) == types.ListType:
185 ## length = len(LineWidths)
186 ## else:
187 ## LineWidths = [LineWidths]
188
189 ## if length > 1:
190 ## if len(LineColors) == 1:
191 ## LineColors = LineColors*length
192 ## if len(LineStyles) == 1:
193 ## LineStyles = LineStyles*length
194 ## if len(LineWidths) == 1:
195 ## LineWidths = LineWidths*length
196
197 ## self.Pens = []
198 ## for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths):
199 ## if LineColor is None or LineStyle is None:
200 ## self.Pens.append(wx.TRANSPARENT_PEN)
201 ## # what's this for?> self.LineStyle = 'Transparent'
202 ## if not self.PenList.has_key((LineColor,LineStyle,LineWidth)):
203 ## Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle])
204 ## self.Pens.append(Pen)
205 ## else:
206 ## self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)])
207 ## if length == 1:
208 ## self.Pens = self.Pens[0]
209
210 class DrawObject:
211 """
212 This is the base class for all the objects that can be drawn.
213
214 One must subclass from this (and an assortment of Mixins) to create
215 a new DrawObject.
216
217 """
218
219 def __init__(self, InForeground = False, IsVisible = True):
220 self.InForeground = InForeground
221
222 self._Canvas = None
223
224 self.HitColor = None
225 self.CallBackFuncs = {}
226
227 ## these are the defaults
228 self.HitAble = False
229 self.HitLine = True
230 self.HitFill = True
231 self.MinHitLineWidth = 3
232 self.HitLineWidth = 3 ## this gets re-set by the subclasses if necessary
233
234 self.Brush = None
235 self.Pen = None
236
237 self.FillStyle = "Solid"
238
239 self.Visible = IsVisible
240
241 # I pre-define all these as class variables to provide an easier
242 # interface, and perhaps speed things up by caching all the Pens
243 # and Brushes, although that may not help, as I think wx now
244 # does that on it's own. Send me a note if you know!
245
246 BrushList = {
247 ( None,"Transparent") : wx.TRANSPARENT_BRUSH,
248 ("Blue","Solid") : wx.BLUE_BRUSH,
249 ("Green","Solid") : wx.GREEN_BRUSH,
250 ("White","Solid") : wx.WHITE_BRUSH,
251 ("Black","Solid") : wx.BLACK_BRUSH,
252 ("Grey","Solid") : wx.GREY_BRUSH,
253 ("MediumGrey","Solid") : wx.MEDIUM_GREY_BRUSH,
254 ("LightGrey","Solid") : wx.LIGHT_GREY_BRUSH,
255 ("Cyan","Solid") : wx.CYAN_BRUSH,
256 ("Red","Solid") : wx.RED_BRUSH
257 }
258 PenList = {
259 (None,"Transparent",1) : wx.TRANSPARENT_PEN,
260 ("Green","Solid",1) : wx.GREEN_PEN,
261 ("White","Solid",1) : wx.WHITE_PEN,
262 ("Black","Solid",1) : wx.BLACK_PEN,
263 ("Grey","Solid",1) : wx.GREY_PEN,
264 ("MediumGrey","Solid",1) : wx.MEDIUM_GREY_PEN,
265 ("LightGrey","Solid",1) : wx.LIGHT_GREY_PEN,
266 ("Cyan","Solid",1) : wx.CYAN_PEN,
267 ("Red","Solid",1) : wx.RED_PEN
268 }
269
270 FillStyleList = {
271 "Transparent" : wx.TRANSPARENT,
272 "Solid" : wx.SOLID,
273 "BiDiagonalHatch": wx.BDIAGONAL_HATCH,
274 "CrossDiagHatch" : wx.CROSSDIAG_HATCH,
275 "FDiagonal_Hatch": wx.FDIAGONAL_HATCH,
276 "CrossHatch" : wx.CROSS_HATCH,
277 "HorizontalHatch": wx.HORIZONTAL_HATCH,
278 "VerticalHatch" : wx.VERTICAL_HATCH
279 }
280
281 LineStyleList = {
282 "Solid" : wx.SOLID,
283 "Transparent": wx.TRANSPARENT,
284 "Dot" : wx.DOT,
285 "LongDash" : wx.LONG_DASH,
286 "ShortDash" : wx.SHORT_DASH,
287 "DotDash" : wx.DOT_DASH,
288 }
289
290 def Bind(self, Event, CallBackFun):
291 self.CallBackFuncs[Event] = CallBackFun
292 self.HitAble = True
293 self._Canvas.UseHitTest = True
294 if not self._Canvas._HTdc:
295 self._Canvas.MakeNewHTdc()
296 if not self.HitColor:
297 if not self._Canvas.HitColorGenerator:
298 self._Canvas.HitColorGenerator = _colorGenerator()
299 self._Canvas.HitColorGenerator.next() # first call to prevent the background color from being used.
300 self.HitColor = self._Canvas.HitColorGenerator.next()
301 self.SetHitPen(self.HitColor,self.HitLineWidth)
302 self.SetHitBrush(self.HitColor)
303 # put the object in the hit dict, indexed by it's color
304 if not self._Canvas.HitDict:
305 self._Canvas.MakeHitDict()
306 self._Canvas.HitDict[Event][self.HitColor] = (self) # put the object in the hit dict, indexed by it's color
307
308 def UnBindAll(self):
309 ## fixme: this only removes one from each list, there could be more.
310 if self._Canvas.HitDict:
311 for List in self._Canvas.HitDict.itervalues():
312 try:
313 List.remove(self)
314 except ValueError:
315 pass
316 self.HitAble = False
317
318
319 def SetBrush(self,FillColor,FillStyle):
320 if FillColor is None or FillStyle is None:
321 self.Brush = wx.TRANSPARENT_BRUSH
322 ##fixme: should I really re-set the style?
323 self.FillStyle = "Transparent"
324 else:
325 self.Brush = self.BrushList.setdefault( (FillColor,FillStyle), wx.Brush(FillColor,self.FillStyleList[FillStyle] ) )
326
327 def SetPen(self,LineColor,LineStyle,LineWidth):
328 if (LineColor is None) or (LineStyle is None):
329 self.Pen = wx.TRANSPARENT_PEN
330 self.LineStyle = 'Transparent'
331 else:
332 self.Pen = self.PenList.setdefault( (LineColor,LineStyle,LineWidth), wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) )
333
334 def SetHitBrush(self,HitColor):
335 if not self.HitFill:
336 self.HitBrush = wx.TRANSPARENT_BRUSH
337 else:
338 self.HitBrush = self.BrushList.setdefault( (HitColor,"solid"), wx.Brush(HitColor,self.FillStyleList["Solid"] ) )
339
340 def SetHitPen(self,HitColor,LineWidth):
341 if not self.HitLine:
342 self.HitPen = wx.TRANSPARENT_PEN
343 else:
344 self.HitPen = self.PenList.setdefault( (HitColor, "solid", self.HitLineWidth), wx.Pen(HitColor, self.HitLineWidth, self.LineStyleList["Solid"]) )
345
346 def PutInBackground(self):
347 if self._Canvas and self.InForeground:
348 self._Canvas._ForeDrawList.remove(self)
349 self._Canvas._DrawList.append(self)
350 self._Canvas._BackgroundDirty = True
351 self.InForeground = False
352
353 def PutInForeground(self):
354 if self._Canvas and (not self.InForeground):
355 self._Canvas._ForeDrawList.append(self)
356 self._Canvas._DrawList.remove(self)
357 self._Canvas._BackgroundDirty = True
358 self.InForeground = True
359
360 def Hide(self):
361 self.Visible = False
362
363 def Show(self):
364 self.Visible = True
365
366 class ColorOnlyMixin:
367 """
368
369 Mixin class for objects that have just one color, rather than a fill
370 color and line color
371
372 """
373
374 def SetColor(self, Color):
375 self.SetPen(Color,"Solid",1)
376 self.SetBrush(Color,"Solid")
377
378 SetFillColor = SetColor # Just to provide a consistant interface
379
380 class LineOnlyMixin:
381 """
382
383 Mixin class for objects that have just one color, rather than a fill
384 color and line color
385
386 """
387
388 def SetLineColor(self, LineColor):
389 self.LineColor = LineColor
390 self.SetPen(LineColor,self.LineStyle,self.LineWidth)
391
392 def SetLineStyle(self, LineStyle):
393 self.LineStyle = LineStyle
394 self.SetPen(self.LineColor,LineStyle,self.LineWidth)
395
396 def SetLineWidth(self, LineWidth):
397 self.LineWidth = LineWidth
398 self.SetPen(self.LineColor,self.LineStyle,LineWidth)
399
400 class LineAndFillMixin(LineOnlyMixin):
401 """
402
403 Mixin class for objects that have both a line and a fill color and
404 style.
405
406 """
407 def SetFillColor(self, FillColor):
408 self.FillColor = FillColor
409 self.SetBrush(FillColor, self.FillStyle)
410
411 def SetFillStyle(self, FillStyle):
412 self.FillStyle = FillStyle
413 self.SetBrush(self.FillColor,FillStyle)
414
415 class XYObjectMixin:
416 """
417
418 This is a mixin class that provides some methods suitable for use
419 with objects that have a single (x,y) coordinate pair.
420
421 """
422
423 def Move(self, Delta ):
424 """
425
426 Move(Delta): moves the object by delta, where delta is a
427 (dx,dy) pair. Ideally a Numpy array of shape (2,)
428
429 """
430
431 Delta = asarray(Delta, Float)
432 self.XY += Delta
433 self.BoundingBox = self.BoundingBox + Delta
434
435 if self._Canvas:
436 self._Canvas.BoundingBoxDirty = True
437
438 def CalcBoundingBox(self):
439 ## This may get overwritten in some subclasses
440 self.BoundingBox = array( (self.XY, self.XY), Float )
441
442 def SetPoint(self, xy):
443 xy = array( xy, Float)
444 xy.shape = (2,)
445 Delta = xy - self.XY
446
447 self.XY = xy
448 self.BoundingBox = self.BoundingBox + Delta
449
450 #self.CalcBoundingBox()
451 if self._Canvas:
452 self._Canvas.BoundingBoxDirty = True
453
454 class PointsObjectMixin:
455 """
456
457 This is a mixin class that provides some methods suitable for use
458 with objects that have a set of (x,y) coordinate pairs.
459
460 """
461
462
463 ## This is code for the PointsObjectMixin object, it needs to be adapted and tested.
464 ## Is the neccesary at all: you can always do:
465 ## Object.SetPoints( Object.Points + delta, copy = False)
466 ## def Move(self, Delta ):
467 ## """
468
469 ## Move(Delta): moves the object by delta, where delta is an (dx,
470 ## dy) pair. Ideally a Numpy array of shape (2,)
471
472 ## """
473
474 ## Delta = array(Delta, Float)
475 ## self.XY += Delta
476 ## self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float)
477 ## if self._Canvas:
478 ## self._Canvas.BoundingBoxDirty = True
479
480 def CalcBoundingBox(self):
481 self.BoundingBox = array(((min(self.Points[:,0]),
482 min(self.Points[:,1]) ),
483 (max(self.Points[:,0]),
484 max(self.Points[:,1]) ) ), Float )
485 if self._Canvas:
486 self._Canvas.BoundingBoxDirty = True
487
488 def SetPoints(self, Points, copy = True):
489 """
490 Sets the coordinates of the points of the object to Points (NX2 array).
491
492 By default, a copy is made, if copy is set to False, a reference
493 is used, iff Points is a NumPy array of Floats. This allows you
494 to change some or all of the points without making any copies.
495
496 For example:
497
498 Points = Object.Points
499 Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir.
500 Object.SetPoints(Points, False) # Sets the points to the same array as it was
501
502 """
503 if copy:
504 self.Points = array(Points, Float)
505 self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point
506 else:
507 self.Points = asarray(Points, Float)
508 self.CalcBoundingBox()
509
510
511 class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin):
512
513 """
514
515 The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of
516 point coordinates. so that Points[N][0] is the x-coordinate of
517 point N and Points[N][1] is the y-coordinate or Points[N,0] is the
518 x-coordinate of point N and Points[N,1] is the y-coordinate for
519 arrays.
520
521 The other parameters specify various properties of the Polygon, and
522 should be self explanatory.
523
524 """
525 def __init__(self,
526 Points,
527 LineColor = "Black",
528 LineStyle = "Solid",
529 LineWidth = 1,
530 FillColor = None,
531 FillStyle = "Solid",
532 InForeground = False):
533 DrawObject.__init__(self,InForeground)
534 self.Points = array(Points,Float) # this DOES need to make a copy
535 self.CalcBoundingBox()
536
537 self.LineColor = LineColor
538 self.LineStyle = LineStyle
539 self.LineWidth = LineWidth
540 self.FillColor = FillColor
541 self.FillStyle = FillStyle
542
543 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
544
545 self.SetPen(LineColor,LineStyle,LineWidth)
546 self.SetBrush(FillColor,FillStyle)
547
548 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None):
549 Points = WorldToPixel(self.Points)#.tolist()
550 dc.SetPen(self.Pen)
551 dc.SetBrush(self.Brush)
552 dc.DrawPolygon(Points)
553 if HTdc and self.HitAble:
554 HTdc.SetPen(self.HitPen)
555 HTdc.SetBrush(self.HitBrush)
556 HTdc.DrawPolygon(Points)
557
558 ##class PolygonSet(DrawObject):
559 ## """
560 ## The PolygonSet class takes a Geometry.Polygon object.
561 ## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
562
563 ## it creates a set of line segments, from (x1,y1) to (x2,y2)
564
565 ## """
566
567 ## def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False):
568 ## DrawObject.__init__(self, InForeground)
569
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 ## self.FillColors = FillColors
580 ## self.FillStyles = FillStyles
581
582 ## self.SetPens(LineColors,LineStyles,LineWidths)
583
584 ## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
585 ## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
586 ## Points = WorldToPixel(self.Points)
587 ## Points.shape = (-1,4)
588 ## dc.DrawLineList(Points,self.Pens)
589
590
591 class Line(DrawObject,PointsObjectMixin,LineOnlyMixin):
592 """
593
594 The Line class takes a list of 2-tuples, or a NX2 NumPy Float array
595 of point coordinates.
596
597 It will draw a straight line if there are two points, and a polyline
598 if there are more than two.
599
600 """
601 def __init__(self,Points,
602 LineColor = "Black",
603 LineStyle = "Solid",
604 LineWidth = 1,
605 InForeground = False):
606 DrawObject.__init__(self, InForeground)
607
608
609 self.Points = array(Points,Float)
610 self.CalcBoundingBox()
611
612 self.LineColor = LineColor
613 self.LineStyle = LineStyle
614 self.LineWidth = LineWidth
615
616 self.SetPen(LineColor,LineStyle,LineWidth)
617
618 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
619
620
621 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
622 Points = WorldToPixel(self.Points)
623 dc.SetPen(self.Pen)
624 dc.DrawLines(Points)
625 if HTdc and self.HitAble:
626 HTdc.SetPen(self.HitPen)
627 HTdc.DrawLines(Points)
628
629 class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin):
630 """
631
632 Arrow(XY, # coords of origin of arrow (x,y)
633 Length, # length of arrow in pixels
634 theta, # angle of arrow in degrees: zero is straight up
635 # angle is to the right
636 LineColor = "Black",
637 LineStyle = "Solid",
638 LineWidth = 1,
639 ArrowHeadSize = 4,
640 ArrowHeadAngle = 45,
641 InForeground = False):
642
643 It will draw an arrow , starting at the point, (X,Y) pointing in
644 direction, theta.
645
646
647 """
648 def __init__(self,
649 XY,
650 Length,
651 Direction,
652 LineColor = "Black",
653 LineStyle = "Solid",
654 LineWidth = 2, # pixels
655 ArrowHeadSize = 8, # pixels
656 ArrowHeadAngle = 30, # degrees
657 InForeground = False):
658
659 DrawObject.__init__(self, InForeground)
660
661 self.XY = array(XY, Float)
662 self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
663 self.Length = Length
664 self.Direction = float(Direction)
665 self.ArrowHeadSize = ArrowHeadSize
666 self.ArrowHeadAngle = float(ArrowHeadAngle)
667
668 self.CalcArrowPoints()
669 self.CalcBoundingBox()
670
671 self.LineColor = LineColor
672 self.LineStyle = LineStyle
673 self.LineWidth = LineWidth
674
675 self.SetPen(LineColor,LineStyle,LineWidth)
676
677 ##fixme: How should the HitTest be drawn?
678 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
679
680 def SetDirection(self, Direction):
681 self.Direction = float(Direction)
682 self.CalcArrowPoints()
683
684 def SetLength(self, Length):
685 self.Length = Length
686 self.CalcArrowPoints()
687
688 def SetLengthDirection(self, Length, Direction):
689 self.Direction = float(Direction)
690 self.Length = Length
691 self.CalcArrowPoints()
692
693 def SetLength(self, Length):
694 self.Length = Length
695 self.CalcArrowPoints()
696
697 ## fixme: cache this?
698 def CalcArrowPoints(self):
699 L = self.Length
700 S = self.ArrowHeadSize
701 phi = self.ArrowHeadAngle * pi / 360
702 theta = (self.Direction-90.0) * pi / 180
703 ArrowPoints = array( ( (0, L, L - S*cos(phi),L, L - S*cos(phi) ),
704 (0, 0, S*sin(phi), 0, -S*sin(phi) ) ),
705 Float )
706 RotationMatrix = array( ( ( cos(theta), -sin(theta) ),
707 ( sin(theta), cos(theta) ) ),
708 Float
709 )
710 ArrowPoints = matrixmultiply(RotationMatrix, ArrowPoints)
711 self.ArrowPoints = transpose(ArrowPoints)
712
713 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
714 dc.SetPen(self.Pen)
715 xy = WorldToPixel(self.XY)
716 ArrowPoints = xy + self.ArrowPoints
717 dc.DrawLines(ArrowPoints)
718 if HTdc and self.HitAble:
719 HTdc.SetPen(self.HitPen)
720 HTdc.DrawLines(ArrowPoints)
721
722 ##class LineSet(DrawObject, ObjectSetMixin):
723 ## """
724 ## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates.
725 ## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
726
727 ## it creates a set of line segments, from (x1,y1) to (x2,y2)
728
729 ## """
730
731 ## def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False):
732 ## DrawObject.__init__(self, InForeground)
733
734 ## NumLines = len(Points) / 2
735 ## ##fixme: there should be some error checking for everything being the right length.
736
737
738 ## self.Points = array(Points,Float)
739 ## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
740
741 ## self.LineColors = LineColors
742 ## self.LineStyles = LineStyles
743 ## self.LineWidths = LineWidths
744
745 ## self.SetPens(LineColors,LineStyles,LineWidths)
746
747 ## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
748 ## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
749 ## Points = WorldToPixel(self.Points)
750 ## Points.shape = (-1,4)
751 ## dc.DrawLineList(Points,self.Pens)
752
753 class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin):
754 """
755
756 The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of
757 point coordinates.
758
759 If Points is a sequence of tuples: Points[N][0] is the x-coordinate of
760 point N and Points[N][1] is the y-coordinate.
761
762 If Points is a NumPy array: Points[N,0] is the x-coordinate of point
763 N and Points[N,1] is the y-coordinate for arrays.
764
765 Each point will be drawn the same color and Diameter. The Diameter
766 is in screen pixels, not world coordinates.
767
768 The hit-test code does not distingish between the points, you will
769 only know that one of the points got hit, not which one. You can use
770 PointSet.FindClosestPoint(WorldPoint) to find out which one
771
772 In the case of points, the HitLineWidth is used as diameter.
773
774 """
775 def __init__(self, Points, Color = "Black", Diameter = 1, InForeground = False):
776 DrawObject.__init__(self,InForeground)
777
778 self.Points = array(Points,Float)
779 self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point
780 self.CalcBoundingBox()
781 self.Diameter = Diameter
782
783 self.HitLineWidth = self.MinHitLineWidth
784 self.SetColor(Color)
785
786 def SetDiameter(self,Diameter):
787 self.Diameter = Diameter
788
789 def FindClosestPoint(self, XY):
790 """
791
792 Returns the index of the closest point to the point, XY, given
793 in World coordinates. It's essentially random which you get if
794 there are more than one that are the same.
795
796 This can be used to figure out which point got hit in a mouse
797 binding callback, for instance. It's a lot faster that using a
798 lot of separate points.
799
800 """
801 d = self.Points - XY
802 return argmin(hypot(d[:,0],d[:,1]))
803
804
805 def DrawD2(self, dc, Points):
806 # A Little optimization for a diameter2 - point
807 dc.DrawPointList(Points)
808 dc.DrawPointList(Points + (1,0))
809 dc.DrawPointList(Points + (0,1))
810 dc.DrawPointList(Points + (1,1))
811
812 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
813 dc.SetPen(self.Pen)
814 Points = WorldToPixel(self.Points)
815 if self.Diameter <= 1:
816 dc.DrawPointList(Points)
817 elif self.Diameter <= 2:
818 self.DrawD2(dc, Points)
819 else:
820 dc.SetBrush(self.Brush)
821 radius = int(round(self.Diameter/2))
822 ##fixme: I really should add a DrawCircleList to wxPython
823 if len(Points) > 100:
824 xy = Points
825 xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 )
826 dc.DrawEllipseList(xywh)
827 else:
828 for xy in Points:
829 dc.DrawCircle(xy[0],xy[1], radius)
830 if HTdc and self.HitAble:
831 HTdc.SetPen(self.HitPen)
832 HTdc.SetBrush(self.HitBrush)
833 if self.Diameter <= 1:
834 HTdc.DrawPointList(Points)
835 elif self.Diameter <= 2:
836 self.DrawD2(HTdc, Points)
837 else:
838 if len(Points) > 100:
839 xy = Points
840 xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 )
841 HTdc.DrawEllipseList(xywh)
842 else:
843 for xy in Points:
844 HTdc.DrawCircle(xy[0],xy[1], radius)
845
846 class Point(DrawObject,XYObjectMixin,ColorOnlyMixin):
847 """
848
849 The Point class takes a 2-tuple, or a (2,) NumPy array of point
850 coordinates.
851
852 The Diameter is in screen points, not world coordinates, So the
853 Bounding box is just the point, and doesn't include the Diameter.
854
855 The HitLineWidth is used as diameter for the
856 Hit Test.
857
858 """
859 def __init__(self, XY, Color = "Black", Diameter = 1, InForeground = False):
860 DrawObject.__init__(self, InForeground)
861
862 self.XY = array(XY, Float)
863 self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
864 self.CalcBoundingBox()
865 self.SetColor(Color)
866 self.Diameter = Diameter
867
868 self.HitLineWidth = self.MinHitLineWidth
869
870 def SetDiameter(self,Diameter):
871 self.Diameter = Diameter
872
873
874 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
875 dc.SetPen(self.Pen)
876 xy = WorldToPixel(self.XY)
877 if self.Diameter <= 1:
878 dc.DrawPoint(xy[0], xy[1])
879 else:
880 dc.SetBrush(self.Brush)
881 radius = int(round(self.Diameter/2))
882 dc.DrawCircle(xy[0],xy[1], radius)
883 if HTdc and self.HitAble:
884 HTdc.SetPen(self.HitPen)
885 if self.Diameter <= 1:
886 HTdc.DrawPoint(xy[0], xy[1])
887 else:
888 HTdc.SetBrush(self.HitBrush)
889 HTdc.DrawCircle(xy[0],xy[1], radius)
890
891 class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin):
892 """
893
894 The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point
895 coordinates. It produces a square dot, centered on Point
896
897 The Size is in screen points, not world coordinates, so the
898 Bounding box is just the point, and doesn't include the Size.
899
900 The HitLineWidth is used as diameter for the
901 Hit Test.
902
903 """
904 def __init__(self, Point, Color = "Black", Size = 4, InForeground = False):
905 DrawObject.__init__(self, InForeground)
906
907 self.XY = array(Point, Float)
908 self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
909 self.CalcBoundingBox()
910 self.SetColor(Color)
911 self.Size = Size
912
913 self.HitLineWidth = self.MinHitLineWidth
914
915 def SetSize(self,Size):
916 self.Size = Size
917
918 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
919 Size = self.Size
920 dc.SetPen(self.Pen)
921 xc,yc = WorldToPixel(self.XY)
922
923 if self.Size <= 1:
924 dc.DrawPoint(xc, yc)
925 else:
926 x = xc - Size/2.0
927 y = yc - Size/2.0
928 dc.SetBrush(self.Brush)
929 dc.DrawRectangle(x, y, Size, Size)
930 if HTdc and self.HitAble:
931 HTdc.SetPen(self.HitPen)
932 if self.Size <= 1:
933 HTdc.DrawPoint(xc, xc)
934 else:
935 HTdc.SetBrush(self.HitBrush)
936 HTdc.DrawRectangle(x, y, Size, Size)
937
938 class RectEllipse(DrawObject, XYObjectMixin, LineAndFillMixin):
939 def __init__(self, XY, WH,
940 LineColor = "Black",
941 LineStyle = "Solid",
942 LineWidth = 1,
943 FillColor = None,
944 FillStyle = "Solid",
945 InForeground = False):
946
947 DrawObject.__init__(self,InForeground)
948
949 self.XY = array( XY, Float)
950 self.XY.shape = (2,)
951 self.WH = array( WH, Float )
952 self.WH.shape = (2,)
953 self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float)
954 self.LineColor = LineColor
955 self.LineStyle = LineStyle
956 self.LineWidth = LineWidth
957 self.FillColor = FillColor
958 self.FillStyle = FillStyle
959
960 self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
961
962 self.SetPen(LineColor,LineStyle,LineWidth)
963 self.SetBrush(FillColor,FillStyle)
964
965 def SetShape(self, XY, WH):
966 self.XY = array( XY, Float)
967 self.WH = array( WH, Float )
968 self.CalcBoundingBox()
969
970
971 def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
972 dc.SetPen(self.Pen)
973 dc.SetBrush(self.Brush)
974 if HTdc and self.HitAble:
975 HTdc.SetPen(self.HitPen)
976 HTdc.SetBrush(self.HitBrush)
977 return ( WorldToPixel(self.XY),
978 ScaleWorldToPixel(self.WH) )
979
980 def CalcBoundingBox(self):
981 self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float)
982 self._Canvas.BoundingBoxDirty = True
983
984
985 class Rectangle(RectEllipse):
986
987 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
988 ( XY, WH ) = self.SetUpDraw(dc,
989 WorldToPixel,
990 ScaleWorldToPixel,
991 HTdc)
992 dc.DrawRectanglePointSize(XY, WH)
993 if HTdc and self.HitAble:
994 HTdc.DrawRectanglePointSize(XY, WH)
995
996 class Ellipse(RectEllipse):
997
998 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
999 ( XY, WH ) = self.SetUpDraw(dc,
1000 WorldToPixel,
1001 ScaleWorldToPixel,
1002 HTdc)
1003 dc.DrawEllipsePointSize(XY, WH)
1004 if HTdc and self.HitAble:
1005 HTdc.DrawEllipsePointSize(XY, WH)
1006
1007 class Circle(Ellipse):
1008
1009 def __init__(self, XY, Diameter, **kwargs):
1010 self.Center = array(XY, Float)
1011 Diameter = float(Diameter)
1012 RectEllipse.__init__(self ,
1013 self.Center - Diameter/2.0,
1014 (Diameter, Diameter),
1015 **kwargs)
1016
1017 def SetDiameter(self, Diameter):
1018 Diameter = float(Diameter)
1019 XY = self.Center - (Diameter/2.0)
1020 self.SetShape(XY,
1021 (Diameter, Diameter)
1022 )
1023
1024 class TextObjectMixin(XYObjectMixin):
1025 """
1026
1027 A mix in class that holds attributes and methods that are needed by
1028 the Text objects
1029
1030 """
1031
1032 ## I'm caching fonts, because on GTK, getting a new font can take a
1033 ## while. However, it gets cleared after every full draw as hanging
1034 ## on to a bunch of large fonts takes a massive amount of memory.
1035
1036 FontList = {}
1037
1038 LayoutFontSize = 12 # font size used for calculating layout
1039
1040 def SetFont(self, Size, Family, Style, Weight, Underline, FaceName):
1041 self.Font = self.FontList.setdefault( (Size,
1042 Family,
1043 Style,
1044 Weight,
1045 Underline,
1046 FaceName),
1047 wx.Font(Size,
1048 Family,
1049 Style,
1050 Weight,
1051 Underline,
1052 FaceName) )
1053 return self.Font
1054
1055 def SetColor(self, Color):
1056 self.Color = Color
1057
1058 def SetBackgroundColor(self, BackgroundColor):
1059 self.BackgroundColor = BackgroundColor
1060
1061 def SetText(self, String):
1062 """
1063 Re-sets the text displayed by the object
1064
1065 In the case of the ScaledTextBox, it will re-do the layout as appropriate
1066
1067 Note: only tested with the ScaledTextBox
1068
1069 """
1070
1071 self.String = String
1072 self.LayoutText()
1073
1074 def LayoutText(self):
1075 """
1076 A dummy method to re-do the layout of the text.
1077
1078 A derived object needs to override this if required.
1079
1080 """
1081 pass
1082
1083 ## store the function that shift the coords for drawing text. The
1084 ## "c" parameter is the correction for world coordinates, rather
1085 ## than pixel coords as the y axis is reversed
1086 ## pad is the extra space around the text
1087 ## if world = 1, the vertical shift is done in y-up coordinates
1088 ShiftFunDict = {'tl': lambda x, y, w, h, world=0, pad=0: (x + pad, y + pad - 2*world*pad),
1089 'tc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y + pad - 2*world*pad),
1090 'tr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y + pad - 2*world*pad),
1091 'cl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h/2 + world*h),
1092 'cc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h/2 + world*h),
1093 'cr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h/2 + world*h),
1094 'bl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h + 2*world*h - pad + world*2*pad) ,
1095 'bc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h + 2*world*h - pad + world*2*pad) ,
1096 'br': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h + 2*world*h - pad + world*2*pad)}
1097
1098 class Text(DrawObject, TextObjectMixin):
1099 """
1100 This class creates a text object, placed at the coordinates,
1101 x,y. the "Position" argument is a two charactor string, indicating
1102 where in relation to the coordinates the string should be oriented.
1103
1104 The first letter is: t, c, or b, for top, center and bottom The
1105 second letter is: l, c, or r, for left, center and right The
1106 position refers to the position relative to the text itself. It
1107 defaults to "tl" (top left).
1108
1109 Size is the size of the font in pixels, or in points for printing
1110 (if it ever gets implimented). Those will be the same, If you assume
1111 72 PPI.
1112
1113 Family:
1114 Font family, a generic way of referring to fonts without
1115 specifying actual facename. One of:
1116 wx.DEFAULT: Chooses a default font.
1117 wx.DECORATIVE: A decorative font.
1118 wx.ROMAN: A formal, serif font.
1119 wx.SCRIPT: A handwriting font.
1120 wx.SWISS: A sans-serif font.
1121 wx.MODERN: A fixed pitch font.
1122 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1123 Style:
1124 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1125 Weight:
1126 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1127 Underline:
1128 The value can be True or False. At present this may have an an
1129 effect on Windows only.
1130
1131 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1132 above will be ignored.
1133
1134 The size is fixed, and does not scale with the drawing.
1135
1136 The hit-test is done on the entire text extent
1137
1138 """
1139
1140 def __init__(self,String, xy,
1141 Size = 12,
1142 Color = "Black",
1143 BackgroundColor = None,
1144 Family = wx.MODERN,
1145 Style = wx.NORMAL,
1146 Weight = wx.NORMAL,
1147 Underline = False,
1148 Position = 'tl',
1149 InForeground = False,
1150 Font = None):
1151
1152 DrawObject.__init__(self,InForeground)
1153
1154 self.String = String
1155 # Input size in in Pixels, compute points size from PPI info.
1156 # fixme: for printing, we'll have to do something a little different
1157 self.Size = int(round(72.0 * Size / ScreenPPI))
1158
1159 self.Color = Color
1160 self.BackgroundColor = BackgroundColor
1161
1162 if not Font:
1163 FaceName = ''
1164 else:
1165 FaceName = Font.GetFaceName()
1166 Family = Font.GetFamily()
1167 Size = Font.GetPointSize()
1168 Style = Font.GetStyle()
1169 Underlined = Font.GetUnderlined()
1170 Weight = Font.GetWeight()
1171 self.SetFont(Size, Family, Style, Weight, Underline, FaceName)
1172
1173 self.BoundingBox = array((xy, xy),Float)
1174
1175 self.XY = asarray(xy)
1176 self.XY.shape = (2,)
1177
1178 (self.TextWidth, self.TextHeight) = (None, None)
1179 self.ShiftFun = self.ShiftFunDict[Position]
1180
1181 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1182 XY = WorldToPixel(self.XY)
1183 dc.SetFont(self.Font)
1184 dc.SetTextForeground(self.Color)
1185 if self.BackgroundColor:
1186 dc.SetBackgroundMode(wx.SOLID)
1187 dc.SetTextBackground(self.BackgroundColor)
1188 else:
1189 dc.SetBackgroundMode(wx.TRANSPARENT)
1190 if self.TextWidth is None or self.TextHeight is None:
1191 (self.TextWidth, self.TextHeight) = dc.GetTextExtent(self.String)
1192 XY = self.ShiftFun(XY[0], XY[1], self.TextWidth, self.TextHeight)
1193 dc.DrawTextPoint(self.String, XY)
1194 if HTdc and self.HitAble:
1195 HTdc.SetPen(self.HitPen)
1196 HTdc.SetBrush(self.HitBrush)
1197 HTdc.DrawRectanglePointSize(XY, (self.TextWidth, self.TextHeight) )
1198
1199 class ScaledText(DrawObject, TextObjectMixin):
1200 """
1201 This class creates a text object that is scaled when zoomed. It is
1202 placed at the coordinates, x,y. the "Position" argument is a two
1203 charactor string, indicating where in relation to the coordinates
1204 the string should be oriented.
1205
1206 The first letter is: t, c, or b, for top, center and bottom The
1207 second letter is: l, c, or r, for left, center and right The
1208 position refers to the position relative to the text itself. It
1209 defaults to "tl" (top left).
1210
1211 Size is the size of the font in world coordinates.
1212
1213 Family:
1214 Font family, a generic way of referring to fonts without
1215 specifying actual facename. One of:
1216 wx.DEFAULT: Chooses a default font.
1217 wx.DECORATI: A decorative font.
1218 wx.ROMAN: A formal, serif font.
1219 wx.SCRIPT: A handwriting font.
1220 wx.SWISS: A sans-serif font.
1221 wx.MODERN: A fixed pitch font.
1222 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1223 Style:
1224 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1225 Weight:
1226 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1227 Underline:
1228 The value can be True or False. At present this may have an an
1229 effect on Windows only.
1230
1231 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1232 above will be ignored. The size of the font you specify will be
1233 ignored, but the rest of its attributes will be preserved.
1234
1235 The size will scale as the drawing is zoomed.
1236
1237 Bugs/Limitations:
1238
1239 As fonts are scaled, the do end up a little different, so you don't
1240 get exactly the same picture as you scale up and doen, but it's
1241 pretty darn close.
1242
1243 On wxGTK1 on my Linux system, at least, using a font of over about
1244 3000 pts. brings the system to a halt. It's the Font Server using
1245 huge amounts of memory. My work around is to max the font size to
1246 3000 points, so it won't scale past there. GTK2 uses smarter font
1247 drawing, so that may not be an issue in future versions, so feel
1248 free to test. Another smarter way to do it would be to set a global
1249 zoom limit at that point.
1250
1251 The hit-test is done on the entire text extent. This could be made
1252 optional, but I haven't gotten around to it.
1253
1254 """
1255
1256 def __init__(self, String, XY , Size,
1257 Color = "Black",
1258 BackgroundColor = None,
1259 Family = wx.MODERN,
1260 Style = wx.NORMAL,
1261 Weight = wx.NORMAL,
1262 Underline = False,
1263 Position = 'tl',
1264 Font = None,
1265 InForeground = False):
1266
1267 DrawObject.__init__(self,InForeground)
1268
1269 self.String = String
1270 self.XY = array( XY, Float)
1271 self.XY.shape = (2,)
1272 self.Size = Size
1273 self.Color = Color
1274 self.BackgroundColor = BackgroundColor
1275 self.Family = Family
1276 self.Style = Style
1277 self.Weight = Weight
1278 self.Underline = Underline
1279 if not Font:
1280 self.FaceName = ''
1281 else:
1282 self.FaceName = Font.GetFaceName()
1283 self.Family = Font.GetFamily()
1284 self.Style = Font.GetStyle()
1285 self.Underlined = Font.GetUnderlined()
1286 self.Weight = Font.GetWeight()
1287
1288 # Experimental max font size value on wxGTK2: this works OK on
1289 # my system. If it's a lot larger, there is a crash, with the
1290 # message:
1291 #
1292 # The application 'FloatCanvasDemo.py' lost its
1293 # connection to the display :0.0; most likely the X server was
1294 # shut down or you killed/destroyed the application.
1295 #
1296 # Windows and OS-X seem to be better behaved in this regard.
1297 # They may not draw it, but they don't crash either!
1298 self.MaxFontSize = 1000
1299
1300 self.ShiftFun = self.ShiftFunDict[Position]
1301
1302 self.CalcBoundingBox()
1303
1304 def LayoutText(self):
1305 # This will be called when the text is re-set
1306 # nothing much to be done here
1307 self.CalcBoundingBox()
1308
1309 def CalcBoundingBox(self):
1310 ## this isn't exact, as fonts don't scale exactly.
1311 dc = wx.MemoryDC()
1312 bitmap = wx.EmptyBitmap(1, 1)
1313 dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
1314 DrawingSize = 40 # pts This effectively determines the resolution that the BB is computed to.
1315 ScaleFactor = float(self.Size) / DrawingSize
1316 dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
1317 (w,h) = dc.GetTextExtent(self.String)
1318 w = w * ScaleFactor
1319 h = h * ScaleFactor
1320 x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1)
1321 self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
1322
1323 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1324 (X,Y) = WorldToPixel( (self.XY) )
1325
1326 # compute the font size:
1327 Size = abs( ScaleWorldToPixel( (self.Size, self.Size) )[1] ) # only need a y coordinate length
1328 ## Check to see if the font size is large enough to blow up the X font server
1329 ## If so, limit it. Would it be better just to not draw it?
1330 ## note that this limit is dependent on how much memory you have, etc.
1331 Size = min(Size, self.MaxFontSize)
1332 dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName))
1333 dc.SetTextForeground(self.Color)
1334 if self.BackgroundColor:
1335 dc.SetBackgroundMode(wx.SOLID)
1336 dc.SetTextBackground(self.BackgroundColor)
1337 else:
1338 dc.SetBackgroundMode(wx.TRANSPARENT)
1339 (w,h) = dc.GetTextExtent(self.String)
1340 # compute the shift, and adjust the coordinates, if neccesary
1341 # This had to be put in here, because it changes with Zoom, as
1342 # fonts don't scale exactly.
1343 xy = self.ShiftFun(X, Y, w, h)
1344
1345 dc.DrawTextPoint(self.String, xy)
1346 if HTdc and self.HitAble:
1347 HTdc.SetPen(self.HitPen)
1348 HTdc.SetBrush(self.HitBrush)
1349 HTdc.DrawRectanglePointSize(xy, (w, h) )
1350
1351 class ScaledTextBox(DrawObject, TextObjectMixin):
1352 """
1353 This class creates a TextBox object that is scaled when zoomed. It is
1354 placed at the coordinates, x,y.
1355
1356 If the Width parameter is defined, the text will be wrapped to the width given.
1357
1358 A Box can be drawn around the text, be specifying:
1359 LineWidth and/or FillColor
1360
1361 A space(margin) can be put all the way around the text, be specifying:
1362 the PadSize argument in world coordinates.
1363
1364 The spacing between lines can be adjusted with the:
1365 LineSpacing argument.
1366
1367 The "Position" argument is a two character string, indicating where
1368 in relation to the coordinates the Box should be oriented.
1369 -The first letter is: t, c, or b, for top, center and bottom.
1370 -The second letter is: l, c, or r, for left, center and right The
1371 position refers to the position relative to the text itself. It
1372 defaults to "tl" (top left).
1373
1374 Size is the size of the font in world coordinates.
1375
1376 Family:
1377 Font family, a generic way of referring to fonts without
1378 specifying actual facename. One of:
1379 wx.DEFAULT: Chooses a default font.
1380 wx.DECORATIVE: A decorative font.
1381 wx.ROMAN: A formal, serif font.
1382 wx.SCRIPT: A handwriting font.
1383 wx.SWISS: A sans-serif font.
1384 wx.MODERN: A fixed pitch font.
1385 NOTE: these are only as good as the wxWindows defaults, which aren't so good.
1386 Style:
1387 One of wx.NORMAL, wx.SLANT and wx.ITALIC.
1388 Weight:
1389 One of wx.NORMAL, wx.LIGHT and wx.BOLD.
1390 Underline:
1391 The value can be True or False. At present this may have an an
1392 effect on Windows only.
1393
1394 Alternatively, you can set the kw arg: Font, to a wx.Font, and the
1395 above will be ignored. The size of the font you specify will be
1396 ignored, but the rest of its attributes will be preserved.
1397
1398 The size will scale as the drawing is zoomed.
1399
1400 Bugs/Limitations:
1401
1402 As fonts are scaled, they do end up a little different, so you don't
1403 get exactly the same picture as you scale up and down, but it's
1404 pretty darn close.
1405
1406 On wxGTK1 on my Linux system, at least, using a font of over about
1407 1000 pts. brings the system to a halt. It's the Font Server using
1408 huge amounts of memory. My work around is to max the font size to
1409 1000 points, so it won't scale past there. GTK2 uses smarter font
1410 drawing, so that may not be an issue in future versions, so feel
1411 free to test. Another smarter way to do it would be to set a global
1412 zoom limit at that point.
1413
1414 The hit-test is done on the entire box. This could be made
1415 optional, but I haven't gotten around to it.
1416
1417 """
1418
1419 def __init__(self, String,
1420 Point,
1421 Size,
1422 Color = "Black",
1423 BackgroundColor = None,
1424 LineColor = 'Black',
1425 LineStyle = 'Solid',
1426 LineWidth = 1,
1427 Width = None,
1428 PadSize = None,
1429 Family = wx.MODERN,
1430 Style = wx.NORMAL,
1431 Weight = wx.NORMAL,
1432 Underline = False,
1433 Position = 'tl',
1434 Alignment = "left",
1435 Font = None,
1436 LineSpacing = 1.0,
1437 InForeground = False):
1438
1439 DrawObject.__init__(self,InForeground)
1440
1441 self.XY = array(Point, Float)
1442 self.Size = Size
1443 self.Color = Color
1444 self.BackgroundColor = BackgroundColor
1445 self.LineColor = LineColor
1446 self.LineStyle = LineStyle
1447 self.LineWidth = LineWidth
1448 self.Width = Width
1449 if PadSize is None: # the default is just a little bit of padding
1450 self.PadSize = Size/10.0
1451 else:
1452 self.PadSize = float(PadSize)
1453 self.Family = Family
1454 self.Style = Style
1455 self.Weight = Weight
1456 self.Underline = Underline
1457 self.Alignment = Alignment.lower()
1458 self.LineSpacing = float(LineSpacing)
1459 self.Position = Position
1460
1461 if not Font:
1462 self.FaceName = ''
1463 else:
1464 self.FaceName = Font.GetFaceName()
1465 self.Family = Font.GetFamily()
1466 self.Style = Font.GetStyle()
1467 self.Underlined = Font.GetUnderlined()
1468 self.Weight = Font.GetWeight()
1469
1470 # Experimental max font size value on wxGTK2: this works OK on
1471 # my system. If it's a lot larger, there is a crash, with the
1472 # message:
1473 #
1474 # The application 'FloatCanvasDemo.py' lost its
1475 # connection to the display :0.0; most likely the X server was
1476 # shut down or you killed/destroyed the application.
1477 #
1478 # Windows and OS-X seem to be better behaved in this regard.
1479 # They may not draw it, but they don't crash either!
1480
1481 self.MaxFontSize = 1000
1482 self.ShiftFun = self.ShiftFunDict[Position]
1483
1484 self.String = String
1485 self.LayoutText()
1486 self.CalcBoundingBox()
1487
1488 self.SetPen(LineColor,LineStyle,LineWidth)
1489 self.SetBrush(BackgroundColor, "Solid")
1490
1491
1492 def WrapToWidth(self):
1493 dc = wx.MemoryDC()
1494 bitmap = wx.EmptyBitmap(1, 1)
1495 dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
1496 DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to.
1497 ScaleFactor = float(self.Size) / DrawingSize
1498 Width = (self.Width - 2*self.PadSize) / ScaleFactor #Width to wrap to
1499 dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
1500
1501 NewStrings = []
1502 for s in self.Strings:
1503 #beginning = True
1504 text = s.split(" ")
1505 text.reverse()
1506 LineLength = 0
1507 NewText = text[-1]
1508 del text[-1]
1509 while text:
1510 w = dc.GetTextExtent(' ' + text[-1])[0]
1511 if LineLength + w <= Width:
1512 NewText += ' '
1513 NewText += text[-1]
1514 LineLength = dc.GetTextExtent(NewText)[0]
1515 else:
1516 NewStrings.append(NewText)
1517 NewText = text[-1]
1518 LineLength = dc.GetTextExtent(text[-1])[0]
1519 del text[-1]
1520 NewStrings.append(NewText)
1521 self.Strings = NewStrings
1522
1523 def ReWrap(self, Width):
1524 self.Width = Width
1525 self.LayoutText()
1526
1527 def LayoutText(self):
1528 """
1529
1530 Calculates the positions of the words of text.
1531
1532 This isn't exact, as fonts don't scale exactly.
1533 To help this, the position of each individual word
1534 is stored separately, so that the general layout stays
1535 the same in world coordinates, as the fonts scale.
1536
1537 """
1538 self.Strings = self.String.split("\n")
1539 if self.Width:
1540 self.WrapToWidth()
1541
1542 dc = wx.MemoryDC()
1543 bitmap = wx.EmptyBitmap(1, 1)
1544 dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
1545
1546 DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to.
1547 ScaleFactor = float(self.Size) / DrawingSize
1548
1549 dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
1550
1551 TextHeight = dc.GetTextExtent("X")[1]
1552 SpaceWidth = dc.GetTextExtent(" ")[0]
1553 LineHeight = TextHeight * self.LineSpacing
1554
1555 LineWidths = zeros((len(self.Strings),), Float)
1556 y = 0
1557 Words = []
1558 AllLinePoints = []
1559
1560 for i, s in enumerate(self.Strings):
1561 LineWidths[i] = 0
1562 LineWords = s.split(" ")
1563 LinePoints = zeros((len(LineWords),2), Float)
1564 for j, word in enumerate(LineWords):
1565 if j > 0:
1566 LineWidths[i] += SpaceWidth
1567 Words.append(word)
1568 LinePoints[j] = (LineWidths[i], y)
1569 w = dc.GetTextExtent(word)[0]
1570 LineWidths[i] += w
1571 y -= LineHeight
1572 AllLinePoints.append(LinePoints)
1573 TextWidth = maximum.reduce(LineWidths)
1574 self.Words = Words
1575
1576 if self.Width is None:
1577 BoxWidth = TextWidth * ScaleFactor + 2*self.PadSize
1578 else: # use the defined Width
1579 BoxWidth = self.Width
1580 Points = zeros((0,2), Float)
1581
1582 for i, LinePoints in enumerate(AllLinePoints):
1583 ## Scale to World Coords.
1584 LinePoints *= (ScaleFactor, ScaleFactor)
1585 if self.Alignment == 'left':
1586 LinePoints[:,0] += self.PadSize
1587 elif self.Alignment == 'center':
1588 LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor)/2.0
1589 elif self.Alignment == 'right':
1590 LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor-self.PadSize)
1591 Points = concatenate((Points, LinePoints))
1592
1593 BoxHeight = -(Points[-1,1] - (TextHeight * ScaleFactor)) + 2*self.PadSize
1594 (x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1)
1595 Points += (0, -self.PadSize)
1596 self.Points = Points
1597 self.BoxWidth = BoxWidth
1598 self.BoxHeight = BoxHeight
1599 self.CalcBoundingBox()
1600
1601 def CalcBoundingBox(self):
1602
1603 """
1604
1605 Calculates the Bounding Box
1606
1607 """
1608
1609 w, h = self.BoxWidth, self.BoxHeight
1610 x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world=1)
1611 self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
1612
1613 def GetBoxRect(self):
1614 wh = (self.BoxWidth, self.BoxHeight)
1615 xy = (self.BoundingBox[0,0], self.BoundingBox[1,1])
1616
1617 return (xy, wh)
1618
1619 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1620 xy, wh = self.GetBoxRect()
1621
1622 Points = self.Points + xy
1623 Points = WorldToPixel(Points)
1624 xy = WorldToPixel(xy)
1625 wh = ScaleWorldToPixel(wh) * (1,-1)
1626
1627 # compute the font size:
1628 Size = abs( ScaleWorldToPixel( (self.Size, self.Size) )[1] ) # only need a y coordinate length
1629 ## Check to see if the font size is large enough to blow up the X font server
1630 ## If so, limit it. Would it be better just to not draw it?
1631 ## note that this limit is dependent on how much memory you have, etc.
1632 Size = min(Size, self.MaxFontSize)
1633
1634 dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName))
1635 dc.SetTextForeground(self.Color)
1636 dc.SetBackgroundMode(wx.TRANSPARENT)
1637
1638 # Draw The Box
1639 if (self.LineStyle and self.LineColor) or self.BackgroundColor:
1640 dc.SetBrush(self.Brush)
1641 dc.SetPen(self.Pen)
1642 dc.DrawRectanglePointSize(xy , wh)
1643
1644 # Draw the Text
1645 dc.DrawTextList(self.Words, Points)
1646
1647 # Draw the hit box.
1648 if HTdc and self.HitAble:
1649 HTdc.SetPen(self.HitPen)
1650 HTdc.SetBrush(self.HitBrush)
1651 HTdc.DrawRectanglePointSize(xy, wh)
1652
1653 class Bitmap(DrawObject, TextObjectMixin):
1654 """
1655 This class creates a bitmap object, placed at the coordinates,
1656 x,y. the "Position" argument is a two charactor string, indicating
1657 where in relation to the coordinates the bitmap should be oriented.
1658
1659 The first letter is: t, c, or b, for top, center and bottom The
1660 second letter is: l, c, or r, for left, center and right The
1661 position refers to the position relative to the text itself. It
1662 defaults to "tl" (top left).
1663
1664 The size is fixed, and does not scale with the drawing.
1665
1666 """
1667
1668 def __init__(self,Bitmap,XY,
1669 Position = 'tl',
1670 InForeground = False):
1671
1672 DrawObject.__init__(self,InForeground)
1673
1674 if type(Bitmap) == wx._gdi.Bitmap:
1675 self.Bitmap = Bitmap
1676 elif type(Bitmap) == wx._core.Image:
1677 self.Bitmap = wx.BitmapFromImage(Bitmap)
1678
1679 # Note the BB is just the point, as the size in World coordinates is not fixed
1680 self.BoundingBox = array((XY,XY),Float)
1681
1682 self.XY = XY
1683
1684 (self.Width, self.Height) = self.Bitmap.GetWidth(), self.Bitmap.GetHeight()
1685 self.ShiftFun = self.ShiftFunDict[Position]
1686
1687 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1688 XY = WorldToPixel(self.XY)
1689 XY = self.ShiftFun(XY[0], XY[1], self.Width, self.Height)
1690 dc.DrawBitmapPoint(self.Bitmap, XY, True)
1691 if HTdc and self.HitAble:
1692 HTdc.SetPen(self.HitPen)
1693 HTdc.SetBrush(self.HitBrush)
1694 HTdc.DrawRectanglePointSize(XY, (self.Width, self.Height) )
1695
1696 class ScaledBitmap(DrawObject, TextObjectMixin):
1697 """
1698
1699 This class creates a bitmap object, placed at the coordinates, XY,
1700 of Height, H, in World coorsinates. The width is calculated from the
1701 aspect ratio of the bitmap.
1702
1703 the "Position" argument is a two charactor string, indicating
1704 where in relation to the coordinates the bitmap should be oriented.
1705
1706 The first letter is: t, c, or b, for top, center and bottom The
1707 second letter is: l, c, or r, for left, center and right The
1708 position refers to the position relative to the text itself. It
1709 defaults to "tl" (top left).
1710
1711 The size scales with the drawing
1712
1713 """
1714
1715 def __init__(self,
1716 Bitmap,
1717 XY,
1718 Height,
1719 Position = 'tl',
1720 InForeground = False):
1721
1722 DrawObject.__init__(self,InForeground)
1723
1724 if type(Bitmap) == wx._gdi.Bitmap:
1725 self.Image = Bitmap.ConvertToImage()
1726 elif type(Bitmap) == wx._core.Image:
1727 self.Image = Bitmap
1728
1729 self.XY = XY
1730 self.Height = Height
1731 (self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight()
1732 self.Width = self.bmpWidth / self.bmpHeight * Height
1733 self.ShiftFun = self.ShiftFunDict[Position]
1734 self.CalcBoundingBox()
1735 self.ScaledBitmap = None
1736 self.ScaledHeight = None
1737
1738 def CalcBoundingBox(self):
1739 ## this isn't exact, as fonts don't scale exactly.
1740 w,h = self.Width, self.Height
1741 x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1)
1742 self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
1743
1744 def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
1745 XY = WorldToPixel(self.XY)
1746 H = ScaleWorldToPixel(self.Height)[0]
1747 W = H * (self.bmpWidth / self.bmpHeight)
1748 if (self.ScaledBitmap is None) or (H <> self.ScaledHeight) :
1749 self.ScaledHeight = H
1750 Img = self.Image.Scale(W, H)
1751 self.ScaledBitmap = wx.BitmapFromImage(Img)
1752
1753 XY = self.ShiftFun(XY[0], XY[1], W, H)
1754 dc.DrawBitmapPoint(self.ScaledBitmap, XY, True)
1755 if HTdc and self.HitAble:
1756 HTdc.SetPen(self.HitPen)
1757 HTdc.SetBrush(self.HitBrush)
1758 HTdc.DrawRectanglePointSize(XY, (W, H) )
1759
1760
1761 #---------------------------------------------------------------------------
1762 class FloatCanvas(wx.Panel):
1763 ## fixme: could this be a wx.Window?
1764 """
1765 FloatCanvas.py
1766
1767 This is a high level window for drawing maps and anything else in an
1768 arbitrary coordinate system.
1769
1770 The goal is to provide a convenient way to draw stuff on the screen
1771 without having to deal with handling OnPaint events, converting to pixel
1772 coordinates, knowing about wxWindows brushes, pens, and colors, etc. It
1773 also provides virtually unlimited zooming and scrolling
1774
1775 I am using it for two things:
1776 1) general purpose drawing in floating point coordinates
1777 2) displaying map data in Lat-long coordinates
1778
1779 If the projection is set to None, it will draw in general purpose
1780 floating point coordinates. If the projection is set to 'FlatEarth', it
1781 will draw a FlatEarth projection, centered on the part of the map that
1782 you are viewing. You can also pass in your own projection function.
1783
1784 It is double buffered, so re-draws after the window is uncovered by something
1785 else are very quick.
1786
1787 It relies on NumPy, which is needed for speed (maybe, I havn't profiled it)
1788
1789 Bugs and Limitations:
1790 Lots: patches, fixes welcome
1791
1792 For Map drawing: It ignores the fact that the world is, in fact, a
1793 sphere, so it will do strange things if you are looking at stuff near
1794 the poles or the date line. so far I don't have a need to do that, so I
1795 havn't bothered to add any checks for that yet.
1796
1797 Zooming:
1798 I have set no zoom limits. What this means is that if you zoom in really
1799 far, you can get integer overflows, and get wierd results. It
1800 doesn't seem to actually cause any problems other than wierd output, at
1801 least when I have run it.
1802
1803 Speed:
1804 I have done a couple of things to improve speed in this app. The one
1805 thing I have done is used NumPy Arrays to store the coordinates of the
1806 points of the objects. This allowed me to use array oriented functions
1807 when doing transformations, and should provide some speed improvement
1808 for objects with a lot of points (big polygons, polylines, pointsets).
1809
1810 The real slowdown comes when you have to draw a lot of objects, because
1811 you have to call the wx.DC.DrawSomething call each time. This is plenty
1812 fast for tens of objects, OK for hundreds of objects, but pretty darn
1813 slow for thousands of objects.
1814
1815 The solution is to be able to pass some sort of object set to the DC
1816 directly. I've used DC.DrawPointList(Points), and it helped a lot with
1817 drawing lots of points. I havn't got a LineSet type object, so I havn't
1818 used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList()
1819 methods implimented, and then I'd also have a full set of Object sets
1820 that could take advantage of them. I hope to get to it some day.
1821
1822 Mouse Events:
1823
1824 At this point, there are a full set of custom mouse events. They are
1825 just like the rebulsr mouse events, but include an extra attribute:
1826 Event.GetCoords(), that returns the (x,y) position in world
1827 coordinates, as a length-2 NumPy vector of Floats.
1828
1829 Copyright: Christopher Barker
1830
1831 License: Same as the version of wxPython you are using it with
1832
1833 Please let me know if you're using this!!!
1834
1835 Contact me at:
1836
1837 Chris.Barker@noaa.gov
1838
1839 """
1840
1841 def __init__(self, parent, id = -1,
1842 size = wx.DefaultSize,
1843 ProjectionFun = None,
1844 BackgroundColor = "WHITE",
1845 Debug = False):
1846
1847 wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size)
1848
1849 global ScreenPPI ## A global variable to hold the Pixels per inch that wxWindows thinks is in use.
1850 dc = wx.ScreenDC()
1851 ScreenPPI = dc.GetPPI()[1] # Pixel height
1852 del dc
1853
1854 self.HitColorGenerator = None
1855 self.UseHitTest = None
1856
1857 self.NumBetweenBlits = 500
1858
1859 self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID)
1860
1861 self.Debug = Debug
1862
1863 wx.EVT_PAINT(self, self.OnPaint)
1864 wx.EVT_SIZE(self, self.OnSize)
1865
1866 wx.EVT_LEFT_DOWN(self, self.LeftDownEvent )
1867 wx.EVT_LEFT_UP(self, self.LeftUpEvent )
1868 wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent )
1869 wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent )
1870 wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent )
1871 wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent )
1872 wx.EVT_RIGHT_DOWN(self, self.RightDownEvent)
1873 wx.EVT_RIGHT_UP(self, self.RightUpEvent )
1874 wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent )
1875 wx.EVT_MOTION(self, self.MotionEvent )
1876 wx.EVT_MOUSEWHEEL(self, self.WheelEvent )
1877
1878 ## CHB: I'm leaving these out for now.
1879 #wx.EVT_ENTER_WINDOW(self, self. )
1880 #wx.EVT_LEAVE_WINDOW(self, self. )
1881
1882 ## create the Hit Test Dicts:
1883 self.HitDict = None
1884 self._HTdc = None
1885
1886 self._DrawList = []
1887 self._ForeDrawList = []
1888 self._ForegroundBuffer = None
1889 self.BoundingBox = None
1890 self.BoundingBoxDirty = False
1891 self.ViewPortCenter= array( (0,0), Float)
1892
1893 self.SetProjectionFun(ProjectionFun)
1894
1895 self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
1896 self.TransformVector = array( (1,-1), Float) # default Transformation
1897
1898 self.Scale = 1
1899
1900 self.GUIMode = None
1901 self.StartRBBox = None
1902 self.PrevRBBox = None
1903 self.StartMove = None
1904 self.PrevMoveXY = None
1905 self.ObjectUnderMouse = None
1906
1907 # called just to make sure everything is initialized
1908 # this is a bug on OS-X, maybe it's not required?
1909 self.OnSize(None)
1910
1911 self.InHereNum = 0
1912
1913 self.CreateCursors()
1914
1915 def CreateCursors(self):
1916
1917 ## create all the Cursors, so they don't need to be created each time.
1918 ##
1919 if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac
1920 self.HandCursor = wx.CursorFromImage(Resources.getHand16Image())
1921 self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image())
1922
1923 img = Resources.getMagPlus16Image()
1924 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
1925 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
1926 self.MagPlusCursor = wx.CursorFromImage(img)
1927
1928 img = Resources.getMagMinus16Image()
1929 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
1930 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
1931 self.MagMinusCursor = wx.CursorFromImage(img)
1932 else: # use 24X24 cursors for GTK and Windows
1933 self.HandCursor = wx.CursorFromImage(Resources.getHandImage())
1934 self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage())
1935
1936 img = Resources.getMagPlusImage()
1937 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
1938 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
1939 self.MagPlusCursor = wx.CursorFromImage(img)
1940
1941 img = Resources.getMagMinusImage()
1942 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
1943 img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
1944 self.MagMinusCursor = wx.CursorFromImage(img)
1945
1946 def SetProjectionFun(self,ProjectionFun):
1947 if ProjectionFun == 'FlatEarth':
1948 self.ProjectionFun = self.FlatEarthProjection
1949 elif callable(ProjectionFun):
1950 self.ProjectionFun = ProjectionFun
1951 elif ProjectionFun is None:
1952 self.ProjectionFun = lambda x=None: array( (1,1), Float)
1953 else:
1954 raise FloatCanvasError('Projectionfun must be either: "FlatEarth", None, or a callable object (function, for instance) that takes the ViewPortCenter and returns a MapProjectionVector')
1955
1956 def FlatEarthProjection(self, CenterPoint):
1957 return array((cos(pi*CenterPoint[1]/180),1),Float)
1958
1959 def SetMode(self,Mode):
1960 if Mode in ["ZoomIn","ZoomOut","Move","Mouse", None]:
1961 if Mode == "Move":
1962 self.SetCursor(self.HandCursor)
1963 elif Mode == "ZoomIn":
1964 self.SetCursor(self.MagPlusCursor)
1965 elif Mode == "ZoomOut":
1966 self.SetCursor(self.MagMinusCursor)
1967 else:
1968 self.SetCursor(wx.NullCursor)
1969
1970 self.GUIMode = Mode
1971
1972 else:
1973 raise FloatCanvasError('"%s" is Not a valid Mode'%Mode)
1974
1975 def MakeHitDict(self):
1976 ##fixme: Should this just be None if nothing has been bound?
1977 self.HitDict = {EVT_FC_LEFT_DOWN: {},
1978 EVT_FC_LEFT_UP: {},
1979 EVT_FC_LEFT_DCLICK: {},
1980 EVT_FC_MIDDLE_DOWN: {},
1981 EVT_FC_MIDDLE_UP: {},
1982 EVT_FC_MIDDLE_DCLICK: {},
1983 EVT_FC_RIGHT_DOWN: {},
1984 EVT_FC_RIGHT_UP: {},
1985 EVT_FC_RIGHT_DCLICK: {},
1986 EVT_FC_ENTER_OBJECT: {},
1987 EVT_FC_LEAVE_OBJECT: {},
1988 }
1989
1990 def _RaiseMouseEvent(self, Event, EventType):
1991 """
1992 This is called in various other places to raise a Mouse Event
1993 """
1994 #print "in Raise Mouse Event", Event
1995 pt = self.PixelToWorld( Event.GetPosition() )
1996 evt = _MouseEvent(EventType, Event, self.GetId(), pt)
1997 self.GetEventHandler().ProcessEvent(evt)
1998
1999 def HitTest(self, event, HitEvent):
2000 if self.HitDict:
2001 # check if there are any objects in the dict for this event
2002 if self.HitDict[ HitEvent ]:
2003 xy = event.GetPosition()
2004 if self._ForegroundHTdc:
2005 hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
2006 else:
2007 hitcolor = self._HTdc.GetPixelPoint( xy )
2008 color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
2009 if color in self.HitDict[ HitEvent ]:
2010 Object = self.HitDict[ HitEvent ][color]
2011 ## Add the hit coords to the Object
2012 Object.HitCoords = self.PixelToWorld( xy )
2013 Object.HitCoordsPixel = xy
2014 Object.CallBackFuncs[HitEvent](Object)
2015 return True
2016 return False
2017
2018 def MouseOverTest(self, event):
2019 ##fixme: Can this be cleaned up?
2020 if self.HitDict:
2021 xy = event.GetPosition()
2022 if self._ForegroundHTdc:
2023 hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
2024 else:
2025 hitcolor = self._HTdc.GetPixelPoint( xy )
2026 color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
2027 OldObject = self.ObjectUnderMouse
2028 ObjectCallbackCalled = False
2029 if color in self.HitDict[ EVT_FC_ENTER_OBJECT ]:
2030 Object = self.HitDict[ EVT_FC_ENTER_OBJECT][color]
2031 if (OldObject is None):
2032 try:
2033 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
2034 ObjectCallbackCalled = True
2035 except KeyError:
2036 pass # this means the enter event isn't bound for that object
2037 elif OldObject == Object: # the mouse is still on the same object
2038 pass
2039 ## Is the mouse on a differnt object as it was...
2040 elif not (Object == OldObject):
2041 # call the leave object callback
2042 try:
2043 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
2044 ObjectCallbackCalled = True
2045 except KeyError:
2046 pass # this means the leave event isn't bound for that object
2047 try:
2048 Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
2049 ObjectCallbackCalled = True
2050 except KeyError:
2051 pass # this means the enter event isn't bound for that object
2052 ## set the new object under mouse
2053 self.ObjectUnderMouse = Object
2054 elif color in self.HitDict[ EVT_FC_LEAVE_OBJECT ]:
2055 Object = self.HitDict[ EVT_FC_LEAVE_OBJECT][color]
2056 self.ObjectUnderMouse = Object
2057 else:
2058 # no objects under mouse bound to mouse-over events
2059 self.ObjectUnderMouse = None
2060 if OldObject:
2061 try:
2062 OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
2063 ObjectCallbackCalled = True
2064 except KeyError:
2065 pass # this means the leave event isn't bound for that object
2066 return ObjectCallbackCalled
2067
2068
2069 ## fixme: There is a lot of repeated code here
2070 ## Is there a better way?
2071 def LeftDoubleClickEvent(self,event):
2072 if self.GUIMode == "Mouse":
2073 EventType = EVT_FC_LEFT_DCLICK
2074 if not self.HitTest(event, EventType):
2075 self._RaiseMouseEvent(event, EventType)
2076
2077 def MiddleDownEvent(self,event):
2078 if self.GUIMode == "Mouse":
2079 EventType = EVT_FC_MIDDLE_DOWN
2080 if not self.HitTest(event, EventType):
2081 self._RaiseMouseEvent(event, EventType)
2082
2083 def MiddleUpEvent(self,event):
2084 if self.GUIMode == "Mouse":
2085 EventType = EVT_FC_MIDDLE_UP
2086 if not self.HitTest(event, EventType):
2087 self._RaiseMouseEvent(event, EventType)
2088
2089 def MiddleDoubleClickEvent(self,event):
2090 if self.GUIMode == "Mouse":
2091 EventType = EVT_FC_MIDDLE_DCLICK
2092 if not self.HitTest(event, EventType):
2093 self._RaiseMouseEvent(event, EventType)
2094
2095 def RightUpEvent(self,event):
2096 if self.GUIMode == "Mouse":
2097 EventType = EVT_FC_RIGHT_UP
2098 if not self.HitTest(event, EventType):
2099 self._RaiseMouseEvent(event, EventType)
2100
2101 def RightDoubleCLickEvent(self,event):
2102 if self.GUIMode == "Mouse":
2103 EventType = EVT_FC_RIGHT_DCLICK
2104 if not self.HitTest(event, EventType):
2105 self._RaiseMouseEvent(event, EventType)
2106
2107 def WheelEvent(self,event):
2108 ##if self.GUIMode == "Mouse":
2109 ## Why not always raise this?
2110 self._RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL)
2111
2112
2113 def LeftDownEvent(self,event):
2114 if self.GUIMode:
2115 if self.GUIMode == "ZoomIn":
2116 self.StartRBBox = array( event.GetPosition() )
2117 self.PrevRBBox = None
2118 self.CaptureMouse()
2119 elif self.GUIMode == "ZoomOut":
2120 Center = self.PixelToWorld( event.GetPosition() )
2121 self.Zoom(1/1.5,Center)
2122 elif self.GUIMode == "Move":
2123 self.SetCursor(self.GrabHandCursor)
2124 self.StartMove = array( event.GetPosition() )
2125 self.PrevMoveXY = (0,0)
2126 elif self.GUIMode == "Mouse":
2127 ## check for a hit
2128 if not self.HitTest(event, EVT_FC_LEFT_DOWN):
2129 self._RaiseMouseEvent(event,EVT_FC_LEFT_DOWN)
2130 else:
2131 pass
2132
2133 def LeftUpEvent(self,event):
2134 if self.HasCapture():
2135 self.ReleaseMouse()
2136 if self.GUIMode:
2137 if self.GUIMode == "ZoomIn":
2138 if event.LeftUp() and not self.StartRBBox is None:
2139 self.PrevRBBox = None
2140 EndRBBox = event.GetPosition()
2141 StartRBBox = self.StartRBBox
2142 # if mouse has moved less that ten pixels, don't use the box.
2143 if ( abs(StartRBBox[0] - EndRBBox[0]) > 10
2144 and abs(StartRBBox[1] - EndRBBox[1]) > 10 ):
2145 EndRBBox = self.PixelToWorld(EndRBBox)
2146 StartRBBox = self.PixelToWorld(StartRBBox)
2147 BB = array(((min(EndRBBox[0],StartRBBox[0]),
2148 min(EndRBBox[1],StartRBBox[1])),
2149 (max(EndRBBox[0],StartRBBox[0]),
2150 max(EndRBBox[1],StartRBBox[1]))),Float)
2151 self.ZoomToBB(BB)
2152 else:
2153 Center = self.PixelToWorld(StartRBBox)
2154 self.Zoom(1.5,Center)
2155 self.StartRBBox = None
2156 elif self.GUIMode == "Move":
2157 self.SetCursor(self.HandCursor)
2158 if self.StartMove is not None:
2159 StartMove = self.StartMove
2160 EndMove = array((event.GetX(),event.GetY()))
2161 if sum((StartMove-EndMove)**2) > 16:
2162 self.MoveImage(StartMove-EndMove,'Pixel')
2163 self.StartMove = None
2164 elif self.GUIMode == "Mouse":
2165 EventType = EVT_FC_LEFT_UP
2166 if not self.HitTest(event, EventType):
2167 self._RaiseMouseEvent(event, EventType)
2168 else:
2169 pass
2170
2171 def MotionEvent(self,event):
2172 if self.GUIMode:
2173 if self.GUIMode == "ZoomIn":
2174 if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None):
2175 xy0 = self.StartRBBox
2176 xy1 = array( event.GetPosition() )
2177 wh = abs(xy1 - xy0)
2178 wh[0] = max(wh[0], int(wh[1]*self.AspectRatio))
2179 wh[1] = int(wh[0] / self.AspectRatio)
2180 xy_c = (xy0 + xy1) / 2
2181 dc = wx.ClientDC(self)
2182 dc.BeginDrawing()
2183 dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
2184 dc.SetBrush(wx.TRANSPARENT_BRUSH)
2185 dc.SetLogicalFunction(wx.XOR)
2186 if self.PrevRBBox:
2187 dc.DrawRectanglePointSize(*self.PrevRBBox)
2188 self.PrevRBBox = ( xy_c - wh/2, wh )
2189 dc.DrawRectanglePointSize( *self.PrevRBBox )
2190 dc.EndDrawing()
2191 elif self.GUIMode == "Move":
2192 if event.Dragging() and event.LeftIsDown() and not self.StartMove is None:
2193 xy1 = array( event.GetPosition() )
2194 wh = self.PanelSize
2195 xy_tl = xy1 - self.StartMove
2196 dc = wx.ClientDC(self)
2197 dc.BeginDrawing()
2198 x1,y1 = self.PrevMoveXY
2199 x2,y2 = xy_tl
2200 w,h = self.PanelSize
2201 ##fixme: This sure could be cleaner!
2202 if x2 > x1 and y2 > y1:
2203 xa = xb = x1
2204 ya = yb = y1
2205 wa = w
2206 ha = y2 - y1
2207 wb = x2- x1
2208 hb = h
2209 elif x2 > x1 and y2 <= y1:
2210 xa = x1
2211 ya = y1
2212 wa = x2 - x1
2213 ha = h
2214 xb = x1
2215 yb = y2 + h
2216 wb = w
2217 hb = y1 - y2
2218 elif x2 <= x1 and y2 > y1:
2219 xa = x1
2220 ya = y1
2221 wa = w
2222 ha = y2 - y1
2223 xb = x2 + w
2224 yb = y1
2225 wb = x1 - x2
2226 hb = h - y2 + y1
2227 elif x2 <= x1 and y2 <= y1:
2228 xa = x2 + w
2229 ya = y1
2230 wa = x1 - x2
2231 ha = h
2232 xb = x1
2233 yb = y2 + h
2234 wb = w
2235 hb = y1 - y2
2236
2237 dc.SetPen(wx.TRANSPARENT_PEN)
2238 dc.SetBrush(self.BackgroundBrush)
2239 dc.DrawRectangle(xa, ya, wa, ha)
2240 dc.DrawRectangle(xb, yb, wb, hb)
2241 self.PrevMoveXY = xy_tl
2242 if self._ForeDrawList:
2243 ##if self._ForegroundBuffer:
2244 dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl)
2245 else:
2246 dc.DrawBitmapPoint(self._Buffer,xy_tl)
2247 dc.EndDrawing()
2248 elif self.GUIMode == "Mouse":
2249 ## Only do something if there are mouse over events bound
2250 if self.HitDict and (self.HitDict[ EVT_FC_ENTER_OBJECT ] or self.HitDict[ EVT_FC_LEAVE_OBJECT ] ):
2251 if not self.MouseOverTest(event):
2252 self._RaiseMouseEvent(event,EVT_FC_MOTION)
2253 else:
2254 pass
2255 self._RaiseMouseEvent(event,EVT_FC_MOTION)
2256 else:
2257 pass
2258
2259 def RightDownEvent(self,event):
2260 if self.GUIMode:
2261 if self.GUIMode == "ZoomIn":
2262 Center = self.PixelToWorld((event.GetX(),event.GetY()))
2263 self.Zoom(1/1.5,Center)
2264 elif self.GUIMode == "ZoomOut":
2265 Center = self.PixelToWorld((event.GetX(),event.GetY()))
2266 self.Zoom(1.5,Center)
2267 elif self.GUIMode == "Mouse":
2268 EventType = EVT_FC_RIGHT_DOWN
2269 if not self.HitTest(event, EventType):
2270 self._RaiseMouseEvent(event, EventType)
2271 else:
2272 pass
2273
2274 def MakeNewBuffers(self):
2275 #print "Making new buffers"
2276 self._BackgroundDirty = True
2277 # Make new offscreen bitmap:
2278 self._Buffer = wx.EmptyBitmap(*self.PanelSize)
2279 #dc = wx.MemoryDC()
2280 #dc.SelectObject(self._Buffer)
2281 #dc.Clear()
2282 if self._ForeDrawList:
2283 self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize)
2284 else:
2285 self._ForegroundBuffer = None
2286 if self.UseHitTest:
2287 self.MakeNewHTdc()
2288 else:
2289 self._HTdc = None
2290 self._ForegroundHTdc = None
2291
2292 def MakeNewHTdc(self):
2293 ## Note: While it's considered a "bad idea" to keep a
2294 ## MemoryDC around I'm doing it here because a wx.Bitmap
2295 ## doesn't have a GetPixel method so a DC is needed to do
2296 ## the hit-test. It didn't seem like a good idea to re-create
2297 ## a wx.MemoryDC on every single mouse event, so I keep it
2298 ## around instead
2299 self._HTdc = wx.MemoryDC()
2300 self._HTBitmap = wx.EmptyBitmap(*self.PanelSize)
2301 self._HTdc.SelectObject( self._HTBitmap )
2302 self._HTdc.SetBackground(wx.BLACK_BRUSH)
2303 if self._ForeDrawList:
2304 self._ForegroundHTdc = wx.MemoryDC()
2305 self._ForegroundHTBitmap = wx.EmptyBitmap(*self.PanelSize)
2306 self._ForegroundHTdc.SelectObject( self._ForegroundHTBitmap )
2307 self._ForegroundHTdc.SetBackground(wx.BLACK_BRUSH)
2308 else:
2309 self._ForegroundHTdc = None
2310
2311 def OnSize(self,event):
2312 self.PanelSize = self.GetClientSizeTuple()
2313 if self.PanelSize == (0,0):
2314 ## OS-X sometimes gives a Size event when the panel is size (0,0)
2315 self.PanelSize = (2,2)
2316 self.PanelSize = array(self.PanelSize, Int32)
2317 self.HalfPanelSize = self.PanelSize / 2 # lrk: added for speed in WorldToPixel
2318 if self.PanelSize[0] == 0 or self.PanelSize[1] == 0:
2319 self.AspectRatio = 1.0
2320 else:
2321 self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1]
2322 self.MakeNewBuffers()
2323 self.Draw()
2324
2325 def OnPaint(self, event):
2326 dc = wx.PaintDC(self)
2327 if self._ForegroundBuffer:
2328 dc.DrawBitmap(self._ForegroundBuffer,0,0)
2329 else:
2330 dc.DrawBitmap(self._Buffer,0,0)
2331
2332 def Draw(self, Force=False):
2333 """
2334 There is a main buffer set up to double buffer the screen, so
2335 you can get quick re-draws when the window gets uncovered.
2336
2337 If there are any objects in self._ForeDrawList, then the
2338 background gets drawn to a new buffer, and the foreground
2339 objects get drawn on top of it. The final result if blitted to
2340 the screen, and stored for future Paint events. This is done so
2341 that you can have a complicated background, but have something
2342 changing on the foreground, without having to wait for the
2343 background to get re-drawn. This can be used to support simple
2344 animation, for instance.
2345
2346 """
2347 if sometrue(self.PanelSize < 1 ): # it's possible for this to get called before being properly initialized.
2348 return
2349 if self.Debug: start = clock()
2350 ScreenDC = wx.ClientDC(self)
2351 ViewPortWorld = ( self.PixelToWorld((0,0)),
2352 self.PixelToWorld(self.PanelSize) )
2353 ViewPortBB = array( ( minimum.reduce(ViewPortWorld),
2354 maximum.reduce(ViewPortWorld) ) )
2355 dc = wx.MemoryDC()
2356 dc.SelectObject(self._Buffer)
2357 if self._BackgroundDirty or Force:
2358 #print "Background is Dirty"
2359 dc.SetBackground(self.BackgroundBrush)
2360 dc.Clear()
2361 if self._HTdc:
2362 self._HTdc.Clear()
2363 self._DrawObjects(dc, self._DrawList, ScreenDC, ViewPortBB, self._HTdc)
2364 self._BackgroundDirty = False
2365
2366 if self._ForeDrawList:
2367 ## If an object was just added to the Foreground, there might not yet be a buffer
2368 if self._ForegroundBuffer is None:
2369 self._ForegroundBuffer = wx.EmptyBitmap(self.PanelSize[0],
2370 self.PanelSize[1])
2371
2372 dc = wx.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here
2373 dc.SelectObject(self._ForegroundBuffer)
2374 dc.DrawBitmap(self._Buffer,0,0)
2375 if self._ForegroundHTdc is None:
2376 self._ForegroundHTdc = wx.MemoryDC()
2377 self._ForegroundHTdc.SelectObject( wx.EmptyBitmap(
2378 self.PanelSize[0],
2379 self.PanelSize[1]) )
2380 if self._HTdc:
2381 ## blit the background HT buffer to the foreground HT buffer
2382 self._ForegroundHTdc.Blit(0, 0,
2383 self.PanelSize[0], self.PanelSize[1],
2384 self._HTdc, 0, 0)
2385 self._DrawObjects(dc,
2386 self._ForeDrawList,
2387 ScreenDC,
2388 ViewPortBB,
2389 self._ForegroundHTdc)
2390 ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0)
2391 # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn
2392 # This seeems out of place, but it works.
2393 if self.PrevRBBox:
2394 ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH))
2395 ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH)
2396 ScreenDC.SetLogicalFunction(wx.XOR)
2397 ScreenDC.DrawRectanglePointSize(*self.PrevRBBox)
2398 if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start)
2399
2400 ## Clear the font cache
2401 ## IF you don't do this, the X font server starts to take up Massive amounts of memory
2402 ## This is mostly a problem with very large fonts, that you get with scaled text when zoomed in.
2403 DrawObject.FontList = {}
2404
2405 def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck
2406 # lrk: Returns the objects that should be redrawn
2407
2408 BB2 = ViewPortBB
2409 redrawlist = []
2410 for Object in DrawList:
2411 BB1 = Object.BoundingBox
2412 if (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and
2413 BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1]):
2414 redrawlist.append(Object)
2415 return redrawlist
2416 _ShouldRedraw = staticmethod(_ShouldRedraw)
2417
2418
2419 ## def BBCheck(self, BB1, BB2):
2420 ## """
2421
2422 ## BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise
2423
2424 ## """
2425 ## if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and
2426 ## (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ):
2427 ## return True
2428 ## else:
2429 ## return False
2430
2431 def MoveImage(self,shift,CoordType):
2432 """
2433 move the image in the window.
2434
2435 shift is an (x,y) tuple, specifying the amount to shift in each direction
2436
2437 It can be in any of three coordinates: Panel, Pixel, World,
2438 specified by the CoordType parameter
2439
2440 Panel coordinates means you want to shift the image by some
2441 fraction of the size of the displaed image
2442
2443 Pixel coordinates means you want to shift the image by some number of pixels
2444
2445 World coordinates mean you want to shift the image by an amount
2446 in Floating point world coordinates
2447
2448 """
2449
2450 shift = asarray(shift,Float)
2451 #print "shifting by:", shift
2452 if CoordType == 'Panel':# convert from panel coordinates
2453 shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector
2454 elif CoordType == 'Pixel': # convert from pixel coordinates
2455 shift = shift/self.TransformVector
2456 elif CoordType == 'World': # No conversion
2457 pass
2458 else:
2459 raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"')
2460
2461 #print "shifting by:", shift
2462
2463 self.ViewPortCenter = self.ViewPortCenter + shift
2464 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
2465 self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
2466 self._BackgroundDirty = True
2467 self.Draw()
2468
2469 def Zoom(self,factor,center = None):
2470
2471 """
2472 Zoom(factor, center) changes the amount of zoom of the image by factor.
2473 If factor is greater than one, the image gets larger.
2474 If factor is less than one, the image gets smaller.
2475
2476 Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming.
2477 If center is not given, the center will stay the same.
2478
2479 """
2480 self.Scale = self.Scale*factor
2481 if not center is None:
2482 self.ViewPortCenter = array(center,Float)
2483 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
2484 self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
2485 self._BackgroundDirty = True
2486 self.Draw()
2487
2488 def ZoomToBB(self, NewBB = None, DrawFlag = True):
2489
2490 """
2491
2492 Zooms the image to the bounding box given, or to the bounding
2493 box of all the objects on the canvas, if none is given.
2494
2495 """
2496
2497 if not NewBB is None:
2498 BoundingBox = NewBB
2499 else:
2500 if self.BoundingBoxDirty:
2501 self._ResetBoundingBox()
2502 BoundingBox = self.BoundingBox
2503 if not BoundingBox is None:
2504 self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2,
2505 (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float)
2506 self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
2507 # Compute the new Scale
2508 BoundingBox = BoundingBox * self.MapProjectionVector
2509 try:
2510 self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])),
2511 abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )*0.95
2512 except ZeroDivisionError: # this will happen if the BB has zero width or height
2513 try: #width == 0
2514 self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95
2515 except ZeroDivisionError:
2516 try: # height == 0
2517 self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95
2518 except ZeroDivisionError: #zero size! (must be a single point)
2519 self.Scale = 1
2520
2521 self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector
2522 if DrawFlag:
2523 self._BackgroundDirty = True
2524 self.Draw()
2525 else:
2526 # Reset the shifting and scaling to defaults when there is no BB
2527 self.ViewPortCenter= array( (0,0), Float)
2528 self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
2529 self.TransformVector = array( (1,-1), Float) # default Transformation
2530 self.Scale = 1
2531
2532 def RemoveObjects(self, Objects):
2533 for Object in Objects:
2534 self.RemoveObject(Object, ResetBB = False)
2535 self.BoundingBoxDirty = True
2536
2537 def RemoveObject(self, Object, ResetBB = True):
2538 ##fixme: Using the list.remove method is kind of slow
2539 if Object.InForeground:
2540 self._ForeDrawList.remove(Object)
2541 if not self._ForeDrawList:
2542 self._ForegroundBuffer = None
2543 self._ForegroundHTdc = None
2544 else:
2545 self._DrawList.remove(Object)
2546 self._BackgroundDirty = True
2547 if ResetBB:
2548 self.BoundingBoxDirty = True
2549
2550 def ClearAll(self, ResetBB = True):
2551 self._DrawList = []
2552 self._ForeDrawList = []
2553 self._BackgroundDirty = True
2554 self.HitColorGenerator = None
2555 self.UseHitTest = False
2556 if ResetBB:
2557 self._ResetBoundingBox()
2558 self.MakeNewBuffers()
2559 self.HitDict = None
2560
2561 ## No longer called
2562 ## def _AddBoundingBox(self,NewBB):
2563 ## if self.BoundingBox is None:
2564 ## self.BoundingBox = NewBB
2565 ## self.ZoomToBB(NewBB,DrawFlag = False)
2566 ## else:
2567 ## self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]),
2568 ## min(self.BoundingBox[0,1],NewBB[0,1])),
2569 ## (max(self.BoundingBox[1,0],NewBB[1,0]),
2570 ## max(self.BoundingBox[1,1],NewBB[1,1]))),
2571 ## Float)
2572
2573 def _getboundingbox(bboxarray): # lrk: added this
2574
2575 upperleft = minimum.reduce(bboxarray[:,0])
2576 lowerright = maximum.reduce(bboxarray[:,1])
2577 return array((upperleft, lowerright), Float)
2578
2579 _getboundingbox = staticmethod(_getboundingbox)
2580
2581 def _ResetBoundingBox(self):
2582 if self._DrawList or self._ForeDrawList:
2583 bboxarray = zeros((len(self._DrawList)+len(self._ForeDrawList), 2, 2),Float)
2584 i = -1 # just in case _DrawList is empty
2585 for (i, BB) in enumerate(self._DrawList):
2586 bboxarray[i] = BB.BoundingBox
2587 for (j, BB) in enumerate(self._ForeDrawList):
2588 bboxarray[i+j+1] = BB.BoundingBox
2589 self.BoundingBox = self._getboundingbox(bboxarray)
2590 else:
2591 self.BoundingBox = None
2592 self.ViewPortCenter= array( (0,0), Float)
2593 self.TransformVector = array( (1,-1), Float)
2594 self.MapProjectionVector = array( (1,1), Float)
2595 self.Scale = 1
2596 self.BoundingBoxDirty = False
2597
2598 def PixelToWorld(self,Points):
2599 """
2600 Converts coordinates from Pixel coordinates to world coordinates.
2601
2602 Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates.
2603
2604 """
2605 return (((asarray(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter)
2606
2607 def WorldToPixel(self,Coordinates):
2608 """
2609 This function will get passed to the drawing functions of the objects,
2610 to transform from world to pixel coordinates.
2611 Coordinates should be a NX2 array of (x,y) coordinates, or
2612 a 2-tuple, or sequence of 2-tuples.
2613 """
2614 #Note: this can be called by users code for various reasons, so asarray is needed.
2615 return (((asarray(Coordinates,Float) -
2616 self.ViewPortCenter)*self.TransformVector)+
2617 (self.HalfPanelSize)).astype('i')
2618
2619 def ScaleWorldToPixel(self,Lengths):
2620 """
2621 This function will get passed to the drawing functions of the objects,
2622 to Change a length from world to pixel coordinates.
2623
2624 Lengths should be a NX2 array of (x,y) coordinates, or
2625 a 2-tuple, or sequence of 2-tuples.
2626 """
2627 return ( (asarray(Lengths,Float)*self.TransformVector) ).astype('i')
2628
2629 def ScalePixelToWorld(self,Lengths):
2630 """
2631 This function computes a pair of x.y lengths,
2632 to change then from pixel to world coordinates.
2633
2634 Lengths should be a NX2 array of (x,y) coordinates, or
2635 a 2-tuple, or sequence of 2-tuples.
2636 """
2637
2638 return (asarray(Lengths,Float) / self.TransformVector)
2639
2640 def AddObject(self,obj):
2641 # put in a reference to the Canvas, so remove and other stuff can work
2642 obj._Canvas = self
2643 if obj.InForeground:
2644 self._ForeDrawList.append(obj)
2645 self.UseForeground = True
2646 else:
2647 self._DrawList.append(obj)
2648 self._BackgroundDirty = True
2649 self.BoundingBoxDirty = True
2650 return True
2651
2652 def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None):
2653 """
2654 This is a convenience function;
2655 This function takes the list of objects and draws them to specified
2656 device context.
2657 """
2658 dc.SetBackground(self.BackgroundBrush)
2659 dc.BeginDrawing()
2660 #i = 0
2661 PanelSize0, PanelSize1 = self.PanelSize # for speed
2662 WorldToPixel = self.WorldToPixel # for speed
2663 ScaleWorldToPixel = self.ScaleWorldToPixel # for speed
2664 Blit = ScreenDC.Blit # for speed
2665 NumBetweenBlits = self.NumBetweenBlits # for speed
2666 for i, Object in enumerate(self._ShouldRedraw(DrawList, ViewPortBB)):
2667 if Object.Visible:
2668 Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc)
2669 if (i+1) % NumBetweenBlits == 0:
2670 Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0)
2671 dc.EndDrawing()
2672
2673 def SaveAsImage(self, filename, ImageType=wx.BITMAP_TYPE_PNG):
2674 """
2675
2676 Saves the current image as an image file. The default is in the
2677 PNG format. Other formats can be spcified using the wx flags:
2678
2679 wx.BITMAP_TYPE_BMP
2680 wx.BITMAP_TYPE_XBM
2681 wx.BITMAP_TYPE_XPM
2682 etc. (see the wx docs for the complete list)
2683
2684 """
2685
2686 self._Buffer.SaveFile(filename, ImageType)
2687
2688
2689 def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__
2690 classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon",
2691 "Line", "Text", "PointSet","Point", "Arrow","ScaledTextBox",
2692 "SquarePoint","Bitmap", "ScaledBitmap"]
2693 for classname in classnames:
2694 klass = globals()[classname]
2695 def getaddshapemethod(klass=klass):
2696 def addshape(self, *args, **kwargs):
2697 Object = klass(*args, **kwargs)
2698 self.AddObject(Object)
2699 return Object
2700 return addshape
2701 addshapemethod = getaddshapemethod()
2702 methodname = "Add" + classname
2703 setattr(FloatCanvas, methodname, addshapemethod)
2704 docstring = "Creates %s and adds its reference to the canvas.\n" % classname
2705 docstring += "Argument protocol same as %s class" % classname
2706 if klass.__doc__:
2707 docstring += ", whose docstring is:\n%s" % klass.__doc__
2708 FloatCanvas.__dict__[methodname].__doc__ = docstring
2709
2710 _makeFloatCanvasAddMethods()
2711
2712