~toolpart/openobject-server/toolpart

« back to all changes in this revision

Viewing changes to bin/reportlab/graphics/shapes.py

  • Committer: pinky
  • Date: 2006-12-07 13:41:40 UTC
  • Revision ID: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
New trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#Copyright ReportLab Europe Ltd. 2000-2004
 
2
#see license.txt for license details
 
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/shapes.py
 
4
"""
 
5
core of the graphics library - defines Drawing and Shapes
 
6
"""
 
7
__version__=''' $Id$ '''
 
8
 
 
9
import string, os, sys
 
10
from math import pi, cos, sin, tan
 
11
from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType
 
12
from pprint import pprint
 
13
 
 
14
from reportlab.platypus import Flowable
 
15
from reportlab.rl_config import shapeChecking, verbose
 
16
from reportlab.lib import logger
 
17
from reportlab.lib import colors
 
18
from reportlab.lib.validators import *
 
19
from reportlab.lib.attrmap import *
 
20
from reportlab.lib.utils import fp_str
 
21
from reportlab.pdfbase.pdfmetrics import stringWidth
 
22
 
 
23
class NotImplementedError(Exception):
 
24
    pass
 
25
 
 
26
# two constants for filling rules
 
27
NON_ZERO_WINDING = 'Non-Zero Winding'
 
28
EVEN_ODD = 'Even-Odd'
 
29
 
 
30
## these can be overridden at module level before you start
 
31
#creating shapes.  So, if using a special color model,
 
32
#this provides support for the rendering mechanism.
 
33
#you can change defaults globally before you start
 
34
#making shapes; one use is to substitute another
 
35
#color model cleanly throughout the drawing.
 
36
 
 
37
STATE_DEFAULTS = {   # sensible defaults for all
 
38
    'transform': (1,0,0,1,0,0),
 
39
 
 
40
    # styles follow SVG naming
 
41
    'strokeColor': colors.black,
 
42
    'strokeWidth': 1,
 
43
    'strokeLineCap': 0,
 
44
    'strokeLineJoin': 0,
 
45
    'strokeMiterLimit' : 'TBA',  # don't know yet so let bomb here
 
46
    'strokeDashArray': None,
 
47
    'strokeOpacity': 1.0,  #100%
 
48
 
 
49
    'fillColor': colors.black,   #...or text will be invisible
 
50
    #'fillRule': NON_ZERO_WINDING, - these can be done later
 
51
    #'fillOpacity': 1.0,  #100% - can be done later
 
52
 
 
53
    'fontSize': 10,
 
54
    'fontName': 'Times-Roman',
 
55
    'textAnchor':  'start' # can be start, middle, end, inherited
 
56
    }
 
57
 
 
58
 
 
59
####################################################################
 
60
# math utilities.  These could probably be moved into lib
 
61
# somewhere.
 
62
####################################################################
 
63
 
 
64
# constructors for matrices:
 
65
def nullTransform():
 
66
    return (1, 0, 0, 1, 0, 0)
 
67
 
 
68
def translate(dx, dy):
 
69
    return (1, 0, 0, 1, dx, dy)
 
70
 
 
71
def scale(sx, sy):
 
72
    return (sx, 0, 0, sy, 0, 0)
 
73
 
 
74
def rotate(angle):
 
75
    a = angle * pi/180
 
76
    return (cos(a), sin(a), -sin(a), cos(a), 0, 0)
 
77
 
 
78
def skewX(angle):
 
79
    a = angle * pi/180
 
80
    return (1, 0, tan(a), 1, 0, 0)
 
81
 
 
82
def skewY(angle):
 
83
    a = angle * pi/180
 
84
    return (1, tan(a), 0, 1, 0, 0)
 
85
 
 
86
def mmult(A, B):
 
87
    "A postmultiplied by B"
 
88
    # I checked this RGB
 
89
    # [a0 a2 a4]    [b0 b2 b4]
 
90
    # [a1 a3 a5] *  [b1 b3 b5]
 
91
    # [      1 ]    [      1 ]
 
92
    #
 
93
    return (A[0]*B[0] + A[2]*B[1],
 
94
            A[1]*B[0] + A[3]*B[1],
 
95
            A[0]*B[2] + A[2]*B[3],
 
96
            A[1]*B[2] + A[3]*B[3],
 
97
            A[0]*B[4] + A[2]*B[5] + A[4],
 
98
            A[1]*B[4] + A[3]*B[5] + A[5])
 
99
 
 
100
def inverse(A):
 
101
    "For A affine 2D represented as 6vec return 6vec version of A**(-1)"
 
102
    # I checked this RGB
 
103
    det = float(A[0]*A[3] - A[2]*A[1])
 
104
    R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det]
 
105
    return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]])
 
106
 
 
107
def zTransformPoint(A,v):
 
108
    "Apply the homogenous part of atransformation a to vector v --> A*v"
 
109
    return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1])
 
110
 
 
111
def transformPoint(A,v):
 
112
    "Apply transformation a to vector v --> A*v"
 
113
    return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5])
 
114
 
 
115
def transformPoints(matrix, V):
 
116
    return map(transformPoint, V)
 
117
 
 
118
def zTransformPoints(matrix, V):
 
119
    return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V)
 
120
 
 
121
def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor):
 
122
    w = 0
 
123
    for t in text:
 
124
        w = max(w,stringWidth(t,font, fontSize))
 
125
 
 
126
    h = len(text)*leading
 
127
    yt = fontSize
 
128
    if boxAnchor[0]=='s':
 
129
        yb = -h
 
130
        yt = yt - h
 
131
    elif boxAnchor[0]=='n':
 
132
        yb = 0
 
133
    else:
 
134
        yb = -h/2.0
 
135
        yt = yt + yb
 
136
 
 
137
    if boxAnchor[-1]=='e':
 
138
        xb = -w
 
139
        if textAnchor=='end': xt = 0
 
140
        elif textAnchor=='start': xt = -w
 
141
        else: xt = -w/2.0
 
142
    elif boxAnchor[-1]=='w':
 
143
        xb = 0
 
144
        if textAnchor=='end': xt = w
 
145
        elif textAnchor=='start': xt = 0
 
146
        else: xt = w/2.0
 
147
    else:
 
148
        xb = -w/2.0
 
149
        if textAnchor=='end': xt = -xb
 
150
        elif textAnchor=='start': xt = xb
 
151
        else: xt = 0
 
152
 
 
153
    return xb, yb, w, h, xt, yt
 
