1
""" A collection of Chaco tools that respond to a multi-pointer interface
3
from numpy import asarray, dot, sqrt
5
# Enthought library imports
6
from traits.api import Delegate, Dict, Enum, Instance, Int, Property, Trait, Tuple, CArray
9
from chaco.api import BaseTool
10
from chaco.tools.api import PanTool, DragZoom, LegendTool, RangeSelection
18
class MPPanTool(PanTool):
19
cur_bid = Int(BOGUS_BLOB_ID)
21
def normal_blob_down(self, event):
22
if self.cur_bid == BOGUS_BLOB_ID:
23
self.cur_bid = event.bid
24
self._start_pan(event, capture_mouse=False)
25
event.window.capture_blob(self, event.bid, event.net_transform())
27
def panning_blob_up(self, event):
28
if event.bid == self.cur_bid:
29
self.cur_bid = BOGUS_BLOB_ID
32
def panning_blob_move(self, event):
33
if event.bid == self.cur_bid:
34
self._dispatch_stateful_event(event, "mouse_move")
36
def panning_mouse_leave(self, event):
37
""" Handles the mouse leaving the plot when the tool is in the 'panning'
44
def _end_pan(self, event):
45
if hasattr(event, "bid"):
46
event.window.release_blob(event.bid)
47
PanTool._end_pan(self, event)
50
class MPDragZoom(DragZoom):
54
# The original dataspace points where blobs 1 and 2 went down
55
_orig_low = CArray #Trait(None, None, Tuple)
56
_orig_high = CArray #Trait(None, None, Tuple)
58
# Dataspace center of the zoom action
59
_center_pt = Trait(None, None, Tuple)
61
# Maps blob ID numbers to the (x,y) coordinates that came in.
64
# Maps blob ID numbers to the (x0,y0) coordinates from blob_move events.
67
# Properties to convert the dictionaries to map from blob ID numbers to
68
# a single coordinate appropriate for the axis the range selects on.
69
_axis_blobs = Property(Dict)
70
_axis_moves = Property(Dict)
72
def _convert_to_axis(self, d):
73
""" Convert a mapping of ID to (x,y) tuple to a mapping of ID to just
74
the coordinate appropriate for the selected axis.
76
if self.axis == 'index':
79
idx = 1-self.axis_index
81
for id, coords in d.items():
85
def _get__axis_blobs(self):
86
return self._convert_to_axis(self._blobs)
88
def _get__axis_moves(self):
89
return self._convert_to_axis(self._moves)
91
def drag_start(self, event, capture_mouse=False):
92
bid1, bid2 = sorted(self._moves)
93
xy01, xy02 = self._moves[bid1], self._moves[bid2]
94
self._orig_low, self._orig_high = map(asarray,
95
self._map_coordinate_box(xy01, xy02))
96
self.orig_center = (self._orig_high + self._orig_low) / 2.0
97
self.orig_diag = l2norm(self._orig_high - self._orig_low)
99
#DragZoom.drag_start(self, event, capture_mouse)
100
self._original_xy = xy02
102
self._orig_screen_bounds = ((c.x,c.y), (c.x2,c.y2))
103
self._original_data = (c.x_mapper.map_data(xy02[0]), c.y_mapper.map_data(xy02[1]))
104
self._prev_y = xy02[1]
106
event.window.set_pointer(self.drag_pointer)
108
def normal_blob_down(self, event):
109
if len(self._blobs) < 2:
110
self._blobs[event.bid] = (event.x, event.y)
111
event.window.capture_blob(self, event.bid,
112
transform=event.net_transform())
115
def normal_blob_up(self, event):
116
self._handle_blob_leave(event)
118
def normal_blob_move(self, event):
119
self._handle_blob_move(event)
121
def normal_blob_frame_end(self, event):
122
if len(self._moves) == 2:
123
self.event_state = "dragging"
124
self.drag_start(event, capture_mouse=False)
126
def dragging_blob_move(self, event):
127
self._handle_blob_move(event)
129
def dragging_blob_frame_end(self, event):
130
# Get dataspace coordinates of the previous and new coordinates
131
bid1, bid2 = sorted(self._moves)
132
p1, p2 = self._blobs[bid1], self._blobs[bid2]
133
low, high = map(asarray, self._map_coordinate_box(p1, p2))
135
# Compute the amount of translation
136
center = (high + low) / 2.0
137
translation = center - self.orig_center
139
# Computing the zoom factor. We have the coordinates of the original
140
# blob_down events, and we have a new box as well. For now, just use
141
# the relative sizes of the diagonals.
142
diag = l2norm(high - low)
143
zoom = self.speed * self.orig_diag / diag
145
# The original screen bounds are used to test if we've reached max_zoom
146
orig_screen_low, orig_screen_high = \
147
map(asarray, self._map_coordinate_box(*self._orig_screen_bounds))
148
new_low = center - zoom * (center - orig_screen_low) - translation
149
new_high = center + zoom * (orig_screen_high - center) - translation
152
if self._zoom_limit_reached(orig_screen_low[ndx],
153
orig_screen_high[ndx], new_low[ndx], new_high[ndx]):
157
c.x_mapper.range.set_bounds(new_low[0], new_high[0])
158
c.y_mapper.range.set_bounds(new_low[1], new_high[1])
160
self.component.request_redraw()
162
def dragging_blob_up(self, event):
163
self._handle_blob_leave(event)
165
def _handle_blob_move(self, event):
166
if event.bid not in self._blobs:
168
self._blobs[event.bid] = event.x, event.y
169
self._moves[event.bid] = event.x0, event.y0
172
def _handle_blob_leave(self, event):
173
if event.bid in self._blobs:
174
del self._blobs[event.bid]
175
self._moves.pop(event.bid, None)
176
event.window.release_blob(event.bid)
177
if len(self._blobs) < 2:
178
self.event_state = "normal"
181
class MPPanZoom(BaseTool):
182
""" This tool wraps a pan and a zoom tool, and automatically switches
183
behavior back and forth depending on how many blobs are tracked on
187
pan = Instance(MPPanTool)
189
zoom = Instance(MPDragZoom)
191
event_state = Enum("normal", "pan", "zoom")
193
_blobs = Delegate('zoom')
194
_moves = Delegate('zoom')
196
def _dispatch_stateful_event(self, event, suffix):
197
self.zoom.dispatch(event, suffix)
198
event.handled = False
199
self.pan.dispatch(event, suffix)
200
if len(self._blobs) == 2:
201
self.event_state = 'zoom'
202
elif len(self._blobs) == 1:
203
self.event_state = 'pan'
204
elif len(self._blobs) == 0:
205
self.event_state = 'normal'
207
assert len(self._blobs) <= 2
208
if suffix == 'blob_up':
209
event.window.release_blob(event.bid)
210
elif suffix == 'blob_down':
211
event.window.release_blob(event.bid)
212
event.window.capture_blob(self, event.bid, event.net_transform())
215
def _component_changed(self, old, new):
216
self.pan.component = new
217
self.zoom.component = new
219
def _pan_default(self):
220
return MPPanTool(self.component)
222
def _zoom_default(self):
223
return MPDragZoom(self.component)
226
class MPLegendTool(LegendTool):
228
event_state = Enum("normal", "dragging")
232
def normal_blob_down(self, event):
233
if self.cur_bid == -1 and self.is_draggable(event.x, event.y):
234
self.cur_bid = event.bid
235
self.drag_start(event)
237
def dragging_blob_up(self, event):
238
if event.bid == self.cur_bid:
242
def dragging_blob_move(self, event):
243
if event.bid == self.cur_bid:
246
def drag_start(self, event):
248
self.original_padding = self.component.padding
249
if hasattr(event, "bid"):
250
event.window.capture_blob(self, event.bid,
251
event.net_transform())
253
event.window.set_mouse_owner(self, event.net_transform())
254
self.mouse_down_position = (event.x,event.y)
255
self.event_state = "dragging"
259
def drag_end(self, event):
260
if hasattr(event, "bid"):
261
event.window.release_blob(event.bid)
262
self.event_state = "normal"
263
LegendTool.drag_end(self, event)
267
class MPRangeSelection(RangeSelection):
269
# Maps blob ID numbers to the (x,y) coordinates that came in.
272
# Maps blob ID numbers to the (x0,y0) coordinates from blob_move events.
275
# Properties to convert the dictionaries to map from blob ID numbers to
276
# a single coordinate appropriate for the axis the range selects on.
277
_axis_blobs = Property(Dict)
278
_axis_moves = Property(Dict)
280
def _convert_to_axis(self, d):
281
""" Convert a mapping of ID to (x,y) tuple to a mapping of ID to just
282
the coordinate appropriate for the selected axis.
284
if self.axis == 'index':
285
idx = self.axis_index
287
idx = 1-self.axis_index
289
for id, coords in d.items():
293
def _get__axis_blobs(self):
294
return self._convert_to_axis(self._blobs)
296
def _get__axis_moves(self):
297
return self._convert_to_axis(self._moves)
299
def normal_blob_down(self, event):
300
if len(self._blobs) < 2:
301
self._blobs[event.bid] = (event.x, event.y)
302
event.window.capture_blob(self, event.bid,
303
transform=event.net_transform())
306
def normal_blob_up(self, event):
307
self._handle_blob_leave(event)
309
def normal_blob_frame_end(self, event):
310
if len(self._blobs) == 2:
311
self.event_state = "selecting"
312
#self.drag_start(event, capture_mouse=False)
313
#self.selecting_mouse_move(event)
314
self._set_sizing_cursor(event)
315
self.selection = sorted(self._axis_blobs.values())
317
def selecting_blob_move(self, event):
318
if event.bid in self._blobs:
319
self._blobs[event.bid] = event.x, event.y
320
self._moves[event.bid] = event.x0, event.y0
322
def selecting_blob_up(self, event):
323
self._handle_blob_leave(event)
325
def selecting_blob_frame_end(self, event):
326
if self.selection is None:
328
elif len(self._blobs) == 2:
329
axis_index = self.axis_index
330
low = self.plot.position[axis_index]
331
high = low + self.plot.bounds[axis_index] - 1
332
p1, p2 = self._axis_blobs.values()
333
# XXX: what if p1 or p2 is out of bounds?
334
m1 = self.mapper.map_data(p1)
335
m2 = self.mapper.map_data(p2)
336
low_val = min(m1, m2)
337
high_val = max(m1, m2)
338
self.selection = (low_val, high_val)
339
self.component.request_redraw()
340
elif len(self._moves) == 1:
341
id, p0 = self._axis_moves.items()[0]
342
m0 = self.mapper.map_data(p0)
343
low, high = self.selection
344
if low <= m0 <= high:
345
m1 = self.mapper.map_data(self._axis_blobs[id])
347
self.selection = (low+dm, high+dm)
349
def selected_blob_down(self, event):
350
if len(self._blobs) < 2:
351
self._blobs[event.bid] = (event.x, event.y)
352
event.window.capture_blob(self, event.bid,
353
transform=event.net_transform())
356
def selected_blob_move(self, event):
357
if event.bid in self._blobs:
358
self._blobs[event.bid] = event.x, event.y
359
self._moves[event.bid] = event.x0, event.y0
361
def selected_blob_frame_end(self, event):
362
self.selecting_blob_frame_end(event)
364
def selected_blob_up(self, event):
365
self._handle_blob_leave(event)
367
def _handle_blob_leave(self, event):
368
self._moves.pop(event.bid, None)
369
if event.bid in self._blobs:
370
del self._blobs[event.bid]
371
event.window.release_blob(event.bid)
373
# Treat the blob leave as a selecting_mouse_up event
374
self.selecting_right_up(event)
376
if len(self._blobs) < 2:
377
self.event_state = "selected"