~alexzak/bzr/mascyma

« back to all changes in this revision

Viewing changes to matplotlib/transforms.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Andreas Benkard
  • Date: 2004-04-11 15:52:53 UTC
  • Revision ID: james.westby@ubuntu.com-20040411155253-uxyl17oylqzidez4
Tags: 0.59-1
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
This module contains the newfangled transform class which allows the
 
3
placement of artists (lines, patches, text) in a variety of coordinate
 
4
systems (display, arbitrary data, relative axes, physical sizes)
 
5
 
 
6
The default Transform() is identity.
 
7
 
 
8
  t = Transform()
 
9
  x == Transform.positions(x)  True
 
10
  x == Transform.scale(x)      True
 
11
 
 
12
A linear Transformation is specified by giving a Bound1D (min, max)
 
13
instance for the domain and range.  The transform below maps the
 
14
interval [0,1] to [-10,10]
 
15
 
 
16
  t = Transform( Bound1D(0,1), Bound1D(-10,10) )
 
17
 
 
18
Since all Transforms know their inverse function, you can compute an
 
19
inverse transformation by calling inverse_positions or inverse_scale
 
20
 
 
21
  t = Transform( Bound1D(0,1), Bound1D(-10,10) )
 
22
  val = t.inverse_positions(5)  # maps [-10,10] to [0,1]
 
23
 
 
24
The difference between 'positions' and 'scale' is that the positions
 
25
func is appropriate for locations (eg, x,y) and the scale func is
 
26
appropriate for lengths (eg, width, height).
 
27
 
 
28
The Bound1D methods provide a number of utility functions: the
 
29
interval max-min, determining if a point is in the open or closed
 
30
interval, constraining the bound to be positive (useful for log
 
31
transforms), and so on.  These are useful for storing view, data and
 
32
display limits of a given axis.
 
33
 
 
34
The Bound2D is a straight-forward generalization of Bound1D, and
 
35
stores 2 Bound1D instances 'x' and 'y' to represent a 2D Bound (useful
 
36
for Axes bounding boxes, clipping etc).  All Artists are responsible
 
37
for returning their extent in display coords as a Bound2D instance,
 
38
which is useful for determining whether 2 Artists overlap.  Some
 
39
utility functions, eg, bound2d_all, return the Bound2D instance that
 
40
bounds all the Bound2D instances passed as args.  This helps in text
 
41
layout, eg, in positioning the axis labels to not overlap the tick
 
42
labels.
 
43
 
 
44
The Bound1D instances store their max and min values as RWVals
 
45
(read/write references).  These are mutable scalars that can be shared
 
46
among all the figure components.  When a figure clas resizes and thus
 
47
changes the display limits of an Axes, the Axes and all its components
 
48
know about the changes because they store a reference to the
 
49
displaylim, not the scalar value of the display lim.  Likewise for
 
50
DPI.
 
51
 
 
52
Also, it is possible to do simple arithmetic in RRefs via the derived
 
53
BinOp class, which stores both sides of a binary arithmetic operation,
 
54
as well as the binary function to return the result of the binop
 
55
applied to the dereferenced scalars.  This allows you to place artists
 
56
with locations like '3 centimenters below the x axis'
 
57
 
 
58
Here are some concepts and how to apply them via the transform
 
59
architecture
 
60
 
 
61
  * Map view limits to display limits via a linear tranformation
 
62
 
 
63
    # viewlim and displaylim are Bound1D instances
 
64
    tx = Transform( axes.xaxis.viewlim, axes.xaxis.displaylim )
 
65
    ty = Transform( axes.yaxis.viewlim, axes.yaxis.displaylim )
 
66
    l = Line2D(dpi, bbox, xdata, ydata, transx=tx, transy=ty)
 
67
 
 
68
  * Map relative axes coords ( 0,0 is lower left and 1,1 is upper
 
69
    right ) to display coords.  This example puts text in the middle
 
70
    of the axes (0.5, 0.5)
 
71
    
 
72
    tx = Transform( Bound1D(0,1), axes.xaxis.displaylim )
 
73
    ty = Transform( Bound1D(0,1), axes.yaxis.displaylim )
 
74
    text = AxisText(dpi, bbox, 0.5, 0.5, transx=tx, transy=ty)
 
75
    
 
76
 * Map x view limits to display limits via a log tranformation and y
 
77
   view limits via linear transform.  The funcs pair is the
 
78
   transform/inverse pair
 
79
 
 
80
    funcs = logwarn, pow10
 