154
 
 
155
def _rotatedBoxLimits( x, y, w, h, angle):
 
156
    '''
 
157
    Find the corner points of the rotated w x h sized box at x,y
 
158
    return the corner points and the min max points in the original space
 
159
    '''
 
160
    C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h)))
 
161
    X = map(lambda x: x[0], C)
 
162
    Y = map(lambda x: x[1], C)
 
163
    return min(X), max(X), min(Y), max(Y), C
 
164
 
 
165
 
 
166
class _DrawTimeResizeable:
 
167
    '''Addin class to provide the horribleness of _drawTimeResize'''
 
168
    def _drawTimeResize(self,w,h):
 
169
        if hasattr(self,'_canvas'):
 
170
            canvas = self._canvas
 
171
            drawing = canvas._drawing
 
172
            drawing.width, drawing.height = w, h
 
173
            if hasattr(canvas,'_drawTimeResize'):
 
174
                canvas._drawTimeResize(w,h)
 
175
 
 
176
class _SetKeyWordArgs:
 
177
    def __init__(self, keywords={}):
 
178
        """In general properties may be supplied to the constructor."""
 
179
        for key, value in keywords.items():
 
180
            setattr(self, key, value)
 
181
 
 
182
 
 
183
#################################################################
 
184
#
 
185
#    Helper functions for working out bounds
 
186
#
 
187
#################################################################
 
188
 
 
189
def getRectsBounds(rectList):
 
190
    # filter out any None objects, e.g. empty groups
 
191
    L = filter(lambda x: x is not None, rectList)
 
192
    if not L: return None
 
193
 
 
194
    xMin, yMin, xMax, yMax = L[0]
 
195
    for (x1, y1, x2, y2) in L[1:]:
 
196
        if x1 < xMin:
 
197
            xMin = x1
 
198
        if x2 > xMax:
 
199
            xMax = x2
 
200
        if y1 < yMin:
 
201
            yMin = y1
 
202
        if y2 > yMax:
 
203
            yMax = y2
 
204
    return (xMin, yMin, xMax, yMax)
 
205
 
 
206
def getPathBounds(points):
 
207
    n = len(points)
 
208
    f = lambda i,p = points: p[i]
 
209
    xs = map(f,xrange(0,n,2))
 
210
    ys = map(f,xrange(1,n,2))
 
211
    return (min(xs), min(ys), max(xs), max(ys))
 
212
 
 
213
def getPointsBounds(pointList):
 
214
    "Helper function for list of points"
 
215
    first = pointList[0]
 
216
    if type(first) in (ListType, TupleType):
 
217
        xs = map(lambda xy: xy[0],pointList)
 
218
        ys = map(lambda xy: xy[1],pointList)
 
219
        return (min(xs), min(ys), max(xs), max(ys))
 
220
    else:
 
221
        return getPathBounds(pointList)
 
222
 
 
223
#################################################################
 
224
#
 
225
#    And now the shapes themselves....
 
226
#
 
227
#################################################################
 
228
class Shape(_SetKeyWordArgs,_DrawTimeResizeable):
 
229
    """Base class for all nodes in the tree. Nodes are simply
 
230
    packets of data to be created, stored, and ultimately
 
231
    rendered - they don't do anything active.  They provide
 
232
    convenience methods for verification but do not
 
233
    check attribiute assignments or use any clever setattr
 
234
    tricks this time."""
 
235
    _attrMap = AttrMap()
 
236
 
 
237
    def copy(self):
 
238
        """Return a clone of this shape."""
 
239
 
 
240
        # implement this in the descendants as they need the right init methods.
 
241
        raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__
 
242
 
 
243
    def getProperties(self,recur=1):
 
244
        """Interface to make it easy to extract automatic
 
245
        documentation"""
 
246
 
 
247
        #basic nodes have no children so this is easy.
 
248
        #for more complex objects like widgets you
 
249
        #may need to override this.
 
250
        props = {}
 
251
        for key, value in self.__dict__.items():
 
252
            if key[0:1] <> '_':
 
253
                props[key] = value
 
254
        return props
 
255
 
 
256
    def setProperties(self, props):
 
257
        """Supports the bulk setting if properties from,
 
258
        for example, a GUI application or a config file."""
 
259
 
 
260
        self.__dict__.update(props)
 
261
        #self.verify()
 
262
 
 
263
    def dumpProperties(self, prefix=""):
 
264
        """Convenience. Lists them on standard output.  You
 
265
        may provide a prefix - mostly helps to generate code
 
266
        samples for documentation."""
 
267
 
 
268
        propList = self.getProperties().items()
 
269
        propList.sort()
 
270
        if prefix:
 
271
            prefix = prefix + '.'
 
272
        for (name, value) in propList:
 
273
            print '%s%s = %s' % (prefix, name, value)
 
274
 
 
275
    def verify(self):
 
276
        """If the programmer has provided the optional
 
277
        _attrMap attribute, this checks all expected
 
278
        attributes are present; no unwanted attributes
 
279
        are present; and (if a checking function is found)
 
280
        checks each attribute.  Either succeeds or raises
 
281
        an informative exception."""
 
282
 
 
283
        if self._attrMap is not None:
 
284
            for key in self.__dict__.keys():
 
285
                if key[0] <> '_':
 
286
                    assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self)
 
287
            for (attr, metavalue) in self._attrMap.items():
 
288
                assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self)
 
289
                value = getattr(self, attr)
 
290
                assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__)
 
291
 
 
292
    if shapeChecking:
 
293
        """This adds the ability to check every attribute assignment as it is made.
 
294
        It slows down shapes but is a big help when developing. It does not
 
295
        get defined if rl_config.shapeChecking = 0"""
 
296
        def __setattr__(self, attr, value):
 
297
            """By default we verify.  This could be off
 
298
            in some parallel base classes."""
 
299
            validateSetattr(self,attr,value)    #from reportlab.lib.attrmap
 
300
 
 
301
    def getBounds(self):
 
302
        "Returns bounding rectangle of object as (x1,y1,x2,y2)"
 
303
        raise NotImplementedError("Shapes and widgets must implement getBounds")
 
304
 
 
305
class Group(Shape):
 
306
    """Groups elements together.  May apply a transform
 
307
    to its contents.  Has a publicly accessible property
 
308
    'contents' which may be used to iterate over contents.
 
309
    In addition, child nodes may be given a name in which
 
310
    case they are subsequently accessible as properties."""
 
311
 
 
312
    _attrMap = AttrMap(
 
313
        transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"),
 
314
        contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"),
 
315
        )
 
