X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/ec873c943d71f0d5f13e3398557071448cda6c23..a4027e74873007e3430af3bd77019bcab76f6c04:/wxPython/wx/lib/floatcanvas/FloatCanvas.py diff --git a/wxPython/wx/lib/floatcanvas/FloatCanvas.py b/wxPython/wx/lib/floatcanvas/FloatCanvas.py deleted file mode 100644 index 60d773a1d0..0000000000 --- a/wxPython/wx/lib/floatcanvas/FloatCanvas.py +++ /dev/null @@ -1,2920 +0,0 @@ - -from __future__ import division - -try: - import numpy as N -except ImportError: - raise ImportError("I could not import numpy") - -from time import clock -import wx - -from Utilities import BBox - - -## 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 initialized yet. -global FontScale - -## Custom Exceptions: - -class FloatCanvasError(Exception): - pass - -## Create all the mouse events -EVT_FC_ENTER_WINDOW = wx.NewEventType() -EVT_FC_LEAVE_WINDOW = wx.NewEventType() -EVT_FC_LEFT_DOWN = wx.NewEventType() -EVT_FC_LEFT_UP = wx.NewEventType() -EVT_FC_LEFT_DCLICK = wx.NewEventType() -EVT_FC_MIDDLE_DOWN = wx.NewEventType() -EVT_FC_MIDDLE_UP = wx.NewEventType() -EVT_FC_MIDDLE_DCLICK = wx.NewEventType() -EVT_FC_RIGHT_DOWN = wx.NewEventType() -EVT_FC_RIGHT_UP = wx.NewEventType() -EVT_FC_RIGHT_DCLICK = wx.NewEventType() -EVT_FC_MOTION = wx.NewEventType() -EVT_FC_MOUSEWHEEL = wx.NewEventType() -## these two are for the hit-test stuff, I never make them real Events -## fixme: could I use the PyEventBinder for the Object events too? -EVT_FC_ENTER_OBJECT = wx.NewEventType() -EVT_FC_LEAVE_OBJECT = wx.NewEventType() - -##Create all mouse event binding objects -EVT_LEFT_DOWN = wx.PyEventBinder(EVT_FC_LEFT_DOWN) -EVT_LEFT_UP = wx.PyEventBinder(EVT_FC_LEFT_UP) -EVT_LEFT_DCLICK = wx.PyEventBinder(EVT_FC_LEFT_DCLICK) -EVT_MIDDLE_DOWN = wx.PyEventBinder(EVT_FC_MIDDLE_DOWN) -EVT_MIDDLE_UP = wx.PyEventBinder(EVT_FC_MIDDLE_UP) -EVT_MIDDLE_DCLICK = wx.PyEventBinder(EVT_FC_MIDDLE_DCLICK) -EVT_RIGHT_DOWN = wx.PyEventBinder(EVT_FC_RIGHT_DOWN) -EVT_RIGHT_UP = wx.PyEventBinder(EVT_FC_RIGHT_UP) -EVT_RIGHT_DCLICK = wx.PyEventBinder(EVT_FC_RIGHT_DCLICK) -EVT_MOTION = wx.PyEventBinder(EVT_FC_MOTION) -EVT_ENTER_WINDOW = wx.PyEventBinder(EVT_FC_ENTER_WINDOW) -EVT_LEAVE_WINDOW = wx.PyEventBinder(EVT_FC_LEAVE_WINDOW) -EVT_MOUSEWHEEL = wx.PyEventBinder(EVT_FC_MOUSEWHEEL) - -class _MouseEvent(wx.PyCommandEvent): - - """! - - This event class takes a regular wxWindows mouse event as a parameter, - and wraps it so that there is access to all the original methods. This - is similar to subclassing, but you can't subclass a wxWindows event - - The goal is to be able to it just like a regular mouse event. - - It adds the method: - - GetCoords() , which returns and (x,y) tuple in world coordinates. - - Another difference is that it is a CommandEvent, which propagates up - the window hierarchy until it is handled. - - """ - - def __init__(self, EventType, NativeEvent, WinID, Coords = None): - wx.PyCommandEvent.__init__(self) - - self.SetEventType( EventType ) - self._NativeEvent = NativeEvent - self.Coords = Coords - - def GetCoords(self): - return self.Coords - - def __getattr__(self, name): - return getattr(self._NativeEvent, name) - -def _cycleidxs(indexcount, maxvalue, step): - - """! - Utility function used by _colorGenerator - - """ - - if indexcount == 0: - yield () - else: - for idx in xrange(0, maxvalue, step): - for tail in _cycleidxs(indexcount - 1, maxvalue, step): - yield (idx, ) + tail - -def _colorGenerator(): - - """! - - Generates a series of unique colors used to do hit-tests with the Hit - Test bitmap - """ - - depth = wx.GetDisplayDepth() -## ##there have been problems with 16 bbp displays, to I'm disabling this for now. -## if depth == 16: -## print "Warning: There have been problems with hit-testing on 16bbp displays" -## step = 8 - if depth >= 24: - step = 1 - else: - msg= ["ColorGenerator does not work with depth = %s" % depth] - msg.append("It is required for hit testing -- binding events to mouse") - msg.append("actions on objects on the Canvas.") - msg.append("Please set your display to 24bit") - msg.append("Alternatively, the code could be adapted to 16 bit if that's required") - raise FloatCanvasError(msg) - return _cycleidxs(indexcount=3, maxvalue=256, step=step) - -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. - - \note This class contain a series of static dictionaries: - - * BrushList - * PenList - * FillStyleList - * LineStyleList - - Is this still necessary? - - """ - - def __init__(self, InForeground = False, IsVisible = True): - """! \param InForeground (bool) - \param IsVisible (Bool) - """ - self.InForeground = InForeground - - self._Canvas = None - - self.HitColor = None - self.CallBackFuncs = {} - - ## these are the defaults - self.HitAble = False - self.HitLine = True - 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 - # does that on it's own. Send me a note if you know! - - BrushList = { - ( None,"Transparent") : wx.TRANSPARENT_BRUSH, - ("Blue","Solid") : wx.BLUE_BRUSH, - ("Green","Solid") : wx.GREEN_BRUSH, - ("White","Solid") : wx.WHITE_BRUSH, - ("Black","Solid") : wx.BLACK_BRUSH, - ("Grey","Solid") : wx.GREY_BRUSH, - ("MediumGrey","Solid") : wx.MEDIUM_GREY_BRUSH, - ("LightGrey","Solid") : wx.LIGHT_GREY_BRUSH, - ("Cyan","Solid") : wx.CYAN_BRUSH, - ("Red","Solid") : wx.RED_BRUSH - } - PenList = { - (None,"Transparent",1) : wx.TRANSPARENT_PEN, - ("Green","Solid",1) : wx.GREEN_PEN, - ("White","Solid",1) : wx.WHITE_PEN, - ("Black","Solid",1) : wx.BLACK_PEN, - ("Grey","Solid",1) : wx.GREY_PEN, - ("MediumGrey","Solid",1) : wx.MEDIUM_GREY_PEN, - ("LightGrey","Solid",1) : wx.LIGHT_GREY_PEN, - ("Cyan","Solid",1) : wx.CYAN_PEN, - ("Red","Solid",1) : wx.RED_PEN - } - - FillStyleList = { - "Transparent" : wx.TRANSPARENT, - "Solid" : wx.SOLID, - "BiDiagonalHatch": wx.BDIAGONAL_HATCH, - "CrossDiagHatch" : wx.CROSSDIAG_HATCH, - "FDiagonal_Hatch": wx.FDIAGONAL_HATCH, - "CrossHatch" : wx.CROSS_HATCH, - "HorizontalHatch": wx.HORIZONTAL_HATCH, - "VerticalHatch" : wx.VERTICAL_HATCH - } - - LineStyleList = { - "Solid" : wx.SOLID, - "Transparent": wx.TRANSPARENT, - "Dot" : wx.DOT, - "LongDash" : wx.LONG_DASH, - "ShortDash" : wx.SHORT_DASH, - "DotDash" : wx.DOT_DASH, - } - -# def BBFromPoints(self, Points): -# """! -# Calculates a Bounding box from a set of points (NX2 array of coordinates) -# \param Points (array?) -# """ -# -# ## fixme: this could be done with array.min() and vstack() in numpy. -# ## This could use the Utilities.BBox module now. -# #return N.array( (N.minimum.reduce(Points), -# # N.maximum.reduce(Points) ), -# # ) -# return BBox.fromPoints(Points) - - def Bind(self, Event, CallBackFun): - self.CallBackFuncs[Event] = CallBackFun - self.HitAble = True - self._Canvas.UseHitTest = True - if self.InForeground and self._Canvas._ForegroundHTBitmap is None: - self._Canvas.MakeNewForegroundHTBitmap() - elif self._Canvas._HTBitmap is None: - self._Canvas.MakeNewHTBitmap() - if not self.HitColor: - if not self._Canvas.HitColorGenerator: - 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) - self.SetHitBrush(self.HitColor) - # put the object in the hit dict, indexed by it's color - if not self._Canvas.HitDict: - 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: - for List in self._Canvas.HitDict.itervalues(): - try: - List.remove(self) - except ValueError: - 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] ) ) - - def SetPen(self,LineColor,LineStyle,LineWidth): - if (LineColor is None) or (LineStyle is None): - self.Pen = wx.TRANSPARENT_PEN - self.LineStyle = 'Transparent' - else: - self.Pen = self.PenList.setdefault( (LineColor,LineStyle,LineWidth), wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) ) - - def SetHitBrush(self,HitColor): - if not self.HitFill: - self.HitBrush = wx.TRANSPARENT_BRUSH - else: - self.HitBrush = self.BrushList.setdefault( (HitColor,"solid"), wx.Brush(HitColor,self.FillStyleList["Solid"] ) ) - - def SetHitPen(self,HitColor,LineWidth): - if not self.HitLine: - self.HitPen = wx.TRANSPARENT_PEN - else: - self.HitPen = self.PenList.setdefault( (HitColor, "solid", self.HitLineWidth), wx.Pen(HitColor, self.HitLineWidth, self.LineStyleList["Solid"]) ) - - ## Just to make sure that they will always be there - ## the appropriate ones should be overridden in the subclasses - def SetColor(self, Color): - pass - def SetLineColor(self, LineColor): - pass - def SetLineStyle(self, LineStyle): - pass - def SetLineWidth(self, LineWidth): - pass - def SetFillColor(self, FillColor): - pass - def SetFillStyle(self, FillStyle): - pass - - def PutInBackground(self): - if self._Canvas and self.InForeground: - self._Canvas._ForeDrawList.remove(self) - self._Canvas._DrawList.append(self) - self._Canvas._BackgroundDirty = True - self.InForeground = False - - def PutInForeground(self): - if self._Canvas and (not self.InForeground): - self._Canvas._ForeDrawList.append(self) - self._Canvas._DrawList.remove(self) - self._Canvas._BackgroundDirty = True - self.InForeground = True - - def Hide(self): - """! \brief Make an object hidden. - """ - self.Visible = False - - def Show(self): - """! \brief Make an object visible on the canvas. - """ - self.Visible = True - -class Group(DrawObject): - """ - A group of other FloatCanvas Objects - - Not all DrawObject methods may apply here. In particular, you can't Bind events to a group. - - Note that if an object is in more than one group, it will get drawn more than once. - - """ - - def __init__(self, ObjectList=[], InForeground = False, IsVisible = True): - self.ObjectList = list(ObjectList) - DrawObject.__init__(self, InForeground, IsVisible) - self.CalcBoundingBox() - - def AddObject(self, obj): - self.ObjectList.append(obj) - self.BoundingBox.Merge(obj.BoundingBox) - - def AddObjects(self, Objects): - for o in Objects: - self.AddObject(o) - - def CalcBoundingBox(self): - if self.ObjectList: - BB = BBox.asBBox(self.ObjectList[0].BoundingBox) - for obj in self.ObjectList[1:]: - BB.Merge(obj.BoundingBox) - else: - BB = None - self.BoundingBox = BB - - def SetColor(self, Color): - for o in self.ObjectList: - o.SetColor(Color) - def SetLineColor(self, Color): - for o in self.ObjectList: - o.SetLineColor(Color) - def SetLineStyle(self, LineStyle): - for o in self.ObjectList: - o.SetLineStyle(LineStyle) - def SetLineWidth(self, LineWidth): - for o in self.ObjectList: - o.SetLineWidth(LineWidth) - def SetFillColor(self, Color): - for o in self.ObjectList: - o.SetFillColor(Color) - def SetFillStyle(self, FillStyle): - for o in self.ObjectList: - o.SetFillStyle(FillStyle) - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None): - for obj in self.ObjectList: - obj._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) - - -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) - SetColor = SetLineColor# so that it will do somethign reasonable - - 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) - - def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): - dc.SetPen(self.Pen) - dc.SetBrush(self.Brush) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - return ( WorldToPixel(self.XY), - ScaleWorldToPixel(self.WH) ) - -class XYObjectMixin: - """ - - This is a mixin class that provides some methods suitable for use - with objects that have a single (x,y) coordinate pair. - - """ - - def Move(self, Delta ): - """ - - Move(Delta): moves the object by delta, where delta is a - (dx,dy) pair. Ideally a Numpy array of shape (2,) - - """ - - Delta = N.asarray(Delta, N.float) - self.XY += Delta - self.BoundingBox += Delta - - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - def CalcBoundingBox(self): - ## This may get overwritten in some subclasses - self.BoundingBox = N.array( (self.XY, self.XY), N.float ) - self.BoundingBox = BBox.asBBox((self.XY, self.XY)) - - def SetPoint(self, xy): - xy = N.array(xy, N.float) - xy.shape = (2,) - - self.XY = xy - self.CalcBoundingBox() - - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - -class PointsObjectMixin: - """ - - This is a mixin class that provides some methods suitable for use - with objects that have a set of (x,y) coordinate pairs. - - """ - - def Move(self, Delta): - """ - Move(Delta): moves the object by delta, where delta is an (dx, - dy) pair. Ideally a Numpy array of shape (2,) - """ - - Delta = N.asarray(Delta, N.float) - Delta.shape = (2,) - self.Points += Delta - self.BoundingBox += Delta - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - def CalcBoundingBox(self): - self.BoundingBox = BBox.fromPoints(self.Points) - 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 = N.array(Points, N.float) - self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point - else: - self.Points = N.asarray(Points, N.float) - self.CalcBoundingBox() - - -class Polygon(PointsObjectMixin, LineAndFillMixin, DrawObject): - - """ - - The Polygon 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. - - The other parameters specify various properties of the Polygon, and - should be self explanatory. - - """ - def __init__(self, - Points, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - DrawObject.__init__(self, InForeground) - self.Points = N.array(Points ,N.float) # this DOES need to make a copy - self.CalcBoundingBox() - - self.LineColor = LineColor - self.LineStyle = LineStyle - self.LineWidth = LineWidth - self.FillColor = FillColor - self.FillStyle = FillStyle - - self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - - self.SetPen(LineColor,LineStyle,LineWidth) - self.SetBrush(FillColor,FillStyle) - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None): - Points = WorldToPixel(self.Points)#.tolist() - dc.SetPen(self.Pen) - dc.SetBrush(self.Brush) - dc.DrawPolygon(Points) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawPolygon(Points) - -class Line(PointsObjectMixin, LineOnlyMixin, DrawObject,): - """ - - 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, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - InForeground = False): - DrawObject.__init__(self, InForeground) - - - self.Points = N.array(Points,N.float) - self.CalcBoundingBox() - - self.LineColor = LineColor - self.LineStyle = LineStyle - self.LineWidth = LineWidth - - self.SetPen(LineColor,LineStyle,LineWidth) - - self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - Points = WorldToPixel(self.Points) - dc.SetPen(self.Pen) - dc.DrawLines(Points) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.DrawLines(Points) - -class Spline(Line): - def __init__(self, *args, **kwargs): - Line.__init__(self, *args, **kwargs) - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - Points = WorldToPixel(self.Points) - dc.SetPen(self.Pen) - dc.DrawSpline(Points) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.DrawSpline(Points) - - -class Arrow(XYObjectMixin, LineOnlyMixin, DrawObject): - """ - - 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, # size of arrowhead in pixels - ArrowHeadAngle = 45, # angle of arrow head in degrees - 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 = N.array(XY, N.float) - self.XY.shape = (2,) # Make sure it is a length 2 vector - 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 CalcArrowPoints(self): -## L = self.Length -## S = self.ArrowHeadSize -## phi = self.ArrowHeadAngle * N.pi / 360 -## theta = (self.Direction-90.0) * N.pi / 180 -## ArrowPoints = N.array( ( (0, L, L - S*N.cos(phi),L, L - S*N.cos(phi) ), -## (0, 0, S*N.sin(phi), 0, -S*N.sin(phi) ) ), -## N.float ) -## RotationMatrix = N.array( ( ( N.cos(theta), -N.sin(theta) ), -## ( N.sin(theta), N.cos(theta) ) ), -## N.float -## ) -## ArrowPoints = N.matrixmultiply(RotationMatrix, ArrowPoints) -## self.ArrowPoints = N.transpose(ArrowPoints) - - def CalcArrowPoints(self): - L = self.Length - S = self.ArrowHeadSize - phi = self.ArrowHeadAngle * N.pi / 360 - theta = (270 - self.Direction) * N.pi / 180 - AP = N.array( ( (0,0), - (0,0), - (N.cos(theta - phi), -N.sin(theta - phi) ), - (0,0), - (N.cos(theta + phi), -N.sin(theta + phi) ), - ), N.float ) - AP *= S - shift = (-L*N.cos(theta), L*N.sin(theta) ) - AP[1:,:] += shift - self.ArrowPoints = AP - - 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 ArrowLine(PointsObjectMixin, LineOnlyMixin, DrawObject): - """ - - ArrowLine(Points, # coords of points - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - ArrowHeadSize = 4, # in pixels - ArrowHeadAngle = 45, - InForeground = False): - - It will draw a set of arrows from point to point. - - It takes a list of 2-tuples, or a NX2 NumPy Float array - of point coordinates. - - - """ - - def __init__(self, - Points, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, # pixels - ArrowHeadSize = 8, # pixels - ArrowHeadAngle = 30, # degrees - InForeground = False): - - DrawObject.__init__(self, InForeground) - - self.Points = N.asarray(Points,N.float) - self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point - 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) - - self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - - def CalcArrowPoints(self): - S = self.ArrowHeadSize - phi = self.ArrowHeadAngle * N.pi / 360 - Points = self.Points - n = Points.shape[0] - self.ArrowPoints = N.zeros((n-1, 3, 2), N.float) - for i in xrange(n-1): - dx, dy = self.Points[i] - self.Points[i+1] - theta = N.arctan2(dy, dx) - AP = N.array( ( - (N.cos(theta - phi), -N.sin(theta-phi)), - (0,0), - (N.cos(theta + phi), -N.sin(theta + phi)) - ), - N.float ) - self.ArrowPoints[i,:,:] = AP - self.ArrowPoints *= S - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - Points = WorldToPixel(self.Points) - ArrowPoints = Points[1:,N.newaxis,:] + self.ArrowPoints - dc.SetPen(self.Pen) - dc.DrawLines(Points) - for arrow in ArrowPoints: - dc.DrawLines(arrow) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.DrawLines(Points) - for arrow in ArrowPoints: - HTdc.DrawLines(arrow) - - -class PointSet(PointsObjectMixin, ColorOnlyMixin, DrawObject): - """ - - 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. - - 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. - - """ - def __init__(self, Points, Color = "Black", Diameter = 1, InForeground = False): - DrawObject.__init__(self,InForeground) - - self.Points = N.array(Points,N.float) - self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point - self.CalcBoundingBox() - self.Diameter = Diameter - - self.HitLineWidth = min(self.MinHitLineWidth, Diameter) - self.SetColor(Color) - - 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 N.argmin(N.hypot(d[:,0],d[:,1])) - - - def DrawD2(self, dc, Points): - # A Little optimization for a diameter2 - point - dc.DrawPointList(Points) - dc.DrawPointList(Points + (1,0)) - dc.DrawPointList(Points + (0,1)) - dc.DrawPointList(Points + (1,1)) - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - dc.SetPen(self.Pen) - Points = WorldToPixel(self.Points) - if self.Diameter <= 1: - dc.DrawPointList(Points) - elif self.Diameter <= 2: - self.DrawD2(dc, Points) - else: - dc.SetBrush(self.Brush) - radius = int(round(self.Diameter/2)) - ##fixme: I really should add a DrawCircleList to wxPython - if len(Points) > 100: - xy = Points - xywh = N.concatenate((xy-radius, N.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 = N.concatenate((xy-radius, N.ones(xy.shape) * self.Diameter ), 1 ) - HTdc.DrawEllipseList(xywh) - else: - for xy in Points: - HTdc.DrawCircle(xy[0],xy[1], radius) - -class Point(XYObjectMixin, ColorOnlyMixin, DrawObject): - """ - - 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 = N.array(XY, N.float) - self.XY.shape = (2,) # Make sure it is a length 2 vector - 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) - HTdc.DrawCircle(xy[0],xy[1], radius) - -class SquarePoint(XYObjectMixin, ColorOnlyMixin, DrawObject): - """ - - The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point - coordinates. It produces a square dot, centered on Point - - The Size is in screen points, not world coordinates, so the - Bounding box is just the point, and doesn't include the Size. - - The HitLineWidth is used as diameter for the - Hit Test. - - """ - def __init__(self, Point, Color = "Black", Size = 4, InForeground = False): - DrawObject.__init__(self, InForeground) - - self.XY = N.array(Point, N.float) - self.XY.shape = (2,) # Make sure it is a length 2 vector - self.CalcBoundingBox() - self.SetColor(Color) - self.Size = Size - - self.HitLineWidth = self.MinHitLineWidth - - 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(XYObjectMixin, LineAndFillMixin, DrawObject): - def __init__(self, XY, WH, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - - DrawObject.__init__(self,InForeground) - - self.SetShape(XY, WH) - self.LineColor = LineColor - self.LineStyle = LineStyle - self.LineWidth = LineWidth - self.FillColor = FillColor - self.FillStyle = FillStyle - - self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - - self.SetPen(LineColor,LineStyle,LineWidth) - self.SetBrush(FillColor,FillStyle) - - def SetShape(self, XY, WH): - self.XY = N.array( XY, N.float) - self.XY.shape = (2,) - self.WH = N.array( WH, N.float) - self.WH.shape = (2,) - self.CalcBoundingBox() - - - def CalcBoundingBox(self): - # you need this in case Width or Height are negative - corners = N.array((self.XY, (self.XY + self.WH) ), N.float) - self.BoundingBox = BBox.fromPoints(corners) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - -class Rectangle(RectEllipse): - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - ( XY, WH ) = self.SetUpDraw(dc, - WorldToPixel, - ScaleWorldToPixel, - HTdc) - dc.DrawRectanglePointSize(XY, WH) - if HTdc and self.HitAble: - HTdc.DrawRectanglePointSize(XY, WH) - - - -class Ellipse(RectEllipse): - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - ( XY, WH ) = self.SetUpDraw(dc, - WorldToPixel, - ScaleWorldToPixel, - HTdc) - dc.DrawEllipsePointSize(XY, WH) - if HTdc and self.HitAble: - HTdc.DrawEllipsePointSize(XY, WH) - -class Circle(Ellipse): - ## fixme: this should probably be use the DC.DrawCircle! - def __init__(self, XY, Diameter, **kwargs): - self.Center = N.array(XY, N.float) - Diameter = float(Diameter) - RectEllipse.__init__(self , - 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(XYObjectMixin): - """ - - A mix in class that holds attributes and methods that are needed by - the Text objects - - """ - - ## I'm caching fonts, because on GTK, getting a new font can take a - ## while. However, it gets cleared after every full draw as hanging - ## on to a bunch of large fonts takes a massive amount of memory. - - FontList = {} - - LayoutFontSize = 16 # font size used for calculating layout - - def SetFont(self, Size, Family, Style, Weight, Underlined, FaceName): - self.Font = self.FontList.setdefault( (Size, - Family, - Style, - Weight, - Underlined, - FaceName), - #wx.FontFromPixelSize((0.45*Size,Size), # this seemed to give a decent height/width ratio on Windows - wx.Font(Size, - Family, - Style, - Weight, - Underlined, - FaceName) ) - - 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 - ## 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(TextObjectMixin, DrawObject, ): - """ - This class creates a text object, placed at the coordinates, - x,y. the "Position" argument is a two charactor string, indicating - where in relation to the coordinates the string 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 pixels, or in points for printing - (if it ever gets implimented). Those will be the same, If you assume - 72 PPI. - - 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. - Underlined: - 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 is fixed, and does not scale with the drawing. - - The hit-test is done on the entire text extent - - """ - - def __init__(self,String, xy, - Size = 14, - Color = "Black", - BackgroundColor = None, - Family = wx.MODERN, - Style = wx.NORMAL, - Weight = wx.NORMAL, - Underlined = False, - Position = 'tl', - InForeground = False, - Font = None): - - DrawObject.__init__(self,InForeground) - - self.String = String - # Input size in in Pixels, compute points size from FontScaleinfo. - # fixme: for printing, we'll have to do something a little different - self.Size = Size * FontScale - - self.Color = Color - self.BackgroundColor = BackgroundColor - - if not Font: - FaceName = '' - else: - FaceName = Font.GetFaceName() - Family = Font.GetFamily() - Size = Font.GetPointSize() - Style = Font.GetStyle() - Underlined = Font.GetUnderlined() - Weight = Font.GetWeight() - self.SetFont(Size, Family, Style, Weight, Underlined, FaceName) - - self.BoundingBox = BBox.asBBox((xy, xy)) - - self.XY = N.asarray(xy) - self.XY.shape = (2,) - - (self.TextWidth, self.TextHeight) = (None, None) - self.ShiftFun = self.ShiftFunDict[Position] - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - XY = WorldToPixel(self.XY) - dc.SetFont(self.Font) - dc.SetTextForeground(self.Color) - if self.BackgroundColor: - dc.SetBackgroundMode(wx.SOLID) - dc.SetTextBackground(self.BackgroundColor) - else: - dc.SetBackgroundMode(wx.TRANSPARENT) - if self.TextWidth is None or self.TextHeight is None: - (self.TextWidth, self.TextHeight) = dc.GetTextExtent(self.String) - XY = self.ShiftFun(XY[0], XY[1], self.TextWidth, self.TextHeight) - dc.DrawTextPoint(self.String, XY) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectanglePointSize(XY, (self.TextWidth, self.TextHeight) ) - -class ScaledText(TextObjectMixin, DrawObject, ): - """ - 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 - charactor string, indicating where in relation to the coordinates - the string 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.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. - Underlined: - 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, the do end up a little different, so you don't - get exactly the same picture as you scale up and doen, but it's - pretty darn close. - - On wxGTK1 on my Linux system, at least, using a font of over about - 3000 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 - 3000 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 text extent. This could be made - optional, but I haven't gotten around to it. - - """ - - def __init__(self, - String, - XY, - Size, - Color = "Black", - BackgroundColor = None, - Family = wx.MODERN, - Style = wx.NORMAL, - Weight = wx.NORMAL, - Underlined = False, - Position = 'tl', - Font = None, - InForeground = False): - - DrawObject.__init__(self,InForeground) - - self.String = String - self.XY = N.array( XY, N.float) - self.XY.shape = (2,) - self.Size = Size - self.Color = Color - self.BackgroundColor = BackgroundColor - self.Family = Family - self.Style = Style - self.Weight = Weight - self.Underlined = Underlined - 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.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(self.Size) / DrawingSize - self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) - dc.SetFont(self.Font) - (w,h) = dc.GetTextExtent(self.String) - w = w * ScaleFactor - h = h * ScaleFactor - x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) - self.BoundingBox = BBox.asBBox(((x, y-h ),(x + w, y))) - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - (X,Y) = WorldToPixel( (self.XY) ) - - # 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) - self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) - dc.SetFont(self.Font) - dc.SetTextForeground(self.Color) - if self.BackgroundColor: - dc.SetBackgroundMode(wx.SOLID) - dc.SetTextBackground(self.BackgroundColor) - else: - dc.SetBackgroundMode(wx.TRANSPARENT) - (w,h) = dc.GetTextExtent(self.String) - # compute the shift, and adjust the coordinates, if neccesary - # This had to be put in here, because it changes with Zoom, as - # fonts don't scale exactly. - xy = self.ShiftFun(X, Y, w, h) - - dc.DrawTextPoint(self.String, xy) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectanglePointSize(xy, (w, h) ) - -class ScaledTextBox(TextObjectMixin, DrawObject): - """ - 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. - Underlined: - 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, - Underlined = False, - Position = 'tl', - Alignment = "left", - Font = None, - LineSpacing = 1.0, - InForeground = False): - - DrawObject.__init__(self,InForeground) - - self.XY = N.array(Point, N.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.Underlined = Underlined - 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 - self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) - dc.SetFont(self.Font) - 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 - - self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) - dc.SetFont(self.Font) - TextHeight = dc.GetTextExtent("X")[1] - SpaceWidth = dc.GetTextExtent(" ")[0] - LineHeight = TextHeight * self.LineSpacing - - LineWidths = N.zeros((len(self.Strings),), N.float) - y = 0 - Words = [] - AllLinePoints = [] - - for i, s in enumerate(self.Strings): - LineWidths[i] = 0 - LineWords = s.split(" ") - LinePoints = N.zeros((len(LineWords),2), N.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 = N.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 = N.zeros((0,2), N.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 = N.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 = BBox.asBBox(((x, y-h ),(x + w, y))) - - 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) - - self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) - dc.SetFont(self.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(TextObjectMixin, DrawObject, ): - """ - 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 = BBox.asBBox( (XY,XY) ) - - 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(TextObjectMixin, DrawObject, ): - """ - - 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 = BBox.asBBox( ( (x, y-h ), (x + w, y) ) ) - - - 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 ScaledBitmap2(TextObjectMixin, DrawObject, ): - """ - - An alternative scaled bitmap that only scaled the required amount of - the main bitmap when zoomed in: EXPERIMENTAL! - - """ - - def __init__(self, - Bitmap, - XY, - Height, - Width=None, - 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 = N.array(XY, N.float) - self.Height = Height - (self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight() - self.bmpWH = N.array((self.bmpWidth, self.bmpHeight), N.int32) - ## fixme: this should all accommodate different scales for X and Y - if Width is None: - self.BmpScale = float(self.bmpHeight) / Height - self.Width = self.bmpWidth / self.BmpScale - self.WH = N.array((self.Width, Height), N.float) - ##fixme: should this have a y = -1 to shift to y-up? - self.BmpScale = self.bmpWH / self.WH - - print "bmpWH:", self.bmpWH - print "Width, Height:", self.WH - print "self.BmpScale", self.BmpScale - self.ShiftFun = self.ShiftFunDict[Position] - self.CalcBoundingBox() - self.ScaledBitmap = None # cache of the last existing scaled bitmap - - 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 = BBox.asBBox( ((x, y-h ), (x + w, y)) ) - - def WorldToBitmap(self, Pw): - """ - computes bitmap coords from World coords - """ - delta = Pw - self.XY - Pb = delta * self.BmpScale - Pb *= (1, -1) ##fixme: this may only works for Yup projection! - ## and may only work for top left position - - return Pb.astype(N.int_) - - def _DrawEntireBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): - """ - this is pretty much the old code - - Scales and Draws the entire bitmap. - - """ - XY = WorldToPixel(self.XY) - H = ScaleWorldToPixel(self.Height)[0] - W = H * (self.bmpWidth / self.bmpHeight) - if (self.ScaledBitmap is None) or (self.ScaledBitmap[0] != (0, 0, self.bmpWidth, self.bmpHeight, W, H) ): - #if True: #fixme: (self.ScaledBitmap is None) or (H <> self.ScaledHeight) : - self.ScaledHeight = H - print "Scaling to:", W, H - Img = self.Image.Scale(W, H) - bmp = wx.BitmapFromImage(Img) - self.ScaledBitmap = ((0, 0, self.bmpWidth, self.bmpHeight , W, H), bmp)# this defines the cached bitmap - else: - print "Using Cached bitmap" - bmp = self.ScaledBitmap[1] - XY = self.ShiftFun(XY[0], XY[1], W, H) - dc.DrawBitmapPoint(bmp, XY, True) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectanglePointSize(XY, (W, H) ) - - def _DrawSubBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): - """ - Subsets just the part of the bitmap that is visible - then scales and draws that. - - """ - BBworld = BBox.asBBox(self._Canvas.ViewPortBB) - BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld)) - - XYs = WorldToPixel(self.XY) - # figure out subimage: - # fixme: this should be able to be done more succinctly! - - if BBbitmap[0,0] < 0: - Xb = 0 - elif BBbitmap[0,0] > self.bmpWH[0]: # off the bitmap - Xb = 0 - else: - Xb = BBbitmap[0,0] - XYs[0] = 0 # draw at origin - - if BBbitmap[0,1] < 0: - Yb = 0 - elif BBbitmap[0,1] > self.bmpWH[1]: # off the bitmap - Yb = 0 - ShouldDraw = False - else: - Yb = BBbitmap[0,1] - XYs[1] = 0 # draw at origin - - if BBbitmap[1,0] < 0: - #off the screen -- This should never happen! - Wb = 0 - elif BBbitmap[1,0] > self.bmpWH[0]: - Wb = self.bmpWH[0] - Xb - else: - Wb = BBbitmap[1,0] - Xb - - if BBbitmap[1,1] < 0: - # off the screen -- This should never happen! - Hb = 0 - ShouldDraw = False - elif BBbitmap[1,1] > self.bmpWH[1]: - Hb = self.bmpWH[1] - Yb - else: - Hb = BBbitmap[1,1] - Yb - - FullHeight = ScaleWorldToPixel(self.Height)[0] - scale = FullHeight / self.bmpWH[1] - Ws = int(scale * Wb + 0.5) # add the 0.5 to round - Hs = int(scale * Hb + 0.5) - if (self.ScaledBitmap is None) or (self.ScaledBitmap[0] != (Xb, Yb, Wb, Hb, Ws, Ws) ): - Img = self.Image.GetSubImage(wx.Rect(Xb, Yb, Wb, Hb)) - Img.Rescale(Ws, Hs) - bmp = wx.BitmapFromImage(Img) - self.ScaledBitmap = ((Xb, Yb, Wb, Hb, Ws, Ws), bmp)# this defines the cached bitmap - #XY = self.ShiftFun(XY[0], XY[1], W, H) - #fixme: get the shiftfun working! - else: - print "Using cached bitmap" - ##fixme: The cached bitmap could be used if the one needed is the same scale, but - ## a subset of the cached one. - bmp = self.ScaledBitmap[1] - dc.DrawBitmapPoint(bmp, XYs, True) - - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectanglePointSize(XYs, (Ws, Hs) ) - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - BBworld = BBox.asBBox(self._Canvas.ViewPortBB) - ## first see if entire bitmap is displayed: - if BBworld.Inside(self.BoundingBox): - print "Drawing entire bitmap with old code" - self._DrawEntireBitmap(dc , WorldToPixel, ScaleWorldToPixel, HTdc) - return None - elif BBworld.Overlaps(self.BoundingBox): - #BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld)) - print "Drawing a sub-bitmap" - self._DrawSubBitmap(dc , WorldToPixel, ScaleWorldToPixel, HTdc) - else: - print "Not Drawing -- no part of image is showing" - -class DotGrid: - """ - An example of a Grid Object -- it is set on teh FloatCAnvas with one of: - - FloatCanvas.GridUnder = Grid - FloatCanvas.GridOver = Grid - - It will be drawn every time, regardless of the viewport. - - In its _Draw method, it computes what to draw, given the ViewPortBB - of the Canvas it's being drawn on. - - """ - def __init__(self, Spacing, Size = 2, Color = "Black", Cross=False, CrossThickness = 1): - - self.Spacing = N.array(Spacing, N.float) - self.Spacing.shape = (2,) - self.Size = Size - self.Color = Color - self.Cross = Cross - self.CrossThickness = CrossThickness - - def CalcPoints(self, Canvas): - ViewPortBB = Canvas.ViewPortBB - - Spacing = self.Spacing - - minx, miny = N.floor(ViewPortBB[0] / Spacing) * Spacing - maxx, maxy = N.ceil(ViewPortBB[1] / Spacing) * Spacing - - ##fixme: this could use vstack or something with numpy - x = N.arange(minx, maxx+Spacing[0], Spacing[0]) # making sure to get the last point - y = N.arange(miny, maxy+Spacing[1], Spacing[1]) # an extra is OK - Points = N.zeros((len(y), len(x), 2), N.float) - x.shape = (1,-1) - y.shape = (-1,1) - Points[:,:,0] += x - Points[:,:,1] += y - Points.shape = (-1,2) - - return Points - - def _Draw(self, dc, Canvas): - Points = self.CalcPoints(Canvas) - - Points = Canvas.WorldToPixel(Points) - - dc.SetPen(wx.Pen(self.Color,self.CrossThickness)) - - if self.Cross: # Use cross shaped markers - #Horizontal lines - LinePoints = N.concatenate((Points + (self.Size,0),Points + (-self.Size,0)),1) - dc.DrawLineList(LinePoints) - # Vertical Lines - LinePoints = N.concatenate((Points + (0,self.Size),Points + (0,-self.Size)),1) - dc.DrawLineList(LinePoints) - pass - else: # use dots - ## Note: this code borrowed from Pointset -- itreally shouldn't be repeated here!. - if self.Size <= 1: - dc.DrawPointList(Points) - elif self.Size <= 2: - dc.DrawPointList(Points + (0,-1)) - dc.DrawPointList(Points + (0, 1)) - dc.DrawPointList(Points + (1, 0)) - dc.DrawPointList(Points + (-1,0)) - else: - dc.SetBrush(wx.Brush(self.Color)) - radius = int(round(self.Size/2)) - ##fixme: I really should add a DrawCircleList to wxPython - if len(Points) > 100: - xy = Points - xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Size ), 1 ) - dc.DrawEllipseList(xywh) - else: - for xy in Points: - dc.DrawCircle(xy[0],xy[1], radius) - - - -#--------------------------------------------------------------------------- -class FloatCanvas(wx.Panel): - """ - FloatCanvas.py - - This is a high level window for drawing maps and anything else in an - arbitrary coordinate system. - - The goal is to provide a convenient way to draw stuff on the screen - without having to deal with handling OnPaint events, converting to pixel - coordinates, knowing about wxWindows brushes, pens, and colors, etc. It - also provides virtually unlimited zooming and scrolling - - I am using it for two things: - 1) general purpose drawing in floating point coordinates - 2) displaying map data in Lat-long coordinates - - If the projection is set to None, it will draw in general purpose - floating point coordinates. If the projection is set to 'FlatEarth', it - will draw a FlatEarth projection, centered on the part of the map that - you are viewing. You can also pass in your own projection function. - - It is double buffered, so re-draws after the window is uncovered by something - else are very quick. - - It relies on NumPy, which is needed for speed (maybe, I havn't profiled it) - - Bugs and Limitations: - Lots: patches, fixes welcome - - For Map drawing: It ignores the fact that the world is, in fact, a - sphere, so it will do strange things if you are looking at stuff near - the poles or the date line. so far I don't have a need to do that, so I - havn't bothered to add any checks for that yet. - - Zooming: - I have set no zoom limits. What this means is that if you zoom in really - far, you can get integer overflows, and get wierd results. It - doesn't seem to actually cause any problems other than wierd output, at - least when I have run it. - - Speed: - I have done a couple of things to improve speed in this app. The one - thing I have done is used NumPy Arrays to store the coordinates of the - points of the objects. This allowed me to use array oriented functions - when doing transformations, and should provide some speed improvement - for objects with a lot of points (big polygons, polylines, pointsets). - - The real slowdown comes when you have to draw a lot of objects, because - you have to call the wx.DC.DrawSomething call each time. This is plenty - fast for tens of objects, OK for hundreds of objects, but pretty darn - slow for thousands of objects. - - The solution is to be able to pass some sort of object set to the DC - directly. I've used DC.DrawPointList(Points), and it helped a lot with - drawing lots of points. I havn't got a LineSet type object, so I havn't - used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList() - methods implimented, and then I'd also have a full set of Object sets - that could take advantage of them. I hope to get to it some day. - - Mouse Events: - - At this point, there are a full set of custom mouse events. They are - just like the regular mouse events, but include an extra attribute: - Event.GetCoords(), that returns the (x,y) position in world - coordinates, as a length-2 NumPy vector of Floats. - - Copyright: Christopher Barker - - License: Same as the version of wxPython you are using it with - - Please let me know if you're using this!!! - - Contact me at: - - Chris.Barker@noaa.gov - - """ - - def __init__(self, parent, id = -1, - size = wx.DefaultSize, - ProjectionFun = None, - BackgroundColor = "WHITE", - Debug = False): - - wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size) - - self.ComputeFontScale() - self.InitAll() - - self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID) - - self.Debug = Debug - - wx.EVT_PAINT(self, self.OnPaint) - wx.EVT_SIZE(self, self.OnSize) - - wx.EVT_LEFT_DOWN(self, self.LeftDownEvent) - wx.EVT_LEFT_UP(self, self.LeftUpEvent) - wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent) - wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent) - wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent) - wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent) - wx.EVT_RIGHT_DOWN(self, self.RightDownEvent) - wx.EVT_RIGHT_UP(self, self.RightUpEvent) - wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent) - wx.EVT_MOTION(self, self.MotionEvent) - wx.EVT_MOUSEWHEEL(self, self.WheelEvent) - - ## CHB: I'm leaving these out for now. - #wx.EVT_ENTER_WINDOW(self, self. ) - #wx.EVT_LEAVE_WINDOW(self, self. ) - - self.SetProjectionFun(ProjectionFun) - self.GUIMode = None - - # timer to give a delay when re-sizing so that buffers aren't re-built too many times. - self.SizeTimer = wx.PyTimer(self.OnSizeTimer) - - self.InitializePanel() - self.MakeNewBuffers() - -# self.CreateCursors() - - def ComputeFontScale(self): - ## A global variable to hold the scaling from pixel size to point size. - global FontScale - dc = wx.ScreenDC() - dc.SetFont(wx.Font(16, wx.ROMAN, wx.NORMAL, wx.NORMAL)) - E = dc.GetTextExtent("X") - FontScale = 16/E[1] - del dc - - def InitAll(self): - """ - InitAll() sets everything in the Canvas to default state. - - It can be used to reset the Canvas - - """ - - self.HitColorGenerator = None - self.UseHitTest = False - - self.NumBetweenBlits = 500 - - ## create the Hit Test Dicts: - self.HitDict = None - self._HTdc = None - - self._DrawList = [] - self._ForeDrawList = [] - self._ForegroundBuffer = None - self.BoundingBox = None - self.BoundingBoxDirty = False - self.MinScale = None - self.MaxScale = None - self.ViewPortCenter= N.array( (0,0), N.float) - - self.SetProjectionFun(None) - - self.MapProjectionVector = N.array( (1,1), N.float) # No Projection to start! - self.TransformVector = N.array( (1,-1), N.float) # default Transformation - - self.Scale = 1 - self.ObjectUnderMouse = None - - self.GridUnder = None - self.GridOver = None - - self._BackgroundDirty = True - - def SetProjectionFun(self,ProjectionFun): - if ProjectionFun == 'FlatEarth': - self.ProjectionFun = self.FlatEarthProjection - elif callable(ProjectionFun): - self.ProjectionFun = ProjectionFun - elif ProjectionFun is None: - self.ProjectionFun = lambda x=None: N.array( (1,1), N.float) - else: - 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): - MaxLatitude = 75 # these were determined essentially arbitrarily - MinLatitude = -75 - Lat = min(CenterPoint[1],MaxLatitude) - Lat = max(Lat,MinLatitude) - return N.array((N.cos(N.pi*Lat/180),1),N.float) - - def SetMode(self, Mode): - ''' - Set the GUImode to any of the availble mode. - ''' - # Set mode - self.GUIMode = Mode - #self.GUIMode.SetCursor() - self.SetCursor(self.GUIMode.Cursor) - - def MakeHitDict(self): - ##fixme: Should this just be None if nothing has been bound? - self.HitDict = {EVT_FC_LEFT_DOWN: {}, - EVT_FC_LEFT_UP: {}, - EVT_FC_LEFT_DCLICK: {}, - EVT_FC_MIDDLE_DOWN: {}, - EVT_FC_MIDDLE_UP: {}, - EVT_FC_MIDDLE_DCLICK: {}, - EVT_FC_RIGHT_DOWN: {}, - EVT_FC_RIGHT_UP: {}, - EVT_FC_RIGHT_DCLICK: {}, - EVT_FC_ENTER_OBJECT: {}, - EVT_FC_LEAVE_OBJECT: {}, - } - - def _RaiseMouseEvent(self, Event, EventType): - """ - This is called in various other places to raise a Mouse Event - """ - pt = self.PixelToWorld( Event.GetPosition() ) - evt = _MouseEvent(EventType, Event, self.GetId(), pt) - self.GetEventHandler().ProcessEvent(evt) - - if wx.__version__ >= "2.8": - HitTestBitmapDepth = 32 - #print "Using hit test code for 2.8" - def GetHitTestColor(self, xy): - if self._ForegroundHTBitmap: - pdata = wx.AlphaPixelData(self._ForegroundHTBitmap) - else: - pdata = wx.AlphaPixelData(self._HTBitmap) - if not pdata: - raise RuntimeError("Trouble Accessing Hit Test bitmap") - pacc = pdata.GetPixels() - pacc.MoveTo(pdata, xy[0], xy[1]) - return pacc.Get()[:3] - else: - HitTestBitmapDepth = 24 - #print "using pre-2.8 hit test code" - def GetHitTestColor(self, xy ): - dc = wx.MemoryDC() - if self._ForegroundHTBitmap: - dc.SelectObject(self._ForegroundHTBitmap) - else: - dc.SelectObject(self._HTBitmap) - hitcolor = dc.GetPixelPoint( xy ) - return hitcolor.Get() - - def HitTest(self, event, HitEvent): - if self.HitDict: - # check if there are any objects in the dict for this event - if self.HitDict[ HitEvent ]: - xy = event.GetPosition() - color = self.GetHitTestColor( xy ) - if color in self.HitDict[ HitEvent ]: - 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 - - def MouseOverTest(self, event): - ##fixme: Can this be cleaned up? - if (self.HitDict and - - (self.HitDict[EVT_FC_ENTER_OBJECT ] or - self.HitDict[EVT_FC_LEAVE_OBJECT ] ) - ): - xy = event.GetPosition() - color = self.GetHitTestColor( xy ) - OldObject = self.ObjectUnderMouse - ObjectCallbackCalled = False - if color in self.HitDict[ EVT_FC_ENTER_OBJECT ]: - Object = self.HitDict[ EVT_FC_ENTER_OBJECT][color] - if (OldObject is None): - try: - Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object) - ObjectCallbackCalled = True - except KeyError: - pass # this means the enter event isn't bound for that object - elif OldObject == Object: # the mouse is still on the same object - pass - ## Is the mouse on a differnt object as it was... - elif not (Object == OldObject): - # call the leave object callback - try: - OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject) - ObjectCallbackCalled = True - except KeyError: - pass # this means the leave event isn't bound for that object - try: - Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object) - ObjectCallbackCalled = True - except KeyError: - pass # this means the enter event isn't bound for that object - ## set the new object under mouse - self.ObjectUnderMouse = Object - elif color in self.HitDict[ EVT_FC_LEAVE_OBJECT ]: - Object = self.HitDict[ EVT_FC_LEAVE_OBJECT][color] - self.ObjectUnderMouse = Object - else: - # no objects under mouse bound to mouse-over events - self.ObjectUnderMouse = None - if OldObject: - try: - OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject) - ObjectCallbackCalled = True - except KeyError: - pass # this means the leave event isn't bound for that object - return ObjectCallbackCalled - return False - - ## fixme: There is a lot of repeated code here - ## Is there a better way? - def LeftDoubleClickEvent(self, event): - if self.GUIMode: - self.GUIMode.OnLeftDouble(event) - event.Skip() - - def MiddleDownEvent(self, event): - if self.GUIMode: - self.GUIMode.OnMiddleDown(event) - event.Skip() - - def MiddleUpEvent(self, event): - if self.GUIMode: - self.GUIMode.OnMiddleUp(event) - event.Skip() - - def MiddleDoubleClickEvent(self, event): - if self.GUIMode: - self.GUIMode.OnMiddleDouble(event) - event.Skip() - - def RightDoubleCLickEvent(self, event): - if self.GUIMode: - self.GUIMode.OnRightDouble(event) - event.Skip() - - def WheelEvent(self, event): - if self.GUIMode: - self.GUIMode.OnWheel(event) - event.Skip() - - def LeftDownEvent(self, event): - if self.GUIMode: - self.GUIMode.OnLeftDown(event) - event.Skip() - - def LeftUpEvent(self, event): - if self.HasCapture(): - self.ReleaseMouse() - if self.GUIMode: - self.GUIMode.OnLeftUp(event) - event.Skip() - - def MotionEvent(self, event): - if self.GUIMode: - self.GUIMode.OnMove(event) - event.Skip() - - def RightDownEvent(self, event): - if self.GUIMode: - self.GUIMode.OnRightDown(event) - event.Skip() - - def RightUpEvent(self, event): - if self.GUIMode: - self.GUIMode.OnRightUp(event) - event.Skip() - - def MakeNewBuffers(self): - self._BackgroundDirty = True - # Make new offscreen bitmap: - self._Buffer = wx.EmptyBitmap(*self.PanelSize) - if self._ForeDrawList: - self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize) - if self.UseHitTest: - self.MakeNewHTBitmap() - else: - self._ForegroundHTBitmap = None - else: - self._ForegroundBuffer = None - self._ForegroundHTBitmap = None - - if self.UseHitTest: - self.MakeNewHTBitmap() - else: - self._HTBitmap = None - self._ForegroundHTBitmap = None - - def MakeNewHTBitmap(self): - """ - Off screen Bitmap used for Hit tests on background objects - - """ - self._HTBitmap = wx.EmptyBitmap(self.PanelSize[0], - - self.PanelSize[1], - - depth=self.HitTestBitmapDepth) - - def MakeNewForegroundHTBitmap(self): - ## Note: the foreground and backround HT bitmaps are in separate functions - ## so that they can be created separate --i.e. when a foreground is - ## added after the backgound is drawn - """ - Off screen Bitmap used for Hit tests on foreground objects - - """ - self._ForegroundHTBitmap = wx.EmptyBitmap(self.PanelSize[0], - - self.PanelSize[1], - - depth=self.HitTestBitmapDepth) - - 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 = N.array(self.PanelSize, N.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] - - def OnPaint(self, event): - dc = wx.PaintDC(self) - if self._ForegroundBuffer: - dc.DrawBitmap(self._ForegroundBuffer,0,0) - else: - dc.DrawBitmap(self._Buffer,0,0) - - def Draw(self, Force=False): - """ - - Canvas.Draw(Force=False) - - Re-draws the canvas. - - Note that the buffer will not be re-drawn unless something has - changed. If you change a DrawObject directly, then the canvas - will not know anything has changed. In this case, you can force - a re-draw by passing int True for the Force flag: - - Canvas.Draw(Force=True) - - There is a main buffer set up to double buffer the screen, so - you can get quick re-draws when the window gets uncovered. - - If there are any objects in self._ForeDrawList, then the - background gets drawn to a new buffer, and the foreground - objects get drawn on top of it. The final result if blitted to - the screen, and stored for future Paint events. This is done so - that you can have a complicated background, but have something - changing on the foreground, without having to wait for the - background to get re-drawn. This can be used to support simple - animation, for instance. - - """ - - if N.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) - ViewPortWorld = N.array(( self.PixelToWorld((0,0)), - self.PixelToWorld(self.PanelSize) ) - ) - self.ViewPortBB = N.array( ( N.minimum.reduce(ViewPortWorld), - N.maximum.reduce(ViewPortWorld) ) ) - #self.ViewPortWorld = ViewPortWorld - - dc = wx.MemoryDC() - dc.SelectObject(self._Buffer) - if self._BackgroundDirty or Force: - dc.SetBackground(self.BackgroundBrush) - dc.Clear() - if self._HTBitmap is not None: - HTdc = wx.MemoryDC() - HTdc.SelectObject(self._HTBitmap) - HTdc.Clear() - else: - HTdc = None - if self.GridUnder is not None: - self.GridUnder._Draw(dc, self) - self._DrawObjects(dc, self._DrawList, ScreenDC, self.ViewPortBB, HTdc) - self._BackgroundDirty = False - del HTdc - - if self._ForeDrawList: - ## If an object was just added to the Foreground, there might not yet be a buffer - if self._ForegroundBuffer is None: - self._ForegroundBuffer = wx.EmptyBitmap(self.PanelSize[0], - self.PanelSize[1]) - - dc = wx.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here - dc.SelectObject(self._ForegroundBuffer) - dc.DrawBitmap(self._Buffer,0,0) - if self._ForegroundHTBitmap is not None: - ForegroundHTdc = wx.MemoryDC() - ForegroundHTdc.SelectObject( self._ForegroundHTBitmap) - ForegroundHTdc.Clear() - if self._HTBitmap is not None: - #Draw the background HT buffer to the foreground HT buffer - ForegroundHTdc.DrawBitmap(self._HTBitmap, 0, 0) - else: - ForegroundHTdc = None - self._DrawObjects(dc, - self._ForeDrawList, - ScreenDC, - self.ViewPortBB, - ForegroundHTdc) - if self.GridOver is not None: - self.GridOver._Draw(dc, self) - ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0) - # If the canvas is in the middle of a zoom or move, - # the Rubber Band box needs to be re-drawn - ##fixme: maybe GUIModes should never be None, and rather have a Do-nothing GUI-Mode. - if self.GUIMode is not None: - self.GUIMode.UpdateScreen() - - if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start) - - ## Clear the font cache. If you don't do this, the X font server - ## starts to take up Massive amounts of memory This is mostly a - ## problem with very large fonts, that you get with scaled text - ## when zoomed in. - DrawObject.FontList = {} - - def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck - # lrk: Returns the objects that should be redrawn - - ## fixme: should this check be moved into the object? - ## also: a BB object would make this cleaner too - BB2 = ViewPortBB - redrawlist = [] - for Object in DrawList: - BB1 = Object.BoundingBox - ## note: this could use the Utilities.BBCheck function - ## butthis saves a function call - if (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and - BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1]): - redrawlist.append(Object) - #return redrawlist - ##fixme: disabled this!!!! - return redrawlist - _ShouldRedraw = staticmethod(_ShouldRedraw) - - def MoveImage(self,shift,CoordType): - """ - move the image in the window. - - shift is an (x,y) tuple, specifying the amount to shift in each direction - - It can be in any of three coordinates: Panel, Pixel, World, - specified by the CoordType parameter - - Panel coordinates means you want to shift the image by some - fraction of the size of the displaed image - - Pixel coordinates means you want to shift the image by some number of pixels - - World coordinates mean you want to shift the image by an amount - in Floating point world coordinates - - """ - shift = N.asarray(shift,N.float) - if CoordType == 'Panel':# convert from panel coordinates - shift = shift * N.array((-1,1),N.float) *self.PanelSize/self.TransformVector - elif CoordType == 'Pixel': # convert from pixel coordinates - shift = shift/self.TransformVector - elif CoordType == 'World': # No conversion - pass - else: - raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"') - - self.ViewPortCenter = self.ViewPortCenter + shift - self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) - self.TransformVector = N.array((self.Scale,-self.Scale),N.float) * self.MapProjectionVector - self._BackgroundDirty = True - self.Draw() - - def Zoom(self, factor, center = None, centerCoords="world"): - - """ - Zoom(factor, center) changes the amount of zoom of the image by factor. - If factor is greater than one, the image gets larger. - If factor is less than one, the image gets smaller. - - center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. - If center is not given, the center will stay the same. - - centerCoords is a flag indicating whether the center given is in pixel or world - coords. Options are: "world" or "pixel" - - """ - self.Scale = self.Scale*factor - if not center is None: - if centerCoords == "pixel": - center = self.PixelToWorld( center ) - else: - center = N.array(center,N.float) - self.ViewPortCenter = center - self.SetToNewScale() - - def ZoomToBB(self, NewBB=None, DrawFlag=True): - - """ - - Zooms the image to the bounding box given, or to the bounding - box of all the objects on the canvas, if none is given. - - """ - - if NewBB is not None: - BoundingBox = NewBB - else: - if self.BoundingBoxDirty: - self._ResetBoundingBox() - BoundingBox = self.BoundingBox - if BoundingBox is not None: - self.ViewPortCenter = N.array(((BoundingBox[0,0]+BoundingBox[1,0])/2, - (BoundingBox[0,1]+BoundingBox[1,1])/2 ),N.float_) - self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) - # Compute the new Scale - BoundingBox = BoundingBox*self.MapProjectionVector # this does need to make a copy! - try: - self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])), - abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )*0.95 - except ZeroDivisionError: # this will happen if the BB has zero width or height - try: #width == 0 - self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95 - except ZeroDivisionError: - try: # height == 0 - self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95 - except ZeroDivisionError: #zero size! (must be a single point) - self.Scale = 1 - - if DrawFlag: - self._BackgroundDirty = True - else: - # Reset the shifting and scaling to defaults when there is no BB - self.ViewPortCenter= N.array( (0,0), N.float) - self.Scale= 1 - self.SetToNewScale(DrawFlag=DrawFlag) - - def SetToNewScale(self, DrawFlag=True): - Scale = self.Scale - if self.MinScale is not None: - Scale = max(Scale, self.MinScale) - if self.MaxScale is not None: - Scale = min(Scale, self.MaxScale) - self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) - self.TransformVector = N.array((Scale,-Scale),N.float) * self.MapProjectionVector - self.Scale = Scale - self._BackgroundDirty = True - if DrawFlag: - self.Draw() - - def RemoveObjects(self, Objects): - for Object in Objects: - self.RemoveObject(Object, ResetBB=False) - self.BoundingBoxDirty = True - - def RemoveObject(self, Object, ResetBB = True): - ##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 - if ResetBB: - self.BoundingBoxDirty = True - - def ClearAll(self, ResetBB=True): - """ - ClearAll(ResetBB=True) - - Removes all DrawObjects from the Canvas - - If ResetBB is set to False, the original bounding box will remain - - """ - self._DrawList = [] - self._ForeDrawList = [] - self._BackgroundDirty = True - self.HitColorGenerator = None - self.UseHitTest = False - if ResetBB: - self._ResetBoundingBox() - self.MakeNewBuffers() - self.HitDict = None - - def _ResetBoundingBox(self): - if self._DrawList or self._ForeDrawList: - bblist = [] - for (i, obj) in enumerate(self._DrawList): - bblist.append(obj.BoundingBox) - for (j, obj) in enumerate(self._ForeDrawList): - bblist.append(obj.BoundingBox) - self.BoundingBox = BBox.fromBBArray(bblist) - else: - self.BoundingBox = None - self.ViewPortCenter= N.array( (0,0), N.float) - self.TransformVector = N.array( (1,-1), N.float) - self.MapProjectionVector = N.array( (1,1), N.float) - self.Scale = 1 - self.BoundingBoxDirty = False - - def PixelToWorld(self, Points): - """ - Converts coordinates from Pixel coordinates to world coordinates. - - Points is a tuple of (x,y) coordinates, or a list of such tuples, - or a NX2 Numpy array of x,y coordinates. - - """ - return (((N.asarray(Points, N.float) - - (self.PanelSize/2))/self.TransformVector) + - self.ViewPortCenter) - - def WorldToPixel(self,Coordinates): - """ - This function will get passed to the drawing functions of the objects, - to transform from world to pixel coordinates. - Coordinates should be a NX2 array of (x,y) coordinates, or - a 2-tuple, or sequence of 2-tuples. - """ - #Note: this can be called by users code for various reasons, so N.asarray is needed. - return (((N.asarray(Coordinates,N.float) - - self.ViewPortCenter)*self.TransformVector)+ - (self.HalfPanelSize)).astype('i') - - def ScaleWorldToPixel(self,Lengths): - """ - This function will get passed to the drawing functions of the objects, - to Change a length from world to pixel coordinates. - - Lengths should be a NX2 array of (x,y) coordinates, or - a 2-tuple, or sequence of 2-tuples. - """ - return ( (N.asarray(Lengths, N.float)*self.TransformVector) ).astype('i') - - def ScalePixelToWorld(self,Lengths): - """ - This function computes a pair of x.y lengths, - to change then from pixel to world coordinates. - - Lengths should be a NX2 array of (x,y) coordinates, or - a 2-tuple, or sequence of 2-tuples. - """ - - return (N.asarray(Lengths,N.float) / self.TransformVector) - - def AddObject(self, obj): - # put in a reference to the Canvas, so remove and other stuff can work - obj._Canvas = self - if obj.InForeground: - self._ForeDrawList.append(obj) - self.UseForeground = True - else: - self._DrawList.append(obj) - self._BackgroundDirty = True - self.BoundingBoxDirty = True - return True - - def AddObjects(self, Objects): - for Object in Objects: - self.AddObject(Object) - - def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None): - """ - This is a convenience function; - This function takes the list of objects and draws them to specified - device context. - """ - dc.SetBackground(self.BackgroundBrush) - dc.BeginDrawing() - #i = 0 - PanelSize0, PanelSize1 = self.PanelSize # for speed - WorldToPixel = self.WorldToPixel # for speed - ScaleWorldToPixel = self.ScaleWorldToPixel # for speed - Blit = ScreenDC.Blit # for speed - NumBetweenBlits = self.NumBetweenBlits # for speed - for i, Object in enumerate(self._ShouldRedraw(DrawList, ViewPortBB)): - if Object.Visible: - Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) - if (i+1) % NumBetweenBlits == 0: - Blit(0, 0, PanelSize0, PanelSize1, dc, 0, 0) - dc.EndDrawing() - - 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 specified using the wx flags: - - wx.BITMAP_TYPE_PNG - wx.BITMAP_TYPE_JPG - 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","Point", "Arrow", "ArrowLine", "ScaledTextBox", - "SquarePoint","Bitmap", "ScaledBitmap", "Spline", "Group"] - for classname in classnames: - klass = globals()[classname] - def getaddshapemethod(klass=klass): - def addshape(self, *args, **kwargs): - Object = klass(*args, **kwargs) - self.AddObject(Object) - return Object - return addshape - 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 - if klass.__doc__: - docstring += ", whose docstring is:\n%s" % klass.__doc__ - FloatCanvas.__dict__[methodname].__doc__ = docstring - -_makeFloatCanvasAddMethods() - -