81
    tx = Transform( axes.xaxis.viewlim, axes.xaxis.displaylim, funcs )
 
82
    ty = Transform( axes.yaxis.viewlim, axes.yaxis.displaylim )
 
83
    l = Line2D(dpi, bbox, xdata, ydata, transx=tx, transy=ty)
 
84
 
 
85
 * You can also do transformation from one physical scale (inches, cm,
 
86
   points, ...) to another.  You need to specify an offset in output
 
87
   coords.
 
88
 
 
89
      offset = 100  # dots
 
90
      cm = Centimeter( self.dpi)
 
91
      dots =  Dots( self.dpi)
 
92
      t =  TransformSize(cm, dots, offset)
 
93
 
 
94
   If you don't know the offset in output coords, you can supply an
 
95
   optional transform to transform the offset to output coords.  Eg,
 
96
   if you want to offset by x in data coords, and the output is
 
97
   display coords, you can do
 
98
 
 
99
      offset = 0.2  # x data coords
 
100
      cm = Centimeter( self.dpi)
 
101
      dots =  Dots( self.dpi)
 
102
      t =  TransformSize(cm, dots, offset, axes.xaxis.transData)
 
103
   
 
104
 * Combining the above, we can specify that a text instance is at an x
 
105
   location in data coords and a y location in points relative to an
 
106
   axis.  Eg. the transformation below indicates that the x value of
 
107
   an xticklabel position is in data corrdinates and the y value is 3
 
108
   points below the x axis, top justified
 
109
 
 
110
        # the top of the x ticklabel text is the bottom of the y axis
 
111
        # minus 3 points.  Note the code below uses the overloading of
 
112
        # __sub__ and __mul__ to return a BinOp.  Changes in the
 
113
        # position of bbox.y or dpi, eg, on a resize event, are
 
114
        # automagically reflected in the tick label position.
 
115
        # dpi*3/72 converts 3 points to dots.
 
116
        
 
117
        top = self.bbox.y.get_refmin() - self.dpi*RRef(3/72.0)
 