316
 
 
317
    def __init__(self, *elements, **keywords):
 
318
        """Initial lists of elements may be provided to allow
 
319
        compact definitions in literal Python code.  May or
 
320
        may not be useful."""
 
321
 
 
322
        # Groups need _attrMap to be an instance rather than
 
323
        # a class attribute, as it may be extended at run time.
 
324
        self._attrMap = self._attrMap.clone()
 
325
        self.contents = []
 
326
        self.transform = (1,0,0,1,0,0)
 
327
        for elt in elements:
 
328
            self.add(elt)
 
329
        # this just applies keywords; do it at the end so they
 
330
        #don;t get overwritten
 
331
        _SetKeyWordArgs.__init__(self, keywords)
 
332
 
 
333
    def _addNamedNode(self,name,node):
 
334
        'if name is not None add an attribute pointing to node and add to the attrMap'
 
335
        if name:
 
336
            if name not in self._attrMap.keys():
 
337
                self._attrMap[name] = AttrMapValue(isValidChild)
 
338
            setattr(self, name, node)
 
339
 
 
340
    def add(self, node, name=None):
 
341
        """Appends non-None child node to the 'contents' attribute. In addition,
 
342
        if a name is provided, it is subsequently accessible by name
 
343
        """
 
344
        # propagates properties down
 
345
        if node is not None:
 
346
            assert isValidChild(node), "Can only add Shape or UserNode objects to a Group"
 
347
            self.contents.append(node)
 
348
            self._addNamedNode(name,node)
 
349
 
 
350
    def _nn(self,node):
 
351
        self.add(node)
 
352
        return self.contents[-1]
 
353
 
 
354
    def insert(self, i, n, name=None):
 
355
        'Inserts sub-node n in contents at specified location'
 
356
        if n is not None:
 
357
            assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group"
 
358
            if i<0:
 
359
                self.contents[i:i] =[n]
 
360
            else:
 
361
                self.contents.insert(i,n)
 
362
            self._addNamedNode(name,n)
 
363
 
 
364
    def expandUserNodes(self):
 
365
        """Return a new object which only contains primitive shapes."""
 
366
 
 
367
        # many limitations - shared nodes become multiple ones,
 
368
        obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group()
 
369
        obj._attrMap = self._attrMap.clone()
 
370
        if hasattr(obj,'transform'): obj.transform = self.transform[:]
 
371
 
 
372
        self_contents = self.contents
 
373
        a = obj.contents.append
 
374
        for child in self_contents:
 
375
            if isinstance(child, UserNode):
 
376
                newChild = child.provideNode()
 
377
            elif isinstance(child, Group):
 
378
                newChild = child.expandUserNodes()
 
379
            else:
 
380
                newChild = child.copy()
 
381
            a(newChild)
 
382
 
 
383
        self._copyNamedContents(obj)
 
384
        return obj
 
385
 
 
386
    def _explode(self):
 
387
        ''' return a fully expanded object'''
 
388
        from reportlab.graphics.widgetbase import Widget
 
389
        obj = Group()
 
390
        if hasattr(obj,'transform'): obj.transform = self.transform[:]
 
391
        P = self.contents[:]    # pending nodes
 
392
        while P:
 
393
            n = P.pop(0)
 
394
            if isinstance(n, UserNode):
 
395
                P.append(n.provideNode())
 
396
            elif isinstance(n, Group):
 
397
                n = n._explode()
 
398
                if n.transform==(1,0,0,1,0,0):
 
399
                    obj.contents.extend(n.contents)
 
400
                else:
 
401
                    obj.add(n)
 
402
            else:
 
403
                obj.add(n)
 
404
        return obj
 
405
 
 
406
    def _copyContents(self,obj):
 
407
        for child in self.contents:
 
408
            obj.contents.append(child)
 
409
 
 
410
    def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)):
 
411
        from copy import copy
 
412
        self_contents = self.contents
 
413
        if not aKeys: aKeys = self._attrMap.keys()
 
414
        for (k, v) in self.__dict__.items():
 
415
            if v in self_contents:
 
416
                pos = self_contents.index(v)
 
417
                setattr(obj, k, obj.contents[pos])
 
418
            elif k in aKeys and k not in noCopy:
 
419
                setattr(obj, k, copy(v))
 
420
 
 
421
    def _copy(self,obj):
 
422
        """copies to obj"""
 
423
        obj._attrMap = self._attrMap.clone()
 
424
        self._copyContents(obj)
 
425
        self._copyNamedContents(obj)
 
426
        return obj
 
427
 
 
428
    def copy(self):
 
429
        """returns a copy"""
 
430
        return self._copy(self.__class__())
 
431
 
 
432
    def rotate(self, theta):
 
433
        """Convenience to help you set transforms"""
 
434
        self.transform = mmult(self.transform, rotate(theta))
 
435
 
 
436
    def translate(self, dx, dy):
 
437
        """Convenience to help you set transforms"""
 
438
        self.transform = mmult(self.transform, translate(dx, dy))
 
439
 
 
440
    def scale(self, sx, sy):
 
441
        """Convenience to help you set transforms"""
 
442
        self.transform = mmult(self.transform, scale(sx, sy))
 
443
 
 
444
 
 
445
    def skew(self, kx, ky):
 
446
        """Convenience to help you set transforms"""
 
447
        self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky))
 
448
 
 
449
    def shift(self, x, y):
 
450
        '''Convenience function to set the origin arbitrarily'''
 
451
        self.transform = self.transform[:-2]+(x,y)
 
452
 
 
453
    def asDrawing(self, width, height):
 
454
        """ Convenience function to make a drawing from a group
 
455
            After calling this the instance will be a drawing!
 
456
        """
 
457
        self.__class__ = Drawing
 
458
        self._attrMap.update(self._xtraAttrMap)
 
459
        self.width = width
 
460
        self.height = height
 
461
 
 
462
    def getContents(self):
 
463
        '''Return the list of things to be rendered
 
464
        override to get more complicated behaviour'''
 
465
        b = getattr(self,'background',None)
 
466
        C = self.contents
 
467
        if b and b not in C: C = [b]+C
 
468
        return C
 
469
 
 
470
    def getBounds(self):
 
471
        if self.contents:
 
472
            b = []
 
473
            for elem in self.contents:
 
474
                b.append(elem.getBounds())
 
475
            x1 = getRectsBounds(b)
 
476
            if x1 is None: return None
 
477
            x1, y1, x2, y2 = x1
 
