X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/d85004c5ae08c3ae2f66e39fc271e3761dc1366e..42463de2678493f48631c42b4d1a7152c2786893:/wxPython/wx/lib/floatcanvas/FloatCanvas.py diff --git a/wxPython/wx/lib/floatcanvas/FloatCanvas.py b/wxPython/wx/lib/floatcanvas/FloatCanvas.py new file mode 100644 index 0000000000..83865fbc34 --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/FloatCanvas.py @@ -0,0 +1,1962 @@ + +try: + from Numeric import array,asarray,Float,cos,pi,sum,minimum,maximum,Int32,zeros +except ImportError: + from numarray import array, asarray, Float, cos, pi, sum, minimum, maximum, Int32, zeros + +from time import clock, sleep + +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 + +## a custom Exceptions: + +class FloatCanvasException(Exception): + pass + +## 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 +EVT_FC_ENTER_OBJECT = wx.NewEventType() +EVT_FC_LEAVE_OBJECT = wx.NewEventType() + +#def EVT_ENTER_WINDOW( window, function ): +# window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function ) +#def EVT_LEAVE_WINDOW( window, function ): +# window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function ) +def EVT_LEFT_DOWN( window, function ): + window.Connect( -1, -1,EVT_FC_LEFT_DOWN , function ) +def EVT_LEFT_UP( window, function ): + window.Connect( -1, -1,EVT_FC_LEFT_UP , function ) +def EVT_LEFT_DCLICK ( window, function ): + window.Connect( -1, -1,EVT_FC_LEFT_DCLICK , function ) +def EVT_MIDDLE_DOWN ( window, function ): + window.Connect( -1, -1,EVT_FC_MIDDLE_DOWN , function ) +def EVT_MIDDLE_UP ( window, function ): + window.Connect( -1, -1,EVT_FC_MIDDLE_UP , function ) +def EVT_MIDDLE_DCLICK ( window, function ): + window.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK , function ) +def EVT_RIGHT_DOWN ( window, function ): + window.Connect( -1, -1,EVT_FC_RIGHT_DOWN , function ) +def EVT_RIGHT_UP( window, function ): + window.Connect( -1, -1,EVT_FC_RIGHT_UP , function ) +def EVT_RIGHT_DCLICK( window, function ): + window.Connect( -1, -1,EVT_FC_RIGHT_DCLICK , function ) +def EVT_MOTION( window, function ): + window.Connect( -1, -1,EVT_FC_MOTION , function ) +def EVT_MOUSEWHEEL( window, function ): + window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function ) + +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 differnce 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 SetCoords(self,Coords): + self.Coords = Coords + + def GetCoords(self): + return self.Coords + + def __getattr__(self, name): + #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: + +## """ + +## 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 + +## """ + +## 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): + yield (idx, ) + tail + +def colorGenerator(): + import sys + if sys.platform == 'darwin': + depth = 24 + else: + b = wx.EmptyBitmap(1,1) + depth = b.GetDepth() + if depth == 16: + step = 8 + elif depth >= 24: + step = 1 + else: + raise "ColorGenerator does not work with depth = %s" % depth + 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 + +##class ObjectSetMixin: +## """ +## A mix-in class for draw objects that are sets of objects + +## It contains methods for setting lists of pens and brushes + +## """ +## def SetPens(self,LineColors,LineStyles,LineWidths): +## """ +## This method used when an object could have a list of pens, rather than just one +## It is used for LineSet, and perhaps others in the future. + +## fixme: this should be in a mixin + +## fixme: this is really kludgy, there has got to be a better way! + +## """ + +## length = 1 +## if type(LineColors) == types.ListType: +## length = len(LineColors) +## else: +## LineColors = [LineColors] + +## if type(LineStyles) == types.ListType: +## length = len(LineStyles) +## else: +## LineStyles = [LineStyles] + +## if type(LineWidths) == types.ListType: +## length = len(LineWidths) +## else: +## LineWidths = [LineWidths] + +## if length > 1: +## if len(LineColors) == 1: +## LineColors = LineColors*length +## if len(LineStyles) == 1: +## LineStyles = LineStyles*length +## if len(LineWidths) == 1: +## LineWidths = LineWidths*length + +## self.Pens = [] +## for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths): +## if LineColor is None or LineStyle is None: +## self.Pens.append(wx.TRANSPARENT_PEN) +## # what's this for?> self.LineStyle = 'Transparent' +## if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): +## Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) +## self.Pens.append(Pen) +## else: +## self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)]) +## if length == 1: +## self.Pens = self.Pens[0] + + + +class DrawObject: + """ + This is the base class for all the objects that can be drawn. + + """ + + def __init__(self,InForeground = False): + 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 + + # 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 Bind(self, Event, CallBackFun): + self.CallBackFuncs[Event] = CallBackFun + self.HitAble = True + self._Canvas.UseHitTest = True + if not self._Canvas._HTdc: + self._Canvas.MakeNewHTdc() + 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 + 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", LineWidth), wx.Pen(HitColor, LineWidth, self.LineStyleList["Solid"]) ) + + 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 + +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 = asarray(Delta, Float) + self.XY += Delta + self.BoundingBox = self.BoundingBox + Delta + 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. + + """ + +## This is code for the XYMixin object, it needs to be adapeted and tested. +## 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,) + +## """ + +## Delta = array(Delta, Float) +## self.XY += Delta +## self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float) +## 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) + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + + + +class Polygon(DrawObject,PointsObjectMixin): + + """ + + 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. + + """ + def __init__(self, + Points, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + 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.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) + 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 PolygonSet(DrawObject): +## """ +## The PolygonSet class takes a Geometry.Polygon object. +## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! + +## it creates a set of line segments, from (x1,y1) to (x2,y2) + +## """ + +## def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False): +## DrawObject.__init__(self, InForeground) + +## ##fixme: there should be some error checking for everything being the right length. + + +## 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.LineColors = LineColors +## self.LineStyles = LineStyles +## self.LineWidths = LineWidths +## self.FillColors = FillColors +## self.FillStyles = FillStyles + +## self.SetPens(LineColors,LineStyles,LineWidths) + +## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): +## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): +## Points = WorldToPixel(self.Points) +## Points.shape = (-1,4) +## dc.DrawLineList(Points,self.Pens) + + +class Line(DrawObject,PointsObjectMixin): + """ + 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. + + """ + def __init__(self,Points, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + InForeground = False): + DrawObject.__init__(self, InForeground) + + + 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.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 LineSet(DrawObject, ObjectSetMixin): +## """ +## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. +## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! + +## it creates a set of line segments, from (x1,y1) to (x2,y2) + +## """ + +## def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False): +## DrawObject.__init__(self, InForeground) + +## NumLines = len(Points) / 2 +## ##fixme: there should be some error checking for everything being the right length. + + +## 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.LineColors = LineColors +## self.LineStyles = LineStyles +## self.LineWidths = LineWidths + +## self.SetPens(LineColors,LineStyles,LineWidths) + +## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): +## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): +## Points = WorldToPixel(self.Points) +## Points.shape = (-1,4) +## dc.DrawLineList(Points,self.Pens) + +class PointSet(DrawObject): + """ + 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. + + 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. + + 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 = 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.Diameter = Diameter + + self.HitLineWidth = self.MinHitLineWidth + self.SetPen(Color,"Solid",1) + self.SetBrush(Color,"Solid") + + 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 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)) + for xy in Points: + dc.DrawEllipsePointSize( (xy - radius), (self.Diameter, self.Diameter) ) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + if self.Diameter <= 1: + HTdc.DrawPointList(Points) + elif self.Diameter <= 2: + self.DrawD2(HTdc, Points) + 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. + +## Also Fill and line data + +## """ +## def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,InForeground = False): +## DrawObject.__init__(self,InForeground) + +## 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) + +## self.LineColor = LineColor +## self.LineStyle = LineStyle +## self.LineWidth = LineWidth +## self.FillColor = FillColor +## self.FillStyle = FillStyle + +## self.SetPen(LineColor,LineStyle,LineWidth) +## self.SetBrush(FillColor,FillStyle) + +## 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, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + InForeground = False): + + 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.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 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) ) + + def SetXY(self, x, y): + self.XY = array( (x, y), Float) + self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float) + if self._Canvas: + 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, + WorldToPixel, + ScaleWorldToPixel, + HTdc) + dc.DrawRectanglePointSize(XY, WH) + if HTdc and self.HitAble: + 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, + WorldToPixel, + ScaleWorldToPixel, + HTdc) + dc.DrawEllipsePointSize(XY, WH) + if HTdc and self.HitAble: + HTdc.DrawEllipsePointSize(XY, WH) + +class Circle(Ellipse): + def __init__(self, x ,y, Diameter, **kwargs): + RectEllipse.__init__(self , + x-Diameter/2., + y-Diameter/2., + Diameter, + Diameter, + **kwargs) + +class TextObjectMixin: + """ + + 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 = {} + + def SetFont(self, Size, Family, Style, Weight, Underline, FaceName): + self.Font = self.FontList.setdefault( (Size, + Family, + Style, + Weight, + Underline, + FaceName), + wx.Font(Size, + Family, + Style, + Weight, + Underline, + FaceName) ) + return self.Font + + ## 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)} + +class Text(DrawObject, TextObjectMixin): + """ + 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. + 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 is fixed, and does not scale with the drawing. + + The hit-test is done on the entire text extent + + """ + + def __init__(self,String,x,y, + Size = 12, + Color = "Black", + BackgroundColor = None, + Family = wx.MODERN, + Style = wx.NORMAL, + Weight = wx.NORMAL, + Underline = False, + Position = 'tl', + InForeground = False, + Font = None): + + DrawObject.__init__(self,InForeground) + + self.String = String + # Input size in in Pixels, compute points size from PPI info. + # fixme: for printing, we'll have to do something a little different + self.Size = int(round(72.0 * Size / ScreenPPI)) + + 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, Underline, FaceName) + + self.BoundingBox = array(((x,y),(x,y)),Float) + + self.XY = ( x,y ) + + # 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) + 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(DrawObject, TextObjectMixin, XYObjectMixin): + """ + 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. + 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. + + 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 havn't gotten around to it. + + """ + + def __init__(self, String, x, y , Size, + Color = "Black", + BackgroundColor = None, + Family = wx.MODERN, + Style = wx.NORMAL, + Weight = wx.NORMAL, + Underline = False, + Position = 'tl', + Font = None, + InForeground = False): + + DrawObject.__init__(self,InForeground) + + self.String = String + self.XY = array( (x, y), Float) + self.Size = Size + self.Color = Color + self.BackgroundColor = BackgroundColor + self.Family = Family + self.Style = Style + self.Weight = Weight + self.Underline = Underline + 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 any 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 + + self.ShiftFun = self.ShiftFunDict[Position] + + ## Compute the BB + ## 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 + 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) + 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) ) + + # 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. + if Size > self.MaxSize: + Size = self.MaxSize + dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName)) + 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 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 rebulsr 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) + + 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 + del dc + + self.HitColorGenerator = None + self.UseHitTest = None + + self.NumBetweenBlits = 500 + + 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. ) + + ## create the Hit Test Dicts: + self.HitDict = None + + + self._DrawList = [] + self._ForeDrawList = [] + self._ForegroundBuffer = None + self.BoundingBox = None + self.BoundingBoxDirty = False + self.ViewPortCenter= array( (0,0), Float) + + self.SetProjectionFun(ProjectionFun) + + self.MapProjectionVector = array( (1,1), Float) # No Projection to start! + self.TransformVector = array( (1,-1), Float) # default Transformation + + self.Scale = 1 + + self.GUIMode = None + self.StartRBBox = None + self.PrevRBBox = None + self.StartMove = None + self.PrevMoveXY = None + self.ObjectUnderMouse = None + + # called just to make sure everything is initialized + self.OnSize(None) + + self.InHereNum = 0 + + def SetProjectionFun(self,ProjectionFun): + if ProjectionFun == 'FlatEarth': + self.ProjectionFun = self.FlatEarthProjection + elif type(ProjectionFun) == types.FunctionType: + 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') + + 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]: + self.GUIMode = Mode + else: + raise FloatCanvasException('"%s" is Not a valid Mode'%Mode) + + 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 + """ + #print "in Raise Mouse Event", Event + pt = self.PixelToWorld( Event.GetPosition() ) + evt = MouseEvent(EventType, Event, self.GetId(), pt) + self.GetEventHandler().ProcessEvent(evt) + + 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() + if self._ForegroundHTdc: + hitcolor = self._ForegroundHTdc.GetPixelPoint( xy ) + else: + hitcolor = self._HTdc.GetPixelPoint( xy ) + color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() ) + if color in self.HitDict[ HitEvent ]: + Object = self.HitDict[ HitEvent ][color] + ## Add the hit coords to the Object + Object.HitCoords = self.PixelToWorld( xy ) + Object.CallBackFuncs[HitEvent](Object) + return True + return False + + def MouseOverTest(self, event): + ##fixme: Can this be cleaned up? + if self.HitDict: + xy = event.GetPosition() + if self._ForegroundHTdc: + hitcolor = self._ForegroundHTdc.GetPixelPoint( xy ) + else: + hitcolor = self._HTdc.GetPixelPoint( xy ) + color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() ) + 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 + + + ## fixme: There is a lot of repeated code here + ## Is there a better way? + def LeftDoubleClickEvent(self,event): + if self.GUIMode == "Mouse": + EventType = EVT_FC_LEFT_DCLICK + if not self.HitTest(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) + + def MiddleUpEvent(self,event): + if self.GUIMode == "Mouse": + EventType = EVT_FC_MIDDLE_UP + if not self.HitTest(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) + + def RightUpEvent(self,event): + if self.GUIMode == "Mouse": + EventType = EVT_FC_RIGHT_UP + if not self.HitTest(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) + + def WheelEvent(self,event): + if self.GUIMode == "Mouse": + self.RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL) + + + def LeftDownEvent(self,event): + if self.GUIMode: + if self.GUIMode == "ZoomIn": + self.StartRBBox = array( event.GetPosition() ) + self.PrevRBBox = None + self.CaptureMouse() + elif self.GUIMode == "ZoomOut": + Center = self.PixelToWorld( event.GetPosition() ) + self.Zoom(1/1.5,Center) + elif self.GUIMode == "Move": + 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) + else: + pass + + def LeftUpEvent(self,event): + if self.HasCapture(): + self.ReleaseMouse() + if self.GUIMode: + if self.GUIMode == "ZoomIn": + if event.LeftUp() and not self.StartRBBox is None: + self.PrevRBBox = None + EndRBBox = event.GetPosition() + StartRBBox = self.StartRBBox + # if mouse has moved less that ten pixels, don't use the box. + if ( abs(StartRBBox[0] - EndRBBox[0]) > 10 + and abs(StartRBBox[1] - EndRBBox[1]) > 10 ): + EndRBBox = self.PixelToWorld(EndRBBox) + StartRBBox = self.PixelToWorld(StartRBBox) + BB = array(((min(EndRBBox[0],StartRBBox[0]), + min(EndRBBox[1],StartRBBox[1])), + (max(EndRBBox[0],StartRBBox[0]), + max(EndRBBox[1],StartRBBox[1]))),Float) + self.ZoomToBB(BB) + else: + Center = self.PixelToWorld(StartRBBox) + self.Zoom(1.5,Center) + self.StartRBBox = None + elif self.GUIMode == "Move": + if not self.StartMove is None: + StartMove = self.StartMove + EndMove = array((event.GetX(),event.GetY())) + if sum((StartMove-EndMove)**2) > 16: + self.Move(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) + else: + pass + + def MotionEvent(self,event): + if self.GUIMode: + if self.GUIMode == "ZoomIn": + if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None): + xy0 = self.StartRBBox + xy1 = array( event.GetPosition() ) + wh = abs(xy1 - xy0) + wh[0] = max(wh[0], int(wh[1]*self.AspectRatio)) + wh[1] = int(wh[0] / self.AspectRatio) + xy_c = (xy0 + xy1) / 2 + dc = wx.ClientDC(self) + dc.BeginDrawing() + dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.PrevRBBox: + dc.DrawRectanglePointSize(*self.PrevRBBox) + self.PrevRBBox = ( xy_c - wh/2, wh ) + dc.DrawRectanglePointSize( *self.PrevRBBox ) + dc.EndDrawing() + elif self.GUIMode == "Move": + if event.Dragging() and event.LeftIsDown() and not self.StartMove is None: + xy1 = array( event.GetPosition() ) + wh = self.PanelSize + xy_tl = xy1 - self.StartMove + dc = wx.ClientDC(self) + dc.BeginDrawing() + x1,y1 = self.PrevMoveXY + x2,y2 = xy_tl + w,h = self.PanelSize + if x2 > x1 and y2 > y1: + xa = xb = x1 + ya = yb = y1 + wa = w + ha = y2 - y1 + wb = x2- x1 + hb = h + elif x2 > x1 and y2 <= y1: + xa = x1 + ya = y1 + wa = x2 - x1 + ha = h + xb = x1 + yb = y2 + h + wb = w + hb = y1 - y2 + elif x2 <= x1 and y2 > y1: + xa = x1 + ya = y1 + wa = w + ha = y2 - y1 + xb = x2 + w + yb = y1 + wb = x1 - x2 + hb = h - y2 + y1 + elif x2 <= x1 and y2 <= y1: + xa = x2 + w + ya = y1 + wa = x1 - x2 + ha = h + xb = x1 + 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: + dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl) + else: + dc.DrawBitmapPoint(self._Buffer,xy_tl) + dc.EndDrawing() + elif self.GUIMode == "Mouse": + ## 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) + else: + pass + self.RaiseMouseEvent(event,EVT_FC_MOTION) + else: + pass + + def RightDownEvent(self,event): + if self.GUIMode: + if self.GUIMode == "ZoomIn": + Center = self.PixelToWorld((event.GetX(),event.GetY())) + self.Zoom(1/1.5,Center) + elif self.GUIMode == "ZoomOut": + Center = self.PixelToWorld((event.GetX(),event.GetY())) + self.Zoom(1.5,Center) + elif self.GUIMode == "Mouse": + EventType = EVT_FC_RIGHT_DOWN + if not self.HitTest(event, EventType): + self.RaiseMouseEvent(event, EventType) + else: + pass + + def MakeNewBuffers(self): + self._BackgroundDirty = True + # Make new offscreen bitmap: + self._Buffer = wx.EmptyBitmap(*self.PanelSize) + #dc = wx.MemoryDC() + #dc.SelectObject(self._Buffer) + #dc.Clear() + if self._ForeDrawList: + self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize) + else: + self._ForegroundBuffer = None + if self.UseHitTest: + self.MakeNewHTdc() + else: + self._HTdc = None + self._ForegroundHTdc = None + + def MakeNewHTdc(self): + ## Note: While it's considered a "bad idea" to keep a + ## MemoryDC around I'm doing it here because a wx.Bitmap + ## doesn't have a GetPixel method so a DC is needed to do + ## the hit-test. It didn't seem like a good idea to re-create + ## a wx.MemoryDC on every single mouse event, so I keep it + ## around instead + self._HTdc = wx.MemoryDC() + self._HTBitmap = wx.EmptyBitmap(*self.PanelSize) + self._HTdc.SelectObject( self._HTBitmap ) + self._HTdc.SetBackground(wx.BLACK_BRUSH) + if self._ForeDrawList: + self._ForegroundHTdc = wx.MemoryDC() + self._ForegroundHTBitmap = wx.EmptyBitmap(*self.PanelSize) + self._ForegroundHTdc.SelectObject( self._ForegroundHTBitmap ) + self._ForegroundHTdc.SetBackground(wx.BLACK_BRUSH) + else: + self._ForegroundHTdc = None + + def OnSize(self,event): + self.PanelSize = array(self.GetClientSizeTuple(),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) + if self._ForegroundBuffer: + dc.DrawBitmap(self._ForegroundBuffer,0,0) + else: + dc.DrawBitmap(self._Buffer,0,0) + + def Draw(self, Force=False): + """ + 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. + + """ + #print "In Draw" + if self.Debug: start = clock() + ScreenDC = wx.ClientDC(self) + ViewPortWorld = ( self.PixelToWorld((0,0)), + self.PixelToWorld(self.PanelSize) ) + ViewPortBB = array( ( minimum.reduce(ViewPortWorld), + maximum.reduce(ViewPortWorld) ) ) + dc = wx.MemoryDC() + dc.SelectObject(self._Buffer) + if self._BackgroundDirty or Force: + #print "Background is Dirty" + dc.SetBackground(self.BackgroundBrush) + dc.Clear() + if self._HTdc: + self._HTdc.Clear() + self._DrawObjects(dc, self._DrawList, ScreenDC, ViewPortBB, self._HTdc) + self._BackgroundDirty = False + + 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._ForegroundHTdc is None: + self._ForegroundHTdc = wx.MemoryDC() + self._ForegroundHTdc.SelectObject( wx.EmptyBitmap( + self.PanelSize[0], + self.PanelSize[1]) ) + if self._HTdc: + ## blit the background HT buffer to the foreground HT buffer + self._ForegroundHTdc.Blit(0, 0, + self.PanelSize[0], self.PanelSize[1], + self._HTdc, 0, 0) + self._DrawObjects(dc, + self._ForeDrawList, + ScreenDC, + ViewPortBB, + self._ForegroundHTdc) + ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0) +## wx.GetApp().Yield(True) + # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn + # This seeems out of place, but it works. + if self.PrevRBBox: + ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) + ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) + ScreenDC.SetLogicalFunction(wx.XOR) + ScreenDC.DrawRectanglePointSize(*self.PrevRBBox) + 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 + + BB2 = ViewPortBB + redrawlist = [] + for Object in DrawList: + BB1 = Object.BoundingBox + 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 + _ShouldRedraw = staticmethod(_ShouldRedraw) + + +## def BBCheck(self, BB1, BB2): +## """ + +## BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise + +## """ +## 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]) ): +## return True +## else: +## return False + + def Move(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 = array(shift,Float) + if CoordType == 'Panel':# convert from panel coordinates + shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector + elif CoordType == 'Pixel': # convert from pixel coordinates + shift = shift/self.TransformVector + elif CoordType == 'World': # No conversion + pass + else: + raise FloatCanvasException('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 + self._BackgroundDirty = True + self.Draw() + + def Zoom(self,factor,center = None): + + """ + 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. + + """ + self.Scale = self.Scale*factor + if not center is None: + self.ViewPortCenter = array(center,Float) + self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) + self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector + self._BackgroundDirty = True + self.Draw() + + 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 not NewBB is None: + BoundingBox = NewBB + else: + if self.BoundingBoxDirty: + self._ResetBoundingBox() + BoundingBox = self.BoundingBox + if not BoundingBox is None: + self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2, + (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float) + self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) + # Compute the new Scale + BoundingBox = BoundingBox * self.MapProjectionVector + 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 + + self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector + if DrawFlag: + self._BackgroundDirty = True + self.Draw() + else: + # Reset the shifting and scaling to defaults when there is no BB + self.ViewPortCenter= array( (0,0), Float) + self.MapProjectionVector = array( (1,1), Float) # No Projection to start! + self.TransformVector = array( (1,-1), Float) # default Transformation + self.Scale = 1 + + 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) + else: + self._DrawList.remove(Object) + self._BackgroundDirty = True + if ResetBB: + self.BoundingBoxDirty = True + + def ClearAll(self, ResetBB = True): + self._DrawList = [] + self._ForeDrawList = [] + self._BackgroundDirty = True + self.HitColorGenerator = None + self.UseHitTest = False + if ResetBB: + self._ResetBoundingBox() + self.MakeNewBuffers() + self.HitDict = None + +## No longer called +## def _AddBoundingBox(self,NewBB): +## if self.BoundingBox is None: +## self.BoundingBox = NewBB +## self.ZoomToBB(NewBB,DrawFlag = False) +## else: +## self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]), +## min(self.BoundingBox[0,1],NewBB[0,1])), +## (max(self.BoundingBox[1,0],NewBB[1,0]), +## max(self.BoundingBox[1,1],NewBB[1,1]))), +## Float) + + def _getboundingbox(bboxarray): # lrk: added this + + upperleft = minimum.reduce(bboxarray[:,0]) + lowerright = maximum.reduce(bboxarray[:,1]) + return array((upperleft, lowerright), Float) + + _getboundingbox = staticmethod(_getboundingbox) + + def _ResetBoundingBox(self): + if self._DrawList or self._ForeDrawList: + bboxarray = zeros((len(self._DrawList)+len(self._ForeDrawList), 2, 2),Float) + i = -1 # just in case _DrawList is empty + for (i, BB) in enumerate(self._DrawList): + bboxarray[i] = BB.BoundingBox + for (j, BB) in enumerate(self._ForeDrawList): + bboxarray[i+j+1] = BB.BoundingBox + self.BoundingBox = self._getboundingbox(bboxarray) + else: + self.BoundingBox = None + self.ViewPortCenter= array( (0,0), Float) + self.TransformVector = array( (1,-1), Float) + self.MapProjectionVector = array( (1,1), 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 (((asarray(Points,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 asarray is needed. + return (((asarray(Coordinates,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 ( (asarray(Lengths,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 (asarray(Lengths,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 _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)): + Object._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) + if i % 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 _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__ + classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon", + "Line", "Text", "PointSet"] + 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() + +