118
        text =  backends.AxisText(dpi, bbox, x=xdata, y=top,  
 
119
            verticalalignment='top',
 
120
            horizontalalignment='center',
 
121
            transx = self.axes.xaxis.transData,
 
122
            # transy is default, identity transform)
 
123
 
 
124
The unittest code for the transforms module is unit/transforms_unit.py
 
125
 
 
126
 
 
127
"""
 
128
from __future__ import division
 
129
import sys
 
130
from Numeric import array, asarray, log10, take, nonzero, arange
 
131
from cbook import iterable
 
132
 
 
133
 
 
134
 
 
135
class Size:
 
136
    def __init__(self, dpi, val=1):
 
137
        self._dpi = dpi
 
138
        self._val = val
 
139
 
 
140
    def get(self):
 
141
        return self._val
 
142
 
 
143
    def to_dots(self):
 
144
        return self._dpi.get() * self.to_inches()
 
145
 
 
146
    def to_inches(self):
 
147
        return self.to_inches()
 
148
 
 
149
 
 
150
    def __repr__(self): return '%s %s ' % (self._val, self.units())
 
151
 
 
152
    def set_val(self, val):
 
153
        self._val  = val
 
154
 
 
155
    def to_inches(self):
 
156
        raise NotImplementedError('Derived must override')
 
157
 
 
158
    def units(self):
 
159
        raise NotImplementedError('Derived must override')
 
160
    
 
161
class Inches(Size):
 
162
    def to_inches(self):
 
163
        return self._val
 
164
 
 
165
    def units(self):
 
166
        return 'inches'
 
167
 
 
168
class Dots(Size):
 
169
    def to_inches(self):
 
170
        return self._val/self._dpi.get()
 
171
 
 
172
    def to_dots(self):
 
173
        return self._val
 
174
 
 
175
    def units(self):
 
176
        return 'dots'
 
177
 
 
178
class Points(Size):
 
179
    def to_inches(self):
 
180
        return self._val/72.0
 
181
 
 
182
    def units(self):
 
183
        return 'pts'
 
184
 
 
185
class Millimeter(Size):
 
186
    def to_inches(self):
 
187
        return self._val/25.4
 
188
 
 
189
    def units(self):
 
190
        return 'mm'
 
191
 
 
192
class Centimeter(Size):
 
193
    def to_inches(self):
 
194
        return self._val/2.54
 
195
 
 
196
    def units(self):
 
197
        return 'cm'
 
198
 
 
199
 
 
200
def bintimes(x,y): return x*y
 
201
def binadd(x,y): return x+y
 
202
def binsub(x,y): return x-y
 
203
 
 
204
    
 
205
class RRef:
 
206
    'A read only ref'
 
207
    def __init__(self, val):
 
208
        self._val = val
 
209
 
 
210
    def get(self):
 
211
        return self._val
 
212
 
 
213
    def __add__(self, other):
 
214
        return BinOp(self, other, binadd)
 
215
 
 
216
    def __mul__(self, other):
 
217
        return BinOp(self, other, bintimes)
 
218
 
 
219
    def __sub__(self, other):
 
220
        return BinOp(self, other, binsub)
 
221
 
 
222
class BinOp(RRef):
 
223
    'A read only ref that handles binary ops of refs'
 
224
    def __init__(self, ref1, ref2, func=binadd):
 
225
        self.ref1 = ref1
 
226
        self.ref2 = ref2
 
227
        self.func = func
 
228
 
 
229
    def get(self):
 
230
        return self.func(self.ref1.get(), self.ref2.get())
 
231
 
 
232
class RWRef(RRef):
 
233
    'A readable and writable ref'
 
234
    def set(self, val):
 
235
        self._val = val
 
236
 
 
237
 
 
238
 
 
239
class Bound1D:
 
240
    """
 
241
    Store and update information about a 1D bound
 
242
    """
 
243
    def __init__(self, minval=None, maxval=None, isPos=False):        
 
244
        self._min = RWRef(minval)
 
245
        self._max = RWRef(maxval)
 
246
        self._isPos = isPos
 
247
        if self.defined(): assert(self._max.get()>=self._min.get())
 
248
        
 
249
    def bounds(self):
 
250
        'Return the min, max of the bounds'
 
251
        return self._min.get(), self._max.get()
 
252
 
 
253
    def defined(self):
 
254
        'return true if both endpoints defined'
 
255
        return self._min.get() is not None and self._max.get() is not None
 
256
 
 
257
    def get_refmin(self):
 
258
        return self._min
 
259
 
 
260
    def get_refmax(self):
 
261
        return self._max
 
262
 
 
263
    def in_interval(self, val):
 
264
        'Return true if val is in [min,max]'
 
265
        if not self.defined(): return False
 
266
        smin, smax = self.bounds()
 
267
        if val>=smin and val<=smax: return True
 
268
        else: return False
 
269
 
 
270
    def in_open_interval(self, val):
 
271
        'Return true if val is in (min,max)'
 
272
        if not self.defined(): return False
 
273
        smin, smax = self.bounds()
 
274
        if val>smin and val<smax: return True
 
275
        else: return False
 
276
        
 
277
    def interval(self):
 
278
        'return max - min if defined, else None'
 
279
        if not self.defined(): return None
 
280
        smin, smax = self.bounds()
 
281
        return smax-smin
 
282
 
 
283
    def max(self):
 
284
        'return the max of the bounds'
 
285
        return self._max.get()
 
286
 
 
287
    def min(self):
 
288
        'return the min of the bounds'
 
289
        return self._min.get()
 
290
 
 
291
    def overlap(self, bound):
 
292
       """
 
293
       Return true if bound overlaps with self.
 
294
 
 
295
       Return False if either bound undefined
 
296
       """
 
297
       if not self.defined() or not bound.defined():
 
298
          return False
 
299
       smin, smax = self.bounds()
 
300
       bmin, bmax = bound.bounds()
 
301
       return ( ( smin >= bmin and smin <= bmax)  or 
 
302
                ( bmin >= smin and bmin <= smax ) )
 
303
 
 
304
       
 
305
    def __repr__(self):
 
306
        smin, smax = self.bounds()
 
307
        return 'Bound1D: %s %s' % (smin, smax)
 
308
 
 
309
 
 
310
    def shift(self, val):
 
311
        'Shift min and max by val and return a reference to self'
 
312
        smin, smax = self.bounds()
 
313
        if self._min.get() is not None:
 
314
            newmin = smin + val
 
315
            self.set_min(newmin)
 
316
        if self._max.get() is not None:
 
317
            newmax = smax + val
 
318
            self.set_max(newmax)
 
319
 
 
320
        return self
 
321
    
 
322
    def set_bounds(self, vmin, vmax):
 
323
        'set the min and max to vmin, vmax'
 
324
        self.set_min(vmin)
 
325
        self.set_max(vmax)
 
326
 
 
327
    def set_min(self, vmin):
 
328
        if self._isPos and vmin<=0: return
 
329
        self._min.set(vmin)
 
330
 
 
331
    def set_max(self, vmax):
 
332
        'set the max to vmax'
 
333
        if self._isPos and vmax<=0: return
 
334
        self._max.set(vmax)
 
335
 
 
336
 
 
337
    def scale(self, s):
 
338
        'scale the min and max by ratio s and return a reference to self'
 
339
        if self._isPos and s<=0: return 
 
340
        if not self.defined(): return 
 
341
        i = self.interval()
 
342
        delta = (s*i-i)/2  # todo; correct for s<1
 
343
 
 
344
        vmin, vmax = self.bounds()
 
345
        self._min.set(vmin - delta)
 
346
        self._max.set(vmax + delta)
 
347
        return self
 
348
 
 
349
    def update(self, x):
 
350
        """
 
351
        Update the min and max with values in x.  Eg, only update min
 
352
        if min(x)<self.min().  Return a reference to self
 
353
        """
 
354
        if iterable(x) and len(x)==0: return
 
355
            
 
356
        if not iterable(x):
 
357
            if self._isPos and x<=0: return
 
358
            minx, maxx = x, x
 
359
        else:
 
360
            if self._isPos: x = take(x, nonzero(x>0))
 
361
            if not len(x): return 
 
362
            minx, maxx = min(x), max(x)
 
363
            
 
364
 
 
365
        if self._max.get() is None: self._max.set(maxx)
 
366
        else: self._max.set(max(self._max.get(), maxx))
 
367
 
 
368
        if self._min.get() is None: self._min.set(minx)
 
369
        else: self._min.set( min(self._min.get(), minx))
 
370
        return self
 
371
 
 
372
    def is_positive(self, b):
 
373
        """
 
374
        If true, bound will only return positive endpoints.
 
375
        """
 
376
        self._isPos = b
 
377
        if b:
 
378
            if self._min.get()<=0: self._min.set(None)
 
379
            if self._max.get()<=0: self._max.set( None)
 
380
            
 
381
        
 
382
def bound1d_all(bounds):
 
383
    """
 
384
    Return a Bound1D instance that bounds all the Bound1D instances in
 
385
    sequence bounds.
 
386
 
 
387
    If the min or max val for any of the bounds is None, the
 
388
    respective value for the returned bbox will also be None
 
389
    """
 
390
    
 
391
    if not len(bounds): return Bound1D(None, None)
 
392
    if len(bounds)==1: return bounds[0]
 
393
 
 
394
    # min with a sequence with None is None
 
395
    minval = min([b.min() for b in bounds])
 
396
    
 
397
    # max with a sequence with None is not None, so we use a different
 
398
    # approach
 
399
    maxvals = [b.max() for b in bounds]
 
400
    if None in maxvals: maxval = None
 
401
    else: maxval = max(maxvals)
 
402
    return Bound1D(minval, maxval)
 
403
 
 
404
class Bound2D:
 
405
    """
 
406
    Store and update 2D bounding box information
 
407
 
 
408
    Publicly accessible attributes
 
409
 
 
410
     x the x Bound1D instance
 
411
     y the y Bound2D instance
 
412
 
 
413
    """
 
414
    def __init__(self, left, bottom, width, height):
 
415
        self.x = Bound1D()
 
416
        self.y = Bound1D()
 
417
        self.set_bounds(left, bottom, width, height)
 
418
        
 
419
    def __repr__(self):
 
420
        return 'Bound2D: %s\n\tx: %s\n\ty: %s' % \
 
421
               (list(self.get_bounds()), self.x, self.y) 
 
422
 
 
423
    def copy(self):
 
424
       'Return a deep copy of self'
 
425
       return Bound2D(*self.get_bounds())
 
426
 
 
427
    def defined(self):
 
428
        return self.x.defined() and self.y.defined()
 
429
 
 
430
    def set_bounds(self, left, bottom, width, height):
 
431
        'Reset the bounds'
 
432
        assert(left is not None)
 
433
        assert(bottom is not None)
 
434
        assert(width is not None)
 
435
        assert(height is not None)
 
436
        assert(width>=0)
 
437
        assert(height>=0)
 
438
 
 
439
        minx = left
 
440
        maxx = left + width
 
441
        miny = bottom
 
442
        maxy = bottom + height
 
443
        self.x.set_bounds(minx, maxx)
 
444
        self.y.set_bounds(miny, maxy)
 
445
        
 
446
    def get_bounds(self):
 
447
        left = self.x.min()
 
448
        bottom = self.y.min()
 
449
        width = self.x.interval()
 
450
        height = self.y.interval()
 
451
        return left, bottom, width, height
 
452
 
 
453
    def overlap(self, bound):
 
454
       """
 
455
       Return true if bound overlaps with self.
 
456
 
 
457
       Return False if either bound undefined
 
458
       """
 
459
       if not self.defined() or not bound.defined():
 
460
          return False
 
461
       return self.x.overlap(bound.x) and self.y.overlap(bound.y)
 
462
 
 
463
def bound2d_all(bounds):
 
464
    """
 
465
    Return a Bound2D instance that bounds all the Bound2D instances in
 
466
    sequence bounds.
 
467
 
 
468
    If the min or max val for any of the bounds is None, the
 
469
    respective value for the returned bbox will also be None
 
470
    """
 
471
 
 
472
    bx = bound1d_all([b.x for b in bounds])
 
473
    by = bound1d_all([b.y for b in bounds])
 
474
    
 
475
    left = bx.min()
 
476
    bottom = by.min()
 
477
    width = bx.interval()
 
478
    height = by.interval()
 
479
    return Bound2D(left, bottom, width, height)
 
480
 
 
481
def iterable_to_array(x):
 
482
    if iterable(x): return asarray(x)
 
483
    else: return x
 
484
 
 
485
def identity(x):
 
486
    'The identity function'
 
487
    try: return x.get()
 
488
    except AttributeError: return x
 
489
 
 
490
 
 
491
def logwarn(x):
 
492
    'Return log10 for positive x'
 
493
    # x is a scalar
 
494
    if not iterable(x):
 
495
        if x<=0:
 
496
            print >>sys.stderr, 'log scalar warning, non-positive data ignored'
 
497
            return 0 #todo: JDH: what should we return here?  None?
 
498
        return log10(x)
 
499
 
 
500
    # x is a sequence
 
501
    ind = nonzero(x>0)
 
502
    if len(ind)<len(x):
 
503
        print >>sys.stderr, 'log iterable warning, non-positive data ignored'
 
504
        x = take(x, ind)
 
505
    return log10(x)
 
506
 
 
507
def pow10(x):
 
508
    'the inverse of log10; 10**x'
 
509
    return 10**asarray(x)
 
510
 
 
511
class Transform:
 
512
    """
 
513
    Abstract base class for transforming data
 
514
 
 
515
    Publicly accessible attributes are
 
516
      func            : the transform func
 
517
      ifunc           : the inverse tranform func
 
518
 
 
519
    A tranform from in->out is defined by
 
520
 
 
521
    scale = (maxout-maxin)/( func(maxin)-func(minin) )
 
522
    out =  scale * ( func(in)-func(minin) ) + minout
 
523
 
 
524
    funcs are paired with inverses, allowing Transforms to return
 
525
    their inverse
 
526
    
 
527
    """
 
528
 
 
529
    def __init__(self, boundin=Bound1D(0,1), boundout=Bound1D(0,1), 
 
530
                 funcs = (identity, identity),
 
531
                 ):
 
532
        """
 
533
        The default transform is identity.
 
534
 
 
535
        To do a linear transform, replace the bounds with the
 
536
        coodinate bounds of the input and output spaces
 
537
 
 
538
        To do a log transform, use funcs=(log10, pow10)
 
539
        """
 
540
        
 
541
        self._boundin = boundin
 
542
        self._boundout = boundout
 
543
        self.set_funcs(funcs)
 
544
 
 
545
 
 
546
    def inverse_positions(self, x):
 
547
        'Return the inverse transform of x'
 
548
        x = iterable_to_array(x)
 
549
        minin, maxin = self._boundin.bounds()
 
550
        if self.func != identity:
 
551
            minin, maxin = self.func(minin), self.func(maxin)
 
552
        scale = (maxin-minin)/self._boundout.interval()
 
553
        return  self.ifunc(scale*(x-self._boundout.min()) + minin)
 
554
 
 
555
    def inverse_scale(self, x):
 
556
        'Return the inverse transform of scale x'
 
557
        x = iterable_to_array(x)
 
558
        minin, maxin = self._boundin.bounds()
 
559
        if self.func != identity:
 
560
            minin, maxin = self.func(minin), self.func(maxin)
 
561
        scale = (maxin-minin)/self._boundout.interval()
 
562
        return  self.ifunc(scale*x)
 
563
        
 
564
        
 
565
    def positions(self, x):
 
566
        'Transform the positions in x.'
 
567
        if not self._boundin.defined() or not self._boundout.defined():
 
568
            raise RuntimeError('You must first define the boundaries of the transform')
 
569
        x = iterable_to_array(x)
 
570
        minin, maxin = self._boundin.bounds()
 
571
        if self.func != identity:
 
572
            minin, maxin = self.func(minin), self.func(maxin)
 
573
        scale = self._boundout.interval()/(maxin-minin)
 
574
        return  scale*(self.func(x)-minin) + self._boundout.min()
 
575
    
 
576
    def scale(self, s):
 
577
        'Transform the scale in s'
 
578
        if not self._boundin.defined() or not self._boundout.defined():
 
579
            raise RuntimeError('You must first define the boundaries of the transform')
 
580
        s = iterable_to_array(s)
 
581
        minin, maxin = self._boundin.bounds()
 
582
        if self.func != identity:
 
583
            minin, maxin = self.func(minin), self.func(maxin)
 
584
        scale = self._boundout.interval()/(maxin-minin)
 
585
        return  scale*self.func(s)
 
586
 
 
587
    def __repr__(self):
 
588
        return  'Transform: %s to %s' %(self._boundin, self._boundout)
 
589
    
 
590
    def set_funcs(self, funcs):
 
591
        'Set the func, ifunc to funcs'
 
592
        self.func, self.ifunc = funcs
 
593
 
 
594
 
 
595
class TransformSize:
 
596
    def __init__(self, sin, sout, offset, transOffset=Transform()):
 
597
        """
 
598
        transform size in Size instance sin to Size instance sout,
 
599
        offsetting by Bound1D instance RRef instance offset.
 
600
        transOffset is used to transform the offset if not None
 
601
        """
 
602
        self._sin = sin
 
603
        self._sout = sout
 
604
        self._offset = offset
 
605
        self._transOffset = transOffset
 
606
 
 
607
    def positions(self, x):
 
608
        offset = self._get_offset() 
 
609
        return offset + self._sin.to_inches()/self._sout.to_inches()*x
 
610
 
 
611
    def inverse_positions(self, x):
 
612
        offset = self._get_offset() 
 
613
        return (x - offset)*self._sout.to_inches()/self._sin.to_inches()
 
614
 
 
615
    def inverse_scale(self, x):
 
616
        return self._sout.to_inches()/self._sin.to_inches()*x
 
617
 
 
618
    def scale(self, x):
 
619
        return self._sin.to_inches()/self._sout.to_inches()*x
 
620
 
 
621
    def _get_offset(self):
 
622
        try: o =  self._offset.get()
 
623
        except AttributeError: o =  self._offset
 
624
        return self._transOffset.positions(o)
 
625
 
 
626
 
 
627
 
 
628
def transform_bound1d(bound, trans):
 
629
    """
 
630
    Transform a Bound1D instance using transforms trans
 
631
    """
 
632
    
 
633
    tmin, tmax = trans.positions(bound.bounds())
 
634
    return Bound1D(tmin, tmax)
 
635
 
 
636
def inverse_transform_bound1d(bound, trans):
 
637
    """
 
638
    Inverse transform a Bound1D instance using trans.inverse()
 
639
    """
 
640
    
 
641
    tmin, tmax = trans.inverse_positions(bound.bounds())
 
642
    return Bound1D(tmin, tmax)
 
643
 
 
644
 
 
645
def transform_bound2d(bbox, transx, transy):
 
646
    """
 
647
    Transform a Bound2D instance using transforms transx, transy
 
648
    """
 
649
    b1 = transform_bound1d(bbox.x, transx)
 
650
    b2 = transform_bound1d(bbox.y, transy)
 
651
    l = b1.min()
 
652
    w = b1.interval()
 
653
    b = b2.min()
 
654
    h = b2.interval()
 
655
    return Bound2D(l,b,w,h)
 
656
 
 
657
def inverse_transform_bound2d(bbox, transx, transy):
 
658
    """
 
659
    Inverse transform a Bound2D instance using transforms transx, transy
 
660
    """
 
661
    b1 = inverse_transform_bound1d(bbox.x, transx)
 
662
    b2 = inverse_transform_bound1d(bbox.y, transy)
 
663
    l = b1.min()
 
664
    w = b1.interval()
 
665
    b = b2.min()
 
666
    h = b2.interval()
 
667
    return Bound2D(l,b,w,h)
 
668
    
 
669
 
 
670
 
 
671