478
            trans = self.transform
 
479
            corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]]
 
480
            newCorners = []
 
481
            for corner in corners:
 
482
                newCorners.append(transformPoint(trans, corner))
 
483
            return getPointsBounds(newCorners)
 
484
        else:
 
485
            #empty group needs a sane default; this
 
486
            #will happen when interactively creating a group
 
487
            #nothing has been added to yet.  The alternative is
 
488
            #to handle None as an allowed return value everywhere.
 
489
            return None
 
490
 
 
491
def _addObjImport(obj,I,n=None):
 
492
    '''add an import of obj's class to a dictionary of imports''' #'
 
493
    from inspect import getmodule
 
494
    c = obj.__class__
 
495
    m = getmodule(c).__name__
 
496
    n = n or c.__name__
 
497
    if not I.has_key(m):
 
498
        I[m] = [n]
 
499
    elif n not in I[m]:
 
500
        I[m].append(n)
 
501
 
 
502
def _repr(self,I=None):
 
503
    '''return a repr style string with named fixed args first, then keywords'''
 
504
    if type(self) is InstanceType:
 
505
        if self is EmptyClipPath:
 
506
            _addObjImport(self,I,'EmptyClipPath')
 
507
            return 'EmptyClipPath'
 
508
        if I: _addObjImport(self,I)
 
509
        if isinstance(self,Shape):
 
510
            from inspect import getargs
 
511
            args, varargs, varkw = getargs(self.__init__.im_func.func_code)
 
512
            P = self.getProperties()
 
513
            s = self.__class__.__name__+'('
 
514
            for n in args[1:]:
 
515
                v = P[n]
 
516
                del P[n]
 
517
                s = s + '%s,' % _repr(v,I)
 
518
            for n,v in P.items():
 
519
                v = P[n]
 
520
                s = s + '%s=%s,' % (n, _repr(v,I))
 
521
            return s[:-1]+')'
 
522
        else:
 
523
            return repr(self)
 
524
    elif type(self) is FloatType:
 
525
        return fp_str(self)
 
526
    elif type(self) in (ListType,TupleType):
 
527
        s = ''
 
528
        for v in self:
 
529
            s = s + '%s,' % _repr(v,I)
 
530
        if type(self) is ListType:
 
531
            return '[%s]' % s[:-1]
 
532
        else:
 
533
            return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '')
 
534
    else:
 
535
        return repr(self)
 
536
 
 
537
def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'):
 
538
    s = ''
 
539
    C = getattr(G,'transform',None)
 
540
    if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C)))
 
541
    C  = G.contents
 
542
    for n in C:
 
543
        if isinstance(n, Group):
 
544
            npfx = 'v%d' % i
 
545
            i = i + 1
 
546
            s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx)
 
547
            s = s + _renderGroupPy(n,npfx,I,i,indent)
 
548
            i = i - 1
 
549
        else:
 
550
            s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I))
 
551
    return s
 
552
 
 
553
class Drawing(Group, Flowable):
 
554
    """Outermost container; the thing a renderer works on.
 
555
    This has no properties except a height, width and list
 
556
    of contents."""
 
557
 
 
558
    _xtraAttrMap = AttrMap(
 
559
        width = AttrMapValue(isNumber,desc="Drawing width in points."),
 
560
        height = AttrMapValue(isNumber,desc="Drawing height in points."),
 
561
        canv = AttrMapValue(None),
 
562
        background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"),
 
563
        hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Alignment within parent document"),
 
564
        vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Alignment within parent document")
 
565
        )
 
566
 
 
567
    _attrMap = AttrMap(BASE=Group)
 
568
    _attrMap.update(_xtraAttrMap)
 
569
 
 
570
    def __init__(self, width=400, height=200, *nodes, **keywords):
 
571
        self.background = None
 
572
        apply(Group.__init__,(self,)+nodes,keywords)
 
573
        self.width = width
 
574
        self.height = height
 
575
        self.hAlign = 'LEFT'
 
576
        self.vAlign = 'BOTTOM'
 
577
 
 
578
    def _renderPy(self):
 
579
        I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']}
 
580
        G = _renderGroupPy(self._explode(),'self',I)
 
581
        n = 'ExplodedDrawing_' + self.__class__.__name__
 
582
        s = '#Autogenerated by ReportLab guiedit do not edit\n'
 
583
        for m, o in I.items():
 
584
            s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'",""))
 
585
        s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n
 
586
        s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height)
 
587
        s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n'
 
588
        s = s + G
 
589
        s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n
 
590
        return s
 
591
 
 
592
    def draw(self):
 
593
        """This is used by the Platypus framework to let the document
 
594
        draw itself in a story.  It is specific to PDF and should not
 
595
        be used directly."""
 
596
 
 
597
        import renderPDF
 
598
        R = renderPDF._PDFRenderer()
 
599
        R.draw(self, self.canv, 0, 0)
 
600
 
 
601
    def expandUserNodes(self):
 
602
        """Return a new drawing which only contains primitive shapes."""
 
603
        obj = Group.expandUserNodes(self)
 
604
        obj.width = self.width
 
605
        obj.height = self.height
 
606
        return obj
 
607
 
 
608
    def copy(self,obj):
 
609
        """Returns a copy"""
 
610
        return self._copy(Drawing(self.width, self.height))
 
611
 
 
612
    def asGroup(self,*args,**kw):
 
613
        return self._copy(apply(Group,args,kw))
 
614
 
 
615
    def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title=''):
 
616
        """Saves copies of self in desired location and formats.
 
617
 
 
618
        Multiple formats can be supported in one call"""
 
619
        from reportlab import rl_config
 
620
        ext = ''
 
621
        if not fnRoot:
 
622
            fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d'))
 
623
            chartId = getattr(self,'chartId',0)
 
624
            if callable(fnRoot):
 
625
                fnRoot = fnRoot(chartId)
 
626
            else:
 
627
                try:
 
628
                    fnRoot = fnRoot % getattr(self,'chartId',0)
 
629
                except TypeError, err:
 
630
                    if str(err) != 'not all arguments converted': raise
 
631
 
 
632
        if os.path.isabs(fnRoot):
 
633
            outDir, fnRoot = os.path.split(fnRoot)
 
634
        else:
 
635
            outDir = outDir or getattr(self,'outDir','.')
 
636
        if not os.path.isabs(outDir): outDir = os.path.join(os.path.dirname(sys.argv[0]),outDir)
 
637
        if not os.path.isdir(outDir): os.makedirs(outDir)
 
