diff --git a/chaco/datamapper.py b/chaco/datamapper.py deleted file mode 100644 index 89bc42918..000000000 --- a/chaco/datamapper.py +++ /dev/null @@ -1,301 +0,0 @@ -""" -CAUTION: This is an old file from Chaco 1.x to support the spatial subdivision -structures. It will be refactored soon. - -If you are looking for Chaco's mappers (subclasses of AbstractMapper), -look in abstract_mapper.py, linear_mapper.py, and log_mapper.py. - -Defines AbstractDataMapper and BruteForceDataMapper classes, and related trait -and functions. -""" - - -from numpy import array, concatenate, take, argsort, argmin, \ - argmax, transpose, newaxis, sort - -from traits.api import HasStrictTraits, Bool, Enum, Tuple, \ - Property, Any, Float - - -#------------------------------------------------------------------- -# Module-specific traits -#------------------------------------------------------------------- - -# Expresses sorting order of -ArraySortTrait = Enum('ascending', 'descending') - - -#------------------------------------------------------------------- -# Module-specific utility functions -#------------------------------------------------------------------- - -def right_shift(ary, newval): - "Returns a right-shifted version of *ary* with *newval* inserted on the left." - return concatenate([[newval], ary[:-1]]) - -def left_shift(ary, newval): - "Returns a left-shifted version of *ary* with *newval* inserted on the right." - return concatenate([ary[1:], [newval]]) - -def sort_points(points, index=0): - """ - sort_points(array_of_points, index=<0|1>) -> sorted_array - - Takes a list of points as an Nx2 array and sorts them according - to their x-coordinate (index=0) or y-coordinate (index=1). - """ - if len(points.shape) != 2 or (2 not in points.shape): - raise RuntimeError("sort_points(): Array of wrong shape.") - return take( points, argsort(points[:,index]) ) - -def array_zip(*arys): - """ - Returns a Numeric array that is the concatenation of the input 1-D - *arys* along a new axis. This function is basically equivalent to - ``array(zip(*arys))``, but is more resource-efficient. - """ - return transpose(array(arys)) - - -class AbstractDataMapper(HasStrictTraits): - """ - A data mapper maps from coordinate space to data elements. In its most - basic form, it loops over all the available data points to find the ones - near a given coordinate or within an area. More advanced functionality - includes returning rect-aligned "affected regions" enclosing all the - returned points, etc. - """ - - # How to sort the output list of intersected points that the - # get_points_near_*() function returns. The points are always sorted - # by their domain (first/X-value) coordinate. - sort_order = ArraySortTrait - - # A read-only property that describes the origin and size of the data - # set in data space as a 4-tuple (min_x, min_y, width, height) - extents = Property() - - - #------------------------------------------------------------------- - # Private traits - #------------------------------------------------------------------- - - _data = Any - - # Internally we expect Nx2 arrays; if the user hands in something - # different, we stored a transposed version but always remember to - # transpose once again whenever we return data. - _is_transposed = Bool(False) - - # the max and min points in data space expressed as a 4-tuple (x,y,w,h) - _extents = Tuple - - # a "fudge factor" to make the extents slightly larger than the actual - # values in the data set - _extents_delta = Float(0.1) - - def __init__(self, data=None, data_sorting='none', **kw): - "See set_data() for description." - self._data = array([]) - HasStrictTraits.__init__(self, **kw) - if data is not None: - self.set_data(data, data_sorting) - return - - def get_points_near(self, pointlist, radius=0.0): - """ - get_points_near([points], radius=0.0) -> Nx2 array of candidate points - - Returns a list of points near the input points (Nx2 array). - - For each point in the input set, *radius* is used to create a - conceptual circle; if any points in the DataMapper's values lie inside - this circle, they are returned. - - The returned list is not guaranteed to be a minimum or exact set, - but it is guaranteed to contain all points that intersect the - *pointlist*. The caller still must do fine-grained testing to see - if the points in the returned point list are a match. - """ - raise NotImplementedError - - def get_points_near_polyline(self, line): - """ - get_points_near_polyline([v1, ... vN]) -> [ [points], [points], ... ] - - This method is like get_points_near(), except that it takes a polyline - as input. A polyline is a list of vertices, each connected to the next - by a straight line. The polyline has infinitely thin width. - - The input array can have shape 2xN or Nx2. - """ - raise NotImplementedError - - def get_points_in_rect(self, rect): - """ - get_points_in_rect( (x,y,w,h) ) -> [ [points], [points], ... ] - - This method is like get_points_near(), except that it takes a rectangle - as input. The rectangle has infinitely thin width. - """ - raise NotImplementedError - - def get_points_in_poly(self, poly): - """ - get_points_in_poly([v1, ... vN]) -> [ [points], [points], ... ] - - This method is like get_points_near(), except that it takes a polygon - as input. The polygon has infinitely thin width and can be - self-intersecting and concave. - - The input array can have shape 2xN or Nx2. - """ - raise NotImplementedError - - def get_last_region(self): - """ - Returns a region of screen space that contains all of the - points/lines/rect/polys in the last get_points_in_*() call. The - region returned by this method is guaranteed to only contain the points - that were returned by the previous call. - - The region is returned as a list of (possibly disjoint) rectangles, - where each rectangle is a 4-tuple (x,y,w,h). - """ - raise NotImplementedError - - def set_data(self, new_data, new_data_sorting='none'): - """ - set_data(new_data, new_data_sorting='none') - - Sets the data used by this DataMapper. The *new_data_sorting* parameter - indicates how the new data is sorted: 'none', 'ascending', or 'descending'. - The default is 'none', which causes the data mapper to perform - a full sort of the input data. - - The input data can be shaped 2xN or Nx2. - """ - if len(new_data) == 0: - self.clear() - return - - if new_data.shape[0] == 2: - self._is_transposed = True - self._data = transpose(new_data) - else: - self._is_transposed = False - self._data = new_data - - if new_data_sorting == 'none': - if self.sort_order == 'ascending': - self._data = sort_points(self._data) - else: - self._data = sort_points(self._data)[::-1] - elif new_data_sorting != self.sort_order: - self._data = self._data[::-1] - - self._calc_data_extents() - self._update_datamap() - # a re-sorting is unnecessary because any internal data structures - # will have been updated by the _data update process. - return - - def clear(self): - """ - clear() - - Resets internal state and any cached data to reflect an empty - data set/data space. - """ - self._data = None - self._extents = (0,0,0,0) - self._clear() - return - - def get_data(self): - "Returns the actual data used by the DataMapper." - if self._is_transposed: - return transpose(self._data) - else: - return self._data - - #------------------------------------------------------------------- - # Concrete private methods and event handlers - # Child classes shouldn't have to override these. - #------------------------------------------------------------------- - - def _get_extents(self): - return self._extents - - def _calc_data_extents(self): - """ - Computes ((minX, minY), (width, height)) of self._data; sets self._extent and - returns nothing. - """ - if len(self._data) == 0: - self._extents = ((0,0), (0,0)) - else: - value = self._data - min_indices = argmin(value, axis=0) - max_indices = argmax(value, axis=0) - x = value[min_indices[0], 0] - self._extents_delta - y = value[min_indices[1], 1] - self._extents_delta - maxX = value[max_indices[0], 0] + self._extents_delta - maxY = value[max_indices[1], 1] + self._extents_delta - self._extents = ((x, y), (maxX-x, maxY-y)) - return - - - #------------------------------------------------------------------- - # Abstract private methods and event handlers - #------------------------------------------------------------------- - - def _update_datamap(self): - """ - This function gets called after self._data has changed. Child classes - should implement this function if they need to recompute any cached - data structures, etc. - """ - return - - def _clear(self): - "Performs subclass-specific clearing/cleanup." - return - - def _sort_order_changed(self, old, new): - return - - -class BruteForceDataMapper(AbstractDataMapper): - """ - The BruteForceDataMapper returns all the points, all the time. - This is basically the same behavior as not having a data mapper in - the pipeline at all. - """ - - def get_points_near(self, pointlist, radius=0): - return self.get_data() - - def get_points_near_polyline(self, line): - return self.get_data() - - def get_points_in_rect(self, rect): - return self.get_data() - - def get_points_in_poly(self, poly): - return self.get_data() - - def get_last_region(self): - return self._extents - - def _sort_order_changed(self, old, new): - if len(self._data) == 0: - return - else: - if self.sort_order == 'ascending': - self._data = sort_points(self._data) - else: - self._data = sort_points(self._data)[::-1] - return - -#EOF diff --git a/chaco/subdivision_cells.py b/chaco/subdivision_cells.py deleted file mode 100644 index 164e6e3d9..000000000 --- a/chaco/subdivision_cells.py +++ /dev/null @@ -1,245 +0,0 @@ -""" Defines cell-related classes and functions. -""" -import itertools - -from numpy import take, array, concatenate, nonzero - -from traits.api import HasStrictTraits, Instance, Delegate, Array, List, \ - Tuple, Property, Trait, Any, Disallow - -from .datamapper import AbstractDataMapper, right_shift, left_shift, sort_points - - -def find_runs(int_array, order='ascending'): - """ - find_runs(int_array, order=<'ascending'|'flat'|'descending'>) -> list_of_int_arrays - - Given an integer array sorted in ascending, descending, or flat order, this - function returns a list of continuous runs of integers inside the list. - For example:: - - find_runs([1,2,3,6,7,8,9,10,11,15]) - - returns [ [1,2,3], [6,7,8,9,10,11], [15] ] - and:: - - find_runs([0,0,0,1,1,1,1,0,0,0,0]) - - returns [ [0,0,0], [1,1,1,1], [0,0,0,0] ] - """ - ranges = arg_find_runs(int_array, order) - if ranges: - return [int_array[i:j] for (i,j) in ranges] - else: - return [] - -def arg_find_runs(int_array, order='ascending'): - """ - This function is like find_runs(), but it returns a list of tuples - indicating the start and end indices of runs in the input *int_array*. - """ - if len(int_array) == 0: - return [] - assert len(int_array.shape)==1, "find_runs() requires a 1D integer array." - if order == 'ascending': - increment = 1 - elif order == 'descending': - increment = -1 - else: - increment = 0 - rshifted = right_shift(int_array, int_array[0]-increment) - start_indices = concatenate([[0], nonzero(int_array - (rshifted+increment))[0]]) - end_indices = left_shift(start_indices, len(int_array)) - return list(zip(start_indices, end_indices)) - - -class AbstractCell(HasStrictTraits): - """ Abstract class for grid cells in a uniform subdivision. - - Individual subclasses store points in different, possibly optimized - fashion, and performance may be drastically different between different - cell subclasses for a given set of data. - """ - # The parent of this cell. - parent = Instance(AbstractDataMapper) - - # The sort traits characterizes the internal points list. - _sort_order = Delegate('parent') - - # The point array for this cell. This attribute delegates to parent._data, - # which references the actual point array. For the sake of simplicity, - # cells assume that _data is sorted in fashion indicated by **_sort_order**. - # If this doesn't hold, then each cell needs to have its own duplicate - # copy of the sorted data. - data = Delegate('parent', '_data') - - # A list of indices into **data** that reflect the points inside this cell. - indices = Property - - # Shadow trait for **indices**. - _indices = Any - - def add_indices(self, indices): - """ Adds a list of integer indices to the existing list of indices. - """ - raise NotImplementedError - - def get_points(self): - """ Returns a list of points that was previously set. - - This operation might be large and expensive; in general, use - _get_indices() instead. - """ - raise NotImplementedError - - def reverse_indices(self): - """ Tells the cell to manipulate its indices so that they index to the - same values in a reversed data array. - - Generally this method handles the situation when the parent's _data - array has been flipped due to a sort order change. - - The length of _data must not have changed; otherwise there is no way to - know the proper way to manipulate indices. - """ - raise NotImplementedError - - def _set_indices(self, indices): - raise NotImplementedError - - def _get_indices(self): - """ Returns the list of indices into _data that reflect the points - inside this cell. - """ - raise NotImplementedError - - -class Cell(AbstractCell): - """ - A basic cell that stores its point indices as an array of integers. - """ - # A list of indices into **data** that reflect the points inside this cell - # (overrides AbstractCell). - indices = Property(Array) - - # A 1-D array of indices into _data. - _indices = Array - - def __init__(self, **kw): - self._indices = array([]) - super(AbstractCell, self).__init__(**kw) - - def add_indices(self, indices): - """ Adds a list of integer indices to the existing list of indices. - - Implements AbstractCell. - """ - self._indices = concatenate([self._indices, indices]) - return - - def get_points(self): - """ Returns a list of points that was previously set. - - Implements AbstractCell. - """ - return take(self.data, self._indices) - - def reverse_indices(self): - """ Tells the cell to manipulate its indices so that they index to the - same values in a reversed data array. - - Implements AbstractCell. - """ - length = len(self.data) - self._indices = [length-i-1 for i in self._indices] - return - - def _set_indices(self, indices): - self._indices = indices - return - - def _get_indices(self): - return self._indices - - - -class RangedCell(AbstractCell): - """ A cell optimized for storing lists of continuous points. - - Rather than storing each individual point index as an element in an array, - RangedCell stores a list of index ranges; each range is a (start,end) tuple). - """ - - # A list of indices into **data** that reflect the points inside this cell - # (overrides AbstractCell). - indices = Property - - # Don't use the _indices shadow trait; rather, the getters and setters - # for 'index' procedurally generate indices from **ranges**. - _indices = Disallow - - # Ranges are an additional interface on RangedCells. - ranges = Property(List(Tuple)) - - # Shadow trait for ranges. - _ranges = List(Tuple) - - #--------------------------------------------------------------------- - # AbstractCell methods - #--------------------------------------------------------------------- - - def add_indices(self, indices): - """ Adds a list of integer indices to the existing list of indices. - - Implements AbstractCell. - """ - self.add_ranges(find_runs(indices)) - return - - def get_points(self): - """ Returns a list of points that was previously set. - - Implements AbstractCell. - """ - return take(self.data, self.indices) - - def reverse_indices(self): - """ Tells the cell to manipulate its indices so that they index to the - same values in a reversed data array. - - Implements AbstractCell. - """ - length = len(self.data) - self._ranges = [(length-end-1, length-start-1) for (start,end) in self._ranges] - return - - def _set_indices(self, indices): - self._ranges = find_runs(indices) - return - - def _get_indices(self): - list_of_indices = [range(i, j) for (i, j) in self._ranges] - return list(itertools.chain(*list_of_indices)) - - - #--------------------------------------------------------------------- - # additional RangedCell methods - #--------------------------------------------------------------------- - - def get_ranges(self): - """ Returns a list of tuples representing the (start,end) indices of - continuous ranges of points in self._data. - """ - return self._ranges() - - def add_ranges(self, ranges): - """ Adds a list of ranges ((start,end) tuples) to the current list. - - This method doesn't check for duplicate or overlapping ranges. - """ - if self._ranges: - self._ranges.extend(ranges) - else: - self._ranges = ranges - return -#EOF diff --git a/chaco/subdivision_mapper.py b/chaco/subdivision_mapper.py deleted file mode 100644 index 6b6fdfa5f..000000000 --- a/chaco/subdivision_mapper.py +++ /dev/null @@ -1,217 +0,0 @@ -""" Defines the SubdivisionDataMapper and SubdivisionLineDataMapper classes. -""" -# Major library imports - -import math -from numpy import array, arange, concatenate, searchsorted, nonzero, transpose, \ - argsort, zeros, sort, vstack -import numpy - -# Enthought library imports -from traits.api import List, Array, Tuple, Int, Float - -# Local, relative imports -from .datamapper import AbstractDataMapper, right_shift, left_shift, \ - sort_points, ArraySortTrait, \ - array_zip -from .subdivision_cells import AbstractCell, Cell, RangedCell, find_runs, \ - arg_find_runs - - -class SubdivisionDataMapper(AbstractDataMapper): - """ - A data mapper that uses a uniform grid of rectangular cells. It doesn't make - any assumptions about the continuity of the input data set, and explicitly - stores each point in the data set in its cell. - - If the incoming data is ordered in some fashion such that most cells end - up with large ranges of data, then it's better to use the - SubdivisionLineDataMapper subclass. - """ - celltype = Cell - _last_region = List(Tuple) - _cellgrid = Array # a Numeric array of Cell objects - _points_per_cell = Int(100) # number of datapoints/cell to shoot for - _cell_lefts = Array # locations of left edge for all cells - _cell_bottoms = Array # locations of bottom edge for all cells - _cell_extents = Tuple(Float, Float) # the width and height of a cell - - #------------------------------------------------------------------- - # Public AbstractDataMapper methods - #------------------------------------------------------------------- - - def get_points_near(self, pointlist, radius=0.0): - if radius != 0: - # tmp is a list of list of arrays - d = 2*radius - cell_points = [ self.get_points_in_rect((px-radius,py-radius,d,d)) - for (px,py) in pointlist ] - else: - indices = self._get_indices_for_points(pointlist) - cells = [self._cellgrid[i,j] for (i,j) in indices] - self._last_region = self._cells_to_rects(indices) - # unique-ify the list of cells - cell_points = [c.get_points() for c in set(cells)] - return vstack(cell_points) - - - def get_points_in_rect(self, rect): - x_span = (rect[0], rect[0]+rect[2]) - y_span = (rect[1], rect[1]+rect[3]) - min_i, max_i = searchsorted(self._cell_lefts, x_span) - 1 - min_j, max_j = searchsorted(self._cell_bottoms, y_span) - 1 - cellpts = [ self._cellgrid[i,j].get_points() - for i in range(min_i, max_i+1) \ - for j in range(min_j, max_j+1) ] - self._last_region = ( self._cell_lefts[min_i], self._cell_bottoms[min_j], \ - (max_i - min_i + 1) * self._cell_extents[0], \ - (max_j - min_j + 1) * self._cell_extents[1] ) - return vstack(cellpts) - - - def get_last_region(self): - return self._last_region - - #------------------------------------------------------------------- - # AbstractDataMapper's abstract private methods - #------------------------------------------------------------------- - - def _update_datamap(self): - self._last_region = [] - # Create a new grid of the appropriate size, initialize it with new - # Cell instance (of type self.celltype), and perform point insertion - # on the new data. - if self._data is None: - self._cellgrid = array([], dtype=object) - self._cell_lefts = array([]) - self._cell_bottoms = array([]) - else: - num_x_cells, num_y_cells = self._calc_grid_dimensions() - self._cellgrid = zeros((num_x_cells, num_y_cells), dtype=object) - for i in range(num_x_cells): - for j in range(num_y_cells): - self._cellgrid[i,j] = self.celltype(parent=self) - ll, ur = self._extents - cell_width = ur[0]/num_x_cells - cell_height = ur[1]/num_y_cells - - # calculate the left and bottom edges of all the cells and store - # them in two arrays - self._cell_lefts = arange(ll[0], ll[0]+ur[0]-cell_width/2, step=cell_width) - self._cell_bottoms = arange(ll[1], ll[1]+ur[1]-cell_height/2, step=cell_height) - - self._cell_extents = (cell_width, cell_height) - - # insert the data points - self._basic_insertion(self.celltype) - return - - def _clear(self): - self._last_region = [] - self._cellgrid = [] - self._cell_lefts = [] - self._cell_bottoms = [] - self._cell_extents = (0,0) - return - - def _sort_order_changed(self, old, new): - # since trait event notification only happens if the value has changed, - # and there are only two types of sorting, it's safe to just reverse our - # internal _data object - self._data = self._data[::-1] - for cell in self._cellgrid: - # since cellgrid is a Numeric array, iterating over it produces - # a length-1 array - cell[0].reverse_indices() - return - - - #------------------------------------------------------------------- - # helper private methods - #------------------------------------------------------------------- - - def _calc_grid_dimensions(self): - numpoints = self._data.shape[0] - numcells = numpoints / self._points_per_cell - ll, ur = self._extents - aspect_ratio = (ur[0]-ll[0]) / (ur[1]-ll[1]) - num_y_cells = int(math.sqrt(numcells / aspect_ratio)) - num_x_cells = int(aspect_ratio * num_y_cells) - if num_y_cells == 0: - num_y_cells += 1 - if num_y_cells*num_x_cells*self._points_per_cell < numpoints: - num_x_cells += 1 - return (num_x_cells, num_y_cells) - - def _basic_insertion(self, celltype): - # generate a list of which cell each point in self._data belongs in - cell_indices = self._get_indices_for_points(self._data) - - # We now look for ranges of points belonging to the same cell. - # 1. shift lengthwise and difference; runs of cells with the same - # (i,j) indices will be zero, and nonzero value for i or j will - # indicate a transition to a new cell. (Just like find_runs().) - differences = cell_indices[1:] - cell_indices[:-1] - - # Since nonzero() only works for 1D arrays, we merge the X and Y columns - # together to detect any point where either X or Y are nonzero. We have - # to add 1 because we shifted cell_indices before differencing (above). - diff_indices = nonzero(differences[:,0] + differences[:,1])[0] + 1 - - start_indices = concatenate([[0], diff_indices]) - end_indices = concatenate([diff_indices, [len(self._data)]]) - - for start,end in zip(start_indices, end_indices): - gridx, gridy = cell_indices[start] # can use 'end' here just as well - if celltype == RangedCell: - self._cellgrid[gridx,gridy].add_ranges([(start,end)]) - else: - self._cellgrid[gridx,gridy].add_indices(list(range(start,end))) - return - - def _get_indices_for_points(self, pointlist): - """ - Given an input Nx2 array of points, returns a list Nx2 corresponding - to the column and row indices into the cell grid. - """ - x_array = searchsorted(self._cell_lefts, pointlist[:,0]) - 1 - y_array = searchsorted(self._cell_bottoms, pointlist[:,1]) - 1 - return array_zip(x_array, y_array) - - - def _cells_to_rects(self, cells): - """ - Converts the extents of a list of cell grid coordinates (i,j) into - a list of rect tuples (x,y,w,h). The set should be disjoint, but may - or may not be minimal. - """ - # Since this function is generally used to generate clipping regions - # or other screen-related graphics, we should try to return large - # rectangular blocks if possible. - # For now, we just look for horizontal runs and return those. - cells = array(cells) - y_sorted = sort_points(cells, index=1) # sort acoording to row - rownums = sort(array(tuple(set(cells[:,1])))) - - row_start_indices = searchsorted(y_sorted[:,1], rownums) - row_end_indices = left_shift(row_start_indices, len(cells)) - - rects = [] - for rownum, start, end in zip(rownums, row_start_indices, row_end_indices): - # y_sorted is sorted by the J (row) coordinate, so after we - # extract the column indices, we need to sort them before - # passing them to find_runs(). - grid_column_indices = sort(y_sorted[start:end][:,0]) - for span in find_runs(grid_column_indices): - x = self._cell_lefts[span[0]] - y = self._cell_bottoms[rownum] - w = (span[-1] - span[0] + 1) * self._cell_extents[0] - h = self._cell_extents[1] - rects.append((x,y,w,h)) - return rects - - -class SubdivisionLineDataMapper(SubdivisionDataMapper): - """ A subdivision data mapper that uses ranged cells. - """ - celltype = RangedCell