Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / draw / Draw.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.draw.Draw
17  * Base Drawing class.  Provides base drawing functions.
18  * @private
19  */
20 Ext.define('Ext.draw.Draw', {
21     /* Begin Definitions */
22
23     singleton: true,
24
25     requires: ['Ext.draw.Color'],
26
27     /* End Definitions */
28
29     pathToStringRE: /,?([achlmqrstvxz]),?/gi,
30     pathCommandRE: /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,
31     pathValuesRE: /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig,
32     stopsRE: /^(\d+%?)$/,
33     radian: Math.PI / 180,
34
35     availableAnimAttrs: {
36         along: "along",
37         blur: null,
38         "clip-rect": "csv",
39         cx: null,
40         cy: null,
41         fill: "color",
42         "fill-opacity": null,
43         "font-size": null,
44         height: null,
45         opacity: null,
46         path: "path",
47         r: null,
48         rotation: "csv",
49         rx: null,
50         ry: null,
51         scale: "csv",
52         stroke: "color",
53         "stroke-opacity": null,
54         "stroke-width": null,
55         translation: "csv",
56         width: null,
57         x: null,
58         y: null
59     },
60
61     is: function(o, type) {
62         type = String(type).toLowerCase();
63         return (type == "object" && o === Object(o)) ||
64             (type == "undefined" && typeof o == type) ||
65             (type == "null" && o === null) ||
66             (type == "array" && Array.isArray && Array.isArray(o)) ||
67             (Object.prototype.toString.call(o).toLowerCase().slice(8, -1)) == type;
68     },
69
70     ellipsePath: function(sprite) {
71         var attr = sprite.attr;
72         return Ext.String.format("M{0},{1}A{2},{3},0,1,1,{0},{4}A{2},{3},0,1,1,{0},{1}z", attr.x, attr.y - attr.ry, attr.rx, attr.ry, attr.y + attr.ry);
73     },
74
75     rectPath: function(sprite) {
76         var attr = sprite.attr;
77         if (attr.radius) {
78             return Ext.String.format("M{0},{1}l{2},0a{3},{3},0,0,1,{3},{3}l0,{5}a{3},{3},0,0,1,{4},{3}l{6},0a{3},{3},0,0,1,{4},{4}l0,{7}a{3},{3},0,0,1,{3},{4}z", attr.x + attr.radius, attr.y, attr.width - attr.radius * 2, attr.radius, -attr.radius, attr.height - attr.radius * 2, attr.radius * 2 - attr.width, attr.radius * 2 - attr.height);
79         }
80         else {
81             return Ext.String.format("M{0},{1}l{2},0,0,{3},{4},0z", attr.x, attr.y, attr.width, attr.height, -attr.width);
82         }
83     },
84
85     // To be deprecated, converts itself (an arrayPath) to a proper SVG path string
86     path2string: function () {
87         return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
88     },
89
90     // Convert the passed arrayPath to a proper SVG path string (d attribute)
91     pathToString: function(arrayPath) {
92         return arrayPath.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
93     },
94
95     parsePathString: function (pathString) {
96         if (!pathString) {
97             return null;
98         }
99         var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0},
100             data = [],
101             me = this;
102         if (me.is(pathString, "array") && me.is(pathString[0], "array")) { // rough assumption
103             data = me.pathClone(pathString);
104         }
105         if (!data.length) {
106             String(pathString).replace(me.pathCommandRE, function (a, b, c) {
107                 var params = [],
108                     name = b.toLowerCase();
109                 c.replace(me.pathValuesRE, function (a, b) {
110                     b && params.push(+b);
111                 });
112                 if (name == "m" && params.length > 2) {
113                     data.push([b].concat(Ext.Array.splice(params, 0, 2)));
114                     name = "l";
115                     b = (b == "m") ? "l" : "L";
116                 }
117                 while (params.length >= paramCounts[name]) {
118                     data.push([b].concat(Ext.Array.splice(params, 0, paramCounts[name])));
119                     if (!paramCounts[name]) {
120                         break;
121                     }
122                 }
123             });
124         }
125         data.toString = me.path2string;
126         return data;
127     },
128
129     mapPath: function (path, matrix) {
130         if (!matrix) {
131             return path;
132         }
133         var x, y, i, ii, j, jj, pathi;
134         path = this.path2curve(path);
135         for (i = 0, ii = path.length; i < ii; i++) {
136             pathi = path[i];
137             for (j = 1, jj = pathi.length; j < jj-1; j += 2) {
138                 x = matrix.x(pathi[j], pathi[j + 1]);
139                 y = matrix.y(pathi[j], pathi[j + 1]);
140                 pathi[j] = x;
141                 pathi[j + 1] = y;
142             }
143         }
144         return path;
145     },
146
147     pathClone: function(pathArray) {
148         var res = [],
149             j, jj, i, ii;
150         if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption
151             pathArray = this.parsePathString(pathArray);
152         }
153         for (i = 0, ii = pathArray.length; i < ii; i++) {
154             res[i] = [];
155             for (j = 0, jj = pathArray[i].length; j < jj; j++) {
156                 res[i][j] = pathArray[i][j];
157             }
158         }
159         res.toString = this.path2string;
160         return res;
161     },
162
163     pathToAbsolute: function (pathArray) {
164         if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption
165             pathArray = this.parsePathString(pathArray);
166         }
167         var res = [],
168             x = 0,
169             y = 0,
170             mx = 0,
171             my = 0,
172             i = 0,
173             ln = pathArray.length,
174             r, pathSegment, j, ln2;
175         // MoveTo initial x/y position
176         if (ln && pathArray[0][0] == "M") {
177             x = +pathArray[0][1];
178             y = +pathArray[0][2];
179             mx = x;
180             my = y;
181             i++;
182             res[0] = ["M", x, y];
183         }
184         for (; i < ln; i++) {
185             r = res[i] = [];
186             pathSegment = pathArray[i];
187             if (pathSegment[0] != pathSegment[0].toUpperCase()) {
188                 r[0] = pathSegment[0].toUpperCase();
189                 switch (r[0]) {
190                     // Elliptical Arc
191                     case "A":
192                         r[1] = pathSegment[1];
193                         r[2] = pathSegment[2];
194                         r[3] = pathSegment[3];
195                         r[4] = pathSegment[4];
196                         r[5] = pathSegment[5];
197                         r[6] = +(pathSegment[6] + x);
198                         r[7] = +(pathSegment[7] + y);
199                         break;
200                     // Vertical LineTo
201                     case "V":
202                         r[1] = +pathSegment[1] + y;
203                         break;
204                     // Horizontal LineTo
205                     case "H":
206                         r[1] = +pathSegment[1] + x;
207                         break;
208                     case "M":
209                     // MoveTo
210                         mx = +pathSegment[1] + x;
211                         my = +pathSegment[2] + y;
212                     default:
213                         j = 1;
214                         ln2 = pathSegment.length;
215                         for (; j < ln2; j++) {
216                             r[j] = +pathSegment[j] + ((j % 2) ? x : y);
217                         }
218                 }
219             }
220             else {
221                 j = 0;
222                 ln2 = pathSegment.length;
223                 for (; j < ln2; j++) {
224                     res[i][j] = pathSegment[j];
225                 }
226             }
227             switch (r[0]) {
228                 // ClosePath
229                 case "Z":
230                     x = mx;
231                     y = my;
232                     break;
233                 // Horizontal LineTo
234                 case "H":
235                     x = r[1];
236                     break;
237                 // Vertical LineTo
238                 case "V":
239                     y = r[1];
240                     break;
241                 // MoveTo
242                 case "M":
243                     pathSegment = res[i];
244                     ln2 = pathSegment.length;
245                     mx = pathSegment[ln2 - 2];
246                     my = pathSegment[ln2 - 1];
247                 default:
248                     pathSegment = res[i];
249                     ln2 = pathSegment.length;
250                     x = pathSegment[ln2 - 2];
251                     y = pathSegment[ln2 - 1];
252             }
253         }
254         res.toString = this.path2string;
255         return res;
256     },
257
258     // TO BE DEPRECATED
259     pathToRelative: function (pathArray) {
260         if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) {
261             pathArray = this.parsePathString(pathArray);
262         }
263         var res = [],
264             x = 0,
265             y = 0,
266             mx = 0,
267             my = 0,
268             start = 0;
269         if (pathArray[0][0] == "M") {
270             x = pathArray[0][1];
271             y = pathArray[0][2];
272             mx = x;
273             my = y;
274             start++;
275             res.push(["M", x, y]);
276         }
277         for (var i = start, ii = pathArray.length; i < ii; i++) {
278             var r = res[i] = [],
279                 pa = pathArray[i];
280             if (pa[0] != pa[0].toLowerCase()) {
281                 r[0] = pa[0].toLowerCase();
282                 switch (r[0]) {
283                     case "a":
284                         r[1] = pa[1];
285                         r[2] = pa[2];
286                         r[3] = pa[3];
287                         r[4] = pa[4];
288                         r[5] = pa[5];
289                         r[6] = +(pa[6] - x).toFixed(3);
290                         r[7] = +(pa[7] - y).toFixed(3);
291                         break;
292                     case "v":
293                         r[1] = +(pa[1] - y).toFixed(3);
294                         break;
295                     case "m":
296                         mx = pa[1];
297                         my = pa[2];
298                     default:
299                         for (var j = 1, jj = pa.length; j < jj; j++) {
300                             r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
301                         }
302                 }
303             } else {
304                 r = res[i] = [];
305                 if (pa[0] == "m") {
306                     mx = pa[1] + x;
307                     my = pa[2] + y;
308                 }
309                 for (var k = 0, kk = pa.length; k < kk; k++) {
310                     res[i][k] = pa[k];
311                 }
312             }
313             var len = res[i].length;
314             switch (res[i][0]) {
315                 case "z":
316                     x = mx;
317                     y = my;
318                     break;
319                 case "h":
320                     x += +res[i][len - 1];
321                     break;
322                 case "v":
323                     y += +res[i][len - 1];
324                     break;
325                 default:
326                     x += +res[i][len - 2];
327                     y += +res[i][len - 1];
328             }
329         }
330         res.toString = this.path2string;
331         return res;
332     },
333
334     // Returns a path converted to a set of curveto commands
335     path2curve: function (path) {
336         var me = this,
337             points = me.pathToAbsolute(path),
338             ln = points.length,
339             attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
340             i, seg, segLn, point;
341             
342         for (i = 0; i < ln; i++) {
343             points[i] = me.command2curve(points[i], attrs);
344             if (points[i].length > 7) {
345                     points[i].shift();
346                     point = points[i];
347                     while (point.length) {
348                         Ext.Array.splice(points, i++, 0, ["C"].concat(Ext.Array.splice(point, 0, 6)));
349                     }
350                     Ext.Array.erase(points, i, 1);
351                     ln = points.length;
352                 }
353             seg = points[i];
354             segLn = seg.length;
355             attrs.x = seg[segLn - 2];
356             attrs.y = seg[segLn - 1];
357             attrs.bx = parseFloat(seg[segLn - 4]) || attrs.x;
358             attrs.by = parseFloat(seg[segLn - 3]) || attrs.y;
359         }
360         return points;
361     },
362     
363     interpolatePaths: function (path, path2) {
364         var me = this,
365             p = me.pathToAbsolute(path),
366             p2 = me.pathToAbsolute(path2),
367             attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
368             attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
369             fixArc = function (pp, i) {
370                 if (pp[i].length > 7) {
371                     pp[i].shift();
372                     var pi = pp[i];
373                     while (pi.length) {
374                         Ext.Array.splice(pp, i++, 0, ["C"].concat(Ext.Array.splice(pi, 0, 6)));
375                     }
376                     Ext.Array.erase(pp, i, 1);
377                     ii = Math.max(p.length, p2.length || 0);
378                 }
379             },
380             fixM = function (path1, path2, a1, a2, i) {
381                 if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
382                     Ext.Array.splice(path2, i, 0, ["M", a2.x, a2.y]);
383                     a1.bx = 0;
384                     a1.by = 0;
385                     a1.x = path1[i][1];
386                     a1.y = path1[i][2];
387                     ii = Math.max(p.length, p2.length || 0);
388                 }
389             };
390         for (var i = 0, ii = Math.max(p.length, p2.length || 0); i < ii; i++) {
391             p[i] = me.command2curve(p[i], attrs);
392             fixArc(p, i);
393             (p2[i] = me.command2curve(p2[i], attrs2));
394             fixArc(p2, i);
395             fixM(p, p2, attrs, attrs2, i);
396             fixM(p2, p, attrs2, attrs, i);
397             var seg = p[i],
398                 seg2 = p2[i],
399                 seglen = seg.length,
400                 seg2len = seg2.length;
401             attrs.x = seg[seglen - 2];
402             attrs.y = seg[seglen - 1];
403             attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x;
404             attrs.by = parseFloat(seg[seglen - 3]) || attrs.y;
405             attrs2.bx = (parseFloat(seg2[seg2len - 4]) || attrs2.x);
406             attrs2.by = (parseFloat(seg2[seg2len - 3]) || attrs2.y);
407             attrs2.x = seg2[seg2len - 2];
408             attrs2.y = seg2[seg2len - 1];
409         }
410         return [p, p2];
411     },
412     
413     //Returns any path command as a curveto command based on the attrs passed
414     command2curve: function (pathCommand, d) {
415         var me = this;
416         if (!pathCommand) {
417             return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
418         }
419         if (pathCommand[0] != "T" && pathCommand[0] != "Q") {
420             d.qx = d.qy = null;
421         }
422         switch (pathCommand[0]) {
423             case "M":
424                 d.X = pathCommand[1];
425                 d.Y = pathCommand[2];
426                 break;
427             case "A":
428                 pathCommand = ["C"].concat(me.arc2curve.apply(me, [d.x, d.y].concat(pathCommand.slice(1))));
429                 break;
430             case "S":
431                 pathCommand = ["C", d.x + (d.x - (d.bx || d.x)), d.y + (d.y - (d.by || d.y))].concat(pathCommand.slice(1));
432                 break;
433             case "T":
434                 d.qx = d.x + (d.x - (d.qx || d.x));
435                 d.qy = d.y + (d.y - (d.qy || d.y));
436                 pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, d.qx, d.qy, pathCommand[1], pathCommand[2]));
437                 break;
438             case "Q":
439                 d.qx = pathCommand[1];
440                 d.qy = pathCommand[2];
441                 pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[3], pathCommand[4]));
442                 break;
443             case "L":
444                 pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[1], pathCommand[2]);
445                 break;
446             case "H":
447                 pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], d.y, pathCommand[1], d.y);
448                 break;
449             case "V":
450                 pathCommand = ["C"].concat(d.x, d.y, d.x, pathCommand[1], d.x, pathCommand[1]);
451                 break;
452             case "Z":
453                 pathCommand = ["C"].concat(d.x, d.y, d.X, d.Y, d.X, d.Y);
454                 break;
455         }
456         return pathCommand;
457     },
458
459     quadratic2curve: function (x1, y1, ax, ay, x2, y2) {
460         var _13 = 1 / 3,
461             _23 = 2 / 3;
462         return [
463                 _13 * x1 + _23 * ax,
464                 _13 * y1 + _23 * ay,
465                 _13 * x2 + _23 * ax,
466                 _13 * y2 + _23 * ay,
467                 x2,
468                 y2
469             ];
470     },
471     
472     rotate: function (x, y, rad) {
473         var cos = Math.cos(rad),
474             sin = Math.sin(rad),
475             X = x * cos - y * sin,
476             Y = x * sin + y * cos;
477         return {x: X, y: Y};
478     },
479
480     arc2curve: function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
481         // for more information of where this Math came from visit:
482         // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
483         var me = this,
484             PI = Math.PI,
485             radian = me.radian,
486             _120 = PI * 120 / 180,
487             rad = radian * (+angle || 0),
488             res = [],
489             math = Math,
490             mcos = math.cos,
491             msin = math.sin,
492             msqrt = math.sqrt,
493             mabs = math.abs,
494             masin = math.asin,
495             xy, cos, sin, x, y, h, rx2, ry2, k, cx, cy, f1, f2, df, c1, s1, c2, s2,
496             t, hx, hy, m1, m2, m3, m4, newres, i, ln, f2old, x2old, y2old;
497         if (!recursive) {
498             xy = me.rotate(x1, y1, -rad);
499             x1 = xy.x;
500             y1 = xy.y;
501             xy = me.rotate(x2, y2, -rad);
502             x2 = xy.x;
503             y2 = xy.y;
504             cos = mcos(radian * angle);
505             sin = msin(radian * angle);
506             x = (x1 - x2) / 2;
507             y = (y1 - y2) / 2;
508             h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
509             if (h > 1) {
510                 h = msqrt(h);
511                 rx = h * rx;
512                 ry = h * ry;
513             }
514             rx2 = rx * rx;
515             ry2 = ry * ry;
516             k = (large_arc_flag == sweep_flag ? -1 : 1) *
517                     msqrt(mabs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
518             cx = k * rx * y / ry + (x1 + x2) / 2;
519             cy = k * -ry * x / rx + (y1 + y2) / 2;
520             f1 = masin(((y1 - cy) / ry).toFixed(7));
521             f2 = masin(((y2 - cy) / ry).toFixed(7));
522
523             f1 = x1 < cx ? PI - f1 : f1;
524             f2 = x2 < cx ? PI - f2 : f2;
525             if (f1 < 0) {
526                 f1 = PI * 2 + f1;
527             }
528             if (f2 < 0) {
529                 f2 = PI * 2 + f2;
530             }
531             if (sweep_flag && f1 > f2) {
532                 f1 = f1 - PI * 2;
533             }
534             if (!sweep_flag && f2 > f1) {
535                 f2 = f2 - PI * 2;
536             }
537         }
538         else {
539             f1 = recursive[0];
540             f2 = recursive[1];
541             cx = recursive[2];
542             cy = recursive[3];
543         }
544         df = f2 - f1;
545         if (mabs(df) > _120) {
546             f2old = f2;
547             x2old = x2;
548             y2old = y2;
549             f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
550             x2 = cx + rx * mcos(f2);
551             y2 = cy + ry * msin(f2);
552             res = me.arc2curve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
553         }
554         df = f2 - f1;
555         c1 = mcos(f1);
556         s1 = msin(f1);
557         c2 = mcos(f2);
558         s2 = msin(f2);
559         t = math.tan(df / 4);
560         hx = 4 / 3 * rx * t;
561         hy = 4 / 3 * ry * t;
562         m1 = [x1, y1];
563         m2 = [x1 + hx * s1, y1 - hy * c1];
564         m3 = [x2 + hx * s2, y2 - hy * c2];
565         m4 = [x2, y2];
566         m2[0] = 2 * m1[0] - m2[0];
567         m2[1] = 2 * m1[1] - m2[1];
568         if (recursive) {
569             return [m2, m3, m4].concat(res);
570         }
571         else {
572             res = [m2, m3, m4].concat(res).join().split(",");
573             newres = [];
574             ln = res.length;
575             for (i = 0;  i < ln; i++) {
576                 newres[i] = i % 2 ? me.rotate(res[i - 1], res[i], rad).y : me.rotate(res[i], res[i + 1], rad).x;
577             }
578             return newres;
579         }
580     },
581
582     // TO BE DEPRECATED
583     rotateAndTranslatePath: function (sprite) {
584         var alpha = sprite.rotation.degrees,
585             cx = sprite.rotation.x,
586             cy = sprite.rotation.y,
587             dx = sprite.translation.x,
588             dy = sprite.translation.y,
589             path,
590             i,
591             p,
592             xy,
593             j,
594             res = [];
595         if (!alpha && !dx && !dy) {
596             return this.pathToAbsolute(sprite.attr.path);
597         }
598         dx = dx || 0;
599         dy = dy || 0;
600         path = this.pathToAbsolute(sprite.attr.path);
601         for (i = path.length; i--;) {
602             p = res[i] = path[i].slice();
603             if (p[0] == "A") {
604                 xy = this.rotatePoint(p[6], p[7], alpha, cx, cy);
605                 p[6] = xy.x + dx;
606                 p[7] = xy.y + dy;
607             } else {
608                 j = 1;
609                 while (p[j + 1] != null) {
610                     xy = this.rotatePoint(p[j], p[j + 1], alpha, cx, cy);
611                     p[j] = xy.x + dx;
612                     p[j + 1] = xy.y + dy;
613                     j += 2;
614                 }
615             }
616         }
617         return res;
618     },
619
620     // TO BE DEPRECATED
621     rotatePoint: function (x, y, alpha, cx, cy) {
622         if (!alpha) {
623             return {
624                 x: x,
625                 y: y
626             };
627         }
628         cx = cx || 0;
629         cy = cy || 0;
630         x = x - cx;
631         y = y - cy;
632         alpha = alpha * this.radian;
633         var cos = Math.cos(alpha),
634             sin = Math.sin(alpha);
635         return {
636             x: x * cos - y * sin + cx,
637             y: x * sin + y * cos + cy
638         };
639     },
640
641     pathDimensions: function (path) {
642         if (!path || !(path + "")) {
643             return {x: 0, y: 0, width: 0, height: 0};
644         }
645         path = this.path2curve(path);
646         var x = 0, 
647             y = 0,
648             X = [],
649             Y = [],
650             i = 0,
651             ln = path.length,
652             p, xmin, ymin, dim;
653         for (; i < ln; i++) {
654             p = path[i];
655             if (p[0] == "M") {
656                 x = p[1];
657                 y = p[2];
658                 X.push(x);
659                 Y.push(y);
660             }
661             else {
662                 dim = this.curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
663                 X = X.concat(dim.min.x, dim.max.x);
664                 Y = Y.concat(dim.min.y, dim.max.y);
665                 x = p[5];
666                 y = p[6];
667             }
668         }
669         xmin = Math.min.apply(0, X);
670         ymin = Math.min.apply(0, Y);
671         return {
672             x: xmin,
673             y: ymin,
674             path: path,
675             width: Math.max.apply(0, X) - xmin,
676             height: Math.max.apply(0, Y) - ymin
677         };
678     },
679
680     intersectInside: function(path, cp1, cp2) {
681         return (cp2[0] - cp1[0]) * (path[1] - cp1[1]) > (cp2[1] - cp1[1]) * (path[0] - cp1[0]);
682     },
683
684     intersectIntersection: function(s, e, cp1, cp2) {
685         var p = [],
686             dcx = cp1[0] - cp2[0],
687             dcy = cp1[1] - cp2[1],
688             dpx = s[0] - e[0],
689             dpy = s[1] - e[1],
690             n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0],
691             n2 = s[0] * e[1] - s[1] * e[0],
692             n3 = 1 / (dcx * dpy - dcy * dpx);
693
694         p[0] = (n1 * dpx - n2 * dcx) * n3;
695         p[1] = (n1 * dpy - n2 * dcy) * n3;
696         return p;
697     },
698
699     intersect: function(subjectPolygon, clipPolygon) {
700         var me = this,
701             i = 0,
702             ln = clipPolygon.length,
703             cp1 = clipPolygon[ln - 1],
704             outputList = subjectPolygon,
705             cp2, s, e, point, ln2, inputList, j;
706         for (; i < ln; ++i) {
707             cp2 = clipPolygon[i];
708             inputList = outputList;
709             outputList = [];
710             s = inputList[inputList.length - 1];
711             j = 0;
712             ln2 = inputList.length;
713             for (; j < ln2; j++) {
714                 e = inputList[j];
715                 if (me.intersectInside(e, cp1, cp2)) {
716                     if (!me.intersectInside(s, cp1, cp2)) {
717                         outputList.push(me.intersectIntersection(s, e, cp1, cp2));
718                     }
719                     outputList.push(e);
720                 }
721                 else if (me.intersectInside(s, cp1, cp2)) {
722                     outputList.push(me.intersectIntersection(s, e, cp1, cp2));
723                 }
724                 s = e;
725             }
726             cp1 = cp2;
727         }
728         return outputList;
729     },
730
731     curveDim: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
732         var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
733             b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
734             c = p1x - c1x,
735             t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a,
736             t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a,
737             y = [p1y, p2y],
738             x = [p1x, p2x],
739             dot;
740         if (Math.abs(t1) > 1e12) {
741             t1 = 0.5;
742         }
743         if (Math.abs(t2) > 1e12) {
744             t2 = 0.5;
745         }
746         if (t1 > 0 && t1 < 1) {
747             dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
748             x.push(dot.x);
749             y.push(dot.y);
750         }
751         if (t2 > 0 && t2 < 1) {
752             dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
753             x.push(dot.x);
754             y.push(dot.y);
755         }
756         a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
757         b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
758         c = p1y - c1y;
759         t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
760         t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
761         if (Math.abs(t1) > 1e12) {
762             t1 = 0.5;
763         }
764         if (Math.abs(t2) > 1e12) {
765             t2 = 0.5;
766         }
767         if (t1 > 0 && t1 < 1) {
768             dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
769             x.push(dot.x);
770             y.push(dot.y);
771         }
772         if (t2 > 0 && t2 < 1) {
773             dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
774             x.push(dot.x);
775             y.push(dot.y);
776         }
777         return {
778             min: {x: Math.min.apply(0, x), y: Math.min.apply(0, y)},
779             max: {x: Math.max.apply(0, x), y: Math.max.apply(0, y)}
780         };
781     },
782
783     /**
784      * @private
785      *
786      * Calculates bezier curve control anchor points for a particular point in a path, with a
787      * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
788      * Note that this algorithm assumes that the line being smoothed is normalized going from left
789      * to right; it makes special adjustments assuming this orientation.
790      *
791      * @param {Number} prevX X coordinate of the previous point in the path
792      * @param {Number} prevY Y coordinate of the previous point in the path
793      * @param {Number} curX X coordinate of the current point in the path
794      * @param {Number} curY Y coordinate of the current point in the path
795      * @param {Number} nextX X coordinate of the next point in the path
796      * @param {Number} nextY Y coordinate of the next point in the path
797      * @param {Number} value A value to control the smoothness of the curve; this is used to
798      * divide the distance between points, so a value of 2 corresponds to
799      * half the distance between points (a very smooth line) while higher values
800      * result in less smooth curves. Defaults to 4.
801      * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
802      * are the control point for the curve toward the previous path point, and
803      * x2 and y2 are the control point for the curve toward the next path point.
804      */
805     getAnchors: function (prevX, prevY, curX, curY, nextX, nextY, value) {
806         value = value || 4;
807         var M = Math,
808             PI = M.PI,
809             halfPI = PI / 2,
810             abs = M.abs,
811             sin = M.sin,
812             cos = M.cos,
813             atan = M.atan,
814             control1Length, control2Length, control1Angle, control2Angle,
815             control1X, control1Y, control2X, control2Y, alpha;
816
817         // Find the length of each control anchor line, by dividing the horizontal distance
818         // between points by the value parameter.
819         control1Length = (curX - prevX) / value;
820         control2Length = (nextX - curX) / value;
821
822         // Determine the angle of each control anchor line. If the middle point is a vertical
823         // turnaround then we force it to a flat horizontal angle to prevent the curve from
824         // dipping above or below the middle point. Otherwise we use an angle that points
825         // toward the previous/next target point.
826         if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
827             control1Angle = control2Angle = halfPI;
828         } else {
829             control1Angle = atan((curX - prevX) / abs(curY - prevY));
830             if (prevY < curY) {
831                 control1Angle = PI - control1Angle;
832             }
833             control2Angle = atan((nextX - curX) / abs(curY - nextY));
834             if (nextY < curY) {
835                 control2Angle = PI - control2Angle;
836             }
837         }
838
839         // Adjust the calculated angles so they point away from each other on the same line
840         alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
841         if (alpha > halfPI) {
842             alpha -= PI;
843         }
844         control1Angle += alpha;
845         control2Angle += alpha;
846
847         // Find the control anchor points from the angles and length
848         control1X = curX - control1Length * sin(control1Angle);
849         control1Y = curY + control1Length * cos(control1Angle);
850         control2X = curX + control2Length * sin(control2Angle);
851         control2Y = curY + control2Length * cos(control2Angle);
852
853         // One last adjustment, make sure that no control anchor point extends vertically past
854         // its target prev/next point, as that results in curves dipping above or below and
855         // bending back strangely. If we find this happening we keep the control angle but
856         // reduce the length of the control line so it stays within bounds.
857         if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
858             control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
859             control1Y = prevY;
860         }
861         if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
862             control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
863             control2Y = nextY;
864         }
865         
866         return {
867             x1: control1X,
868             y1: control1Y,
869             x2: control2X,
870             y2: control2Y
871         };
872     },
873
874     /* Smoothing function for a path.  Converts a path into cubic beziers.  Value defines the divider of the distance between points.
875      * Defaults to a value of 4.
876      */
877     smooth: function (originalPath, value) {
878         var path = this.path2curve(originalPath),
879             newp = [path[0]],
880             x = path[0][1],
881             y = path[0][2],
882             j,
883             points,
884             i = 1,
885             ii = path.length,
886             beg = 1,
887             mx = x,
888             my = y,
889             cx = 0,
890             cy = 0;
891         for (; i < ii; i++) {
892             var pathi = path[i],
893                 pathil = pathi.length,
894                 pathim = path[i - 1],
895                 pathiml = pathim.length,
896                 pathip = path[i + 1],
897                 pathipl = pathip && pathip.length;
898             if (pathi[0] == "M") {
899                 mx = pathi[1];
900                 my = pathi[2];
901                 j = i + 1;
902                 while (path[j][0] != "C") {
903                     j++;
904                 }
905                 cx = path[j][5];
906                 cy = path[j][6];
907                 newp.push(["M", mx, my]);
908                 beg = newp.length;
909                 x = mx;
910                 y = my;
911                 continue;
912             }
913             if (pathi[pathil - 2] == mx && pathi[pathil - 1] == my && (!pathip || pathip[0] == "M")) {
914                 var begl = newp[beg].length;
915                 points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], mx, my, newp[beg][begl - 2], newp[beg][begl - 1], value);
916                 newp[beg][1] = points.x2;
917                 newp[beg][2] = points.y2;
918             }
919             else if (!pathip || pathip[0] == "M") {
920                 points = {
921                     x1: pathi[pathil - 2],
922                     y1: pathi[pathil - 1]
923                 };
924             } else {
925                 points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], pathi[pathil - 2], pathi[pathil - 1], pathip[pathipl - 2], pathip[pathipl - 1], value);
926             }
927             newp.push(["C", x, y, points.x1, points.y1, pathi[pathil - 2], pathi[pathil - 1]]);
928             x = points.x2;
929             y = points.y2;
930         }
931         return newp;
932     },
933
934     findDotAtSegment: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
935         var t1 = 1 - t;
936         return {
937             x: Math.pow(t1, 3) * p1x + Math.pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + Math.pow(t, 3) * p2x,
938             y: Math.pow(t1, 3) * p1y + Math.pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + Math.pow(t, 3) * p2y
939         };
940     },
941
942     /**
943      * A utility method to deduce an appropriate tick configuration for the data set of given
944      * feature.
945      * 
946      * @param {Number/Date} from The minimum value in the data
947      * @param {Number/Date} to The maximum value in the data
948      * @param {Number} stepsMax The maximum number of ticks
949      * @return {Object} The calculated step and ends info; When `from` and `to` are Dates, refer to the
950      * return value of {@link #snapEndsByDate}. For numerical `from` and `to` the return value contains:
951      * @return {Number} return.from The result start value, which may be lower than the original start value
952      * @return {Number} return.to The result end value, which may be higher than the original end value
953      * @return {Number} return.power The calculate power.
954      * @return {Number} return.step The value size of each step
955      * @return {Number} return.steps The number of steps.
956      */
957     snapEnds: function (from, to, stepsMax) {
958         if (Ext.isDate(from)) {
959             return this.snapEndsByDate(from, to, stepsMax);
960         }
961         var step = (to - from) / stepsMax,
962             level = Math.floor(Math.log(step) / Math.LN10) + 1,
963             m = Math.pow(10, level),
964             cur,
965             modulo = Math.round((step % m) * Math.pow(10, 2 - level)),
966             interval = [[0, 15], [20, 4], [30, 2], [40, 4], [50, 9], [60, 4], [70, 2], [80, 4], [100, 15]],
967             stepCount = 0,
968             value,
969             weight,
970             i,
971             topValue,
972             topWeight = 1e9,
973             ln = interval.length;
974         cur = from = Math.floor(from / m) * m;
975         for (i = 0; i < ln; i++) {
976             value = interval[i][0];
977             weight = (value - modulo) < 0 ? 1e6 : (value - modulo) / interval[i][1];
978             if (weight < topWeight) {
979                 topValue = value;
980                 topWeight = weight;
981             }
982         }
983         step = Math.floor(step * Math.pow(10, -level)) * Math.pow(10, level) + topValue * Math.pow(10, level - 2);
984         while (cur < to) {
985             cur += step;
986             stepCount++;
987         }
988         to = +cur.toFixed(10);
989         return {
990             from: from,
991             to: to,
992             power: level,
993             step: step,
994             steps: stepCount
995         };
996     },
997
998     /**
999      * A utility method to deduce an appropriate tick configuration for the data set of given
1000      * feature when data is Dates. Refer to {@link #snapEnds} for numeric data.
1001      *
1002      * @param {Date} from The minimum value in the data
1003      * @param {Date} to The maximum value in the data
1004      * @param {Number} stepsMax The maximum number of ticks
1005      * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
1006      * and will not be adjusted
1007      * @return {Object} The calculated step and ends info; properties are:
1008      * @return {Date} return.from The result start value, which may be lower than the original start value
1009      * @return {Date} return.to The result end value, which may be higher than the original end value
1010      * @return {Number} return.step The value size of each step
1011      * @return {Number} return.steps The number of steps.
1012      * NOTE: the steps may not divide the from/to range perfectly evenly;
1013      * there may be a smaller distance between the last step and the end value than between prior
1014      * steps, particularly when the `endsLocked` param is true. Therefore it is best to not use
1015      * the `steps` result when finding the axis tick points, instead use the `step`, `to`, and
1016      * `from` to find the correct point for each tick.
1017      */
1018     snapEndsByDate: function (from, to, stepsMax, lockEnds) {
1019         var selectedStep = false, scales = [
1020                 [Ext.Date.MILLI, [1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500]],
1021                 [Ext.Date.SECOND, [1, 2, 3, 5, 10, 15, 30]],
1022                 [Ext.Date.MINUTE, [1, 2, 3, 5, 10, 20, 30]],
1023                 [Ext.Date.HOUR, [1, 2, 3, 4, 6, 12]],
1024                 [Ext.Date.DAY, [1, 2, 3, 7, 14]],
1025                 [Ext.Date.MONTH, [1, 2, 3, 4, 6]]
1026             ], j, yearDiff;
1027
1028         // Find the most desirable scale
1029         Ext.each(scales, function(scale, i) {
1030             for (j = 0; j < scale[1].length; j++) {
1031                 if (to < Ext.Date.add(from, scale[0], scale[1][j] * stepsMax)) {
1032                     selectedStep = [scale[0], scale[1][j]];
1033                     return false;
1034                 }
1035             }
1036         });
1037         if (!selectedStep) {
1038             yearDiff = this.snapEnds(from.getFullYear(), to.getFullYear() + 1, stepsMax, lockEnds);
1039             selectedStep = [Date.YEAR, Math.round(yearDiff.step)];
1040         }
1041         return this.snapEndsByDateAndStep(from, to, selectedStep, lockEnds);
1042     },
1043
1044
1045     /**
1046      * A utility method to deduce an appropriate tick configuration for the data set of given
1047      * feature and specific step size.
1048      * @param {Date} from The minimum value in the data
1049      * @param {Date} to The maximum value in the data
1050      * @param {Array} step An array with two components: The first is the unit of the step (day, month, year, etc).
1051      * The second one is the number of units for the step (1, 2, etc.).
1052      * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
1053      * and will not be adjusted
1054      * @return {Object} See the return value of {@link #snapEndsByDate}.
1055      */
1056     snapEndsByDateAndStep: function(from, to, step, lockEnds) {
1057         var fromStat = [from.getFullYear(), from.getMonth(), from.getDate(),
1058                 from.getHours(), from.getMinutes(), from.getSeconds(), from.getMilliseconds()],
1059             steps = 0, testFrom, testTo;
1060         if (lockEnds) {
1061             testFrom = from;
1062         } else {
1063             switch (step[0]) {
1064                 case Ext.Date.MILLI:
1065                     testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
1066                             fromStat[4], fromStat[5], Math.floor(fromStat[6] / step[1]) * step[1]);
1067                     break;
1068                 case Ext.Date.SECOND:
1069                     testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
1070                             fromStat[4], Math.floor(fromStat[5] / step[1]) * step[1], 0);
1071                     break;
1072                 case Ext.Date.MINUTE:
1073                     testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
1074                             Math.floor(fromStat[4] / step[1]) * step[1], 0, 0);
1075                     break;
1076                 case Ext.Date.HOUR:
1077                     testFrom = new Date(fromStat[0], fromStat[1], fromStat[2],
1078                             Math.floor(fromStat[3] / step[1]) * step[1], 0, 0, 0);
1079                     break;
1080                 case Ext.Date.DAY:
1081                     testFrom = new Date(fromStat[0], fromStat[1],
1082                             Math.floor(fromStat[2] - 1 / step[1]) * step[1] + 1, 0, 0, 0, 0);
1083                     break;
1084                 case Ext.Date.MONTH:
1085                     testFrom = new Date(fromStat[0], Math.floor(fromStat[1] / step[1]) * step[1], 1, 0, 0, 0, 0);
1086                     break;
1087                 default: // Ext.Date.YEAR
1088                     testFrom = new Date(Math.floor(fromStat[0] / step[1]) * step[1], 0, 1, 0, 0, 0, 0);
1089                     break;
1090             }
1091         }
1092
1093         testTo = testFrom;
1094         // TODO(zhangbei) : We can do it better somehow...
1095         while (testTo < to) {
1096             testTo = Ext.Date.add(testTo, step[0], step[1]);
1097             steps++;
1098         }
1099
1100         if (lockEnds) {
1101             testTo = to;
1102         }
1103         return {
1104             from : +testFrom,
1105             to : +testTo,
1106             step : (testTo - testFrom) / steps,
1107             steps : steps
1108         };
1109     },
1110
1111     sorter: function (a, b) {
1112         return a.offset - b.offset;
1113     },
1114
1115     rad: function(degrees) {
1116         return degrees % 360 * Math.PI / 180;
1117     },
1118
1119     degrees: function(radian) {
1120         return radian * 180 / Math.PI % 360;
1121     },
1122
1123     withinBox: function(x, y, bbox) {
1124         bbox = bbox || {};
1125         return (x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height));
1126     },
1127
1128     parseGradient: function(gradient) {
1129         var me = this,
1130             type = gradient.type || 'linear',
1131             angle = gradient.angle || 0,
1132             radian = me.radian,
1133             stops = gradient.stops,
1134             stopsArr = [],
1135             stop,
1136             vector,
1137             max,
1138             stopObj;
1139
1140         if (type == 'linear') {
1141             vector = [0, 0, Math.cos(angle * radian), Math.sin(angle * radian)];
1142             max = 1 / (Math.max(Math.abs(vector[2]), Math.abs(vector[3])) || 1);
1143             vector[2] *= max;
1144             vector[3] *= max;
1145             if (vector[2] < 0) {
1146                 vector[0] = -vector[2];
1147                 vector[2] = 0;
1148             }
1149             if (vector[3] < 0) {
1150                 vector[1] = -vector[3];
1151                 vector[3] = 0;
1152             }
1153         }
1154
1155         for (stop in stops) {
1156             if (stops.hasOwnProperty(stop) && me.stopsRE.test(stop)) {
1157                 stopObj = {
1158                     offset: parseInt(stop, 10),
1159                     color: Ext.draw.Color.toHex(stops[stop].color) || '#ffffff',
1160                     opacity: stops[stop].opacity || 1
1161                 };
1162                 stopsArr.push(stopObj);
1163             }
1164         }
1165         // Sort by pct property
1166         Ext.Array.sort(stopsArr, me.sorter);
1167         if (type == 'linear') {
1168             return {
1169                 id: gradient.id,
1170                 type: type,
1171                 vector: vector,
1172                 stops: stopsArr
1173             };
1174         }
1175         else {
1176             return {
1177                 id: gradient.id,
1178                 type: type,
1179                 centerX: gradient.centerX,
1180                 centerY: gradient.centerY,
1181                 focalX: gradient.focalX,
1182                 focalY: gradient.focalY,
1183                 radius: gradient.radius,
1184                 vector: vector,
1185                 stops: stopsArr
1186             };
1187         }
1188     }
1189 });
1190
1191