638
        fnroot = os.path.normpath(os.path.join(outDir,fnRoot))
 
639
        plotMode = os.path.splitext(fnroot)
 
640
        if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp']:
 
641
            fnroot = plotMode[0]
 
642
 
 
643
        plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0]
 
644
        _saved = logger.warnOnce.enabled, logger.infoOnce.enabled
 
645
        logger.warnOnce.enabled = logger.infoOnce.enabled = verbose
 
646
        if 'pdf' in plotMode:
 
647
            from reportlab.graphics import renderPDF
 
648
            filename = fnroot+'.pdf'
 
649
            if verbose: print "generating PDF file %s" % filename
 
650
            renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
651
            ext = ext +  '/.pdf'
 
652
            if sys.platform=='mac':
 
653
                import macfs, macostools
 
654
                macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ")
 
655
                macostools.touched(filename)
 
656
 
 
657
        for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']:
 
658
            if bmFmt in plotMode:
 
659
                from reportlab.graphics import renderPM
 
660
                filename = '%s.%s' % (fnroot,bmFmt)
 
661
                if verbose: print "generating %s file %s" % (bmFmt,filename)
 
662
                renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
663
                ext = ext + '/.' + bmFmt
 
664
 
 
665
        if 'eps' in plotMode:
 
666
            from rlextra.graphics import renderPS_SEP
 
667
            filename = fnroot+'.eps'
 
668
            if verbose: print "generating EPS file %s" % filename
 
669
            renderPS_SEP.drawToFile(self,
 
670
                                filename,
 
671
                                title = fnroot,
 
672
                                dept = getattr(self,'EPS_info',['Testing'])[0],
 
673
                                company = getattr(self,'EPS_info',['','ReportLab'])[1],
 
674
                                preview = getattr(self,'preview',1),
 
675
                                showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
676
            ext = ext +  '/.eps'
 
677
 
 
678
        if 'ps' in plotMode:
 
679
            from reportlab.graphics import renderPS
 
680
            filename = fnroot+'.ps'
 
681
            if verbose: print "generating EPS file %s" % filename
 
682
            renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
683
            ext = ext +  '/.ps'
 
684
 
 
685
        if 'py' in plotMode:
 
686
            filename = fnroot+'.py'
 
687
            if verbose: print "generating py file %s" % filename
 
688
            open(filename,'w').write(self._renderPy())
 
689
            ext = ext +  '/.py'
 
690
 
 
691
        logger.warnOnce.enabled, logger.infoOnce.enabled = _saved
 
692
        if hasattr(self,'saveLogger'):
 
693
            self.saveLogger(fnroot,ext)
 
694
        return ext and fnroot+ext[1:] or ''
 
695
 
 
696
 
 
697
    def asString(self, format, verbose=None, preview=0):
 
698
        """Converts to an 8 bit string in given format."""
 
699
        assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format
 
700
        from reportlab import rl_config
 
701
        #verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0]
 
702
        if format == 'pdf':
 
703
            from reportlab.graphics import renderPDF
 
704
            return renderPDF.drawToString(self)
 
705
        elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']:
 
706
            from reportlab.graphics import renderPM
 
707
            return renderPM.drawToString(self, fmt=format)
 
708
        elif format == 'eps':
 
709
            from rlextra.graphics import renderPS_SEP
 
710
            return renderPS_SEP.drawToString(self,
 
711
                                preview = preview,
 
712
                                showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
713
        elif format == 'ps':
 
714
            from reportlab.graphics import renderPS
 
715
            return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary))
 
716
        elif format == 'py':
 
717
            return self._renderPy()
 
718
 
 
719
class _DrawingEditorMixin:
 
720
    '''This is a mixin to provide functionality for edited drawings'''
 
721
    def _add(self,obj,value,name=None,validate=None,desc=None,pos=None):
 
722
        '''
 
723
        effectively setattr(obj,name,value), but takes care of things with _attrMaps etc
 
724
        '''
 
725
        ivc = isValidChild(value)
 
726
        if name and hasattr(obj,'_attrMap'):
 
727
            if not obj.__dict__.has_key('_attrMap'):
 
728
                obj._attrMap = obj._attrMap.clone()
 
729
            if ivc and validate is None: validate = isValidChild
 
730
            obj._attrMap[name] = AttrMapValue(validate,desc)
 
731
        if hasattr(obj,'add') and ivc:
 
732
            if pos:
 
733
                obj.insert(pos,value,name)
 
734
            else:
 
735
                obj.add(value,name)
 
736
        elif name:
 
737
            setattr(obj,name,value)
 
738
        else:
 
739
            raise ValueError, "Can't add, need name"
 
740
 
 
741
class LineShape(Shape):
 
742
    # base for types of lines
 
743
 
 
744
    _attrMap = AttrMap(
 
745
        strokeColor = AttrMapValue(isColorOrNone),
 
746
        strokeWidth = AttrMapValue(isNumber),
 
747
        strokeLineCap = AttrMapValue(None),
 
748
        strokeLineJoin = AttrMapValue(None),
 
749
        strokeMiterLimit = AttrMapValue(isNumber),
 
750
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
 
751
        )
 
752
 
 
753
    def __init__(self, kw):
 
754
        self.strokeColor = STATE_DEFAULTS['strokeColor']
 
755
        self.strokeWidth = 1
 
756
        self.strokeLineCap = 0
 
757
        self.strokeLineJoin = 0
 
758
        self.strokeMiterLimit = 0
 
759
        self.strokeDashArray = None
 
760
        self.setProperties(kw)
 
761
 
 
762
 
 
763
class Line(LineShape):
 
764
    _attrMap = AttrMap(BASE=LineShape,
 
765
        x1 = AttrMapValue(isNumber),
 
766
        y1 = AttrMapValue(isNumber),
 
767
        x2 = AttrMapValue(isNumber),
 
768
        y2 = AttrMapValue(isNumber),
 
769
        )
 
770
 
 
771
    def __init__(self, x1, y1, x2, y2, **kw):
 
772
        LineShape.__init__(self, kw)
 
773
        self.x1 = x1
 
774
        self.y1 = y1
 
775
        self.x2 = x2
 
776
        self.y2 = y2
 
777
 
 
778
    def getBounds(self):
 
779
        "Returns bounding rectangle of object as (x1,y1,x2,y2)"
 
780
        return (self.x1, self.y1, self.x2, self.y2)
 
781
 
 
782
 
 
783
class SolidShape(LineShape):
 
784
    # base for anything with outline and content
 
