+++ /dev/null
-
-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()
-
-