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