785
 
 
786
    _attrMap = AttrMap(BASE=LineShape,
 
787
        fillColor = AttrMapValue(isColorOrNone),
 
788
        )
 
789
 
 
790
    def __init__(self, kw):
 
791
        self.fillColor = STATE_DEFAULTS['fillColor']
 
792
        # do this at the end so keywords overwrite
 
793
        #the above settings
 
794
        LineShape.__init__(self, kw)
 
795
 
 
796
 
 
797
# path operator  constants
 
798
_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4)
 
799
_PATH_OP_ARG_COUNT = (2, 2, 6, 0)  # [moveTo, lineTo, curveTo, closePath]
 
800
_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath']
 
801
 
 
802
def _renderPath(path, drawFuncs):
 
803
    """Helper function for renderers."""
 
804
    # this could be a method of Path...
 
805
    points = path.points
 
806
    i = 0
 
807
    hadClosePath = 0
 
808
    hadMoveTo = 0
 
809
    for op in path.operators:
 
810
        nArgs = _PATH_OP_ARG_COUNT[op]
 
811
        func = drawFuncs[op]
 
812
        j = i + nArgs
 
813
        apply(func, points[i:j])
 
814
        i = j
 
815
        if op == _CLOSEPATH:
 
816
            hadClosePath = hadClosePath + 1
 
817
        if op == _MOVETO:
 
818
            hadMoveTo = hadMoveTo + 1
 
819
    return hadMoveTo == hadClosePath
 
820
 
 
821
class Path(SolidShape):
 
822
    """Path, made up of straight lines and bezier curves."""
 
823
 
 
824
    _attrMap = AttrMap(BASE=SolidShape,
 
825
        points = AttrMapValue(isListOfNumbers),
 
826
        operators = AttrMapValue(isListOfNumbers),
 
827
        isClipPath = AttrMapValue(isBoolean),
 
828
        )
 
829
 
 
830
    def __init__(self, points=None, operators=None, isClipPath=0, **kw):
 
831
        SolidShape.__init__(self, kw)
 
832
        if points is None:
 
833
            points = []
 
834
        if operators is None:
 
835
            operators = []
 
836
        assert len(points) % 2 == 0, 'Point list must have even number of elements!'
 
837
        self.points = points
 
838
        self.operators = operators
 
839
        self.isClipPath = isClipPath
 
840
 
 
841
    def copy(self):
 
842
        new = Path(self.points[:], self.operators[:])
 
843
        new.setProperties(self.getProperties())
 
844
        return new
 
845
 
 
846
    def moveTo(self, x, y):
 
847
        self.points.extend([x, y])
 
848
        self.operators.append(_MOVETO)
 
849
 
 
850
    def lineTo(self, x, y):
 
851
        self.points.extend([x, y])
 
852
        self.operators.append(_LINETO)
 
853
 
 
854
    def curveTo(self, x1, y1, x2, y2, x3, y3):
 
855
        self.points.extend([x1, y1, x2, y2, x3, y3])
 
856
        self.operators.append(_CURVETO)
 
857
 
 
858
    def closePath(self):
 
859
        self.operators.append(_CLOSEPATH)
 
860
 
 
861
    def getBounds(self):
 
862
        return getPathBounds(self.points)
 
863
 
 
864
EmptyClipPath=Path()    #special path
 
865
 
 
866
def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None):
 
867
    degreedelta = degreedelta or 1
 
868
    if yradius is None: yradius = radius
 
869
    points = []
 
870
    from math import sin, cos, pi
 
871
    degreestoradians = pi/180.0
 
872
    radiansdelta = degreedelta*degreestoradians
 
873
    startangle = startangledegrees*degreestoradians
 
874
    endangle = endangledegrees*degreestoradians
 
875
    while endangle<startangle:
 
876
        endangle = endangle+2*pi
 
877
    angle = float(endangle - startangle)
 
878
    if angle>.001:
 
879
        n = max(int(angle/radiansdelta+0.5),1)
 
880
        radiansdelta = angle/n
 
881
        a = points.append
 
882
        for angle in xrange(n+1):
 
883
            angle = startangle+angle*radiansdelta
 
884
            a((centerx+radius*cos(angle),centery+yradius*sin(angle)))
 
885
        #a((centerx+radius*cos(endangle),centery+yradius*sin(endangle)))
 
886
        if reverse: points.reverse()
 
887
    return points
 
888
 
 
889
class ArcPath(Path):
 
890
    '''Path with an addArc method'''
 
891
    def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None):
 
892
        P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse)
 
893
        if moveTo or not len(self.operators):
 
894
            self.moveTo(P[0][0],P[0][1])
 
895
            del P[0]
 
896
        for x, y in P: self.lineTo(x,y)
 
897
 
 
898
def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw):
 
899
    O = []
 
900
    P = []
 
901
    for seg in pathSegs:
 
902
        if type(seg) not in [ListType,TupleType]:
 
903
            opName = seg
 
904
            args = []
 
905
        else:
 
906
            opName = seg[0]
 
907
            args = seg[1:]
 
908
        if opName not in _PATH_OP_NAMES:
 
909
            raise ValueError, 'bad operator name %s' % opName
 
910
        op = _PATH_OP_NAMES.index(opName)
 
911
        if len(args)!=_PATH_OP_ARG_COUNT[op]:
 
912
            raise ValueError, '%s bad arguments %s' % (opName,str(args))
 
913
        O.append(op)
 
914
        P.extend(list(args))
 
915
    for d,o in (dx,0), (dy,1):
 
916
        for i in xrange(o,len(P),2):
 
917
            P[i] = P[i]+d
 
918
    return apply(Path,(P,O,isClipPath),kw)
 
919
 
 
920
class Rect(SolidShape):
 
921
    """Rectangle, possibly with rounded corners."""
 
922
 
 
923
    _attrMap = AttrMap(BASE=SolidShape,
 
924
        x = AttrMapValue(isNumber),
 
925
        y = AttrMapValue(isNumber),
 
926
        width = AttrMapValue(isNumber),
 
927
        height = AttrMapValue(isNumber),
 
928
        rx = AttrMapValue(isNumber),
 
929
        ry = AttrMapValue(isNumber),
 
930
        )
 
931
 
 
932
    def __init__(self, x, y, width, height, rx=0, ry=0, **kw):
 
933
        SolidShape.__init__(self, kw)
 
934
        self.x = x
 
935
        self.y = y
 
936
        self.width = width
 
937
        self.height = height
 
938
        self.rx = rx
 
