From: <md...@us...> - 2007-09-14 13:03:33
|
Revision: 3851 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3851&view=rev Author: mdboom Date: 2007-09-14 06:03:31 -0700 (Fri, 14 Sep 2007) Log Message: ----------- Removed transforms on the C++ side -- removed many methods that depend on it in backend_agg in preparation for path generalization. Lots of general renaming... Modified Paths: -------------- branches/transforms/lib/matplotlib/artist.py branches/transforms/lib/matplotlib/axes.py branches/transforms/lib/matplotlib/axis.py branches/transforms/lib/matplotlib/backends/backend_agg.py branches/transforms/lib/matplotlib/figure.py branches/transforms/lib/matplotlib/finance.py branches/transforms/lib/matplotlib/legend.py branches/transforms/lib/matplotlib/lines.py branches/transforms/lib/matplotlib/table.py branches/transforms/lib/matplotlib/text.py branches/transforms/lib/matplotlib/ticker.py branches/transforms/lib/matplotlib/widgets.py branches/transforms/setup.py branches/transforms/setupext.py branches/transforms/src/_backend_agg.cpp branches/transforms/src/_backend_agg.h branches/transforms/src/_gtkagg.cpp branches/transforms/src/_tkagg.cpp Added Paths: ----------- branches/transforms/lib/matplotlib/transforms.py Removed Paths: ------------- branches/transforms/lib/matplotlib/affine.py branches/transforms/src/_transforms.cpp branches/transforms/src/_transforms.h Deleted: branches/transforms/lib/matplotlib/affine.py =================================================================== --- branches/transforms/lib/matplotlib/affine.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/affine.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -1,854 +0,0 @@ -""" -A set of classes to handle transformations. - -2007 Michael Droettboom -""" - -import numpy as npy -from numpy.linalg import inv -from sets import Set - -# MGDTODO: The name of this module is bad, since it deals with -# non-affine transformations as well. It should probably just be -# "transforms", but we already had one of those... ;) - -# MGDTODO: This creates a ton of cyclical references. We may want to -# consider using weak references - -# MGDTODO: deep copying is probably incorrect wrt the parent/child -# relationships - -class TransformNode(object): - def __init__(self): - self._parents = Set() - - def invalidate(self): - if not self._do_invalidation(): - for parent in self._parents: - parent.invalidate() - - def _do_invalidation(self): - return False - - def set_children(self, children): - for child in children: - getattr(self, child)._parents.add(self) - self._children = children - -# def replace_child(self, index, child): -# children = self._children -# getattr(self, children[index])._parents.remove(self) -# setattr(self, children[index], child) -# # We have to reset children in case two or more -# # of the children are the same -# for child in children: -# getattr(self, child)._parents.add(self) -# self.invalidate() - -class BboxBase(TransformNode): - ''' - This is the read-only part of a bounding-box - ''' - - def __init__(self): - TransformNode.__init__(self) - - def __array__(self): - return self.get_points() - - # MGDTODO: Probably a more efficient ways to do this... - def _get_xmin(self): - return self.get_points()[0, 0] - xmin = property(_get_xmin) - - def _get_ymin(self): - return self.get_points()[0, 1] - ymin = property(_get_ymin) - - def _get_xmax(self): - return self.get_points()[1, 0] - xmax = property(_get_xmax) - - def _get_ymax(self): - return self.get_points()[1, 1] - ymax = property(_get_ymax) - - def _get_min(self): - return self.get_points()[0] - min = property(_get_min) - - def _get_max(self): - return self.get_points()[1] - max = property(_get_max) - - def _get_intervalx(self): - return self.get_points()[:, 0] - intervalx = property(_get_intervalx) - - def _get_intervaly(self): - return self.get_points()[:, 1] - intervaly = property(_get_intervaly) - - def _get_width(self): - return self.xmax - self.xmin - width = property(_get_width) - - def _get_height(self): - return self.ymax - self.ymin - height = property(_get_height) - - def _get_bounds(self): - return (self.xmin, self.ymin, - self.xmax - self.xmin, self.ymax - self.ymin) - bounds = property(_get_bounds) - - def get_points(self): - return NotImplementedError() - - # MGDTODO: Optimize - def containsx(self, x): - return x >= self.xmin and x <= self.xmax - - def containsy(self, y): - return y >= self.ymin and y <= self.ymax - - def contains(self, x, y): - return self.containsx(x) and self.containsy(y) - - def overlapsx(self, other): - return self.containsx(other.xmin) \ - or self.containsx(other.xmax) - - def overlapsy(self, other): - return self.containsy(other.ymin) \ - or self.containsx(other.ymax) - - def overlaps(self, other): - return self.overlapsx(other) \ - and self.overlapsy(other) - - def fully_containsx(self, x): - return x > self.xmin and x < self.xmax - - def fully_containsy(self, y): - return y > self.ymin and y < self.ymax - - def fully_contains(self, x, y): - return self.fully_containsx(x) \ - and self.fully_containsy(y) - - def fully_overlapsx(self, other): - return self.fully_containsx(other.xmin) \ - or self.fully_containsx(other.xmax) - - def fully_overlapsy(self, other): - return self.fully_containsy(other.ymin) \ - or self.fully_containsx(other.ymax) - - def fully_overlaps(self, other): - return self.fully_overlapsx(other) and \ - self.fully_overlapsy(other) - - -class Bbox(BboxBase): - def __init__(self, points): - BboxBase.__init__(self) - self._points = npy.asarray(points, npy.float_) - - #@staticmethod - def unit(): - return Bbox.from_lbrt(0., 0., 1., 1.) - unit = staticmethod(unit) - - #@staticmethod - def from_lbwh(left, bottom, width, height): - return Bbox.from_lbrt(left, bottom, left + width, bottom + height) - from_lbwh = staticmethod(from_lbwh) - - #@staticmethod - def from_lbrt(*args): - points = npy.array(args, dtype=npy.float_).reshape(2, 2) - return Bbox(points) - from_lbrt = staticmethod(from_lbrt) - - def __copy__(self): - return Bbox(self._points.copy()) - - def __deepcopy__(self, memo): - return Bbox(self._points.copy()) - - def __cmp__(self, other): - # MGDTODO: Totally suboptimal - if isinstance(other, Bbox) and (self._points == other._points).all(): - return 0 - return -1 - - def __repr__(self): - return 'Bbox(%s)' % repr(self._points) - __str__ = __repr__ - - # JDH: the update method will update the box limits from the - # existing limits and the new data; it appears here you are just - # using the new data. We use an "ignore" flag to specify whether - # you want to include the existing data or not in the update - def update_from_data(self, x, y, ignore=True): - self._points = npy.array( - [[x.min(), y.min()], [x.max(), y.max()]], - npy.float_) - self.invalidate() - - # MGDTODO: Probably a more efficient ways to do this... - def _set_xmin(self, val): - self._points[0, 0] = val - self.invalidate() - xmin = property(BboxBase._get_xmin, _set_xmin) - - def _set_ymin(self, val): - self._points[0, 1] = val - self.invalidate() - ymin = property(BboxBase._get_ymin, _set_ymin) - - def _set_xmax(self, val): - self._points[1, 0] = val - self.invalidate() - xmax = property(BboxBase._get_xmax, _set_xmax) - - def _set_ymax(self, val): - self._points[1, 1] = val - self.invalidate() - ymax = property(BboxBase._get_ymax, _set_ymax) - - def _set_min(self, val): - self._points[0] = val - self.invalidate() - min = property(BboxBase._get_min, _set_min) - - def _set_max(self, val): - self._points[1] = val - self.invalidate() - max = property(BboxBase._get_max, _set_max) - - def _set_intervalx(self, interval): - self._points[:, 0] = interval - self.invalidate() - intervalx = property(BboxBase._get_intervalx, _set_intervalx) - - def _set_intervaly(self, interval): - self._points[:, 1] = interval - self.invalidate() - intervaly = property(BboxBase._get_intervaly, _set_intervaly) - - def _set_bounds(self, bounds): - l,b,w,h = bounds - self._points = npy.array([[l, b], [l+w, b+h]], npy.float_) - self.invalidate() - bounds = property(BboxBase._get_bounds, _set_bounds) - - def get_points(self): - return self._points - - def set_points(self, points): - self._points = points - self.invalidate() - - def set(self, other): - self._points = other.get_points() - self.invalidate() - - def transformed(self, transform): - return Bbox(transform(self._points)) - - def inverse_transformed(self, transform): - return Bbox(transform.inverted()(self._points)) - - def expanded(self, sw, sh): - width = self.width - height = self.height - deltaw = (sw * width - width) / 2.0 - deltah = (sh * height - height) / 2.0 - a = npy.array([[-deltaw, -deltah], [deltaw, deltah]]) - return Bbox(self._points + a) - - #@staticmethod - def union(bboxes): - """ - Return the Bbox that bounds all bboxes - """ - assert(len(bboxes)) - - if len(bboxes) == 1: - return bboxes[0] - - bbox = bboxes[0] - xmin = bbox.xmin - ymin = bbox.ymin - xmax = bbox.xmax - ymax = bbox.ymax - - for bbox in bboxes[1:]: - xmin = min(xmin, bbox.xmin) - ymin = min(ymin, bbox.ymin) - xmax = max(xmax, bbox.xmax) - ymax = max(ymax, bbox.ymax) - - return Bbox.from_lbrt(xmin, ymin, xmax, ymax) - union = staticmethod(union) - -class TransformedBbox(BboxBase): - def __init__(self, bbox, transform): - assert isinstance(bbox, Bbox) - assert isinstance(transform, Transform) - - BboxBase.__init__(self) - self.bbox = bbox - self.transform = transform - self.set_children(['bbox', 'transform']) - self._points = None - - def __repr__(self): - return "TransformedBbox(%s, %s)" % (self.bbox, self.transform) - __str__ = __repr__ - - def _do_invalidation(self): - self._points = None - - def get_points(self): - if self._points is None: - self._points = self.transform(self.bbox.get_points()) - return self._points - -# MGDTODO: This code probably works, but I don't think it's a good idea -# (from a code clarity perspective) -# class BlendedBbox(BboxBase): -# def __init__(self, bbox_x, bbox_y): -# assert isinstance(bbox_x, BboxBase) -# assert isinstance(bbox_y, BboxBase) - -# BboxBase.__init__(self) -# self._x = bbox_x -# self._y = bbox_y -# self.set_children(['_x', '_y']) -# self._points = None - -# def __repr__(self): -# return "TransformedBbox(%s, %s)" % (self.bbox, self.transform) -# __str__ = __repr__ - -# def _do_invalidation(self): -# self._points = None - -# def get_points(self): -# if self._points is None: -# # MGDTODO: Optimize -# if self._x == self._y: -# self._points = self._x.get_points() -# else: -# x_points = self._x.get_points() -# y_points = self._y.get_points() -# self._points = npy.array( -# [[x_points[0,0], y_points[0,1]], -# [x_points[1,0], y_points[1,1]]], -# npy.float_) -# return self._points - -# def _set_intervalx(self, pair): -# # MGDTODO: Optimize -# bbox = Bbox([[pair[0], 0.0], [pair[1], 0.0]]) -# self.replace_child(0, bbox) -# intervalx = property(BboxBase._get_intervalx, _set_intervalx) - -# def _set_intervaly(self, pair): -# # MGDTODO: Optimize -# bbox = Bbox([[0.0, pair[0]], [0.0, pair[1]]]) -# self.replace_child(1, bbox) -# intervaly = property(BboxBase._get_intervaly, _set_intervaly) - -class Transform(TransformNode): - def __init__(self): - TransformNode.__init__(self) - - def __call__(self, points): - raise NotImplementedError() - - def __add__(self, other): - if isinstance(other, Transform): - return composite_transform_factory(self, other) - raise TypeError( - "Can not add Transform to object of type '%s'" % type(other)) - - def __radd__(self, other): - if isinstance(other, Transform): - return composite_transform_factory(other, self) - raise TypeError( - "Can not add Transform to object of type '%s'" % type(other)) - - def transform_point(self, point): - return self.__call__(npy.asarray([point]))[0] - - def has_inverse(self): - raise NotImplementedError() - - def inverted(self): - raise NotImplementedError() - - def is_separable(self): - return False - - def is_affine(self): - return False - -class Affine2DBase(Transform): - input_dims = 2 - output_dims = 2 - - def __init__(self): - Transform.__init__(self) - self._inverted = None - - def _do_invalidation(self): - result = self._inverted is None - self._inverted = None - return result - - #@staticmethod - def _concat(a, b): - return npy.dot(b, a) - _concat = staticmethod(_concat) - - #@staticmethod - def concat(a, b): - return Affine2D(Affine2D._concat(a.get_matrix(), b.get_matrix())) - concat = staticmethod(concat) - - def to_values(self): - mtx = self.get_matrix() - return tuple(mtx[:2].swapaxes(0, 1).flatten()) - - #@staticmethod - def matrix_from_values(a, b, c, d, e, f): - affine = npy.zeros((3, 3), npy.float_) - affine[0, ] = a, c, e - affine[1, ] = b, d, f - affine[2, 2] = 1 - return affine - matrix_from_values = staticmethod(matrix_from_values) - - def get_matrix(self): - raise NotImplementedError() - - def __call__(self, points): - """ - Applies the transformation to an array of 2D points and - returns the result. - - points must be a numpy array of shape (N, 2), where N is the - number of points. - """ - # MGDTODO: The major speed trap here is just converting to - # the points to an array in the first place. If we can use - # more arrays upstream, that should help here. - if not isinstance(points, npy.ndarray): - import traceback - print '-' * 60 - print 'A non-numpy array was passed in for transformation. Please ' - print 'correct this.' - print "".join(traceback.format_stack()) - print points - mtx = self.get_matrix() - points = npy.asarray(points, npy.float_) - points = points.transpose() - points = npy.dot(mtx[0:2, 0:2], points) - points = points + mtx[0:2, 2:] - return points.transpose() - - def inverted(self): - if self._inverted is None: - mtx = self.get_matrix() - self._inverted = Affine2D(inv(mtx)) - return self._inverted - - def is_separable(self): - mtx = self.get_matrix() - return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 - - def is_affine(self): - return True - - -class Affine2D(Affine2DBase): - input_dims = 2 - output_dims = 2 - - def __init__(self, matrix = None): - """ - Initialize an Affine transform from a 3x3 numpy float array. - - a c e - b d f - 0 0 1 - """ - Affine2DBase.__init__(self) - if matrix is None: - matrix = npy.identity(3) - else: - assert matrix.shape == (3, 3) - self._mtx = matrix - self._inverted = None - - def __repr__(self): - return "Affine2D(%s)" % repr(self._mtx) - __str__ = __repr__ - - def __cmp__(self, other): - if (isinstance(other, Affine2D) and - (self.get_matrix() == other.get_matrix()).all()): - return 0 - return -1 - - def __copy__(self): - return Affine2D(self._mtx.copy()) - - def __deepcopy__(self, memo): - return Affine2D(self._mtx.copy()) - - #@staticmethod - def from_values(a, b, c, d, e, f): - return Affine2D(Affine2D.matrix_from_values(a, b, c, d, e, f)) - from_values = staticmethod(from_values) - - def get_matrix(self): - return self._mtx - - def set_matrix(self, mtx): - self._mtx = mtx - self.invalidate() - - def set(self, other): - self._mtx = other.get_matrix() - self.invalidate() - - #@staticmethod - def identity(): - return Affine2D(npy.identity(3)) - identity = staticmethod(identity) - - def clear(self): - self._mtx = npy.identity(3) - self.invalidate() - return self - - def rotate(self, theta): - a = npy.cos(theta) - b = npy.sin(theta) - rotate_mtx = self.matrix_from_values(a, b, -b, a, 0, 0) - self._mtx = self._concat(self._mtx, rotate_mtx) - self.invalidate() - return self - - def rotate_deg(self, degrees): - return self.rotate(degrees*npy.pi/180.) - - def rotate_around(self, x, y, theta): - return self.translate(-x, -y).rotate(theta).translate(x, y) - - def rotate_deg_around(self, x, y, degrees): - return self.translate(-x, -y).rotate_deg(degrees).translate(x, y) - - def translate(self, tx, ty): - translate_mtx = self.matrix_from_values(1., 0., 0., 1., tx, ty) - self._mtx = self._concat(self._mtx, translate_mtx) - self.invalidate() - return self - - def scale(self, sx, sy=None): - if sy is None: - sy = sx - scale_mtx = self.matrix_from_values(sx, 0., 0., sy, 0., 0.) - self._mtx = self._concat(self._mtx, scale_mtx) - self.invalidate() - return self - - def inverted(self): - if self._inverted is None: - mtx = self.get_matrix() - self._inverted = Affine2D(inv(mtx)) - return self._inverted - - def is_separable(self): - mtx = self.get_matrix() - return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 - - def is_affine(self): - return True - -class BlendedTransform(Transform): - def __init__(self, x_transform, y_transform): - assert x_transform.is_separable() - assert y_transform.is_separable() - - Transform.__init__(self) - self._x = x_transform - self._y = y_transform - self.set_children(['_x', '_y']) - - def __call__(self, points): - if self._x == self._y: - return self._x(points) - - x_points = self._x(points) - y_points = self._y(points) - # This works because we already know the transforms are - # separable - return npy.hstack((x_points[:, 0:1], y_points[:, 1:2])) - -# def set_x_transform(self, x_transform): -# self.replace_child(0, x_transform) - -# def set_y_transform(self, y_transform): -# self.replace_child(1, y_transform) - -class BlendedAffine2D(Affine2DBase, BlendedTransform): - def __init__(self, x_transform, y_transform): - assert x_transform.is_affine() - assert y_transform.is_affine() - assert x_transform.is_separable() - assert y_transform.is_separable() - BlendedTransform.__init__(self, x_transform, y_transform) - - Affine2DBase.__init__(self) - self._mtx = None - - def __repr__(self): - return "BlendedAffine2D(%s,%s)" % (self._x, self._y) - __str__ = __repr__ - - def _do_invalidation(self): - if self._mtx is not None: - self._mtx = None - Affine2DBase._do_invalidation(self) - return False - return True - - def is_separable(self): - return True - - def get_matrix(self): - if self._mtx is None: - if self._x == self._y: - self._mtx = self._x.get_matrix() - else: - x_mtx = self._x.get_matrix() - y_mtx = self._y.get_matrix() - # This works because we already know the transforms are - # separable, though normally one would want to set b and - # c to zero. - self._mtx = npy.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0])) - return self._mtx - -class CompositeTransform(Transform): - def __init__(self, a, b): - assert a.output_dims == b.input_dims - self.input_dims = a.input_dims - self.output_dims = b.output_dims - - Transform.__init__(self) - self._a = a - self._b = b - self.set_children(['_a', '_b']) - - def __call__(self, points): - return self._b(self._a(points)) - -class CompositeAffine2D(Affine2DBase): - def __init__(self, a, b): - assert a.is_affine() - assert b.is_affine() - - Affine2DBase.__init__(self) - self._a = a - self._b = b - self.set_children(['_a', '_b']) - self._mtx = None - - def __repr__(self): - return "CompositeAffine2D(%s, %s)" % (self._a, self._b) - __str__ = __repr__ - - def _do_invalidation(self): - self._mtx = None - Affine2DBase._do_invalidation(self) - - def get_matrix(self): - if self._mtx is None: - self._mtx = self._concat( - self._a.get_matrix(), - self._b.get_matrix()) - return self._mtx - -class BboxTransform(Affine2DBase): - def __init__(self, boxin, boxout): - assert isinstance(boxin, BboxBase) - assert isinstance(boxout, BboxBase) - - Affine2DBase.__init__(self) - self._boxin = boxin - self._boxout = boxout - self.set_children(['_boxin', '_boxout']) - self._mtx = None - self._inverted = None - - def __repr__(self): - return "BboxTransform(%s, %s)" % (self._boxin, self._boxout) - __str__ = __repr__ - - def _do_invalidation(self): - if self._mtx is not None: - self._mtx = None - Affine2DBase._do_invalidation(self) - return False - return True - - def is_separable(self): - return True - - def get_matrix(self): - if self._mtx is None: - boxin = self._boxin - boxout = self._boxout - x_scale = boxout.width / boxin.width - y_scale = boxout.height / boxin.height - - # MGDTODO: Optimize - affine = Affine2D() \ - .translate(-boxin.xmin, -boxin.ymin) \ - .scale(x_scale, y_scale) \ - .translate(boxout.xmin, boxout.ymin) - - self._mtx = affine._mtx - return self._mtx - -def blend_xy_sep_transform(x_transform, y_transform): - if x_transform.is_affine() and y_transform.is_affine(): - return BlendedAffine2D(x_transform, y_transform) - return BlendedTransform(x_transform, y_transform) - -def composite_transform_factory(a, b): - if a.is_affine() and b.is_affine(): - return CompositeAffine2D(a, b) - return CompositeTransform(a, b) - -# MGDTODO: There's probably a better place for this -def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): - ''' - Ensure the endpoints of a range are not too close together. - - "too close" means the interval is smaller than 'tiny' times - the maximum absolute value. - - If they are too close, each will be moved by the 'expander'. - If 'increasing' is True and vmin > vmax, they will be swapped, - regardless of whether they are too close. - ''' - swapped = False - if vmax < vmin: - vmin, vmax = vmax, vmin - swapped = True - if vmax - vmin <= max(abs(vmin), abs(vmax)) * tiny: - if vmin == 0.0: - vmin = -expander - vmax = expander - else: - vmin -= expander*abs(vmin) - vmax += expander*abs(vmax) - if swapped and not increasing: - vmin, vmax = vmax, vmin - return vmin, vmax - -# MGDTODO: Optimize -def interval_contains(interval, val): - return interval[0] <= val and interval[1] >= val - -def interval_contains_open(interval, val): - return interval[0] < val and interval[1] > val - -if __name__ == '__main__': - import copy - from random import random - import timeit - - bbox = Bbox.from_lbrt(10., 15., 20., 25.) - assert bbox.xmin == 10 - assert bbox.ymin == 15 - assert bbox.xmax == 20 - assert bbox.ymax == 25 - - assert npy.all(bbox.min == [10, 15]) - assert npy.all(bbox.max == [20, 25]) - assert npy.all(bbox.intervalx == (10, 20)) - assert npy.all(bbox.intervaly == (15, 25)) - - assert bbox.width == 10 - assert bbox.height == 10 - - assert bbox.bounds == (10, 15, 10, 10) - - print npy.asarray(bbox) - - bbox.intervalx = (11, 21) - bbox.intervaly = (16, 26) - - assert bbox.bounds == (11, 16, 10, 10) - - bbox.xmin = 12 - bbox.ymin = 17 - bbox.xmax = 22 - bbox.ymax = 27 - - assert bbox.bounds == (12, 17, 10, 10) - - bbox = Bbox.from_lbwh(10, 11, 12, 13) - assert bbox.bounds == (10, 11, 12, 13) - - bbox_copy = copy.copy(bbox) - assert bbox == bbox_copy - bbox_copy.max = (14, 15) - assert bbox.bounds == (10, 11, 12, 13) - assert bbox_copy.bounds == (10, 11, 4, 4) - - bbox1 = Bbox([[10., 15.], [20., 25.]]) - bbox2 = Bbox([[30., 35.], [40., 45.]]) - trans = BboxTransform(bbox1, bbox2) - bbox3 = bbox1.transformed(trans) - assert bbox3 == bbox2 - - translation = Affine2D().translate(10, 20) - assert translation.to_values() == (1, 0, 0, 1, 10, 20) - scale = Affine2D().scale(10, 20) - assert scale.to_values() == (10, 0, 0, 20, 0, 0) - rotation = Affine2D().rotate_deg(30) - print rotation.to_values() == (0.86602540378443871, 0.49999999999999994, - -0.49999999999999994, 0.86602540378443871, - 0.0, 0.0) - - points = npy.array([[1,2],[3,4],[5,6],[7,8]], npy.float_) - translated_points = translation(points) - assert (translated_points == [[11., 22.], [13., 24.], [15., 26.], [17., 28.]]).all() - scaled_points = scale(points) - print scaled_points - rotated_points = rotation(points) - print rotated_points - - tpoints1 = rotation(translation(scale(points))) - trans_sum = scale + translation + rotation - tpoints2 = trans_sum(points) - print tpoints1, tpoints2 - print tpoints1 == tpoints2 - # Need to do some sort of fuzzy comparison here? - # assert (tpoints1 == tpoints2).all() - - # Here are some timing tests - points = npy.asarray([(random(), random()) for i in xrange(10000)]) - t = timeit.Timer("trans_sum(points)", "from __main__ import trans_sum, points") - print "Time to transform 10000 x 10 points:", t.timeit(10) - -__all__ = ['Transform', 'Affine2D'] Modified: branches/transforms/lib/matplotlib/artist.py =================================================================== --- branches/transforms/lib/matplotlib/artist.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/artist.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -1,7 +1,7 @@ from __future__ import division import sys, re from cbook import iterable, flatten -from affine import Affine2D +from transforms import Affine2D import matplotlib.units as units ## Note, matplotlib artists use the doc strings for set and get Modified: branches/transforms/lib/matplotlib/axes.py =================================================================== --- branches/transforms/lib/matplotlib/axes.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/axes.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -9,7 +9,6 @@ rcParams = matplotlib.rcParams from matplotlib import artist as martist -from matplotlib import affine as maffine from matplotlib import agg from matplotlib import axis as maxis from matplotlib import cbook @@ -29,6 +28,7 @@ from matplotlib import table as mtable from matplotlib import text as mtext from matplotlib import ticker as mticker +from matplotlib import transforms as mtransforms iterable = cbook.iterable is_string_like = cbook.is_string_like @@ -481,7 +481,7 @@ """ martist.Artist.__init__(self) - self._position = maffine.Bbox.from_lbwh(*rect) + self._position = mtransforms.Bbox.from_lbwh(*rect) self._originalPosition = copy.deepcopy(self._position) self.set_axes(self) self.set_aspect('auto') @@ -612,7 +612,7 @@ """ martist.Artist.set_figure(self, fig) - self.bbox = maffine.TransformedBbox(self._position, fig.transFigure) + self.bbox = mtransforms.TransformedBbox(self._position, fig.transFigure) #these will be updated later as data is added self._set_lim_and_transforms() @@ -631,7 +631,7 @@ set the dataLim and viewLim BBox attributes and the transData and transAxes Transformation attributes """ - Bbox = maffine.Bbox + Bbox = mtransforms.Bbox self.viewLim = Bbox.unit() if self._sharex is not None: @@ -647,20 +647,11 @@ self.dataLim = Bbox.unit() - self.transAxes = maffine.BboxTransform( + self.transAxes = mtransforms.BboxTransform( Bbox.unit(), self.bbox) - localTransData = maffine.BboxTransform( + self.transData = mtransforms.BboxTransform( self.viewLim, self.bbox) - if self._sharex: - transDataX = self._sharex.transData - else: - transDataX = localTransData - if self._sharey: - transDataY = self._sharey.transData - else: - transDataY = localTransData - self.transData = localTransData # maffine.blend_xy_sep_transform(transDataX, transDataY) def get_position(self, original=False): @@ -1538,7 +1529,7 @@ # and min(xmin, xmax)<=0): # raise ValueError('Cannot set nonpositive limits with log transform') - xmin, xmax = maffine.nonsingular(xmin, xmax, increasing=False) + xmin, xmax = mtransforms.nonsingular(xmin, xmax, increasing=False) self.viewLim.intervalx = (xmin, xmax) if emit: @@ -1670,7 +1661,7 @@ # and min(ymin, ymax)<=0): # raise ValueError('Cannot set nonpositive limits with log transform') - ymin, ymax = maffine.nonsingular(ymin, ymax, increasing=False) + ymin, ymax = mtransforms.nonsingular(ymin, ymax, increasing=False) self.viewLim.intervaly = (ymin, ymax) if emit: self.callbacks.process('ylim_changed', self) @@ -2167,7 +2158,7 @@ %(Annotation)s """ a = mtext.Annotation(*args, **kwargs) - a.set_transform(maffine.Affine2D()) + a.set_transform(mtransforms.Affine2D()) self._set_artist_props(a) if kwargs.has_key('clip_on'): a.set_clip_box(self.bbox) self.texts.append(a) @@ -2206,7 +2197,7 @@ %(Line2D)s """ - trans = maffine.blend_xy_sep_transform(self.transAxes, self.transData) + trans = mtransforms.blend_xy_sep_transform(self.transAxes, self.transData) l, = self.plot([xmin,xmax], [y,y], transform=trans, scalex=False, **kwargs) return l @@ -2242,7 +2233,7 @@ %(Line2D)s """ - trans = maffine.blend_xy_sep_transform( self.transData, self.transAxes ) + trans = mtransforms.blend_xy_sep_transform( self.transData, self.transAxes ) l, = self.plot([x,x], [ymin,ymax] , transform=trans, scaley=False, **kwargs) return l @@ -2281,7 +2272,7 @@ %(Polygon)s """ # convert y axis units - trans = maffine.blend_xy_sep_transform( self.transAxes, self.transData) + trans = mtransforms.blend_xy_sep_transform( self.transAxes, self.transData) verts = (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin) p = mpatches.Polygon(verts, **kwargs) p.set_transform(trans) @@ -2321,7 +2312,7 @@ %(Polygon)s """ # convert x axis units - trans = maffine.blend_xy_sep_transform(self.transData, self.transAxes) + trans = mtransforms.blend_xy_sep_transform(self.transData, self.transAxes) verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] p = mpatches.Polygon(verts, **kwargs) p.set_transform(trans) @@ -4104,7 +4095,7 @@ offsets = zip(x,y), transOffset = self.transData, ) - collection.set_transform(maffine.Affine2D()) + collection.set_transform(mtransforms.Affine2D()) collection.set_alpha(alpha) collection.update(kwargs) Modified: branches/transforms/lib/matplotlib/axis.py =================================================================== --- branches/transforms/lib/matplotlib/axis.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/axis.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -15,10 +15,10 @@ from ticker import NullFormatter, FixedFormatter, ScalarFormatter, LogFormatter from ticker import NullLocator, FixedLocator, LinearLocator, LogLocator, AutoLocator -from affine import Affine2D, Bbox, blend_xy_sep_transform, interval_contains, \ - interval_contains_open from font_manager import FontProperties from text import Text, TextWithDash, _process_text_args +from transforms import Affine2D, Bbox, blended_transform_factory, interval_contains, \ + interval_contains_open from patches import bbox_artist import matplotlib.units as units @@ -236,7 +236,7 @@ xaxis=True, ) - trans = blend_xy_sep_transform( + trans = blended_transform_factory( self.axes.transData, self.axes.transAxes) #offset the text downward with a post transformation trans = trans + Affine2D().translate(0, -1 * self._padPixels) @@ -261,7 +261,7 @@ horizontalalignment='center', ) - trans = blend_xy_sep_transform( + trans = blended_transform_factory( self.axes.transData, self.axes.transAxes) # offset the text upward with a post transformation trans = trans + Affine2D().translate(0, self._padPixels) @@ -279,7 +279,7 @@ marker = self._xtickmarkers[0], markersize=self._size, ) - l.set_transform(blend_xy_sep_transform( + l.set_transform(blended_transform_factory( self.axes.transData, self.axes.transAxes) ) self._set_artist_props(l) return l @@ -295,7 +295,7 @@ markersize=self._size, ) - l.set_transform(blend_xy_sep_transform( + l.set_transform(blended_transform_factory( self.axes.transData, self.axes.transAxes) ) self._set_artist_props(l) return l @@ -310,7 +310,7 @@ antialiased=False, ) l.set_transform( - blend_xy_sep_transform( + blended_transform_factory( self.axes.transData, self.axes.transAxes)) l.set_clip_box(self.axes.bbox) self._set_artist_props(l) @@ -359,7 +359,7 @@ dashdirection=0, xaxis=False, ) - trans = blend_xy_sep_transform( + trans = blended_transform_factory( self.axes.transAxes, self.axes.transData) # offset the text leftward with a post transformation trans = trans + Affine2D().translate(-1 * self._padPixels, 0) @@ -382,7 +382,7 @@ xaxis=False, horizontalalignment='left', ) - trans = blend_xy_sep_transform( + trans = blended_transform_factory( self.axes.transAxes, self.axes.transData) # offset the text rightward with a post transformation trans = trans + Affine2D().translate(self._padPixels, 0) @@ -401,7 +401,7 @@ markersize=self._size, ) l.set_transform( - blend_xy_sep_transform( + blended_transform_factory( self.axes.transAxes, self.axes.transData)) self._set_artist_props(l) return l @@ -417,7 +417,7 @@ ) l.set_transform( - blend_xy_sep_transform( + blended_transform_factory( self.axes.transAxes, self.axes.transData)) self._set_artist_props(l) return l @@ -432,7 +432,7 @@ antialiased=False, ) - l.set_transform( blend_xy_sep_transform( + l.set_transform( blended_transform_factory( self.axes.transAxes, self.axes.transData) ) l.set_clip_box(self.axes.bbox) @@ -979,7 +979,7 @@ verticalalignment='top', horizontalalignment='center', ) - label.set_transform( blend_xy_sep_transform( + label.set_transform( blended_transform_factory( self.axes.transAxes, Affine2D() )) self._set_artist_props(label) @@ -994,7 +994,7 @@ verticalalignment='top', horizontalalignment='right', ) - offsetText.set_transform( blend_xy_sep_transform( + offsetText.set_transform( blended_transform_factory( self.axes.transAxes, Affine2D() )) self._set_artist_props(offsetText) self.offset_text_position='bottom' @@ -1169,7 +1169,7 @@ horizontalalignment='right', rotation='vertical', ) - label.set_transform( blend_xy_sep_transform( + label.set_transform( blended_transform_factory( Affine2D(), self.axes.transAxes) ) self._set_artist_props(label) @@ -1184,7 +1184,7 @@ verticalalignment = 'bottom', horizontalalignment = 'left', ) - offsetText.set_transform(blend_xy_sep_transform( + offsetText.set_transform(blended_transform_factory( self.axes.transAxes, Affine2D()) ) self._set_artist_props(offsetText) self.offset_text_position='left' Modified: branches/transforms/lib/matplotlib/backends/backend_agg.py =================================================================== --- branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/backends/backend_agg.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -84,7 +84,7 @@ from matplotlib.font_manager import findfont from matplotlib.ft2font import FT2Font, LOAD_DEFAULT from matplotlib.mathtext import MathTextParser -from matplotlib.affine import Bbox +from matplotlib.transforms import Bbox from _backend_agg import RendererAgg as _RendererAgg @@ -115,15 +115,10 @@ 'debug-annoying') # self.draw_polygon = self._renderer.draw_polygon self.draw_rectangle = self._renderer.draw_rectangle - self.draw_path = self._renderer.draw_path # MGDTODO -- remove these lines # self.draw_lines = self._renderer.draw_lines # self.draw_markers = self._renderer.draw_markers self.draw_image = self._renderer.draw_image - self.draw_line_collection = self._renderer.draw_line_collection - self.draw_quad_mesh = self._renderer.draw_quad_mesh - self.draw_poly_collection = self._renderer.draw_poly_collection - self.draw_regpoly_collection = self._renderer.draw_regpoly_collection self.copy_from_bbox = self._renderer.copy_from_bbox self.restore_region = self._renderer.restore_region Modified: branches/transforms/lib/matplotlib/figure.py =================================================================== --- branches/transforms/lib/matplotlib/figure.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/figure.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -18,8 +18,8 @@ from text import Text, _process_text_args from legend import Legend -from affine import Affine2D, Bbox, BboxTransform, TransformedBbox from ticker import FormatStrFormatter +from transforms import Affine2D, Bbox, BboxTransform, TransformedBbox from cm import ScalarMappable from contour import ContourSet import warnings Modified: branches/transforms/lib/matplotlib/finance.py =================================================================== --- branches/transforms/lib/matplotlib/finance.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/finance.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -22,7 +22,7 @@ from matplotlib.colors import colorConverter from lines import Line2D, TICKLEFT, TICKRIGHT from patches import Rectangle -from matplotlib.affine import Affine2D +from matplotlib.transforms import Affine2D Modified: branches/transforms/lib/matplotlib/legend.py =================================================================== --- branches/transforms/lib/matplotlib/legend.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/legend.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -34,7 +34,7 @@ from patches import Patch, Rectangle, RegularPolygon, Shadow, bbox_artist, draw_bbox from collections import LineCollection, RegularPolyCollection, PatchCollection from text import Text -from affine import Bbox, BboxTransform +from transforms import Bbox, BboxTransform def line_cuts_bbox(line, bbox): """ Return True if and only if line cuts bbox. """ Modified: branches/transforms/lib/matplotlib/lines.py =================================================================== --- branches/transforms/lib/matplotlib/lines.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/lines.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -14,10 +14,10 @@ import numerix.ma as ma from matplotlib import verbose import artist -from affine import Bbox from artist import Artist, setp from cbook import iterable, is_string_like, is_numlike from colors import colorConverter +from transforms import Bbox from matplotlib import rcParams Modified: branches/transforms/lib/matplotlib/table.py =================================================================== --- branches/transforms/lib/matplotlib/table.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/table.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -29,7 +29,7 @@ from patches import Rectangle from cbook import enumerate, is_string_like, flatten from text import Text -from affine import Bbox +from transforms import Bbox Modified: branches/transforms/lib/matplotlib/text.py =================================================================== --- branches/transforms/lib/matplotlib/text.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/text.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -15,7 +15,7 @@ from cbook import enumerate, is_string_like, maxdict, is_numlike from font_manager import FontProperties from patches import bbox_artist, YAArrow -from affine import Affine2D, Bbox +from transforms import Affine2D, Bbox from lines import Line2D import matplotlib.nxutils as nxutils Modified: branches/transforms/lib/matplotlib/ticker.py =================================================================== --- branches/transforms/lib/matplotlib/ticker.py 2007-09-14 12:24:20 UTC (rev 3850) +++ branches/transforms/lib/matplotlib/ticker.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -109,7 +109,7 @@ import matplotlib as mpl from matplotlib import verbose, rcParams from matplotlib import cbook -from matplotlib import affine as maffine +from matplotlib import transforms as mtransforms @@ -540,7 +540,7 @@ def autoscale(self): 'autoscale the view limits' self.verify_intervals() - return maffine.nonsingular(*self.dataInterval.get_bounds()) + return mtransforms.nonsingular(*self.dataInterval.get_bounds()) def pan(self, numsteps): 'Pan numticks (can be positive or negative)' @@ -682,7 +682,7 @@ vmin = math.floor(scale*vmin)/scale vmax = math.ceil(scale*vmax)/scale - return maffine.nonsingular(vmin, vmax) + return mtransforms.nonsingular(vmin, vmax) def closeto(x,y): @@ -766,7 +766,7 @@ vmin -=1 vmax +=1 - return maffine.nonsingular(vmin, vmax) + return mtransforms.nonsingular(vmin, vmax) def scale_range(vmin, vmax, n = 1, threshold=100): dv = abs(vmax - vmin) @@ -833,12 +833,12 @@ def __call__(self): vmin, vmax = self.axis.get_view_interval() - vmin, vmax = maffine.nonsingular(vmin, vmax, expander = 0.05) + vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander = 0.05) return self.bin_boundaries(vmin, vmax) def autoscale(self): dmin, dmax = self.axis.get_data_interval() - dmin, dmax = maffine.nonsingular(dmin, dmax, expander = 0.05) + dmin, dmax = mtransforms.nonsingular(dmin, dmax, expander = 0.05) return npy.take(self.bin_boundaries(dmin, dmax), [0,-1]) @@ -939,7 +939,7 @@ if vmin==vmax: vmin = decade_down(vmin,self._base) vmax = decade_up(vmax,self._base) - return maffine.nonsingular(vmin, vmax) + return mtransforms.nonsingular(vmin, vmax) class AutoLocator(MaxNLocator): def __init__(self): Copied: branches/transforms/lib/matplotlib/transforms.py (from rev 3849, branches/transforms/lib/matplotlib/affine.py) =================================================================== --- branches/transforms/lib/matplotlib/transforms.py (rev 0) +++ branches/transforms/lib/matplotlib/transforms.py 2007-09-14 13:03:31 UTC (rev 3851) @@ -0,0 +1,852 @@ +""" +A set of classes to handle transformations. + +2007 Michael Droettboom +""" + +import numpy as npy +from numpy.linalg import inv +from sets import Set + +# MGDTODO: This creates a ton of cyclical references. We may want to +# consider using weak references + +# MGDTODO: deep copying is probably incorrect wrt the parent/child +# relationships + +class TransformNode(object): + def __init__(self): + self._parents = Set() + + def invalidate(self): + if not self._do_invalidation(): + for parent in self._parents: + parent.invalidate() + + def _do_invalidation(self): + return False + + def set_children(self, children): + for child in children: + getattr(self, child)._parents.add(self) + self._children = children + + # MGDTODO: decide whether we need this in-place updating and + # remove if not +# def replace_child(self, index, child): +# children = self._children +# getattr(self, children[index])._parents.remove(self) +# setattr(self, children[index], child) +# # We have to reset children in case two or more +# # of the children are the same +# for child in children: +# getattr(self, child)._parents.add(self) +# self.invalidate() + +class BboxBase(TransformNode): + ''' + This is the read-only part of a bounding-box + ''' + + def __init__(self): + TransformNode.__init__(self) + + def __array__(self): + return self.get_points() + + # MGDTODO: Probably a more efficient ways to do this... + def _get_xmin(self): + return self.get_points()[0, 0] + xmin = property(_get_xmin) + + def _get_ymin(self): + return self.get_points()[0, 1] + ymin = property(_get_ymin) + + def _get_xmax(self): + return self.get_points()[1, 0] + xmax = property(_get_xmax) + + def _get_ymax(self): + return self.get_points()[1, 1] + ymax = property(_get_ymax) + + def _get_min(self): + return self.get_points()[0] + min = property(_get_min) + + def _get_max(self): + return self.get_points()[1] + max = property(_get_max) + + def _get_intervalx(self): + return self.get_points()[:, 0] + intervalx = property(_get_intervalx) + + def _get_intervaly(self): + return self.get_points()[:, 1] + intervaly = property(_get_intervaly) + + def _get_width(self): + return self.xmax - self.xmin + width = property(_get_width) + + def _get_height(self): + return self.ymax - self.ymin + height = property(_get_height) + + def _get_bounds(self): + return (self.xmin, self.ymin, + self.xmax - self.xmin, self.ymax - self.ymin) + bounds = property(_get_bounds) + + def get_points(self): + return NotImplementedError() + + # MGDTODO: Optimize + def containsx(self, x): + return x >= self.xmin and x <= self.xmax + + def containsy(self, y): + return y >= self.ymin and y <= self.ymax + + def contains(self, x, y): + return self.containsx(x) and self.containsy(y) + + def overlapsx(self, other): + return self.containsx(other.xmin) \ + or self.containsx(other.xmax) + + def overlapsy(self, other): + return self.containsy(other.ymin) \ + or self.containsx(other.ymax) + + def overlaps(self, other): + return self.overlapsx(other) \ + and self.overlapsy(other) + + def fully_containsx(self, x): + return x > self.xmin and x < self.xmax + + def fully_containsy(self, y): + return y > self.ymin and y < self.ymax + + def fully_contains(self, x, y): + return self.fully_containsx(x) \ + and self.fully_containsy(y) + + def fully_overlapsx(self, other): + return self.fully_containsx(other.xmin) \ + or self.fully_containsx(other.xmax) + + def fully_overlapsy(self, other): + return self.fully_containsy(other.ymin) \ + or self.fully_containsx(other.ymax) + + def fully_overlaps(self, other): + return self.fully_overlapsx(other) and \ + self.fully_overlapsy(other) + + +class Bbox(BboxBase): + def __init__(self, points): + BboxBase.__init__(self) + self._points = npy.asarray(points, npy.float_) + + #@staticmethod + def unit(): + return Bbox.from_lbrt(0., 0., 1., 1.) + unit = staticmethod(unit) + + #@staticmethod + def from_lbwh(left, bottom, width, height): + return Bbox.from_lbrt(left, bottom, left + width, bottom + height) + from_lbwh = staticmethod(from_lbwh) + + #@staticmethod + def from_lbrt(*args): + points = npy.array(args, dtype=npy.float_).reshape(2, 2) + return Bbox(points) + from_lbrt = staticmethod(from_lbrt) + + def __copy__(self): + return Bbox(self._points.copy()) + + def __deepcopy__(self, memo): + return Bbox(self._points.copy()) + + def __cmp__(self, other): + # MGDTODO: Totally suboptimal + if isinstance(other, Bbox) and (self._points == other._points).all(): + return 0 + return -1 + + def __repr__(self): + return 'Bbox(%s)' % repr(self._points) + __str__ = __repr__ + + # JDH: the update method will update the box limits from the + # existing limits and the new data; it appears here you are just + # using the new data. We use an "ignore" flag to specify whether + # you want to include the existing data or not in the update + def update_from_data(self, x, y, ignore=True): + self._points = npy.array( + [[x.min(), y.min()], [x.max(), y.max()]], + npy.float_) + self.invalidate() + + # MGDTODO: Probably a more efficient ways to do this... + def _set_xmin(self, val): + self._points[0, 0] = val + self.invalidate() + xmin = property(BboxBase._get_xmin, _set_xmin) + + def _set_ymin(self, val): + self._points[0, 1] = val + self.invalidate() + ymin = property(BboxBase._get_ymin, _set_ymin) + + def _set_xmax(self, val): + self._points[1, 0] = val + self.invalidate() + xmax = property(BboxBase._get_xmax, _set_xmax) + + def _set_ymax(self, val): + self._points[1, 1] = val + self.invalidate() + ymax = property(BboxBase._get_ymax, _set_ymax) + + def _set_min(self, val): + self._points[0] = val + self.invalidate() + min = property(BboxBase._get_min, _set_min) + + def _set_max(self, val): + self._points[1] = val + self.invalidate() + max = property(BboxBase._get_max, _set_max) + + def _set_intervalx(self, interval): + self._points[:, 0] = interval + self.invalidate() + intervalx = property(BboxBase._get_intervalx, _set_intervalx) + + def _set_intervaly(self, interval): + self._points[:, 1] = interval + self.invalidate() + intervaly = property(BboxBase._get_intervaly, _set_intervaly) + + def _set_bounds(self, bounds): + l,b,w,h = bounds + self._points = npy.array([[l, b], [l+w, b+h]], npy.float_) + self.invalidate() + bounds = property(BboxBase._get_bounds, _set_bounds) + + def get_points(self): + return self._points + + def set_points(self, points): + self._points = points + self.invalidate() + + def set(self, other): + self._points = other.get_points() + self.invalidate() + + def transformed(self, transform): + return Bbox(transform(self._points)) + + def inverse_transformed(self, transform): + return Bbox(transform.inverted()(self._points)) + + def expanded(self, sw, sh): + width = self.width + height = self.height + deltaw = (sw * width - width) / 2.0 + deltah = (sh * height - height) / 2.0 + a = npy.array([[-deltaw, -deltah], [deltaw, deltah]]) + return Bbox(self._points + a) + + #@staticmethod + def union(bboxes): + """ + Return the Bbox that bounds all bboxes + """ + assert(len(bboxes)) + + if len(bboxes) == 1: + return bboxes[0] + + bbox = bboxes[0] + xmin = bbox.xmin + ymin = bbox.ymin + xmax = bbox.xmax + ymax = bbox.ymax + + for bbox in bboxes[1:]: + xmin = min(xmin, bbox.xmin) + ymin = min(ymin, bbox.ymin) + xmax = max(xmax, bbox.xmax) + ymax = max(ymax, bbox.ymax) + + return Bbox.from_lbrt(xmin, ymin, xmax, ymax) + union = staticmethod(union) + +class TransformedBbox(BboxBase): + def __init__(self, bbox, transform): + assert isinstance(bbox, Bbox) + assert isinstance(transform, Transform) + + BboxBase.__init__(self) + self.bbox = bbox + self.transform = transform + self.set_children(['bbox', 'transform']) + self._points = None + + def __repr__(self): + return "TransformedBbox(%s, %s)" % (self.bbox, self.transform) + __str__ = __repr__ + + def _do_invalidation(self): + self._points = None + + def get_points(self): + if self._points is None: + self._points = self.transform(self.bbox.get_points()) + return self._points + +# MGDTODO: This code probably works, but I don't think it's a good idea +# (from a code clarity perspective) +# class BlendedBbox(BboxBase): +# def __init__(self, bbox_x, bbox_y): +# assert isinstance(bbox_x, BboxBase) +# assert isinstance(bbox_y, BboxBase) + +# BboxBase.__init__(self) +# self._x = bbox_x +# self._y = bbox_y +# self.set_children(['_x', '_y']) +# self._points = None + +# def __repr__(self): +# return "TransformedBbox(%s, %s)" % (self.bbox, self.transform) +# __str__ = __repr__ + +# def _do_invalidation(self): +# self._points = None + +# def get_points(self): +# if self._points is None: +# # MGDTODO: Optimize +# if self._x == self._y: +# self._points = self._x.get_points() +# else: +# x_points = self._x.get_points() +# y_points = self._y.get_points() +# self._points = npy.array( +# [[x_points[0,0], y_points[0,1]], +# [x_points[1,0], y_points[1,1]]], +# npy.float_) +# return self._points + +# def _set_intervalx(self, pair): +# # MGDTODO: Optimize +# bbox = Bbox([[pair[0], 0.0], [pair[1], 0.0]]) +# self.replace_child(0, bbox) +# intervalx = property(BboxBase._get_intervalx, _set_intervalx) + +# def _set_intervaly(self, pair): +# # MGDTODO: Optimize +# bbox = Bbox([[0.0, pair[0]], [0.0, pair[1]]]) +# self.replace_child(1, bbox) +# intervaly = property(BboxBase._get_intervaly, _set_intervaly) + +class Transform(TransformNode): + def __init__(self): + TransformNode.__init__(self) + + def __call__(self, points): + raise NotImplementedError() + + def __add__(self, other): + if isinstance(other, Transform): + return composite_transform_factory(self, other) + raise TypeError( + "Can not add Transform to object of type '%s'" % type(other)) + + def __radd__(self, other): + if isinstance(other, Transform): + return composite_transform_factory(other, self) + raise TypeError( + "Can not add Transform to object of type '%s'" % type(other)) + + def transform_point(self, point): + return self.__call__(npy.asarray([point]))[0] + + def has_inverse(self): + raise NotImplementedError() + + def inverted(self): + raise NotImplementedError() + + def is_separable(self): + return False + + def is_affine(self): + return False + +class Affine2DBase(Transform): + input_dims = 2 + output_dims = 2 + + def __init__(self): + Transform.__init__(self) + self._inverted = None + + def _do_invalidation(self): + result = self._inverted is None + self._inverted = None + return result + + #@staticmethod + def _concat(a, b): + return npy.dot(b, a) + _concat = staticmethod(_concat) + + #@staticmethod + def concat(a, b): + return Affine2D(Affine2D._concat(a.get_matrix(), b.get_matrix())) + concat = staticmethod(concat) + + def to_values(self): + mtx = self.get_matrix() + return tuple(mtx[:2].swapaxes(0, 1).flatten()) + + #@staticmethod + def matrix_from_values(a, b, c, d, e, f): + affine = npy.zeros((3, 3), npy.float_) + affine[0, ] = a, c, e + affine[1, ] = b, d, f + affine[2, 2] = 1 + return affine + matrix_from_values = staticmethod(matrix_from_values) + + def get_matrix(self): + raise NotImplementedError() + + def __call__(self, points): + """ + Applies the transformation to an array of 2D points and + returns the result. + + points must be a numpy array of shape (N, 2), where N is the + number of points. + """ + # MGDTODO: The major speed trap here is just converting to + # the points to an array in the first place. If we can use + # more arrays upstream, that should help here. + if not isinstance(points, npy.ndarray): + import traceback + print '-' * 60 + print 'A non-numpy array was passed in for transformation. Please ' + print 'correct this.' + print "".join(traceback.format_stack()) + print points + mtx = self.get_matrix()... [truncated message content] |