2458faeb60
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@37211 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
2720 lines
102 KiB
Python
2720 lines
102 KiB
Python
from __future__ import division
|
|
|
|
try:
|
|
from Numeric import array,asarray,Float,cos, sin, pi,sum,minimum,maximum,Int32,zeros, ones, concatenate, sqrt, argmin, power, absolute, matrixmultiply, transpose, sometrue, arange, hypot
|
|
except ImportError:
|
|
try:
|
|
from numarray import array, asarray, Float, cos, sin, pi, sum, minimum, maximum, Int32, zeros, concatenate, matrixmultiply, transpose, sometrue, arange, hypot
|
|
except ImportError:
|
|
raise ImportError("I could not import either Numeric or numarray")
|
|
|
|
from time import clock, sleep
|
|
|
|
import Resources # A file with icons, etc for FloatCanvas
|
|
|
|
import wx
|
|
|
|
import types
|
|
import os
|
|
|
|
## 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 ScreenPPI
|
|
|
|
## a custom Exceptions:
|
|
|
|
class FloatCanvasError(Exception):
|
|
pass
|
|
|
|
## Create all the mouse events
|
|
# I don't see a need for these two, but maybe some day!
|
|
#EVT_FC_ENTER_WINDOW = wx.NewEventType()
|
|
#EVT_FC_LEAVE_WINDOW = wx.NewEventType()
|
|
EVT_FC_LEFT_DOWN = wx.NewEventType()
|
|
EVT_FC_LEFT_UP = wx.NewEventType()
|
|
EVT_FC_LEFT_DCLICK = wx.NewEventType()
|
|
EVT_FC_MIDDLE_DOWN = wx.NewEventType()
|
|
EVT_FC_MIDDLE_UP = wx.NewEventType()
|
|
EVT_FC_MIDDLE_DCLICK = wx.NewEventType()
|
|
EVT_FC_RIGHT_DOWN = wx.NewEventType()
|
|
EVT_FC_RIGHT_UP = wx.NewEventType()
|
|
EVT_FC_RIGHT_DCLICK = wx.NewEventType()
|
|
EVT_FC_MOTION = wx.NewEventType()
|
|
EVT_FC_MOUSEWHEEL = wx.NewEventType()
|
|
## these two are for the hit-test stuff, I never make them real Events
|
|
EVT_FC_ENTER_OBJECT = wx.NewEventType()
|
|
EVT_FC_LEAVE_OBJECT = wx.NewEventType()
|
|
|
|
##Create all mouse event binding functions
|
|
#def EVT_ENTER_WINDOW( window, function ):
|
|
# window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function )
|
|
#def EVT_LEAVE_WINDOW( window, function ):
|
|
# window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function )
|
|
def EVT_LEFT_DOWN( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_LEFT_DOWN , function )
|
|
def EVT_LEFT_UP( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_LEFT_UP , function )
|
|
def EVT_LEFT_DCLICK ( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_LEFT_DCLICK , function )
|
|
def EVT_MIDDLE_DOWN ( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_MIDDLE_DOWN , function )
|
|
def EVT_MIDDLE_UP ( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_MIDDLE_UP , function )
|
|
def EVT_MIDDLE_DCLICK ( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK , function )
|
|
def EVT_RIGHT_DOWN ( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_RIGHT_DOWN , function )
|
|
def EVT_RIGHT_UP( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_RIGHT_UP , function )
|
|
def EVT_RIGHT_DCLICK( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_RIGHT_DCLICK , function )
|
|
def EVT_MOTION( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_MOTION , function )
|
|
def EVT_MOUSEWHEEL( window, function ):
|
|
window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function )
|
|
|
|
class _MouseEvent(wx.PyCommandEvent):
|
|
|
|
"""
|
|
|
|
This event class takes a regular wxWindows mouse event as a parameter,
|
|
and wraps it so that there is access to all the original methods. This
|
|
is similar to subclassing, but you can't subclass a wxWindows event
|
|
|
|
The goal is to be able to it just like a regular mouse event.
|
|
|
|
It adds the method:
|
|
|
|
GetCoords() , which returns and (x,y) tuple in world coordinates.
|
|
|
|
Another 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
|
|
|
|
# I don't think this is used.
|
|
# def SetCoords(self,Coords):
|
|
# self.Coords = Coords
|
|
|
|
def GetCoords(self):
|
|
return self.Coords
|
|
|
|
def __getattr__(self, name):
|
|
#return eval(self.NativeEvent.__getattr__(name) )
|
|
return getattr(self._NativeEvent, name)
|
|
|
|
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 seris of unique colors used to do hit-tests with the HIt
|
|
Test bitmap
|
|
|
|
"""
|
|
import sys
|
|
if sys.platform == 'darwin':
|
|
depth = 24
|
|
else:
|
|
b = wx.EmptyBitmap(1,1)
|
|
depth = b.GetDepth()
|
|
if depth == 16:
|
|
step = 8
|
|
elif depth >= 24:
|
|
step = 1
|
|
else:
|
|
raise "ColorGenerator does not work with depth = %s" % depth
|
|
return _cycleidxs(indexcount=3, maxvalue=256, step=step)
|
|
|
|
|
|
#### I don't know if the Set objects are useful, beyond the pointset
|
|
#### object. The problem is that when zoomed in, the BB is checked to see
|
|
#### whether to draw the object. A Set object can defeat this. One day
|
|
#### I plan to write some custon C++ code to draw sets of objects
|
|
|
|
##class ObjectSetMixin:
|
|
## """
|
|
## A mix-in class for draw objects that are sets of objects
|
|
|
|
## It contains methods for setting lists of pens and brushes
|
|
|
|
## """
|
|
## def SetPens(self,LineColors,LineStyles,LineWidths):
|
|
## """
|
|
## This method used when an object could have a list of pens, rather than just one
|
|
## It is used for LineSet, and perhaps others in the future.
|
|
|
|
## fixme: this should be in a mixin
|
|
|
|
## fixme: this is really kludgy, there has got to be a better way!
|
|
|
|
## """
|
|
|
|
## length = 1
|
|
## if type(LineColors) == types.ListType:
|
|
## length = len(LineColors)
|
|
## else:
|
|
## LineColors = [LineColors]
|
|
|
|
## if type(LineStyles) == types.ListType:
|
|
## length = len(LineStyles)
|
|
## else:
|
|
## LineStyles = [LineStyles]
|
|
|
|
## if type(LineWidths) == types.ListType:
|
|
## length = len(LineWidths)
|
|
## else:
|
|
## LineWidths = [LineWidths]
|
|
|
|
## if length > 1:
|
|
## if len(LineColors) == 1:
|
|
## LineColors = LineColors*length
|
|
## if len(LineStyles) == 1:
|
|
## LineStyles = LineStyles*length
|
|
## if len(LineWidths) == 1:
|
|
## LineWidths = LineWidths*length
|
|
|
|
## self.Pens = []
|
|
## for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths):
|
|
## if LineColor is None or LineStyle is None:
|
|
## self.Pens.append(wx.TRANSPARENT_PEN)
|
|
## # what's this for?> self.LineStyle = 'Transparent'
|
|
## if not self.PenList.has_key((LineColor,LineStyle,LineWidth)):
|
|
## Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle])
|
|
## self.Pens.append(Pen)
|
|
## else:
|
|
## self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)])
|
|
## if length == 1:
|
|
## self.Pens = self.Pens[0]
|
|
|
|
class DrawObject:
|
|
"""
|
|
This is the base class for all the objects that can be drawn.
|
|
|
|
One must subclass from this (and an assortment of Mixins) to create
|
|
a new DrawObject.
|
|
|
|
"""
|
|
|
|
def __init__(self, InForeground = False, IsVisible = True):
|
|
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 Bind(self, Event, CallBackFun):
|
|
self.CallBackFuncs[Event] = CallBackFun
|
|
self.HitAble = True
|
|
self._Canvas.UseHitTest = True
|
|
if not self._Canvas._HTdc:
|
|
self._Canvas.MakeNewHTdc()
|
|
if not self.HitColor:
|
|
if not self._Canvas.HitColorGenerator:
|
|
self._Canvas.HitColorGenerator = _colorGenerator()
|
|
self._Canvas.HitColorGenerator.next() # first call to prevent the background color from being used.
|
|
self.HitColor = self._Canvas.HitColorGenerator.next()
|
|
self.SetHitPen(self.HitColor,self.HitLineWidth)
|
|
self.SetHitBrush(self.HitColor)
|
|
# put the object in the hit dict, indexed by it's color
|
|
if not self._Canvas.HitDict:
|
|
self._Canvas.MakeHitDict()
|
|
self._Canvas.HitDict[Event][self.HitColor] = (self) # put the object in the hit dict, indexed by it's color
|
|
|
|
def UnBindAll(self):
|
|
## fixme: this only removes one from each list, there could be more.
|
|
if self._Canvas.HitDict:
|
|
for List in self._Canvas.HitDict.itervalues():
|
|
try:
|
|
List.remove(self)
|
|
except ValueError:
|
|
pass
|
|
self.HitAble = False
|
|
|
|
|
|
def SetBrush(self,FillColor,FillStyle):
|
|
if FillColor is None or FillStyle is None:
|
|
self.Brush = wx.TRANSPARENT_BRUSH
|
|
##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"]) )
|
|
|
|
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):
|
|
self.Visible = False
|
|
|
|
def Show(self):
|
|
self.Visible = True
|
|
|
|
class ColorOnlyMixin:
|
|
"""
|
|
|
|
Mixin class for objects that have just one color, rather than a fill
|
|
color and line color
|
|
|
|
"""
|
|
|
|
def SetColor(self, Color):
|
|
self.SetPen(Color,"Solid",1)
|
|
self.SetBrush(Color,"Solid")
|
|
|
|
SetFillColor = SetColor # Just to provide a consistant interface
|
|
|
|
class LineOnlyMixin:
|
|
"""
|
|
|
|
Mixin class for objects that have just one color, rather than a fill
|
|
color and line color
|
|
|
|
"""
|
|
|
|
def SetLineColor(self, LineColor):
|
|
self.LineColor = LineColor
|
|
self.SetPen(LineColor,self.LineStyle,self.LineWidth)
|
|
|
|
def SetLineStyle(self, LineStyle):
|
|
self.LineStyle = LineStyle
|
|
self.SetPen(self.LineColor,LineStyle,self.LineWidth)
|
|
|
|
def SetLineWidth(self, LineWidth):
|
|
self.LineWidth = LineWidth
|
|
self.SetPen(self.LineColor,self.LineStyle,LineWidth)
|
|
|
|
class LineAndFillMixin(LineOnlyMixin):
|
|
"""
|
|
|
|
Mixin class for objects that have both a line and a fill color and
|
|
style.
|
|
|
|
"""
|
|
def SetFillColor(self, FillColor):
|
|
self.FillColor = FillColor
|
|
self.SetBrush(FillColor, self.FillStyle)
|
|
|
|
def SetFillStyle(self, FillStyle):
|
|
self.FillStyle = FillStyle
|
|
self.SetBrush(self.FillColor,FillStyle)
|
|
|
|
class XYObjectMixin:
|
|
"""
|
|
|
|
This is a mixin class that provides some methods suitable for use
|
|
with objects that have a single (x,y) coordinate pair.
|
|
|
|
"""
|
|
|
|
def Move(self, Delta ):
|
|
"""
|
|
|
|
Move(Delta): moves the object by delta, where delta is a
|
|
(dx,dy) pair. Ideally a Numpy array of shape (2,)
|
|
|
|
"""
|
|
|
|
Delta = asarray(Delta, Float)
|
|
self.XY += Delta
|
|
self.BoundingBox = self.BoundingBox + Delta
|
|
|
|
if self._Canvas:
|
|
self._Canvas.BoundingBoxDirty = True
|
|
|
|
def CalcBoundingBox(self):
|
|
## This may get overwritten in some subclasses
|
|
self.BoundingBox = array( (self.XY, self.XY), Float )
|
|
|
|
def SetPoint(self, xy):
|
|
xy = array( xy, Float)
|
|
xy.shape = (2,)
|
|
Delta = xy - self.XY
|
|
|
|
self.XY = xy
|
|
self.BoundingBox = self.BoundingBox + Delta
|
|
|
|
#self.CalcBoundingBox()
|
|
if self._Canvas:
|
|
self._Canvas.BoundingBoxDirty = True
|
|
|
|
class PointsObjectMixin:
|
|
"""
|
|
|
|
This is a mixin class that provides some methods suitable for use
|
|
with objects that have a set of (x,y) coordinate pairs.
|
|
|
|
"""
|
|
|
|
|
|
## This is code for the PointsObjectMixin object, it needs to be adapted and tested.
|
|
## Is the neccesary at all: you can always do:
|
|
## Object.SetPoints( Object.Points + delta, copy = False)
|
|
## def Move(self, Delta ):
|
|
## """
|
|
|
|
## Move(Delta): moves the object by delta, where delta is an (dx,
|
|
## dy) pair. Ideally a Numpy array of shape (2,)
|
|
|
|
## """
|
|
|
|
## Delta = array(Delta, Float)
|
|
## self.XY += Delta
|
|
## self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float)
|
|
## if self._Canvas:
|
|
## self._Canvas.BoundingBoxDirty = True
|
|
|
|
def CalcBoundingBox(self):
|
|
self.BoundingBox = array(((min(self.Points[:,0]),
|
|
min(self.Points[:,1]) ),
|
|
(max(self.Points[:,0]),
|
|
max(self.Points[:,1]) ) ), Float )
|
|
if self._Canvas:
|
|
self._Canvas.BoundingBoxDirty = True
|
|
|
|
def SetPoints(self, Points, copy = True):
|
|
"""
|
|
Sets the coordinates of the points of the object to Points (NX2 array).
|
|
|
|
By default, a copy is made, if copy is set to False, a reference
|
|
is used, iff Points is a NumPy array of Floats. This allows you
|
|
to change some or all of the points without making any copies.
|
|
|
|
For example:
|
|
|
|
Points = Object.Points
|
|
Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir.
|
|
Object.SetPoints(Points, False) # Sets the points to the same array as it was
|
|
|
|
"""
|
|
if copy:
|
|
self.Points = array(Points, Float)
|
|
self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point
|
|
else:
|
|
self.Points = asarray(Points, Float)
|
|
self.CalcBoundingBox()
|
|
|
|
|
|
class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin):
|
|
|
|
"""
|
|
|
|
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 = array(Points,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 PolygonSet(DrawObject):
|
|
## """
|
|
## The PolygonSet class takes a Geometry.Polygon object.
|
|
## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
|
|
|
|
## it creates a set of line segments, from (x1,y1) to (x2,y2)
|
|
|
|
## """
|
|
|
|
## def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False):
|
|
## DrawObject.__init__(self, InForeground)
|
|
|
|
## ##fixme: there should be some error checking for everything being the right length.
|
|
|
|
|
|
## self.Points = array(Points,Float)
|
|
## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
|
|
|
|
## self.LineColors = LineColors
|
|
## self.LineStyles = LineStyles
|
|
## self.LineWidths = LineWidths
|
|
## self.FillColors = FillColors
|
|
## self.FillStyles = FillStyles
|
|
|
|
## self.SetPens(LineColors,LineStyles,LineWidths)
|
|
|
|
## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
|
|
## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
## Points = WorldToPixel(self.Points)
|
|
## Points.shape = (-1,4)
|
|
## dc.DrawLineList(Points,self.Pens)
|
|
|
|
|
|
class Line(DrawObject,PointsObjectMixin,LineOnlyMixin):
|
|
"""
|
|
|
|
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 = array(Points,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 Arrow(DrawObject,XYObjectMixin,LineOnlyMixin):
|
|
"""
|
|
|
|
Arrow(XY, # coords of origin of arrow (x,y)
|
|
Length, # length of arrow in pixels
|
|
theta, # angle of arrow in degrees: zero is straight up
|
|
# angle is to the right
|
|
LineColor = "Black",
|
|
LineStyle = "Solid",
|
|
LineWidth = 1,
|
|
ArrowHeadSize = 4,
|
|
ArrowHeadAngle = 45,
|
|
InForeground = False):
|
|
|
|
It will draw an arrow , starting at the point, (X,Y) pointing in
|
|
direction, theta.
|
|
|
|
|
|
"""
|
|
def __init__(self,
|
|
XY,
|
|
Length,
|
|
Direction,
|
|
LineColor = "Black",
|
|
LineStyle = "Solid",
|
|
LineWidth = 2, # pixels
|
|
ArrowHeadSize = 8, # pixels
|
|
ArrowHeadAngle = 30, # degrees
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self, InForeground)
|
|
|
|
self.XY = array(XY, Float)
|
|
self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
|
|
self.Length = Length
|
|
self.Direction = float(Direction)
|
|
self.ArrowHeadSize = ArrowHeadSize
|
|
self.ArrowHeadAngle = float(ArrowHeadAngle)
|
|
|
|
self.CalcArrowPoints()
|
|
self.CalcBoundingBox()
|
|
|
|
self.LineColor = LineColor
|
|
self.LineStyle = LineStyle
|
|
self.LineWidth = LineWidth
|
|
|
|
self.SetPen(LineColor,LineStyle,LineWidth)
|
|
|
|
##fixme: How should the HitTest be drawn?
|
|
self.HitLineWidth = max(LineWidth,self.MinHitLineWidth)
|
|
|
|
def SetDirection(self, Direction):
|
|
self.Direction = float(Direction)
|
|
self.CalcArrowPoints()
|
|
|
|
def SetLength(self, Length):
|
|
self.Length = Length
|
|
self.CalcArrowPoints()
|
|
|
|
def SetLengthDirection(self, Length, Direction):
|
|
self.Direction = float(Direction)
|
|
self.Length = Length
|
|
self.CalcArrowPoints()
|
|
|
|
def SetLength(self, Length):
|
|
self.Length = Length
|
|
self.CalcArrowPoints()
|
|
|
|
## fixme: cache this?
|
|
def CalcArrowPoints(self):
|
|
L = self.Length
|
|
S = self.ArrowHeadSize
|
|
phi = self.ArrowHeadAngle * pi / 360
|
|
theta = (self.Direction-90.0) * pi / 180
|
|
ArrowPoints = array( ( (0, L, L - S*cos(phi),L, L - S*cos(phi) ),
|
|
(0, 0, S*sin(phi), 0, -S*sin(phi) ) ),
|
|
Float )
|
|
RotationMatrix = array( ( ( cos(theta), -sin(theta) ),
|
|
( sin(theta), cos(theta) ) ),
|
|
Float
|
|
)
|
|
ArrowPoints = matrixmultiply(RotationMatrix, ArrowPoints)
|
|
self.ArrowPoints = transpose(ArrowPoints)
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
dc.SetPen(self.Pen)
|
|
xy = WorldToPixel(self.XY)
|
|
ArrowPoints = xy + self.ArrowPoints
|
|
dc.DrawLines(ArrowPoints)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.DrawLines(ArrowPoints)
|
|
|
|
##class LineSet(DrawObject, ObjectSetMixin):
|
|
## """
|
|
## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates.
|
|
## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number!
|
|
|
|
## it creates a set of line segments, from (x1,y1) to (x2,y2)
|
|
|
|
## """
|
|
|
|
## def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False):
|
|
## DrawObject.__init__(self, InForeground)
|
|
|
|
## NumLines = len(Points) / 2
|
|
## ##fixme: there should be some error checking for everything being the right length.
|
|
|
|
|
|
## self.Points = array(Points,Float)
|
|
## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float)
|
|
|
|
## self.LineColors = LineColors
|
|
## self.LineStyles = LineStyles
|
|
## self.LineWidths = LineWidths
|
|
|
|
## self.SetPens(LineColors,LineStyles,LineWidths)
|
|
|
|
## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel):
|
|
## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
## Points = WorldToPixel(self.Points)
|
|
## Points.shape = (-1,4)
|
|
## dc.DrawLineList(Points,self.Pens)
|
|
|
|
class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin):
|
|
"""
|
|
|
|
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 = array(Points,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 = self.MinHitLineWidth
|
|
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 argmin(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 = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 )
|
|
dc.DrawEllipseList(xywh)
|
|
else:
|
|
for xy in Points:
|
|
dc.DrawCircle(xy[0],xy[1], radius)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
if self.Diameter <= 1:
|
|
HTdc.DrawPointList(Points)
|
|
elif self.Diameter <= 2:
|
|
self.DrawD2(HTdc, Points)
|
|
else:
|
|
if len(Points) > 100:
|
|
xy = Points
|
|
xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 )
|
|
HTdc.DrawEllipseList(xywh)
|
|
else:
|
|
for xy in Points:
|
|
HTdc.DrawCircle(xy[0],xy[1], radius)
|
|
|
|
class Point(DrawObject,XYObjectMixin,ColorOnlyMixin):
|
|
"""
|
|
|
|
The Point class takes a 2-tuple, or a (2,) NumPy array of point
|
|
coordinates.
|
|
|
|
The Diameter is in screen points, not world coordinates, So the
|
|
Bounding box is just the point, and doesn't include the Diameter.
|
|
|
|
The HitLineWidth is used as diameter for the
|
|
Hit Test.
|
|
|
|
"""
|
|
def __init__(self, XY, Color = "Black", Diameter = 1, InForeground = False):
|
|
DrawObject.__init__(self, InForeground)
|
|
|
|
self.XY = array(XY, Float)
|
|
self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
|
|
self.CalcBoundingBox()
|
|
self.SetColor(Color)
|
|
self.Diameter = Diameter
|
|
|
|
self.HitLineWidth = self.MinHitLineWidth
|
|
|
|
def SetDiameter(self,Diameter):
|
|
self.Diameter = Diameter
|
|
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
dc.SetPen(self.Pen)
|
|
xy = WorldToPixel(self.XY)
|
|
if self.Diameter <= 1:
|
|
dc.DrawPoint(xy[0], xy[1])
|
|
else:
|
|
dc.SetBrush(self.Brush)
|
|
radius = int(round(self.Diameter/2))
|
|
dc.DrawCircle(xy[0],xy[1], radius)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
if self.Diameter <= 1:
|
|
HTdc.DrawPoint(xy[0], xy[1])
|
|
else:
|
|
HTdc.SetBrush(self.HitBrush)
|
|
HTdc.DrawCircle(xy[0],xy[1], radius)
|
|
|
|
class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin):
|
|
"""
|
|
|
|
The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point
|
|
coordinates. It produces a square dot, centered on Point
|
|
|
|
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 = array(Point, Float)
|
|
self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point
|
|
self.CalcBoundingBox()
|
|
self.SetColor(Color)
|
|
self.Size = Size
|
|
|
|
self.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(DrawObject, XYObjectMixin, LineAndFillMixin):
|
|
def __init__(self, XY, WH,
|
|
LineColor = "Black",
|
|
LineStyle = "Solid",
|
|
LineWidth = 1,
|
|
FillColor = None,
|
|
FillStyle = "Solid",
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
self.XY = array( XY, Float)
|
|
self.XY.shape = (2,)
|
|
self.WH = array( WH, Float )
|
|
self.WH.shape = (2,)
|
|
self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float)
|
|
self.LineColor = LineColor
|
|
self.LineStyle = LineStyle
|
|
self.LineWidth = LineWidth
|
|
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 = array( XY, Float)
|
|
self.WH = array( WH, Float )
|
|
self.CalcBoundingBox()
|
|
|
|
|
|
def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc):
|
|
dc.SetPen(self.Pen)
|
|
dc.SetBrush(self.Brush)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
return ( WorldToPixel(self.XY),
|
|
ScaleWorldToPixel(self.WH) )
|
|
|
|
def CalcBoundingBox(self):
|
|
self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float)
|
|
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):
|
|
|
|
def __init__(self, XY, Diameter, **kwargs):
|
|
self.Center = array(XY, 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 = 12 # font size used for calculating layout
|
|
|
|
def SetFont(self, Size, Family, Style, Weight, Underline, FaceName):
|
|
self.Font = self.FontList.setdefault( (Size,
|
|
Family,
|
|
Style,
|
|
Weight,
|
|
Underline,
|
|
FaceName),
|
|
wx.Font(Size,
|
|
Family,
|
|
Style,
|
|
Weight,
|
|
Underline,
|
|
FaceName) )
|
|
return self.Font
|
|
|
|
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(DrawObject, TextObjectMixin):
|
|
"""
|
|
This class creates a text object, placed at the coordinates,
|
|
x,y. the "Position" argument is a two charactor string, indicating
|
|
where in relation to the coordinates the string should be oriented.
|
|
|
|
The first letter is: t, c, or b, for top, center and bottom The
|
|
second letter is: l, c, or r, for left, center and right The
|
|
position refers to the position relative to the text itself. It
|
|
defaults to "tl" (top left).
|
|
|
|
Size is the size of the font in pixels, or in points for printing
|
|
(if it ever gets implimented). Those will be the same, If you assume
|
|
72 PPI.
|
|
|
|
Family:
|
|
Font family, a generic way of referring to fonts without
|
|
specifying actual facename. One of:
|
|
wx.DEFAULT: Chooses a default font.
|
|
wx.DECORATIVE: A decorative font.
|
|
wx.ROMAN: A formal, serif font.
|
|
wx.SCRIPT: A handwriting font.
|
|
wx.SWISS: A sans-serif font.
|
|
wx.MODERN: A fixed pitch font.
|
|
NOTE: these are only as good as the wxWindows defaults, which aren't so good.
|
|
Style:
|
|
One of wx.NORMAL, wx.SLANT and wx.ITALIC.
|
|
Weight:
|
|
One of wx.NORMAL, wx.LIGHT and wx.BOLD.
|
|
Underline:
|
|
The value can be True or False. At present this may have an an
|
|
effect on Windows only.
|
|
|
|
Alternatively, you can set the kw arg: Font, to a wx.Font, and the
|
|
above will be ignored.
|
|
|
|
The size is fixed, and does not scale with the drawing.
|
|
|
|
The hit-test is done on the entire text extent
|
|
|
|
"""
|
|
|
|
def __init__(self,String, xy,
|
|
Size = 12,
|
|
Color = "Black",
|
|
BackgroundColor = None,
|
|
Family = wx.MODERN,
|
|
Style = wx.NORMAL,
|
|
Weight = wx.NORMAL,
|
|
Underline = False,
|
|
Position = 'tl',
|
|
InForeground = False,
|
|
Font = None):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
self.String = String
|
|
# Input size in in Pixels, compute points size from PPI info.
|
|
# fixme: for printing, we'll have to do something a little different
|
|
self.Size = int(round(72.0 * Size / ScreenPPI))
|
|
|
|
self.Color = Color
|
|
self.BackgroundColor = BackgroundColor
|
|
|
|
if not Font:
|
|
FaceName = ''
|
|
else:
|
|
FaceName = Font.GetFaceName()
|
|
Family = Font.GetFamily()
|
|
Size = Font.GetPointSize()
|
|
Style = Font.GetStyle()
|
|
Underlined = Font.GetUnderlined()
|
|
Weight = Font.GetWeight()
|
|
self.SetFont(Size, Family, Style, Weight, Underline, FaceName)
|
|
|
|
self.BoundingBox = array((xy, xy),Float)
|
|
|
|
self.XY = 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(DrawObject, TextObjectMixin):
|
|
"""
|
|
This class creates a text object that is scaled when zoomed. It is
|
|
placed at the coordinates, x,y. the "Position" argument is a two
|
|
charactor string, indicating where in relation to the coordinates
|
|
the string should be oriented.
|
|
|
|
The first letter is: t, c, or b, for top, center and bottom The
|
|
second letter is: l, c, or r, for left, center and right The
|
|
position refers to the position relative to the text itself. It
|
|
defaults to "tl" (top left).
|
|
|
|
Size is the size of the font in world coordinates.
|
|
|
|
Family:
|
|
Font family, a generic way of referring to fonts without
|
|
specifying actual facename. One of:
|
|
wx.DEFAULT: Chooses a default font.
|
|
wx.DECORATI: A decorative font.
|
|
wx.ROMAN: A formal, serif font.
|
|
wx.SCRIPT: A handwriting font.
|
|
wx.SWISS: A sans-serif font.
|
|
wx.MODERN: A fixed pitch font.
|
|
NOTE: these are only as good as the wxWindows defaults, which aren't so good.
|
|
Style:
|
|
One of wx.NORMAL, wx.SLANT and wx.ITALIC.
|
|
Weight:
|
|
One of wx.NORMAL, wx.LIGHT and wx.BOLD.
|
|
Underline:
|
|
The value can be True or False. At present this may have an an
|
|
effect on Windows only.
|
|
|
|
Alternatively, you can set the kw arg: Font, to a wx.Font, and the
|
|
above will be ignored. The size of the font you specify will be
|
|
ignored, but the rest of 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,
|
|
Underline = False,
|
|
Position = 'tl',
|
|
Font = None,
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
self.String = String
|
|
self.XY = array( XY, Float)
|
|
self.XY.shape = (2,)
|
|
self.Size = Size
|
|
self.Color = Color
|
|
self.BackgroundColor = BackgroundColor
|
|
self.Family = Family
|
|
self.Style = Style
|
|
self.Weight = Weight
|
|
self.Underline = Underline
|
|
if not Font:
|
|
self.FaceName = ''
|
|
else:
|
|
self.FaceName = Font.GetFaceName()
|
|
self.Family = Font.GetFamily()
|
|
self.Style = Font.GetStyle()
|
|
self.Underlined = Font.GetUnderlined()
|
|
self.Weight = Font.GetWeight()
|
|
|
|
# Experimental max font size value on wxGTK2: this works OK on
|
|
# my system. If it's 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
|
|
dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
|
|
(w,h) = dc.GetTextExtent(self.String)
|
|
w = w * ScaleFactor
|
|
h = h * ScaleFactor
|
|
x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1)
|
|
self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
(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)
|
|
dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName))
|
|
dc.SetTextForeground(self.Color)
|
|
if self.BackgroundColor:
|
|
dc.SetBackgroundMode(wx.SOLID)
|
|
dc.SetTextBackground(self.BackgroundColor)
|
|
else:
|
|
dc.SetBackgroundMode(wx.TRANSPARENT)
|
|
(w,h) = dc.GetTextExtent(self.String)
|
|
# compute the shift, and adjust the coordinates, if neccesary
|
|
# This had to be put in here, because it changes with Zoom, as
|
|
# fonts don't scale exactly.
|
|
xy = self.ShiftFun(X, Y, w, h)
|
|
|
|
dc.DrawTextPoint(self.String, xy)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
HTdc.DrawRectanglePointSize(xy, (w, h) )
|
|
|
|
class ScaledTextBox(DrawObject, TextObjectMixin):
|
|
"""
|
|
This class creates a TextBox object that is scaled when zoomed. It is
|
|
placed at the coordinates, x,y.
|
|
|
|
If the Width parameter is defined, the text will be wrapped to the width given.
|
|
|
|
A Box can be drawn around the text, be specifying:
|
|
LineWidth and/or FillColor
|
|
|
|
A space(margin) can be put all the way around the text, be specifying:
|
|
the PadSize argument in world coordinates.
|
|
|
|
The spacing between lines can be adjusted with the:
|
|
LineSpacing argument.
|
|
|
|
The "Position" argument is a two character string, indicating where
|
|
in relation to the coordinates the Box should be oriented.
|
|
-The first letter is: t, c, or b, for top, center and bottom.
|
|
-The second letter is: l, c, or r, for left, center and right The
|
|
position refers to the position relative to the text itself. It
|
|
defaults to "tl" (top left).
|
|
|
|
Size is the size of the font in world coordinates.
|
|
|
|
Family:
|
|
Font family, a generic way of referring to fonts without
|
|
specifying actual facename. One of:
|
|
wx.DEFAULT: Chooses a default font.
|
|
wx.DECORATIVE: A decorative font.
|
|
wx.ROMAN: A formal, serif font.
|
|
wx.SCRIPT: A handwriting font.
|
|
wx.SWISS: A sans-serif font.
|
|
wx.MODERN: A fixed pitch font.
|
|
NOTE: these are only as good as the wxWindows defaults, which aren't so good.
|
|
Style:
|
|
One of wx.NORMAL, wx.SLANT and wx.ITALIC.
|
|
Weight:
|
|
One of wx.NORMAL, wx.LIGHT and wx.BOLD.
|
|
Underline:
|
|
The value can be True or False. At present this may have an an
|
|
effect on Windows only.
|
|
|
|
Alternatively, you can set the kw arg: Font, to a wx.Font, and the
|
|
above will be ignored. The size of the font you specify will be
|
|
ignored, but the rest of its attributes will be preserved.
|
|
|
|
The size will scale as the drawing is zoomed.
|
|
|
|
Bugs/Limitations:
|
|
|
|
As fonts are scaled, they do end up a little different, so you don't
|
|
get exactly the same picture as you scale up and down, but it's
|
|
pretty darn close.
|
|
|
|
On wxGTK1 on my Linux system, at least, using a font of over about
|
|
1000 pts. brings the system to a halt. It's the Font Server using
|
|
huge amounts of memory. My work around is to max the font size to
|
|
1000 points, so it won't scale past there. GTK2 uses smarter font
|
|
drawing, so that may not be an issue in future versions, so feel
|
|
free to test. Another smarter way to do it would be to set a global
|
|
zoom limit at that point.
|
|
|
|
The hit-test is done on the entire box. This could be made
|
|
optional, but I haven't gotten around to it.
|
|
|
|
"""
|
|
|
|
def __init__(self, String,
|
|
Point,
|
|
Size,
|
|
Color = "Black",
|
|
BackgroundColor = None,
|
|
LineColor = 'Black',
|
|
LineStyle = 'Solid',
|
|
LineWidth = 1,
|
|
Width = None,
|
|
PadSize = None,
|
|
Family = wx.MODERN,
|
|
Style = wx.NORMAL,
|
|
Weight = wx.NORMAL,
|
|
Underline = False,
|
|
Position = 'tl',
|
|
Alignment = "left",
|
|
Font = None,
|
|
LineSpacing = 1.0,
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
self.XY = array(Point, Float)
|
|
self.Size = Size
|
|
self.Color = Color
|
|
self.BackgroundColor = BackgroundColor
|
|
self.LineColor = LineColor
|
|
self.LineStyle = LineStyle
|
|
self.LineWidth = LineWidth
|
|
self.Width = Width
|
|
if PadSize is None: # the default is just a little bit of padding
|
|
self.PadSize = Size/10.0
|
|
else:
|
|
self.PadSize = float(PadSize)
|
|
self.Family = Family
|
|
self.Style = Style
|
|
self.Weight = Weight
|
|
self.Underline = Underline
|
|
self.Alignment = Alignment.lower()
|
|
self.LineSpacing = float(LineSpacing)
|
|
self.Position = Position
|
|
|
|
if not Font:
|
|
self.FaceName = ''
|
|
else:
|
|
self.FaceName = Font.GetFaceName()
|
|
self.Family = Font.GetFamily()
|
|
self.Style = Font.GetStyle()
|
|
self.Underlined = Font.GetUnderlined()
|
|
self.Weight = Font.GetWeight()
|
|
|
|
# Experimental max font size value on wxGTK2: this works OK on
|
|
# my system. If it's a lot larger, there is a crash, with the
|
|
# message:
|
|
#
|
|
# The application 'FloatCanvasDemo.py' lost its
|
|
# connection to the display :0.0; most likely the X server was
|
|
# shut down or you killed/destroyed the application.
|
|
#
|
|
# Windows and OS-X seem to be better behaved in this regard.
|
|
# They may not draw it, but they don't crash either!
|
|
|
|
self.MaxFontSize = 1000
|
|
self.ShiftFun = self.ShiftFunDict[Position]
|
|
|
|
self.String = String
|
|
self.LayoutText()
|
|
self.CalcBoundingBox()
|
|
|
|
self.SetPen(LineColor,LineStyle,LineWidth)
|
|
self.SetBrush(BackgroundColor, "Solid")
|
|
|
|
|
|
def WrapToWidth(self):
|
|
dc = wx.MemoryDC()
|
|
bitmap = wx.EmptyBitmap(1, 1)
|
|
dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
|
|
DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to.
|
|
ScaleFactor = float(self.Size) / DrawingSize
|
|
Width = (self.Width - 2*self.PadSize) / ScaleFactor #Width to wrap to
|
|
dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
|
|
|
|
NewStrings = []
|
|
for s in self.Strings:
|
|
#beginning = True
|
|
text = s.split(" ")
|
|
text.reverse()
|
|
LineLength = 0
|
|
NewText = text[-1]
|
|
del text[-1]
|
|
while text:
|
|
w = dc.GetTextExtent(' ' + text[-1])[0]
|
|
if LineLength + w <= Width:
|
|
NewText += ' '
|
|
NewText += text[-1]
|
|
LineLength = dc.GetTextExtent(NewText)[0]
|
|
else:
|
|
NewStrings.append(NewText)
|
|
NewText = text[-1]
|
|
LineLength = dc.GetTextExtent(text[-1])[0]
|
|
del text[-1]
|
|
NewStrings.append(NewText)
|
|
self.Strings = NewStrings
|
|
|
|
def ReWrap(self, Width):
|
|
self.Width = Width
|
|
self.LayoutText()
|
|
|
|
def LayoutText(self):
|
|
"""
|
|
|
|
Calculates the positions of the words of text.
|
|
|
|
This isn't exact, as fonts don't scale exactly.
|
|
To help this, the position of each individual word
|
|
is stored separately, so that the general layout stays
|
|
the same in world coordinates, as the fonts scale.
|
|
|
|
"""
|
|
self.Strings = self.String.split("\n")
|
|
if self.Width:
|
|
self.WrapToWidth()
|
|
|
|
dc = wx.MemoryDC()
|
|
bitmap = wx.EmptyBitmap(1, 1)
|
|
dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work.
|
|
|
|
DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to.
|
|
ScaleFactor = float(self.Size) / DrawingSize
|
|
|
|
dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) )
|
|
|
|
TextHeight = dc.GetTextExtent("X")[1]
|
|
SpaceWidth = dc.GetTextExtent(" ")[0]
|
|
LineHeight = TextHeight * self.LineSpacing
|
|
|
|
LineWidths = zeros((len(self.Strings),), Float)
|
|
y = 0
|
|
Words = []
|
|
AllLinePoints = []
|
|
|
|
for i, s in enumerate(self.Strings):
|
|
LineWidths[i] = 0
|
|
LineWords = s.split(" ")
|
|
LinePoints = zeros((len(LineWords),2), Float)
|
|
for j, word in enumerate(LineWords):
|
|
if j > 0:
|
|
LineWidths[i] += SpaceWidth
|
|
Words.append(word)
|
|
LinePoints[j] = (LineWidths[i], y)
|
|
w = dc.GetTextExtent(word)[0]
|
|
LineWidths[i] += w
|
|
y -= LineHeight
|
|
AllLinePoints.append(LinePoints)
|
|
TextWidth = maximum.reduce(LineWidths)
|
|
self.Words = Words
|
|
|
|
if self.Width is None:
|
|
BoxWidth = TextWidth * ScaleFactor + 2*self.PadSize
|
|
else: # use the defined Width
|
|
BoxWidth = self.Width
|
|
Points = zeros((0,2), Float)
|
|
|
|
for i, LinePoints in enumerate(AllLinePoints):
|
|
## Scale to World Coords.
|
|
LinePoints *= (ScaleFactor, ScaleFactor)
|
|
if self.Alignment == 'left':
|
|
LinePoints[:,0] += self.PadSize
|
|
elif self.Alignment == 'center':
|
|
LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor)/2.0
|
|
elif self.Alignment == 'right':
|
|
LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor-self.PadSize)
|
|
Points = concatenate((Points, LinePoints))
|
|
|
|
BoxHeight = -(Points[-1,1] - (TextHeight * ScaleFactor)) + 2*self.PadSize
|
|
(x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1)
|
|
Points += (0, -self.PadSize)
|
|
self.Points = Points
|
|
self.BoxWidth = BoxWidth
|
|
self.BoxHeight = BoxHeight
|
|
self.CalcBoundingBox()
|
|
|
|
def CalcBoundingBox(self):
|
|
|
|
"""
|
|
|
|
Calculates the Bounding Box
|
|
|
|
"""
|
|
|
|
w, h = self.BoxWidth, self.BoxHeight
|
|
x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world=1)
|
|
self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
|
|
|
|
def GetBoxRect(self):
|
|
wh = (self.BoxWidth, self.BoxHeight)
|
|
xy = (self.BoundingBox[0,0], self.BoundingBox[1,1])
|
|
|
|
return (xy, wh)
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
xy, wh = self.GetBoxRect()
|
|
|
|
Points = self.Points + xy
|
|
Points = WorldToPixel(Points)
|
|
xy = WorldToPixel(xy)
|
|
wh = ScaleWorldToPixel(wh) * (1,-1)
|
|
|
|
# compute the font size:
|
|
Size = abs( ScaleWorldToPixel( (self.Size, self.Size) )[1] ) # only need a y coordinate length
|
|
## Check to see if the font size is large enough to blow up the X font server
|
|
## If so, limit it. Would it be better just to not draw it?
|
|
## note that this limit is dependent on how much memory you have, etc.
|
|
Size = min(Size, self.MaxFontSize)
|
|
|
|
font = self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName)
|
|
dc.SetFont(font)
|
|
dc.SetTextForeground(self.Color)
|
|
dc.SetBackgroundMode(wx.TRANSPARENT)
|
|
|
|
# Draw The Box
|
|
if (self.LineStyle and self.LineColor) or self.BackgroundColor:
|
|
dc.SetBrush(self.Brush)
|
|
dc.SetPen(self.Pen)
|
|
dc.DrawRectanglePointSize(xy , wh)
|
|
|
|
# Draw the Text
|
|
dc.DrawTextList(self.Words, Points)
|
|
|
|
# Draw the hit box.
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
HTdc.DrawRectanglePointSize(xy, wh)
|
|
|
|
class Bitmap(DrawObject, TextObjectMixin):
|
|
"""
|
|
This class creates a bitmap object, placed at the coordinates,
|
|
x,y. the "Position" argument is a two charactor string, indicating
|
|
where in relation to the coordinates the bitmap should be oriented.
|
|
|
|
The first letter is: t, c, or b, for top, center and bottom The
|
|
second letter is: l, c, or r, for left, center and right The
|
|
position refers to the position relative to the text itself. It
|
|
defaults to "tl" (top left).
|
|
|
|
The size is fixed, and does not scale with the drawing.
|
|
|
|
"""
|
|
|
|
def __init__(self,Bitmap,XY,
|
|
Position = 'tl',
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
if type(Bitmap) == wx._gdi.Bitmap:
|
|
self.Bitmap = Bitmap
|
|
elif type(Bitmap) == wx._core.Image:
|
|
self.Bitmap = wx.BitmapFromImage(Bitmap)
|
|
|
|
# Note the BB is just the point, as the size in World coordinates is not fixed
|
|
self.BoundingBox = array((XY,XY),Float)
|
|
|
|
self.XY = XY
|
|
|
|
(self.Width, self.Height) = self.Bitmap.GetWidth(), self.Bitmap.GetHeight()
|
|
self.ShiftFun = self.ShiftFunDict[Position]
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
XY = WorldToPixel(self.XY)
|
|
XY = self.ShiftFun(XY[0], XY[1], self.Width, self.Height)
|
|
dc.DrawBitmapPoint(self.Bitmap, XY, True)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
HTdc.DrawRectanglePointSize(XY, (self.Width, self.Height) )
|
|
|
|
class ScaledBitmap(DrawObject, TextObjectMixin):
|
|
"""
|
|
|
|
This class creates a bitmap object, placed at the coordinates, XY,
|
|
of Height, H, in World coorsinates. The width is calculated from the
|
|
aspect ratio of the bitmap.
|
|
|
|
the "Position" argument is a two charactor string, indicating
|
|
where in relation to the coordinates the bitmap should be oriented.
|
|
|
|
The first letter is: t, c, or b, for top, center and bottom The
|
|
second letter is: l, c, or r, for left, center and right The
|
|
position refers to the position relative to the text itself. It
|
|
defaults to "tl" (top left).
|
|
|
|
The size scales with the drawing
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
Bitmap,
|
|
XY,
|
|
Height,
|
|
Position = 'tl',
|
|
InForeground = False):
|
|
|
|
DrawObject.__init__(self,InForeground)
|
|
|
|
if type(Bitmap) == wx._gdi.Bitmap:
|
|
self.Image = Bitmap.ConvertToImage()
|
|
elif type(Bitmap) == wx._core.Image:
|
|
self.Image = Bitmap
|
|
|
|
self.XY = XY
|
|
self.Height = Height
|
|
(self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight()
|
|
self.Width = self.bmpWidth / self.bmpHeight * Height
|
|
self.ShiftFun = self.ShiftFunDict[Position]
|
|
self.CalcBoundingBox()
|
|
self.ScaledBitmap = None
|
|
self.ScaledHeight = None
|
|
|
|
def CalcBoundingBox(self):
|
|
## this isn't exact, as fonts don't scale exactly.
|
|
w,h = self.Width, self.Height
|
|
x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1)
|
|
self.BoundingBox = array(((x, y-h ),(x + w, y)),Float)
|
|
|
|
def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
|
|
XY = WorldToPixel(self.XY)
|
|
H = ScaleWorldToPixel(self.Height)[0]
|
|
W = H * (self.bmpWidth / self.bmpHeight)
|
|
if (self.ScaledBitmap is None) or (H <> self.ScaledHeight) :
|
|
self.ScaledHeight = H
|
|
Img = self.Image.Scale(W, H)
|
|
self.ScaledBitmap = wx.BitmapFromImage(Img)
|
|
|
|
XY = self.ShiftFun(XY[0], XY[1], W, H)
|
|
dc.DrawBitmapPoint(self.ScaledBitmap, XY, True)
|
|
if HTdc and self.HitAble:
|
|
HTdc.SetPen(self.HitPen)
|
|
HTdc.SetBrush(self.HitBrush)
|
|
HTdc.DrawRectanglePointSize(XY, (W, H) )
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
class FloatCanvas(wx.Panel):
|
|
## fixme: could this be a wx.Window?
|
|
"""
|
|
FloatCanvas.py
|
|
|
|
This is a high level window for drawing maps and anything else in an
|
|
arbitrary coordinate system.
|
|
|
|
The goal is to provide a convenient way to draw stuff on the screen
|
|
without having to deal with handling OnPaint events, converting to pixel
|
|
coordinates, knowing about wxWindows brushes, pens, and colors, etc. It
|
|
also provides virtually unlimited zooming and scrolling
|
|
|
|
I am using it for two things:
|
|
1) general purpose drawing in floating point coordinates
|
|
2) displaying map data in Lat-long coordinates
|
|
|
|
If the projection is set to None, it will draw in general purpose
|
|
floating point coordinates. If the projection is set to 'FlatEarth', it
|
|
will draw a FlatEarth projection, centered on the part of the map that
|
|
you are viewing. You can also pass in your own projection function.
|
|
|
|
It is double buffered, so re-draws after the window is uncovered by something
|
|
else are very quick.
|
|
|
|
It relies on NumPy, which is needed for speed (maybe, I havn't profiled it)
|
|
|
|
Bugs and Limitations:
|
|
Lots: patches, fixes welcome
|
|
|
|
For Map drawing: It ignores the fact that the world is, in fact, a
|
|
sphere, so it will do strange things if you are looking at stuff near
|
|
the poles or the date line. so far I don't have a need to do that, so I
|
|
havn't bothered to add any checks for that yet.
|
|
|
|
Zooming:
|
|
I have set no zoom limits. What this means is that if you zoom in really
|
|
far, you can get integer overflows, and get wierd results. It
|
|
doesn't seem to actually cause any problems other than wierd output, at
|
|
least when I have run it.
|
|
|
|
Speed:
|
|
I have done a couple of things to improve speed in this app. The one
|
|
thing I have done is used NumPy Arrays to store the coordinates of the
|
|
points of the objects. This allowed me to use array oriented functions
|
|
when doing transformations, and should provide some speed improvement
|
|
for objects with a lot of points (big polygons, polylines, pointsets).
|
|
|
|
The real slowdown comes when you have to draw a lot of objects, because
|
|
you have to call the wx.DC.DrawSomething call each time. This is plenty
|
|
fast for tens of objects, OK for hundreds of objects, but pretty darn
|
|
slow for thousands of objects.
|
|
|
|
The solution is to be able to pass some sort of object set to the DC
|
|
directly. I've used DC.DrawPointList(Points), and it helped a lot with
|
|
drawing lots of points. I havn't got a LineSet type object, so I havn't
|
|
used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList()
|
|
methods implimented, and then I'd also have a full set of Object sets
|
|
that could take advantage of them. I hope to get to it some day.
|
|
|
|
Mouse Events:
|
|
|
|
At this point, there are a full set of custom mouse events. They are
|
|
just like the rebulsr mouse events, but include an extra attribute:
|
|
Event.GetCoords(), that returns the (x,y) position in world
|
|
coordinates, as a length-2 NumPy vector of Floats.
|
|
|
|
Copyright: Christopher Barker
|
|
|
|
License: Same as the version of wxPython you are using it with
|
|
|
|
Please let me know if you're using this!!!
|
|
|
|
Contact me at:
|
|
|
|
Chris.Barker@noaa.gov
|
|
|
|
"""
|
|
|
|
def __init__(self, parent, id = -1,
|
|
size = wx.DefaultSize,
|
|
ProjectionFun = None,
|
|
BackgroundColor = "WHITE",
|
|
Debug = False):
|
|
|
|
wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size)
|
|
|
|
global ScreenPPI ## A global variable to hold the Pixels per inch that wxWindows thinks is in use.
|
|
dc = wx.ScreenDC()
|
|
ScreenPPI = dc.GetPPI()[1] # Pixel height
|
|
del dc
|
|
|
|
self.HitColorGenerator = None
|
|
self.UseHitTest = None
|
|
|
|
self.NumBetweenBlits = 500
|
|
|
|
self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID)
|
|
|
|
self.Debug = Debug
|
|
|
|
wx.EVT_PAINT(self, self.OnPaint)
|
|
wx.EVT_SIZE(self, self.OnSize)
|
|
|
|
wx.EVT_LEFT_DOWN(self, self.LeftDownEvent )
|
|
wx.EVT_LEFT_UP(self, self.LeftUpEvent )
|
|
wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent )
|
|
wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent )
|
|
wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent )
|
|
wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent )
|
|
wx.EVT_RIGHT_DOWN(self, self.RightDownEvent)
|
|
wx.EVT_RIGHT_UP(self, self.RightUpEvent )
|
|
wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent )
|
|
wx.EVT_MOTION(self, self.MotionEvent )
|
|
wx.EVT_MOUSEWHEEL(self, self.WheelEvent )
|
|
|
|
## CHB: I'm leaving these out for now.
|
|
#wx.EVT_ENTER_WINDOW(self, self. )
|
|
#wx.EVT_LEAVE_WINDOW(self, self. )
|
|
|
|
## create the Hit Test Dicts:
|
|
self.HitDict = None
|
|
self._HTdc = None
|
|
|
|
self._DrawList = []
|
|
self._ForeDrawList = []
|
|
self._ForegroundBuffer = None
|
|
self.BoundingBox = None
|
|
self.BoundingBoxDirty = False
|
|
self.ViewPortCenter= array( (0,0), Float)
|
|
|
|
self.SetProjectionFun(ProjectionFun)
|
|
|
|
self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
|
|
self.TransformVector = array( (1,-1), Float) # default Transformation
|
|
|
|
self.Scale = 1
|
|
|
|
self.GUIMode = None
|
|
self.StartRBBox = None
|
|
self.PrevRBBox = None
|
|
self.StartMove = None
|
|
self.PrevMoveXY = None
|
|
self.ObjectUnderMouse = None
|
|
|
|
# called just to make sure everything is initialized
|
|
# this is a bug on OS-X, maybe it's not required?
|
|
self.SizeTimer = wx.PyTimer(self.OnSizeTimer) # timer to give a delay when re-sizing so that bufferes aren't re-built too many times.
|
|
|
|
self.InitializePanel()
|
|
self.MakeNewBuffers()
|
|
|
|
self.InHereNum = 0
|
|
|
|
self.CreateCursors()
|
|
|
|
def CreateCursors(self):
|
|
|
|
## create all the Cursors, so they don't need to be created each time.
|
|
##
|
|
if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac
|
|
self.HandCursor = wx.CursorFromImage(Resources.getHand16Image())
|
|
self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image())
|
|
|
|
img = Resources.getMagPlus16Image()
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
|
|
self.MagPlusCursor = wx.CursorFromImage(img)
|
|
|
|
img = Resources.getMagMinus16Image()
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6)
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6)
|
|
self.MagMinusCursor = wx.CursorFromImage(img)
|
|
else: # use 24X24 cursors for GTK and Windows
|
|
self.HandCursor = wx.CursorFromImage(Resources.getHandImage())
|
|
self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage())
|
|
|
|
img = Resources.getMagPlusImage()
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
|
|
self.MagPlusCursor = wx.CursorFromImage(img)
|
|
|
|
img = Resources.getMagMinusImage()
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9)
|
|
img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9)
|
|
self.MagMinusCursor = wx.CursorFromImage(img)
|
|
|
|
def SetProjectionFun(self,ProjectionFun):
|
|
if ProjectionFun == 'FlatEarth':
|
|
self.ProjectionFun = self.FlatEarthProjection
|
|
elif callable(ProjectionFun):
|
|
self.ProjectionFun = ProjectionFun
|
|
elif ProjectionFun is None:
|
|
self.ProjectionFun = lambda x=None: array( (1,1), 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):
|
|
return array((cos(pi*CenterPoint[1]/180),1),Float)
|
|
|
|
def SetMode(self,Mode):
|
|
if Mode in ["ZoomIn","ZoomOut","Move","Mouse", None]:
|
|
if Mode == "Move":
|
|
self.SetCursor(self.HandCursor)
|
|
elif Mode == "ZoomIn":
|
|
self.SetCursor(self.MagPlusCursor)
|
|
elif Mode == "ZoomOut":
|
|
self.SetCursor(self.MagMinusCursor)
|
|
else:
|
|
self.SetCursor(wx.NullCursor)
|
|
|
|
self.GUIMode = Mode
|
|
|
|
else:
|
|
raise FloatCanvasError('"%s" is Not a valid Mode'%Mode)
|
|
|
|
def MakeHitDict(self):
|
|
##fixme: Should this just be None if nothing has been bound?
|
|
self.HitDict = {EVT_FC_LEFT_DOWN: {},
|
|
EVT_FC_LEFT_UP: {},
|
|
EVT_FC_LEFT_DCLICK: {},
|
|
EVT_FC_MIDDLE_DOWN: {},
|
|
EVT_FC_MIDDLE_UP: {},
|
|
EVT_FC_MIDDLE_DCLICK: {},
|
|
EVT_FC_RIGHT_DOWN: {},
|
|
EVT_FC_RIGHT_UP: {},
|
|
EVT_FC_RIGHT_DCLICK: {},
|
|
EVT_FC_ENTER_OBJECT: {},
|
|
EVT_FC_LEAVE_OBJECT: {},
|
|
}
|
|
|
|
def _RaiseMouseEvent(self, Event, EventType):
|
|
"""
|
|
This is called in various other places to raise a Mouse Event
|
|
"""
|
|
#print "in Raise Mouse Event", Event
|
|
pt = self.PixelToWorld( Event.GetPosition() )
|
|
evt = _MouseEvent(EventType, Event, self.GetId(), pt)
|
|
self.GetEventHandler().ProcessEvent(evt)
|
|
|
|
def HitTest(self, event, HitEvent):
|
|
if self.HitDict:
|
|
# check if there are any objects in the dict for this event
|
|
if self.HitDict[ HitEvent ]:
|
|
xy = event.GetPosition()
|
|
if self._ForegroundHTdc:
|
|
hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
|
|
else:
|
|
hitcolor = self._HTdc.GetPixelPoint( xy )
|
|
color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
|
|
if color in self.HitDict[ HitEvent ]:
|
|
Object = self.HitDict[ HitEvent ][color]
|
|
## Add the hit coords to the Object
|
|
Object.HitCoords = self.PixelToWorld( xy )
|
|
Object.HitCoordsPixel = xy
|
|
Object.CallBackFuncs[HitEvent](Object)
|
|
return True
|
|
return False
|
|
|
|
def MouseOverTest(self, event):
|
|
##fixme: Can this be cleaned up?
|
|
if self.HitDict:
|
|
xy = event.GetPosition()
|
|
if self._ForegroundHTdc:
|
|
hitcolor = self._ForegroundHTdc.GetPixelPoint( xy )
|
|
else:
|
|
hitcolor = self._HTdc.GetPixelPoint( xy )
|
|
color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() )
|
|
OldObject = self.ObjectUnderMouse
|
|
ObjectCallbackCalled = False
|
|
if color in self.HitDict[ EVT_FC_ENTER_OBJECT ]:
|
|
Object = self.HitDict[ EVT_FC_ENTER_OBJECT][color]
|
|
if (OldObject is None):
|
|
try:
|
|
Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
|
|
ObjectCallbackCalled = True
|
|
except KeyError:
|
|
pass # this means the enter event isn't bound for that object
|
|
elif OldObject == Object: # the mouse is still on the same object
|
|
pass
|
|
## Is the mouse on a differnt object as it was...
|
|
elif not (Object == OldObject):
|
|
# call the leave object callback
|
|
try:
|
|
OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
|
|
ObjectCallbackCalled = True
|
|
except KeyError:
|
|
pass # this means the leave event isn't bound for that object
|
|
try:
|
|
Object.CallBackFuncs[EVT_FC_ENTER_OBJECT](Object)
|
|
ObjectCallbackCalled = True
|
|
except KeyError:
|
|
pass # this means the enter event isn't bound for that object
|
|
## set the new object under mouse
|
|
self.ObjectUnderMouse = Object
|
|
elif color in self.HitDict[ EVT_FC_LEAVE_OBJECT ]:
|
|
Object = self.HitDict[ EVT_FC_LEAVE_OBJECT][color]
|
|
self.ObjectUnderMouse = Object
|
|
else:
|
|
# no objects under mouse bound to mouse-over events
|
|
self.ObjectUnderMouse = None
|
|
if OldObject:
|
|
try:
|
|
OldObject.CallBackFuncs[EVT_FC_LEAVE_OBJECT](OldObject)
|
|
ObjectCallbackCalled = True
|
|
except KeyError:
|
|
pass # this means the leave event isn't bound for that object
|
|
return ObjectCallbackCalled
|
|
|
|
|
|
## fixme: There is a lot of repeated code here
|
|
## Is there a better way?
|
|
def LeftDoubleClickEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_LEFT_DCLICK
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def MiddleDownEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_MIDDLE_DOWN
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def MiddleUpEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_MIDDLE_UP
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def MiddleDoubleClickEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_MIDDLE_DCLICK
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def RightUpEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_RIGHT_UP
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def RightDoubleCLickEvent(self,event):
|
|
if self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_RIGHT_DCLICK
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
|
|
def WheelEvent(self,event):
|
|
##if self.GUIMode == "Mouse":
|
|
## Why not always raise this?
|
|
self._RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL)
|
|
|
|
|
|
def LeftDownEvent(self,event):
|
|
if self.GUIMode:
|
|
if self.GUIMode == "ZoomIn":
|
|
self.StartRBBox = array( event.GetPosition() )
|
|
self.PrevRBBox = None
|
|
self.CaptureMouse()
|
|
elif self.GUIMode == "ZoomOut":
|
|
Center = self.PixelToWorld( event.GetPosition() )
|
|
self.Zoom(1/1.5,Center)
|
|
elif self.GUIMode == "Move":
|
|
self.SetCursor(self.GrabHandCursor)
|
|
self.StartMove = array( event.GetPosition() )
|
|
self.PrevMoveXY = (0,0)
|
|
elif self.GUIMode == "Mouse":
|
|
## check for a hit
|
|
if not self.HitTest(event, EVT_FC_LEFT_DOWN):
|
|
self._RaiseMouseEvent(event,EVT_FC_LEFT_DOWN)
|
|
else:
|
|
pass
|
|
|
|
def LeftUpEvent(self,event):
|
|
if self.HasCapture():
|
|
self.ReleaseMouse()
|
|
if self.GUIMode:
|
|
if self.GUIMode == "ZoomIn":
|
|
if event.LeftUp() and not self.StartRBBox is None:
|
|
self.PrevRBBox = None
|
|
EndRBBox = event.GetPosition()
|
|
StartRBBox = self.StartRBBox
|
|
# if mouse has moved less that ten pixels, don't use the box.
|
|
if ( abs(StartRBBox[0] - EndRBBox[0]) > 10
|
|
and abs(StartRBBox[1] - EndRBBox[1]) > 10 ):
|
|
EndRBBox = self.PixelToWorld(EndRBBox)
|
|
StartRBBox = self.PixelToWorld(StartRBBox)
|
|
BB = array(((min(EndRBBox[0],StartRBBox[0]),
|
|
min(EndRBBox[1],StartRBBox[1])),
|
|
(max(EndRBBox[0],StartRBBox[0]),
|
|
max(EndRBBox[1],StartRBBox[1]))),Float)
|
|
self.ZoomToBB(BB)
|
|
else:
|
|
Center = self.PixelToWorld(StartRBBox)
|
|
self.Zoom(1.5,Center)
|
|
self.StartRBBox = None
|
|
elif self.GUIMode == "Move":
|
|
self.SetCursor(self.HandCursor)
|
|
if self.StartMove is not None:
|
|
StartMove = self.StartMove
|
|
EndMove = array((event.GetX(),event.GetY()))
|
|
if sum((StartMove-EndMove)**2) > 16:
|
|
self.MoveImage(StartMove-EndMove,'Pixel')
|
|
self.StartMove = None
|
|
elif self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_LEFT_UP
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
else:
|
|
pass
|
|
|
|
def MotionEvent(self,event):
|
|
if self.GUIMode:
|
|
if self.GUIMode == "ZoomIn":
|
|
if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None):
|
|
xy0 = self.StartRBBox
|
|
xy1 = array( event.GetPosition() )
|
|
wh = abs(xy1 - xy0)
|
|
wh[0] = max(wh[0], int(wh[1]*self.AspectRatio))
|
|
wh[1] = int(wh[0] / self.AspectRatio)
|
|
xy_c = (xy0 + xy1) / 2
|
|
dc = wx.ClientDC(self)
|
|
dc.BeginDrawing()
|
|
dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH))
|
|
dc.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
dc.SetLogicalFunction(wx.XOR)
|
|
if self.PrevRBBox:
|
|
dc.DrawRectanglePointSize(*self.PrevRBBox)
|
|
self.PrevRBBox = ( xy_c - wh/2, wh )
|
|
dc.DrawRectanglePointSize( *self.PrevRBBox )
|
|
dc.EndDrawing()
|
|
elif self.GUIMode == "Move":
|
|
if event.Dragging() and event.LeftIsDown() and not self.StartMove is None:
|
|
xy1 = array( event.GetPosition() )
|
|
wh = self.PanelSize
|
|
xy_tl = xy1 - self.StartMove
|
|
dc = wx.ClientDC(self)
|
|
dc.BeginDrawing()
|
|
x1,y1 = self.PrevMoveXY
|
|
x2,y2 = xy_tl
|
|
w,h = self.PanelSize
|
|
##fixme: This sure could be cleaner!
|
|
if x2 > x1 and y2 > y1:
|
|
xa = xb = x1
|
|
ya = yb = y1
|
|
wa = w
|
|
ha = y2 - y1
|
|
wb = x2- x1
|
|
hb = h
|
|
elif x2 > x1 and y2 <= y1:
|
|
xa = x1
|
|
ya = y1
|
|
wa = x2 - x1
|
|
ha = h
|
|
xb = x1
|
|
yb = y2 + h
|
|
wb = w
|
|
hb = y1 - y2
|
|
elif x2 <= x1 and y2 > y1:
|
|
xa = x1
|
|
ya = y1
|
|
wa = w
|
|
ha = y2 - y1
|
|
xb = x2 + w
|
|
yb = y1
|
|
wb = x1 - x2
|
|
hb = h - y2 + y1
|
|
elif x2 <= x1 and y2 <= y1:
|
|
xa = x2 + w
|
|
ya = y1
|
|
wa = x1 - x2
|
|
ha = h
|
|
xb = x1
|
|
yb = y2 + h
|
|
wb = w
|
|
hb = y1 - y2
|
|
|
|
dc.SetPen(wx.TRANSPARENT_PEN)
|
|
dc.SetBrush(self.BackgroundBrush)
|
|
dc.DrawRectangle(xa, ya, wa, ha)
|
|
dc.DrawRectangle(xb, yb, wb, hb)
|
|
self.PrevMoveXY = xy_tl
|
|
if self._ForeDrawList:
|
|
##if self._ForegroundBuffer:
|
|
dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl)
|
|
else:
|
|
dc.DrawBitmapPoint(self._Buffer,xy_tl)
|
|
dc.EndDrawing()
|
|
elif self.GUIMode == "Mouse":
|
|
## Only do something if there are mouse over events bound
|
|
if self.HitDict and (self.HitDict[ EVT_FC_ENTER_OBJECT ] or self.HitDict[ EVT_FC_LEAVE_OBJECT ] ):
|
|
if not self.MouseOverTest(event):
|
|
self._RaiseMouseEvent(event,EVT_FC_MOTION)
|
|
else:
|
|
pass
|
|
self._RaiseMouseEvent(event,EVT_FC_MOTION)
|
|
else:
|
|
pass
|
|
|
|
def RightDownEvent(self,event):
|
|
if self.GUIMode:
|
|
if self.GUIMode == "ZoomIn":
|
|
Center = self.PixelToWorld((event.GetX(),event.GetY()))
|
|
self.Zoom(1/1.5,Center)
|
|
elif self.GUIMode == "ZoomOut":
|
|
Center = self.PixelToWorld((event.GetX(),event.GetY()))
|
|
self.Zoom(1.5,Center)
|
|
elif self.GUIMode == "Mouse":
|
|
EventType = EVT_FC_RIGHT_DOWN
|
|
if not self.HitTest(event, EventType):
|
|
self._RaiseMouseEvent(event, EventType)
|
|
else:
|
|
pass
|
|
|
|
def MakeNewBuffers(self):
|
|
self._BackgroundDirty = True
|
|
# Make new offscreen bitmap:
|
|
self._Buffer = wx.EmptyBitmap(*self.PanelSize)
|
|
#dc = wx.MemoryDC()
|
|
#dc.SelectObject(self._Buffer)
|
|
#dc.Clear()
|
|
if self._ForeDrawList:
|
|
self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize)
|
|
else:
|
|
self._ForegroundBuffer = None
|
|
if self.UseHitTest:
|
|
self.MakeNewHTdc()
|
|
else:
|
|
self._HTdc = None
|
|
self._ForegroundHTdc = None
|
|
|
|
def MakeNewHTdc(self):
|
|
## Note: While it's considered a "bad idea" to keep a
|
|
## MemoryDC around I'm doing it here because a wx.Bitmap
|
|
## doesn't have a GetPixel method so a DC is needed to do
|
|
## the hit-test. It didn't seem like a good idea to re-create
|
|
## a wx.MemoryDC on every single mouse event, so I keep it
|
|
## around instead
|
|
self._HTdc = wx.MemoryDC()
|
|
self._HTBitmap = wx.EmptyBitmap(*self.PanelSize)
|
|
self._HTdc.SelectObject( self._HTBitmap )
|
|
self._HTdc.SetBackground(wx.BLACK_BRUSH)
|
|
if self._ForeDrawList:
|
|
self._ForegroundHTdc = wx.MemoryDC()
|
|
self._ForegroundHTBitmap = wx.EmptyBitmap(*self.PanelSize)
|
|
self._ForegroundHTdc.SelectObject( self._ForegroundHTBitmap )
|
|
self._ForegroundHTdc.SetBackground(wx.BLACK_BRUSH)
|
|
else:
|
|
self._ForegroundHTdc = None
|
|
|
|
def OnSize(self, event=None):
|
|
self.InitializePanel()
|
|
self.SizeTimer.Start(50, oneShot=True)
|
|
|
|
def OnSizeTimer(self, event=None):
|
|
self.MakeNewBuffers()
|
|
self.Draw()
|
|
|
|
def InitializePanel(self):
|
|
self.PanelSize = self.GetClientSizeTuple()
|
|
if self.PanelSize == (0,0):
|
|
## OS-X sometimes gives a Size event when the panel is size (0,0)
|
|
self.PanelSize = (2,2)
|
|
self.PanelSize = array(self.PanelSize, Int32)
|
|
self.HalfPanelSize = self.PanelSize / 2 # lrk: added for speed in WorldToPixel
|
|
if self.PanelSize[0] == 0 or self.PanelSize[1] == 0:
|
|
self.AspectRatio = 1.0
|
|
else:
|
|
self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1]
|
|
|
|
def OnPaint(self, event):
|
|
dc = wx.PaintDC(self)
|
|
if self._ForegroundBuffer:
|
|
dc.DrawBitmap(self._ForegroundBuffer,0,0)
|
|
else:
|
|
dc.DrawBitmap(self._Buffer,0,0)
|
|
|
|
def Draw(self, Force=False):
|
|
"""
|
|
There is a main buffer set up to double buffer the screen, so
|
|
you can get quick re-draws when the window gets uncovered.
|
|
|
|
If there are any objects in self._ForeDrawList, then the
|
|
background gets drawn to a new buffer, and the foreground
|
|
objects get drawn on top of it. The final result if blitted to
|
|
the screen, and stored for future Paint events. This is done so
|
|
that you can have a complicated background, but have something
|
|
changing on the foreground, without having to wait for the
|
|
background to get re-drawn. This can be used to support simple
|
|
animation, for instance.
|
|
|
|
"""
|
|
if sometrue(self.PanelSize <= 2 ): # it's possible for this to get called before being properly initialized.
|
|
return
|
|
if self.Debug: start = clock()
|
|
ScreenDC = wx.ClientDC(self)
|
|
ViewPortWorld = ( self.PixelToWorld((0,0)),
|
|
self.PixelToWorld(self.PanelSize) )
|
|
ViewPortBB = array( ( minimum.reduce(ViewPortWorld),
|
|
maximum.reduce(ViewPortWorld) ) )
|
|
dc = wx.MemoryDC()
|
|
dc.SelectObject(self._Buffer)
|
|
if self._BackgroundDirty or Force:
|
|
#print "Background is Dirty"
|
|
dc.SetBackground(self.BackgroundBrush)
|
|
dc.Clear()
|
|
if self._HTdc:
|
|
self._HTdc.Clear()
|
|
self._DrawObjects(dc, self._DrawList, ScreenDC, ViewPortBB, self._HTdc)
|
|
self._BackgroundDirty = False
|
|
|
|
if self._ForeDrawList:
|
|
## If an object was just added to the Foreground, there might not yet be a buffer
|
|
if self._ForegroundBuffer is None:
|
|
self._ForegroundBuffer = wx.EmptyBitmap(self.PanelSize[0],
|
|
self.PanelSize[1])
|
|
|
|
dc = wx.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here
|
|
dc.SelectObject(self._ForegroundBuffer)
|
|
dc.DrawBitmap(self._Buffer,0,0)
|
|
if self._ForegroundHTdc is None:
|
|
self._ForegroundHTdc = wx.MemoryDC()
|
|
self._ForegroundHTdc.SelectObject( wx.EmptyBitmap(
|
|
self.PanelSize[0],
|
|
self.PanelSize[1]) )
|
|
if self._HTdc:
|
|
## blit the background HT buffer to the foreground HT buffer
|
|
self._ForegroundHTdc.Blit(0, 0,
|
|
self.PanelSize[0], self.PanelSize[1],
|
|
self._HTdc, 0, 0)
|
|
self._DrawObjects(dc,
|
|
self._ForeDrawList,
|
|
ScreenDC,
|
|
ViewPortBB,
|
|
self._ForegroundHTdc)
|
|
ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0)
|
|
# If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn
|
|
# This seeems out of place, but it works.
|
|
if self.PrevRBBox:
|
|
ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH))
|
|
ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
ScreenDC.SetLogicalFunction(wx.XOR)
|
|
ScreenDC.DrawRectanglePointSize(*self.PrevRBBox)
|
|
if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start)
|
|
|
|
## Clear the font cache
|
|
## IF you don't do this, the X font server starts to take up Massive amounts of memory
|
|
## This is mostly a problem with very large fonts, that you get with scaled text when zoomed in.
|
|
DrawObject.FontList = {}
|
|
|
|
def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck
|
|
# lrk: Returns the objects that should be redrawn
|
|
|
|
BB2 = ViewPortBB
|
|
redrawlist = []
|
|
for Object in DrawList:
|
|
BB1 = Object.BoundingBox
|
|
if (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and
|
|
BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1]):
|
|
redrawlist.append(Object)
|
|
return redrawlist
|
|
_ShouldRedraw = staticmethod(_ShouldRedraw)
|
|
|
|
|
|
## def BBCheck(self, BB1, BB2):
|
|
## """
|
|
|
|
## BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise
|
|
|
|
## """
|
|
## if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and
|
|
## (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ):
|
|
## return True
|
|
## else:
|
|
## return False
|
|
|
|
def 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 = asarray(shift,Float)
|
|
#print "shifting by:", shift
|
|
if CoordType == 'Panel':# convert from panel coordinates
|
|
shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector
|
|
elif CoordType == 'Pixel': # convert from pixel coordinates
|
|
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 = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
|
|
self._BackgroundDirty = True
|
|
self.Draw()
|
|
|
|
def Zoom(self,factor,center = None):
|
|
|
|
"""
|
|
Zoom(factor, center) changes the amount of zoom of the image by factor.
|
|
If factor is greater than one, the image gets larger.
|
|
If factor is less than one, the image gets smaller.
|
|
|
|
Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming.
|
|
If center is not given, the center will stay the same.
|
|
|
|
"""
|
|
self.Scale = self.Scale*factor
|
|
if not center is None:
|
|
self.ViewPortCenter = array(center,Float)
|
|
self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
|
|
self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector
|
|
self._BackgroundDirty = True
|
|
self.Draw()
|
|
|
|
def ZoomToBB(self, NewBB = None, DrawFlag = True):
|
|
|
|
"""
|
|
|
|
Zooms the image to the bounding box given, or to the bounding
|
|
box of all the objects on the canvas, if none is given.
|
|
|
|
"""
|
|
|
|
if not NewBB is None:
|
|
BoundingBox = NewBB
|
|
else:
|
|
if self.BoundingBoxDirty:
|
|
self._ResetBoundingBox()
|
|
BoundingBox = self.BoundingBox
|
|
if not BoundingBox is None:
|
|
self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2,
|
|
(BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float)
|
|
self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
|
|
# Compute the new Scale
|
|
BoundingBox = BoundingBox * self.MapProjectionVector
|
|
try:
|
|
self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])),
|
|
abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )*0.95
|
|
except ZeroDivisionError: # this will happen if the BB has zero width or height
|
|
try: #width == 0
|
|
self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95
|
|
except ZeroDivisionError:
|
|
try: # height == 0
|
|
self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95
|
|
except ZeroDivisionError: #zero size! (must be a single point)
|
|
self.Scale = 1
|
|
|
|
self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector
|
|
if DrawFlag:
|
|
self._BackgroundDirty = True
|
|
self.Draw()
|
|
else:
|
|
# Reset the shifting and scaling to defaults when there is no BB
|
|
self.ViewPortCenter= array( (0,0), Float)
|
|
self.MapProjectionVector = array( (1,1), Float) # No Projection to start!
|
|
self.TransformVector = array( (1,-1), Float) # default Transformation
|
|
self.Scale = 1
|
|
|
|
def RemoveObjects(self, Objects):
|
|
for Object in Objects:
|
|
self.RemoveObject(Object, ResetBB = False)
|
|
self.BoundingBoxDirty = True
|
|
|
|
def RemoveObject(self, Object, ResetBB = True):
|
|
##fixme: Using the list.remove method is kind of slow
|
|
if Object.InForeground:
|
|
self._ForeDrawList.remove(Object)
|
|
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):
|
|
self._DrawList = []
|
|
self._ForeDrawList = []
|
|
self._BackgroundDirty = True
|
|
self.HitColorGenerator = None
|
|
self.UseHitTest = False
|
|
if ResetBB:
|
|
self._ResetBoundingBox()
|
|
self.MakeNewBuffers()
|
|
self.HitDict = None
|
|
|
|
## No longer called
|
|
## def _AddBoundingBox(self,NewBB):
|
|
## if self.BoundingBox is None:
|
|
## self.BoundingBox = NewBB
|
|
## self.ZoomToBB(NewBB,DrawFlag = False)
|
|
## else:
|
|
## self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]),
|
|
## min(self.BoundingBox[0,1],NewBB[0,1])),
|
|
## (max(self.BoundingBox[1,0],NewBB[1,0]),
|
|
## max(self.BoundingBox[1,1],NewBB[1,1]))),
|
|
## Float)
|
|
|
|
def _getboundingbox(bboxarray): # lrk: added this
|
|
|
|
upperleft = minimum.reduce(bboxarray[:,0])
|
|
lowerright = maximum.reduce(bboxarray[:,1])
|
|
return array((upperleft, lowerright), Float)
|
|
|
|
_getboundingbox = staticmethod(_getboundingbox)
|
|
|
|
def _ResetBoundingBox(self):
|
|
if self._DrawList or self._ForeDrawList:
|
|
bboxarray = zeros((len(self._DrawList)+len(self._ForeDrawList), 2, 2),Float)
|
|
i = -1 # just in case _DrawList is empty
|
|
for (i, BB) in enumerate(self._DrawList):
|
|
bboxarray[i] = BB.BoundingBox
|
|
for (j, BB) in enumerate(self._ForeDrawList):
|
|
bboxarray[i+j+1] = BB.BoundingBox
|
|
self.BoundingBox = self._getboundingbox(bboxarray)
|
|
else:
|
|
self.BoundingBox = None
|
|
self.ViewPortCenter= array( (0,0), Float)
|
|
self.TransformVector = array( (1,-1), Float)
|
|
self.MapProjectionVector = array( (1,1), Float)
|
|
self.Scale = 1
|
|
self.BoundingBoxDirty = False
|
|
|
|
def PixelToWorld(self,Points):
|
|
"""
|
|
Converts coordinates from Pixel coordinates to world coordinates.
|
|
|
|
Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates.
|
|
|
|
"""
|
|
return (((asarray(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter)
|
|
|
|
def WorldToPixel(self,Coordinates):
|
|
"""
|
|
This function will get passed to the drawing functions of the objects,
|
|
to transform from world to pixel coordinates.
|
|
Coordinates should be a NX2 array of (x,y) coordinates, or
|
|
a 2-tuple, or sequence of 2-tuples.
|
|
"""
|
|
#Note: this can be called by users code for various reasons, so asarray is needed.
|
|
return (((asarray(Coordinates,Float) -
|
|
self.ViewPortCenter)*self.TransformVector)+
|
|
(self.HalfPanelSize)).astype('i')
|
|
|
|
def ScaleWorldToPixel(self,Lengths):
|
|
"""
|
|
This function will get passed to the drawing functions of the objects,
|
|
to Change a length from world to pixel coordinates.
|
|
|
|
Lengths should be a NX2 array of (x,y) coordinates, or
|
|
a 2-tuple, or sequence of 2-tuples.
|
|
"""
|
|
return ( (asarray(Lengths,Float)*self.TransformVector) ).astype('i')
|
|
|
|
def ScalePixelToWorld(self,Lengths):
|
|
"""
|
|
This function computes a pair of x.y lengths,
|
|
to change then from pixel to world coordinates.
|
|
|
|
Lengths should be a NX2 array of (x,y) coordinates, or
|
|
a 2-tuple, or sequence of 2-tuples.
|
|
"""
|
|
|
|
return (asarray(Lengths,Float) / self.TransformVector)
|
|
|
|
def AddObject(self,obj):
|
|
# put in a reference to the Canvas, so remove and other stuff can work
|
|
obj._Canvas = self
|
|
if obj.InForeground:
|
|
self._ForeDrawList.append(obj)
|
|
self.UseForeground = True
|
|
else:
|
|
self._DrawList.append(obj)
|
|
self._BackgroundDirty = True
|
|
self.BoundingBoxDirty = True
|
|
return True
|
|
|
|
def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None):
|
|
"""
|
|
This is a convenience function;
|
|
This function takes the list of objects and draws them to specified
|
|
device context.
|
|
"""
|
|
dc.SetBackground(self.BackgroundBrush)
|
|
dc.BeginDrawing()
|
|
#i = 0
|
|
PanelSize0, PanelSize1 = self.PanelSize # for speed
|
|
WorldToPixel = self.WorldToPixel # for speed
|
|
ScaleWorldToPixel = self.ScaleWorldToPixel # for speed
|
|
Blit = ScreenDC.Blit # for speed
|
|
NumBetweenBlits = self.NumBetweenBlits # for speed
|
|
for i, Object in enumerate(self._ShouldRedraw(DrawList, ViewPortBB)):
|
|
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 spcified using the wx flags:
|
|
|
|
wx.BITMAP_TYPE_BMP
|
|
wx.BITMAP_TYPE_XBM
|
|
wx.BITMAP_TYPE_XPM
|
|
etc. (see the wx docs for the complete list)
|
|
|
|
"""
|
|
|
|
self._Buffer.SaveFile(filename, ImageType)
|
|
|
|
|
|
def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__
|
|
classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon",
|
|
"Line", "Text", "PointSet","Point", "Arrow","ScaledTextBox",
|
|
"SquarePoint","Bitmap", "ScaledBitmap"]
|
|
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()
|
|
|
|
|