939
        self.ry = ry
 
940
 
 
941
    def copy(self):
 
942
        new = Rect(self.x, self.y, self.width, self.height)
 
943
        new.setProperties(self.getProperties())
 
944
        return new
 
945
 
 
946
    def getBounds(self):
 
947
        return (self.x, self.y, self.x + self.width, self.y + self.height)
 
948
 
 
949
 
 
950
class Image(SolidShape):
 
951
    """Bitmap image."""
 
952
 
 
953
    _attrMap = AttrMap(BASE=SolidShape,
 
954
        x = AttrMapValue(isNumber),
 
955
        y = AttrMapValue(isNumber),
 
956
        width = AttrMapValue(isNumberOrNone),
 
957
        height = AttrMapValue(isNumberOrNone),
 
958
        path = AttrMapValue(None),
 
959
        )
 
960
 
 
961
    def __init__(self, x, y, width, height, path, **kw):
 
962
        SolidShape.__init__(self, kw)
 
963
        self.x = x
 
964
        self.y = y
 
965
        self.width = width
 
966
        self.height = height
 
967
        self.path = path
 
968
 
 
969
    def copy(self):
 
970
        new = Image(self.x, self.y, self.width, self.height, self.path)
 
971
        new.setProperties(self.getProperties())
 
972
        return new
 
973
 
 
974
    def getBounds(self):
 
975
        return (self.x, self.y, self.x + width, self.y + width)
 
976
 
 
977
class Circle(SolidShape):
 
978
 
 
979
    _attrMap = AttrMap(BASE=SolidShape,
 
980
        cx = AttrMapValue(isNumber),
 
981
        cy = AttrMapValue(isNumber),
 
982
        r = AttrMapValue(isNumber),
 
983
        )
 
984
 
 
985
    def __init__(self, cx, cy, r, **kw):
 
986
        SolidShape.__init__(self, kw)
 
987
        self.cx = cx
 
988
        self.cy = cy
 
989
        self.r = r
 
990
 
 
991
    def copy(self):
 
992
        new = Circle(self.cx, self.cy, self.r)
 
993
        new.setProperties(self.getProperties())
 
994
        return new
 
995
 
 
996
    def getBounds(self):
 
997
        return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r)
 
998
 
 
999
class Ellipse(SolidShape):
 
1000
 
 
1001
    _attrMap = AttrMap(BASE=SolidShape,
 
1002
        cx = AttrMapValue(isNumber),
 
1003
        cy = AttrMapValue(isNumber),
 
1004
        rx = AttrMapValue(isNumber),
 
1005
        ry = AttrMapValue(isNumber),
 
1006
        )
 
1007
 
 
1008
    def __init__(self, cx, cy, rx, ry, **kw):
 
1009
        SolidShape.__init__(self, kw)
 
1010
        self.cx = cx
 
1011
        self.cy = cy
 
1012
        self.rx = rx
 
1013
        self.ry = ry
 
1014
 
 
1015
    def copy(self):
 
1016
        new = Ellipse(self.cx, self.cy, self.rx, self.ry)
 
1017
        new.setProperties(self.getProperties())
 
1018
        return new
 
1019
 
 
1020
    def getBounds(self):
 
1021
            return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry)
 
1022
 
 
1023
class Wedge(SolidShape):
 
1024
    """A "slice of a pie" by default translates to a polygon moves anticlockwise
 
1025
       from start angle to end angle"""
 
1026
 
 
1027
    _attrMap = AttrMap(BASE=SolidShape,
 
1028
        centerx = AttrMapValue(isNumber),
 
1029
        centery = AttrMapValue(isNumber),
 
1030
        radius = AttrMapValue(isNumber),
 
1031
        startangledegrees = AttrMapValue(isNumber),
 
1032
        endangledegrees = AttrMapValue(isNumber),
 
1033
        yradius = AttrMapValue(isNumberOrNone),
 
1034
        radius1 = AttrMapValue(isNumberOrNone),
 
1035
        yradius1 = AttrMapValue(isNumberOrNone),
 
1036
        )
 
1037
 
 
1038
    degreedelta = 1 # jump every 1 degrees
 
1039
 
 
1040
    def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw):
 
1041
        SolidShape.__init__(self, kw)
 
1042
        while endangledegrees<startangledegrees:
 
1043
            endangledegrees = endangledegrees+360
 
1044
        #print "__init__"
 
1045
        self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees = \
 
1046
            centerx, centery, radius, startangledegrees, endangledegrees
 
1047
        self.yradius = yradius
 
1048
 
 
1049
    def _xtraRadii(self):
 
1050
        yradius = getattr(self, 'yradius', None)
 
1051
        if yradius is None: yradius = self.radius
 
1052
        radius1 = getattr(self,'radius1', None)
 
1053
        yradius1 = getattr(self,'yradius1',radius1)
 
1054
        if radius1 is None: radius1 = yradius1
 
1055
        return yradius, radius1, yradius1
 
1056
 
 
1057
    #def __repr__(self):
 
1058
    #        return "Wedge"+repr((self.centerx, self.centery, self.radius, self.startangledegrees, self.endangledegrees ))
 
1059
    #__str__ = __repr__
 
1060
 
 
1061
    def asPolygon(self):
 
1062
        #print "asPolygon"
 
1063
        centerx= self.centerx
 
1064
        centery = self.centery
 
1065
        radius = self.radius
 
1066
        yradius, radius1, yradius1 = self._xtraRadii()
 
1067
        startangledegrees = self.startangledegrees
 
1068
        endangledegrees = self.endangledegrees
 
1069
        degreedelta = self.degreedelta
 
1070
        from math import sin, cos, pi
 
1071
        degreestoradians = pi/180.0
 
1072
        radiansdelta = degreedelta*degreestoradians
 
1073
        startangle = startangledegrees*degreestoradians
 
1074
        endangle = endangledegrees*degreestoradians
 
1075
        while endangle<startangle:
 
1076
            endangle = endangle+2*pi
 
1077
        angle = float(endangle-startangle)
 
1078
        points = []
 
1079
        if angle>.001:
 
1080
            n = max(1,int(angle/radiansdelta+0.5))
 
1081
            radiansdelta = angle/n
 
1082
            a = points.append
 
1083
            CA = []
 
1084
            CAA = CA.append
 
1085
            for angle in xrange(n+1):
 
1086
                angle = startangle+angle*radiansdelta
 
1087
                CAA((cos(angle),sin(angle)))
 
1088
            #CAA((cos(endangle),sin(endangle)))
 
