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