|
1 /* global jQuery */ |
|
2 |
|
3 (function($) { |
|
4 'use strict'; |
|
5 |
|
6 var options = { |
|
7 zoom: { |
|
8 enableTouch: false |
|
9 }, |
|
10 pan: { |
|
11 enableTouch: false, |
|
12 touchMode: 'manual' |
|
13 }, |
|
14 recenter: { |
|
15 enableTouch: true |
|
16 } |
|
17 }; |
|
18 |
|
19 var ZOOM_DISTANCE_MARGIN = $.plot.uiConstants.ZOOM_DISTANCE_MARGIN; |
|
20 |
|
21 function init(plot) { |
|
22 plot.hooks.processOptions.push(initTouchNavigation); |
|
23 } |
|
24 |
|
25 function initTouchNavigation(plot, options) { |
|
26 var gestureState = { |
|
27 zoomEnable: false, |
|
28 prevDistance: null, |
|
29 prevTapTime: 0, |
|
30 prevPanPosition: { x: 0, y: 0 }, |
|
31 prevTapPosition: { x: 0, y: 0 } |
|
32 }, |
|
33 navigationState = { |
|
34 prevTouchedAxis: 'none', |
|
35 currentTouchedAxis: 'none', |
|
36 touchedAxis: null, |
|
37 navigationConstraint: 'unconstrained', |
|
38 initialState: null, |
|
39 }, |
|
40 useManualPan = options.pan.interactive && options.pan.touchMode === 'manual', |
|
41 smartPanLock = options.pan.touchMode === 'smartLock', |
|
42 useSmartPan = options.pan.interactive && (smartPanLock || options.pan.touchMode === 'smart'), |
|
43 pan, pinch, doubleTap; |
|
44 |
|
45 function bindEvents(plot, eventHolder) { |
|
46 var o = plot.getOptions(); |
|
47 |
|
48 if (o.zoom.interactive && o.zoom.enableTouch) { |
|
49 eventHolder[0].addEventListener('pinchstart', pinch.start, false); |
|
50 eventHolder[0].addEventListener('pinchdrag', pinch.drag, false); |
|
51 eventHolder[0].addEventListener('pinchend', pinch.end, false); |
|
52 } |
|
53 |
|
54 if (o.pan.interactive && o.pan.enableTouch) { |
|
55 eventHolder[0].addEventListener('panstart', pan.start, false); |
|
56 eventHolder[0].addEventListener('pandrag', pan.drag, false); |
|
57 eventHolder[0].addEventListener('panend', pan.end, false); |
|
58 } |
|
59 |
|
60 if ((o.recenter.interactive && o.recenter.enableTouch)) { |
|
61 eventHolder[0].addEventListener('doubletap', doubleTap.recenterPlot, false); |
|
62 } |
|
63 } |
|
64 |
|
65 function shutdown(plot, eventHolder) { |
|
66 eventHolder[0].removeEventListener('panstart', pan.start); |
|
67 eventHolder[0].removeEventListener('pandrag', pan.drag); |
|
68 eventHolder[0].removeEventListener('panend', pan.end); |
|
69 eventHolder[0].removeEventListener('pinchstart', pinch.start); |
|
70 eventHolder[0].removeEventListener('pinchdrag', pinch.drag); |
|
71 eventHolder[0].removeEventListener('pinchend', pinch.end); |
|
72 eventHolder[0].removeEventListener('doubletap', doubleTap.recenterPlot); |
|
73 } |
|
74 |
|
75 pan = { |
|
76 start: function(e) { |
|
77 presetNavigationState(e, 'pan', gestureState); |
|
78 updateData(e, 'pan', gestureState, navigationState); |
|
79 |
|
80 if (useSmartPan) { |
|
81 var point = getPoint(e, 'pan'); |
|
82 navigationState.initialState = plot.navigationState(point.x, point.y); |
|
83 } |
|
84 }, |
|
85 |
|
86 drag: function(e) { |
|
87 presetNavigationState(e, 'pan', gestureState); |
|
88 |
|
89 if (useSmartPan) { |
|
90 var point = getPoint(e, 'pan'); |
|
91 plot.smartPan({ |
|
92 x: navigationState.initialState.startPageX - point.x, |
|
93 y: navigationState.initialState.startPageY - point.y |
|
94 }, navigationState.initialState, navigationState.touchedAxis, false, smartPanLock); |
|
95 } else if (useManualPan) { |
|
96 plot.pan({ |
|
97 left: -delta(e, 'pan', gestureState).x, |
|
98 top: -delta(e, 'pan', gestureState).y, |
|
99 axes: navigationState.touchedAxis |
|
100 }); |
|
101 updatePrevPanPosition(e, 'pan', gestureState, navigationState); |
|
102 } |
|
103 }, |
|
104 |
|
105 end: function(e) { |
|
106 presetNavigationState(e, 'pan', gestureState); |
|
107 |
|
108 if (useSmartPan) { |
|
109 plot.smartPan.end(); |
|
110 } |
|
111 |
|
112 if (wasPinchEvent(e, gestureState)) { |
|
113 updateprevPanPosition(e, 'pan', gestureState, navigationState); |
|
114 } |
|
115 } |
|
116 }; |
|
117 |
|
118 var pinchDragTimeout; |
|
119 pinch = { |
|
120 start: function(e) { |
|
121 if (pinchDragTimeout) { |
|
122 clearTimeout(pinchDragTimeout); |
|
123 pinchDragTimeout = null; |
|
124 } |
|
125 presetNavigationState(e, 'pinch', gestureState); |
|
126 setPrevDistance(e, gestureState); |
|
127 updateData(e, 'pinch', gestureState, navigationState); |
|
128 }, |
|
129 |
|
130 drag: function(e) { |
|
131 if (pinchDragTimeout) { |
|
132 return; |
|
133 } |
|
134 pinchDragTimeout = setTimeout(function() { |
|
135 presetNavigationState(e, 'pinch', gestureState); |
|
136 plot.pan({ |
|
137 left: -delta(e, 'pinch', gestureState).x, |
|
138 top: -delta(e, 'pinch', gestureState).y, |
|
139 axes: navigationState.touchedAxis |
|
140 }); |
|
141 updatePrevPanPosition(e, 'pinch', gestureState, navigationState); |
|
142 |
|
143 var dist = pinchDistance(e); |
|
144 |
|
145 if (gestureState.zoomEnable || Math.abs(dist - gestureState.prevDistance) > ZOOM_DISTANCE_MARGIN) { |
|
146 zoomPlot(plot, e, gestureState, navigationState); |
|
147 |
|
148 //activate zoom mode |
|
149 gestureState.zoomEnable = true; |
|
150 } |
|
151 pinchDragTimeout = null; |
|
152 }, 1000 / 60); |
|
153 }, |
|
154 |
|
155 end: function(e) { |
|
156 if (pinchDragTimeout) { |
|
157 clearTimeout(pinchDragTimeout); |
|
158 pinchDragTimeout = null; |
|
159 } |
|
160 presetNavigationState(e, 'pinch', gestureState); |
|
161 gestureState.prevDistance = null; |
|
162 } |
|
163 }; |
|
164 |
|
165 doubleTap = { |
|
166 recenterPlot: function(e) { |
|
167 if (e && e.detail && e.detail.type === 'touchstart') { |
|
168 // only do not recenter for touch start; |
|
169 recenterPlotOnDoubleTap(plot, e, gestureState, navigationState); |
|
170 } |
|
171 } |
|
172 }; |
|
173 |
|
174 if (options.pan.enableTouch === true || options.zoom.enableTouch === true) { |
|
175 plot.hooks.bindEvents.push(bindEvents); |
|
176 plot.hooks.shutdown.push(shutdown); |
|
177 } |
|
178 |
|
179 function presetNavigationState(e, gesture, gestureState) { |
|
180 navigationState.touchedAxis = getAxis(plot, e, gesture, navigationState); |
|
181 if (noAxisTouched(navigationState)) { |
|
182 navigationState.navigationConstraint = 'unconstrained'; |
|
183 } else { |
|
184 navigationState.navigationConstraint = 'axisConstrained'; |
|
185 } |
|
186 } |
|
187 } |
|
188 |
|
189 $.plot.plugins.push({ |
|
190 init: init, |
|
191 options: options, |
|
192 name: 'navigateTouch', |
|
193 version: '0.3' |
|
194 }); |
|
195 |
|
196 function recenterPlotOnDoubleTap(plot, e, gestureState, navigationState) { |
|
197 checkAxesForDoubleTap(plot, e, navigationState); |
|
198 if ((navigationState.currentTouchedAxis === 'x' && navigationState.prevTouchedAxis === 'x') || |
|
199 (navigationState.currentTouchedAxis === 'y' && navigationState.prevTouchedAxis === 'y') || |
|
200 (navigationState.currentTouchedAxis === 'none' && navigationState.prevTouchedAxis === 'none')) { |
|
201 var event; |
|
202 |
|
203 plot.recenter({ axes: navigationState.touchedAxis }); |
|
204 |
|
205 if (navigationState.touchedAxis) { |
|
206 event = new $.Event('re-center', { detail: { axisTouched: navigationState.touchedAxis } }); |
|
207 } else { |
|
208 event = new $.Event('re-center', { detail: e }); |
|
209 } |
|
210 plot.getPlaceholder().trigger(event); |
|
211 } |
|
212 } |
|
213 |
|
214 function checkAxesForDoubleTap(plot, e, navigationState) { |
|
215 var axis = plot.getTouchedAxis(e.detail.firstTouch.x, e.detail.firstTouch.y); |
|
216 if (axis[0] !== undefined) { |
|
217 navigationState.prevTouchedAxis = axis[0].direction; |
|
218 } |
|
219 |
|
220 axis = plot.getTouchedAxis(e.detail.secondTouch.x, e.detail.secondTouch.y); |
|
221 if (axis[0] !== undefined) { |
|
222 navigationState.touchedAxis = axis; |
|
223 navigationState.currentTouchedAxis = axis[0].direction; |
|
224 } |
|
225 |
|
226 if (noAxisTouched(navigationState)) { |
|
227 navigationState.touchedAxis = null; |
|
228 navigationState.prevTouchedAxis = 'none'; |
|
229 navigationState.currentTouchedAxis = 'none'; |
|
230 } |
|
231 } |
|
232 |
|
233 function zoomPlot(plot, e, gestureState, navigationState) { |
|
234 var offset = plot.offset(), |
|
235 center = { |
|
236 left: 0, |
|
237 top: 0 |
|
238 }, |
|
239 zoomAmount = pinchDistance(e) / gestureState.prevDistance, |
|
240 dist = pinchDistance(e); |
|
241 |
|
242 center.left = getPoint(e, 'pinch').x - offset.left; |
|
243 center.top = getPoint(e, 'pinch').y - offset.top; |
|
244 |
|
245 // send the computed touched axis to the zoom function so that it only zooms on that one |
|
246 plot.zoom({ |
|
247 center: center, |
|
248 amount: zoomAmount, |
|
249 axes: navigationState.touchedAxis |
|
250 }); |
|
251 gestureState.prevDistance = dist; |
|
252 } |
|
253 |
|
254 function wasPinchEvent(e, gestureState) { |
|
255 return (gestureState.zoomEnable && e.detail.touches.length === 1); |
|
256 } |
|
257 |
|
258 function getAxis(plot, e, gesture, navigationState) { |
|
259 if (e.type === 'pinchstart') { |
|
260 var axisTouch1 = plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); |
|
261 var axisTouch2 = plot.getTouchedAxis(e.detail.touches[1].pageX, e.detail.touches[1].pageY); |
|
262 |
|
263 if (axisTouch1.length === axisTouch2.length && axisTouch1.toString() === axisTouch2.toString()) { |
|
264 return axisTouch1; |
|
265 } |
|
266 } else if (e.type === 'panstart') { |
|
267 return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); |
|
268 } else if (e.type === 'pinchend') { |
|
269 //update axis since instead on pinch, a pan event is made |
|
270 return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY); |
|
271 } else { |
|
272 return navigationState.touchedAxis; |
|
273 } |
|
274 } |
|
275 |
|
276 function noAxisTouched(navigationState) { |
|
277 return (!navigationState.touchedAxis || navigationState.touchedAxis.length === 0); |
|
278 } |
|
279 |
|
280 function setPrevDistance(e, gestureState) { |
|
281 gestureState.prevDistance = pinchDistance(e); |
|
282 } |
|
283 |
|
284 function updateData(e, gesture, gestureState, navigationState) { |
|
285 var axisDir, |
|
286 point = getPoint(e, gesture); |
|
287 |
|
288 switch (navigationState.navigationConstraint) { |
|
289 case 'unconstrained': |
|
290 navigationState.touchedAxis = null; |
|
291 gestureState.prevTapPosition = { |
|
292 x: gestureState.prevPanPosition.x, |
|
293 y: gestureState.prevPanPosition.y |
|
294 }; |
|
295 gestureState.prevPanPosition = { |
|
296 x: point.x, |
|
297 y: point.y |
|
298 }; |
|
299 break; |
|
300 case 'axisConstrained': |
|
301 axisDir = navigationState.touchedAxis[0].direction; |
|
302 navigationState.currentTouchedAxis = axisDir; |
|
303 gestureState.prevTapPosition[axisDir] = gestureState.prevPanPosition[axisDir]; |
|
304 gestureState.prevPanPosition[axisDir] = point[axisDir]; |
|
305 break; |
|
306 default: |
|
307 break; |
|
308 } |
|
309 } |
|
310 |
|
311 function distance(x1, y1, x2, y2) { |
|
312 return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); |
|
313 } |
|
314 |
|
315 function pinchDistance(e) { |
|
316 var t1 = e.detail.touches[0], |
|
317 t2 = e.detail.touches[1]; |
|
318 return distance(t1.pageX, t1.pageY, t2.pageX, t2.pageY); |
|
319 } |
|
320 |
|
321 function updatePrevPanPosition(e, gesture, gestureState, navigationState) { |
|
322 var point = getPoint(e, gesture); |
|
323 |
|
324 switch (navigationState.navigationConstraint) { |
|
325 case 'unconstrained': |
|
326 gestureState.prevPanPosition.x = point.x; |
|
327 gestureState.prevPanPosition.y = point.y; |
|
328 break; |
|
329 case 'axisConstrained': |
|
330 gestureState.prevPanPosition[navigationState.currentTouchedAxis] = |
|
331 point[navigationState.currentTouchedAxis]; |
|
332 break; |
|
333 default: |
|
334 break; |
|
335 } |
|
336 } |
|
337 |
|
338 function delta(e, gesture, gestureState) { |
|
339 var point = getPoint(e, gesture); |
|
340 |
|
341 return { |
|
342 x: point.x - gestureState.prevPanPosition.x, |
|
343 y: point.y - gestureState.prevPanPosition.y |
|
344 } |
|
345 } |
|
346 |
|
347 function getPoint(e, gesture) { |
|
348 if (gesture === 'pinch') { |
|
349 return { |
|
350 x: (e.detail.touches[0].pageX + e.detail.touches[1].pageX) / 2, |
|
351 y: (e.detail.touches[0].pageY + e.detail.touches[1].pageY) / 2 |
|
352 } |
|
353 } else { |
|
354 return { |
|
355 x: e.detail.touches[0].pageX, |
|
356 y: e.detail.touches[0].pageY |
|
357 } |
|
358 } |
|
359 } |
|
360 })(jQuery); |