X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/fdc775af55c604458345c7045b8478fa230e3bce..122c2bae32138468f1da8bd78d6166bff19de3d1:/wxPython/wx/lib/floatcanvas/FloatCanvas.py diff --git a/wxPython/wx/lib/floatcanvas/FloatCanvas.py b/wxPython/wx/lib/floatcanvas/FloatCanvas.py index 0d5142fcab..5a9b74186c 100644 --- a/wxPython/wx/lib/floatcanvas/FloatCanvas.py +++ b/wxPython/wx/lib/floatcanvas/FloatCanvas.py @@ -1,29 +1,34 @@ +from __future__ import division try: - from Numeric import array,asarray,Float,cos,pi,sum,minimum,maximum,Int32,zeros + 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 except ImportError: - from numarray import array, asarray, Float, cos, pi, sum, minimum, maximum, Int32, zeros + try: + from numarray import array, asarray, Float, cos, sin, pi, sum, minimum, maximum, Int32, zeros, concatenate, matrixmultiply, transpose, sometrue, arange, hypot + except ImportError: + raise ImportError("I could not import either Numeric or numarray") from time import clock, sleep +import Resources # A file with icons, etc for FloatCanvas + import wx import types import os -import Resources - ## A global variable to hold the Pixels per inch that wxWindows thinks is in use ## This is used for scaling fonts. -## This can't be computed on module __init__, because a wx.App might not have iniitalized yet. -global ScreenPPI +## This can't be computed on module __init__, because a wx.App might not have initialized yet. +global ScreenPPI ## a custom Exceptions: -class FloatCanvasException(Exception): +class FloatCanvasError(Exception): pass -## All the mouse events +## Create all the mouse events +# I don't see a need for these two, but maybe some day! #EVT_FC_ENTER_WINDOW = wx.NewEventType() #EVT_FC_LEAVE_WINDOW = wx.NewEventType() EVT_FC_LEFT_DOWN = wx.NewEventType() @@ -41,6 +46,7 @@ EVT_FC_MOUSEWHEEL = wx.NewEventType() EVT_FC_ENTER_OBJECT = wx.NewEventType() EVT_FC_LEAVE_OBJECT = wx.NewEventType() +##Create all mouse event binding functions #def EVT_ENTER_WINDOW( window, function ): # window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function ) #def EVT_LEAVE_WINDOW( window, function ): @@ -68,7 +74,7 @@ def EVT_MOTION( window, function ): def EVT_MOUSEWHEEL( window, function ): window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function ) -class MouseEvent(wx.PyCommandEvent): +class _MouseEvent(wx.PyCommandEvent): """ @@ -82,7 +88,7 @@ class MouseEvent(wx.PyCommandEvent): GetCoords() , which returns and (x,y) tuple in world coordinates. - Another differnce is that it is a CommandEvent, which propagates up + Another difference is that it is a CommandEvent, which propagates up the window hierarchy until it is handled. """ @@ -94,8 +100,9 @@ class MouseEvent(wx.PyCommandEvent): self._NativeEvent = NativeEvent self.Coords = Coords - def SetCoords(self,Coords): - self.Coords = Coords +# I don't think this is used. +# def SetCoords(self,Coords): +# self.Coords = Coords def GetCoords(self): return self.Coords @@ -104,74 +111,27 @@ class MouseEvent(wx.PyCommandEvent): #return eval(self.NativeEvent.__getattr__(name) ) return getattr(self._NativeEvent, name) -#### ColorGEnerator class is now obsolete. I'm using a python generator function instead. -##class ColorGenerator: +def _cycleidxs(indexcount, maxvalue, step): -## """ - -## An instance of this class generates a unique color each time -## GetNextColor() is called. Someday I will use a proper Python -## generator for this class. - -## The point of this generator is for the hit-test bitmap, each object -## needs to be a unique color. Also, each system can be running a -## different number of colors, and it doesn't appear to be possible to -## have a wxMemDC with a different colordepth as the screen so this -## generates colors far enough apart that they can be distinguished on -## a 16bit screen. Anything less than 16bits won't work. It could, but -## I havn't written the code that way. You also wouldn't get many -## distict colors - -## """ + """ + Utility function used by _colorGenerator -## def __init__(self): -## import sys -## ## figure out the color depth of the screen -## ## for some bizare reason, thisdoesn't work on OS-X -## if sys.platform == 'darwin': -## depth = 24 -## else: -## b = wx.EmptyBitmap(1,1) -## depth = b.GetDepth() -## self.r = 0 -## self.g = 0 -## self.b = 0 -## if depth == 16: -## self.step = 8 -## elif depth >= 24: -## self.step = 1 -## else: -## raise FloatCanvasException("ColorGenerator does not work with depth = %s"%depth ) - -## def GetNextColor(self): -## step = self.step -## ##r,g,b = self.r,self.g,self.b -## self.r += step -## if self.r > 255: -## self.r = step -## self.g += step -## if self.g > 255: -## self.g = step -## self.b += step -## if self.b > 255: -## ## fixme: this should be a derived exception -## raise FloatCanvasException("Too many objects in colorgenerator for HitTest") -## return (self.r,self.g,self.b) - -## def Reset(self): -## self.r = 0 -## self.g = 0 -## self.b = 0 - -def cycleidxs(indexcount, maxvalue, step): + """ if indexcount == 0: yield () else: for idx in xrange(0, maxvalue, step): - for tail in cycleidxs(indexcount - 1, maxvalue, step): + for tail in _cycleidxs(indexcount - 1, maxvalue, step): yield (idx, ) + tail -def colorGenerator(): +def _colorGenerator(): + + """ + + Generates a seris of unique colors used to do hit-tests with the HIt + Test bitmap + + """ import sys if sys.platform == 'darwin': depth = 24 @@ -184,12 +144,13 @@ def colorGenerator(): step = 1 else: raise "ColorGenerator does not work with depth = %s" % depth - return cycleidxs(indexcount=3, maxvalue=256, step=step) + return _cycleidxs(indexcount=3, maxvalue=256, step=step) -#### I don't know if the Set objects are useful, beyond the pointset object -#### The problem is that when zoomed in, the BB is checked to see whether to draw the object. -#### A Set object can defeat this +#### I don't know if the Set objects are useful, beyond the pointset +#### object. The problem is that when zoomed in, the BB is checked to see +#### whether to draw the object. A Set object can defeat this. One day +#### I plan to write some custon C++ code to draw sets of objects ##class ObjectSetMixin: ## """ @@ -246,15 +207,16 @@ def colorGenerator(): ## if length == 1: ## self.Pens = self.Pens[0] - - class DrawObject: """ This is the base class for all the objects that can be drawn. + One must subclass from this (and an assortment of Mixins) to create + a new DrawObject. + """ - def __init__(self,InForeground = False): + def __init__(self, InForeground = False, IsVisible = True): self.InForeground = InForeground self._Canvas = None @@ -268,7 +230,14 @@ class DrawObject: self.HitFill = True self.MinHitLineWidth = 3 self.HitLineWidth = 3 ## this gets re-set by the subclasses if necessary + + self.Brush = None + self.Pen = None + + self.FillStyle = "Solid" + self.Visible = IsVisible + # I pre-define all these as class variables to provide an easier # interface, and perhaps speed things up by caching all the Pens # and Brushes, although that may not help, as I think wx now @@ -326,7 +295,7 @@ class DrawObject: self._Canvas.MakeNewHTdc() if not self.HitColor: if not self._Canvas.HitColorGenerator: - self._Canvas.HitColorGenerator = colorGenerator() + self._Canvas.HitColorGenerator = _colorGenerator() self._Canvas.HitColorGenerator.next() # first call to prevent the background color from being used. self.HitColor = self._Canvas.HitColorGenerator.next() self.SetHitPen(self.HitColor,self.HitLineWidth) @@ -336,7 +305,6 @@ class DrawObject: self._Canvas.MakeHitDict() self._Canvas.HitDict[Event][self.HitColor] = (self) # put the object in the hit dict, indexed by it's color - def UnBindAll(self): ## fixme: this only removes one from each list, there could be more. if self._Canvas.HitDict: @@ -347,9 +315,11 @@ class DrawObject: pass self.HitAble = False + def SetBrush(self,FillColor,FillStyle): if FillColor is None or FillStyle is None: self.Brush = wx.TRANSPARENT_BRUSH + ##fixme: should I really re-set the style? self.FillStyle = "Transparent" else: self.Brush = self.BrushList.setdefault( (FillColor,FillStyle), wx.Brush(FillColor,self.FillStyleList[FillStyle] ) ) @@ -371,7 +341,7 @@ class DrawObject: if not self.HitLine: self.HitPen = wx.TRANSPARENT_PEN else: - self.HitPen = self.PenList.setdefault( (HitColor, "solid", LineWidth), wx.Pen(HitColor, LineWidth, self.LineStyleList["Solid"]) ) + self.HitPen = self.PenList.setdefault( (HitColor, "solid", self.HitLineWidth), wx.Pen(HitColor, self.HitLineWidth, self.LineStyleList["Solid"]) ) def PutInBackground(self): if self._Canvas and self.InForeground: @@ -387,6 +357,61 @@ class DrawObject: self._Canvas._BackgroundDirty = True self.InForeground = True + def Hide(self): + self.Visible = False + + def Show(self): + self.Visible = True + +class ColorOnlyMixin: + """ + + Mixin class for objects that have just one color, rather than a fill + color and line color + + """ + + def SetColor(self, Color): + self.SetPen(Color,"Solid",1) + self.SetBrush(Color,"Solid") + + SetFillColor = SetColor # Just to provide a consistant interface + +class LineOnlyMixin: + """ + + Mixin class for objects that have just one color, rather than a fill + color and line color + + """ + + def SetLineColor(self, LineColor): + self.LineColor = LineColor + self.SetPen(LineColor,self.LineStyle,self.LineWidth) + + def SetLineStyle(self, LineStyle): + self.LineStyle = LineStyle + self.SetPen(self.LineColor,LineStyle,self.LineWidth) + + def SetLineWidth(self, LineWidth): + self.LineWidth = LineWidth + self.SetPen(self.LineColor,self.LineStyle,LineWidth) + +class LineAndFillMixin(LineOnlyMixin): + """ + + Mixin class for objects that have both a line and a fill color and + style. + + """ + def SetFillColor(self, FillColor): + self.FillColor = FillColor + self.SetBrush(FillColor, self.FillStyle) + + def SetFillStyle(self, FillStyle): + self.FillStyle = FillStyle + self.SetBrush(self.FillColor,FillStyle) + class XYObjectMixin: """ @@ -406,9 +431,26 @@ class XYObjectMixin: Delta = asarray(Delta, Float) self.XY += Delta self.BoundingBox = self.BoundingBox + Delta + if self._Canvas: self._Canvas.BoundingBoxDirty = True + def CalcBoundingBox(self): + ## This may get overwritten in some subclasses + self.BoundingBox = array( (self.XY, self.XY), Float ) + + def SetPoint(self, xy): + xy = array( xy, Float) + xy.shape = (2,) + Delta = xy - self.XY + + self.XY = xy + self.BoundingBox = self.BoundingBox + Delta + + #self.CalcBoundingBox() + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + class PointsObjectMixin: """ @@ -417,12 +459,15 @@ class PointsObjectMixin: """ -## This is code for the XYMixin object, it needs to be adapeted and tested. + +## This is code for the PointsObjectMixin object, it needs to be adapted and tested. +## Is the neccesary at all: you can always do: +## Object.SetPoints( Object.Points + delta, copy = False) ## def Move(self, Delta ): ## """ ## Move(Delta): moves the object by delta, where delta is an (dx, -## dy) pair. Ideally a Numpy array or shape (2,) +## dy) pair. Ideally a Numpy array of shape (2,) ## """ @@ -432,15 +477,38 @@ class PointsObjectMixin: ## if self._Canvas: ## self._Canvas.BoundingBoxDirty = True - def SetPoints(self,Points): - self.Points = Points - self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) + def CalcBoundingBox(self): + self.BoundingBox = array(((min(self.Points[:,0]), + min(self.Points[:,1]) ), + (max(self.Points[:,0]), + max(self.Points[:,1]) ) ), Float ) if self._Canvas: self._Canvas.BoundingBoxDirty = True + def SetPoints(self, Points, copy = True): + """ + Sets the coordinates of the points of the object to Points (NX2 array). + + By default, a copy is made, if copy is set to False, a reference + is used, iff Points is a NumPy array of Floats. This allows you + to change some or all of the points without making any copies. + + For example: + + Points = Object.Points + Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir. + Object.SetPoints(Points, False) # Sets the points to the same array as it was + + """ + if copy: + self.Points = array(Points, Float) + self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point + else: + self.Points = asarray(Points, Float) + self.CalcBoundingBox() -class Polygon(DrawObject,PointsObjectMixin): +class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin): """ @@ -450,6 +518,9 @@ class Polygon(DrawObject,PointsObjectMixin): x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. + The other parameters specify various properties of the Polygon, and + should be self explanatory. + """ def __init__(self, Points, @@ -461,7 +532,7 @@ class Polygon(DrawObject,PointsObjectMixin): InForeground = False): DrawObject.__init__(self,InForeground) self.Points = array(Points,Float) # this DOES need to make a copy - self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) + self.CalcBoundingBox() self.LineColor = LineColor self.LineStyle = LineStyle @@ -475,7 +546,7 @@ class Polygon(DrawObject,PointsObjectMixin): self.SetBrush(FillColor,FillStyle) def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None): - Points = WorldToPixel(self.Points) + Points = WorldToPixel(self.Points)#.tolist() dc.SetPen(self.Pen) dc.SetBrush(self.Brush) dc.DrawPolygon(Points) @@ -517,13 +588,14 @@ class Polygon(DrawObject,PointsObjectMixin): ## dc.DrawLineList(Points,self.Pens) -class Line(DrawObject,PointsObjectMixin): +class Line(DrawObject,PointsObjectMixin,LineOnlyMixin): """ - The Line class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. - so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate - or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. - It will draw a straight line if there are two points, and a polyline if there are more than two. + The Line class takes a list of 2-tuples, or a NX2 NumPy Float array + of point coordinates. + + It will draw a straight line if there are two points, and a polyline + if there are more than two. """ def __init__(self,Points, @@ -535,7 +607,7 @@ class Line(DrawObject,PointsObjectMixin): self.Points = array(Points,Float) - self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) + self.CalcBoundingBox() self.LineColor = LineColor self.LineStyle = LineStyle @@ -554,6 +626,99 @@ class Line(DrawObject,PointsObjectMixin): HTdc.SetPen(self.HitPen) HTdc.DrawLines(Points) +class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): + """ + + Arrow(XY, # coords of origin of arrow (x,y) + Length, # length of arrow in pixels + theta, # angle of arrow in degrees: zero is straight up + # angle is to the right + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + ArrowHeadSize = 4, + ArrowHeadAngle = 45, + InForeground = False): + + It will draw an arrow , starting at the point, (X,Y) pointing in + direction, theta. + + + """ + def __init__(self, + XY, + Length, + Direction, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 2, # pixels + ArrowHeadSize = 8, # pixels + ArrowHeadAngle = 30, # degrees + InForeground = False): + + DrawObject.__init__(self, InForeground) + + self.XY = array(XY, Float) + self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + self.Length = Length + self.Direction = float(Direction) + self.ArrowHeadSize = ArrowHeadSize + self.ArrowHeadAngle = float(ArrowHeadAngle) + + self.CalcArrowPoints() + self.CalcBoundingBox() + + self.LineColor = LineColor + self.LineStyle = LineStyle + self.LineWidth = LineWidth + + self.SetPen(LineColor,LineStyle,LineWidth) + + ##fixme: How should the HitTest be drawn? + self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) + + def SetDirection(self, Direction): + self.Direction = float(Direction) + self.CalcArrowPoints() + + def SetLength(self, Length): + self.Length = Length + self.CalcArrowPoints() + + def SetLengthDirection(self, Length, Direction): + self.Direction = float(Direction) + self.Length = Length + self.CalcArrowPoints() + + def SetLength(self, Length): + self.Length = Length + self.CalcArrowPoints() + + ## fixme: cache this? + def CalcArrowPoints(self): + L = self.Length + S = self.ArrowHeadSize + phi = self.ArrowHeadAngle * pi / 360 + theta = (self.Direction-90.0) * pi / 180 + ArrowPoints = array( ( (0, L, L - S*cos(phi),L, L - S*cos(phi) ), + (0, 0, S*sin(phi), 0, -S*sin(phi) ) ), + Float ) + RotationMatrix = array( ( ( cos(theta), -sin(theta) ), + ( sin(theta), cos(theta) ) ), + Float + ) + ArrowPoints = matrixmultiply(RotationMatrix, ArrowPoints) + self.ArrowPoints = transpose(ArrowPoints) + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + dc.SetPen(self.Pen) + xy = WorldToPixel(self.XY) + ArrowPoints = xy + self.ArrowPoints + dc.DrawLines(ArrowPoints) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.DrawLines(ArrowPoints) + ##class LineSet(DrawObject, ObjectSetMixin): ## """ ## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. @@ -585,18 +750,24 @@ class Line(DrawObject,PointsObjectMixin): ## Points.shape = (-1,4) ## dc.DrawLineList(Points,self.Pens) -class PointSet(DrawObject): +class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): """ - The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. - so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate - or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. - Each point will be drawn the same color and Diameter. The Diameter is in screen points, - not world coordinates. + The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of + point coordinates. + + If Points is a sequence of tuples: Points[N][0] is the x-coordinate of + point N and Points[N][1] is the y-coordinate. - At this point, the hit-test code does not distingish between the - points, you will only know that one of the poins got hit, not which - one. + If Points is a NumPy array: Points[N,0] is the x-coordinate of point + N and Points[N,1] is the y-coordinate for arrays. + + Each point will be drawn the same color and Diameter. The Diameter + is in screen pixels, not world coordinates. + + The hit-test code does not distingish between the points, you will + only know that one of the points got hit, not which one. You can use + PointSet.FindClosestPoint(WorldPoint) to find out which one In the case of points, the HitLineWidth is used as diameter. @@ -606,27 +777,30 @@ class PointSet(DrawObject): self.Points = array(Points,Float) self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point - self.BoundingBox = array(((min(self.Points[:,0]), - min(self.Points[:,1])), - (max(self.Points[:,0]), - max(self.Points[:,1]))),Float) - - self.Color = Color + self.CalcBoundingBox() self.Diameter = Diameter self.HitLineWidth = self.MinHitLineWidth - self.SetPen(Color,"Solid",1) - self.SetBrush(Color,"Solid") + self.SetColor(Color) - def SetPoints(self,Points): - self.Points = array(Points, Float) - self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point - self.BoundingBox = array(((min(self.Points[:,0]), - min(self.Points[:,1]) ), - (max(self.Points[:,0]), - max(self.Points[:,1]) ) ) ) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True + def SetDiameter(self,Diameter): + self.Diameter = Diameter + + def FindClosestPoint(self, XY): + """ + + Returns the index of the closest point to the point, XY, given + in World coordinates. It's essentially random which you get if + there are more than one that are the same. + + This can be used to figure out which point got hit in a mouse + binding callback, for instance. It's a lot faster that using a + lot of separate points. + + """ + d = self.Points - XY + return argmin(hypot(d[:,0],d[:,1])) + def DrawD2(self, dc, Points): # A Little optimization for a diameter2 - point @@ -645,58 +819,124 @@ class PointSet(DrawObject): else: dc.SetBrush(self.Brush) radius = int(round(self.Diameter/2)) - for xy in Points: - dc.DrawEllipsePointSize( (xy - radius), (self.Diameter, self.Diameter) ) + ##fixme: I really should add a DrawCircleList to wxPython + if len(Points) > 100: + xy = Points + xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 ) + dc.DrawEllipseList(xywh) + else: + for xy in Points: + dc.DrawCircle(xy[0],xy[1], radius) if HTdc and self.HitAble: HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) if self.Diameter <= 1: HTdc.DrawPointList(Points) elif self.Diameter <= 2: self.DrawD2(HTdc, Points) + else: + if len(Points) > 100: + xy = Points + xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 ) + HTdc.DrawEllipseList(xywh) + else: + for xy in Points: + HTdc.DrawCircle(xy[0],xy[1], radius) + +class Point(DrawObject,XYObjectMixin,ColorOnlyMixin): + """ + + The Point class takes a 2-tuple, or a (2,) NumPy array of point + coordinates. + + The Diameter is in screen points, not world coordinates, So the + Bounding box is just the point, and doesn't include the Diameter. + + The HitLineWidth is used as diameter for the + Hit Test. + + """ + def __init__(self, XY, Color = "Black", Diameter = 1, InForeground = False): + DrawObject.__init__(self, InForeground) + + self.XY = array(XY, Float) + self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + self.CalcBoundingBox() + self.SetColor(Color) + self.Diameter = Diameter + + self.HitLineWidth = self.MinHitLineWidth + + def SetDiameter(self,Diameter): + self.Diameter = Diameter + + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + dc.SetPen(self.Pen) + xy = WorldToPixel(self.XY) + if self.Diameter <= 1: + dc.DrawPoint(xy[0], xy[1]) + else: + dc.SetBrush(self.Brush) + radius = int(round(self.Diameter/2)) + dc.DrawCircle(xy[0],xy[1], radius) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + if self.Diameter <= 1: + HTdc.DrawPoint(xy[0], xy[1]) else: HTdc.SetBrush(self.HitBrush) - radius = int(round(self.Diameter/2)) - for xy in Points: - HTdc.DrawEllipsePointSize( (xy - radius), (self.Diameter, self.Diameter) ) - -#### Does anyone need this? -##class Dot(DrawObject): -## """ -## The Dot class takes an x.y coordinate pair, and the Diameter of the circle. -## The Diameter is in pixels, so it won't change with zoom. + HTdc.DrawCircle(xy[0],xy[1], radius) -## Also Fill and line data +class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin): + """ + + The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point + coordinates. It produces a square dot, centered on Point -## """ -## def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,InForeground = False): -## DrawObject.__init__(self,InForeground) + The Size is in screen points, not world coordinates, so the + Bounding box is just the point, and doesn't include the Size. -## self.X = x -## self.Y = y -## self.Diameter = Diameter -## # NOTE: the bounding box does not include the diameter of the dot, as that is in pixel coords. -## # If this is a problem, perhaps you should use a circle, instead! -## self.BoundingBox = array(((x,y),(x,y)),Float) + The HitLineWidth is used as diameter for the + Hit Test. -## self.LineColor = LineColor -## self.LineStyle = LineStyle -## self.LineWidth = LineWidth -## self.FillColor = FillColor -## self.FillStyle = FillStyle + """ + def __init__(self, Point, Color = "Black", Size = 4, InForeground = False): + DrawObject.__init__(self, InForeground) + + self.XY = array(Point, Float) + self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + self.CalcBoundingBox() + self.SetColor(Color) + self.Size = Size -## self.SetPen(LineColor,LineStyle,LineWidth) -## self.SetBrush(FillColor,FillStyle) + self.HitLineWidth = self.MinHitLineWidth -## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): -## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): -## dc.SetPen(self.Pen) -## dc.SetBrush(self.Brush) -## radius = int(round(self.Diameter/2)) -## (X,Y) = WorldToPixel((self.X,self.Y)) -## dc.DrawEllipse((X - radius), (Y - radius), self.Diameter, self.Diameter) - -class RectEllipse(DrawObject, XYObjectMixin): - def __init__(self,x,y,width,height, + def SetSize(self,Size): + self.Size = Size + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + Size = self.Size + dc.SetPen(self.Pen) + xc,yc = WorldToPixel(self.XY) + + if self.Size <= 1: + dc.DrawPoint(xc, yc) + else: + x = xc - Size/2.0 + y = yc - Size/2.0 + dc.SetBrush(self.Brush) + dc.DrawRectangle(x, y, Size, Size) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + if self.Size <= 1: + HTdc.DrawPoint(xc, xc) + else: + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(x, y, Size, Size) + +class RectEllipse(DrawObject, XYObjectMixin, LineAndFillMixin): + def __init__(self, XY, WH, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, @@ -706,9 +946,11 @@ class RectEllipse(DrawObject, XYObjectMixin): DrawObject.__init__(self,InForeground) - self.XY = array( (x, y), Float) - self.WH = array( (width, height), Float ) - self.BoundingBox = array(((x,y), (self.XY + self.WH)), Float) + self.XY = array( XY, Float) + self.XY.shape = (2,) + self.WH = array( WH, Float ) + self.WH.shape = (2,) + self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth @@ -720,6 +962,11 @@ class RectEllipse(DrawObject, XYObjectMixin): self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) + def SetShape(self, XY, WH): + self.XY = array( XY, Float) + self.WH = array( WH, Float ) + self.CalcBoundingBox() + def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): dc.SetPen(self.Pen) @@ -730,17 +977,12 @@ class RectEllipse(DrawObject, XYObjectMixin): return ( WorldToPixel(self.XY), ScaleWorldToPixel(self.WH) ) - def SetXY(self, x, y): - self.XY = array( (x, y), Float) + def CalcBoundingBox(self): self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True + self._Canvas.BoundingBoxDirty = True class Rectangle(RectEllipse): -# def __init__(*args, **kwargs): -# RectEllipse.__init__(*args, **kwargs) -# raise "an error" def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): ( XY, WH ) = self.SetUpDraw(dc, @@ -752,8 +994,6 @@ class Rectangle(RectEllipse): HTdc.DrawRectanglePointSize(XY, WH) class Ellipse(RectEllipse): -# def __init__(*args, **kwargs): -# RectEllipse.__init__(*args, **kwargs) def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): ( XY, WH ) = self.SetUpDraw(dc, @@ -765,15 +1005,23 @@ class Ellipse(RectEllipse): HTdc.DrawEllipsePointSize(XY, WH) class Circle(Ellipse): - def __init__(self, x ,y, Diameter, **kwargs): + + def __init__(self, XY, Diameter, **kwargs): + self.Center = array(XY, Float) + Diameter = float(Diameter) RectEllipse.__init__(self , - x-Diameter/2., - y-Diameter/2., - Diameter, - Diameter, + self.Center - Diameter/2.0, + (Diameter, Diameter), **kwargs) + + def SetDiameter(self, Diameter): + Diameter = float(Diameter) + XY = self.Center - (Diameter/2.0) + self.SetShape(XY, + (Diameter, Diameter) + ) -class TextObjectMixin: +class TextObjectMixin(XYObjectMixin): """ A mix in class that holds attributes and methods that are needed by @@ -787,6 +1035,8 @@ class TextObjectMixin: FontList = {} + LayoutFontSize = 12 # font size used for calculating layout + def SetFont(self, Size, Family, Style, Weight, Underline, FaceName): self.Font = self.FontList.setdefault( (Size, Family, @@ -802,18 +1052,48 @@ class TextObjectMixin: FaceName) ) return self.Font + def SetColor(self, Color): + self.Color = Color + + def SetBackgroundColor(self, BackgroundColor): + self.BackgroundColor = BackgroundColor + + def SetText(self, String): + """ + Re-sets the text displayed by the object + + In the case of the ScaledTextBox, it will re-do the layout as appropriate + + Note: only tested with the ScaledTextBox + + """ + + self.String = String + self.LayoutText() + + def LayoutText(self): + """ + A dummy method to re-do the layout of the text. + + A derived object needs to override this if required. + + """ + pass + ## store the function that shift the coords for drawing text. The ## "c" parameter is the correction for world coordinates, rather ## than pixel coords as the y axis is reversed - ShiftFunDict = {'tl': lambda x, y, w, h, world=0: (x, y) , - 'tc': lambda x, y, w, h, world=0: (x - w/2, y) , - 'tr': lambda x, y, w, h, world=0: (x - w, y) , - 'cl': lambda x, y, w, h, world=0: (x, y - h/2 + world*h) , - 'cc': lambda x, y, w, h, world=0: (x - w/2, y - h/2 + world*h) , - 'cr': lambda x, y, w, h, world=0: (x - w, y - h/2 + world*h) , - 'bl': lambda x, y, w, h, world=0: (x, y - h + 2*world*h) , - 'bc': lambda x, y, w, h, world=0: (x - w/2, y - h + 2*world*h) , - 'br': lambda x, y, w, h, world=0: (x - w, y - h + 2*world*h)} + ## pad is the extra space around the text + ## if world = 1, the vertical shift is done in y-up coordinates + ShiftFunDict = {'tl': lambda x, y, w, h, world=0, pad=0: (x + pad, y + pad - 2*world*pad), + 'tc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y + pad - 2*world*pad), + 'tr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y + pad - 2*world*pad), + 'cl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h/2 + world*h), + 'cc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h/2 + world*h), + 'cr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h/2 + world*h), + 'bl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h + 2*world*h - pad + world*2*pad) , + 'bc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h + 2*world*h - pad + world*2*pad) , + 'br': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h + 2*world*h - pad + world*2*pad)} class Text(DrawObject, TextObjectMixin): """ @@ -848,7 +1128,8 @@ class Text(DrawObject, TextObjectMixin): The value can be True or False. At present this may have an an effect on Windows only. - Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored. + Alternatively, you can set the kw arg: Font, to a wx.Font, and the + above will be ignored. The size is fixed, and does not scale with the drawing. @@ -856,7 +1137,7 @@ class Text(DrawObject, TextObjectMixin): """ - def __init__(self,String,x,y, + def __init__(self,String, xy, Size = 12, Color = "Black", BackgroundColor = None, @@ -889,25 +1170,14 @@ class Text(DrawObject, TextObjectMixin): Weight = Font.GetWeight() self.SetFont(Size, Family, Style, Weight, Underline, FaceName) - self.BoundingBox = array(((x,y),(x,y)),Float) + self.BoundingBox = array((xy, xy),Float) - self.XY = ( x,y ) + self.XY = asarray(xy) + self.XY.shape = (2,) - # use a memDC -- ScreenDC doesn't work with 2.5.1 and GTK2 - #dc = wx.MemoryDC() - #bitmap = wx.EmptyBitmap(1, 1) - #dc.SelectObject(bitmap) - #dc.SetFont(self.Font) - #(self.TextWidth, self.TextHeight) = dc.GetTextExtent(self.String) (self.TextWidth, self.TextHeight) = (None, None) self.ShiftFun = self.ShiftFunDict[Position] - def SetXY(self, x, y): - self.XY = ( x,y ) - self.BoundingBox = array((self.XY, self.XY),Float) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): XY = WorldToPixel(self.XY) dc.SetFont(self.Font) @@ -926,7 +1196,7 @@ class Text(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(XY, (self.TextWidth, self.TextHeight) ) -class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): +class ScaledText(DrawObject, TextObjectMixin): """ This class creates a text object that is scaled when zoomed. It is placed at the coordinates, x,y. the "Position" argument is a two @@ -942,29 +1212,25 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): Family: Font family, a generic way of referring to fonts without - specifying actual facename. One of:: + specifying actual facename. One of: wx.DEFAULT: Chooses a default font. wx.DECORATI: A decorative font. wx.ROMAN: A formal, serif font. wx.SCRIPT: A handwriting font. wx.SWISS: A sans-serif font. wx.MODERN: A fixed pitch font. - NOTE: these are only as good as the wxWindows defaults, which aren't so good. - Style: One of wx.NORMAL, wx.SLANT and wx.ITALIC. - Weight: One of wx.NORMAL, wx.LIGHT and wx.BOLD. - Underline: The value can be True or False. At present this may have an an effect on Windows only. Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored. The size of the font you specify will be - ignored, but the rest of it's attributes will be preserved. + ignored, but the rest of its attributes will be preserved. The size will scale as the drawing is zoomed. @@ -983,11 +1249,11 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): zoom limit at that point. The hit-test is done on the entire text extent. This could be made - optional, but I havn't gotten around to it. + optional, but I haven't gotten around to it. """ - def __init__(self, String, x, y , Size, + def __init__(self, String, XY , Size, Color = "Black", BackgroundColor = None, Family = wx.MODERN, @@ -1001,7 +1267,8 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): DrawObject.__init__(self,InForeground) self.String = String - self.XY = array( (x, y), Float) + self.XY = array( XY, Float) + self.XY.shape = (2,) self.Size = Size self.Color = Color self.BackgroundColor = BackgroundColor @@ -1019,31 +1286,40 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): self.Weight = Font.GetWeight() # Experimental max font size value on wxGTK2: this works OK on - # my system If it's any larger, there is a crash, with the - # message: The application 'FloatCanvasDemo.py' lost its + # my system. If it's a lot larger, there is a crash, with the + # message: + # + # The application 'FloatCanvasDemo.py' lost its # connection to the display :0.0; most likely the X server was # shut down or you killed/destroyed the application. - self.MaxSize = 2750 + # + # Windows and OS-X seem to be better behaved in this regard. + # They may not draw it, but they don't crash either! + self.MaxFontSize = 1000 self.ShiftFun = self.ShiftFunDict[Position] - ## Compute the BB + self.CalcBoundingBox() + + def LayoutText(self): + # This will be called when the text is re-set + # nothing much to be done here + self.CalcBoundingBox() + + def CalcBoundingBox(self): ## this isn't exact, as fonts don't scale exactly. dc = wx.MemoryDC() bitmap = wx.EmptyBitmap(1, 1) dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work. DrawingSize = 40 # pts This effectively determines the resolution that the BB is computed to. - ScaleFactor = float(Size) / DrawingSize + ScaleFactor = float(self.Size) / DrawingSize dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) (w,h) = dc.GetTextExtent(self.String) w = w * ScaleFactor h = h * ScaleFactor - x, y = self.ShiftFun(x, y, w, h, world = 1) + x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) - - # the new coords are set to the corner of the BB: - #self.X = self.BoundingBox[0,0] - #self.Y = self.BoundingBox[1,1] + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): (X,Y) = WorldToPixel( (self.XY) ) @@ -1052,8 +1328,7 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): ## Check to see if the font size is large enough to blow up the X font server ## If so, limit it. Would it be better just to not draw it? ## note that this limit is dependent on how much memory you have, etc. - if Size > self.MaxSize: - Size = self.MaxSize + Size = min(Size, self.MaxFontSize) dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName)) dc.SetTextForeground(self.Color) if self.BackgroundColor: @@ -1073,9 +1348,420 @@ class ScaledText(DrawObject, TextObjectMixin, XYObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(xy, (w, h) ) +class ScaledTextBox(DrawObject, TextObjectMixin): + """ + This class creates a TextBox object that is scaled when zoomed. It is + placed at the coordinates, x,y. + + If the Width parameter is defined, the text will be wrapped to the width given. + + A Box can be drawn around the text, be specifying: + LineWidth and/or FillColor + + A space(margin) can be put all the way around the text, be specifying: + the PadSize argument in world coordinates. + + The spacing between lines can be adjusted with the: + LineSpacing argument. + + The "Position" argument is a two character string, indicating where + in relation to the coordinates the Box should be oriented. + -The first letter is: t, c, or b, for top, center and bottom. + -The second letter is: l, c, or r, for left, center and right The + position refers to the position relative to the text itself. It + defaults to "tl" (top left). + + Size is the size of the font in world coordinates. + + Family: + Font family, a generic way of referring to fonts without + specifying actual facename. One of: + wx.DEFAULT: Chooses a default font. + wx.DECORATIVE: A decorative font. + wx.ROMAN: A formal, serif font. + wx.SCRIPT: A handwriting font. + wx.SWISS: A sans-serif font. + wx.MODERN: A fixed pitch font. + NOTE: these are only as good as the wxWindows defaults, which aren't so good. + Style: + One of wx.NORMAL, wx.SLANT and wx.ITALIC. + Weight: + One of wx.NORMAL, wx.LIGHT and wx.BOLD. + Underline: + The value can be True or False. At present this may have an an + effect on Windows only. + + Alternatively, you can set the kw arg: Font, to a wx.Font, and the + above will be ignored. The size of the font you specify will be + ignored, but the rest of its attributes will be preserved. + + The size will scale as the drawing is zoomed. + + Bugs/Limitations: + + As fonts are scaled, they do end up a little different, so you don't + get exactly the same picture as you scale up and down, but it's + pretty darn close. + + On wxGTK1 on my Linux system, at least, using a font of over about + 1000 pts. brings the system to a halt. It's the Font Server using + huge amounts of memory. My work around is to max the font size to + 1000 points, so it won't scale past there. GTK2 uses smarter font + drawing, so that may not be an issue in future versions, so feel + free to test. Another smarter way to do it would be to set a global + zoom limit at that point. + + The hit-test is done on the entire box. This could be made + optional, but I haven't gotten around to it. + + """ + + def __init__(self, String, + Point, + Size, + Color = "Black", + BackgroundColor = None, + LineColor = 'Black', + LineStyle = 'Solid', + LineWidth = 1, + Width = None, + PadSize = None, + Family = wx.MODERN, + Style = wx.NORMAL, + Weight = wx.NORMAL, + Underline = False, + Position = 'tl', + Alignment = "left", + Font = None, + LineSpacing = 1.0, + InForeground = False): + + DrawObject.__init__(self,InForeground) + + self.XY = array(Point, Float) + self.Size = Size + self.Color = Color + self.BackgroundColor = BackgroundColor + self.LineColor = LineColor + self.LineStyle = LineStyle + self.LineWidth = LineWidth + self.Width = Width + if PadSize is None: # the default is just a little bit of padding + self.PadSize = Size/10.0 + else: + self.PadSize = float(PadSize) + self.Family = Family + self.Style = Style + self.Weight = Weight + self.Underline = Underline + self.Alignment = Alignment.lower() + self.LineSpacing = float(LineSpacing) + self.Position = Position + + if not Font: + self.FaceName = '' + else: + self.FaceName = Font.GetFaceName() + self.Family = Font.GetFamily() + self.Style = Font.GetStyle() + self.Underlined = Font.GetUnderlined() + self.Weight = Font.GetWeight() + + # Experimental max font size value on wxGTK2: this works OK on + # my system. If it's a lot larger, there is a crash, with the + # message: + # + # The application 'FloatCanvasDemo.py' lost its + # connection to the display :0.0; most likely the X server was + # shut down or you killed/destroyed the application. + # + # Windows and OS-X seem to be better behaved in this regard. + # They may not draw it, but they don't crash either! + + self.MaxFontSize = 1000 + self.ShiftFun = self.ShiftFunDict[Position] + + self.String = String + self.LayoutText() + self.CalcBoundingBox() + + self.SetPen(LineColor,LineStyle,LineWidth) + self.SetBrush(BackgroundColor, "Solid") + + + def WrapToWidth(self): + dc = wx.MemoryDC() + bitmap = wx.EmptyBitmap(1, 1) + dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work. + DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to. + ScaleFactor = float(self.Size) / DrawingSize + Width = (self.Width - 2*self.PadSize) / ScaleFactor #Width to wrap to + dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) + + NewStrings = [] + for s in self.Strings: + #beginning = True + text = s.split(" ") + text.reverse() + LineLength = 0 + NewText = text[-1] + del text[-1] + while text: + w = dc.GetTextExtent(' ' + text[-1])[0] + if LineLength + w <= Width: + NewText += ' ' + NewText += text[-1] + LineLength = dc.GetTextExtent(NewText)[0] + else: + NewStrings.append(NewText) + NewText = text[-1] + LineLength = dc.GetTextExtent(text[-1])[0] + del text[-1] + NewStrings.append(NewText) + self.Strings = NewStrings + + def ReWrap(self, Width): + self.Width = Width + self.LayoutText() + + def LayoutText(self): + """ + + Calculates the positions of the words of text. + + This isn't exact, as fonts don't scale exactly. + To help this, the position of each individual word + is stored separately, so that the general layout stays + the same in world coordinates, as the fonts scale. + + """ + self.Strings = self.String.split("\n") + if self.Width: + self.WrapToWidth() + + dc = wx.MemoryDC() + bitmap = wx.EmptyBitmap(1, 1) + dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work. + + DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to. + ScaleFactor = float(self.Size) / DrawingSize + + dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) + + TextHeight = dc.GetTextExtent("X")[1] + SpaceWidth = dc.GetTextExtent(" ")[0] + LineHeight = TextHeight * self.LineSpacing + + LineWidths = zeros((len(self.Strings),), Float) + y = 0 + Words = [] + AllLinePoints = [] + + for i, s in enumerate(self.Strings): + LineWidths[i] = 0 + LineWords = s.split(" ") + LinePoints = zeros((len(LineWords),2), Float) + for j, word in enumerate(LineWords): + if j > 0: + LineWidths[i] += SpaceWidth + Words.append(word) + LinePoints[j] = (LineWidths[i], y) + w = dc.GetTextExtent(word)[0] + LineWidths[i] += w + y -= LineHeight + AllLinePoints.append(LinePoints) + TextWidth = maximum.reduce(LineWidths) + self.Words = Words + + if self.Width is None: + BoxWidth = TextWidth * ScaleFactor + 2*self.PadSize + else: # use the defined Width + BoxWidth = self.Width + Points = zeros((0,2), Float) + + for i, LinePoints in enumerate(AllLinePoints): + ## Scale to World Coords. + LinePoints *= (ScaleFactor, ScaleFactor) + if self.Alignment == 'left': + LinePoints[:,0] += self.PadSize + elif self.Alignment == 'center': + LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor)/2.0 + elif self.Alignment == 'right': + LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor-self.PadSize) + Points = concatenate((Points, LinePoints)) + + BoxHeight = -(Points[-1,1] - (TextHeight * ScaleFactor)) + 2*self.PadSize + (x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1) + Points += (0, -self.PadSize) + self.Points = Points + self.BoxWidth = BoxWidth + self.BoxHeight = BoxHeight + self.CalcBoundingBox() + + def CalcBoundingBox(self): + + """ + + Calculates the Bounding Box + + """ + + w, h = self.BoxWidth, self.BoxHeight + x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world=1) + self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) + + def GetBoxRect(self): + wh = (self.BoxWidth, self.BoxHeight) + xy = (self.BoundingBox[0,0], self.BoundingBox[1,1]) + + return (xy, wh) + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + xy, wh = self.GetBoxRect() + + Points = self.Points + xy + Points = WorldToPixel(Points) + xy = WorldToPixel(xy) + wh = ScaleWorldToPixel(wh) * (1,-1) + + # compute the font size: + Size = abs( ScaleWorldToPixel( (self.Size, self.Size) )[1] ) # only need a y coordinate length + ## Check to see if the font size is large enough to blow up the X font server + ## If so, limit it. Would it be better just to not draw it? + ## note that this limit is dependent on how much memory you have, etc. + Size = min(Size, self.MaxFontSize) + + font = self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) + dc.SetFont(font) + dc.SetTextForeground(self.Color) + dc.SetBackgroundMode(wx.TRANSPARENT) + + # Draw The Box + if (self.LineStyle and self.LineColor) or self.BackgroundColor: + dc.SetBrush(self.Brush) + dc.SetPen(self.Pen) + dc.DrawRectanglePointSize(xy , wh) + + # Draw the Text + dc.DrawTextList(self.Words, Points) + + # Draw the hit box. + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectanglePointSize(xy, wh) + +class Bitmap(DrawObject, TextObjectMixin): + """ + This class creates a bitmap object, placed at the coordinates, + x,y. the "Position" argument is a two charactor string, indicating + where in relation to the coordinates the bitmap should be oriented. + + The first letter is: t, c, or b, for top, center and bottom The + second letter is: l, c, or r, for left, center and right The + position refers to the position relative to the text itself. It + defaults to "tl" (top left). + + The size is fixed, and does not scale with the drawing. + + """ + + def __init__(self,Bitmap,XY, + Position = 'tl', + InForeground = False): + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx._gdi.Bitmap: + self.Bitmap = Bitmap + elif type(Bitmap) == wx._core.Image: + self.Bitmap = wx.BitmapFromImage(Bitmap) + + # Note the BB is just the point, as the size in World coordinates is not fixed + self.BoundingBox = array((XY,XY),Float) + + self.XY = XY + + (self.Width, self.Height) = self.Bitmap.GetWidth(), self.Bitmap.GetHeight() + self.ShiftFun = self.ShiftFunDict[Position] + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + XY = WorldToPixel(self.XY) + XY = self.ShiftFun(XY[0], XY[1], self.Width, self.Height) + dc.DrawBitmapPoint(self.Bitmap, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectanglePointSize(XY, (self.Width, self.Height) ) + +class ScaledBitmap(DrawObject, TextObjectMixin): + """ + + This class creates a bitmap object, placed at the coordinates, XY, + of Height, H, in World coorsinates. The width is calculated from the + aspect ratio of the bitmap. + + the "Position" argument is a two charactor string, indicating + where in relation to the coordinates the bitmap should be oriented. + + The first letter is: t, c, or b, for top, center and bottom The + second letter is: l, c, or r, for left, center and right The + position refers to the position relative to the text itself. It + defaults to "tl" (top left). + + The size scales with the drawing + + """ + + def __init__(self, + Bitmap, + XY, + Height, + Position = 'tl', + InForeground = False): + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx._gdi.Bitmap: + self.Image = Bitmap.ConvertToImage() + elif type(Bitmap) == wx._core.Image: + self.Image = Bitmap + + self.XY = XY + self.Height = Height + (self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight() + self.Width = self.bmpWidth / self.bmpHeight * Height + self.ShiftFun = self.ShiftFunDict[Position] + self.CalcBoundingBox() + self.ScaledBitmap = None + self.ScaledHeight = None + + def CalcBoundingBox(self): + ## this isn't exact, as fonts don't scale exactly. + w,h = self.Width, self.Height + x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) + self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + XY = WorldToPixel(self.XY) + H = ScaleWorldToPixel(self.Height)[0] + W = H * (self.bmpWidth / self.bmpHeight) + if (self.ScaledBitmap is None) or (H <> self.ScaledHeight) : + self.ScaledHeight = H + Img = self.Image.Scale(W, H) + self.ScaledBitmap = wx.BitmapFromImage(Img) + + XY = self.ShiftFun(XY[0], XY[1], W, H) + dc.DrawBitmapPoint(self.ScaledBitmap, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectanglePointSize(XY, (W, H) ) + #--------------------------------------------------------------------------- class FloatCanvas(wx.Panel): + ## fixme: could this be a wx.Window? """ FloatCanvas.py @@ -1163,7 +1849,7 @@ class FloatCanvas(wx.Panel): global ScreenPPI ## A global variable to hold the Pixels per inch that wxWindows thinks is in use. dc = wx.ScreenDC() - ScreenPPI = dc.GetPPI()[0] # Assume square pixels + ScreenPPI = dc.GetPPI()[1] # Pixel height del dc self.HitColorGenerator = None @@ -1196,7 +1882,7 @@ class FloatCanvas(wx.Panel): ## create the Hit Test Dicts: self.HitDict = None - + self._HTdc = None self._DrawList = [] self._ForeDrawList = [] @@ -1220,28 +1906,75 @@ class FloatCanvas(wx.Panel): self.ObjectUnderMouse = None # called just to make sure everything is initialized - self.OnSize(None) - + # this is a bug on OS-X, maybe it's not required? + self.SizeTimer = wx.PyTimer(self.OnSizeTimer) # timer to give a delay when re-sizing so that bufferes aren't re-built too many times. + + self.InitializePanel() + self.MakeNewBuffers() + self.InHereNum = 0 + self.CreateCursors() + + def CreateCursors(self): + + ## create all the Cursors, so they don't need to be created each time. + ## + if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac + self.HandCursor = wx.CursorFromImage(Resources.getHand16Image()) + self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image()) + + img = Resources.getMagPlus16Image() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) + self.MagPlusCursor = wx.CursorFromImage(img) + + img = Resources.getMagMinus16Image() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) + self.MagMinusCursor = wx.CursorFromImage(img) + else: # use 24X24 cursors for GTK and Windows + self.HandCursor = wx.CursorFromImage(Resources.getHandImage()) + self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage()) + + img = Resources.getMagPlusImage() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) + self.MagPlusCursor = wx.CursorFromImage(img) + + img = Resources.getMagMinusImage() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) + self.MagMinusCursor = wx.CursorFromImage(img) + def SetProjectionFun(self,ProjectionFun): if ProjectionFun == 'FlatEarth': self.ProjectionFun = self.FlatEarthProjection - elif type(ProjectionFun) == types.FunctionType: + elif callable(ProjectionFun): self.ProjectionFun = ProjectionFun elif ProjectionFun is None: self.ProjectionFun = lambda x=None: array( (1,1), Float) else: - raise FloatCanvasException('Projectionfun must be either: "FlatEarth", None, or a function that takes the ViewPortCenter and returns a MapProjectionVector') + raise FloatCanvasError('Projectionfun must be either: "FlatEarth", None, or a callable object (function, for instance) that takes the ViewPortCenter and returns a MapProjectionVector') - def FlatEarthProjection(self,CenterPoint): + def FlatEarthProjection(self, CenterPoint): return array((cos(pi*CenterPoint[1]/180),1),Float) def SetMode(self,Mode): - if Mode in ["ZoomIn","ZoomOut","Move","Mouse",None]: + if Mode in ["ZoomIn","ZoomOut","Move","Mouse", None]: + if Mode == "Move": + self.SetCursor(self.HandCursor) + elif Mode == "ZoomIn": + self.SetCursor(self.MagPlusCursor) + elif Mode == "ZoomOut": + self.SetCursor(self.MagMinusCursor) + else: + self.SetCursor(wx.NullCursor) + self.GUIMode = Mode + else: - raise FloatCanvasException('"%s" is Not a valid Mode'%Mode) + raise FloatCanvasError('"%s" is Not a valid Mode'%Mode) def MakeHitDict(self): ##fixme: Should this just be None if nothing has been bound? @@ -1258,13 +1991,13 @@ class FloatCanvas(wx.Panel): EVT_FC_LEAVE_OBJECT: {}, } - def RaiseMouseEvent(self, Event, EventType): + def _RaiseMouseEvent(self, Event, EventType): """ This is called in various other places to raise a Mouse Event """ #print "in Raise Mouse Event", Event pt = self.PixelToWorld( Event.GetPosition() ) - evt = MouseEvent(EventType, Event, self.GetId(), pt) + evt = _MouseEvent(EventType, Event, self.GetId(), pt) self.GetEventHandler().ProcessEvent(evt) def HitTest(self, event, HitEvent): @@ -1281,6 +2014,7 @@ class FloatCanvas(wx.Panel): Object = self.HitDict[ HitEvent ][color] ## Add the hit coords to the Object Object.HitCoords = self.PixelToWorld( xy ) + Object.HitCoordsPixel = xy Object.CallBackFuncs[HitEvent](Object) return True return False @@ -1342,42 +2076,42 @@ class FloatCanvas(wx.Panel): if self.GUIMode == "Mouse": EventType = EVT_FC_LEFT_DCLICK if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) - + self._RaiseMouseEvent(event, EventType) def MiddleDownEvent(self,event): if self.GUIMode == "Mouse": EventType = EVT_FC_MIDDLE_DOWN if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) def MiddleUpEvent(self,event): if self.GUIMode == "Mouse": EventType = EVT_FC_MIDDLE_UP if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) def MiddleDoubleClickEvent(self,event): if self.GUIMode == "Mouse": EventType = EVT_FC_MIDDLE_DCLICK if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) def RightUpEvent(self,event): if self.GUIMode == "Mouse": EventType = EVT_FC_RIGHT_UP if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) def RightDoubleCLickEvent(self,event): if self.GUIMode == "Mouse": EventType = EVT_FC_RIGHT_DCLICK if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) def WheelEvent(self,event): - if self.GUIMode == "Mouse": - self.RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL) + ##if self.GUIMode == "Mouse": + ## Why not always raise this? + self._RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL) def LeftDownEvent(self,event): @@ -1390,17 +2124,18 @@ class FloatCanvas(wx.Panel): Center = self.PixelToWorld( event.GetPosition() ) self.Zoom(1/1.5,Center) elif self.GUIMode == "Move": + self.SetCursor(self.GrabHandCursor) self.StartMove = array( event.GetPosition() ) self.PrevMoveXY = (0,0) elif self.GUIMode == "Mouse": ## check for a hit if not self.HitTest(event, EVT_FC_LEFT_DOWN): - self.RaiseMouseEvent(event,EVT_FC_LEFT_DOWN) + self._RaiseMouseEvent(event,EVT_FC_LEFT_DOWN) else: pass def LeftUpEvent(self,event): - if self.HasCapture(): + if self.HasCapture(): self.ReleaseMouse() if self.GUIMode: if self.GUIMode == "ZoomIn": @@ -1423,16 +2158,17 @@ class FloatCanvas(wx.Panel): self.Zoom(1.5,Center) self.StartRBBox = None elif self.GUIMode == "Move": - if not self.StartMove is None: + self.SetCursor(self.HandCursor) + if self.StartMove is not None: StartMove = self.StartMove EndMove = array((event.GetX(),event.GetY())) if sum((StartMove-EndMove)**2) > 16: - self.Move(StartMove-EndMove,'Pixel') + self.MoveImage(StartMove-EndMove,'Pixel') self.StartMove = None elif self.GUIMode == "Mouse": EventType = EVT_FC_LEFT_UP if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) else: pass @@ -1466,6 +2202,7 @@ class FloatCanvas(wx.Panel): x1,y1 = self.PrevMoveXY x2,y2 = xy_tl w,h = self.PanelSize + ##fixme: This sure could be cleaner! if x2 > x1 and y2 > y1: xa = xb = x1 ya = yb = y1 @@ -1500,13 +2237,14 @@ class FloatCanvas(wx.Panel): yb = y2 + h wb = w hb = y1 - y2 - + dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(self.BackgroundBrush) dc.DrawRectangle(xa, ya, wa, ha) dc.DrawRectangle(xb, yb, wb, hb) self.PrevMoveXY = xy_tl - if self._ForegroundBuffer: + if self._ForeDrawList: + ##if self._ForegroundBuffer: dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl) else: dc.DrawBitmapPoint(self._Buffer,xy_tl) @@ -1515,10 +2253,10 @@ class FloatCanvas(wx.Panel): ## Only do something if there are mouse over events bound if self.HitDict and (self.HitDict[ EVT_FC_ENTER_OBJECT ] or self.HitDict[ EVT_FC_LEAVE_OBJECT ] ): if not self.MouseOverTest(event): - self.RaiseMouseEvent(event,EVT_FC_MOTION) + self._RaiseMouseEvent(event,EVT_FC_MOTION) else: pass - self.RaiseMouseEvent(event,EVT_FC_MOTION) + self._RaiseMouseEvent(event,EVT_FC_MOTION) else: pass @@ -1533,7 +2271,7 @@ class FloatCanvas(wx.Panel): elif self.GUIMode == "Mouse": EventType = EVT_FC_RIGHT_DOWN if not self.HitTest(event, EventType): - self.RaiseMouseEvent(event, EventType) + self._RaiseMouseEvent(event, EventType) else: pass @@ -1573,15 +2311,25 @@ class FloatCanvas(wx.Panel): else: self._ForegroundHTdc = None - def OnSize(self,event): - self.PanelSize = array(self.GetClientSizeTuple(),Int32) + def OnSize(self, event=None): + self.InitializePanel() + self.SizeTimer.Start(50, oneShot=True) + + def OnSizeTimer(self, event=None): + self.MakeNewBuffers() + self.Draw() + + def InitializePanel(self): + self.PanelSize = self.GetClientSizeTuple() + if self.PanelSize == (0,0): + ## OS-X sometimes gives a Size event when the panel is size (0,0) + self.PanelSize = (2,2) + self.PanelSize = array(self.PanelSize, Int32) self.HalfPanelSize = self.PanelSize / 2 # lrk: added for speed in WorldToPixel if self.PanelSize[0] == 0 or self.PanelSize[1] == 0: self.AspectRatio = 1.0 else: self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1] - self.MakeNewBuffers() - self.Draw() def OnPaint(self, event): dc = wx.PaintDC(self) @@ -1605,8 +2353,7 @@ class FloatCanvas(wx.Panel): animation, for instance. """ - #print "In Draw" - if self.PanelSize < (1,1): + if sometrue(self.PanelSize <= 2 ): # it's possible for this to get called before being properly initialized. return if self.Debug: start = clock() ScreenDC = wx.ClientDC(self) @@ -1690,7 +2437,7 @@ class FloatCanvas(wx.Panel): ## else: ## return False - def Move(self,shift,CoordType): + def MoveImage(self,shift,CoordType): """ move the image in the window. @@ -1709,7 +2456,8 @@ class FloatCanvas(wx.Panel): """ - shift = array(shift,Float) + shift = asarray(shift,Float) + #print "shifting by:", shift if CoordType == 'Panel':# convert from panel coordinates shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector elif CoordType == 'Pixel': # convert from pixel coordinates @@ -1717,8 +2465,8 @@ class FloatCanvas(wx.Panel): elif CoordType == 'World': # No conversion pass else: - raise FloatCanvasException('CoordType must be either "Panel", "Pixel", or "World"') - + raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"') + self.ViewPortCenter = self.ViewPortCenter + shift self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector @@ -1797,6 +2545,9 @@ class FloatCanvas(wx.Panel): ##fixme: Using the list.remove method is kind of slow if Object.InForeground: self._ForeDrawList.remove(Object) + if not self._ForeDrawList: + self._ForegroundBuffer = None + self._ForegroundHTdc = None else: self._DrawList.remove(Object) self._BackgroundDirty = True @@ -1847,7 +2598,7 @@ class FloatCanvas(wx.Panel): self.BoundingBox = None self.ViewPortCenter= array( (0,0), Float) self.TransformVector = array( (1,-1), Float) - self.MapProjectionVector = array( (1,1), Float) + self.MapProjectionVector = array( (1,1), Float) self.Scale = 1 self.BoundingBoxDirty = False @@ -1868,7 +2619,9 @@ class FloatCanvas(wx.Panel): a 2-tuple, or sequence of 2-tuples. """ #Note: this can be called by users code for various reasons, so asarray is needed. - return (((asarray(Coordinates,Float) - self.ViewPortCenter)*self.TransformVector)+(self.HalfPanelSize)).astype('i') + return (((asarray(Coordinates,Float) - + self.ViewPortCenter)*self.TransformVector)+ + (self.HalfPanelSize)).astype('i') def ScaleWorldToPixel(self,Lengths): """ @@ -1918,33 +2671,32 @@ class FloatCanvas(wx.Panel): Blit = ScreenDC.Blit # for speed NumBetweenBlits = self.NumBetweenBlits # for speed for i, Object in enumerate(self._ShouldRedraw(DrawList, ViewPortBB)): - Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) - if i % NumBetweenBlits == 0: - Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0) + if Object.Visible: + Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) + if (i+1) % NumBetweenBlits == 0: + Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0) dc.EndDrawing() -## ## This is a way to automatically add a AddObject method for each -## ## object type This code has been replaced by Leo's code above, so -## ## that it happens at module init, rather than as needed. The -## ## primary advantage of this is that dir(FloatCanvas) will have -## ## them, and docstrings are preserved. Probably more useful -## ## exceptions if there is a problem, as well. -## def __getattr__(self, name): -## if name[:3] == "Add": -## func=globals()[name[3:]] -## def AddFun(*args, **kwargs): -## Object = func(*args, **kwargs) -## self.AddObject(Object) -## return Object -## ## add it to FloatCanvas' dict for future calls. -## self.__dict__[name] = AddFun -## return AddFun -## else: -## raise AttributeError("FloatCanvas has no attribute '%s'"%name) + def SaveAsImage(self, filename, ImageType=wx.BITMAP_TYPE_PNG): + """ + + Saves the current image as an image file. The default is in the + PNG format. Other formats can be spcified using the wx flags: + + wx.BITMAP_TYPE_BMP + wx.BITMAP_TYPE_XBM + wx.BITMAP_TYPE_XPM + etc. (see the wx docs for the complete list) + + """ + + self._Buffer.SaveFile(filename, ImageType) + def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__ classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon", - "Line", "Text", "PointSet"] + "Line", "Text", "PointSet","Point", "Arrow","ScaledTextBox", + "SquarePoint","Bitmap", "ScaledBitmap"] for classname in classnames: klass = globals()[classname] def getaddshapemethod(klass=klass): @@ -1956,8 +2708,8 @@ def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init_ addshapemethod = getaddshapemethod() methodname = "Add" + classname setattr(FloatCanvas, methodname, addshapemethod) - docstring = " Creates %s and adds its reference to the canvas.\n" % classname - docstring += " Argument protocol same as %s class" % classname + docstring = "Creates %s and adds its reference to the canvas.\n" % classname + docstring += "Argument protocol same as %s class" % classname if klass.__doc__: docstring += ", whose docstring is:\n%s" % klass.__doc__ FloatCanvas.__dict__[methodname].__doc__ = docstring