2
CAUTION: This is an old file from Chaco 1.x to support the spatial subdivision
3
structures. It will be refactored soon.
5
If you are looking for Chaco's mappers (subclasses of AbstractMapper),
6
look in abstract_mapper.py, linear_mapper.py, and log_mapper.py.
8
Defines AbstractDataMapper and BruteForceDataMapper classes, and related trait
15
from numpy import array, concatenate, take, argsort, argmin, \
16
argmax, transpose, newaxis, sort
18
from enthought.traits.api import HasStrictTraits, Bool, Enum, Tuple, \
22
#-------------------------------------------------------------------
23
# Module-specific traits
24
#-------------------------------------------------------------------
26
# Expresses sorting order of
27
ArraySortTrait = Enum('ascending', 'descending')
30
#-------------------------------------------------------------------
31
# Module-specific utility functions
32
#-------------------------------------------------------------------
34
def right_shift(ary, newval):
35
"Returns a right-shifted version of *ary* with *newval* inserted on the left."
36
return concatenate([[newval], ary[:-1]])
38
def left_shift(ary, newval):
39
"Returns a left-shifted version of *ary* with *newval* inserted on the right."
40
return concatenate([ary[1:], [newval]])
42
def sort_points(points, index=0):
44
sort_points(array_of_points, index=<0|1>) -> sorted_array
46
Takes a list of points as an Nx2 array and sorts them according
47
to their x-coordinate (index=0) or y-coordinate (index=1).
49
if len(points.shape) != 2 or (2 not in points.shape):
50
raise RuntimeError, "sort_points(): Array of wrong shape."
51
return take( points, argsort(points[:,index]) )
55
Returns a Numeric array that is the concatenation of the input 1-D
56
*arys* along a new axis. This function is basically equivalent to
57
``array(zip(*arys))``, but is more resource-efficient.
59
return transpose(array(arys))
62
class AbstractDataMapper(HasStrictTraits):
64
A data mapper maps from coordinate space to data elements. In its most
65
basic form, it loops over all the available data points to find the ones
66
near a given coordinate or within an area. More advanced functionality
67
includes returning rect-aligned "affected regions" enclosing all the
71
# How to sort the output list of intersected points that the
72
# get_points_near_*() function returns. The points are always sorted
73
# by their domain (first/X-value) coordinate.
74
sort_order = ArraySortTrait
76
# A read-only property that describes the origin and size of the data
77
# set in data space as a 4-tuple (min_x, min_y, width, height)
81
#-------------------------------------------------------------------
83
#-------------------------------------------------------------------
87
# Internally we expect Nx2 arrays; if the user hands in something
88
# different, we stored a transposed version but always remember to
89
# transpose once again whenever we return data.
90
_is_transposed = Bool(False)
92
# the max and min points in data space expressed as a 4-tuple (x,y,w,h)
95
# a "fudge factor" to make the extents slightly larger than the actual
96
# values in the data set
97
_extents_delta = Float(0.1)
99
def __init__(self, data=None, data_sorting='none', **kw):
100
"See set_data() for description."
101
self._data = array([])
102
HasStrictTraits.__init__(self, **kw)
104
self.set_data(data, data_sorting)
107
def get_points_near(self, pointlist, radius=0.0):
109
get_points_near([points], radius=0.0) -> Nx2 array of candidate points
111
Returns a list of points near the input points (Nx2 array).
113
For each point in the input set, *radius* is used to create a
114
conceptual circle; if any points in the DataMapper's values lie inside
115
this circle, they are returned.
117
The returned list is not guaranteed to be a minimum or exact set,
118
but it is guaranteed to contain all points that intersect the
119
*pointlist*. The caller still must do fine-grained testing to see
120
if the points in the returned point list are a match.
122
raise NotImplementedError
124
def get_points_near_polyline(self, line):
126
get_points_near_polyline([v1, ... vN]) -> [ [points], [points], ... ]
128
This method is like get_points_near(), except that it takes a polyline
129
as input. A polyline is a list of vertices, each connected to the next
130
by a straight line. The polyline has infinitely thin width.
132
The input array can have shape 2xN or Nx2.
134
raise NotImplementedError
136
def get_points_in_rect(self, rect):
138
get_points_in_rect( (x,y,w,h) ) -> [ [points], [points], ... ]
140
This method is like get_points_near(), except that it takes a rectangle
141
as input. The rectangle has infinitely thin width.
143
raise NotImplementedError
145
def get_points_in_poly(self, poly):
147
get_points_in_poly([v1, ... vN]) -> [ [points], [points], ... ]
149
This method is like get_points_near(), except that it takes a polygon
150
as input. The polygon has infinitely thin width and can be
151
self-intersecting and concave.
153
The input array can have shape 2xN or Nx2.
155
raise NotImplementedError
157
def get_last_region(self):
159
Returns a region of screen space that contains all of the
160
points/lines/rect/polys in the last get_points_in_*() call. The
161
region returned by this method is guaranteed to only contain the points
162
that were returned by the previous call.
164
The region is returned as a list of (possibly disjoint) rectangles,
165
where each rectangle is a 4-tuple (x,y,w,h).
167
raise NotImplementedError
169
def set_data(self, new_data, new_data_sorting='none'):
171
set_data(new_data, new_data_sorting='none')
173
Sets the data used by this DataMapper. The *new_data_sorting* parameter
174
indicates how the new data is sorted: 'none', 'ascending', or 'descending'.
175
The default is 'none', which causes the data mapper to perform
176
a full sort of the input data.
178
The input data can be shaped 2xN or Nx2.
180
if len(new_data) == 0:
184
if new_data.shape[0] == 2:
185
self._is_transposed = True
186
self._data = transpose(new_data)
188
self._is_transposed = False
189
self._data = new_data
191
if new_data_sorting == 'none':
192
if self.sort_order == 'ascending':
193
self._data = sort_points(self._data)
195
self._data = sort_points(self._data)[::-1]
196
elif new_data_sorting != self.sort_order:
197
self._data = self._data[::-1]
199
self._calc_data_extents()
200
self._update_datamap()
201
# a re-sorting is unnecessary because any internal data structures
202
# will have been updated by the _data update process.
209
Resets internal state and any cached data to reflect an empty
213
self._extents = (0,0,0,0)
218
"Returns the actual data used by the DataMapper."
219
if self._is_transposed:
220
return transpose(self._data)
224
#-------------------------------------------------------------------
225
# Concrete private methods and event handlers
226
# Child classes shouldn't have to override these.
227
#-------------------------------------------------------------------
229
def _get_extents(self):
232
def _calc_data_extents(self):
234
Computes ((minX, minY), (width, height)) of self._data; sets self._extent and
237
if len(self._data) == 0:
238
self._extents = ((0,0), (0,0))
241
min_indices = argmin(value, axis=0)
242
max_indices = argmax(value, axis=0)
243
x = value[min_indices[0], 0] - self._extents_delta
244
y = value[min_indices[1], 1] - self._extents_delta
245
maxX = value[max_indices[0], 0] + self._extents_delta
246
maxY = value[max_indices[1], 1] + self._extents_delta
247
self._extents = ((x, y), (maxX-x, maxY-y))
251
#-------------------------------------------------------------------
252
# Abstract private methods and event handlers
253
#-------------------------------------------------------------------
255
def _update_datamap(self):
257
This function gets called after self._data has changed. Child classes
258
should implement this function if they need to recompute any cached
259
data structures, etc.
264
"Performs subclass-specific clearing/cleanup."
267
def _sort_order_changed(self, old, new):
271
class BruteForceDataMapper(AbstractDataMapper):
273
The BruteForceDataMapper returns all the points, all the time.
274
This is basically the same behavior as not having a data mapper in
278
def get_points_near(self, pointlist, radius=0):
279
return self.get_data()
281
def get_points_near_polyline(self, line):
282
return self.get_data()
284
def get_points_in_rect(self, rect):
285
return self.get_data()
287
def get_points_in_poly(self, poly):
288
return self.get_data()
290
def get_last_region(self):
293
def _sort_order_changed(self, old, new):
294
if len(self._data) == 0:
297
if self.sort_order == 'ascending':
298
self._data = sort_points(self._data)
300
self._data = sort_points(self._data)[::-1]