diff --git a/samples/floatcanvas/PieChart.py b/samples/floatcanvas/PieChart.py index eaacb524..b7d89213 100644 --- a/samples/floatcanvas/PieChart.py +++ b/samples/floatcanvas/PieChart.py @@ -5,7 +5,7 @@ import wx ## import the installed version from wx.lib.floatcanvas import NavCanvas, FloatCanvas -from wx.lib.floatcanvas.SpecialObjects import PieChart +from wx.lib.floatcanvas.FCObjects import PieChart ## import a local version #import sys diff --git a/wx/lib/floatcanvas/Utilities/BBoxTest.py b/unittests/test_lib_floatcanvas_bbox.py old mode 100755 new mode 100644 similarity index 95% rename from wx/lib/floatcanvas/Utilities/BBoxTest.py rename to unittests/test_lib_floatcanvas_bbox.py index 6b46bacd..ea6e6b95 --- a/wx/lib/floatcanvas/Utilities/BBoxTest.py +++ b/unittests/test_lib_floatcanvas_bbox.py @@ -1,27 +1,12 @@ -#!/usr/bin/env python -#---------------------------------------------------------------------------- -# Name: BBoxTest.py -# Purpose: Test code for the BBox Object -# -# Author: -# -# Created: -# Version: -# Date: -# Licence: -# Tags: phoenix-port -#---------------------------------------------------------------------------- +import imp_unittest, unittest +import wtc +import wx -""" -Test code for the BBox Object +from wx.lib.floatcanvas.Utilities.BBox import * -""" +#--------------------------------------------------------------------------- -import unittest - -from BBox import * - -class testCreator(unittest.TestCase): +class testCreator(wtc.WidgetTestCase): def testCreates(self): B = BBox(((0,0),(5,5))) self.failUnless(isinstance(B, BBox)) @@ -74,7 +59,7 @@ class testCreator(unittest.TestCase): # Should catch tiny difference self.failUnlessRaises(ValueError, BBox, ((0,0), (-1e-20,5)) ) -class testAsBBox(unittest.TestCase): +class testAsBBox(wtc.WidgetTestCase): def testPassThrough(self): B = BBox(((0,0),(5,5))) @@ -99,7 +84,7 @@ class testAsBBox(unittest.TestCase): A[0,0] = -10 self.failUnless(C[0,0] == A[0,0]) -class testIntersect(unittest.TestCase): +class testIntersect(wtc.WidgetTestCase): def testSame(self): B = BBox(((-23.5, 456),(56, 532.0))) @@ -188,7 +173,7 @@ class testIntersect(unittest.TestCase): -class testEquality(unittest.TestCase): +class testEquality(wtc.WidgetTestCase): def testSame(self): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) @@ -218,7 +203,7 @@ class testEquality(unittest.TestCase): C = N.array( ( (1.01, 2.0), (5.0, 10.0) ) ) self.failIf(C == B) -class testInside(unittest.TestCase): +class testInside(wtc.WidgetTestCase): def testSame(self): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) @@ -274,7 +259,7 @@ class testInside(unittest.TestCase): C = BBox( ( (17.1, 8),(17.95, 32) ) ) self.failIf(B.Inside(C) ) -class testPointInside(unittest.TestCase): +class testPointInside(wtc.WidgetTestCase): def testPointIn(self): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) P = (3.0, 4.0) @@ -350,7 +335,7 @@ class testPointInside(unittest.TestCase): P = (-1, -10.0) self.failUnless(B.PointInside(P)) -class testFromPoints(unittest.TestCase): +class testFromPoints(wtc.WidgetTestCase): def testCreate(self): Pts = N.array( ((5,2), @@ -398,7 +383,7 @@ class testFromPoints(unittest.TestCase): B[1,0] == 65.0 and B[1,1] == 43.2 ) -class testMerge(unittest.TestCase): +class testMerge(wtc.WidgetTestCase): A = BBox( ((-23.5, 456), (56, 532.0)) ) B = BBox( ((-20.3, 460), (54, 465 )) )# B should be completely inside A C = BBox( ((-23.5, 456), (58, 540.0)) )# up and to the right or A @@ -424,7 +409,7 @@ class testMerge(unittest.TestCase): A.Merge(self.D) self.failUnless(A[0] == self.D[0] and A[1] == self.A[1]) -class testWidthHeight(unittest.TestCase): +class testWidthHeight(wtc.WidgetTestCase): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) def testWidth(self): self.failUnless(self.B.Width == 4.0) @@ -444,7 +429,7 @@ class testWidthHeight(unittest.TestCase): def testSetH(self): self.failUnlessRaises(AttributeError, self.attemptSetHeight) -class testCenter(unittest.TestCase): +class testCenter(wtc.WidgetTestCase): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) def testCenter(self): self.failUnless( (self.B.Center == (3.0, 6.0)).all() ) @@ -456,7 +441,7 @@ class testCenter(unittest.TestCase): self.failUnlessRaises(AttributeError, self.attemptSetCenter) -class testBBarray(unittest.TestCase): +class testBBarray(wtc.WidgetTestCase): BBarray = N.array( ( ((-23.5, 456), (56, 532.0)), ((-20.3, 460), (54, 465 )), ((-23.5, 456), (58, 540.0)), @@ -469,7 +454,7 @@ class testBBarray(unittest.TestCase): BB = fromBBArray(self.BBarray) self.failUnless(BB == self.BB, "Wrong BB was created. It was:\n%s \nit should have been:\n%s"%(BB, self.BB)) -class testNullBBox(unittest.TestCase): +class testNullBBox(wtc.WidgetTestCase): B1 = NullBBox() B2 = NullBBox() B3 = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) @@ -504,7 +489,7 @@ class testNullBBox(unittest.TestCase): self.failUnless( self.B3.Overlaps(self.B1) == False) -class testInfBBox(unittest.TestCase): +class testInfBBox(wtc.WidgetTestCase): B1 = InfBBox() B2 = InfBBox() B3 = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) @@ -556,7 +541,7 @@ class testInfBBox(unittest.TestCase): self.failUnless( self.NB.Overlaps(self.B1) == True) -class testSides(unittest.TestCase): +class testSides(wtc.WidgetTestCase): B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) def testLeft(self): @@ -568,7 +553,8 @@ class testSides(unittest.TestCase): def testTop(self): self.failUnless( self.B.Top == 10.0 ) + +#--------------------------------------------------------------------------- - -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/wx/lib/floatcanvas/FCObjects.py b/wx/lib/floatcanvas/FCObjects.py new file mode 100644 index 00000000..f1095309 --- /dev/null +++ b/wx/lib/floatcanvas/FCObjects.py @@ -0,0 +1,2657 @@ +#!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: FCObjects.py +# Purpose: Contains all the drawing objects FloatCanvas supports +# +# Author: +# +# Created: +# Version: +# Date: +# Licence: +# Tags: phoenix-port, unittest, documented, py3-port +#---------------------------------------------------------------------------- +""" +This is where FloatCanvas defines its drawings objects. +""" + +import sys + +import wx +from wx.lib import six + +import numpy as N + +from .Utilities import BBox +from wx.lib.floatcanvas.Utilities import Colors + +mac = sys.platform.startswith("darwin") + +## 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 + + +def ComputeFontScale(): + """ + Compute the font scale. + + A global variable to hold the scaling from pixel size to point size. + """ + global FontScale + dc = wx.ScreenDC() + dc.SetFont(wx.Font(16, wx.FONTFAMILY_ROMAN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) + E = dc.GetTextExtent("X") + FontScale = 16/E[1] + del dc + +ComputeFontScale() + +## fixme: This should probably be re-factored into a class +_testBitmap = None +_testDC = None +def _cycleidxs(indexcount, maxvalue, step): + + """ + Utility function used by _colorGenerator + """ + def colormatch(color): + """Return True if the color comes back from the bitmap identically.""" + if len(color) < 3: + return True + global _testBitmap, _testDC + B = _testBitmap + if not mac: + dc = _testDC + if not B: + B = _testBitmap = wx.Bitmap(1, 1) + if not mac: + dc = _testDC = wx.MemoryDC() + if mac: + dc = wx.MemoryDC() + dc.SelectObject(B) + dc.SetBackground(wx.BLACK_BRUSH) + dc.Clear() + dc.SetPen(wx.Pen(wx.Colour(*color), 4)) + dc.DrawPoint(0,0) + if mac: + del dc + pdata = wx.AlphaPixelData(B) + pacc = pdata.GetPixels() + pacc.MoveTo(pdata, 0, 0) + outcolor = pacc.Get()[:3] + else: + outcolor = dc.GetPixel(0,0) + return outcolor == color + + if indexcount == 0: + yield () + else: + for idx in range(0, maxvalue, step): + for tail in _cycleidxs(indexcount - 1, maxvalue, step): + color = (idx, ) + tail + if not colormatch(color): + continue + yield color + +def _colorGenerator(): + + """ + Generates a series of unique colors used to do hit-tests with the Hit + Test bitmap + """ + return _cycleidxs(indexcount=3, maxvalue=256, step=1) + + +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, see for example :class:`~lib.floatcanvas.FloatCanvas.Circle`. + + """ + #This class contains a series of static dictionaries: + + #* BrushList + #* PenList + #* FillStyleList + #* LineStyleList + + + def __init__(self, InForeground = False, IsVisible = True): + """ + Default class constructor. + + :param boolean `InForeground`: Define if object should be in foreground + or not + :param boolean `IsVisible`: Define if object should be visible + + """ + 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.BRUSHSTYLE_TRANSPARENT, + "Solid" : wx.BRUSHSTYLE_SOLID, + "BiDiagonalHatch": wx.BRUSHSTYLE_BDIAGONAL_HATCH, + "CrossDiagHatch" : wx.BRUSHSTYLE_CROSSDIAG_HATCH, + "FDiagonal_Hatch": wx.BRUSHSTYLE_FDIAGONAL_HATCH, + "CrossHatch" : wx.BRUSHSTYLE_CROSS_HATCH, + "HorizontalHatch": wx.BRUSHSTYLE_HORIZONTAL_HATCH, + "VerticalHatch" : wx.BRUSHSTYLE_VERTICAL_HATCH + } + + LineStyleList = { + "Solid" : wx.PENSTYLE_SOLID, + "Transparent": wx.PENSTYLE_TRANSPARENT, + "Dot" : wx.PENSTYLE_DOT, + "LongDash" : wx.PENSTYLE_LONG_DASH, + "ShortDash" : wx.PENSTYLE_SHORT_DASH, + "DotDash" : wx.PENSTYLE_DOT_DASH, + } + + def Bind(self, Event, CallBackFun): + """ + Bind an event to the DrawObject + + :param `Event`: see below for supported event types + + - 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 + + :param `CallBackFun`: the call back function for the event + + + """ + ##fixme: Way too much Canvas Manipulation here! + 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: + # first call to prevent the background color from being used. + self._Canvas.HitColorGenerator = _colorGenerator() + if six.PY3: + next(self._Canvas.HitColorGenerator) + else: + self._Canvas.HitColorGenerator.next() + if six.PY3: + self.HitColor = next(self._Canvas.HitColorGenerator) + else: + 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 its color + + def UnBindAll(self): + """ + Unbind all events + + .. note:: Currently only removes one from each list + + """ + ## fixme: this only removes one from each list, there could be more. + ## + patch by Tim Ansel + if self._Canvas.HitDict: + for Event in self._Canvas.HitDict.itervalues(): + try: + del Event[self.HitColor] + except KeyError: + pass + self.HitAble = False + + + def SetBrush(self, FillColor, FillStyle): + """ + Set the brush for this DrawObject + + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid entries + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + for valid entries + """ + 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])) + #print("Setting Brush, BrushList length:", len(self.BrushList)) + + def SetPen(self, LineColor, LineStyle, LineWidth): + """ + Set the Pen for this DrawObject + + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid entries + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + for valid entries + :param integer `LineWidth`: the width in pixels + """ + 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): + """ + Set the brush used for hit test, do not call directly. + + :param `HitColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + + """ + 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): + """ + Set the pen used for hit test, do not call directly. + + :param `HitColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param integer `LineWidth`: the line width in pixels + + """ + 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): + """ + Set the Color - this method is overridden in the subclasses + + :param `Color`: use one of the following values any valid entry from + :class:`ColourDatabase` + + - ``Green`` + - ``White`` + - ``Black`` + - ``Grey`` + - ``MediumGrey`` + - ``LightGrey`` + - ``Cyan`` + - ``Red`` + + """ + + pass + + def SetLineColor(self, LineColor): + """ + Set the LineColor - this method is overridden in the subclasses + + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid values + + """ + pass + + def SetLineStyle(self, LineStyle): + """ + Set the LineStyle - this method is overridden in the subclasses + + :param `LineStyle`: Use one of the following values or ``None`` : + + ===================== ============================= + Style Description + ===================== ============================= + ``Solid`` Solid line + ``Transparent`` A transparent line + ``Dot`` Dotted line + ``LongDash`` Dashed line (long) + ``ShortDash`` Dashed line (short) + ``DotDash`` Dash-dot-dash line + ===================== ============================= + + """ + pass + + def SetLineWidth(self, LineWidth): + """ + Set the LineWidth + + :param integer `LineWidth`: sets the line width in pixel + + """ + pass + + def SetFillColor(self, FillColor): + """ + Set the FillColor - this method is overridden in the subclasses + + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid values + + """ + pass + + def SetFillStyle(self, FillStyle): + """ + Set the FillStyle - this method is overridden in the subclasses + + :param string `FillStyle`: following is a list of valid values: + + ===================== ========================================= + Style Description + ===================== ========================================= + ``Transparent`` Transparent fill + ``Solid`` Solid fill + ``BiDiagonalHatch`` Bi Diagonal hatch fill + ``CrossDiagHatch`` Cross Diagonal hatch fill + ``FDiagonal_Hatch`` F Diagonal hatch fill + ``CrossHatch`` Cross hatch fill + ``HorizontalHatch`` Horizontal hatch fill + ``VerticalHatch`` Vertical hatch fill + ===================== ========================================= + + """ + pass + + def PutInBackground(self): + """Put the object in the background.""" + 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): + """Put the object in the foreground.""" + 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): + """Hide the object.""" + self.Visible = False + + def Show(self): + """Show the object.""" + 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): + """ + Set the Color + + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid values + + """ + 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 a line, rather than a fill + color and line color + + """ + + def SetLineColor(self, LineColor): + """ + Set the LineColor + + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid values + + """ + self.LineColor = LineColor + self.SetPen(LineColor,self.LineStyle,self.LineWidth) + SetColor = SetLineColor# so that it will do something reasonable + + def SetLineStyle(self, LineStyle): + """ + Set the LineStyle + + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + for valid values + + """ + self.LineStyle = LineStyle + self.SetPen(self.LineColor,LineStyle,self.LineWidth) + + def SetLineWidth(self, LineWidth): + """ + Set the LineWidth + + :param integer `LineWidth`: line width in pixels + + """ + 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): + """ + Set the FillColor + + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + for valid values + + """ + self.FillColor = FillColor + self.SetBrush(FillColor, self.FillStyle) + + def SetFillStyle(self, FillStyle): + """ + Set the FillStyle + + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + for valid values + + """ + self.FillStyle = FillStyle + self.SetBrush(self.FillColor,FillStyle) + + def SetUpDraw(self, dc, WorldToPixel, ScaleWorldToPixel, HTdc): + """ + Setup for draw + + :param `dc`: the dc to draw ??? + :param `WorldToPixel`: ??? + :param `ScaleWorldToPixel`: ??? + :param `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): + """ + Moves the object by delta, where delta is a (dx, dy) pair. + + :param `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): + """Calculate the bounding box.""" + ## This may get overwritten in some subclasses + 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: + """ + 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): + """ + Moves the object by delta, where delta is a (dx, dy) pair. + + :param `Delta`: is a (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): + """Calculate the bounding box.""" + 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). + + :param `Points`: takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param boolean `copy`: By default, a copy is made, if copy is set to + ``False``, a reference is used, if 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 + # shifts the points 5 in the x dir, and 10 in the y dir. + Points += (5, 10) + # Sets the points to the same array as it was + Object.SetPoints(Points, False) + + """ + 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): + """Draws a polygon + + Points is a list of 2-tuples, or a NX2 NumPy array of + point coordinates. so that Points[N][0] is the x-coordinate of + point N and Points[N][1] is the y-coordinate or Points[N,0] is the + x-coordinate of point N and Points[N,1] is the y-coordinate for + arrays. + + """ + def __init__(self, + Points, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + InForeground = False): + """Default class constructor. + + :param `Points`: start point, takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + :param boolean `InForeground`: should object be in foreground + + """ + 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): + """Draws a line + + 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): + """Default class constructor. + + :param `Points`: takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param boolean `InForeground`: should object be in foreground + + """ + 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): + """Draws a spline""" + def __init__(self, *args, **kwargs): + """Default class constructor. + + see :class:`~lib.floatcanvas.FloatCanvas.Line` + + """ + 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): + """Draws an arrow + + It will draw an arrow , starting at the point ``XY`` points at an angle + defined by ``Direction``. + + """ + def __init__(self, + XY, + Length, + Direction, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 2, + ArrowHeadSize = 8, + ArrowHeadAngle = 30, + InForeground = False): + """Default class constructor. + + :param `XY`: the (x, y) coordinate of the starting point, or a 2-tuple, + or a (2,) `NumPy `_ array + :param integer `Length`: length of arrow in pixels + :param integer `Direction`: angle of arrow in degrees, zero is straight + up `+` angle is to the right + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `ArrowHeadSize`: size of arrow head in pixels + :param `ArrowHeadAngle`: angle of arrow head in degrees + :param boolean `InForeground`: should object be in foreground + + """ + + 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): + """Set the direction + + :param integer `Direction`: angle of arrow in degrees, zero is straight + up `+` angle is to the right + + """ + self.Direction = float(Direction) + self.CalcArrowPoints() + + def SetLength(self, Length): + """Set the length + + :param integer `Length`: length of arrow in pixels + + """ + self.Length = Length + self.CalcArrowPoints() + + def SetLengthDirection(self, Length, Direction): + """Set the lenght and direction + + :param integer `Length`: length of arrow in pixels + :param integer `Direction`: angle of arrow in degrees, zero is straight + up `+` angle is to the right + + """ + 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): + """Calculate the arrow points.""" + 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): + """Draws an arrow line. + + 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, + ArrowHeadSize = 8, + ArrowHeadAngle = 30, + InForeground = False): + """Default class constructor. + + :param `Points`: takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `ArrowHeadSize`: size of arrow head in pixels + :param `ArrowHeadAngle`: angle of arrow head in degrees + :param boolean `InForeground`: should object be in foreground + + """ + + 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): + """Calculate the arrow points.""" + 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 range(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): + """Draws a set of points + + 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): + """Default class constructor. + + :param `Points`: takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param integer `Diameter`: the points diameter + :param boolean `InForeground`: should object be in foreground + + """ + 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): + """Sets the diameter + + :param integer `Diameter`: the points 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. + + :param `XY`: the (x,y) coordinates of the point to look for, it takes a + 2-tuple or (2,) numpy array in World coordinates + + """ + 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): + """A point DrawObject + + .. note:: + + 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): + """Default class constructor. + + :param `XY`: the (x, y) coordinate of the center of the point, or a + 2-tuple, or a (2,) `NumPy `_ array + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param integer `Diameter`: in screen points + :param `InForeground`: define if object is in foreground + + """ + + 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): + """Set the diameter of the object. + + :param integer `Diameter`: in screen points + + """ + 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): + """Draws a square 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): + """Default class constructor. + + :param `Point`: takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param integer `Size`: the size of the square point + :param boolean `InForeground`: should object be in foreground + + """ + 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): + """Sets the size + + :param integer `Size`: the size of the square point + + """ + 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): + """A RectEllipse draw object.""" + def __init__(self, XY, WH, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + InForeground = False): + """Default class constructor. + + :param `XY`: the (x, y) coordinate of the corner of RectEllipse, or a 2-tuple, + or a (2,) `NumPy `_ array + :param `WH`: a tuple with the Width and Height for the object + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + :param `InForeground`: put object in foreground + + """ + + 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) + + # these define the behaviour when zooming makes the objects really small. + self.MinSize = 1 + self.DisappearWhenSmall = True + + self.SetPen(LineColor,LineStyle,LineWidth) + self.SetBrush(FillColor,FillStyle) + + def SetShape(self, XY, WH): + """Set the shape of the object. + + :param `XY`: takes a 2-tuple, or a (2,) `NumPy `_ + array of point coordinates + :param `WH`: a tuple with the Width and Height for the object + + """ + 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): + """Calculate the bounding box.""" + # 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): + """Draws a rectangle see :class:`~lib.floatcanvas.FloatCanvas.RectEllipse`""" + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + ( XY, WH ) = self.SetUpDraw(dc, + WorldToPixel, + ScaleWorldToPixel, + HTdc) + WH[N.abs(WH) < self.MinSize] = self.MinSize + if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny + dc.DrawRectangle(XY, WH) + if HTdc and self.HitAble: + HTdc.DrawRectangle(XY, WH) + + +class Ellipse(RectEllipse): + """Draws an ellipse see :class:`~lib.floatcanvas.FloatCanvas.RectEllipse`""" + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + ( XY, WH ) = self.SetUpDraw(dc, + WorldToPixel, + ScaleWorldToPixel, + HTdc) + WH[N.abs(WH) < self.MinSize] = self.MinSize + if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny + dc.DrawEllipse(XY, WH) + if HTdc and self.HitAble: + HTdc.DrawEllipse(XY, WH) + +class Circle(XYObjectMixin, LineAndFillMixin, DrawObject): + """Draws a circle""" + def __init__(self, XY, Diameter, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + InForeground = False): + """Default class constructor. + + :param `XY`: the (x, y) coordinate of the center of the circle, or a 2-tuple, + or a (2,) `NumPy `_ array + :param integer `Diameter`: the diameter for the object + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + :param boolean `InForeground`: should object be in foreground + + """ + DrawObject.__init__(self, InForeground) + + self.XY = N.array(XY, N.float) + self.WH = N.array((Diameter/2, Diameter/2), N.float) # just to keep it compatible with others + self.CalcBoundingBox() + + self.LineColor = LineColor + self.LineStyle = LineStyle + self.LineWidth = LineWidth + self.FillColor = FillColor + self.FillStyle = FillStyle + + self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) + + # these define the behaviour when zooming makes the objects really small. + self.MinSize = 1 + self.DisappearWhenSmall = True + + self.SetPen(LineColor,LineStyle,LineWidth) + self.SetBrush(FillColor,FillStyle) + + def SetDiameter(self, Diameter): + """Set the diameter of the object + + :param integer `Diameter`: the diameter for the object + + """ + self.WH = N.array((Diameter/2, Diameter/2), N.float) # just to keep it compatible with others + + def CalcBoundingBox(self): + """Calculate the bounding box of the object.""" + # you need this in case Width or Height are negative + self.BoundingBox = BBox.fromPoints( (self.XY+self.WH, self.XY-self.WH) ) + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + ( XY, WH ) = self.SetUpDraw(dc, + WorldToPixel, + ScaleWorldToPixel, + HTdc) + + WH[N.abs(WH) < self.MinSize] = self.MinSize + if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny + dc.DrawCircle(XY, WH[0]) + if HTdc and self.HitAble: + HTdc.DrawCircle(XY, WH[0]) + + +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): + """Draws a text object + + 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.FONTFAMILY_MODERN, + Style = wx.FONTSTYLE_NORMAL, + Weight = wx.FONTWEIGHT_NORMAL, + Underlined = False, + Position = 'tl', + InForeground = False, + Font = None): + """Default class constructor. + + :param string `string`: the text to draw + :param `XY`: the (x, y) coordinate of the corner of the text, or a 2-tuple, + or a (2,) `NumPy `_ array + :param `Size`: the font size + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `BackgroundColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param FontFamily `Family`: a valid :ref:`FontFamily` + :param FontStyle `Style`: a valid :ref:`FontStyle` + :param FontWeight `Weight`: a valid :ref:`FontWeight` + :param boolean `Underlined`: underline the text + :param string `Position`: a two character string indicating where in + relation to the coordinates the box should be oriented + :param boolean `InForeground`: should object be in foreground + :param Font `Font`: alternatively you can define :ref:`Font` and the + above will be ignored. + + ============== ========================== + 1st character Meaning + ============== ========================== + ``t`` top + ``c`` center + ``b`` bottom + ============== ========================== + + ============== ========================== + 2nd character Meaning + ============== ========================== + ``l`` left + ``c`` center + ``r`` right + ============== ========================== + + :param Font `Font`: a valid :class:`Font` + :param boolean `InForeground`: should object be in foreground + + """ + 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.DrawText(self.String, XY) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(XY, (self.TextWidth, self.TextHeight) ) + +class ScaledText(TextObjectMixin, DrawObject): + """ + ##fixme: this can be depricated and jsut use ScaledTextBox with different defaults. + + 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.FONTFAMILY_DEFAULT: Chooses a default font. + * wx.FONTFAMILY_DECORATIVE: A decorative font. + * wx.FONTFAMILY_ROMAN: A formal, serif font. + * wx.FONTFAMILY_SCRIPT: A handwriting font. + * wx.FONTFAMILY_SWISS: A sans-serif font. + * wx.FONTFAMILY_MODERN: A fixed pitch font. + + .. note:: these are only as good as the wxWindows defaults, which aren't so good. + + * Style: One of wx.FONTSTYLE_NORMAL, wx.FONTSTYLE_SLANT and wx.FONTSTYLE_ITALIC. + * Weight: One of wx.FONTWEIGHT_NORMAL, wx.FONTWEIGHT_LIGHT and wx.FONTWEIGHT_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.FONTFAMILY_MODERN, + Style = wx.FONTSTYLE_NORMAL, + Weight = wx.FONTWEIGHT_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.MinFontSize = 1 # this can be changed to set a minimum size + self.DisappearWhenSmall = True + 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.Bitmap(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) + Size = max(Size, self.MinFontSize) # smallest size you want - default to 0 + + # Draw the Text + if not( self.DisappearWhenSmall and Size <= self.MinFontSize) : # don't try to draw a zero sized font! + 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.DrawText(self.String, xy) + + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(xy, (w, h)) + +class ScaledTextBox(TextObjectMixin, DrawObject): + """Draws a text object + + The object is scaled when zoomed. + + The hit-test is done on the entire text extent + + 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. + + """ + + def __init__(self, String, + Point, + Size, + Color = "Black", + BackgroundColor = None, + LineColor = 'Black', + LineStyle = 'Solid', + LineWidth = 1, + Width = None, + PadSize = None, + Family = wx.FONTFAMILY_MODERN, + Style = wx.FONTSTYLE_NORMAL, + Weight = wx.FONTWEIGHT_NORMAL, + Underlined = False, + Position = 'tl', + Alignment = "left", + Font = None, + LineSpacing = 1.0, + InForeground = False): + """Default class constructor. + + :param `Point`: takes a 2-tuple, or a (2,) `NumPy `_ + array of point coordinates + :param integer `Size`: size in World units + :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `BackgroundColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `Width`: width in pixels or ``None``, text will be wrapped to + the given width. + :param `PadSize`: padding in world units or ``None``, if specified it + will creating a space (margin) around the text + :param FontFamily `Family`: a valid :ref:`FontFamily` + :param FontStyle `Style`: a valid :ref:`FontStyle` + :param FontWeight `Weight`: a valid :ref:`FontWeight` + :param boolean `Underlined`: underline the text + :param string `Position`: a two character string indicating where in + relation to the coordinates the box should be oriented + + ============== ========================== + 1st character Meaning + ============== ========================== + ``t`` top + ``c`` center + ``b`` bottom + ============== ========================== + + ============== ========================== + 2nd character Meaning + ============== ========================== + ``l`` left + ``c`` center + ``r`` right + ============== ========================== + + :param `Alignment`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param Font `Font`: alternatively a valid :class:`Font` can be defined + in which case the above will be ignored + :param float `LineSpacing`: the line space to be used + :param boolean `InForeground`: should object be in foreground + + """ + 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.MinFontSize = 1 # this can be changed to set a larger minimum size + self.DisappearWhenSmall = True + + 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.Bitmap(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.Bitmap(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) + + Size = max(Size, self.MinFontSize) # smallest size you want - default to 1 + + # Draw The Box + if (self.LineStyle and self.LineColor) or self.BackgroundColor: + dc.SetBrush(self.Brush) + dc.SetPen(self.Pen) + dc.DrawRectangle(xy , wh) + + # Draw the Text + if not( self.DisappearWhenSmall and Size <= self.MinFontSize) : # don't try to draw a zero sized font! + 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) + dc.DrawTextList(self.Words, Points) + + # Draw the hit box. + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(xy, wh) + +class Bitmap(TextObjectMixin, DrawObject): + """Draws a bitmap + + The size is fixed, and does not scale with the drawing. + + """ + + def __init__(self, Bitmap, XY, + Position='tl', + InForeground=False): + """Default class constructor. + + :param Bitmap `Bitmap`: the bitmap to be drawn + :param `XY`: the (x, y) coordinate of the corner of the bitmap, or a 2-tuple, + or a (2,) `NumPy `_ array + :param string `Position`: a two character string indicating where in relation to the coordinates + the bitmap should be oriented + + ============== ========================== + 1st character Meaning + ============== ========================== + ``t`` top + ``c`` center + ``b`` bottom + ============== ========================== + + ============== ========================== + 2nd character Meaning + ============== ========================== + ``l`` left + ``c`` center + ``r`` right + ============== ========================== + + :param boolean `InForeground`: should object be in foreground + + """ + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx.Bitmap: + self.Bitmap = Bitmap + elif type(Bitmap) == wx.Image: + self.Bitmap = wx.Bitmap(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.DrawBitmap(self.Bitmap, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(XY, (self.Width, self.Height) ) + +class ScaledBitmap(TextObjectMixin, DrawObject): + """Draws a scaled bitmap + + The size scales with the drawing + + """ + + def __init__(self, + Bitmap, + XY, + Height, + Position = 'tl', + InForeground = False): + """Default class constructor. + + :param wx.Bitmap `Bitmap`: the bitmap to be drawn + :param `XY`: the (x, y) coordinate of the corner of the scaled bitmap, + or a 2-tuple, or a (2,) `NumPy `_ array + :param `Height`: height to be used, width is calculated from the aspect ratio of the bitmap + :param string `Position`: a two character string indicating where in relation to the coordinates + the bitmap should be oriented + + ============== ========================== + 1st character Meaning + ============== ========================== + ``t`` top + ``c`` center + ``b`` bottom + ============== ========================== + + ============== ========================== + 2nd character Meaning + ============== ========================== + ``l`` left + ``c`` center + ``r`` right + ============== ========================== + + :param boolean `InForeground`: should object be in foreground + + """ + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx.Bitmap: + self.Image = Bitmap.ConvertToImage() + elif type(Bitmap) == wx.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): + """Calculate the bounding box.""" + ## 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.Bitmap(Img) + + XY = self.ShiftFun(XY[0], XY[1], W, H) + dc.DrawBitmap(self.ScaledBitmap, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(XY, (W, H) ) + +class ScaledBitmap2(TextObjectMixin, DrawObject, ): + """Draws a scaled bitmap + + 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): + """Default class constructor. + + :param wx.Bitmap `Bitmap`: the bitmap to be drawn + :param `XY`: the (x, y) coordinate of the corner of the scaled bitmap, + or a 2-tuple, or a (2,) `NumPy `_ array + :param `Height`: height to be used + :param `Width`: width to be used, if ``None`` width is calculated from the aspect ratio of the bitmap + :param string `Position`: a two character string indicating where in relation to the coordinates + the bitmap should be oriented + + ============== ========================== + 1st character Meaning + ============== ========================== + ``t`` top + ``c`` center + ``b`` bottom + ============== ========================== + + ============== ========================== + 2nd character Meaning + ============== ========================== + ``l`` left + ``c`` center + ``r`` right + ============== ========================== + + :param boolean `InForeground`: should object be in foreground + + """ + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx.Bitmap: + self.Image = Bitmap.ConvertToImage() + elif type(Bitmap) == wx.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): + """Calculate the bounding box.""" + ## 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 the 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.Bitmap(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.DrawBitmap(bmp, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(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)) + print("rescaling with High quality") + Img.Rescale(Ws, Hs, quality=wx.IMAGE_QUALITY_HIGH) + bmp = wx.Bitmap(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.DrawBitmap(bmp, XYs, True) + + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectangle(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") + pass + +class DotGrid: + """ + An example of a Grid Object -- it is set on the 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 -- it really 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 Arc(XYObjectMixin, LineAndFillMixin, DrawObject): + """Draws an arc of a circle, centered on point ``CenterXY``, from + the first point ``StartXY`` to the second ``EndXY``. + + The arc is drawn in an anticlockwise direction from the start point to + the end point. + + """ + def __init__(self, + StartXY, + EndXY, + CenterXY, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + FillColor = None, + FillStyle = "Solid", + InForeground = False): + """Default class constructor. + + :param `StartXY`: start point, takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `EndXY`: end point, takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `CenterXY`: center point, takes a 2-tuple, or a (2,) + `NumPy `_ array of point coordinates + :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` + :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` + :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` + :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` + :param boolean `InForeground`: should object be in foreground + + """ + + DrawObject.__init__(self, InForeground) + + # There is probably a more elegant way to do this next section + # The bounding box just gets set to the WH of a circle, with center at CenterXY + # This is suitable for a pie chart as it will be a circle anyway + radius = N.sqrt( (StartXY[0]-CenterXY[0])**2 + (StartXY[1]-CenterXY[1])**2 ) + minX = CenterXY[0]-radius + minY = CenterXY[1]-radius + maxX = CenterXY[0]+radius + maxY = CenterXY[1]+radius + XY = [minX,minY] + WH = [maxX-minX,maxY-minY] + + self.XY = N.asarray( XY, N.float).reshape((2,)) + self.WH = N.asarray( WH, N.float).reshape((2,)) + + self.StartXY = N.asarray(StartXY, N.float).reshape((2,)) + self.CenterXY = N.asarray(CenterXY, N.float).reshape((2,)) + self.EndXY = N.asarray(EndXY, N.float).reshape((2,)) + + #self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float) + self.CalcBoundingBox() + + #Finish the setup; allocate color,style etc. + 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) #Why isn't this working ??? + + def Move(self, Delta): + """Move the object by delta + + :param `Delta`: delta is a (dx, dy) pair. Ideally a `NumPy `_ + array of shape (2,) + + """ + + Delta = N.asarray(Delta, N.float) + self.XY += Delta + self.StartXY += Delta + self.CenterXY += Delta + self.EndXY += Delta + self.BoundingBox += Delta + + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + self.SetUpDraw(dc , WorldToPixel, ScaleWorldToPixel, HTdc) + StartXY = WorldToPixel(self.StartXY) + EndXY = WorldToPixel(self.EndXY) + CenterXY = WorldToPixel(self.CenterXY) + + dc.DrawArc(StartXY, EndXY, CenterXY) + if HTdc and self.HitAble: + HTdc.DrawArc(StartXY, EndXY, CenterXY) + + def CalcBoundingBox(self): + """Calculate the bounding box.""" + self.BoundingBox = BBox.asBBox( N.array((self.XY, (self.XY + self.WH) ), + N.float) ) + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + + +class PieChart(XYObjectMixin, LineOnlyMixin, DrawObject): + """ + This is DrawObject for a pie chart + + You can pass in a bunch of values, and it will draw a pie chart for + you, and it will make the chart, scaling the size of each "slice" to + match your values. + + The parameters are: + + XY : The (x,y) coords of the center of the chart + Diameter : The diamter of the chart in worls coords, unless you set + "Scaled" to False, in which case it's in pixel coords. + Values : sequence of values you want to make the chart of. + FillColors=None : sequence of colors you want the slices. If + None, it will choose (no guarantee youll like them!) + FillStyles=None : Fill style you want ("Solid", "Hash", etc) + LineColor = None : Color of lines separating the slices + LineStyle = "Solid" : style of lines separating the slices + LineWidth = 1 : With of lines separating the slices + Scaled = True : Do you want the pie to scale when zooming? or stay the same size in pixels? + InForeground = False: Should it be on the foreground? + + + """ + + + ##fixme: this should be a longer and better designed set. + ## Maybe one from: http://geography.uoregon.edu/datagraphics/color_scales.htm + DefaultColorList = Colors.CategoricalColor1 + #["Red", "Green", "Blue", "Purple", "Yellow", "Cyan"] + + def __init__(self, + XY, + Diameter, + Values, + FillColors=None, + FillStyles=None, + LineColor = None, + LineStyle = "Solid", + LineWidth = 1, + Scaled = True, + InForeground = False): + DrawObject.__init__(self, InForeground) + + self.XY = N.asarray(XY, N.float).reshape( (2,) ) + self.Diameter = Diameter + self.Values = N.asarray(Values, dtype=N.float).reshape((-1,1)) + if FillColors is None: + FillColors = self.DefaultColorList[:len(Values)] + if FillStyles is None: + FillStyles = ['Solid'] * len(FillColors) + self.FillColors = FillColors + self.FillStyles = FillStyles + self.LineColor = LineColor + self.LineStyle = LineStyle + + self.Scaled = Scaled + self.InForeground = InForeground + + self.SetPen(LineColor, LineStyle, LineWidth) + self.SetBrushes() + self.CalculatePoints() + + def SetFillColors(self, FillColors): + self.FillColors = FillColors + self.SetBrushes() + + def SetFillStyles(self, FillStyles): + self.FillStyles = FillStyles + self.SetBrushed() + + def SetValues(self, Values): + Values = N.asarray(Values, dtype=N.float).reshape((-1,1)) + self.Values = Values + self.CalculatePoints() + + def CalculatePoints(self): + # add the zero point to start + Values = N.vstack( ( (0,), self.Values) ) + self.Angles = 360. * Values.cumsum()/Values.sum() + self.CalcBoundingBox() + + def SetBrushes(self): + self.Brushes = [] + for FillColor, FillStyle in zip(self.FillColors, self.FillStyles): + if FillColor is None or FillStyle is None: + self.Brush = wx.TRANSPARENT_BRUSH + else: + self.Brushes.append(self.BrushList.setdefault( (FillColor, FillStyle), + wx.Brush( FillColor, self.FillStyleList[FillStyle] ) + ) + ) + def CalcBoundingBox(self): + if self.Scaled: + self.BoundingBox = BBox.asBBox( ((self.XY-self.Diameter),(self.XY+self.Diameter)) ) + else: + self.BoundingBox = BBox.asBBox((self.XY, self.XY)) + if self._Canvas: + self._Canvas.BoundingBoxDirty = True + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + CenterXY = WorldToPixel(self.XY) + if self.Scaled: + Diameter = ScaleWorldToPixel( (self.Diameter,self.Diameter) )[0] + else: + Diameter = self.Diameter + WH = N.array((Diameter,Diameter), dtype = N.float) + Corner = CenterXY - (WH / 2) + dc.SetPen(self.Pen) + for i, brush in enumerate(self.Brushes): + dc.SetBrush( brush ) + dc.DrawEllipticArc(Corner[0], Corner[1], WH[0], WH[1], self.Angles[i], self.Angles[i+1]) + if HTdc and self.HitAble: + if self.Scaled: + radius = (ScaleWorldToPixel(self.Diameter)/2)[0]# just the x-coord + else: + radius = self.Diameter/2 + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawCircle(CenterXY, radius) diff --git a/wx/lib/floatcanvas/FloatCanvas.py b/wx/lib/floatcanvas/FloatCanvas.py index 04b3752d..a698df66 100644 --- a/wx/lib/floatcanvas/FloatCanvas.py +++ b/wx/lib/floatcanvas/FloatCanvas.py @@ -53,15 +53,12 @@ from time import clock import wx from wx.lib import six +from .FCObjects import * + from .Utilities import BBox from . import GUIMode -## 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): @@ -107,404 +104,6 @@ class _MouseEvent(wx.PyCommandEvent): return getattr(self._NativeEvent, name) -## fixme: This should probably be re-factored into a class -_testBitmap = None -_testDC = None -def _cycleidxs(indexcount, maxvalue, step): - - """ - Utility function used by _colorGenerator - """ - def colormatch(color): - """Return True if the color comes back from the bitmap identically.""" - if len(color) < 3: - return True - global _testBitmap, _testDC - B = _testBitmap - if not mac: - dc = _testDC - if not B: - B = _testBitmap = wx.Bitmap(1, 1) - if not mac: - dc = _testDC = wx.MemoryDC() - if mac: - dc = wx.MemoryDC() - dc.SelectObject(B) - dc.SetBackground(wx.BLACK_BRUSH) - dc.Clear() - dc.SetPen(wx.Pen(wx.Colour(*color), 4)) - dc.DrawPoint(0,0) - if mac: - del dc - pdata = wx.AlphaPixelData(B) - pacc = pdata.GetPixels() - pacc.MoveTo(pdata, 0, 0) - outcolor = pacc.Get()[:3] - else: - outcolor = dc.GetPixel(0,0) - return outcolor == color - - if indexcount == 0: - yield () - else: - for idx in range(0, maxvalue, step): - for tail in _cycleidxs(indexcount - 1, maxvalue, step): - color = (idx, ) + tail - if not colormatch(color): - continue - yield color - -def _colorGenerator(): - - """ - Generates a series of unique colors used to do hit-tests with the Hit - Test bitmap - """ - return _cycleidxs(indexcount=3, maxvalue=256, step=1) - -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, see for example :class:`~lib.floatcanvas.FloatCanvas.Circle`. - - """ - #This class contains a series of static dictionaries: - - #* BrushList - #* PenList - #* FillStyleList - #* LineStyleList - - - def __init__(self, InForeground = False, IsVisible = True): - """ - Default class constructor. - - :param boolean `InForeground`: Define if object should be in foreground - or not - :param boolean `IsVisible`: Define if object should be visible - - """ - 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.BRUSHSTYLE_TRANSPARENT, - "Solid" : wx.BRUSHSTYLE_SOLID, - "BiDiagonalHatch": wx.BRUSHSTYLE_BDIAGONAL_HATCH, - "CrossDiagHatch" : wx.BRUSHSTYLE_CROSSDIAG_HATCH, - "FDiagonal_Hatch": wx.BRUSHSTYLE_FDIAGONAL_HATCH, - "CrossHatch" : wx.BRUSHSTYLE_CROSS_HATCH, - "HorizontalHatch": wx.BRUSHSTYLE_HORIZONTAL_HATCH, - "VerticalHatch" : wx.BRUSHSTYLE_VERTICAL_HATCH - } - - LineStyleList = { - "Solid" : wx.PENSTYLE_SOLID, - "Transparent": wx.PENSTYLE_TRANSPARENT, - "Dot" : wx.PENSTYLE_DOT, - "LongDash" : wx.PENSTYLE_LONG_DASH, - "ShortDash" : wx.PENSTYLE_SHORT_DASH, - "DotDash" : wx.PENSTYLE_DOT_DASH, - } - - def Bind(self, Event, CallBackFun): - """ - Bind an event to the DrawObject - - :param `Event`: see below for supported event types - - - 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 - - :param `CallBackFun`: the call back function for the event - - - """ - ##fixme: Way too much Canvas Manipulation here! - 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: - # first call to prevent the background color from being used. - self._Canvas.HitColorGenerator = _colorGenerator() - if six.PY3: - next(self._Canvas.HitColorGenerator) - else: - self._Canvas.HitColorGenerator.next() - if six.PY3: - self.HitColor = next(self._Canvas.HitColorGenerator) - else: - 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 its color - - def UnBindAll(self): - """ - Unbind all events - - .. note:: Currently only removes one from each list - - """ - ## fixme: this only removes one from each list, there could be more. - ## + patch by Tim Ansel - if self._Canvas.HitDict: - for Event in self._Canvas.HitDict.itervalues(): - try: - del Event[self.HitColor] - except KeyError: - pass - self.HitAble = False - - - def SetBrush(self, FillColor, FillStyle): - """ - Set the brush for this DrawObject - - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid entries - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - for valid entries - """ - 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])) - #print("Setting Brush, BrushList length:", len(self.BrushList)) - - def SetPen(self, LineColor, LineStyle, LineWidth): - """ - Set the Pen for this DrawObject - - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid entries - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - for valid entries - :param integer `LineWidth`: the width in pixels - """ - 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): - """ - Set the brush used for hit test, do not call directly. - - :param `HitColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - - """ - 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): - """ - Set the pen used for hit test, do not call directly. - - :param `HitColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param integer `LineWidth`: the line width in pixels - - """ - 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): - """ - Set the Color - this method is overridden in the subclasses - - :param `Color`: use one of the following values any valid entry from - :class:`ColourDatabase` - - - ``Green`` - - ``White`` - - ``Black`` - - ``Grey`` - - ``MediumGrey`` - - ``LightGrey`` - - ``Cyan`` - - ``Red`` - - """ - - pass - - def SetLineColor(self, LineColor): - """ - Set the LineColor - this method is overridden in the subclasses - - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid values - - """ - pass - - def SetLineStyle(self, LineStyle): - """ - Set the LineStyle - this method is overridden in the subclasses - - :param `LineStyle`: Use one of the following values or ``None`` : - - ===================== ============================= - Style Description - ===================== ============================= - ``Solid`` Solid line - ``Transparent`` A transparent line - ``Dot`` Dotted line - ``LongDash`` Dashed line (long) - ``ShortDash`` Dashed line (short) - ``DotDash`` Dash-dot-dash line - ===================== ============================= - - """ - pass - - def SetLineWidth(self, LineWidth): - """ - Set the LineWidth - - :param integer `LineWidth`: sets the line width in pixel - - """ - pass - - def SetFillColor(self, FillColor): - """ - Set the FillColor - this method is overridden in the subclasses - - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid values - - """ - pass - - def SetFillStyle(self, FillStyle): - """ - Set the FillStyle - this method is overridden in the subclasses - - :param string `FillStyle`: following is a list of valid values: - - ===================== ========================================= - Style Description - ===================== ========================================= - ``Transparent`` Transparent fill - ``Solid`` Solid fill - ``BiDiagonalHatch`` Bi Diagonal hatch fill - ``CrossDiagHatch`` Cross Diagonal hatch fill - ``FDiagonal_Hatch`` F Diagonal hatch fill - ``CrossHatch`` Cross hatch fill - ``HorizontalHatch`` Horizontal hatch fill - ``VerticalHatch`` Vertical hatch fill - ===================== ========================================= - - """ - pass - - def PutInBackground(self): - """Put the object in the background.""" - 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): - """Put the object in the foreground.""" - 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): - """Hide the object.""" - self.Visible = False - - def Show(self): - """Show the object.""" - self.Visible = True - class Group(DrawObject): """ A group of other FloatCanvas Objects @@ -693,2092 +292,6 @@ class Group(DrawObject): 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): - """ - Set the Color - - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid values - - """ - 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 a line, rather than a fill - color and line color - - """ - - def SetLineColor(self, LineColor): - """ - Set the LineColor - - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid values - - """ - self.LineColor = LineColor - self.SetPen(LineColor,self.LineStyle,self.LineWidth) - SetColor = SetLineColor# so that it will do something reasonable - - def SetLineStyle(self, LineStyle): - """ - Set the LineStyle - - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - for valid values - - """ - self.LineStyle = LineStyle - self.SetPen(self.LineColor,LineStyle,self.LineWidth) - - def SetLineWidth(self, LineWidth): - """ - Set the LineWidth - - :param integer `LineWidth`: line width in pixels - - """ - 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): - """ - Set the FillColor - - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - for valid values - - """ - self.FillColor = FillColor - self.SetBrush(FillColor, self.FillStyle) - - def SetFillStyle(self, FillStyle): - """ - Set the FillStyle - - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - for valid values - - """ - self.FillStyle = FillStyle - self.SetBrush(self.FillColor,FillStyle) - - def SetUpDraw(self, dc, WorldToPixel, ScaleWorldToPixel, HTdc): - """ - Setup for draw - - :param `dc`: the dc to draw ??? - :param `WorldToPixel`: ??? - :param `ScaleWorldToPixel`: ??? - :param `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): - """ - Moves the object by delta, where delta is a (dx, dy) pair. - - :param `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): - """Calculate the bounding box.""" - ## This may get overwritten in some subclasses - 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: - """ - 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): - """ - Moves the object by delta, where delta is a (dx, dy) pair. - - :param `Delta`: is a (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): - """Calculate the bounding box.""" - 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). - - :param `Points`: takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param boolean `copy`: By default, a copy is made, if copy is set to - ``False``, a reference is used, if 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 - # shifts the points 5 in the x dir, and 10 in the y dir. - Points += (5, 10) - # Sets the points to the same array as it was - Object.SetPoints(Points, False) - - """ - 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): - """Draws a polygon - - Points is a list of 2-tuples, or a NX2 NumPy array of - point coordinates. so that Points[N][0] is the x-coordinate of - point N and Points[N][1] is the y-coordinate or Points[N,0] is the - x-coordinate of point N and Points[N,1] is the y-coordinate for - arrays. - - """ - def __init__(self, - Points, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - """Default class constructor. - - :param `Points`: start point, takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - :param boolean `InForeground`: should object be in foreground - - """ - 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): - """Draws a line - - 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): - """Default class constructor. - - :param `Points`: takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param boolean `InForeground`: should object be in foreground - - """ - 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): - """Draws a spline""" - def __init__(self, *args, **kwargs): - """Default class constructor. - - see :class:`~lib.floatcanvas.FloatCanvas.Line` - - """ - 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): - """Draws an arrow - - It will draw an arrow , starting at the point ``XY`` points at an angle - defined by ``Direction``. - - """ - def __init__(self, - XY, - Length, - Direction, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 2, - ArrowHeadSize = 8, - ArrowHeadAngle = 30, - InForeground = False): - """Default class constructor. - - :param `XY`: the (x, y) coordinate of the starting point, or a 2-tuple, - or a (2,) `NumPy `_ array - :param integer `Length`: length of arrow in pixels - :param integer `Direction`: angle of arrow in degrees, zero is straight - up `+` angle is to the right - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `ArrowHeadSize`: size of arrow head in pixels - :param `ArrowHeadAngle`: angle of arrow head in degrees - :param boolean `InForeground`: should object be in foreground - - """ - - 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): - """Set the direction - - :param integer `Direction`: angle of arrow in degrees, zero is straight - up `+` angle is to the right - - """ - self.Direction = float(Direction) - self.CalcArrowPoints() - - def SetLength(self, Length): - """Set the length - - :param integer `Length`: length of arrow in pixels - - """ - self.Length = Length - self.CalcArrowPoints() - - def SetLengthDirection(self, Length, Direction): - """Set the lenght and direction - - :param integer `Length`: length of arrow in pixels - :param integer `Direction`: angle of arrow in degrees, zero is straight - up `+` angle is to the right - - """ - 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): - """Calculate the arrow points.""" - 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): - """Draws an arrow line. - - 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, - ArrowHeadSize = 8, - ArrowHeadAngle = 30, - InForeground = False): - """Default class constructor. - - :param `Points`: takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `ArrowHeadSize`: size of arrow head in pixels - :param `ArrowHeadAngle`: angle of arrow head in degrees - :param boolean `InForeground`: should object be in foreground - - """ - - 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): - """Calculate the arrow points.""" - 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 range(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): - """Draws a set of points - - 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): - """Default class constructor. - - :param `Points`: takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param integer `Diameter`: the points diameter - :param boolean `InForeground`: should object be in foreground - - """ - 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): - """Sets the diameter - - :param integer `Diameter`: the points 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. - - :param `XY`: the (x,y) coordinates of the point to look for, it takes a - 2-tuple or (2,) numpy array in World coordinates - - """ - 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): - """A point DrawObject - - .. note:: - - 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): - """Default class constructor. - - :param `XY`: the (x, y) coordinate of the center of the point, or a - 2-tuple, or a (2,) `NumPy `_ array - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param integer `Diameter`: in screen points - :param `InForeground`: define if object is in foreground - - """ - - 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): - """Set the diameter of the object. - - :param integer `Diameter`: in screen points - - """ - 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): - """Draws a square 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): - """Default class constructor. - - :param `Point`: takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param integer `Size`: the size of the square point - :param boolean `InForeground`: should object be in foreground - - """ - 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): - """Sets the size - - :param integer `Size`: the size of the square point - - """ - 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): - """A RectEllipse draw object.""" - def __init__(self, XY, WH, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - """Default class constructor. - - :param `XY`: the (x, y) coordinate of the corner of RectEllipse, or a 2-tuple, - or a (2,) `NumPy `_ array - :param `WH`: a tuple with the Width and Height for the object - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - :param `InForeground`: put object in foreground - - """ - - 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) - - # these define the behaviour when zooming makes the objects really small. - self.MinSize = 1 - self.DisappearWhenSmall = True - - self.SetPen(LineColor,LineStyle,LineWidth) - self.SetBrush(FillColor,FillStyle) - - def SetShape(self, XY, WH): - """Set the shape of the object. - - :param `XY`: takes a 2-tuple, or a (2,) `NumPy `_ - array of point coordinates - :param `WH`: a tuple with the Width and Height for the object - - """ - 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): - """Calculate the bounding box.""" - # 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): - """Draws a rectangle see :class:`~lib.floatcanvas.FloatCanvas.RectEllipse`""" - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - ( XY, WH ) = self.SetUpDraw(dc, - WorldToPixel, - ScaleWorldToPixel, - HTdc) - WH[N.abs(WH) < self.MinSize] = self.MinSize - if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny - dc.DrawRectangle(XY, WH) - if HTdc and self.HitAble: - HTdc.DrawRectangle(XY, WH) - - -class Ellipse(RectEllipse): - """Draws an ellipse see :class:`~lib.floatcanvas.FloatCanvas.RectEllipse`""" - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - ( XY, WH ) = self.SetUpDraw(dc, - WorldToPixel, - ScaleWorldToPixel, - HTdc) - WH[N.abs(WH) < self.MinSize] = self.MinSize - if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny - dc.DrawEllipse(XY, WH) - if HTdc and self.HitAble: - HTdc.DrawEllipse(XY, WH) - -class Circle(XYObjectMixin, LineAndFillMixin, DrawObject): - """Draws a circle""" - def __init__(self, XY, Diameter, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - """Default class constructor. - - :param `XY`: the (x, y) coordinate of the center of the circle, or a 2-tuple, - or a (2,) `NumPy `_ array - :param integer `Diameter`: the diameter for the object - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - :param boolean `InForeground`: should object be in foreground - - """ - DrawObject.__init__(self, InForeground) - - self.XY = N.array(XY, N.float) - self.WH = N.array((Diameter/2, Diameter/2), N.float) # just to keep it compatible with others - self.CalcBoundingBox() - - self.LineColor = LineColor - self.LineStyle = LineStyle - self.LineWidth = LineWidth - self.FillColor = FillColor - self.FillStyle = FillStyle - - self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - - # these define the behaviour when zooming makes the objects really small. - self.MinSize = 1 - self.DisappearWhenSmall = True - - self.SetPen(LineColor,LineStyle,LineWidth) - self.SetBrush(FillColor,FillStyle) - - def SetDiameter(self, Diameter): - """Set the diameter of the object - - :param integer `Diameter`: the diameter for the object - - """ - self.WH = N.array((Diameter/2, Diameter/2), N.float) # just to keep it compatible with others - - def CalcBoundingBox(self): - """Calculate the bounding box of the object.""" - # you need this in case Width or Height are negative - self.BoundingBox = BBox.fromPoints( (self.XY+self.WH, self.XY-self.WH) ) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - ( XY, WH ) = self.SetUpDraw(dc, - WorldToPixel, - ScaleWorldToPixel, - HTdc) - - WH[N.abs(WH) < self.MinSize] = self.MinSize - if not( self.DisappearWhenSmall and N.abs(WH).min() <= self.MinSize) : # don't try to draw it too tiny - dc.DrawCircle(XY, WH[0]) - if HTdc and self.HitAble: - HTdc.DrawCircle(XY, WH[0]) - - -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): - """Draws a text object - - 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.FONTFAMILY_MODERN, - Style = wx.FONTSTYLE_NORMAL, - Weight = wx.FONTWEIGHT_NORMAL, - Underlined = False, - Position = 'tl', - InForeground = False, - Font = None): - """Default class constructor. - - :param string `string`: the text to draw - :param `XY`: the (x, y) coordinate of the corner of the text, or a 2-tuple, - or a (2,) `NumPy `_ array - :param `Size`: the font size - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `BackgroundColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param FontFamily `Family`: a valid :ref:`FontFamily` - :param FontStyle `Style`: a valid :ref:`FontStyle` - :param FontWeight `Weight`: a valid :ref:`FontWeight` - :param boolean `Underlined`: underline the text - :param string `Position`: a two character string indicating where in - relation to the coordinates the box should be oriented - :param boolean `InForeground`: should object be in foreground - :param Font `Font`: alternatively you can define :ref:`Font` and the - above will be ignored. - - ============== ========================== - 1st character Meaning - ============== ========================== - ``t`` top - ``c`` center - ``b`` bottom - ============== ========================== - - ============== ========================== - 2nd character Meaning - ============== ========================== - ``l`` left - ``c`` center - ``r`` right - ============== ========================== - - :param Font `Font`: a valid :class:`Font` - :param boolean `InForeground`: should object be in foreground - - """ - 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.DrawText(self.String, XY) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(XY, (self.TextWidth, self.TextHeight) ) - -class ScaledText(TextObjectMixin, DrawObject): - """ - ##fixme: this can be depricated and jsut use ScaledTextBox with different defaults. - - 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.FONTFAMILY_DEFAULT: Chooses a default font. - * wx.FONTFAMILY_DECORATIVE: A decorative font. - * wx.FONTFAMILY_ROMAN: A formal, serif font. - * wx.FONTFAMILY_SCRIPT: A handwriting font. - * wx.FONTFAMILY_SWISS: A sans-serif font. - * wx.FONTFAMILY_MODERN: A fixed pitch font. - - .. note:: these are only as good as the wxWindows defaults, which aren't so good. - - * Style: One of wx.FONTSTYLE_NORMAL, wx.FONTSTYLE_SLANT and wx.FONTSTYLE_ITALIC. - * Weight: One of wx.FONTWEIGHT_NORMAL, wx.FONTWEIGHT_LIGHT and wx.FONTWEIGHT_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.FONTFAMILY_MODERN, - Style = wx.FONTSTYLE_NORMAL, - Weight = wx.FONTWEIGHT_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.MinFontSize = 1 # this can be changed to set a minimum size - self.DisappearWhenSmall = True - 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.Bitmap(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) - Size = max(Size, self.MinFontSize) # smallest size you want - default to 0 - - # Draw the Text - if not( self.DisappearWhenSmall and Size <= self.MinFontSize) : # don't try to draw a zero sized font! - 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.DrawText(self.String, xy) - - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(xy, (w, h)) - -class ScaledTextBox(TextObjectMixin, DrawObject): - """Draws a text object - - The object is scaled when zoomed. - - The hit-test is done on the entire text extent - - 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. - - """ - - def __init__(self, String, - Point, - Size, - Color = "Black", - BackgroundColor = None, - LineColor = 'Black', - LineStyle = 'Solid', - LineWidth = 1, - Width = None, - PadSize = None, - Family = wx.FONTFAMILY_MODERN, - Style = wx.FONTSTYLE_NORMAL, - Weight = wx.FONTWEIGHT_NORMAL, - Underlined = False, - Position = 'tl', - Alignment = "left", - Font = None, - LineSpacing = 1.0, - InForeground = False): - """Default class constructor. - - :param `Point`: takes a 2-tuple, or a (2,) `NumPy `_ - array of point coordinates - :param integer `Size`: size in World units - :param `Color`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `BackgroundColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `Width`: width in pixels or ``None``, text will be wrapped to - the given width. - :param `PadSize`: padding in world units or ``None``, if specified it - will creating a space (margin) around the text - :param FontFamily `Family`: a valid :ref:`FontFamily` - :param FontStyle `Style`: a valid :ref:`FontStyle` - :param FontWeight `Weight`: a valid :ref:`FontWeight` - :param boolean `Underlined`: underline the text - :param string `Position`: a two character string indicating where in - relation to the coordinates the box should be oriented - - ============== ========================== - 1st character Meaning - ============== ========================== - ``t`` top - ``c`` center - ``b`` bottom - ============== ========================== - - ============== ========================== - 2nd character Meaning - ============== ========================== - ``l`` left - ``c`` center - ``r`` right - ============== ========================== - - :param `Alignment`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param Font `Font`: alternatively a valid :class:`Font` can be defined - in which case the above will be ignored - :param float `LineSpacing`: the line space to be used - :param boolean `InForeground`: should object be in foreground - - """ - 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.MinFontSize = 1 # this can be changed to set a larger minimum size - self.DisappearWhenSmall = True - - 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.Bitmap(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.Bitmap(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) - - Size = max(Size, self.MinFontSize) # smallest size you want - default to 1 - - # Draw The Box - if (self.LineStyle and self.LineColor) or self.BackgroundColor: - dc.SetBrush(self.Brush) - dc.SetPen(self.Pen) - dc.DrawRectangle(xy , wh) - - # Draw the Text - if not( self.DisappearWhenSmall and Size <= self.MinFontSize) : # don't try to draw a zero sized font! - 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) - dc.DrawTextList(self.Words, Points) - - # Draw the hit box. - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(xy, wh) - -class Bitmap(TextObjectMixin, DrawObject): - """Draws a bitmap - - The size is fixed, and does not scale with the drawing. - - """ - - def __init__(self, Bitmap, XY, - Position='tl', - InForeground=False): - """Default class constructor. - - :param Bitmap `Bitmap`: the bitmap to be drawn - :param `XY`: the (x, y) coordinate of the corner of the bitmap, or a 2-tuple, - or a (2,) `NumPy `_ array - :param string `Position`: a two character string indicating where in relation to the coordinates - the bitmap should be oriented - - ============== ========================== - 1st character Meaning - ============== ========================== - ``t`` top - ``c`` center - ``b`` bottom - ============== ========================== - - ============== ========================== - 2nd character Meaning - ============== ========================== - ``l`` left - ``c`` center - ``r`` right - ============== ========================== - - :param boolean `InForeground`: should object be in foreground - - """ - - DrawObject.__init__(self,InForeground) - - if type(Bitmap) == wx.Bitmap: - self.Bitmap = Bitmap - elif type(Bitmap) == wx.Image: - self.Bitmap = wx.Bitmap(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.DrawBitmap(self.Bitmap, XY, True) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(XY, (self.Width, self.Height) ) - -class ScaledBitmap(TextObjectMixin, DrawObject): - """Draws a scaled bitmap - - The size scales with the drawing - - """ - - def __init__(self, - Bitmap, - XY, - Height, - Position = 'tl', - InForeground = False): - """Default class constructor. - - :param wx.Bitmap `Bitmap`: the bitmap to be drawn - :param `XY`: the (x, y) coordinate of the corner of the scaled bitmap, - or a 2-tuple, or a (2,) `NumPy `_ array - :param `Height`: height to be used, width is calculated from the aspect ratio of the bitmap - :param string `Position`: a two character string indicating where in relation to the coordinates - the bitmap should be oriented - - ============== ========================== - 1st character Meaning - ============== ========================== - ``t`` top - ``c`` center - ``b`` bottom - ============== ========================== - - ============== ========================== - 2nd character Meaning - ============== ========================== - ``l`` left - ``c`` center - ``r`` right - ============== ========================== - - :param boolean `InForeground`: should object be in foreground - - """ - - DrawObject.__init__(self,InForeground) - - if type(Bitmap) == wx.Bitmap: - self.Image = Bitmap.ConvertToImage() - elif type(Bitmap) == wx.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): - """Calculate the bounding box.""" - ## 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.Bitmap(Img) - - XY = self.ShiftFun(XY[0], XY[1], W, H) - dc.DrawBitmap(self.ScaledBitmap, XY, True) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(XY, (W, H) ) - -class ScaledBitmap2(TextObjectMixin, DrawObject, ): - """Draws a scaled bitmap - - 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): - """Default class constructor. - - :param wx.Bitmap `Bitmap`: the bitmap to be drawn - :param `XY`: the (x, y) coordinate of the corner of the scaled bitmap, - or a 2-tuple, or a (2,) `NumPy `_ array - :param `Height`: height to be used - :param `Width`: width to be used, if ``None`` width is calculated from the aspect ratio of the bitmap - :param string `Position`: a two character string indicating where in relation to the coordinates - the bitmap should be oriented - - ============== ========================== - 1st character Meaning - ============== ========================== - ``t`` top - ``c`` center - ``b`` bottom - ============== ========================== - - ============== ========================== - 2nd character Meaning - ============== ========================== - ``l`` left - ``c`` center - ``r`` right - ============== ========================== - - :param boolean `InForeground`: should object be in foreground - - """ - - DrawObject.__init__(self,InForeground) - - if type(Bitmap) == wx.Bitmap: - self.Image = Bitmap.ConvertToImage() - elif type(Bitmap) == wx.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): - """Calculate the bounding box.""" - ## 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 the 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.Bitmap(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.DrawBitmap(bmp, XY, True) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(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)) - print("rescaling with High quality") - Img.Rescale(Ws, Hs, quality=wx.IMAGE_QUALITY_HIGH) - bmp = wx.Bitmap(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.DrawBitmap(bmp, XYs, True) - - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawRectangle(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") - pass - -class DotGrid: - """ - An example of a Grid Object -- it is set on the 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 -- it really 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 Arc(XYObjectMixin, LineAndFillMixin, DrawObject): - """Draws an arc of a circle, centered on point ``CenterXY``, from - the first point ``StartXY`` to the second ``EndXY``. - - The arc is drawn in an anticlockwise direction from the start point to - the end point. - - """ - def __init__(self, - StartXY, - EndXY, - CenterXY, - LineColor = "Black", - LineStyle = "Solid", - LineWidth = 1, - FillColor = None, - FillStyle = "Solid", - InForeground = False): - """Default class constructor. - - :param `StartXY`: start point, takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `EndXY`: end point, takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `CenterXY`: center point, takes a 2-tuple, or a (2,) - `NumPy `_ array of point coordinates - :param `LineColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `LineStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineStyle` - :param `LineWidth`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetLineWidth` - :param `FillColor`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetColor` - :param `FillStyle`: see :meth:`~lib.floatcanvas.FloatCanvas.DrawObject.SetFillStyle` - :param boolean `InForeground`: should object be in foreground - - """ - - DrawObject.__init__(self, InForeground) - - # There is probably a more elegant way to do this next section - # The bounding box just gets set to the WH of a circle, with center at CenterXY - # This is suitable for a pie chart as it will be a circle anyway - radius = N.sqrt( (StartXY[0]-CenterXY[0])**2 + (StartXY[1]-CenterXY[1])**2 ) - minX = CenterXY[0]-radius - minY = CenterXY[1]-radius - maxX = CenterXY[0]+radius - maxY = CenterXY[1]+radius - XY = [minX,minY] - WH = [maxX-minX,maxY-minY] - - self.XY = N.asarray( XY, N.float).reshape((2,)) - self.WH = N.asarray( WH, N.float).reshape((2,)) - - self.StartXY = N.asarray(StartXY, N.float).reshape((2,)) - self.CenterXY = N.asarray(CenterXY, N.float).reshape((2,)) - self.EndXY = N.asarray(EndXY, N.float).reshape((2,)) - - #self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float) - self.CalcBoundingBox() - - #Finish the setup; allocate color,style etc. - 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) #Why isn't this working ??? - - def Move(self, Delta): - """Move the object by delta - - :param `Delta`: delta is a (dx, dy) pair. Ideally a `NumPy `_ - array of shape (2,) - - """ - - Delta = N.asarray(Delta, N.float) - self.XY += Delta - self.StartXY += Delta - self.CenterXY += Delta - self.EndXY += Delta - self.BoundingBox += Delta - - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - self.SetUpDraw(dc , WorldToPixel, ScaleWorldToPixel, HTdc) - StartXY = WorldToPixel(self.StartXY) - EndXY = WorldToPixel(self.EndXY) - CenterXY = WorldToPixel(self.CenterXY) - - dc.DrawArc(StartXY, EndXY, CenterXY) - if HTdc and self.HitAble: - HTdc.DrawArc(StartXY, EndXY, CenterXY) - - def CalcBoundingBox(self): - """Calculate the bounding box.""" - self.BoundingBox = BBox.asBBox( N.array((self.XY, (self.XY + self.WH) ), - N.float) ) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - #--------------------------------------------------------------------------- class FloatCanvas(wx.Panel): """ @@ -2815,7 +328,6 @@ class FloatCanvas(wx.Panel): wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size, **kwargs) - self.ComputeFontScale() self.InitAll() self.BackgroundBrush = wx.Brush(BackgroundColor, wx.SOLID) @@ -2857,18 +369,6 @@ class FloatCanvas(wx.Panel): # self.CreateCursors() - def ComputeFontScale(self): - """ - Compute the font scale. - - A global variable to hold the scaling from pixel size to point size. - """ - global FontScale - dc = wx.ScreenDC() - dc.SetFont(wx.Font(16, wx.FONTFAMILY_ROMAN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) - E = dc.GetTextExtent("X") - FontScale = 16/E[1] - del dc def InitAll(self): """ diff --git a/wx/lib/floatcanvas/SpecialObjects/PieChart.py b/wx/lib/floatcanvas/SpecialObjects/PieChart.py deleted file mode 100644 index 2325b0c4..00000000 --- a/wx/lib/floatcanvas/SpecialObjects/PieChart.py +++ /dev/null @@ -1,138 +0,0 @@ -import wx - -## import a local version of FloatCanvas - - -from wx.lib.floatcanvas import FloatCanvas -from wx.lib.floatcanvas.Utilities import BBox -from wx.lib.floatcanvas.Utilities import Colors - -import numpy as N - -XYObjectMixin = FloatCanvas.XYObjectMixin -LineOnlyMixin = FloatCanvas.LineOnlyMixin -DrawObject = FloatCanvas.DrawObject - -class PieChart(XYObjectMixin, LineOnlyMixin, DrawObject): - """ - This is DrawObject for a pie chart - - You can pass in a bunch of values, and it will draw a pie chart for - you, and it will make the chart, scaling the size of each "slice" to - match your values. - - The parameters are: - - XY : The (x,y) coords of the center of the chart - Diameter : The diamter of the chart in worls coords, unless you set - "Scaled" to False, in which case it's in pixel coords. - Values : sequence of values you want to make the chart of. - FillColors=None : sequence of colors you want the slices. If - None, it will choose (no guarantee youll like them!) - FillStyles=None : Fill style you want ("Solid", "Hash", etc) - LineColor = None : Color of lines separating the slices - LineStyle = "Solid" : style of lines separating the slices - LineWidth = 1 : With of lines separating the slices - Scaled = True : Do you want the pie to scale when zooming? or stay the same size in pixels? - InForeground = False: Should it be on the foreground? - - - """ - - - ##fixme: this should be a longer and better designed set. - ## Maybe one from: http://geography.uoregon.edu/datagraphics/color_scales.htm - DefaultColorList = Colors.CategoricalColor1 - #["Red", "Green", "Blue", "Purple", "Yellow", "Cyan"] - - def __init__(self, - XY, - Diameter, - Values, - FillColors=None, - FillStyles=None, - LineColor = None, - LineStyle = "Solid", - LineWidth = 1, - Scaled = True, - InForeground = False): - DrawObject.__init__(self, InForeground) - - self.XY = N.asarray(XY, N.float).reshape( (2,) ) - self.Diameter = Diameter - self.Values = N.asarray(Values, dtype=N.float).reshape((-1,1)) - if FillColors is None: - FillColors = self.DefaultColorList[:len(Values)] - if FillStyles is None: - FillStyles = ['Solid'] * len(FillColors) - self.FillColors = FillColors - self.FillStyles = FillStyles - self.LineColor = LineColor - self.LineStyle = LineStyle - - self.Scaled = Scaled - self.InForeground = InForeground - - self.SetPen(LineColor, LineStyle, LineWidth) - self.SetBrushes() - self.CalculatePoints() - - def SetFillColors(self, FillColors): - self.FillColors = FillColors - self.SetBrushes() - - def SetFillStyles(self, FillStyles): - self.FillStyles = FillStyles - self.SetBrushed() - - def SetValues(self, Values): - Values = N.asarray(Values, dtype=N.float).reshape((-1,1)) - self.Values = Values - self.CalculatePoints() - - def CalculatePoints(self): - # add the zero point to start - Values = N.vstack( ( (0,), self.Values) ) - self.Angles = 360. * Values.cumsum()/Values.sum() - self.CalcBoundingBox() - - def SetBrushes(self): - self.Brushes = [] - for FillColor, FillStyle in zip(self.FillColors, self.FillStyles): - if FillColor is None or FillStyle is None: - self.Brush = wx.TRANSPARENT_BRUSH - else: - self.Brushes.append(self.BrushList.setdefault( (FillColor, FillStyle), - wx.Brush( FillColor, self.FillStyleList[FillStyle] ) - ) - ) - def CalcBoundingBox(self): - if self.Scaled: - self.BoundingBox = BBox.asBBox( ((self.XY-self.Diameter),(self.XY+self.Diameter)) ) - else: - self.BoundingBox = BBox.asBBox((self.XY, self.XY)) - if self._Canvas: - self._Canvas.BoundingBoxDirty = True - - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): - CenterXY = WorldToPixel(self.XY) - if self.Scaled: - Diameter = ScaleWorldToPixel( (self.Diameter,self.Diameter) )[0] - else: - Diameter = self.Diameter - WH = N.array((Diameter,Diameter), dtype = N.float) - Corner = CenterXY - (WH / 2) - dc.SetPen(self.Pen) - for i, brush in enumerate(self.Brushes): - dc.SetBrush( brush ) - dc.DrawEllipticArc(Corner[0], Corner[1], WH[0], WH[1], self.Angles[i], self.Angles[i+1]) - if HTdc and self.HitAble: - if self.Scaled: - radius = (ScaleWorldToPixel(self.Diameter)/2)[0]# just the x-coord - else: - radius = self.Diameter/2 - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - HTdc.DrawCircle(CenterXY, radius) - - diff --git a/wx/lib/floatcanvas/SpecialObjects/__init__.py b/wx/lib/floatcanvas/SpecialObjects/__init__.py deleted file mode 100644 index 14cf0fa3..00000000 --- a/wx/lib/floatcanvas/SpecialObjects/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -SpecialObjects Package - -Various special objects -- to specific for inclusion in the main FloatCanvas - -""" -from PieChart import PieChart diff --git a/wx/lib/floatcanvas/Utilities/Colors.py b/wx/lib/floatcanvas/Utilities/Colors.py index 939e3b54..ea8b5831 100755 --- a/wx/lib/floatcanvas/Utilities/Colors.py +++ b/wx/lib/floatcanvas/Utilities/Colors.py @@ -1,4 +1,16 @@ #!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: Colors.py +# Purpose: Contains color lists used in FloatCanvas +# +# Author: +# +# Created: +# Version: +# Date: +# Licence: +# Tags: phoenix-port, unittest, documented, py3-port +#---------------------------------------------------------------------------- """ Colors.py diff --git a/wx/lib/floatcanvas/Utilities/GUI.py b/wx/lib/floatcanvas/Utilities/GUI.py index 075c4492..44df6c4d 100644 --- a/wx/lib/floatcanvas/Utilities/GUI.py +++ b/wx/lib/floatcanvas/Utilities/GUI.py @@ -1,3 +1,16 @@ +#!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: GUI.py +# Purpose: Contains GUI related utilities for FloatCanvas +# +# Author: +# +# Created: +# Version: +# Date: +# Licence: +# Tags: phoenix-port, unittest, documented, py3-port +#---------------------------------------------------------------------------- """ Part of the floatcanvas.Utilities package.