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