1089
            for c,s in CA:
 
1090
                a(centerx+radius*c)
 
1091
                a(centery+yradius*s)
 
1092
            if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
 
1093
                a(centerx); a(centery)
 
1094
            else:
 
1095
                CA.reverse()
 
1096
                for c,s in CA:
 
1097
                    a(centerx+radius1*c)
 
1098
                    a(centery+yradius1*s)
 
1099
        return Polygon(points)
 
1100
 
 
1101
    def copy(self):
 
1102
        new = Wedge(self.centerx,
 
1103
                    self.centery,
 
1104
                    self.radius,
 
1105
                    self.startangledegrees,
 
1106
                    self.endangledegrees)
 
1107
        new.setProperties(self.getProperties())
 
1108
        return new
 
1109
 
 
1110
    def getBounds(self):
 
1111
        return self.asPolygon().getBounds()
 
1112
 
 
1113
class Polygon(SolidShape):
 
1114
    """Defines a closed shape; Is implicitly
 
1115
    joined back to the start for you."""
 
1116
 
 
1117
    _attrMap = AttrMap(BASE=SolidShape,
 
1118
        points = AttrMapValue(isListOfNumbers),
 
1119
        )
 
1120
 
 
1121
    def __init__(self, points=[], **kw):
 
1122
        SolidShape.__init__(self, kw)
 
1123
        assert len(points) % 2 == 0, 'Point list must have even number of elements!'
 
1124
        self.points = points
 
1125
 
 
1126
    def copy(self):
 
1127
        new = Polygon(self.points)
 
1128
        new.setProperties(self.getProperties())
 
1129
        return new
 
1130
 
 
1131
    def getBounds(self):
 
1132
        return getPointsBounds(self.points)
 
1133
 
 
1134
class PolyLine(LineShape):
 
1135
    """Series of line segments.  Does not define a
 
1136
    closed shape; never filled even if apparently joined.
 
1137
    Put the numbers in the list, not two-tuples."""
 
1138
 
 
1139
    _attrMap = AttrMap(BASE=LineShape,
 
1140
        points = AttrMapValue(isListOfNumbers),
 
1141
        )
 
1142
 
 
1143
    def __init__(self, points=[], **kw):
 
1144
        LineShape.__init__(self, kw)
 
1145
        lenPoints = len(points)
 
1146
        if lenPoints:
 
1147
            if type(points[0]) in (ListType,TupleType):
 
1148
                L = []
 
1149
                for (x,y) in points:
 
1150
                    L.append(x)
 
1151
                    L.append(y)
 
1152
                points = L
 
1153
            else:
 
1154
                assert len(points) % 2 == 0, 'Point list must have even number of elements!'
 
1155
        self.points = points
 
1156
 
 
1157
    def copy(self):
 
1158
        new = PolyLine(self.points)
 
1159
        new.setProperties(self.getProperties())
 
1160
        return new
 
1161
 
 
1162
    def getBounds(self):
 
1163
        return getPointsBounds(self.points)
 
1164
 
 
1165
class String(Shape):
 
1166
    """Not checked against the spec, just a way to make something work.
 
1167
    Can be anchored left, middle or end."""
 
1168
 
 
1169
    # to do.
 
1170
    _attrMap = AttrMap(
 
1171
        x = AttrMapValue(isNumber),
 
1172
        y = AttrMapValue(isNumber),
 
1173
        text = AttrMapValue(isString),
 
1174
        fontName = AttrMapValue(None),
 
1175
        fontSize = AttrMapValue(isNumber),
 
1176
        fillColor = AttrMapValue(isColorOrNone),
 
1177
        textAnchor = AttrMapValue(isTextAnchor),
 
1178
        )
 
1179
 
 
1180
    def __init__(self, x, y, text, **kw):
 
1181
        self.x = x
 
1182
        self.y = y
 
1183
        self.text = text
 
1184
        self.textAnchor = 'start'
 
1185
        self.fontName = STATE_DEFAULTS['fontName']
 
1186
        self.fontSize = STATE_DEFAULTS['fontSize']
 
1187
        self.fillColor = STATE_DEFAULTS['fillColor']
 
1188
        self.setProperties(kw)
 
1189
 
 
1190
    def getEast(self):
 
1191
        return self.x + stringWidth(self.text,self.fontName,self.fontSize)
 
1192
 
 
1193
    def copy(self):
 
1194
        new = String(self.x, self.y, self.text)
 
1195
        new.setProperties(self.getProperties())
 
1196
        return new
 
1197
 
 
1198
    def getBounds(self):
 
1199
        # assumes constant drop of 0.2*size to baseline
 
1200
        w = stringWidth(self.text,self.fontName,self.fontSize)
 
1201
        if self.textAnchor == 'start':
 
1202
            x = self.x
 
1203
        elif self.textAnchor == 'middle':
 
1204
            x = self.x - 0.5*w
 
1205
        elif self.textAnchor == 'end':
 
1206
            x = self.x - w
 
1207
        return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize)
 
1208
 
 
1209
class UserNode(_DrawTimeResizeable):
 
1210
    """A simple template for creating a new node.  The user (Python
 
1211
    programmer) may subclasses this.  provideNode() must be defined to
 
1212
    provide a Shape primitive when called by a renderer.  It does
 
1213
    NOT inherit from Shape, as the renderer always replaces it, and
 
1214
    your own classes can safely inherit from it without getting
 
1215
    lots of unintended behaviour."""
 
1216
 
 
1217
    def provideNode(self):
 
1218
        """Override this to create your own node. This lets widgets be
 
1219
        added to drawings; they must create a shape (typically a group)
 
1220
        so that the renderer can draw the custom node."""
 
1221
 
 
1222
        raise NotImplementedError, "this method must be redefined by the user/programmer"
 
1223
 
 
1224
 
 
1225
def test():
 
1226
    r = Rect(10,10,200,50)
 
1227
    import pprint
 
1228
    pp = pprint.pprint
 
1229
    print 'a Rectangle:'
 
1230
    pp(r.getProperties())
 
1231
    print
 
1232
    print 'verifying...',
 
1233
    r.verify()
 
1234
    print 'OK'
 
1235
    #print 'setting rect.z = "spam"'
 
1236
    #r.z = 'spam'
 
1237
    print 'deleting rect.width'
 
1238
    del r.width
 
1239
    print 'verifying...',
 
1240
    r.verify()
 
1241
 
 
1242
 
 
1243
if __name__=='__main__':
 
1244
    test()