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