summaryrefslogtreecommitdiff
path: root/js/dojo-1.7.2/dojox/charting/action2d/TouchZoomAndPan.js
blob: 9ad36b94200687d03c97bedd473ff17537b6f16b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
//>>built
define("dojox/charting/action2d/TouchZoomAndPan", ["dojo/_base/lang", "dojo/_base/declare", "dojo/_base/event", "dojo/_base/sniff",
	"./ChartAction", "../Element", "dojox/gesture/tap", "../plot2d/common"], 
	function(lang, declare, eventUtil, has, ChartAction, Element, tap, common){
	var GlassView = declare("dojox.charting.action2d._GlassView", [Element], {
		//	summary: Private internal class used by TouchZoomAndPan actions.
		//	tags:
		//		private
		constructor: function(chart){
		},
		render: function(){
			if(!this.isDirty()){
				return;
			}
			this.cleanGroup();
			this.group.createRect({width: this.chart.dim.width, height: this.chart.dim.height}).setFill("rgba(0,0,0,0)");
		},
		cleanGroup: function(creator){
			//	summary:
			//		Clean any elements (HTML or GFX-based) out of our group, and create a new one.
			//	creator: dojox.gfx.Surface?
			//		An optional surface to work with.
			//	returns: dojox.charting.Element
			//		A reference to this object for functional chaining.
			this.inherited(arguments);
			return this;	//	dojox.charting.Element
		},
		clear: function(){
			//	summary:
			//		Clear out any parameters set on this plot.
			//	returns: dojox.charting.action2d._IndicatorElement
			//		The reference to this plot for functional chaining.
			this.dirty = true;
			// glass view needs to be above
			if(this.chart.stack[0] != this){
				this.chart.movePlotToFront(this.name);
			}
			return this;	//	dojox.charting.plot2d._IndicatorElement
		},
		getSeriesStats: function(){
			//	summary:
			//		Returns default stats (irrelevant for this type of plot).
			//	returns: Object
			//		{hmin, hmax, vmin, vmax} min/max in both directions.
			return lang.delegate(common.defaultStats);
		},
		initializeScalers: function(){
			//	summary:
			//		Does nothing (irrelevant for this type of plot).
			return this;
		},
		isDirty: function(){
			//	summary:
			//		Return whether or not this plot needs to be redrawn.
			//	returns: Boolean
			//		If this plot needs to be rendered, this will return true.
			return this.dirty;
		}
	});
	
	/*=====
	
	declare("dojox.charting.action2d.__TouchZoomAndPanCtorArgs", null, {
		//	summary:
		//		Additional arguments for mouse zoom and pan actions.
		
		//	axis: String?
		//		Target axis name for this action.  Default is "x".
		axis: "x",
		//	scaleFactor: Number?
		//		The scale factor applied on double tap.  Default is 1.2.
		scaleFactor: 1.2,
		//	maxScale: Number?
		//		The max scale factor accepted by this action.  Default is 100.
		maxScale: 100,
		//	enableScroll: Boolean?
		//		Whether touch drag gesture should scroll the chart.  Default is true.
		enableScroll: true,
		//	enableZoom: Boolean?
		//		Whether touch pinch and spread gesture should zoom out or in the chart.  Default is true.
		enableZoom: true,
	});
	var ChartAction = dojox.charting.action2d.ChartAction;
	=====*/
	
	return declare("dojox.charting.action2d.TouchZoomAndPan", ChartAction, {
		//	summary:
		//		Create a touch zoom and pan action. 
		//		You can zoom out or in the data window with pinch and spread gestures. You can scroll using drag gesture. 
		//		Finally this is possible to navigate between a fit window and a zoom one using double tap gesture.
	
		// the data description block for the widget parser
		defaultParams: {
			axis: "x",
			scaleFactor: 1.2,	
			maxScale: 100,
			enableScroll: true,
			enableZoom: true
		},
		optionalParams: {},	// no optional parameters
	
		constructor: function(chart, plot, kwArgs){
			//	summary:
			//		Create a new touch zoom and pan action and connect it.
			//	chart: dojox.charting.Chart
			//		The chart this action applies to.
			//	kwArgs: dojox.charting.action2d.__TouchZoomAndPanCtorArgs?
			//		Optional arguments for the action.
			this._listeners = [{eventName: "ontouchstart", methodName: "onTouchStart"},
			                   {eventName: "ontouchmove", methodName: "onTouchMove"},
			                   {eventName: "ontouchend", methodName: "onTouchEnd"},
							   {eventName: tap.doubletap, methodName: "onDoubleTap"}];
			if(!kwArgs){ kwArgs = {}; }
			this.axis = kwArgs.axis ? kwArgs.axis : "x";
			this.scaleFactor = kwArgs.scaleFactor ? kwArgs.scaleFactor : 1.2;
			this.maxScale = kwArgs.maxScale ? kwArgs.maxScale : 100;
			this.enableScroll = kwArgs.enableScroll != undefined ? kwArgs.enableScroll : true;
			this.enableZoom = kwArgs.enableScroll != undefined ? kwArgs.enableZoom : true;
			this._uName = "touchZoomPan"+this.axis;
			this.connect();
		},
		
		connect: function(){
			//	summary:
			//		Connect this action to the chart. On Safari this adds a new glass view plot
			//		to the chart that's why Chart.render() must be called after connect.
			this.inherited(arguments);
			// this is needed to workaround issue on Safari + SVG, because a touch start action
			// started above a item that is removed during the touch action will stop
			// dispatching touch events!
			if(has("safari") && this.chart.surface.declaredClass.indexOf("svg")!=-1){
				this.chart.addPlot(this._uName, {type: GlassView});
			}
		},
		
		disconnect: function(){
			//	summary:
			//		Disconnect this action from the chart. 
			if(has("safari") && this.chart.surface.declaredClass.indexOf("svg")!=-1){
				this.chart.removePlot(this._uName);
			}
			this.inherited(arguments);
		},
	
		onTouchStart: function(event){
			//	summary:
			//		Called when touch is started on the chart.
			// we always want to be above regular plots and not clipped
			var chart = this.chart, axis = chart.getAxis(this.axis);
			var length = event.touches.length;
			this._startPageCoord = {x: event.touches[0].pageX, y: event.touches[0].pageY};
			if((this.enableZoom || this.enableScroll) && chart._delayedRenderHandle){
				// we have pending rendering from a scroll, let's sync
				clearTimeout(chart._delayedRenderHandle);
				chart._delayedRenderHandle = null;
				chart.render();
			}
			if(this.enableZoom && length >= 2){
				this._endPageCoord =  {x: event.touches[1].pageX, y: event.touches[1].pageY};
				var middlePageCoord = {x: (this._startPageCoord.x + this._endPageCoord.x) / 2,
										y: (this._startPageCoord.y + this._endPageCoord.y) / 2};
				var scaler = axis.getScaler();
				this._initScale = axis.getWindowScale();
				var t = this._initData =  this.plot.toData();
				this._middleCoord = t(middlePageCoord)[this.axis];
				this._startCoord = scaler.bounds.from;
				this._endCoord = scaler.bounds.to;
			}else if(this.enableScroll){
				this._startScroll(axis);
				// needed for Android, otherwise will get a touch cancel while swiping
				eventUtil.stop(event);
			}
		},
	
		onTouchMove: function(event){
			//	summary:
			//		Called when touch is moved on the chart.
			var chart = this.chart, axis = chart.getAxis(this.axis);
			var length = event.touches.length;
			var pAttr = axis.vertical?"pageY":"pageX", 
					attr = axis.vertical?"y":"x";
			if(this.enableZoom && length >= 2){
				var newMiddlePageCoord = {x: (event.touches[1].pageX + event.touches[0].pageX) / 2,
											y: (event.touches[1].pageY + event.touches[0].pageY) / 2};		
				var scale = (this._endPageCoord[attr] - this._startPageCoord[attr]) /
					(event.touches[1][pAttr] - event.touches[0][pAttr]);
	
				if(this._initScale / scale > this.maxScale){
					return;
				}
	
				var newMiddleCoord = this._initData(newMiddlePageCoord)[this.axis];
	
				var newStart = scale * (this._startCoord - newMiddleCoord)  + this._middleCoord,
				newEnd = scale * (this._endCoord - newMiddleCoord) + this._middleCoord;
				chart.zoomIn(this.axis, [newStart, newEnd]);
				// avoid browser pan
				eventUtil.stop(event);
			}else if(this.enableScroll){
				var delta = axis.vertical?(this._startPageCoord[attr] - event.touches[0][pAttr]):
					(event.touches[0][pAttr] - this._startPageCoord[attr]);
				chart.setAxisWindow(this.axis, this._lastScale, this._initOffset - delta / this._lastFactor / this._lastScale);
				chart.delayedRender();
				// avoid browser pan
				eventUtil.stop(event);
			}		
		},
	
		onTouchEnd: function(event){
			//	summary:
			//		Called when touch is ended on the chart.
			var chart = this.chart, axis = chart.getAxis(this.axis);
			if(event.touches.length == 1 && this.enableScroll){
				// still one touch available, let's start back from here for
				// potential pan
				this._startPageCoord = {x: event.touches[0].pageX, y: event.touches[0].pageY};
				this._startScroll(axis);
			}
		},
		
		_startScroll: function(axis){
			var bounds = axis.getScaler().bounds;
			this._initOffset = axis.getWindowOffset();
			// we keep it because of delay rendering we might now always have access to the
			// information to compute it
			this._lastScale = axis.getWindowScale();
			this._lastFactor = bounds.span / (bounds.upper - bounds.lower); 
		},
	
		onDoubleTap: function(event){
			//	summary:
			//		Called when double tap is performed on the chart.
			var chart = this.chart, axis = chart.getAxis(this.axis);
			var scale = 1 / this.scaleFactor;
			// are we fit?
			if(axis.getWindowScale()==1){
				// fit => zoom
				var scaler = axis.getScaler(), start = scaler.bounds.from, end = scaler.bounds.to, 
				oldMiddle = (start + end) / 2, newMiddle = this.plot.toData(this._startPageCoord)[this.axis], 
				newStart = scale * (start - oldMiddle) + newMiddle, newEnd = scale * (end - oldMiddle) + newMiddle;
				chart.zoomIn(this.axis, [newStart, newEnd]);
			}else{
				// non fit => fit
				chart.setAxisWindow(this.axis, 1, 0);
				chart.render();
			}
			eventUtil.stop(event);
		}
	});
});