0b0849b5a5
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@46083 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
2921 lines
104 KiB
Python
2921 lines
104 KiB
Python
|
|
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()
|
|
|
|
|