PageSourceSearch

https://roomstyler.com/assets/austin/merged_austin-d5758d68250b146…0f500a2ff8fcd8f807ebe47058327d5c94398db7.js

js roomstyler.com collected 2026-09-24 08:50:39 UTC 1,341,319 bytes, 46,356 lines download raw bytes

1(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jade = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
2'use strict';
3
4/**
5 * Merge two attribute objects giving precedence
6 * to values in object `b`. Classes are special-cased
7 * allowing for arrays and merging/joining appropriately
8 * resulting in a string.
9 *
10 * @param {Object} a
11 * @param {Object} b
12 * @return {Object} a
13 * @api private
14 */
15
16exports.merge = function merge(a, b) {
17  if (arguments.length === 1) {
18    var attrs = a[0];
19    for (var i = 1; i < a.length; i++) {
20      attrs = merge(attrs, a[i]);
21    }
22    return attrs;
23  }
24  var ac = a['class'];
25  var bc = b['class'];
26
27  if (ac || bc) {
28    ac = ac || [];
29    bc = bc || [];
30    if (!Array.isArray(ac)) ac = [ac];
31    if (!Array.isArray(bc)) bc = [bc];
32    a['class'] = ac.concat(bc).filter(nulls);
33  }
34
35  for (var key in b) {
36    if (key != 'class') {
37      a[key] = b[key];
38    }
39  }
40
41  return a;
42};
43
44/**
45 * Filter null `val`s.
46 *
47 * @param {*} val
48 * @return {Boolean}
49 * @api private
50 */
51
52function nulls(val) {
53  return val != null && val !== '';
54}
55
56/**
57 * join array as classes.
58 *
59 * @param {*} val
60 * @return {String}
61 */
62exports.joinClasses = joinClasses;
63function joinClasses(val) {
64  return (Array.isArray(val) ? val.map(joinClasses) :
65    (val && typeof val === 'object') ? Object.keys(val).filter(function (key) { return val[key]; }) :
66    [val]).filter(nulls).join(' ');
67}
68
69/**
70 * Render the given classes.
71 *
72 * @param {Array} classes
73 * @param {Array.<Boolean>} escaped
74 * @return {String}
75 */
76exports.cls = function cls(classes, escaped) {
77  var buf = [];
78  for (var i = 0; i < classes.length; i++) {
79    if (escaped && escaped[i]) {
80      buf.push(exports.escape(joinClasses([classes[i]])));
81    } else {
82      buf.push(joinClasses(classes[i]));
83    }
84  }
85  var text = joinClasses(buf);
86  if (text.length) {
87    return ' class="' + text + '"';
88  } else {
89    return '';
90  }
91};
92
93
94exports.style = function (val) {
95  if (val && typeof val === 'object') {
96    return Object.keys(val).map(function (style) {
97      return style + ':' + val[style];
98    }).join(';');
99  } else {
100    return val;
101  }
102};
103/**
104 * Render the given attribute.
105 *
106 * @param {String} key
107 * @param {String} val
108 * @param {Boolean} escaped
109 * @param {Boolean} terse
110 * @return {String}
111 */
112exports.attr = function attr(key, val, escaped, terse) {
113  if (key === 'style') {
114    val = exports.style(val);
115  }
116  if ('boolean' == typeof val || null == val) {
117    if (val) {
118      return ' ' + (terse ? key : key + '="' + key + '"');
119    } else {
120      return '';
121    }
122  } else if (0 == key.indexOf('data') && 'string' != typeof val) {
123    if (JSON.stringify(val).indexOf('&') !== -1) {
124      console.warn('Since Jade 2.0.0, ampersands (`&`) in data attributes ' +
125                   'will be escaped to `&amp;`');
126    };
127    if (val && typeof val.toISOString === 'function') {
128      console.warn('Jade will eliminate the double quotes around dates in ' +
129                   'ISO form after 2.0.0');
130    }
131    return ' ' + key + "='" + JSON.stringify(val).replace(/'/g, '&apos;') + "'";
132  } else if (escaped) {
133    if (val && typeof val.toISOString === 'function') {
134      console.warn('Jade will stringify dates in ISO form after 2.0.0');
135    }
136    return ' ' + key + '="' + exports.escape(val) + '"';
137  } else {
138    if (val && typeof val.toISOString === 'function') {
139      console.warn('Jade will stringify dates in ISO form after 2.0.0');
140    }
141    return ' ' + key + '="' + val + '"';
142  }
143};
144
145/**
146 * Render the given attributes object.
147 *
148 * @param {Object} obj
149 * @param {Object} escaped
150 * @return {String}
151 */
152exports.attrs = function attrs(obj, terse){
153  var buf = [];
154
155  var keys = Object.keys(obj);
156
157  if (keys.length) {
158    for (var i = 0; i < keys.length; ++i) {
159      var key = keys[i]
160        , val = obj[key];
161
162      if ('class' == key) {
163        if (val = joinClasses(val)) {
164          buf.push(' ' + key + '="' + val + '"');
165        }
166      } else {
167        buf.push(exports.attr(key, val, false, terse));
168      }
169    }
170  }
171
172  return buf.join('');
173};
174
175/**
176 * Escape the given string of `html`.
177 *
178 * @param {String} html
179 * @return {String}
180 * @api private
181 */
182
183var jade_encode_html_rules = {
184  '&': '&amp;',
185  '<': '&lt;',
186  '>': '&gt;',
187  '"': '&quot;'
188};
189var jade_match_html = /[&<>"]/g;
190
191function jade_encode_char(c) {
192  return jade_encode_html_rules[c] || c;
193}
194
195exports.escape = jade_escape;
196function jade_escape(html){
197  var result = String(html).replace(jade_match_html, jade_encode_char);
198  if (result === '' + html) return html;
199  else return result;
200};
201
202/**
203 * Re-throw the given `err` in context to the
204 * the jade in `filename` at the given `lineno`.
205 *
206 * @param {Error} err
207 * @param {String} filename
208 * @param {String} lineno
209 * @api private
210 */
211
212exports.rethrow = function rethrow(err, filename, lineno, str){
213  if (!(err instanceof Error)) throw err;
214  if ((typeof window != 'undefined' || !filename) && !str) {
215    err.message += ' on line ' + lineno;
216    throw err;
217  }
218  try {
219    str = str || require('fs').readFileSync(filename, 'utf8')
220  } catch (ex) {
221    rethrow(err, null, lineno)
222  }
223  var context = 3
224    , lines = str.split('\n')
225    , start = Math.max(lineno - context, 0)
226    , end = Math.min(lines.length, lineno + context);
227
228  // Error context
229  var context = lines.slice(start, end).map(function(line, i){
230    var curr = i + start + 1;
231    return (curr == lineno ? '  > ' : '    ')
232      + curr
233      + '| '
234      + line;
235  }).join('\n');
236
237  // Alter exception message
238  err.path = filename;
239  err.message = (filename || 'Jade') + ':' + lineno
240    + '\n' + context + '\n\n' + err.message;
241  throw err;
242};
243
244exports.DebugItem = function DebugItem(lineno, filename) {
245  this.lineno = lineno;
246  this.filename = filename;
247}
248
249},{"fs":2}],2:[function(require,module,exports){
250
251},{}]},{},[1])(1)
252});
253/**
254 * SWFObject v1.5: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
255 *
256 * SWFObject is (c) 2007 Geoff Stearns and is released under the MIT License:
257 * http://www.opensource.org/licenses/mit-license.php
258 *
259 */
260if(typeof deconcept=="undefined"){var deconcept=new Object();}if(typeof deconcept.util=="undefined"){deconcept.util=new Object();}if(typeof deconcept.SWFObjectUtil=="undefined"){deconcept.SWFObjectUtil=new Object();}deconcept.SWFObject=function(_1,id,w,h,_5,c,_7,_8,_9,_a){if(!document.getElementById){return;}this.DETECT_KEY=_a?_a:"detectflash";this.skipDetect=deconcept.util.getRequestParameter(this.DETECT_KEY);this.params=new Object();this.variables=new Object();this.attributes=new Array();if(_1){this.setAttribute("swf",_1);}if(id){this.setAttribute("id",id);}if(w){this.setAttribute("width",w);}if(h){this.setAttribute("height",h);}if(_5){this.setAttribute("version",new deconcept.PlayerVersion(_5.toString().split(".")));}this.installedVer=deconcept.SWFObjectUtil.getPlayerVersion();if(!window.opera&&document.all&&this.installedVer.major>7){deconcept.SWFObject.doPrepUnload=true;}if(c){this.addParam("bgcolor",c);}var q=_7?_7:"high";this.addParam("quality",q);this.setAttribute("useExpressInstall",false);this.setAttribute("doExpressInstall",false);var _c=(_8)?_8:window.location;this.setAttribute("xiRedirectUrl",_c);this.setAttribute("redirectUrl","");if(_9){this.setAttribute("redirectUrl",_9);}};deconcept.SWFObject.prototype={useExpressInstall:function(_d){this.xiSWFPath=!_d?"expressinstall.swf":_d;this.setAttribute("useExpressInstall",true);},setAttribute:function(_e,_f){this.attributes[_e]=_f;},getAttribute:function(_10){return this.attributes[_10];},addParam:function(_11,_12){this.params[_11]=_12;},getParams:function(){return this.params;},addVariable:function(_13,_14){this.variables[_13]=_14;},getVariable:function(_15){return this.variables[_15];},getVariables:function(){return this.variables;},getVariablePairs:function(){var _16=new Array();var key;var _18=this.getVariables();for(key in _18){_16[_16.length]=key+"="+_18[key];}return _16;},getSWFHTML:function(){var _19="";if(navigator.plugins&&navigator.mimeTypes&&navigator.mimeTypes.length){if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","PlugIn");this.setAttribute("swf",this.xiSWFPath);}_19="<embed type=\"application/x-shockwave-flash\" src=\""+this.getAttribute("swf")+"\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\"";_19+=" id=\""+this.getAttribute("id")+"\" name=\""+this.getAttribute("id")+"\" ";var _1a=this.getParams();for(var key in _1a){_19+=[key]+"=\""+_1a[key]+"\" ";}var _1c=this.getVariablePairs().join("&");if(_1c.length>0){_19+="flashvars=\""+_1c+"\"";}_19+="/>";}else{if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","ActiveX");this.setAttribute("swf",this.xiSWFPath);}_19="<object id=\""+this.getAttribute("id")+"\" classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\">";_19+="<param name=\"movie\" value=\""+this.getAttribute("swf")+"\" />";var _1d=this.getParams();for(var key in _1d){_19+="<param name=\""+key+"\" value=\""+_1d[key]+"\" />";}var _1f=this.getVariablePairs().join("&");if(_1f.length>0){_19+="<param name=\"flashvars\" value=\""+_1f+"\" />";}_19+="</object>";}return _19;},write:function(_20){if(this.getAttribute("useExpressInstall")){var _21=new deconcept.PlayerVersion([6,0,65]);if(this.installedVer.versionIsValid(_21)&&!this.installedVer.versionIsValid(this.getAttribute("version"))){this.setAttribute("doExpressInstall",true);this.addVariable("MMredirectURL",escape(this.getAttribute("xiRedirectUrl")));document.title=document.title.slice(0,47)+" - Flash Player Installation";this.addVariable("MMdoctitle",document.title);}}if(this.skipDetect||this.getAttribute("doExpressInstall")||this.installedVer.versionIsValid(this.getAttribute("version"))){var n=(typeof _20=="string")?document.getElementById(_20):_20;n.innerHTML=this.getSWFHTML();return true;}else{if(this.getAttribute("redirectUrl")!=""){document.location.replace(this.getAttribute("redirectUrl"));}}return false;}};deconcept.SWFObjectUtil.getPlayerVersion=function(){var _23=new deconcept.PlayerVersion([0,0,0]);if(navigator.plugins&&navigator.mimeTypes.length){var x=navigator.plugins["Shockwave Flash"];if(x&&x.description){_23=new deconcept.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/,"").replace(/(\s+r|\s+b[0-9]+)/,".").split("."));}}else{if(navigator.userAgent&&navigator.userAgent.indexOf("Windows CE")>=0){var axo=1;var _26=3;while(axo){try{_26++;axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+_26);_23=new deconcept.PlayerVersion([_26,0,0]);}catch(e){axo=null;}}}else{try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");_23=new deconcept.PlayerVersion([6,0,21]);axo.AllowScriptAccess="always";}catch(e){if(_23.major==6){return _23;}}try{axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(e){}}if(axo!=null){_23=new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));}}}return _23;};deconcept.PlayerVersion=function(_29){this.major=_29[0]!=null?parseInt(_29[0]):0;this.minor=_29[1]!=null?parseInt(_29[1]):0;this.rev=_29[2]!=null?parseInt(_29[2]):0;};deconcept.PlayerVersion.prototype.versionIsValid=function(fv){if(this.major<fv.major){return false;}if(this.major>fv.major){return true;}if(this.minor<fv.minor){return false;}if(this.minor>fv.minor){return true;}if(this.rev<fv.rev){return false;}return true;};deconcept.util={getRequestParameter:function(_2b){var q=document.location.search||document.location.hash;if(_2b==null){return q;}if(q){var _2d=q.substring(1).split("&");for(var i=0;i<_2d.length;i++){if(_2d[i].substring(0,_2d[i].indexOf("="))==_2b){return _2d[i].substring((_2d[i].indexOf("=")+1));}}}return "";}};deconcept.SWFObjectUtil.cleanupSWFs=function(){var _2f=document.getElementsByTagName("OBJECT");for(var i=_2f.length-1;i>=0;i--){_2f[i].style.display="none";
260for(var x in _2f[i]){if(typeof _2f[i][x]=="function"){_2f[i][x]=function(){};}}}};if(deconcept.SWFObject.doPrepUnload){if(!deconcept.unloadSet){deconcept.SWFObjectUtil.prepUnload=function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};window.attachEvent("onunload",deconcept.SWFObjectUtil.cleanupSWFs);};window.attachEvent("onbeforeunload",deconcept.SWFObjectUtil.prepUnload);deconcept.unloadSet=true;}}if(!document.getElementById&&document.all){document.getElementById=function(id){return document.all[id];};}var getQueryParamValue=deconcept.util.getRequestParameter;var FlashObject=deconcept.SWFObject;var SWFObject=deconcept.SWFObject;
261//
262// Copyright (c) 2008, 2009 Paul Duncan ([email protected])
263//
264// Permission is hereby granted, free of charge, to any person obtaining a copy
265// of this software and associated documentation files (the "Software"), to deal
266// in the Software without restriction, including without limitation the rights
267// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
268// copies of the Software, and to permit persons to whom the Software is
269// furnished to do so, subject to the following conditions:
270//
271// The above copyright notice and this permission notice shall be included in
272// all copies or substantial portions of the Software.
273//
274// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
275// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
276// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
277// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
278// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
279// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
280// THE SOFTWARE.
281//
282
283
284/*
285 * The contents of gears_init.js; we need this because Chrome supports
286 * Gears out of the box, but still requires this constructor.  Note that
287 * if you include gears_init.js then this function does nothing.
288 */
289(function() {
290  // We are already defined. Hooray!
291  if (window.google && google.gears)
292    return;
293
294  // factory
295  var F = null;
296
297  // Firefox
298  if (typeof GearsFactory != 'undefined') {
299    F = new GearsFactory();
300  } else {
301    // IE
302    try {
303      F = new ActiveXObject('Gears.Factory');
304      // privateSetGlobalObject is only required and supported on WinCE.
305      if (F.getBuildInfo().indexOf('ie_mobile') != -1)
306        F.privateSetGlobalObject(this);
307    } catch (e) {
308      // Safari
309      if ((typeof navigator.mimeTypes != 'undefined')
310           && navigator.mimeTypes["application/x-googlegears"]) {
311        F = document.createElement("object");
312        F.style.display = "none";
313        F.width = 0;
314        F.height = 0;
315        F.type = "application/x-googlegears";
316        document.documentElement.appendChild(F);
317      }
318    }
319  }
320
321  // *Do not* define any objects if Gears is not installed. This mimics the
322  // behavior of Gears defining the objects in the future.
323  if (!F)
324    return;
325
326  // Now set up the objects, being careful not to overwrite anything.
327  //
328  // Note: In Internet Explorer for Windows Mobile, you can't add properties to
329  // the window object. However, global objects are automatically added as
330  // properties of the window object in all browsers.
331  if (!window.google)
332    google = {};
333
334  if (!google.gears)
335    google.gears = {factory: F};
336})();
337
338/**
339 * Persist - top-level namespace for Persist library.
340 * @namespace
341 */
342Persist = (function() {
343  var VERSION = '0.2.0', P, B, C, esc, init, empty, ec;
344
345  // wrapper for Array.prototype.indexOf, since IE doesn't have it
346  var index_of = (function() {
347    if (Array.prototype.indexOf)
348      return function(ary, val) {
349        return Array.prototype.indexOf.call(ary, val);
350      };
351    else
352      return function(ary, val) {
353        var i, l;
354
355        for (i = 0, l = ary.length; i < l; i++)
356          if (ary[i] == val)
357            return i;
358
359        return -1;
360      };
361  })();
362
363
364  // empty function
365  empty = function() { };
366
367  /**
368   * Escape spaces and underscores in name.  Used to generate a "safe"
369   * key from a name.
370   *
371   * @private
372   */
373  esc = function(str) {
374    return 'PS' + str.replace(/_/g, '__').replace(/ /g, '_s');
375  };
376
377  C = {
378    /*
379     * Backend search order.
380     *
381     * Note that the search order is significant; the backends are
382     * listed in order of capacity, and many browsers
383     * support multiple backends, so changing the search order could
384     * result in a browser choosing a less capable backend.
385     */
386    search_order: [
387//      'gears',
388      'localstorage',
389      'globalstorage',
390      'whatwg_db',
391      'flash'
392    ],
393    
394    // default flash configuration
395    flash: {
396      // ID of wrapper element
397      div_id:   '_persist_flash_wrap',
398
399      // id of flash object/embed
400      id:       '_persist_flash',
401
402      // default path to flash object
403      path: 'persist.swf',
404      size: { w:1, h:1 },
405
406      // arguments passed to flash object
407      args: {
408        autostart: true
409      }
410    },
411
412    // valid name regular expression
413    name_re: /^[a-z][a-z0-9_ -]+$/i,
414
415    // list of backend methods
416    methods: [
417      'init',
418      'get',
419      'set',
420      'remove',
421      'load',
422      'save'
423      // TODO: clear method?
424    ],
425
426    // sql for db backends (gears and db)
427    sql: {
428      version:  '1', // db schema version
429
430      // XXX: the "IF NOT EXISTS" is a sqlite-ism; fortunately all the
431      // known DB implementations (safari and gears) use sqlite
432      create:   "CREATE TABLE IF NOT EXISTS persist_data (k TEXT UNIQUE NOT NULL PRIMARY KEY, v TEXT NOT NULL)",
433      get:      "SELECT v FROM persist_data WHERE k = ?",
434      set:      "INSERT INTO persist_data(k, v) VALUES (?, ?)",
435      remove:   "DELETE FROM persist_data WHERE k = ?"
436    }
437  };
438
439  // built-in backends
440  B = {
441    // gears db backend
442    // (src: http://code.google.com/apis/gears/api_database.html)
443    gears: {
444      // no known limit
445      size:   -1,
446
447      test: function() {
448        // test for gears
449        return (window.google && window.google.gears) ? true : false;
450      },
451
452      methods: {
453        transaction: function(fn) {
454          var db = this.db;
455
456          // begin transaction
457          db.execute('BEGIN').close();
458
459          // call callback fn
460          fn.call(this, db);
461
462          // commit changes
463          db.execute('COMMIT').close();
464        },
465
466        init: function() {
467          var db;
468
469          // create database handle (TODO: add schema version?)
470          db = this.db = google.gears.factory.create('beta.database');
471
472          // open database
473          // from gears ref:
474          //
475          // Currently the name, if supplied and of length greater than
476          // zero, must consist only of visible ASCII characters
477          // excluding the following characters:
478          //
479          //   / \ : * ? " < > | ; ,
480          //
481          // (this constraint is enforced in the Store constructor)
482          db.open(esc(this.name));
483
484          // create table
485          db.execute(C.sql.create).close();
486        },
487
488        get: function(key, fn, scope) {
489          var r, sql = C.sql.get;
490
491          // if callback isn't defined, then return
492          if (!fn)
493            return;
494
495          // begin transaction
496          this.transaction(function (t) {
497            var is_valid, val;
498            // exec query
499            r = t.execute(sql, [key]);
500
501            // check result and get value
502            is_valid = r.isValidRow();
503            val = is_valid ? r.field(0) : null;
504
505            // close result set
506            r.close();
507
508            // call callback
509            fn.call(scope || this, is_valid, val);
510          });
511        },
512
513        set: function(key, val, fn, scope) {
514          var rm_sql = C.sql.remove,
515              sql    = C.sql.set, r;
516
517          // begin set transaction
518          this.transaction(function(t) {
519            // exec remove query
520            t.execute(rm_sql, [key]).close();
521
522            // exec set query
523            t.execute(sql, [key, val]).close();
524
525            // run callback (TODO: get old value)
526            if (fn)
527              fn.call(scope || this, true, val);
528          });
529        },
530
531        remove: function(key, fn, scope) {
532          var get_sql = C.sql.get,
533              sql = C.sql.remove,
534              r, val = null, is_valid = false;
535
536          // begin remove transaction
537          this.transaction(function(t) {
538            // if a callback was defined, then get the old
539            // value before removing it
540            if (fn) {
541              // exec get query
542              r = t.execute(get_sql, [key]);
543
544              // check validity and get value
545              is_valid = r.isValidRow();
546              val = is_valid ? r.field(0) : null;
547
548              // close result set
549              r.close();
550            }
551
552            // exec remove query if no callback was defined, or if a
553            // callback was defined and there was an existing value
554            if (!fn || is_valid) {
555              // exec remove query
556              t.execute(sql, [key]).close();
557            }
558
559            // exec callback
560            if (fn)
561              fn.call(scope || this, is_valid, val);
562          });
563        }
564      }
565    },
566
567    // whatwg db backend (webkit, Safari 3.1+)
568    // (src: whatwg and http://webkit.org/misc/DatabaseExample.html)
569    whatwg_db: {
570      // size based on DatabaseExample from above (should I increase
571      // this?)
572      size:   200 * 1024,
573
574      test: function() {
575        var name = 'PersistJS Test',
576            desc = 'Persistent database test.';
577
578        // test for openDatabase
579        if (!window.openDatabase)
580          return false;
581
582        // make sure openDatabase works
583        // XXX: will this leak a db handle and/or waste space?
584        if (!window.openDatabase(name, C.sql.version, desc, B.whatwg_db.size))
585          return false;
586
587        // return true
588        return true;
589      },
590
591      methods: {
592        transaction: function(fn) {
593          // lazy create database table;
594          // this is done here because there is no way to
595          // prevent a race condition if the table is created in init()
596          if (!this.db_created) {
597            this.db.transaction(function(t) {
598              // create table
599              t.executeSql(C.sql.create, [], function() {
600                this.db_created = true;
601              });
602            }, empty); // trap exception
603          }
604
605          // execute transaction
606          this.db.transaction(fn);
607        },
608
609        init: function() {
610          // create database handle
611          this.db = openDatabase(
612            this.name,
613            C.sql.version,
614            this.o.about || ("Persistent storage for " + this.name),
615            this.o.size || B.whatwg_db.size
616          );
617        },
618
619        get: function(key, fn, scope) {
620          var sql = C.sql.get;
621
622          // if callback isn't defined, then return
623          if (!fn)
624            return;
625
626          // get callback scope
627          scope = scope || this;
628
629          // begin transaction
630          this.transaction(function (t) {
631            t.executeSql(sql, [key], function(t, r) {
632              if (r.rows.length > 0)
633                fn.call(scope, true, r.rows.item(0)['v']);
634              else
635                fn.call(scope, false, null);
636            });
637          });
638        },
639
640        set: function(key, val, fn, scope) {
641          var rm_sql = C.sql.remove,
642              sql    = C.sql.set;
643
644          // begin set transaction
645          this.transaction(function(t) {
646            // exec remove query
647            t.executeSql(rm_sql, [key], function() {
648              // exec set query
649              t.executeSql(sql, [key, val], function(t, r) {
650                // run callback
651                if (fn)
652                  fn.call(scope || this, true, val);
653              });
654            });
655          });
656
657          return val;
658        },
659
660        // begin remove transaction
661        remove: function(key, fn, scope) {
662          var get_sql = C.sql.get,
663              sql = C.sql.remove;
664
665          this.transaction(function(t) {
666            // if a callback was defined, then get the old
667            // value before removing it
668            if (fn) {
669              // exec get query
670              t.executeSql(get_sql, [key], function(t, r) {
671                if (r.rows.length > 0) {
672                  // key exists, get value
673                  var val = r.rows.item(0)['v'];
674
675                  // exec remove query
676                  t.executeSql(sql, [key], function(t, r) {
677                    // exec callback
678                    fn.call(scope || this, true, val);
679                  });
680                } else {
681                  // key does not exist, exec callback
682                  fn.call(scope || this, false, null);
683                }
684              });
685            } else {
686              // no callback was defined, so just remove the
687              // data without checking the old value
688
689              // exec remove query
690              t.executeSql(sql, [key]);
691            }
692          });
693        }
694      }
695    },
696
697    // globalstorage backend (globalStorage, FF2+, IE8+)
698    // (src: http://developer.mozilla.org/en/docs/DOM:Storage#globalStorage)
699    // https://developer.mozilla.org/En/DOM/Storage
700    //
701    // TODO: test to see if IE8 uses object literal semantics or
702    // getItem/setItem/removeItem semantics
703    globalstorage: {
704      // (5 meg limit, src: http://ejohn.org/blog/dom-storage-answers/)
705      size: 5 * 1024 * 1024,
706
707      test: function() {
708        try {
709          return !!window.globalStorage[location.hostname];
710        } catch (e) {
711          return false;
712        }
713      },
714
715      methods: {
716        key: function(key) {
717          return esc(this.name) + esc(key);
718        },
719
720        init: function() {
721          this.store = globalStorage[this.o.domain];
722        },
723
724        get: function(key, fn, scope) {
725          // expand key
726          key = this.key(key);
727          
728          var item = this.store.getItem(key),
729              val;
730          // FF3.0.x returns an object {security:undefined,value:_val_}
731          // and can raise security error.
732          try {
733	          if (typeof item == 'object' && item != null) {
734	            val = item.value;
735	          } else {
736	           val = item;
737	          }
738          } catch (e) {
739            val = null;
740          }
741          
742          if (val == 'null') val = null;
743
744          if (fn)
745            fn.call(scope || this, true, val);
746        },
747
748        set: function(key, val, fn, scope) {
749          // expand key
750        
751          key = this.key(key);
752
753          // set value
754          this.store.setItem(key, val);
755
756          if (fn)
757            fn.call(scope || this, true, val);
758        },
759
760        remove: function(key, fn, scope) {
761          var val;
762
763          // expand key
764          key = this.key(key);
765
766          // get value
767          val = this.store[key];
768
769          // delete value
770          this.store.removeItem(key);
771
772          if (fn)
773            fn.call(scope || this, (val !== null), val);
774        }
775      }
776    },
777
778    // localstorage backend (globalStorage, FF2+, IE8+)
779    // (src: http://www.whatwg.org/specs/web-apps/current-work/#the-localstorage)
780    // also http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx#_global
781    localstorage: {
782      // (unknown?)
783      // ie has the remainingSpace property, see:
784      // http://msdn.microsoft.com/en-us/library/cc197016(VS.85).aspx
785      size: -1,
786
787      test: function() {
788        try {
789          return !!window.localStorage;
790        } catch (e) {
791          return false;
792        }
793      },
794
795      methods: {
796        key: function(key) {
797          return esc(this.name) + esc(key);
798        },
799
800        init: function() {
801          this.store = localStorage;
802        },
803
804        get: function(key, fn, scope) {
805          // expand key
806          key = this.key(key);
807          var val = this.store.getItem(key);
808          
809          if (val == 'null') val = null;
810
811          if (fn)
812            fn.call(scope || this, true, val);
813        },
814
815        set: function(key, val, fn, scope) {
816          // expand key
817          key = this.key(key);
818
819          // set value
820          this.store.setItem(key, val);
821
822          if (fn)
823            fn.call(scope || this, true, val);
824        },
825
826        remove: function(key, fn, scope) {
827          var val;
828
829          // expand key
830          key = this.key(key);
831
832          // get value
833          val = this.store.getItem(key);
834
835          // delete value
836          this.store.removeItem(key);
837
838          if (fn)
839            fn.call(scope || this, (val !== null), val);
840        }
841      }
842    },
843    // flash backend (requires flash 8 or newer)
844    // http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_16194&sliceId=1
845    // http://livedocs.adobe.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002200.html
846    flash: {
847      test: function() {
848        // TODO: better flash detection
849        if (!deconcept || !deconcept.SWFObjectUtil)
850          return false;
851
852        // get the major version
853        var major = deconcept.SWFObjectUtil.getPlayerVersion().major;
854
855        // check flash version (require 8.0 or newer)
856        return (major >= 8) ? true : false;
857      },
858
859      methods: {
860        init: function() {
861          if (!B.flash.el) {
862            var o, key, el, cfg = C.flash;
863
864            // create wrapper element
865            el = document.createElement('div');
866            el.id = cfg.div_id;
867
868            // FIXME: hide flash element
869            // el.style.display = 'none';
870
871            // append element to body
872            document.body.appendChild(el);
873
874            // create new swf object
875            o = new deconcept.SWFObject(this.o.swf_path || cfg.path, cfg.id, cfg.size.w, cfg.size.h, '8');
876
877            // set parameters
878            for (key in cfg.args)
879              o.addVariable(key, cfg.args[key]);
880
881            // write flash object
882            o.write(el);
883
884            // save flash element
885            B.flash.el = document.getElementById(cfg.id);
886          }
887
888          // use singleton flash element
889          this.el = B.flash.el;
890        },
891
892        get: function(key, fn, scope) {
893          var val;
894
895          // escape key
896          key = esc(key);
897
898          // get value
899          try {
900            val = this.el.get(this.name, key);
901          } catch (e) {
902            val = null;
903          }
904          
905          if (val == 'null') val = null;
906
907          // call handler
908          if (fn)
909            fn.call(scope || this, val !== null, val);
910        },
911
912        set: function(key, val, fn, scope) {
913          var old_val;
914
915          // escape key
916          key = esc(key);
917
918          // set value
919          old_val = this.el.set(this.name, key, val);
920
921          // call handler
922          if (fn)
923            fn.call(scope || this, true, val);
924        },
925
926        remove: function(key, fn, scope) {
927          var val;
928
929          // get key
930          key = esc(key);
931
932          // remove old value
933          val = this.el.remove(this.name, key);
934
935          // call handler
936          if (fn)
937            fn.call(scope || this, true, val);
938        }
939      }
940    }
941    
942  };
943
944  /**
945   * Test for available backends and pick the best one.
946   * @private
947   */
948  var init = function() {
949    var i, l, b, key, fns = C.methods, keys = C.search_order;
950
951    // set all functions to the empty function
952    for (i = 0, l = fns.length; i < l; i++)
953      P.Store.prototype[fns[i]] = empty;
954
955    // clear type and size
956    P.type = null;
957    P.size = -1;
958
959    // loop over all backends and test for each one
960    for (i = 0, l = keys.length; !P.type && i < l; i++) {
961      b = B[keys[i]];
962
963      // test for backend
964      if (b.test()) {
965        // found backend, save type and size
966        P.type = keys[i];
967        P.size = b.size;
968
969        // extend store prototype with backend methods
970        for (key in b.methods)
971          P.Store.prototype[key] = b.methods[key];
972      }
973    }
974
975    // mark library as initialized
976    P._init = true;
977  };
978
979  // create top-level namespace
980  P = {
981    // version of persist library
982    VERSION: VERSION,
983
984    // backend type and size limit
985    type: null,
986    size: 0,
987
988    // XXX: expose init function?
989    // init: init,
990
991    add: function(o) {
992      // add to backend hash
993      B[o.id] = o;
994
995      // add backend to front of search order
996      C.search_order = [o.id].concat(C.search_order);
997
998      // re-initialize library
999      init();
1000    },
1001
1002    remove: function(id) {
1003      var ofs = index_of(C.search_order, id);
1004      if (ofs < 0)
1005        return;
1006
1007      // remove from search order
1008      C.search_order.splice(ofs, 1);
1009
1010      // delete from lut
1011      delete B[id];
1012
1013      // re-initialize library
1014      init();
1015    },
1016
1017    // store API
1018    Store: function(name, o) {
1019      // verify name
1020      if (!C.name_re.exec(name))
1021        throw new Error("Invalid name");
1022
1023      // XXX: should we lazy-load type?
1024      // if (!P._init)
1025      //   init();
1026      
1027      // No suitable storage found
1028      if (!P.type) return false; //throw new Error("No suitable storage found");
1029
1030      o = o || {};
1031      this.name = name;
1032
1033      // get domain (XXX: does this localdomain fix work?)
1034      o.domain = o.domain || location.host || 'localhost';
1035
1036      // strip port from domain (XXX: will this break ipv6?)
1037      o.domain = o.domain.replace(/:\d+$/, '');
1038
1039      // append localdomain to domains w/o '."
1040      // (see https://bugzilla.mozilla.org/show_bug.cgi?id=357323)
1041      // (file://localhost/ works, see:
1042      // https://bugzilla.mozilla.org/show_bug.cgi?id=469192)
1043/*
1044 *       if (!o.domain.match(/\./))
1045 *         o.domain += '.localdomain';
1046 */
1047
1048      this.o = o;
1049
1050      // expires in 2 years
1051      o.expires = o.expires || 365 * 2;
1052
1053      // set path to root
1054      o.path = o.path || '/';
1055
1056      // call init function
1057      this.init();
1058    }
1059  };
1060
1061  // init persist
1062  init();
1063
1064  // return top-level namespace
1065  return P;
1066})();
1067/*!
1068 * jQuery JavaScript Library v1.5.2
1069 * http://jquery.com/
1070 *
1071 * Copyright 2011, John Resig
1072 * Dual licensed under the MIT or GPL Version 2 licenses.
1073 * http://jquery.org/license
1074 *
1075 * Includes Sizzle.js
1076 * http://sizzlejs.com/
1077 * Copyright 2011, The Dojo Foundation
1078 * Released under the MIT, BSD, and GPL Licenses.
1079 *
1080 * Date: Thu Mar 31 15:28:23 2011 -0400
1081 */
1082(function( window, undefined ) {
1083
1084// Use the correct document accordingly with window argument (sandbox)
1085var document = window.document;
1086var jQuery = (function() {
1087
1088// Define a local copy of jQuery
1089var jQuery = function( selector, context ) {
1090		// The jQuery object is actually just the init constructor 'enhanced'
1091		return new jQuery.fn.init( selector, context, rootjQuery );
1092	},
1093
1094	// Map over jQuery in case of overwrite
1095	_jQuery = window.jQuery,
1096
1097	// Map over the $ in case of overwrite
1098	_$ = window.$,
1099
1100	// A central reference to the root jQuery(document)
1101	rootjQuery,
1102
1103	// A simple way to check for HTML strings or ID strings
1104	// (both of which we optimize for)
1105	quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,
1106
1107	// Check if a string has a non-whitespace character in it
1108	rnotwhite = /\S/,
1109
1110	// Used for trimming whitespace
1111	trimLeft = /^\s+/,
1112	trimRight = /\s+$/,
1113
1114	// Check for digits
1115	rdigit = /\d/,
1116
1117	// Match a standalone tag
1118	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
1119
1120	// JSON RegExp
1121	rvalidchars = /^[\],:{}\s]*$/,
1122	rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
1123	rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
1124	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
1125
1126	// Useragent RegExp
1127	rwebkit = /(webkit)[ \/]([\w.]+)/,
1128	ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
1129	rmsie = /(msie) ([\w.]+)/,
1130	rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
1131
1132	// Keep a UserAgent string for use with jQuery.browser
1133	userAgent = navigator.userAgent,
1134
1135	// For matching the engine and version of the browser
1136	browserMatch,
1137
1138	// The deferred used on DOM ready
1139	readyList,
1140
1141	// The ready event handler
1142	DOMContentLoaded,
1143
1144	// Save a reference to some core methods
1145	toString = Object.prototype.toString,
1146	hasOwn = Object.prototype.hasOwnProperty,
1147	push = Array.prototype.push,
1148	slice = Array.prototype.slice,
1149	trim = String.prototype.trim,
1150	indexOf = Array.prototype.indexOf,
1151
1152	// [[Class]] -> type pairs
1153	class2type = {};
1154
1155jQuery.fn = jQuery.prototype = {
1156	constructor: jQuery,
1157	init: function( selector, context, rootjQuery ) {
1158		var match, elem, ret, doc;
1159
1160		// Handle $(""), $(null), or $(undefined)
1161		if ( !selector ) {
1162			return this;
1163		}
1164
1165		// Handle $(DOMElement)
1166		if ( selector.nodeType ) {
1167			this.context = this[0] = selector;
1168			this.length = 1;
1169			return this;
1170		}
1171
1172		// The body element only exists once, optimize finding it
1173		if ( selector === "body" && !context && document.body ) {
1174			this.context = document;
1175			this[0] = document.body;
1176			this.selector = "body";
1177			this.length = 1;
1178			return this;
1179		}
1180
1181		// Handle HTML strings
1182		if ( typeof selector === "string" ) {
1183			// Are we dealing with HTML string or an ID?
1184			match = quickExpr.exec( selector );
1185
1186			// Verify a match, and that no context was specified for #id
1187			if ( match && (match[1] || !context) ) {
1188
1189				// HANDLE: $(html) -> $(array)
1190				if ( match[1] ) {
1191					context = context instanceof jQuery ? context[0] : context;
1192					doc = (context ? context.ownerDocument || context : document);
1193
1194					// If a single string is passed in and it's a single tag
1195					// just do a createElement and skip the rest
1196					ret = rsingleTag.exec( selector );
1197
1198					if ( ret ) {
1199						if ( jQuery.isPlainObject( context ) ) {
1200							selector = [ document.createElement( ret[1] ) ];
1201							jQuery.fn.attr.call( selector, context, true );
1202
1203						} else {
1204							selector = [ doc.createElement( ret[1] ) ];
1205						}
1206
1207					} else {
1208						ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
1209						selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes;
1210					}
1211
1212					return jQuery.merge( this, selector );
1213
1214				// HANDLE: $("#id")
1215				} else {
1216					elem = document.getElementById( match[2] );
1217
1218					// Check parentNode to catch when Blackberry 4.6 returns
1219					// nodes that are no longer in the document #6963
1220					if ( elem && elem.parentNode ) {
1221						// Handle the case where IE and Opera return items
1222						// by name instead of ID
1223						if ( elem.id !== match[2] ) {
1224							return rootjQuery.find( selector );
1225						}
1226
1227						// Otherwise, we inject the element directly into the jQuery object
1228						this.length = 1;
1229						this[0] = elem;
1230					}
1231
1232					this.context = document;
1233					this.selector = selector;
1234					return this;
1235				}
1236
1237			// HANDLE: $(expr, $(...))
1238			} else if ( !context || context.jquery ) {
1239				return (context || rootjQuery).find( selector );
1240
1241			// HANDLE: $(expr, context)
1242			// (which is just equivalent to: $(context).find(expr)
1243			} else {
1244				return this.constructor( context ).find( selector );
1245			}
1246
1247		// HANDLE: $(function)
1248		// Shortcut for document ready
1249		} else if ( jQuery.isFunction( selector ) ) {
1250			return rootjQuery.ready( selector );
1251		}
1252
1253		if (selector.selector !== undefined) {
1254			this.selector = selector.selector;
1255			this.context = selector.context;
1256		}
1257
1258		return jQuery.makeArray( selector, this );
1259	},
1260
1261	// Start with an empty selector
1262	selector: "",
1263
1264	// The current version of jQuery being used
1265	jquery: "1.5.2",
1266
1267	// The default length of a jQuery object is 0
1268	length: 0,
1269
1270	// The number of elements contained in the matched element set
1271	size: function() {
1272		return this.length;
1273	},
1274
1275	toArray: function() {
1276		return slice.call( this, 0 );
1277	},
1278
1279	// Get the Nth element in the matched element set OR
1280	// Get the whole matched element set as a clean array
1281	get: function( num ) {
1282		return num == null ?
1283
1284			// Return a 'clean' array
1285			this.toArray() :
1286
1287			// Return just the object
1288			( num < 0 ? this[ this.length + num ] : this[ num ] );
1289	},
1290
1291	// Take an array of elements and push it onto the stack
1292	// (returning the new matched element set)
1293	pushStack: function( elems, name, selector ) {
1294		// Build a new jQuery matched element set
1295		var ret = this.constructor();
1296
1297		if ( jQuery.isArray( elems ) ) {
1298			push.apply( ret, elems );
1299
1300		} else {
1301			jQuery.merge( ret, elems );
1302		}
1303
1304		// Add the old object onto the stack (as a reference)
1305		ret.prevObject = this;
1306
1307		ret.context = this.context;
1308
1309		if ( name === "find" ) {
1310			ret.selector = this.selector + (this.selector ? " " : "") + selector;
1311		} else if ( name ) {
1312			ret.selector = this.selector + "." + name + "(" + selector + ")";
1313		}
1314
1315		// Return the newly-formed element set
1316		return ret;
1317	},
1318
1319	// Execute a callback for every element in the matched set.
1320	// (You can seed the arguments with an array of args, but this is
1321	// only used internally.)
1322	each: function( callback, args ) {
1323		return jQuery.each( this, callback, args );
1324	},
1325
1326	ready: function( fn ) {
1327		// Attach the listeners
1328		jQuery.bindReady();
1329
1330		// Add the callback
1331		readyList.done( fn );
1332
1333		return this;
1334	},
1335
1336	eq: function( i ) {
1337		return i === -1 ?
1338			this.slice( i ) :
1339			this.slice( i, +i + 1 );
1340	},
1341
1342	first: function() {
1343		return this.eq( 0 );
1344	},
1345
1346	last: function() {
1347		return this.eq( -1 );
1348	},
1349
1350	slice: function() {
1351		return this.pushStack( slice.apply( this, arguments ),
1352			"slice", slice.call(arguments).join(",") );
1353	},
1354
1355	map: function( callback ) {
1356		return this.pushStack( jQuery.map(this, function( elem, i ) {
1357			return callback.call( elem, i, elem );
1358		}));
1359	},
1360
1361	end: function() {
1362		return this.prevObject || this.constructor(null);
1363	},
1364
1365	// For internal use only.
1366	// Behaves like an Array's method, not like a jQuery method.
1367	push: push,
1368	sort: [].sort,
1369	splice: [].splice
1370};
1371
1372// Give the init function the jQuery prototype for later instantiation
1373jQuery.fn.init.prototype = jQuery.fn;
1374
1375jQuery.extend = jQuery.fn.extend = function() {
1376	var options, name, src, copy, copyIsArray, clone,
1377		target = arguments[0] || {},
1378		i = 1,
1379		length = arguments.length,
1380		deep = false;
1381
1382	// Handle a deep copy situation
1383	if ( typeof target === "boolean" ) {
1384		deep = target;
1385		target = arguments[1] || {};
1386		// skip the boolean and the target
1387		i = 2;
1388	}
1389
1390	// Handle case when target is a string or something (possible in deep copy)
1391	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
1392		target = {};
1393	}
1394
1395	// extend jQuery itself if only one argument is passed
1396	if ( length === i ) {
1397		target = this;
1398		--i;
1399	}
1400
1401	for ( ; i < length; i++ ) {
1402		// Only deal with non-null/undefined values
1403		if ( (options = arguments[ i ]) != null ) {
1404			// Extend the base object
1405			for ( name in options ) {
1406				src = target[ name ];
1407				copy = options[ name ];
1408
1409				// Prevent never-ending loop
1410				if ( target === copy ) {
1411					continue;
1412				}
1413
1414				// Recurse if we're merging plain objects or arrays
1415				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
1416					if ( copyIsArray ) {
1417						copyIsArray = false;
1418						clone = src && jQuery.isArray(src) ? src : [];
1419
1420					} else {
1421						clone = src && jQuery.isPlainObject(src) ? src : {};
1422					}
1423
1424					// Never move original objects, clone them
1425					target[ name ] = jQuery.extend( deep, clone, copy );
1426
1427				// Don't bring in undefined values
1428				} else if ( copy !== undefined ) {
1429					target[ name ] = copy;
1430				}
1431			}
1432		}
1433	}
1434
1435	// Return the modified object
1436	return target;
1437};
1438
1439jQuery.extend({
1440	noConflict: function( deep ) {
1441		window.$ = _$;
1442
1443		if ( deep ) {
1444			window.jQuery = _jQuery;
1445		}
1446
1447		return jQuery;
1448	},
1449
1450	// Is the DOM ready to be used? Set to true once it occurs.
1451	isReady: false,
1452
1453	// A counter to track how many items to wait for before
1454	// the ready event fires. See #6781
1455	readyWait: 1,
1456
1457	// Handle when the DOM is ready
1458	ready: function( wait ) {
1459		// A third-party is pushing the ready event forwards
1460		if ( wait === true ) {
1461			jQuery.readyWait--;
1462		}
1463
1464		// Make sure that the DOM is not already loaded
1465		if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) {
1466			// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
1467			if ( !document.body ) {
1468				return setTimeout( jQuery.ready, 1 );
1469			}
1470
1471			// Remember that the DOM is ready
1472			jQuery.isReady = true;
1473
1474			// If a normal DOM Ready event fired, decrement, and wait if need be
1475			if ( wait !== true && --jQuery.readyWait > 0 ) {
1476				return;
1477			}
1478
1479			// If there are functions bound, to execute
1480			readyList.resolveWith( document, [ jQuery ] );
1481
1482			// Trigger any bound ready events
1483			if ( jQuery.fn.trigger ) {
1484				jQuery( document ).trigger( "ready" ).unbind( "ready" );
1485			}
1486		}
1487	},
1488
1489	bindReady: function() {
1490		if ( readyList ) {
1491			return;
1492		}
1493
1494		readyList = jQuery._Deferred();
1495
1496		// Catch cases where $(document).ready() is called after the
1497		// browser event has already occurred.
1498		if ( document.readyState === "complete" ) {
1499			// Handle it asynchronously to allow scripts the opportunity to delay ready
1500			return setTimeout( jQuery.ready, 1 );
1501		}
1502
1503		// Mozilla, Opera and webkit nightlies currently support this event
1504		if ( document.addEventListener ) {
1505			// Use the handy event callback
1506			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
1507
1508			// A fallback to window.onload, that will always work
1509			window.addEventListener( "load", jQuery.ready, false );
1510
1511		// If IE event model is used
1512		} else if ( document.attachEvent ) {
1513			// ensure firing before onload,
1514			// maybe late but safe also for iframes
1515			document.attachEvent("onreadystatechange", DOMContentLoaded);
1516
1517			// A fallback to window.onload, that will always work
1518			window.attachEvent( "onload", jQuery.ready );
1519
1520			// If IE and not a frame
1521			// continually check to see if the document is ready
1522			var toplevel = false;
1523
1524			try {
1525				toplevel = window.frameElement == null;
1526			} catch(e) {}
1527
1528			if ( document.documentElement.doScroll && toplevel ) {
1529				doScrollCheck();
1530			}
1531		}
1532	},
1533
1534	// See test/unit/core.js for details concerning isFunction.
1535	// Since version 1.3, DOM methods and functions like alert
1536	// aren't supported. They return false on IE (#2968).
1537	isFunction: function( obj ) {
1538		return jQuery.type(obj) === "function";
1539	},
1540
1541	isArray: Array.isArray || function( obj ) {
1542		return jQuery.type(obj) === "array";
1543	},
1544
1545	// A crude way of determining if an object is a window
1546	isWindow: function( obj ) {
1547		return obj && typeof obj === "object" && "setInterval" in obj;
1548	},
1549
1550	isNaN: function( obj ) {
1551		return obj == null || !rdigit.test( obj ) || isNaN( obj );
1552	},
1553
1554	type: function( obj ) {
1555		return obj == null ?
1556			String( obj ) :
1557			class2type[ toString.call(obj) ] || "object";
1558	},
1559
1560	isPlainObject: function( obj ) {
1561		// Must be an Object.
1562		// Because of IE, we also have to check the presence of the constructor property.
1563		// Make sure that DOM nodes and window objects don't pass through, as well
1564		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
1565			return false;
1566		}
1567
1568		// Not own constructor property must be Object
1569		if ( obj.constructor &&
1570			!hasOwn.call(obj, "constructor") &&
1571			!hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
1572			return false;
1573		}
1574
1575		// Own properties are enumerated firstly, so to speed up,
1576		// if last one is own, then all properties are own.
1577
1578		var key;
1579		for ( key in obj ) {}
1580
1581		return key === undefined || hasOwn.call( obj, key );
1582	},
1583
1584	isEmptyObject: function( obj ) {
1585		for ( var name in obj ) {
1586			return false;
1587		}
1588		return true;
1589	},
1590
1591	error: function( msg ) {
1592		throw msg;
1593	},
1594
1595	parseJSON: function( data ) {
1596		if ( typeof data !== "string" || !data ) {
1597			return null;
1598		}
1599
1600		// Make sure leading/trailing whitespace is removed (IE can't handle it)
1601		data = jQuery.trim( data );
1602
1603		// Make sure the incoming data is actual JSON
1604		// Logic borrowed from http://json.org/json2.js
1605		if ( rvalidchars.test(data.replace(rvalidescape, "@")
1606			.replace(rvalidtokens, "]")
1607			.replace(rvalidbraces, "")) ) {
1608
1609			// Try to use the native JSON parser first
1610			return window.JSON && window.JSON.parse ?
1611				window.JSON.parse( data ) :
1612				(new Function("return " + data))();
1613
1614		} else {
1615			jQuery.error( "Invalid JSON: " + data );
1616		}
1617	},
1618
1619	// Cross-browser xml parsing
1620	// (xml & tmp used internally)
1621	parseXML: function( data , xml , tmp ) {
1622
1623		if ( window.DOMParser ) { // Standard
1624			tmp = new DOMParser();
1625			xml = tmp.parseFromString( data , "text/xml" );
1626		} else { // IE
1627			xml = new ActiveXObject( "Microsoft.XMLDOM" );
1628			xml.async = "false";
1629			xml.loadXML( data );
1630		}
1631
1632		tmp = xml.documentElement;
1633
1634		if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) {
1635			jQuery.error( "Invalid XML: " + data );
1636		}
1637
1638		return xml;
1639	},
1640
1641	noop: function() {},
1642
1643	// Evalulates a script in a global context
1644	globalEval: function( data ) {
1645		if ( data && rnotwhite.test(data) ) {
1646			// Inspired by code by Andrea Giammarchi
1647			// http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
1648			var head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement,
1649				script = document.createElement( "script" );
1650
1651			if ( jQuery.support.scriptEval() ) {
1652				script.appendChild( document.createTextNode( data ) );
1653			} else {
1654				script.text = data;
1655			}
1656
1657			// Use insertBefore instead of appendChild to circumvent an IE6 bug.
1658			// This arises when a base node is used (#2709).
1659			head.insertBefore( script, head.firstChild );
1660			head.removeChild( script );
1661		}
1662	},
1663
1664	nodeName: function( elem, name ) {
1665		return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
1666	},
1667
1668	// args is for internal usage only
1669	each: function( object, callback, args ) {
1670		var name, i = 0,
1671			length = object.length,
1672			isObj = length === undefined || jQuery.isFunction(object);
1673
1674		if ( args ) {
1675			if ( isObj ) {
1676				for ( name in object ) {
1677					if ( callback.apply( object[ name ], args ) === false ) {
1678						break;
1679					}
1680				}
1681			} else {
1682				for ( ; i < length; ) {
1683					if ( callback.apply( object[ i++ ], args ) === false ) {
1684						break;
1685					}
1686				}
1687			}
1688
1689		// A special, fast, case for the most common use of each
1690		} else {
1691			if ( isObj ) {
1692				for ( name in object ) {
1693					if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
1694						break;
1695					}
1696				}
1697			} else {
1698				for ( var value = object[0];
1699					i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
1700			}
1701		}
1702
1703		return object;
1704	},
1705
1706	// Use native String.trim function wherever possible
1707	trim: trim ?
1708		function( text ) {
1709			return text == null ?
1710				"" :
1711				trim.call( text );
1712		} :
1713
1714		// Otherwise use our own trimming functionality
1715		function( text ) {
1716			return text == null ?
1717				"" :
1718				text.toString().replace( trimLeft, "" ).replace( trimRight, "" );
1719		},
1720
1721	// results is for internal usage only
1722	makeArray: function( array, results ) {
1723		var ret = results || [];
1724
1725		if ( array != null ) {
1726			// The window, strings (and functions) also have 'length'
1727			// The extra typeof function check is to prevent crashes
1728			// in Safari 2 (See: #3039)
1729			// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
1730			var type = jQuery.type(array);
1731
1732			if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
1733				push.call( ret, array );
1734			} else {
1735				jQuery.merge( ret, array );
1736			}
1737		}
1738
1739		return ret;
1740	},
1741
1742	inArray: function( elem, array ) {
1743		if ( array.indexOf ) {
1744			return array.indexOf( elem );
1745		}
1746
1747		for ( var i = 0, length = array.length; i < length; i++ ) {
1748			if ( array[ i ] === elem ) {
1749				return i;
1750			}
1751		}
1752
1753		return -1;
1754	},
1755
1756	merge: function( first, second ) {
1757		var i = first.length,
1758			j = 0;
1759
1760		if ( typeof second.length === "number" ) {
1761			for ( var l = second.length; j < l; j++ ) {
1762				first[ i++ ] = second[ j ];
1763			}
1764
1765		} else {
1766			while ( second[j] !== undefined ) {
1767				first[ i++ ] = second[ j++ ];
1768			}
1769		}
1770
1771		first.length = i;
1772
1773		return first;
1774	},
1775
1776	grep: function( elems, callback, inv ) {
1777		var ret = [], retVal;
1778		inv = !!inv;
1779
1780		// Go through the array, only saving the items
1781		// that pass the validator function
1782		for ( var i = 0, length = elems.length; i < length; i++ ) {
1783			retVal = !!callback( elems[ i ], i );
1784			if ( inv !== retVal ) {
1785				ret.push( elems[ i ] );
1786			}
1787		}
1788
1789		return ret;
1790	},
1791
1792	// arg is for internal usage only
1793	map: function( elems, callback, arg ) {
1794		var ret = [], value;
1795
1796		// Go through the array, translating each of the items to their
1797		// new value (or values).
1798		for ( var i = 0, length = elems.length; i < length; i++ ) {
1799			value = callback( elems[ i ], i, arg );
1800
1801			if ( value != null ) {
1802				ret[ ret.length ] = value;
1803			}
1804		}
1805
1806		// Flatten any nested arrays
1807		return ret.concat.apply( [], ret );
1808	},
1809
1810	// A global GUID counter for objects
1811	guid: 1,
1812
1813	proxy: function( fn, proxy, thisObject ) {
1814		if ( arguments.length === 2 ) {
1815			if ( typeof proxy === "string" ) {
1816				thisObject = fn;
1817				fn = thisObject[ proxy ];
1818				proxy = undefined;
1819
1820			} else if ( proxy && !jQuery.isFunction( proxy ) ) {
1821				thisObject = proxy;
1822				proxy = undefined;
1823			}
1824		}
1825
1826		if ( !proxy && fn ) {
1827			proxy = function() {
1828				return fn.apply( thisObject || this, arguments );
1829			};
1830		}
1831
1832		// Set the guid of unique handler to the same of original handler, so it can be removed
1833		if ( fn ) {
1834			proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
1835		}
1836
1837		// So proxy can be declared as an argument
1838		return proxy;
1839	},
1840
1841	// Mutifunctional method to get and set values to a collection
1842	// The value/s can be optionally by executed if its a function
1843	access: function( elems, key, value, exec, fn, pass ) {
1844		var length = elems.length;
1845
1846		// Setting many attributes
1847		if ( typeof key === "object" ) {
1848			for ( var k in key ) {
1849				jQuery.access( elems, k, key[k], exec, fn, value );
1850			}
1851			return elems;
1852		}
1853
1854		// Setting one attribute
1855		if ( value !== undefined ) {
1856			// Optionally, function values get executed if exec is true
1857			exec = !pass && exec && jQuery.isFunction(value);
1858
1859			for ( var i = 0; i < length; i++ ) {
1860				fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
1861			}
1862
1863			return elems;
1864		}
1865
1866		// Getting an attribute
1867		return length ? fn( elems[0], key ) : undefined;
1868	},
1869
1870	now: function() {
1871		return (new Date()).getTime();
1872	},
1873
1874	// Use of jQuery.browser is frowned upon.
1875	// More details: http://docs.jquery.com/Utilities/jQuery.browser
1876	uaMatch: function( ua ) {
1877		ua = ua.toLowerCase();
1878
1879		var match = rwebkit.exec( ua ) ||
1880			ropera.exec( ua ) ||
1881			rmsie.exec( ua ) ||
1882			ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
1883			[];
1884
1885		return { browser: match[1] || "", version: match[2] || "0" };
1886	},
1887
1888	sub: function() {
1889		function jQuerySubclass( selector, context ) {
1890			return new jQuerySubclass.fn.init( selector, context );
1891		}
1892		jQuery.extend( true, jQuerySubclass, this );
1893		jQuerySubclass.superclass = this;
1894		jQuerySubclass.fn = jQuerySubclass.prototype = this();
1895		jQuerySubclass.fn.constructor = jQuerySubclass;
1896		jQuerySubclass.subclass = this.subclass;
1897		jQuerySubclass.fn.init = function init( selector, context ) {
1898			if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) {
1899				context = jQuerySubclass(context);
1900			}
1901
1902			return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass );
1903		};
1904		jQuerySubclass.fn.init.prototype = jQuerySubclass.fn;
1905		var rootjQuerySubclass = jQuerySubclass(document);
1906		return jQuerySubclass;
1907	},
1908
1909	browser: {}
1910});
1911
1912// Populate the class2type map
1913jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
1914	class2type[ "[object " + name + "]" ] = name.toLowerCase();
1915});
1916
1917browserMatch = jQuery.uaMatch( userAgent );
1918if ( browserMatch.browser ) {
1919	jQuery.browser[ browserMatch.browser ] = true;
1920	jQuery.browser.version = browserMatch.version;
1921}
1922
1923// Deprecated, use jQuery.browser.webkit instead
1924if ( jQuery.browser.webkit ) {
1925	jQuery.browser.safari = true;
1926}
1927
1928if ( indexOf ) {
1929	jQuery.inArray = function( elem, array ) {
1930		return indexOf.call( array, elem );
1931	};
1932}
1933
1934// IE doesn't match non-breaking spaces with \s
1935if ( rnotwhite.test( "\xA0" ) ) {
1936	trimLeft = /^[\s\xA0]+/;
1937	trimRight = /[\s\xA0]+$/;
1938}
1939
1940// All jQuery objects should point back to these
1941rootjQuery = jQuery(document);
1942
1943// Cleanup functions for the document ready method
1944if ( document.addEventListener ) {
1945	DOMContentLoaded = function() {
1946		document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
1947		jQuery.ready();
1948	};
1949
1950} else if ( document.attachEvent ) {
1951	DOMContentLoaded = function() {
1952		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
1953		if ( document.readyState === "complete" ) {
1954			document.detachEvent( "onreadystatechange", DOMContentLoaded );
1955			jQuery.ready();
1956		}
1957	};
1958}
1959
1960// The DOM ready check for Internet Explorer
1961function doScrollCheck() {
1962	if ( jQuery.isReady ) {
1963		return;
1964	}
1965
1966	try {
1967		// If IE is used, use the trick by Diego Perini
1968		// http://javascript.nwbox.com/IEContentLoaded/
1969		document.documentElement.doScroll("left");
1970	} catch(e) {
1971		setTimeout( doScrollCheck, 1 );
1972		return;
1973	}
1974
1975	// and execute any waiting functions
1976	jQuery.ready();
1977}
1978
1979// Expose jQuery to the global object
1980return jQuery;
1981
1982})();
1983
1984
1985var // Promise methods
1986	promiseMethods = "then done fail isResolved isRejected promise".split( " " ),
1987	// Static reference to slice
1988	sliceDeferred = [].slice;
1989
1990jQuery.extend({
1991	// Create a simple deferred (one callbacks list)
1992	_Deferred: function() {
1993		var // callbacks list
1994			callbacks = [],
1995			// stored [ context , args ]
1996			fired,
1997			// to avoid firing when already doing so
1998			firing,
1999			// flag to know if the deferred has been cancelled
2000			cancelled,
2001			// the deferred itself
2002			deferred  = {
2003
2004				// done( f1, f2, ...)
2005				done: function() {
2006					if ( !cancelled ) {
2007						var args = arguments,
2008							i,
2009							length,
2010							elem,
2011							type,
2012							_fired;
2013						if ( fired ) {
2014							_fired = fired;
2015							fired = 0;
2016						}
2017						for ( i = 0, length = args.length; i < length; i++ ) {
2018							elem = args[ i ];
2019							type = jQuery.type( elem );
2020							if ( type === "array" ) {
2021								deferred.done.apply( deferred, elem );
2022							} else if ( type === "function" ) {
2023								callbacks.push( elem );
2024							}
2025						}
2026						if ( _fired ) {
2027							deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
2028						}
2029					}
2030					return this;
2031				},
2032
2033				// resolve with given context and args
2034				resolveWith: function( context, args ) {
2035					if ( !cancelled && !fired && !firing ) {
2036						// make sure args are available (#8421)
2037						args = args || [];
2038						firing = 1;
2039						try {
2040							while( callbacks[ 0 ] ) {
2041								callbacks.shift().apply( context, args );
2042							}
2043						}
2044						finally {
2045							fired = [ context, args ];
2046							firing = 0;
2047						}
2048					}
2049					return this;
2050				},
2051
2052				// resolve with this as context and given arguments
2053				resolve: function() {
2054					deferred.resolveWith( this, arguments );
2055					return this;
2056				},
2057
2058				// Has this deferred been resolved?
2059				isResolved: function() {
2060					return !!( firing || fired );
2061				},
2062
2063				// Cancel
2064				cancel: function() {
2065					cancelled = 1;
2066					callbacks = [];
2067					return this;
2068				}
2069			};
2070
2071		return deferred;
2072	},
2073
2074	// Full fledged deferred (two callbacks list)
2075	Deferred: function( func ) {
2076		var deferred = jQuery._Deferred(),
2077			failDeferred = jQuery._Deferred(),
2078			promise;
2079		// Add errorDeferred methods, then and promise
2080		jQuery.extend( deferred, {
2081			then: function( doneCallbacks, failCallbacks ) {
2082				deferred.done( doneCallbacks ).fail( failCallbacks );
2083				return this;
2084			},
2085			fail: failDeferred.done,
2086			rejectWith: failDeferred.resolveWith,
2087			reject: failDeferred.resolve,
2088			isRejected: failDeferred.isResolved,
2089			// Get a promise for this deferred
2090			// If obj is provided, the promise aspect is added to the object
2091			promise: function( obj ) {
2092				if ( obj == null ) {
2093					if ( promise ) {
2094						return promise;
2095					}
2096					promise = obj = {};
2097				}
2098				var i = promiseMethods.length;
2099				while( i-- ) {
2100					obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
2101				}
2102				return obj;
2103			}
2104		} );
2105		// Make sure only one callback list will be used
2106		deferred.done( failDeferred.cancel ).fail( deferred.cancel );
2107		// Unexpose cancel
2108		delete deferred.cancel;
2109		// Call given func if any
2110		if ( func ) {
2111			func.call( deferred, deferred );
2112		}
2113		return deferred;
2114	},
2115
2116	// Deferred helper
2117	when: function( firstParam ) {
2118		var args = arguments,
2119			i = 0,
2120			length = args.length,
2121			count = length,
2122			deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
2123				firstParam :
2124				jQuery.Deferred();
2125		function resolveFunc( i ) {
2126			return function( value ) {
2127				args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
2128				if ( !( --count ) ) {
2129					// Strange bug in FF4:
2130					// Values changed onto the arguments object sometimes end up as undefined values
2131					// outside the $.when method. Cloning the object into a fresh array solves the issue
2132					deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
2133				}
2134			};
2135		}
2136		if ( length > 1 ) {
2137			for( ; i < length; i++ ) {
2138				if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
2139					args[ i ].promise().then( resolveFunc(i), deferred.reject );
2140				} else {
2141					--count;
2142				}
2143			}
2144			if ( !count ) {
2145				deferred.resolveWith( deferred, args );
2146			}
2147		} else if ( deferred !== firstParam ) {
2148			deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
2149		}
2150		return deferred.promise();
2151	}
2152});
2153
2154
2155
2156
2157(function() {
2158
2159	jQuery.support = {};
2160
2161	var div = document.createElement("div");
2162
2163	div.style.display = "none";
2164	div.innerHTML = "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a>
2164<input type='checkbox'/>";
2165
2166	var all = div.getElementsByTagName("*"),
2167		a = div.getElementsByTagName("a")[0],
2168		select = document.createElement("select"),
2169		opt = select.appendChild( document.createElement("option") ),
2170		input = div.getElementsByTagName("input")[0];
2171
2172	// Can't get basic test support
2173	if ( !all || !all.length || !a ) {
2174		return;
2175	}
2176
2177	jQuery.support = {
2178		// IE strips leading whitespace when .innerHTML is used
2179		leadingWhitespace: div.firstChild.nodeType === 3,
2180
2181		// Make sure that tbody elements aren't automatically inserted
2182		// IE will insert them into empty tables
2183		tbody: !div.getElementsByTagName("tbody").length,
2184
2185		// Make sure that link elements get serialized correctly by innerHTML
2186		// This requires a wrapper element in IE
2187		htmlSerialize: !!div.getElementsByTagName("link").length,
2188
2189		// Get the style information from getAttribute
2190		// (IE uses .cssText insted)
2191		style: /red/.test( a.getAttribute("style") ),
2192
2193		// Make sure that URLs aren't manipulated
2194		// (IE normalizes it by default)
2195		hrefNormalized: a.getAttribute("href") === "/a",
2196
2197		// Make sure that element opacity exists
2198		// (IE uses filter instead)
2199		// Use a regex to work around a WebKit issue. See #5145
2200		opacity: /^0.55$/.test( a.style.opacity ),
2201
2202		// Verify style float existence
2203		// (IE uses styleFloat instead of cssFloat)
2204		cssFloat: !!a.style.cssFloat,
2205
2206		// Make sure that if no value is specified for a checkbox
2207		// that it defaults to "on".
2208		// (WebKit defaults to "" instead)
2209		checkOn: input.value === "on",
2210
2211		// Make sure that a selected-by-default option has a working selected property.
2212		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
2213		optSelected: opt.selected,
2214
2215		// Will be defined later
2216		deleteExpando: true,
2217		optDisabled: false,
2218		checkClone: false,
2219		noCloneEvent: true,
2220		noCloneChecked: true,
2221		boxModel: null,
2222		inlineBlockNeedsLayout: false,
2223		shrinkWrapBlocks: false,
2224		reliableHiddenOffsets: true,
2225		reliableMarginRight: true
2226	};
2227
2228	input.checked = true;
2229	jQuery.support.noCloneChecked = input.cloneNode( true ).checked;
2230
2231	// Make sure that the options inside disabled selects aren't marked as disabled
2232	// (WebKit marks them as diabled)
2233	select.disabled = true;
2234	jQuery.support.optDisabled = !opt.disabled;
2235
2236	var _scriptEval = null;
2237	jQuery.support.scriptEval = function() {
2238		if ( _scriptEval === null ) {
2239			var root = document.documentElement,
2240				script = document.createElement("script"),
2241				id = "script" + jQuery.now();
2242
2243			// Make sure that the execution of code works by injecting a script
2244			// tag with appendChild/createTextNode
2245			// (IE doesn't support this, fails, and uses .text instead)
2246			try {
2247				script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
2248			} catch(e) {}
2249
2250			root.insertBefore( script, root.firstChild );
2251
2252			if ( window[ id ] ) {
2253				_scriptEval = true;
2254				delete window[ id ];
2255			} else {
2256				_scriptEval = false;
2257			}
2258
2259			root.removeChild( script );
2260		}
2261
2262		return _scriptEval;
2263	};
2264
2265	// Test to see if it's possible to delete an expando from an element
2266	// Fails in Internet Explorer
2267	try {
2268		delete div.test;
2269
2270	} catch(e) {
2271		jQuery.support.deleteExpando = false;
2272	}
2273
2274	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
2275		div.attachEvent("onclick", function click() {
2276			// Cloning a node shouldn't copy over any
2277			// bound event handlers (IE does this)
2278			jQuery.support.noCloneEvent = false;
2279			div.detachEvent("onclick", click);
2280		});
2281		div.cloneNode(true).fireEvent("onclick");
2282	}
2283
2284	div = document.createElement("div");
2285	div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";
2286
2287	var fragment = document.createDocumentFragment();
2288	fragment.appendChild( div.firstChild );
2289
2290	// WebKit doesn't clone checked state correctly in fragments
2291	jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
2292
2293	// Figure out if the W3C box model works as expected
2294	// document.body must exist before we can do this
2295	jQuery(function() {
2296		var div = document.createElement("div"),
2297			body = document.getElementsByTagName("body")[0];
2298
2299		// Frameset documents with no body should not run this code
2300		if ( !body ) {
2301			return;
2302		}
2303
2304		div.style.width = div.style.paddingLeft = "1px";
2305		body.appendChild( div );
2306		jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
2307
2308		if ( "zoom" in div.style ) {
2309			// Check if natively block-level elements act like inline-block
2310			// elements when setting their display to 'inline' and giving
2311			// them layout
2312			// (IE < 8 does this)
2313			div.style.display = "inline";
2314			div.style.zoom = 1;
2315			jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2;
2316
2317			// Check if elements with layout shrink-wrap their children
2318			// (IE 6 does this)
2319			div.style.display = "";
2320			div.innerHTML = "<div style='width:4px;'></div>";
2321			jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2;
2322		}
2323
2324		div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";
2325		var tds = div.getElementsByTagName("td");
2326
2327		// Check if table cells still have offsetWidth/Height when they are set
2328		// to display:none and there are still other visible table cells in a
2329		// table row; if so, offsetWidth/Height are not reliable for use when
2330		// determining if an element has been hidden directly using
2331		// display:none (it is still safe to use offsets if a parent element is
2332		// hidden; don safety goggles and see bug #4512 for more information).
2333		// (only IE 8 fails this test)
2334		jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0;
2335
2336		tds[0].style.display = "";
2337		tds[1].style.display = "none";
2338
2339		// Check if empty table cells still have offsetWidth/Height
2340		// (IE < 8 fail this test)
2341		jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0;
2342		div.innerHTML = "";
2343
2344		// Check if div with explicit width and no margin-right incorrectly
2345		// gets computed margin-right based on width of container. For more
2346		// info see bug #3333
2347		// Fails in WebKit before Feb 2011 nightlies
2348		// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
2349		if ( document.defaultView && document.defaultView.getComputedStyle ) {
2350			div.style.width = "1px";
2351			div.style.marginRight = "0";
2352			jQuery.support.reliableMarginRight = ( parseInt(document.defaultView.getComputedStyle(div, null).marginRight, 10) || 0 ) === 0;
2353		}
2354
2355		body.removeChild( div ).style.display = "none";
2356		div = tds = null;
2357	});
2358
2359	// Technique from Juriy Zaytsev
2360	// http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
2361	var eventSupported = function( eventName ) {
2362		var el = document.createElement("div");
2363		eventName = "on" + eventName;
2364
2365		// We only care about the case where non-standard event systems
2366		// are used, namely in IE. Short-circuiting here helps us to
2367		// avoid an eval call (in setAttribute) which can cause CSP
2368		// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
2369		if ( !el.attachEvent ) {
2370			return true;
2371		}
2372
2373		var isSupported = (eventName in el);
2374		if ( !isSupported ) {
2375			el.setAttribute(eventName, "return;");
2376			isSupported = typeof el[eventName] === "function";
2377		}
2378		return isSupported;
2379	};
2380
2381	jQuery.support.submitBubbles = eventSupported("submit");
2382	jQuery.support.changeBubbles = eventSupported("change");
2383
2384	// release memory in IE
2385	div = all = a = null;
2386})();
2387
2388
2389
2390var rbrace = /^(?:\{.*\}|\[.*\])$/;
2391
2392jQuery.extend({
2393	cache: {},
2394
2395	// Please use with caution
2396	uuid: 0,
2397
2398	// Unique for each copy of jQuery on the page
2399	// Non-digits removed to match rinlinejQuery
2400	expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
2401
2402	// The following elements throw uncatchable exceptions if you
2403	// attempt to add expando properties to them.
2404	noData: {
2405		"embed": true,
2406		// Ban all objects except for Flash (which handle expandos)
2407		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
2408		"applet": true
2409	},
2410
2411	hasData: function( elem ) {
2412		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
2413
2414		return !!elem && !isEmptyDataObject( elem );
2415	},
2416
2417	data: function( elem, name, data, pvt /* Internal Use Only */ ) {
2418		if ( !jQuery.acceptData( elem ) ) {
2419			return;
2420		}
2421
2422		var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,
2423
2424			// We have to handle DOM nodes and JS objects differently because IE6-7
2425			// can't GC object references properly across the DOM-JS boundary
2426			isNode = elem.nodeType,
2427
2428			// Only DOM nodes need the global jQuery cache; JS object data is
2429			// attached directly to the object so GC can occur automatically
2430			cache = isNode ? jQuery.cache : elem,
2431
2432			// Only defining an ID for JS objects if its cache already exists allows
2433			// the code to shortcut on the same path as a DOM node with no cache
2434			id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;
2435
2436		// Avoid doing any more work than we need to when trying to get data on an
2437		// object that has no data at all
2438		if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
2439			return;
2440		}
2441
2442		if ( !id ) {
2443			// Only DOM nodes need a new unique ID for each element since their data
2444			// ends up in the global cache
2445			if ( isNode ) {
2446				elem[ jQuery.expando ] = id = ++jQuery.uuid;
2447			} else {
2448				id = jQuery.expando;
2449			}
2450		}
2451
2452		if ( !cache[ id ] ) {
2453			cache[ id ] = {};
2454
2455			// TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
2456			// metadata on plain JS objects when the object is serialized using
2457			// JSON.stringify
2458			if ( !isNode ) {
2459				cache[ id ].toJSON = jQuery.noop;
2460			}
2461		}
2462
2463		// An object can be passed to jQuery.data instead of a key/value pair; this gets
2464		// shallow copied over onto the existing cache
2465		if ( typeof name === "object" || typeof name === "function" ) {
2466			if ( pvt ) {
2467				cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
2468			} else {
2469				cache[ id ] = jQuery.extend(cache[ id ], name);
2470			}
2471		}
2472
2473		thisCache = cache[ id ];
2474
2475		// Internal jQuery data is stored in a separate object inside the object's data
2476		// cache in order to avoid key collisions between internal data and user-defined
2477		// data
2478		if ( pvt ) {
2479			if ( !thisCache[ internalKey ] ) {
2480				thisCache[ internalKey ] = {};
2481			}
2482
2483			thisCache = thisCache[ internalKey ];
2484		}
2485
2486		if ( data !== undefined ) {
2487			thisCache[ name ] = data;
2488		}
2489
2490		// TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should
2491		// not attempt to inspect the internal events object using jQuery.data, as this
2492		// internal data object is undocumented and subject to change.
2493		if ( name === "events" && !thisCache[name] ) {
2494			return thisCache[ internalKey ] && thisCache[ internalKey ].events;
2495		}
2496
2497		return getByName ? thisCache[ name ] : thisCache;
2498	},
2499
2500	removeData: function( elem, name, pvt /* Internal Use Only */ ) {
2501		if ( !jQuery.acceptData( elem ) ) {
2502			return;
2503		}
2504
2505		var internalKey = jQuery.expando, isNode = elem.nodeType,
2506
2507			// See jQuery.data for more information
2508			cache = isNode ? jQuery.cache : elem,
2509
2510			// See jQuery.data for more information
2511			id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
2512
2513		// If there is already no cache entry for this object, there is no
2514		// purpose in continuing
2515		if ( !cache[ id ] ) {
2516			return;
2517		}
2518
2519		if ( name ) {
2520			var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];
2521
2522			if ( thisCache ) {
2523				delete thisCache[ name ];
2524
2525				// If there is no data left in the cache, we want to continue
2526				// and let the cache object itself get destroyed
2527				if ( !isEmptyDataObject(thisCache) ) {
2528					return;
2529				}
2530			}
2531		}
2532
2533		// See jQuery.data for more information
2534		if ( pvt ) {
2535			delete cache[ id ][ internalKey ];
2536
2537			// Don't destroy the parent cache unless the internal data object
2538			// had been the only thing left in it
2539			if ( !isEmptyDataObject(cache[ id ]) ) {
2540				return;
2541			}
2542		}
2543
2544		var internalCache = cache[ id ][ internalKey ];
2545
2546		// Browsers that fail expando deletion also refuse to delete expandos on
2547		// the window, but it will allow it on all other JS objects; other browsers
2548		// don't care
2549		if ( jQuery.support.deleteExpando || cache != window ) {
2550			delete cache[ id ];
2551		} else {
2552			cache[ id ] = null;
2553		}
2554
2555		// We destroyed the entire user cache at once because it's faster than
2556		// iterating through each key, but we need to continue to persist internal
2557		// data if it existed
2558		if ( internalCache ) {
2559			cache[ id ] = {};
2560			// TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
2561			// metadata on plain JS objects when the object is serialized using
2562			// JSON.stringify
2563			if ( !isNode ) {
2564				cache[ id ].toJSON = jQuery.noop;
2565			}
2566
2567			cache[ id ][ internalKey ] = internalCache;
2568
2569		// Otherwise, we need to eliminate the expando on the node to avoid
2570		// false lookups in the cache for entries that no longer exist
2571		} else if ( isNode ) {
2572			// IE does not allow us to delete expando properties from nodes,
2573			// nor does it have a removeAttribute function on Document nodes;
2574			// we must handle all of these cases
2575			if ( jQuery.support.deleteExpando ) {
2576				delete elem[ jQuery.expando ];
2577			} else if ( elem.removeAttribute ) {
2578				elem.removeAttribute( jQuery.expando );
2579			} else {
2580				elem[ jQuery.expando ] = null;
2581			}
2582		}
2583	},
2584
2585	// For internal use only.
2586	_data: function( elem, name, data ) {
2587		return jQuery.data( elem, name, data, true );
2588	},
2589
2590	// A method for determining if a DOM node can handle the data expando
2591	acceptData: function( elem ) {
2592		if ( elem.nodeName ) {
2593			var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
2594
2595			if ( match ) {
2596				return !(match === true || elem.getAttribute("classid") !== match);
2597			}
2598		}
2599
2600		return true;
2601	}
2602});
2603
2604jQuery.fn.extend({
2605	data: function( key, value ) {
2606		var data = null;
2607
2608		if ( typeof key === "undefined" ) {
2609			if ( this.length ) {
2610				data = jQuery.data( this[0] );
2611
2612				if ( this[0].nodeType === 1 ) {
2613					var attr = this[0].attributes, name;
2614					for ( var i = 0, l = attr.length; i < l; i++ ) {
2615						name = attr[i].name;
2616
2617						if ( name.indexOf( "data-" ) === 0 ) {
2618							name = name.substr( 5 );
2619							dataAttr( this[0], name, data[ name ] );
2620						}
2621					}
2622				}
2623			}
2624
2625			return data;
2626
2627		} else if ( typeof key === "object" ) {
2628			return this.each(function() {
2629				jQuery.data( this, key );
2630			});
2631		}
2632
2633		var parts = key.split(".");
2634		parts[1] = parts[1] ? "." + parts[1] : "";
2635
2636		if ( value === undefined ) {
2637			data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
2638
2639			// Try to fetch any internally stored data first
2640			if ( data === undefined && this.length ) {
2641				data = jQuery.data( this[0], key );
2642				data = dataAttr( this[0], key, data );
2643			}
2644
2645			return data === undefined && parts[1] ?
2646				this.data( parts[0] ) :
2647				data;
2648
2649		} else {
2650			return this.each(function() {
2651				var $this = jQuery( this ),
2652					args = [ parts[0], value ];
2653
2654				$this.triggerHandler( "setData" + parts[1] + "!", args );
2655				jQuery.data( this, key, value );
2656				$this.triggerHandler( "changeData" + parts[1] + "!", args );
2657			});
2658		}
2659	},
2660
2661	removeData: function( key ) {
2662		return this.each(function() {
2663			jQuery.removeData( this, key );
2664		});
2665	}
2666});
2667
2668function dataAttr( elem, key, data ) {
2669	// If nothing was found internally, try to fetch any
2670	// data from the HTML5 data-* attribute
2671	if ( data === undefined && elem.nodeType === 1 ) {
2672		data = elem.getAttribute( "data-" + key );
2673
2674		if ( typeof data === "string" ) {
2675			try {
2676				data = data === "true" ? true :
2677				data === "false" ? false :
2678				data === "null" ? null :
2679				!jQuery.isNaN( data ) ? parseFloat( data ) :
2680					rbrace.test( data ) ? jQuery.parseJSON( data ) :
2681					data;
2682			} catch( e ) {}
2683
2684			// Make sure we set the data so it isn't changed later
2685			jQuery.data( elem, key, data );
2686
2687		} else {
2688			data = undefined;
2689		}
2690	}
2691
2692	return data;
2693}
2694
2695// TODO: This is a hack for 1.5 ONLY to allow objects with a single toJSON
2696// property to be considered empty objects; this property always exists in
2697// order to make sure JSON.stringify does not expose internal metadata
2698function isEmptyDataObject( obj ) {
2699	for ( var name in obj ) {
2700		if ( name !== "toJSON" ) {
2701			return false;
2702		}
2703	}
2704
2705	return true;
2706}
2707
2708
2709
2710
2711jQuery.extend({
2712	queue: function( elem, type, data ) {
2713		if ( !elem ) {
2714			return;
2715		}
2716
2717		type = (type || "fx") + "queue";
2718		var q = jQuery._data( elem, type );
2719
2720		// Speed up dequeue by getting out quickly if this is just a lookup
2721		if ( !data ) {
2722			return q || [];
2723		}
2724
2725		if ( !q || jQuery.isArray(data) ) {
2726			q = jQuery._data( elem, type, jQuery.makeArray(data) );
2727
2728		} else {
2729			q.push( data );
2730		}
2731
2732		return q;
2733	},
2734
2735	dequeue: function( elem, type ) {
2736		type = type || "fx";
2737
2738		var queue = jQuery.queue( elem, type ),
2739			fn = queue.shift();
2740
2741		// If the fx queue is dequeued, always remove the progress sentinel
2742		if ( fn === "inprogress" ) {
2743			fn = queue.shift();
2744		}
2745
2746		if ( fn ) {
2747			// Add a progress sentinel to prevent the fx queue from being
2748			// automatically dequeued
2749			if ( type === "fx" ) {
2750				queue.unshift("inprogress");
2751			}
2752
2753			fn.call(elem, function() {
2754				jQuery.dequeue(elem, type);
2755			});
2756		}
2757
2758		if ( !queue.length ) {
2759			jQuery.removeData( elem, type + "queue", true );
2760		}
2761	}
2762});
2763
2764jQuery.fn.extend({
2765	queue: function( type, data ) {
2766		if ( typeof type !== "string" ) {
2767			data = type;
2768			type = "fx";
2769		}
2770
2771		if ( data === undefined ) {
2772			return jQuery.queue( this[0], type );
2773		}
2774		return this.each(function( i ) {
2775			var queue = jQuery.queue( this, type, data );
2776
2777			if ( type === "fx" && queue[0] !== "inprogress" ) {
2778				jQuery.dequeue( this, type );
2779			}
2780		});
2781	},
2782	dequeue: function( type ) {
2783		return this.each(function() {
2784			jQuery.dequeue( this, type );
2785		});
2786	},
2787
2788	// Based off of the plugin by Clint Helfers, with permission.
2789	// http://blindsignals.com/index.php/2009/07/jquery-delay/
2790	delay: function( time, type ) {
2791		time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
2792		type = type || "fx";
2793
2794		return this.queue( type, function() {
2795			var elem = this;
2796			setTimeout(function() {
2797				jQuery.dequeue( elem, type );
2798			}, time );
2799		});
2800	},
2801
2802	clearQueue: function( type ) {
2803		return this.queue( type || "fx", [] );
2804	}
2805});
2806
2807
2808
2809
2810var rclass = /[\n\t\r]/g,
2811	rspaces = /\s+/,
2812	rreturn = /\r/g,
2813	rspecialurl = /^(?:href|src|style)$/,
2814	rtype = /^(?:button|input)$/i,
2815	rfocusable = /^(?:button|input|object|select|textarea)$/i,
2816	rclickable = /^a(?:rea)?$/i,
2817	rradiocheck = /^(?:radio|checkbox)$/i;
2818
2819jQuery.props = {
2820	"for": "htmlFor",
2821	"class": "className",
2822	readonly: "readOnly",
2823	maxlength: "maxLength",
2824	cellspacing: "cellSpacing",
2825	rowspan: "rowSpan",
2826	colspan: "colSpan",
2827	tabindex: "tabIndex",
2828	usemap: "useMap",
2829	frameborder: "frameBorder"
2830};
2831
2832jQuery.fn.extend({
2833	attr: function( name, value ) {
2834		return jQuery.access( this, name, value, true, jQuery.attr );
2835	},
2836
2837	removeAttr: function( name, fn ) {
2838		return this.each(function(){
2839			jQuery.attr( this, name, "" );
2840			if ( this.nodeType === 1 ) {
2841				this.removeAttribute( name );
2842			}
2843		});
2844	},
2845
2846	addClass: function( value ) {
2847		if ( jQuery.isFunction(value) ) {
2848			return this.each(function(i) {
2849				var self = jQuery(this);
2850				self.addClass( value.call(this, i, self.attr("class")) );
2851			});
2852		}
2853
2854		if ( value && typeof value === "string" ) {
2855			var classNames = (value || "").split( rspaces );
2856
2857			for ( var i = 0, l = this.length; i < l; i++ ) {
2858				var elem = this[i];
2859
2860				if ( elem.nodeType === 1 ) {
2861					if ( !elem.className ) {
2862						elem.className = value;
2863
2864					} else {
2865						var className = " " + elem.className + " ",
2866							setClass = elem.className;
2867
2868						for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
2869							if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
2870								setClass += " " + classNames[c];
2871							}
2872						}
2873						elem.className = jQuery.trim( setClass );
2874					}
2875				}
2876			}
2877		}
2878
2879		return this;
2880	},
2881
2882	removeClass: function( value ) {
2883		if ( jQuery.isFunction(value) ) {
2884			return this.each(function(i) {
2885				var self = jQuery(this);
2886				self.removeClass( value.call(this, i, self.attr("class")) );
2887			});
2888		}
2889
2890		if ( (value && typeof value === "string") || value === undefined ) {
2891			var classNames = (value || "").split( rspaces );
2892
2893			for ( var i = 0, l = this.length; i < l; i++ ) {
2894				var elem = this[i];
2895
2896				if ( elem.nodeType === 1 && elem.className ) {
2897					if ( value ) {
2898						var className = (" " + elem.className + " ").replace(rclass, " ");
2899						for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
2900							className = className.replace(" " + classNames[c] + " ", " ");
2901						}
2902						elem.className = jQuery.trim( className );
2903
2904					} else {
2905						elem.className = "";
2906					}
2907				}
2908			}
2909		}
2910
2911		return this;
2912	},
2913
2914	toggleClass: function( value, stateVal ) {
2915		var type = typeof value,
2916			isBool = typeof stateVal === "boolean";
2917
2918		if ( jQuery.isFunction( value ) ) {
2919			return this.each(function(i) {
2920				var self = jQuery(this);
2921				self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
2922			});
2923		}
2924
2925		return this.each(function() {
2926			if ( type === "string" ) {
2927				// toggle individual class names
2928				var className,
2929					i = 0,
2930					self = jQuery( this ),
2931					state = stateVal,
2932					classNames = value.split( rspaces );
2933
2934				while ( (className = classNames[ i++ ]) ) {
2935					// check each className given, space seperated list
2936					state = isBool ? state : !self.hasClass( className );
2937					self[ state ? "addClass" : "removeClass" ]( className );
2938				}
2939
2940			} else if ( type === "undefined" || type === "boolean" ) {
2941				if ( this.className ) {
2942					// store className if set
2943					jQuery._data( this, "__className__", this.className );
2944				}
2945
2946				// toggle whole className
2947				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
2948			}
2949		});
2950	},
2951
2952	hasClass: function( selector ) {
2953		var className = " " + selector + " ";
2954		for ( var i = 0, l = this.length; i < l; i++ ) {
2955			if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
2956				return true;
2957			}
2958		}
2959
2960		return false;
2961	},
2962
2963	val: function( value ) {
2964		if ( !arguments.length ) {
2965			var elem = this[0];
2966
2967			if ( elem ) {
2968				if ( jQuery.nodeName( elem, "option" ) ) {
2969					// attributes.value is undefined in Blackberry 4.7 but
2970					// uses .value. See #6932
2971					var val = elem.attributes.value;
2972					return !val || val.specified ? elem.value : elem.text;
2973				}
2974
2975				// We need to handle select boxes special
2976				if ( jQuery.nodeName( elem, "select" ) ) {
2977					var index = elem.selectedIndex,
2978						values = [],
2979						options = elem.options,
2980						one = elem.type === "select-one";
2981
2982					// Nothing was selected
2983					if ( index < 0 ) {
2984						return null;
2985					}
2986
2987					// Loop through all the selected options
2988					for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
2989						var option = options[ i ];
2990
2991						// Don't return options that are disabled or in a disabled optgroup
2992						if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
2993								(!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
2994
2995							// Get the specific value for the option
2996							value = jQuery(option).val();
2997
2998							// We don't need an array for one selects
2999							if ( one ) {
3000								return value;
3001							}
3002
3003							// Multi-Selects return an array
3004							values.push( value );
3005						}
3006					}
3007
3008					// Fixes Bug #2551 -- select.val() broken in IE after form.reset()
3009					if ( one && !values.length && options.length ) {
3010						return jQuery( options[ index ] ).val();
3011					}
3012
3013					return values;
3014				}
3015
3016				// Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
3017				if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
3018					return elem.getAttribute("value") === null ? "on" : elem.value;
3019				}
3020
3021				// Everything else, we just grab the value
3022				return (elem.value || "").replace(rreturn, "");
3023
3024			}
3025
3026			return undefined;
3027		}
3028
3029		var isFunction = jQuery.isFunction(value);
3030
3031		return this.each(function(i) {
3032			var self = jQuery(this), val = value;
3033
3034			if ( this.nodeType !== 1 ) {
3035				return;
3036			}
3037
3038			if ( isFunction ) {
3039				val = value.call(this, i, self.val());
3040			}
3041
3042			// Treat null/undefined as ""; convert numbers to string
3043			if ( val == null ) {
3044				val = "";
3045			} else if ( typeof val === "number" ) {
3046				val += "";
3047			} else if ( jQuery.isArray(val) ) {
3048				val = jQuery.map(val, function (value) {
3049					return value == null ? "" : value + "";
3050				});
3051			}
3052
3053			if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
3054				this.checked = jQuery.inArray( self.val(), val ) >= 0;
3055
3056			} else if ( jQuery.nodeName( this, "select" ) ) {
3057				var values = jQuery.makeArray(val);
3058
3059				jQuery( "option", this ).each(function() {
3060					this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
3061				});
3062
3063				if ( !values.length ) {
3064					this.selectedIndex = -1;
3065				}
3066
3067			} else {
3068				this.value = val;
3069			}
3070		});
3071	}
3072});
3073
3074jQuery.extend({
3075	attrFn: {
3076		val: true,
3077		css: true,
3078		html: true,
3079		text: true,
3080		data: true,
3081		width: true,
3082		height: true,
3083		offset: true
3084	},
3085
3086	attr: function( elem, name, value, pass ) {
3087		// don't get/set attributes on text, comment and attribute nodes
3088		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) {
3089			return undefined;
3090		}
3091
3092		if ( pass && name in jQuery.attrFn ) {
3093			return jQuery(elem)[name](value);
3094		}
3095
3096		var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ),
3097			// Whether we are setting (or getting)
3098			set = value !== undefined;
3099
3100		// Try to normalize/fix the name
3101		name = notxml && jQuery.props[ name ] || name;
3102
3103		// Only do all the following if this is a node (faster for style)
3104		if ( elem.nodeType === 1 ) {
3105			// These attributes require special treatment
3106			var special = rspecialurl.test( name );
3107
3108			// Safari mis-reports the default selected property of an option
3109			// Accessing the parent's selectedIndex property fixes it
3110			if ( name === "selected" && !jQuery.support.optSelected ) {
3111				var parent = elem.parentNode;
3112				if ( parent ) {
3113					parent.selectedIndex;
3114
3115					// Make sure that it also works with optgroups, see #5701
3116					if ( parent.parentNode ) {
3117						parent.parentNode.selectedIndex;
3118					}
3119				}
3120			}
3121
3122			// If applicable, access the attribute via the DOM 0 way
3123			// 'in' checks fail in Blackberry 4.7 #6931
3124			if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) {
3125				if ( set ) {
3126					// We can't allow the type property to be changed (since it causes problems in IE)
3127					if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) {
3128						jQuery.error( "type property can't be changed" );
3129					}
3130
3131					if ( value === null ) {
3132						if ( elem.nodeType === 1 ) {
3133							elem.removeAttribute( name );
3134						}
3135
3136					} else {
3137						elem[ name ] = value;
3138					}
3139				}
3140
3141				// browsers index elements by id/name on forms, give priority to attributes.
3142				if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) {
3143					return elem.getAttributeNode( name ).nodeValue;
3144				}
3145
3146				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
3147				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
3148				if ( name === "tabIndex" ) {
3149					var attributeNode = elem.getAttributeNode( "tabIndex" );
3150
3151					return attributeNode && attributeNode.specified ?
3152						attributeNode.value :
3153						rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
3154							0 :
3155							undefined;
3156				}
3157
3158				return elem[ name ];
3159			}
3160
3161			if ( !jQuery.support.style && notxml && name === "style" ) {
3162				if ( set ) {
3163					elem.style.cssText = "" + value;
3164				}
3165
3166				return elem.style.cssText;
3167			}
3168
3169			if ( set ) {
3170				// convert the value to a string (all browsers do this but IE) see #1070
3171				elem.setAttribute( name, "" + value );
3172			}
3173
3174			// Ensure that missing attributes return undefined
3175			// Blackberry 4.7 returns "" from getAttribute #6938
3176			if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) {
3177				return undefined;
3178			}
3179
3180			var attr = !jQuery.support.hrefNormalized && notxml && special ?
3181					// Some attributes require a special call on IE
3182					elem.getAttribute( name, 2 ) :
3183					elem.getAttribute( name );
3184
3185			// Non-existent attributes return null, we normalize to undefined
3186			return attr === null ? undefined : attr;
3187		}
3188		// Handle everything which isn't a DOM element node
3189		if ( set ) {
3190			elem[ name ] = value;
3191		}
3192		return elem[ name ];
3193	}
3194});
3195
3196
3197
3198
3199var rnamespaces = /\.(.*)$/,
3200	rformElems = /^(?:textarea|input|select)$/i,
3201	rperiod = /\./g,
3202	rspace = / /g,
3203	rescape = /[^\w\s.|`]/g,
3204	fcleanup = function( nm ) {
3205		return nm.replace(rescape, "\\$&");
3206	};
3207
3208/*
3209 * A number of helper functions used for managing events.
3210 * Many of the ideas behind this code originated from
3211 * Dean Edwards' addEvent library.
3212 */
3213jQuery.event = {
3214
3215	// Bind an event to an element
3216	// Original by Dean Edwards
3217	add: function( elem, types, handler, data ) {
3218		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
3219			return;
3220		}
3221
3222		// TODO :: Use a try/catch until it's safe to pull this out (likely 1.6)
3223		// Minor release fix for bug #8018
3224		try {
3225			// For whatever reason, IE has trouble passing the window object
3226			// around, causing it to be cloned in the process
3227			if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) {
3228				elem = window;
3229			}
3230		}
3231		catch ( e ) {}
3232
3233		if ( handler === false ) {
3234			handler = returnFalse;
3235		} else if ( !handler ) {
3236			// Fixes bug #7229. Fix recommended by jdalton
3237			return;
3238		}
3239
3240		var handleObjIn, handleObj;
3241
3242		if ( handler.handler ) {
3243			handleObjIn = handler;
3244			handler = handleObjIn.handler;
3245		}
3246
3247		// Make sure that the function being executed has a unique ID
3248		if ( !handler.guid ) {
3249			handler.guid = jQuery.guid++;
3250		}
3251
3252		// Init the element's event structure
3253		var elemData = jQuery._data( elem );
3254
3255		// If no elemData is found then we must be trying to bind to one of the
3256		// banned noData elements
3257		if ( !elemData ) {
3258			return;
3259		}
3260
3261		var events = elemData.events,
3262			eventHandle = elemData.handle;
3263
3264		if ( !events ) {
3265			elemData.events = events = {};
3266		}
3267
3268		if ( !eventHandle ) {
3269			elemData.handle = eventHandle = function( e ) {
3270				// Handle the second event of a trigger and when
3271				// an event is called after a page has unloaded
3272				return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
3273					jQuery.event.handle.apply( eventHandle.elem, arguments ) :
3274					undefined;
3275			};
3276		}
3277
3278		// Add elem as a property of the handle function
3279		// This is to prevent a memory leak with non-native events in IE.
3280		eventHandle.elem = elem;
3281
3282		// Handle multiple events separated by a space
3283		// jQuery(...).bind("mouseover mouseout", fn);
3284		types = types.split(" ");
3285
3286		var type, i = 0, namespaces;
3287
3288		while ( (type = types[ i++ ]) ) {
3289			handleObj = handleObjIn ?
3290				jQuery.extend({}, handleObjIn) :
3291				{ handler: handler, data: data };
3292
3293			// Namespaced event handlers
3294			if ( type.indexOf(".") > -1 ) {
3295				namespaces = type.split(".");
3296				type = namespaces.shift();
3297				handleObj.namespace = namespaces.slice(0).sort().join(".");
3298
3299			} else {
3300				namespaces = [];
3301				handleObj.namespace = "";
3302			}
3303
3304			handleObj.type = type;
3305			if ( !handleObj.guid ) {
3306				handleObj.guid = handler.guid;
3307			}
3308
3309			// Get the current list of functions bound to this event
3310			var handlers = events[ type ],
3311				special = jQuery.event.special[ type ] || {};
3312
3313			// Init the event handler queue
3314			if ( !handlers ) {
3315				handlers = events[ type ] = [];
3316
3317				// Check for a special event handler
3318				// Only use addEventListener/attachEvent if the special
3319				// events handler returns false
3320				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
3321					// Bind the global event handler to the element
3322					if ( elem.addEventListener ) {
3323						elem.addEventListener( type, eventHandle, false );
3324
3325					} else if ( elem.attachEvent ) {
3326						elem.attachEvent( "on" + type, eventHandle );
3327					}
3328				}
3329			}
3330
3331			if ( special.add ) {
3332				special.add.call( elem, handleObj );
3333
3334				if ( !handleObj.handler.guid ) {
3335					handleObj.handler.guid = handler.guid;
3336				}
3337			}
3338
3339			// Add the function to the element's handler list
3340			handlers.push( handleObj );
3341
3342			// Keep track of which events have been used, for global triggering
3343			jQuery.event.global[ type ] = true;
3344		}
3345
3346		// Nullify elem to prevent memory leaks in IE
3347		elem = null;
3348	},
3349
3350	global: {},
3351
3352	// Detach an event or set of events from an element
3353	remove: function( elem, types, handler, pos ) {
3354		// don't do events on text and comment nodes
3355		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
3356			return;
3357		}
3358
3359		if ( handler === false ) {
3360			handler = returnFalse;
3361		}
3362
3363		var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
3364			elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
3365			events = elemData && elemData.events;
3366
3367		if ( !elemData || !events ) {
3368			return;
3369		}
3370
3371		// types is actually an event object here
3372		if ( types && types.type ) {
3373			handler = types.handler;
3374			types = types.type;
3375		}
3376
3377		// Unbind all events for the element
3378		if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
3379			types = types || "";
3380
3381			for ( type in events ) {
3382				jQuery.event.remove( elem, type + types );
3383			}
3384
3385			return;
3386		}
3387
3388		// Handle multiple events separated by a space
3389		// jQuery(...).unbind("mouseover mouseout", fn);
3390		types = types.split(" ");
3391
3392		while ( (type = types[ i++ ]) ) {
3393			origType = type;
3394			handleObj = null;
3395			all = type.indexOf(".") < 0;
3396			namespaces = [];
3397
3398			if ( !all ) {
3399				// Namespaced event handlers
3400				namespaces = type.split(".");
3401				type = namespaces.shift();
3402
3403				namespace = new RegExp("(^|\\.)" +
3404					jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)");
3405			}
3406
3407			eventType = events[ type ];
3408
3409			if ( !eventType ) {
3410				continue;
3411			}
3412
3413			if ( !handler ) {
3414				for ( j = 0; j < eventType.length; j++ ) {
3415					handleObj = eventType[ j ];
3416
3417					if ( all || namespace.test( handleObj.namespace ) ) {
3418						jQuery.event.remove( elem, origType, handleObj.handler, j );
3419						eventType.splice( j--, 1 );
3420					}
3421				}
3422
3423				continue;
3424			}
3425
3426			special = jQuery.event.special[ type ] || {};
3427
3428			for ( j = pos || 0; j < eventType.length; j++ ) {
3429				handleObj = eventType[ j ];
3430
3431				if ( handler.guid === handleObj.guid ) {
3432					// remove the given handler for the given type
3433					if ( all || namespace.test( handleObj.namespace ) ) {
3434						if ( pos == null ) {
3435							eventType.splice( j--, 1 );
3436						}
3437
3438						if ( special.remove ) {
3439							special.remove.call( elem, handleObj );
3440						}
3441					}
3442
3443					if ( pos != null ) {
3444						break;
3445					}
3446				}
3447			}
3448
3449			// remove generic event handler if no more handlers exist
3450			if ( eventType.length === 0 || pos != null && eventType.length === 1 ) {
3451				if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
3452					jQuery.removeEvent( elem, type, elemData.handle );
3453				}
3454
3455				ret = null;
3456				delete events[ type ];
3457			}
3458		}
3459
3460		// Remove the expando if it's no longer used
3461		if ( jQuery.isEmptyObject( events ) ) {
3462			var handle = elemData.handle;
3463			if ( handle ) {
3464				handle.elem = null;
3465			}
3466
3467			delete elemData.events;
3468			delete elemData.handle;
3469
3470			if ( jQuery.isEmptyObject( elemData ) ) {
3471				jQuery.removeData( elem, undefined, true );
3472			}
3473		}
3474	},
3475
3476	// bubbling is internal
3477	trigger: function( event, data, elem /*, bubbling */ ) {
3478		// Event object or event type
3479		var type = event.type || event,
3480			bubbling = arguments[3];
3481
3482		if ( !bubbling ) {
3483			event = typeof event === "object" ?
3484				// jQuery.Event object
3485				event[ jQuery.expando ] ? event :
3486				// Object literal
3487				jQuery.extend( jQuery.Event(type), event ) :
3488				// Just the event type (string)
3489				jQuery.Event(type);
3490
3491			if ( type.indexOf("!") >= 0 ) {
3492				event.type = type = type.slice(0, -1);
3493				event.exclusive = true;
3494			}
3495
3496			// Handle a global trigger
3497			if ( !elem ) {
3498				// Don't bubble custom events when global (to avoid too much overhead)
3499				event.stopPropagation();
3500
3501				// Only trigger if we've ever bound an event for it
3502				if ( jQuery.event.global[ type ] ) {
3503					// XXX This code smells terrible. event.js should not be directly
3504					// inspecting the data cache
3505					jQuery.each( jQuery.cache, function() {
3506						// internalKey variable is just used to make it easier to find
3507						// and potentially change this stuff later; currently it just
3508						// points to jQuery.expando
3509						var internalKey = jQuery.expando,
3510							internalCache = this[ internalKey ];
3511						if ( internalCache && internalCache.events && internalCache.events[ type ] ) {
3512							jQuery.event.trigger( event, data, internalCache.handle.elem );
3513						}
3514					});
3515				}
3516			}
3517
3518			// Handle triggering a single element
3519
3520			// don't do events on text and comment nodes
3521			if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
3522				return undefined;
3523			}
3524
3525			// Clean up in case it is reused
3526			event.result = undefined;
3527			event.target = elem;
3528
3529			// Clone the incoming data, if any
3530			data = jQuery.makeArray( data );
3531			data.unshift( event );
3532		}
3533
3534		event.currentTarget = elem;
3535
3536		// Trigger the event, it is assumed that "handle" is a function
3537		var handle = jQuery._data( elem, "handle" );
3538
3539		if ( handle ) {
3540			handle.apply( elem, data );
3541		}
3542
3543		var parent = elem.parentNode || elem.ownerDocument;
3544
3545		// Trigger an inline bound script
3546		try {
3547			if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
3548				if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
3549					event.result = false;
3550					event.preventDefault();
3551				}
3552			}
3553
3554		// prevent IE from throwing an error for some elements with some event types, see #3533
3555		} catch (inlineError) {}
3556
3557		if ( !event.isPropagationStopped() && parent ) {
3558			jQuery.event.trigger( event, data, parent, true );
3559
3560		} else if ( !event.isDefaultPrevented() ) {
3561			var old,
3562				target = event.target,
3563				targetType = type.replace( rnamespaces, "" ),
3564				isClick = jQuery.nodeName( target, "a" ) && targetType === "click",
3565				special = jQuery.event.special[ targetType ] || {};
3566
3567			if ( (!special._default || special._default.call( elem, event ) === false) &&
3568				!isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
3569
3570				try {
3571					if ( target[ targetType ] ) {
3572						// Make sure that we don't accidentally re-trigger the onFOO events
3573						old = target[ "on" + targetType ];
3574
3575						if ( old ) {
3576							target[ "on" + targetType ] = null;
3577						}
3578
3579						jQuery.event.triggered = event.type;
3580						target[ targetType ]();
3581					}
3582
3583				// prevent IE from throwing an error for some elements with some event types, see #3533
3584				} catch (triggerError) {}
3585
3586				if ( old ) {
3587					target[ "on" + targetType ] = old;
3588				}
3589
3590				jQuery.event.triggered = undefined;
3591			}
3592		}
3593	},
3594
3595	handle: function( event ) {
3596		var all, handlers, namespaces, namespace_re, events,
3597			namespace_sort = [],
3598			args = jQuery.makeArray( arguments );
3599
3600		event = args[0] = jQuery.event.fix( event || window.event );
3601		event.currentTarget = this;
3602
3603		// Namespaced event handlers
3604		all = event.type.indexOf(".") < 0 && !event.exclusive;
3605
3606		if ( !all ) {
3607			namespaces = event.type.split(".");
3608			event.type = namespaces.shift();
3609			namespace_sort = namespaces.slice(0).sort();
3610			namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)");
3611		}
3612
3613		event.namespace = event.namespace || namespace_sort.join(".");
3614
3615		events = jQuery._data(this, "events");
3616
3617		handlers = (events || {})[ event.type ];
3618
3619		if ( events && handlers ) {
3620			// Clone the handlers to prevent manipulation
3621			handlers = handlers.slice(0);
3622
3623			for ( var j = 0, l = handlers.length; j < l; j++ ) {
3624				var handleObj = handlers[ j ];
3625
3626				// Filter the functions by class
3627				if ( all || namespace_re.test( handleObj.namespace ) ) {
3628					// Pass in a reference to the handler function itself
3629					// So that we can later remove it
3630					event.handler = handleObj.handler;
3631					event.data = handleObj.data;
3632					event.handleObj = handleObj;
3633
3634					var ret = handleObj.handler.apply( this, args );
3635
3636					if ( ret !== undefined ) {
3637						event.result = ret;
3638						if ( ret === false ) {
3639							event.preventDefault();
3640							event.stopPropagation();
3641						}
3642					}
3643
3644					if ( event.isImmediatePropagationStopped() ) {
3645						break;
3646					}
3647				}
3648			}
3649		}
3650
3651		return event.result;
3652	},
3653
3654	props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
3655
3656	fix: function( event ) {
3657		if ( event[ jQuery.expando ] ) {
3658			return event;
3659		}
3660
3661		// store a copy of the original event object
3662		// and "clone" to set read-only properties
3663		var originalEvent = event;
3664		event = jQuery.Event( originalEvent );
3665
3666		for ( var i = this.props.length, prop; i; ) {
3667			prop = this.props[ --i ];
3668			event[ prop ] = originalEvent[ prop ];
3669		}
3670
3671		// Fix target property, if necessary
3672		if ( !event.target ) {
3673			// Fixes #1925 where srcElement might not be defined either
3674			event.target = event.srcElement || document;
3675		}
3676
3677		// check if target is a textnode (safari)
3678		if ( event.target.nodeType === 3 ) {
3679			event.target = event.target.parentNode;
3680		}
3681
3682		// Add relatedTarget, if necessary
3683		if ( !event.relatedTarget && event.fromElement ) {
3684			event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
3685		}
3686
3687		// Calculate pageX/Y if missing and clientX/Y available
3688		if ( event.pageX == null && event.clientX != null ) {
3689			var doc = document.documentElement,
3690				body = document.body;
3691
3692			event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
3693			event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
3694		}
3695
3696		// Add which for key events
3697		if ( event.which == null && (event.charCode != null || event.keyCode != null) ) {
3698			event.which = event.charCode != null ? event.charCode : event.keyCode;
3699		}
3700
3701		// Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
3702		if ( !event.metaKey && event.ctrlKey ) {
3703			event.metaKey = event.ctrlKey;
3704		}
3705
3706		// Add which for click: 1 === left; 2 === middle; 3 === right
3707		// Note: button is not normalized, so don't use it
3708		if ( !event.which && event.button !== undefined ) {
3709			event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
3710		}
3711
3712		return event;
3713	},
3714
3715	// Deprecated, use jQuery.guid instead
3716	guid: 1E8,
3717
3718	// Deprecated, use jQuery.proxy instead
3719	proxy: jQuery.proxy,
3720
3721	special: {
3722		ready: {
3723			// Make sure the ready event is setup
3724			setup: jQuery.bindReady,
3725			teardown: jQuery.noop
3726		},
3727
3728		live: {
3729			add: function( handleObj ) {
3730				jQuery.event.add( this,
3731					liveConvert( handleObj.origType, handleObj.selector ),
3732					jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) );
3733			},
3734
3735			remove: function( handleObj ) {
3736				jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj );
3737			}
3738		},
3739
3740		beforeunload: {
3741			setup: function( data, namespaces, eventHandle ) {
3742				// We only want to do this special case on windows
3743				if ( jQuery.isWindow( this ) ) {
3744					this.onbeforeunload = eventHandle;
3745				}
3746			},
3747
3748			teardown: function( namespaces, eventHandle ) {
3749				if ( this.onbeforeunload === eventHandle ) {
3750					this.onbeforeunload = null;
3751				}
3752			}
3753		}
3754	}
3755};
3756
3757jQuery.removeEvent = document.removeEventListener ?
3758	function( elem, type, handle ) {
3759		if ( elem.removeEventListener ) {
3760			elem.removeEventListener( type, handle, false );
3761		}
3762	} :
3763	function( elem, type, handle ) {
3764		if ( elem.detachEvent ) {
3765			elem.detachEvent( "on" + type, handle );
3766		}
3767	};
3768
3769jQuery.Event = function( src ) {
3770	// Allow instantiation without the 'new' keyword
3771	if ( !this.preventDefault ) {
3772		return new jQuery.Event( src );
3773	}
3774
3775	// Event object
3776	if ( src && src.type ) {
3777		this.originalEvent = src;
3778		this.type = src.type;
3779
3780		// Events bubbling up the document may have been marked as prevented
3781		// by a handler lower down the tree; reflect the correct value.
3782		this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
3783			src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
3784
3785	// Event type
3786	} else {
3787		this.type = src;
3788	}
3789
3790	// timeStamp is buggy for some events on Firefox(#3843)
3791	// So we won't rely on the native value
3792	this.timeStamp = jQuery.now();
3793
3794	// Mark it as fixed
3795	this[ jQuery.expando ] = true;
3796};
3797
3798function returnFalse() {
3799	return false;
3800}
3801function returnTrue() {
3802	return true;
3803}
3804
3805// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
3806// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
3807jQuery.Event.prototype = {
3808	preventDefault: function() {
3809		this.isDefaultPrevented = returnTrue;
3810
3811		var e = this.originalEvent;
3812		if ( !e ) {
3813			return;
3814		}
3815
3816		// if preventDefault exists run it on the original event
3817		if ( e.preventDefault ) {
3818			e.preventDefault();
3819
3820		// otherwise set the returnValue property of the original event to false (IE)
3821		} else {
3822			e.returnValue = false;
3823		}
3824	},
3825	stopPropagation: function() {
3826		this.isPropagationStopped = returnTrue;
3827
3828		var e = this.originalEvent;
3829		if ( !e ) {
3830			return;
3831		}
3832		// if stopPropagation exists run it on the original event
3833		if ( e.stopPropagation ) {
3834			e.stopPropagation();
3835		}
3836		// otherwise set the cancelBubble property of the original event to true (IE)
3837		e.cancelBubble = true;
3838	},
3839	stopImmediatePropagation: function() {
3840		this.isImmediatePropagationStopped = returnTrue;
3841		this.stopPropagation();
3842	},
3843	isDefaultPrevented: returnFalse,
3844	isPropagationStopped: returnFalse,
3845	isImmediatePropagationStopped: returnFalse
3846};
3847
3848// Checks if an event happened on an element within another element
3849// Used in jQuery.event.special.mouseenter and mouseleave handlers
3850var withinElement = function( event ) {
3851	// Check if mouse(over|out) are still within the same parent element
3852	var parent = event.relatedTarget;
3853
3854	// Firefox sometimes assigns relatedTarget a XUL element
3855	// which we cannot access the parentNode property of
3856	try {
3857
3858		// Chrome does something similar, the parentNode property
3859		// can be accessed but is null.
3860		if ( parent && parent !== document && !parent.parentNode ) {
3861			return;
3862		}
3863		// Traverse up the tree
3864		while ( parent && parent !== this ) {
3865			parent = parent.parentNode;
3866		}
3867
3868		if ( parent !== this ) {
3869			// set the correct event type
3870			event.type = event.data;
3871
3872			// handle event if we actually just moused on to a non sub-element
3873			jQuery.event.handle.apply( this, arguments );
3874		}
3875
3876	// assuming we've left the element since we most likely mousedover a xul element
3877	} catch(e) { }
3878},
3879
3880// In case of event delegation, we only need to rename the event.type,
3881// liveHandler will take care of the rest.
3882delegate = function( event ) {
3883	event.type = event.data;
3884	jQuery.event.handle.apply( this, arguments );
3885};
3886
3887// Create mouseenter and mouseleave events
3888jQuery.each({
3889	mouseenter: "mouseover",
3890	mouseleave: "mouseout"
3891}, function( orig, fix ) {
3892	jQuery.event.special[ orig ] = {
3893		setup: function( data ) {
3894			jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
3895		},
3896		teardown: function( data ) {
3897			jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
3898		}
3899	};
3900});
3901
3902// submit delegation
3903if ( !jQuery.support.submitBubbles ) {
3904
3905	jQuery.event.special.submit = {
3906		setup: function( data, namespaces ) {
3907			if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) {
3908				jQuery.event.add(this, "click.specialSubmit", function( e ) {
3909					var elem = e.target,
3910						type = elem.type;
3911
3912					if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
3913						trigger( "submit", this, arguments );
3914					}
3915				});
3916
3917				jQuery.event.add(this, "keypress.specialSubmit", function( e ) {
3918					var elem = e.target,
3919						type = elem.type;
3920
3921					if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
3922						trigger( "submit", this, arguments );
3923					}
3924				});
3925
3926			} else {
3927				return false;
3928			}
3929		},
3930
3931		teardown: function( namespaces ) {
3932			jQuery.event.remove( this, ".specialSubmit" );
3933		}
3934	};
3935
3936}
3937
3938// change delegation, happens here so we have bind.
3939if ( !jQuery.support.changeBubbles ) {
3940
3941	var changeFilters,
3942
3943	getVal = function( elem ) {
3944		var type = elem.type, val = elem.value;
3945
3946		if ( type === "radio" || type === "checkbox" ) {
3947			val = elem.checked;
3948
3949		} else if ( type === "select-multiple" ) {
3950			val = elem.selectedIndex > -1 ?
3951				jQuery.map( elem.options, function( elem ) {
3952					return elem.selected;
3953				}).join("-") :
3954				"";
3955
3956		} else if ( elem.nodeName.toLowerCase() === "select" ) {
3957			val = elem.selectedIndex;
3958		}
3959
3960		return val;
3961	},
3962
3963	testChange = function testChange( e ) {
3964		var elem = e.target, data, val;
3965
3966		if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) {
3967			return;
3968		}
3969
3970		data = jQuery._data( elem, "_change_data" );
3971		val = getVal(elem);
3972
3973		// the current data will be also retrieved by beforeactivate
3974		if ( e.type !== "focusout" || elem.type !== "radio" ) {
3975			jQuery._data( elem, "_change_data", val );
3976		}
3977
3978		if ( data === undefined || val === data ) {
3979			return;
3980		}
3981
3982		if ( data != null || val ) {
3983			e.type = "change";
3984			e.liveFired = undefined;
3985			jQuery.event.trigger( e, arguments[1], elem );
3986		}
3987	};
3988
3989	jQuery.event.special.change = {
3990		filters: {
3991			focusout: testChange,
3992
3993			beforedeactivate: testChange,
3994
3995			click: function( e ) {
3996				var elem = e.target, type = elem.type;
3997
3998				if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
3999					testChange.call( this, e );
4000				}
4001			},
4002
4003			// Change has to be called before submit
4004			// Keydown will be called before keypress, which is used in submit-event delegation
4005			keydown: function( e ) {
4006				var elem = e.target, type = elem.type;
4007
4008				if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
4009					(e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
4010					type === "select-multiple" ) {
4011					testChange.call( this, e );
4012				}
4013			},
4014
4015			// Beforeactivate happens also before the previous element is blurred
4016			// with this event you can't trigger a change event, but you can store
4017			// information
4018			beforeactivate: function( e ) {
4019				var elem = e.target;
4020				jQuery._data( elem, "_change_data", getVal(elem) );
4021			}
4022		},
4023
4024		setup: function( data, namespaces ) {
4025			if ( this.type === "file" ) {
4026				return false;
4027			}
4028
4029			for ( var type in changeFilters ) {
4030				jQuery.event.add( this, type + ".specialChange", changeFilters[type] );
4031			}
4032
4033			return rformElems.test( this.nodeName );
4034		},
4035
4036		teardown: function( namespaces ) {
4037			jQuery.event.remove( this, ".specialChange" );
4038
4039			return rformElems.test( this.nodeName );
4040		}
4041	};
4042
4043	changeFilters = jQuery.event.special.change.filters;
4044
4045	// Handle when the input is .focus()'d
4046	changeFilters.focus = changeFilters.beforeactivate;
4047}
4048
4049function trigger( type, elem, args ) {
4050	// Piggyback on a donor event to simulate a different one.
4051	// Fake originalEvent to avoid donor's stopPropagation, but if the
4052	// simulated event prevents default then we do the same on the donor.
4053	// Don't pass args or remember liveFired; they apply to the donor event.
4054	var event = jQuery.extend( {}, args[ 0 ] );
4055	event.type = type;
4056	event.originalEvent = {};
4057	event.liveFired = undefined;
4058	jQuery.event.handle.call( elem, event );
4059	if ( event.isDefaultPrevented() ) {
4060		args[ 0 ].preventDefault();
4061	}
4062}
4063
4064// Create "bubbling" focus and blur events
4065if ( document.addEventListener ) {
4066	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
4067
4068		// Attach a single capturing handler while someone wants focusin/focusout
4069		var attaches = 0;
4070
4071		jQuery.event.special[ fix ] = {
4072			setup: function() {
4073				if ( attaches++ === 0 ) {
4074					document.addEventListener( orig, handler, true );
4075				}
4076			},
4077			teardown: function() {
4078				if ( --attaches === 0 ) {
4079					document.removeEventListener( orig, handler, true );
4080				}
4081			}
4082		};
4083
4084		function handler( donor ) {
4085			// Donor event is always a native one; fix it and switch its type.
4086			// Let focusin/out handler cancel the donor focus/blur event.
4087			var e = jQuery.event.fix( donor );
4088			e.type = fix;
4089			e.originalEvent = {};
4090			jQuery.event.trigger( e, null, e.target );
4091			if ( e.isDefaultPrevented() ) {
4092				donor.preventDefault();
4093			}
4094		}
4095	});
4096}
4097
4098jQuery.each(["bind", "one"], function( i, name ) {
4099	jQuery.fn[ name ] = function( type, data, fn ) {
4100		// Handle object literals
4101		if ( typeof type === "object" ) {
4102			for ( var key in type ) {
4103				this[ name ](key, data, type[key], fn);
4104			}
4105			return this;
4106		}
4107
4108		if ( jQuery.isFunction( data ) || data === false ) {
4109			fn = data;
4110			data = undefined;
4111		}
4112
4113		var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
4114			jQuery( this ).unbind( event, handler );
4115			return fn.apply( this, arguments );
4116		}) : fn;
4117
4118		if ( type === "unload" && name !== "one" ) {
4119			this.one( type, data, fn );
4120
4121		} else {
4122			for ( var i = 0, l = this.length; i < l; i++ ) {
4123				jQuery.event.add( this[i], type, handler, data );
4124			}
4125		}
4126
4127		return this;
4128	};
4129});
4130
4131jQuery.fn.extend({
4132	unbind: function( type, fn ) {
4133		// Handle object literals
4134		if ( typeof type === "object" && !type.preventDefault ) {
4135			for ( var key in type ) {
4136				this.unbind(key, type[key]);
4137			}
4138
4139		} else {
4140			for ( var i = 0, l = this.length; i < l; i++ ) {
4141				jQuery.event.remove( this[i], type, fn );
4142			}
4143		}
4144
4145		return this;
4146	},
4147
4148	delegate: function( selector, types, data, fn ) {
4149		return this.live( types, data, fn, selector );
4150	},
4151
4152	undelegate: function( selector, types, fn ) {
4153		if ( arguments.length === 0 ) {
4154				return this.unbind( "live" );
4155
4156		} else {
4157			return this.die( types, null, fn, selector );
4158		}
4159	},
4160
4161	trigger: function( type, data ) {
4162		return this.each(function() {
4163			jQuery.event.trigger( type, data, this );
4164		});
4165	},
4166
4167	triggerHandler: function( type, data ) {
4168		if ( this[0] ) {
4169			var event = jQuery.Event( type );
4170			event.preventDefault();
4171			event.stopPropagation();
4172			jQuery.event.trigger( event, data, this[0] );
4173			return event.result;
4174		}
4175	},
4176
4177	toggle: function( fn ) {
4178		// Save reference to arguments for access in closure
4179		var args = arguments,
4180			i = 1;
4181
4182		// link all the functions, so any of them can unbind this click handler
4183		while ( i < args.length ) {
4184			jQuery.proxy( fn, args[ i++ ] );
4185		}
4186
4187		return this.click( jQuery.proxy( fn, function( event ) {
4188			// Figure out which function to execute
4189			var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
4190			jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
4191
4192			// Make sure that clicks stop
4193			event.preventDefault();
4194
4195			// and execute the function
4196			return args[ lastToggle ].apply( this, arguments ) || false;
4197		}));
4198	},
4199
4200	hover: function( fnOver, fnOut ) {
4201		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
4202	}
4203});
4204
4205var liveMap = {
4206	focus: "focusin",
4207	blur: "focusout",
4208	mouseenter: "mouseover",
4209	mouseleave: "mouseout"
4210};
4211
4212jQuery.each(["live", "die"], function( i, name ) {
4213	jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) {
4214		var type, i = 0, match, namespaces, preType,
4215			selector = origSelector || this.selector,
4216			context = origSelector ? this : jQuery( this.context );
4217
4218		if ( typeof types === "object" && !types.preventDefault ) {
4219			for ( var key in types ) {
4220				context[ name ]( key, data, types[key], selector );
4221			}
4222
4223			return this;
4224		}
4225
4226		if ( jQuery.isFunction( data ) ) {
4227			fn = data;
4228			data = undefined;
4229		}
4230
4231		types = (types || "").split(" ");
4232
4233		while ( (type = types[ i++ ]) != null ) {
4234			match = rnamespaces.exec( type );
4235			namespaces = "";
4236
4237			if ( match )  {
4238				namespaces = match[0];
4239				type = type.replace( rnamespaces, "" );
4240			}
4241
4242			if ( type === "hover" ) {
4243				types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
4244				continue;
4245			}
4246
4247			preType = type;
4248
4249			if ( type === "focus" || type === "blur" ) {
4250				types.push( liveMap[ type ] + namespaces );
4251				type = type + namespaces;
4252
4253			} else {
4254				type = (liveMap[ type ] || type) + namespaces;
4255			}
4256
4257			if ( name === "live" ) {
4258				// bind live handler
4259				for ( var j = 0, l = context.length; j < l; j++ ) {
4260					jQuery.event.add( context[j], "live." + liveConvert( type, selector ),
4261						{ data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
4262				}
4263
4264			} else {
4265				// unbind live handler
4266				context.unbind( "live." + liveConvert( type, selector ), fn );
4267			}
4268		}
4269
4270		return this;
4271	};
4272});
4273
4274function liveHandler( event ) {
4275	var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
4276		elems = [],
4277		selectors = [],
4278		events = jQuery._data( this, "events" );
4279
4280	// Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911)
4281	if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) {
4282		return;
4283	}
4284
4285	if ( event.namespace ) {
4286		namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)");
4287	}
4288
4289	event.liveFired = this;
4290
4291	var live = events.live.slice(0);
4292
4293	for ( j = 0; j < live.length; j++ ) {
4294		handleObj = live[j];
4295
4296		if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) {
4297			selectors.push( handleObj.selector );
4298
4299		} else {
4300			live.splice( j--, 1 );
4301		}
4302	}
4303
4304	match = jQuery( event.target ).closest( selectors, event.currentTarget );
4305
4306	for ( i = 0, l = match.length; i < l; i++ ) {
4307		close = match[i];
4308
4309		for ( j = 0; j < live.length; j++ ) {
4310			handleObj = live[j];
4311
4312			if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) {
4313				elem = close.elem;
4314				related = null;
4315
4316				// Those two events require additional checking
4317				if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) {
4318					event.type = handleObj.preType;
4319					related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0];
4320				}
4321
4322				if ( !related || related !== elem ) {
4323					elems.push({ elem: elem, handleObj: handleObj, level: close.level });
4324				}
4325			}
4326		}
4327	}
4328
4329	for ( i = 0, l = elems.length; i < l; i++ ) {
4330		match = elems[i];
4331
4332		if ( maxLevel && match.level > maxLevel ) {
4333			break;
4334		}
4335
4336		event.currentTarget = match.elem;
4337		event.data = match.handleObj.data;
4338		event.handleObj = match.handleObj;
4339
4340		ret = match.handleObj.origHandler.apply( match.elem, arguments );
4341
4342		if ( ret === false || event.isPropagationStopped() ) {
4343			maxLevel = match.level;
4344
4345			if ( ret === false ) {
4346				stop = false;
4347			}
4348			if ( event.isImmediatePropagationStopped() ) {
4349				break;
4350			}
4351		}
4352	}
4353
4354	return stop;
4355}
4356
4357function liveConvert( type, selector ) {
4358	return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&");
4359}
4360
4361jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
4362	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
4363	"change select submit keydown keypress keyup error").split(" "), function( i, name ) {
4364
4365	// Handle event binding
4366	jQuery.fn[ name ] = function( data, fn ) {
4367		if ( fn == null ) {
4368			fn = data;
4369			data = null;
4370		}
4371
4372		return arguments.length > 0 ?
4373			this.bind( name, data, fn ) :
4374			this.trigger( name );
4375	};
4376
4377	if ( jQuery.attrFn ) {
4378		jQuery.attrFn[ name ] = true;
4379	}
4380});
4381
4382
4383/*!
4384 * Sizzle CSS Selector Engine
4385 *  Copyright 2011, The Dojo Foundation
4386 *  Released under the MIT, BSD, and GPL Licenses.
4387 *  More information: http://sizzlejs.com/
4388 */
4389(function(){
4390
4391var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
4392	done = 0,
4393	toString = Object.prototype.toString,
4394	hasDuplicate = false,
4395	baseHasDuplicate = true,
4396	rBackslash = /\\/g,
4397	rNonWord = /\W/;
4398
4399// Here we check if the JavaScript engine is using some sort of
4400// optimization where it does not always call our comparision
4401// function. If that is the case, discard the hasDuplicate value.
4402//   Thus far that includes Google Chrome.
4403[0, 0].sort(function() {
4404	baseHasDuplicate = false;
4405	return 0;
4406});
4407
4408var Sizzle = function( selector, context, results, seed ) {
4409	results = results || [];
4410	context = context || document;
4411
4412	var origContext = context;
4413
4414	if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
4415		return [];
4416	}
4417
4418	if ( !selector || typeof selector !== "string" ) {
4419		return results;
4420	}
4421
4422	var m, set, checkSet, extra, ret, cur, pop, i,
4423		prune = true,
4424		contextXML = Sizzle.isXML( context ),
4425		parts = [],
4426		soFar = selector;
4427
4428	// Reset the position of the chunker regexp (start from head)
4429	do {
4430		chunker.exec( "" );
4431		m = chunker.exec( soFar );
4432
4433		if ( m ) {
4434			soFar = m[3];
4435
4436			parts.push( m[1] );
4437
4438			if ( m[2] ) {
4439				extra = m[3];
4440				break;
4441			}
4442		}
4443	} while ( m );
4444
4445	if ( parts.length > 1 && origPOS.exec( selector ) ) {
4446
4447		if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
4448			set = posProcess( parts[0] + parts[1], context );
4449
4450		} else {
4451			set = Expr.relative[ parts[0] ] ?
4452				[ context ] :
4453				Sizzle( parts.shift(), context );
4454
4455			while ( parts.length ) {
4456				selector = parts.shift();
4457
4458				if ( Expr.relative[ selector ] ) {
4459					selector += parts.shift();
4460				}
4461
4462				set = posProcess( selector, set );
4463			}
4464		}
4465
4466	} else {
4467		// Take a shortcut and set the context if the root selector is an ID
4468		// (but not if it'll be faster if the inner selector is an ID)
4469		if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
4470				Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
4471
4472			ret = Sizzle.find( parts.shift(), context, contextXML );
4473			context = ret.expr ?
4474				Sizzle.filter( ret.expr, ret.set )[0] :
4475				ret.set[0];
4476		}
4477
4478		if ( context ) {
4479			ret = seed ?
4480				{ expr: parts.pop(), set: makeArray(seed) } :
4481				Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
4482
4483			set = ret.expr ?
4484				Sizzle.filter( ret.expr, ret.set ) :
4485				ret.set;
4486
4487			if ( parts.length > 0 ) {
4488				checkSet = makeArray( set );
4489
4490			} else {
4491				prune = false;
4492			}
4493
4494			while ( parts.length ) {
4495				cur = parts.pop();
4496				pop = cur;
4497
4498				if ( !Expr.relative[ cur ] ) {
4499					cur = "";
4500				} else {
4501					pop = parts.pop();
4502				}
4503
4504				if ( pop == null ) {
4505					pop = context;
4506				}
4507
4508				Expr.relative[ cur ]( checkSet, pop, contextXML );
4509			}
4510
4511		} else {
4512			checkSet = parts = [];
4513		}
4514	}
4515
4516	if ( !checkSet ) {
4517		checkSet = set;
4518	}
4519
4520	if ( !checkSet ) {
4521		Sizzle.error( cur || selector );
4522	}
4523
4524	if ( toString.call(checkSet) === "[object Array]" ) {
4525		if ( !prune ) {
4526			results.push.apply( results, checkSet );
4527
4528		} else if ( context && context.nodeType === 1 ) {
4529			for ( i = 0; checkSet[i] != null; i++ ) {
4530				if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
4531					results.push( set[i] );
4532				}
4533			}
4534
4535		} else {
4536			for ( i = 0; checkSet[i] != null; i++ ) {
4537				if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
4538					results.push( set[i] );
4539				}
4540			}
4541		}
4542
4543	} else {
4544		makeArray( checkSet, results );
4545	}
4546
4547	if ( extra ) {
4548		Sizzle( extra, origContext, results, seed );
4549		Sizzle.uniqueSort( results );
4550	}
4551
4552	return results;
4553};
4554
4555Sizzle.uniqueSort = function( results ) {
4556	if ( sortOrder ) {
4557		hasDuplicate = baseHasDuplicate;
4558		results.sort( sortOrder );
4559
4560		if ( hasDuplicate ) {
4561			for ( var i = 1; i < results.length; i++ ) {
4562				if ( results[i] === results[ i - 1 ] ) {
4563					results.splice( i--, 1 );
4564				}
4565			}
4566		}
4567	}
4568
4569	return results;
4570};
4571
4572Sizzle.matches = function( expr, set ) {
4573	return Sizzle( expr, null, null, set );
4574};
4575
4576Sizzle.matchesSelector = function( node, expr ) {
4577	return Sizzle( expr, null, null, [node] ).length > 0;
4578};
4579
4580Sizzle.find = function( expr, context, isXML ) {
4581	var set;
4582
4583	if ( !expr ) {
4584		return [];
4585	}
4586
4587	for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
4588		var match,
4589			type = Expr.order[i];
4590
4591		if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
4592			var left = match[1];
4593			match.splice( 1, 1 );
4594
4595			if ( left.substr( left.length - 1 ) !== "\\" ) {
4596				match[1] = (match[1] || "").replace( rBackslash, "" );
4597				set = Expr.find[ type ]( match, context, isXML );
4598
4599				if ( set != null ) {
4600					expr = expr.replace( Expr.match[ type ], "" );
4601					break;
4602				}
4603			}
4604		}
4605	}
4606
4607	if ( !set ) {
4608		set = typeof context.getElementsByTagName !== "undefined" ?
4609			context.getElementsByTagName( "*" ) :
4610			[];
4611	}
4612
4613	return { set: set, expr: expr };
4614};
4615
4616Sizzle.filter = function( expr, set, inplace, not ) {
4617	var match, anyFound,
4618		old = expr,
4619		result = [],
4620		curLoop = set,
4621		isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
4622
4623	while ( expr && set.length ) {
4624		for ( var type in Expr.filter ) {
4625			if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
4626				var found, item,
4627					filter = Expr.filter[ type ],
4628					left = match[1];
4629
4630				anyFound = false;
4631
4632				match.splice(1,1);
4633
4634				if ( left.substr( left.length - 1 ) === "\\" ) {
4635					continue;
4636				}
4637
4638				if ( curLoop === result ) {
4639					result = [];
4640				}
4641
4642				if ( Expr.preFilter[ type ] ) {
4643					match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
4644
4645					if ( !match ) {
4646						anyFound = found = true;
4647
4648					} else if ( match === true ) {
4649						continue;
4650					}
4651				}
4652
4653				if ( match ) {
4654					for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
4655						if ( item ) {
4656							found = filter( item, match, i, curLoop );
4657							var pass = not ^ !!found;
4658
4659							if ( inplace && found != null ) {
4660								if ( pass ) {
4661									anyFound = true;
4662
4663								} else {
4664									curLoop[i] = false;
4665								}
4666
4667							} else if ( pass ) {
4668								result.push( item );
4669								anyFound = true;
4670							}
4671						}
4672					}
4673				}
4674
4675				if ( found !== undefined ) {
4676					if ( !inplace ) {
4677						curLoop = result;
4678					}
4679
4680					expr = expr.replace( Expr.match[ type ], "" );
4681
4682					if ( !anyFound ) {
4683						return [];
4684					}
4685
4686					break;
4687				}
4688			}
4689		}
4690
4691		// Improper expression
4692		if ( expr === old ) {
4693			if ( anyFound == null ) {
4694				Sizzle.error( expr );
4695
4696			} else {
4697				break;
4698			}
4699		}
4700
4701		old = expr;
4702	}
4703
4704	return curLoop;
4705};
4706
4707Sizzle.error = function( msg ) {
4708	throw "Syntax error, unrecognized expression: " + msg;
4709};
4710
4711var Expr = Sizzle.selectors = {
4712	order: [ "ID", "NAME", "TAG" ],
4713
4714	match: {
4715		ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
4716		CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
4717		NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
4718		ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
4719		TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
4720		CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
4721		POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
4722		PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
4723	},
4724
4725	leftMatch: {},
4726
4727	attrMap: {
4728		"class": "className",
4729		"for": "htmlFor"
4730	},
4731
4732	attrHandle: {
4733		href: function( elem ) {
4734			return elem.getAttribute( "href" );
4735		},
4736		type: function( elem ) {
4737			return elem.getAttribute( "type" );
4738		}
4739	},
4740
4741	relative: {
4742		"+": function(checkSet, part){
4743			var isPartStr = typeof part === "string",
4744				isTag = isPartStr && !rNonWord.test( part ),
4745				isPartStrNotTag = isPartStr && !isTag;
4746
4747			if ( isTag ) {
4748				part = part.toLowerCase();
4749			}
4750
4751			for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
4752				if ( (elem = checkSet[i]) ) {
4753					while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
4754
4755					checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
4756						elem || false :
4757						elem === part;
4758				}
4759			}
4760
4761			if ( isPartStrNotTag ) {
4762				Sizzle.filter( part, checkSet, true );
4763			}
4764		},
4765
4766		">": function( checkSet, part ) {
4767			var elem,
4768				isPartStr = typeof part === "string",
4769				i = 0,
4770				l = checkSet.length;
4771
4772			if ( isPartStr && !rNonWord.test( part ) ) {
4773				part = part.toLowerCase();
4774
4775				for ( ; i < l; i++ ) {
4776					elem = checkSet[i];
4777
4778					if ( elem ) {
4779						var parent = elem.parentNode;
4780						checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
4781					}
4782				}
4783
4784			} else {
4785				for ( ; i < l; i++ ) {
4786					elem = checkSet[i];
4787
4788					if ( elem ) {
4789						checkSet[i] = isPartStr ?
4790							elem.parentNode :
4791							elem.parentNode === part;
4792					}
4793				}
4794
4795				if ( isPartStr ) {
4796					Sizzle.filter( part, checkSet, true );
4797				}
4798			}
4799		},
4800
4801		"": function(checkSet, part, isXML){
4802			var nodeCheck,
4803				doneName = done++,
4804				checkFn = dirCheck;
4805
4806			if ( typeof part === "string" && !rNonWord.test( part ) ) {
4807				part = part.toLowerCase();
4808				nodeCheck = part;
4809				checkFn = dirNodeCheck;
4810			}
4811
4812			checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
4813		},
4814
4815		"~": function( checkSet, part, isXML ) {
4816			var nodeCheck,
4817				doneName = done++,
4818				checkFn = dirCheck;
4819
4820			if ( typeof part === "string" && !rNonWord.test( part ) ) {
4821				part = part.toLowerCase();
4822				nodeCheck = part;
4823				checkFn = dirNodeCheck;
4824			}
4825
4826			checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
4827		}
4828	},
4829
4830	find: {
4831		ID: function( match, context, isXML ) {
4832			if ( typeof context.getElementById !== "undefined" && !isXML ) {
4833				var m = context.getElementById(match[1]);
4834				// Check parentNode to catch when Blackberry 4.6 returns
4835				// nodes that are no longer in the document #6963
4836				return m && m.parentNode ? [m] : [];
4837			}
4838		},
4839
4840		NAME: function( match, context ) {
4841			if ( typeof context.getElementsByName !== "undefined" ) {
4842				var ret = [],
4843					results = context.getElementsByName( match[1] );
4844
4845				for ( var i = 0, l = results.length; i < l; i++ ) {
4846					if ( results[i].getAttribute("name") === match[1] ) {
4847						ret.push( results[i] );
4848					}
4849				}
4850
4851				return ret.length === 0 ? null : ret;
4852			}
4853		},
4854
4855		TAG: function( match, context ) {
4856			if ( typeof context.getElementsByTagName !== "undefined" ) {
4857				return context.getElementsByTagName( match[1] );
4858			}
4859		}
4860	},
4861	preFilter: {
4862		CLASS: function( match, curLoop, inplace, result, not, isXML ) {
4863			match = " " + match[1].replace( rBackslash, "" ) + " ";
4864
4865			if ( isXML ) {
4866				return match;
4867			}
4868
4869			for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
4870				if ( elem ) {
4871					if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
4872						if ( !inplace ) {
4873							result.push( elem );
4874						}
4875
4876					} else if ( inplace ) {
4877						curLoop[i] = false;
4878					}
4879				}
4880			}
4881
4882			return false;
4883		},
4884
4885		ID: function( match ) {
4886			return match[1].replace( rBackslash, "" );
4887		},
4888
4889		TAG: function( match, curLoop ) {
4890			return match[1].replace( rBackslash, "" ).toLowerCase();
4891		},
4892
4893		CHILD: function( match ) {
4894			if ( match[1] === "nth" ) {
4895				if ( !match[2] ) {
4896					Sizzle.error( match[0] );
4897				}
4898
4899				match[2] = match[2].replace(/^\+|\s*/g, '');
4900
4901				// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
4902				var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
4903					match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
4904					!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
4905
4906				// calculate the numbers (first)n+(last) including if they are negative
4907				match[2] = (test[1] + (test[2] || 1)) - 0;
4908				match[3] = test[3] - 0;
4909			}
4910			else if ( match[2] ) {
4911				Sizzle.error( match[0] );
4912			}
4913
4914			// TODO: Move to normal caching system
4915			match[0] = done++;
4916
4917			return match;
4918		},
4919
4920		ATTR: function( match, curLoop, inplace, result, not, isXML ) {
4921			var name = match[1] = match[1].replace( rBackslash, "" );
4922
4923			if ( !isXML && Expr.attrMap[name] ) {
4924				match[1] = Expr.attrMap[name];
4925			}
4926
4927			// Handle if an un-quoted value was used
4928			match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );
4929
4930			if ( match[2] === "~=" ) {
4931				match[4] = " " + match[4] + " ";
4932			}
4933
4934			return match;
4935		},
4936
4937		PSEUDO: function( match, curLoop, inplace, result, not ) {
4938			if ( match[1] === "not" ) {
4939				// If we're dealing with a complex expression, or a simple one
4940				if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
4941					match[3] = Sizzle(match[3], null, null, curLoop);
4942
4943				} else {
4944					var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
4945
4946					if ( !inplace ) {
4947						result.push.apply( result, ret );
4948					}
4949
4950					return false;
4951				}
4952
4953			} else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
4954				return true;
4955			}
4956
4957			return match;
4958		},
4959
4960		POS: function( match ) {
4961			match.unshift( true );
4962
4963			return match;
4964		}
4965	},
4966
4967	filters: {
4968		enabled: function( elem ) {
4969			return elem.disabled === false && elem.type !== "hidden";
4970		},
4971
4972		disabled: function( elem ) {
4973			return elem.disabled === true;
4974		},
4975
4976		checked: function( elem ) {
4977			return elem.checked === true;
4978		},
4979
4980		selected: function( elem ) {
4981			// Accessing this property makes selected-by-default
4982			// options in Safari work properly
4983			if ( elem.parentNode ) {
4984				elem.parentNode.selectedIndex;
4985			}
4986
4987			return elem.selected === true;
4988		},
4989
4990		parent: function( elem ) {
4991			return !!elem.firstChild;
4992		},
4993
4994		empty: function( elem ) {
4995			return !elem.firstChild;
4996		},
4997
4998		has: function( elem, i, match ) {
4999			return !!Sizzle( match[3], elem ).length;
5000		},
5001
5002		header: function( elem ) {
5003			return (/h\d/i).test( elem.nodeName );
5004		},
5005
5006		text: function( elem ) {
5007			var attr = elem.getAttribute( "type" ), type = elem.type;
5008			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
5009			// use getAttribute instead to test this case
5010			return "text" === type && ( attr === type || attr === null );
5011		},
5012
5013		radio: function( elem ) {
5014			return "radio" === elem.type;
5015		},
5016
5017		checkbox: function( elem ) {
5018			return "checkbox" === elem.type;
5019		},
5020
5021		file: function( elem ) {
5022			return "file" === elem.type;
5023		},
5024		password: function( elem ) {
5025			return "password" === elem.type;
5026		},
5027
5028		submit: function( elem ) {
5029			return "submit" === elem.type;
5030		},
5031
5032		image: function( elem ) {
5033			return "image" === elem.type;
5034		},
5035
5036		reset: function( elem ) {
5037			return "reset" === elem.type;
5038		},
5039
5040		button: function( elem ) {
5041			return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
5042		},
5043
5044		input: function( elem ) {
5045			return (/input|select|textarea|button/i).test( elem.nodeName );
5046		}
5047	},
5048	setFilters: {
5049		first: function( elem, i ) {
5050			return i === 0;
5051		},
5052
5053		last: function( elem, i, match, array ) {
5054			return i === array.length - 1;
5055		},
5056
5057		even: function( elem, i ) {
5058			return i % 2 === 0;
5059		},
5060
5061		odd: function( elem, i ) {
5062			return i % 2 === 1;
5063		},
5064
5065		lt: function( elem, i, match ) {
5066			return i < match[3] - 0;
5067		},
5068
5069		gt: function( elem, i, match ) {
5070			return i > match[3] - 0;
5071		},
5072
5073		nth: function( elem, i, match ) {
5074			return match[3] - 0 === i;
5075		},
5076
5077		eq: function( elem, i, match ) {
5078			return match[3] - 0 === i;
5079		}
5080	},
5081	filter: {
5082		PSEUDO: function( elem, match, i, array ) {
5083			var name = match[1],
5084				filter = Expr.filters[ name ];
5085
5086			if ( filter ) {
5087				return filter( elem, i, match, array );
5088
5089			} else if ( name === "contains" ) {
5090				return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;
5091
5092			} else if ( name === "not" ) {
5093				var not = match[3];
5094
5095				for ( var j = 0, l = not.length; j < l; j++ ) {
5096					if ( not[j] === elem ) {
5097						return false;
5098					}
5099				}
5100
5101				return true;
5102
5103			} else {
5104				Sizzle.error( name );
5105			}
5106		},
5107
5108		CHILD: function( elem, match ) {
5109			var type = match[1],
5110				node = elem;
5111
5112			switch ( type ) {
5113				case "only":
5114				case "first":
5115					while ( (node = node.previousSibling) )	 {
5116						if ( node.nodeType === 1 ) {
5117							return false;
5118						}
5119					}
5120
5121					if ( type === "first" ) {
5122						return true;
5123					}
5124
5125					node = elem;
5126
5127				case "last":
5128					while ( (node = node.nextSibling) )	 {
5129						if ( node.nodeType === 1 ) {
5130							return false;
5131						}
5132					}
5133
5134					return true;
5135
5136				case "nth":
5137					var first = match[2],
5138						last = match[3];
5139
5140					if ( first === 1 && last === 0 ) {
5141						return true;
5142					}
5143
5144					var doneName = match[0],
5145						parent = elem.parentNode;
5146
5147					if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
5148						var count = 0;
5149
5150						for ( node = parent.firstChild; node; node = node.nextSibling ) {
5151							if ( node.nodeType === 1 ) {
5152								node.nodeIndex = ++count;
5153							}
5154						}
5155
5156						parent.sizcache = doneName;
5157					}
5158
5159					var diff = elem.nodeIndex - last;
5160
5161					if ( first === 0 ) {
5162						return diff === 0;
5163
5164					} else {
5165						return ( diff % first === 0 && diff / first >= 0 );
5166					}
5167			}
5168		},
5169
5170		ID: function( elem, match ) {
5171			return elem.nodeType === 1 && elem.getAttribute("id") === match;
5172		},
5173
5174		TAG: function( elem, match ) {
5175			return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
5176		},
5177
5178		CLASS: function( elem, match ) {
5179			return (" " + (elem.className || elem.getAttribute("class")) + " ")
5180				.indexOf( match ) > -1;
5181		},
5182
5183		ATTR: function( elem, match ) {
5184			var name = match[1],
5185				result = Expr.attrHandle[ name ] ?
5186					Expr.attrHandle[ name ]( elem ) :
5187					elem[ name ] != null ?
5188						elem[ name ] :
5189						elem.getAttribute( name ),
5190				value = result + "",
5191				type = match[2],
5192				check = match[4];
5193
5194			return result == null ?
5195				type === "!=" :
5196				type === "=" ?
5197				value === check :
5198				type === "*=" ?
5199				value.indexOf(check) >= 0 :
5200				type === "~=" ?
5201				(" " + value + " ").indexOf(check) >= 0 :
5202				!check ?
5203				value && result !== false :
5204				type === "!=" ?
5205				value !== check :
5206				type === "^=" ?
5207				value.indexOf(check) === 0 :
5208				type === "$=" ?
5209				value.substr(value.length - check.length) === check :
5210				type === "|=" ?
5211				value === check || value.substr(0, check.length + 1) === check + "-" :
5212				false;
5213		},
5214
5215		POS: function( elem, match, i, array ) {
5216			var name = match[2],
5217				filter = Expr.setFilters[ name ];
5218
5219			if ( filter ) {
5220				return filter( elem, i, match, array );
5221			}
5222		}
5223	}
5224};
5225
5226var origPOS = Expr.match.POS,
5227	fescape = function(all, num){
5228		return "\\" + (num - 0 + 1);
5229	};
5230
5231for ( var type in Expr.match ) {
5232	Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
5233	Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
5234}
5235
5236var makeArray = function( array, results ) {
5237	array = Array.prototype.slice.call( array, 0 );
5238
5239	if ( results ) {
5240		results.push.apply( results, array );
5241		return results;
5242	}
5243
5244	return array;
5245};
5246
5247// Perform a simple check to determine if the browser is capable of
5248// converting a NodeList to an array using builtin methods.
5249// Also verifies that the returned array holds DOM nodes
5250// (which is not the case in the Blackberry browser)
5251try {
5252	Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
5253
5254// Provide a fallback method if it does not work
5255} catch( e ) {
5256	makeArray = function( array, results ) {
5257		var i = 0,
5258			ret = results || [];
5259
5260		if ( toString.call(array) === "[object Array]" ) {
5261			Array.prototype.push.apply( ret, array );
5262
5263		} else {
5264			if ( typeof array.length === "number" ) {
5265				for ( var l = array.length; i < l; i++ ) {
5266					ret.push( array[i] );
5267				}
5268
5269			} else {
5270				for ( ; array[i]; i++ ) {
5271					ret.push( array[i] );
5272				}
5273			}
5274		}
5275
5276		return ret;
5277	};
5278}
5279
5280var sortOrder, siblingCheck;
5281
5282if ( document.documentElement.compareDocumentPosition ) {
5283	sortOrder = function( a, b ) {
5284		if ( a === b ) {
5285			hasDuplicate = true;
5286			return 0;
5287		}
5288
5289		if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
5290			return a.compareDocumentPosition ? -1 : 1;
5291		}
5292
5293		return a.compareDocumentPosition(b) & 4 ? -1 : 1;
5294	};
5295
5296} else {
5297	sortOrder = function( a, b ) {
5298		var al, bl,
5299			ap = [],
5300			bp = [],
5301			aup = a.parentNode,
5302			bup = b.parentNode,
5303			cur = aup;
5304
5305		// The nodes are identical, we can exit early
5306		if ( a === b ) {
5307			hasDuplicate = true;
5308			return 0;
5309
5310		// If the nodes are siblings (or identical) we can do a quick check
5311		} else if ( aup === bup ) {
5312			return siblingCheck( a, b );
5313
5314		// If no parents were found then the nodes are disconnected
5315		} else if ( !aup ) {
5316			return -1;
5317
5318		} else if ( !bup ) {
5319			return 1;
5320		}
5321
5322		// Otherwise they're somewhere else in the tree so we need
5323		// to build up a full list of the parentNodes for comparison
5324		while ( cur ) {
5325			ap.unshift( cur );
5326			cur = cur.parentNode;
5327		}
5328
5329		cur = bup;
5330
5331		while ( cur ) {
5332			bp.unshift( cur );
5333			cur = cur.parentNode;
5334		}
5335
5336		al = ap.length;
5337		bl = bp.length;
5338
5339		// Start walking down the tree looking for a discrepancy
5340		for ( var i = 0; i < al && i < bl; i++ ) {
5341			if ( ap[i] !== bp[i] ) {
5342				return siblingCheck( ap[i], bp[i] );
5343			}
5344		}
5345
5346		// We ended someplace up the tree so do a sibling check
5347		return i === al ?
5348			siblingCheck( a, bp[i], -1 ) :
5349			siblingCheck( ap[i], b, 1 );
5350	};
5351
5352	siblingCheck = function( a, b, ret ) {
5353		if ( a === b ) {
5354			return ret;
5355		}
5356
5357		var cur = a.nextSibling;
5358
5359		while ( cur ) {
5360			if ( cur === b ) {
5361				return -1;
5362			}
5363
5364			cur = cur.nextSibling;
5365		}
5366
5367		return 1;
5368	};
5369}
5370
5371// Utility function for retreiving the text value of an array of DOM nodes
5372Sizzle.getText = function( elems ) {
5373	var ret = "", elem;
5374
5375	for ( var i = 0; elems[i]; i++ ) {
5376		elem = elems[i];
5377
5378		// Get the text from text nodes and CDATA nodes
5379		if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
5380			ret += elem.nodeValue;
5381
5382		// Traverse everything else, except comment nodes
5383		} else if ( elem.nodeType !== 8 ) {
5384			ret += Sizzle.getText( elem.childNodes );
5385		}
5386	}
5387
5388	return ret;
5389};
5390
5391// Check to see if the browser returns elements by name when
5392// querying by getElementById (and provide a workaround)
5393(function(){
5394	// We're going to inject a fake input element with a specified name
5395	var form = document.createElement("div"),
5396		id = "script" + (new Date()).getTime(),
5397		root = document.documentElement;
5398
5399	form.innerHTML = "<a name='" + id + "'/>";
5400
5401	// Inject it into the root element, check its status, and remove it quickly
5402	root.insertBefore( form, root.firstChild );
5403
5404	// The workaround has to do additional checks after a getElementById
5405	// Which slows things down for other browsers (hence the branching)
5406	if ( document.getElementById( id ) ) {
5407		Expr.find.ID = function( match, context, isXML ) {
5408			if ( typeof context.getElementById !== "undefined" && !isXML ) {
5409				var m = context.getElementById(match[1]);
5410
5411				return m ?
5412					m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
5413						[m] :
5414						undefined :
5415					[];
5416			}
5417		};
5418
5419		Expr.filter.ID = function( elem, match ) {
5420			var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
5421
5422			return elem.nodeType === 1 && node && node.nodeValue === match;
5423		};
5424	}
5425
5426	root.removeChild( form );
5427
5428	// release memory in IE
5429	root = form = null;
5430})();
5431
5432(function(){
5433	// Check to see if the browser returns only elements
5434	// when doing getElementsByTagName("*")
5435
5436	// Create a fake element
5437	var div = document.createElement("div");
5438	div.appendChild( document.createComment("") );
5439
5440	// Make sure no comments are found
5441	if ( div.getElementsByTagName("*").length > 0 ) {
5442		Expr.find.TAG = function( match, context ) {
5443			var results = context.getElementsByTagName( match[1] );
5444
5445			// Filter out possible comments
5446			if ( match[1] === "*" ) {
5447				var tmp = [];
5448
5449				for ( var i = 0; results[i]; i++ ) {
5450					if ( results[i].nodeType === 1 ) {
5451						tmp.push( results[i] );
5452					}
5453				}
5454
5455				results = tmp;
5456			}
5457
5458			return results;
5459		};
5460	}
5461
5462	// Check to see if an attribute returns normalized href attributes
5463	div.innerHTML = "<a href='#'></a>";
5464
5465	if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
5466			div.firstChild.getAttribute("href") !== "#" ) {
5467
5468		Expr.attrHandle.href = function( elem ) {
5469			return elem.getAttribute( "href", 2 );
5470		};
5471	}
5472
5473	// release memory in IE
5474	div = null;
5475})();
5476
5477if ( document.querySelectorAll ) {
5478	(function(){
5479		var oldSizzle = Sizzle,
5480			div = document.createElement("div"),
5481			id = "__sizzle__";
5482
5483		div.innerHTML = "<p class='TEST'></p>";
5484
5485		// Safari can't handle uppercase or unicode characters when
5486		// in quirks mode.
5487		if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
5488			return;
5489		}
5490
5491		Sizzle = function( query, context, extra, seed ) {
5492			context = context || document;
5493
5494			// Only use querySelectorAll on non-XML documents
5495			// (ID selectors don't work in non-HTML documents)
5496			if ( !seed && !Sizzle.isXML(context) ) {
5497				// See if we find a selector to speed up
5498				var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
5499
5500				if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
5501					// Speed-up: Sizzle("TAG")
5502					if ( match[1] ) {
5503						return makeArray( context.getElementsByTagName( query ), extra );
5504
5505					// Speed-up: Sizzle(".CLASS")
5506					} else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
5507						return makeArray( context.getElementsByClassName( match[2] ), extra );
5508					}
5509				}
5510
5511				if ( context.nodeType === 9 ) {
5512					// Speed-up: Sizzle("body")
5513					// The body element only exists once, optimize finding it
5514					if ( query === "body" && context.body ) {
5515						return makeArray( [ context.body ], extra );
5516
5517					// Speed-up: Sizzle("#ID")
5518					} else if ( match && match[3] ) {
5519						var elem = context.getElementById( match[3] );
5520
5521						// Check parentNode to catch when Blackberry 4.6 returns
5522						// nodes that are no longer in the document #6963
5523						if ( elem && elem.parentNode ) {
5524							// Handle the case where IE and Opera return items
5525							// by name instead of ID
5526							if ( elem.id === match[3] ) {
5527								return makeArray( [ elem ], extra );
5528							}
5529
5530						} else {
5531							return makeArray( [], extra );
5532						}
5533					}
5534
5535					try {
5536						return makeArray( context.querySelectorAll(query), extra );
5537					} catch(qsaError) {}
5538
5539				// qSA works strangely on Element-rooted queries
5540				// We can work around this by specifying an extra ID on the root
5541				// and working up from there (Thanks to Andrew Dupont for the technique)
5542				// IE 8 doesn't work on object elements
5543				} else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
5544					var oldContext = context,
5545						old = context.getAttribute( "id" ),
5546						nid = old || id,
5547						hasParent = context.parentNode,
5548						relativeHierarchySelector = /^\s*[+~]/.test( query );
5549
5550					if ( !old ) {
5551						context.setAttribute( "id", nid );
5552					} else {
5553						nid = nid.replace( /'/g, "\\$&" );
5554					}
5555					if ( relativeHierarchySelector && hasParent ) {
5556						context = context.parentNode;
5557					}
5558
5559					try {
5560						if ( !relativeHierarchySelector || hasParent ) {
5561							return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
5562						}
5563
5564					} catch(pseudoError) {
5565					} finally {
5566						if ( !old ) {
5567							oldContext.removeAttribute( "id" );
5568						}
5569					}
5570				}
5571			}
5572
5573			return oldSizzle(query, context, extra, seed);
5574		};
5575
5576		for ( var prop in oldSizzle ) {
5577			Sizzle[ prop ] = oldSizzle[ prop ];
5578		}
5579
5580		// release memory in IE
5581		div = null;
5582	})();
5583}
5584
5585(function(){
5586	var html = document.documentElement,
5587		matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
5588
5589	if ( matches ) {
5590		// Check to see if it's possible to do matchesSelector
5591		// on a disconnected node (IE 9 fails this)
5592		var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
5593			pseudoWorks = false;
5594
5595		try {
5596			// This should fail with an exception
5597			// Gecko does not error, returns false instead
5598			matches.call( document.documentElement, "[test!='']:sizzle" );
5599
5600		} catch( pseudoError ) {
5601			pseudoWorks = true;
5602		}
5603
5604		Sizzle.matchesSelector = function( node, expr ) {
5605			// Make sure that attribute selectors are quoted
5606			expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
5607
5608			if ( !Sizzle.isXML( node ) ) {
5609				try {
5610					if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
5611						var ret = matches.call( node, expr );
5612
5613						// IE 9's matchesSelector returns false on disconnected nodes
5614						if ( ret || !disconnectedMatch ||
5615								// As well, disconnected nodes are said to be in a document
5616								// fragment in IE 9, so check for that
5617								node.document && node.document.nodeType !== 11 ) {
5618							return ret;
5619						}
5620					}
5621				} catch(e) {}
5622			}
5623
5624			return Sizzle(expr, null, null, [node]).length > 0;
5625		};
5626	}
5627})();
5628
5629(function(){
5630	var div = document.createElement("div");
5631
5632	div.innerHTML = "<div class='test e'></div><div class='test'></div>";
5633
5634	// Opera can't find a second classname (in 9.6)
5635	// Also, make sure that getElementsByClassName actually exists
5636	if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
5637		return;
5638	}
5639
5640	// Safari caches class attributes, doesn't catch changes (in 3.2)
5641	div.lastChild.className = "e";
5642
5643	if ( div.getElementsByClassName("e").length === 1 ) {
5644		return;
5645	}
5646
5647	Expr.order.splice(1, 0, "CLASS");
5648	Expr.find.CLASS = function( match, context, isXML ) {
5649		if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
5650			return context.getElementsByClassName(match[1]);
5651		}
5652	};
5653
5654	// release memory in IE
5655	div = null;
5656})();
5657
5658function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
5659	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
5660		var elem = checkSet[i];
5661
5662		if ( elem ) {
5663			var match = false;
5664
5665			elem = elem[dir];
5666
5667			while ( elem ) {
5668				if ( elem.sizcache === doneName ) {
5669					match = checkSet[elem.sizset];
5670					break;
5671				}
5672
5673				if ( elem.nodeType === 1 && !isXML ){
5674					elem.sizcache = doneName;
5675					elem.sizset = i;
5676				}
5677
5678				if ( elem.nodeName.toLowerCase() === cur ) {
5679					match = elem;
5680					break;
5681				}
5682
5683				elem = elem[dir];
5684			}
5685
5686			checkSet[i] = match;
5687		}
5688	}
5689}
5690
5691function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
5692	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
5693		var elem = checkSet[i];
5694
5695		if ( elem ) {
5696			var match = false;
5697
5698			elem = elem[dir];
5699
5700			while ( elem ) {
5701				if ( elem.sizcache === doneName ) {
5702					match = checkSet[elem.sizset];
5703					break;
5704				}
5705
5706				if ( elem.nodeType === 1 ) {
5707					if ( !isXML ) {
5708						elem.sizcache = doneName;
5709						elem.sizset = i;
5710					}
5711
5712					if ( typeof cur !== "string" ) {
5713						if ( elem === cur ) {
5714							match = true;
5715							break;
5716						}
5717
5718					} else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
5719						match = elem;
5720						break;
5721					}
5722				}
5723
5724				elem = elem[dir];
5725			}
5726
5727			checkSet[i] = match;
5728		}
5729	}
5730}
5731
5732if ( document.documentElement.contains ) {
5733	Sizzle.contains = function( a, b ) {
5734		return a !== b && (a.contains ? a.contains(b) : true);
5735	};
5736
5737} else if ( document.documentElement.compareDocumentPosition ) {
5738	Sizzle.contains = function( a, b ) {
5739		return !!(a.compareDocumentPosition(b) & 16);
5740	};
5741
5742} else {
5743	Sizzle.contains = function() {
5744		return false;
5745	};
5746}
5747
5748Sizzle.isXML = function( elem ) {
5749	// documentElement is verified for cases where it doesn't yet exist
5750	// (such as loading iframes in IE - #4833)
5751	var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
5752
5753	return documentElement ? documentElement.nodeName !== "HTML" : false;
5754};
5755
5756var posProcess = function( selector, context ) {
5757	var match,
5758		tmpSet = [],
5759		later = "",
5760		root = context.nodeType ? [context] : context;
5761
5762	// Position selectors must be done after the filter
5763	// And so must :not(positional) so we move all PSEUDOs to the end
5764	while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
5765		later += match[0];
5766		selector = selector.replace( Expr.match.PSEUDO, "" );
5767	}
5768
5769	selector = Expr.relative[selector] ? selector + "*" : selector;
5770
5771	for ( var i = 0, l = root.length; i < l; i++ ) {
5772		Sizzle( selector, root[i], tmpSet );
5773	}
5774
5775	return Sizzle.filter( later, tmpSet );
5776};
5777
5778// EXPOSE
5779jQuery.find = Sizzle;
5780jQuery.expr = Sizzle.selectors;
5781jQuery.expr[":"] = jQuery.expr.filters;
5782jQuery.unique = Sizzle.uniqueSort;
5783jQuery.text = Sizzle.getText;
5784jQuery.isXMLDoc = Sizzle.isXML;
5785jQuery.contains = Sizzle.contains;
5786
5787
5788})();
5789
5790
5791var runtil = /Until$/,
5792	rparentsprev = /^(?:parents|prevUntil|prevAll)/,
5793	// Note: This RegExp should be improved, or likely pulled from Sizzle
5794	rmultiselector = /,/,
5795	isSimple = /^.[^:#\[\.,]*$/,
5796	slice = Array.prototype.slice,
5797	POS = jQuery.expr.match.POS,
5798	// methods guaranteed to produce a unique set when starting from a unique set
5799	guaranteedUnique = {
5800		children: true,
5801		contents: true,
5802		next: true,
5803		prev: true
5804	};
5805
5806jQuery.fn.extend({
5807	find: function( selector ) {
5808		var ret = this.pushStack( "", "find", selector ),
5809			length = 0;
5810
5811		for ( var i = 0, l = this.length; i < l; i++ ) {
5812			length = ret.length;
5813			jQuery.find( selector, this[i], ret );
5814
5815			if ( i > 0 ) {
5816				// Make sure that the results are unique
5817				for ( var n = length; n < ret.length; n++ ) {
5818					for ( var r = 0; r < length; r++ ) {
5819						if ( ret[r] === ret[n] ) {
5820							ret.splice(n--, 1);
5821							break;
5822						}
5823					}
5824				}
5825			}
5826		}
5827
5828		return ret;
5829	},
5830
5831	has: function( target ) {
5832		var targets = jQuery( target );
5833		return this.filter(function() {
5834			for ( var i = 0, l = targets.length; i < l; i++ ) {
5835				if ( jQuery.contains( this, targets[i] ) ) {
5836					return true;
5837				}
5838			}
5839		});
5840	},
5841
5842	not: function( selector ) {
5843		return this.pushStack( winnow(this, selector, false), "not", selector);
5844	},
5845
5846	filter: function( selector ) {
5847		return this.pushStack( winnow(this, selector, true), "filter", selector );
5848	},
5849
5850	is: function( selector ) {
5851		return !!selector && jQuery.filter( selector, this ).length > 0;
5852	},
5853
5854	closest: function( selectors, context ) {
5855		var ret = [], i, l, cur = this[0];
5856
5857		if ( jQuery.isArray( selectors ) ) {
5858			var match, selector,
5859				matches = {},
5860				level = 1;
5861
5862			if ( cur && selectors.length ) {
5863				for ( i = 0, l = selectors.length; i < l; i++ ) {
5864					selector = selectors[i];
5865
5866					if ( !matches[selector] ) {
5867						matches[selector] = jQuery.expr.match.POS.test( selector ) ?
5868							jQuery( selector, context || this.context ) :
5869							selector;
5870					}
5871				}
5872
5873				while ( cur && cur.ownerDocument && cur !== context ) {
5874					for ( selector in matches ) {
5875						match = matches[selector];
5876
5877						if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
5878							ret.push({ selector: selector, elem: cur, level: level });
5879						}
5880					}
5881
5882					cur = cur.parentNode;
5883					level++;
5884				}
5885			}
5886
5887			return ret;
5888		}
5889
5890		var pos = POS.test( selectors ) ?
5891			jQuery( selectors, context || this.context ) : null;
5892
5893		for ( i = 0, l = this.length; i < l; i++ ) {
5894			cur = this[i];
5895
5896			while ( cur ) {
5897				if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
5898					ret.push( cur );
5899					break;
5900
5901				} else {
5902					cur = cur.parentNode;
5903					if ( !cur || !cur.ownerDocument || cur === context ) {
5904						break;
5905					}
5906				}
5907			}
5908		}
5909
5910		ret = ret.length > 1 ? jQuery.unique(ret) : ret;
5911
5912		return this.pushStack( ret, "closest", selectors );
5913	},
5914
5915	// Determine the position of an element within
5916	// the matched set of elements
5917	index: function( elem ) {
5918		if ( !elem || typeof elem === "string" ) {
5919			return jQuery.inArray( this[0],
5920				// If it receives a string, the selector is used
5921				// If it receives nothing, the siblings are used
5922				elem ? jQuery( elem ) : this.parent().children() );
5923		}
5924		// Locate the position of the desired element
5925		return jQuery.inArray(
5926			// If it receives a jQuery object, the first element is used
5927			elem.jquery ? elem[0] : elem, this );
5928	},
5929
5930	add: function( selector, context ) {
5931		var set = typeof selector === "string" ?
5932				jQuery( selector, context ) :
5933				jQuery.makeArray( selector ),
5934			all = jQuery.merge( this.get(), set );
5935
5936		return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
5937			all :
5938			jQuery.unique( all ) );
5939	},
5940
5941	andSelf: function() {
5942		return this.add( this.prevObject );
5943	}
5944});
5945
5946// A painfully simple check to see if an element is disconnected
5947// from a document (should be improved, where feasible).
5948function isDisconnected( node ) {
5949	return !node || !node.parentNode || node.parentNode.nodeType === 11;
5950}
5951
5952jQuery.each({
5953	parent: function( elem ) {
5954		var parent = elem.parentNode;
5955		return parent && parent.nodeType !== 11 ? parent : null;
5956	},
5957	parents: function( elem ) {
5958		return jQuery.dir( elem, "parentNode" );
5959	},
5960	parentsUntil: function( elem, i, until ) {
5961		return jQuery.dir( elem, "parentNode", until );
5962	},
5963	next: function( elem ) {
5964		return jQuery.nth( elem, 2, "nextSibling" );
5965	},
5966	prev: function( elem ) {
5967		return jQuery.nth( elem, 2, "previousSibling" );
5968	},
5969	nextAll: function( elem ) {
5970		return jQuery.dir( elem, "nextSibling" );
5971	},
5972	prevAll: function( elem ) {
5973		return jQuery.dir( elem, "previousSibling" );
5974	},
5975	nextUntil: function( elem, i, until ) {
5976		return jQuery.dir( elem, "nextSibling", until );
5977	},
5978	prevUntil: function( elem, i, until ) {
5979		return jQuery.dir( elem, "previousSibling", until );
5980	},
5981	siblings: function( elem ) {
5982		return jQuery.sibling( elem.parentNode.firstChild, elem );
5983	},
5984	children: function( elem ) {
5985		return jQuery.sibling( elem.firstChild );
5986	},
5987	contents: function( elem ) {
5988		return jQuery.nodeName( elem, "iframe" ) ?
5989			elem.contentDocument || elem.contentWindow.document :
5990			jQuery.makeArray( elem.childNodes );
5991	}
5992}, function( name, fn ) {
5993	jQuery.fn[ name ] = function( until, selector ) {
5994		var ret = jQuery.map( this, fn, until ),
5995			// The variable 'args' was introduced in
5996			// https://github.com/jquery/jquery/commit/52a0238
5997			// to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
5998			// http://code.google.com/p/v8/issues/detail?id=1050
5999			args = slice.call(arguments);
6000
6001		if ( !runtil.test( name ) ) {
6002			selector = until;
6003		}
6004
6005		if ( selector && typeof selector === "string" ) {
6006			ret = jQuery.filter( selector, ret );
6007		}
6008
6009		ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
6010
6011		if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
6012			ret = ret.reverse();
6013		}
6014
6015		return this.pushStack( ret, name, args.join(",") );
6016	};
6017});
6018
6019jQuery.extend({
6020	filter: function( expr, elems, not ) {
6021		if ( not ) {
6022			expr = ":not(" + expr + ")";
6023		}
6024
6025		return elems.length === 1 ?
6026			jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
6027			jQuery.find.matches(expr, elems);
6028	},
6029
6030	dir: function( elem, dir, until ) {
6031		var matched = [],
6032			cur = elem[ dir ];
6033
6034		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
6035			if ( cur.nodeType === 1 ) {
6036				matched.push( cur );
6037			}
6038			cur = cur[dir];
6039		}
6040		return matched;
6041	},
6042
6043	nth: function( cur, result, dir, elem ) {
6044		result = result || 1;
6045		var num = 0;
6046
6047		for ( ; cur; cur = cur[dir] ) {
6048			if ( cur.nodeType === 1 && ++num === result ) {
6049				break;
6050			}
6051		}
6052
6053		return cur;
6054	},
6055
6056	sibling: function( n, elem ) {
6057		var r = [];
6058
6059		for ( ; n; n = n.nextSibling ) {
6060			if ( n.nodeType === 1 && n !== elem ) {
6061				r.push( n );
6062			}
6063		}
6064
6065		return r;
6066	}
6067});
6068
6069// Implement the identical functionality for filter and not
6070function winnow( elements, qualifier, keep ) {
6071	if ( jQuery.isFunction( qualifier ) ) {
6072		return jQuery.grep(elements, function( elem, i ) {
6073			var retVal = !!qualifier.call( elem, i, elem );
6074			return retVal === keep;
6075		});
6076
6077	} else if ( qualifier.nodeType ) {
6078		return jQuery.grep(elements, function( elem, i ) {
6079			return (elem === qualifier) === keep;
6080		});
6081
6082	} else if ( typeof qualifier === "string" ) {
6083		var filtered = jQuery.grep(elements, function( elem ) {
6084			return elem.nodeType === 1;
6085		});
6086
6087		if ( isSimple.test( qualifier ) ) {
6088			return jQuery.filter(qualifier, filtered, !keep);
6089		} else {
6090			qualifier = jQuery.filter( qualifier, filtered );
6091		}
6092	}
6093
6094	return jQuery.grep(elements, function( elem, i ) {
6095		return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
6096	});
6097}
6098
6099
6100
6101
6102var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
6103	rleadingWhitespace = /^\s+/,
6104	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
6105	rtagName = /<([\w:]+)/,
6106	rtbody = /<tbody/i,
6107	rhtml = /<|&#?\w+;/,
6108	rnocache = /<(?:script|object|embed|option|style)/i,
6109	// checked="checked" or checked
6110	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
6111	wrapMap = {
6112		option: [ 1, "<select multiple='multiple'>", "</select>" ],
6113		legend: [ 1, "<fieldset>", "</fieldset>" ],
6114		thead: [ 1, "<table>", "</table>" ],
6115		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
6116		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
6117		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
6118		area: [ 1, "<map>", "</map>" ],
6119		_default: [ 0, "", "" ]
6120	};
6121
6122wrapMap.optgroup = wrapMap.option;
6123wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
6124wrapMap.th = wrapMap.td;
6125
6126// IE can't serialize <link> and <script> tags normally
6127if ( !jQuery.support.htmlSerialize ) {
6128	wrapMap._default = [ 1, "div<div>", "</div>" ];
6129}
6130
6131jQuery.fn.extend({
6132	text: function( text ) {
6133		if ( jQuery.isFunction(text) ) {
6134			return this.each(function(i) {
6135				var self = jQuery( this );
6136
6137				self.text( text.call(this, i, self.text()) );
6138			});
6139		}
6140
6141		if ( typeof text !== "object" && text !== undefined ) {
6142			return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
6143		}
6144
6145		return jQuery.text( this );
6146	},
6147
6148	wrapAll: function( html ) {
6149		if ( jQuery.isFunction( html ) ) {
6150			return this.each(function(i) {
6151				jQuery(this).wrapAll( html.call(this, i) );
6152			});
6153		}
6154
6155		if ( this[0] ) {
6156			// The elements to wrap the target around
6157			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
6158
6159			if ( this[0].parentNode ) {
6160				wrap.insertBefore( this[0] );
6161			}
6162
6163			wrap.map(function() {
6164				var elem = this;
6165
6166				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
6167					elem = elem.firstChild;
6168				}
6169
6170				return elem;
6171			}).append(this);
6172		}
6173
6174		return this;
6175	},
6176
6177	wrapInner: function( html ) {
6178		if ( jQuery.isFunction( html ) ) {
6179			return this.each(function(i) {
6180				jQuery(this).wrapInner( html.call(this, i) );
6181			});
6182		}
6183
6184		return this.each(function() {
6185			var self = jQuery( this ),
6186				contents = self.contents();
6187
6188			if ( contents.length ) {
6189				contents.wrapAll( html );
6190
6191			} else {
6192				self.append( html );
6193			}
6194		});
6195	},
6196
6197	wrap: function( html ) {
6198		return this.each(function() {
6199			jQuery( this ).wrapAll( html );
6200		});
6201	},
6202
6203	unwrap: function() {
6204		return this.parent().each(function() {
6205			if ( !jQuery.nodeName( this, "body" ) ) {
6206				jQuery( this ).replaceWith( this.childNodes );
6207			}
6208		}).end();
6209	},
6210
6211	append: function() {
6212		return this.domManip(arguments, true, function( elem ) {
6213			if ( this.nodeType === 1 ) {
6214				this.appendChild( elem );
6215			}
6216		});
6217	},
6218
6219	prepend: function() {
6220		return this.domManip(arguments, true, function( elem ) {
6221			if ( this.nodeType === 1 ) {
6222				this.insertBefore( elem, this.firstChild );
6223			}
6224		});
6225	},
6226
6227	before: function() {
6228		if ( this[0] && this[0].parentNode ) {
6229			return this.domManip(arguments, false, function( elem ) {
6230				this.parentNode.insertBefore( elem, this );
6231			});
6232		} else if ( arguments.length ) {
6233			var set = jQuery(arguments[0]);
6234			set.push.apply( set, this.toArray() );
6235			return this.pushStack( set, "before", arguments );
6236		}
6237	},
6238
6239	after: function() {
6240		if ( this[0] && this[0].parentNode ) {
6241			return this.domManip(arguments, false, function( elem ) {
6242				this.parentNode.insertBefore( elem, this.nextSibling );
6243			});
6244		} else if ( arguments.length ) {
6245			var set = this.pushStack( this, "after", arguments );
6246			set.push.apply( set, jQuery(arguments[0]).toArray() );
6247			return set;
6248		}
6249	},
6250
6251	// keepData is for internal use only--do not document
6252	remove: function( selector, keepData ) {
6253		for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
6254			if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
6255				if ( !keepData && elem.nodeType === 1 ) {
6256					jQuery.cleanData( elem.getElementsByTagName("*") );
6257					jQuery.cleanData( [ elem ] );
6258				}
6259
6260				if ( elem.parentNode ) {
6261					elem.parentNode.removeChild( elem );
6262				}
6263			}
6264		}
6265
6266		return this;
6267	},
6268
6269	empty: function() {
6270		for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
6271			// Remove element nodes and prevent memory leaks
6272			if ( elem.nodeType === 1 ) {
6273				jQuery.cleanData( elem.getElementsByTagName("*") );
6274			}
6275
6276			// Remove any remaining nodes
6277			while ( elem.firstChild ) {
6278				elem.removeChild( elem.firstChild );
6279			}
6280		}
6281
6282		return this;
6283	},
6284
6285	clone: function( dataAndEvents, deepDataAndEvents ) {
6286		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
6287		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
6288
6289		return this.map( function() {
6290			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
6291		});
6292	},
6293
6294	html: function( value ) {
6295		if ( value === undefined ) {
6296			return this[0] && this[0].nodeType === 1 ?
6297				this[0].innerHTML.replace(rinlinejQuery, "") :
6298				null;
6299
6300		// See if we can take a shortcut and just use innerHTML
6301		} else if ( typeof value === "string" && !rnocache.test( value ) &&
6302			(jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
6303			!wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
6304
6305			value = value.replace(rxhtmlTag, "<$1></$2>");
6306
6307			try {
6308				for ( var i = 0, l = this.length; i < l; i++ ) {
6309					// Remove element nodes and prevent memory leaks
6310					if ( this[i].nodeType === 1 ) {
6311						jQuery.cleanData( this[i].getElementsByTagName("*") );
6312						this[i].innerHTML = value;
6313					}
6314				}
6315
6316			// If using innerHTML throws an exception, use the fallback method
6317			} catch(e) {
6318				this.empty().append( value );
6319			}
6320
6321		} else if ( jQuery.isFunction( value ) ) {
6322			this.each(function(i){
6323				var self = jQuery( this );
6324
6325				self.html( value.call(this, i, self.html()) );
6326			});
6327
6328		} else {
6329			this.empty().append( value );
6330		}
6331
6332		return this;
6333	},
6334
6335	replaceWith: function( value ) {
6336		if ( this[0] && this[0].parentNode ) {
6337			// Make sure that the elements are removed from the DOM before they are inserted
6338			// this can help fix replacing a parent with child elements
6339			if ( jQuery.isFunction( value ) ) {
6340				return this.each(function(i) {
6341					var self = jQuery(this), old = self.html();
6342					self.replaceWith( value.call( this, i, old ) );
6343				});
6344			}
6345
6346			if ( typeof value !== "string" ) {
6347				value = jQuery( value ).detach();
6348			}
6349
6350			return this.each(function() {
6351				var next = this.nextSibling,
6352					parent = this.parentNode;
6353
6354				jQuery( this ).remove();
6355
6356				if ( next ) {
6357					jQuery(next).before( value );
6358				} else {
6359					jQuery(parent).append( value );
6360				}
6361			});
6362		} else {
6363			return this.length ?
6364				this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
6365				this;
6366		}
6367	},
6368
6369	detach: function( selector ) {
6370		return this.remove( selector, true );
6371	},
6372
6373	domManip: function( args, table, callback ) {
6374		var results, first, fragment, parent,
6375			value = args[0],
6376			scripts = [];
6377
6378		// We can't cloneNode fragments that contain checked, in WebKit
6379		if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
6380			return this.each(function() {
6381				jQuery(this).domManip( args, table, callback, true );
6382			});
6383		}
6384
6385		if ( jQuery.isFunction(value) ) {
6386			return this.each(function(i) {
6387				var self = jQuery(this);
6388				args[0] = value.call(this, i, table ? self.html() : undefined);
6389				self.domManip( args, table, callback );
6390			});
6391		}
6392
6393		if ( this[0] ) {
6394			parent = value && value.parentNode;
6395
6396			// If we're in a fragment, just use that instead of building a new one
6397			if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
6398				results = { fragment: parent };
6399
6400			} else {
6401				results = jQuery.buildFragment( args, this, scripts );
6402			}
6403
6404			fragment = results.fragment;
6405
6406			if ( fragment.childNodes.length === 1 ) {
6407				first = fragment = fragment.firstChild;
6408			} else {
6409				first = fragment.firstChild;
6410			}
6411
6412			if ( first ) {
6413				table = table && jQuery.nodeName( first, "tr" );
6414
6415				for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
6416					callback.call(
6417						table ?
6418							root(this[i], first) :
6419							this[i],
6420						// Make sure that we do not leak memory by inadvertently discarding
6421						// the original fragment (which might have attached data) instead of
6422						// using it; in addition, use the original fragment object for the last
6423						// item instead of first because it can end up being emptied incorrectly
6424						// in certain situations (Bug #8070).
6425						// Fragments from the fragment cache must always be cloned and never used
6426						// in place.
6427						results.cacheable || (l > 1 && i < lastIndex) ?
6428							jQuery.clone( fragment, true, true ) :
6429							fragment
6430					);
6431				}
6432			}
6433
6434			if ( scripts.length ) {
6435				jQuery.each( scripts, evalScript );
6436			}
6437		}
6438
6439		return this;
6440	}
6441});
6442
6443function root( elem, cur ) {
6444	return jQuery.nodeName(elem, "table") ?
6445		(elem.getElementsByTagName("tbody")[0] ||
6446		elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
6447		elem;
6448}
6449
6450function cloneCopyEvent( src, dest ) {
6451
6452	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
6453		return;
6454	}
6455
6456	var internalKey = jQuery.expando,
6457		oldData = jQuery.data( src ),
6458		curData = jQuery.data( dest, oldData );
6459
6460	// Switch to use the internal data object, if it exists, for the next
6461	// stage of data copying
6462	if ( (oldData = oldData[ internalKey ]) ) {
6463		var events = oldData.events;
6464				curData = curData[ internalKey ] = jQuery.extend({}, oldData);
6465
6466		if ( events ) {
6467			delete curData.handle;
6468			curData.events = {};
6469
6470			for ( var type in events ) {
6471				for ( var i = 0, l = events[ type ].length; i < l; i++ ) {
6472					jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data );
6473				}
6474			}
6475		}
6476	}
6477}
6478
6479function cloneFixAttributes(src, dest) {
6480	// We do not need to do anything for non-Elements
6481	if ( dest.nodeType !== 1 ) {
6482		return;
6483	}
6484
6485	var nodeName = dest.nodeName.toLowerCase();
6486
6487	// clearAttributes removes the attributes, which we don't want,
6488	// but also removes the attachEvent events, which we *do* want
6489	dest.clearAttributes();
6490
6491	// mergeAttributes, in contrast, only merges back on the
6492	// original attributes, not the events
6493	dest.mergeAttributes(src);
6494
6495	// IE6-8 fail to clone children inside object elements that use
6496	// the proprietary classid attribute value (rather than the type
6497	// attribute) to identify the type of content to display
6498	if ( nodeName === "object" ) {
6499		dest.outerHTML = src.outerHTML;
6500
6501	} else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) {
6502		// IE6-8 fails to persist the checked state of a cloned checkbox
6503		// or radio button. Worse, IE6-7 fail to give the cloned element
6504		// a checked appearance if the defaultChecked value isn't also set
6505		if ( src.checked ) {
6506			dest.defaultChecked = dest.checked = src.checked;
6507		}
6508
6509		// IE6-7 get confused and end up setting the value of a cloned
6510		// checkbox/radio button to an empty string instead of "on"
6511		if ( dest.value !== src.value ) {
6512			dest.value = src.value;
6513		}
6514
6515	// IE6-8 fails to return the selected option to the default selected
6516	// state when cloning options
6517	} else if ( nodeName === "option" ) {
6518		dest.selected = src.defaultSelected;
6519
6520	// IE6-8 fails to set the defaultValue to the correct value when
6521	// cloning other types of input fields
6522	} else if ( nodeName === "input" || nodeName === "textarea" ) {
6523		dest.defaultValue = src.defaultValue;
6524	}
6525
6526	// Event data gets referenced instead of copied if the expando
6527	// gets copied too
6528	dest.removeAttribute( jQuery.expando );
6529}
6530
6531jQuery.buildFragment = function( args, nodes, scripts ) {
6532	var fragment, cacheable, cacheresults,
6533		doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
6534
6535	// Only cache "small" (1/2 KB) HTML strings that are associated with the main document
6536	// Cloning options loses the selected state, so don't cache them
6537	// IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
6538	// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
6539	if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document &&
6540		args[0].charAt(0) === "<" && !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
6541
6542		cacheable = true;
6543		cacheresults = jQuery.fragments[ args[0] ];
6544		if ( cacheresults ) {
6545			if ( cacheresults !== 1 ) {
6546				fragment = cacheresults;
6547			}
6548		}
6549	}
6550
6551	if ( !fragment ) {
6552		fragment = doc.createDocumentFragment();
6553		jQuery.clean( args, doc, fragment, scripts );
6554	}
6555
6556	if ( cacheable ) {
6557		jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
6558	}
6559
6560	return { fragment: fragment, cacheable: cacheable };
6561};
6562
6563jQuery.fragments = {};
6564
6565jQuery.each({
6566	appendTo: "append",
6567	prependTo: "prepend",
6568	insertBefore: "before",
6569	insertAfter: "after",
6570	replaceAll: "replaceWith"
6571}, function( name, original ) {
6572	jQuery.fn[ name ] = function( selector ) {
6573		var ret = [],
6574			insert = jQuery( selector ),
6575			parent = this.length === 1 && this[0].parentNode;
6576
6577		if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
6578			insert[ original ]( this[0] );
6579			return this;
6580
6581		} else {
6582			for ( var i = 0, l = insert.length; i < l; i++ ) {
6583				var elems = (i > 0 ? this.clone(true) : this).get();
6584				jQuery( insert[i] )[ original ]( elems );
6585				ret = ret.concat( elems );
6586			}
6587
6588			return this.pushStack( ret, name, insert.selector );
6589		}
6590	};
6591});
6592
6593function getAll( elem ) {
6594	if ( "getElementsByTagName" in elem ) {
6595		return elem.getElementsByTagName( "*" );
6596
6597	} else if ( "querySelectorAll" in elem ) {
6598		return elem.querySelectorAll( "*" );
6599
6600	} else {
6601		return [];
6602	}
6603}
6604
6605jQuery.extend({
6606	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
6607		var clone = elem.cloneNode(true),
6608				srcElements,
6609				destElements,
6610				i;
6611
6612		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
6613				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
6614			// IE copies events bound via attachEvent when using cloneNode.
6615			// Calling detachEvent on the clone will also remove the events
6616			// from the original. In order to get around this, we use some
6617			// proprietary methods to clear the events. Thanks to MooTools
6618			// guys for this hotness.
6619
6620			cloneFixAttributes( elem, clone );
6621
6622			// Using Sizzle here is crazy slow, so we use getElementsByTagName
6623			// instead
6624			srcElements = getAll( elem );
6625			destElements = getAll( clone );
6626
6627			// Weird iteration because IE will replace the length property
6628			// with an element if you are cloning the body and one of the
6629			// elements on the page has a name or id of "length"
6630			for ( i = 0; srcElements[i]; ++i ) {
6631				cloneFixAttributes( srcElements[i], destElements[i] );
6632			}
6633		}
6634
6635		// Copy the events from the original to the clone
6636		if ( dataAndEvents ) {
6637			cloneCopyEvent( elem, clone );
6638
6639			if ( deepDataAndEvents ) {
6640				srcElements = getAll( elem );
6641				destElements = getAll( clone );
6642
6643				for ( i = 0; srcElements[i]; ++i ) {
6644					cloneCopyEvent( srcElements[i], destElements[i] );
6645				}
6646			}
6647		}
6648
6649		// Return the cloned set
6650		return clone;
6651},
6652	clean: function( elems, context, fragment, scripts ) {
6653		context = context || document;
6654
6655		// !context.createElement fails in IE with an error but returns typeof 'object'
6656		if ( typeof context.createElement === "undefined" ) {
6657			context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
6658		}
6659
6660		var ret = [];
6661
6662		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
6663			if ( typeof elem === "number" ) {
6664				elem += "";
6665			}
6666
6667			if ( !elem ) {
6668				continue;
6669			}
6670
6671			// Convert html string into DOM nodes
6672			if ( typeof elem === "string" && !rhtml.test( elem ) ) {
6673				elem = context.createTextNode( elem );
6674
6675			} else if ( typeof elem === "string" ) {
6676				// Fix "XHTML"-style tags in all browsers
6677				elem = elem.replace(rxhtmlTag, "<$1></$2>");
6678
6679				// Trim whitespace, otherwise indexOf won't work as expected
6680				var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
6681					wrap = wrapMap[ tag ] || wrapMap._default,
6682					depth = wrap[0],
6683					div = context.createElement("div");
6684
6685				// Go to html and back, then peel off extra wrappers
6686				div.innerHTML = wrap[1] + elem + wrap[2];
6687
6688				// Move to the right depth
6689				while ( depth-- ) {
6690					div = div.lastChild;
6691				}
6692
6693				// Remove IE's autoinserted <tbody> from table fragments
6694				if ( !jQuery.support.tbody ) {
6695
6696					// String was a <table>, *may* have spurious <tbody>
6697					var hasBody = rtbody.test(elem),
6698						tbody = tag === "table" && !hasBody ?
6699							div.firstChild && div.firstChild.childNodes :
6700
6701							// String was a bare <thead> or <tfoot>
6702							wrap[1] === "<table>" && !hasBody ?
6703								div.childNodes :
6704								[];
6705
6706					for ( var j = tbody.length - 1; j >= 0 ; --j ) {
6707						if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
6708							tbody[ j ].parentNode.removeChild( tbody[ j ] );
6709						}
6710					}
6711
6712				}
6713
6714				// IE completely kills leading whitespace when innerHTML is used
6715				if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
6716					div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
6717				}
6718
6719				elem = div.childNodes;
6720			}
6721
6722			if ( elem.nodeType ) {
6723				ret.push( elem );
6724			} else {
6725				ret = jQuery.merge( ret, elem );
6726			}
6727		}
6728
6729		if ( fragment ) {
6730			for ( i = 0; ret[i]; i++ ) {
6731				if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
6732					scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
6733
6734				} else {
6735					if ( ret[i].nodeType === 1 ) {
6736						ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
6737					}
6738					fragment.appendChild( ret[i] );
6739				}
6740			}
6741		}
6742
6743		return ret;
6744	},
6745
6746	cleanData: function( elems ) {
6747		var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special,
6748			deleteExpando = jQuery.support.deleteExpando;
6749
6750		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
6751			if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
6752				continue;
6753			}
6754
6755			id = elem[ jQuery.expando ];
6756
6757			if ( id ) {
6758				data = cache[ id ] && cache[ id ][ internalKey ];
6759
6760				if ( data && data.events ) {
6761					for ( var type in data.events ) {
6762						if ( special[ type ] ) {
6763							jQuery.event.remove( elem, type );
6764
6765						// This is a shortcut to avoid jQuery.event.remove's overhead
6766						} else {
6767							jQuery.removeEvent( elem, type, data.handle );
6768						}
6769					}
6770
6771					// Null the DOM reference to avoid IE6/7/8 leak (#7054)
6772					if ( data.handle ) {
6773						data.handle.elem = null;
6774					}
6775				}
6776
6777				if ( deleteExpando ) {
6778					delete elem[ jQuery.expando ];
6779
6780				} else if ( elem.removeAttribute ) {
6781					elem.removeAttribute( jQuery.expando );
6782				}
6783
6784				delete cache[ id ];
6785			}
6786		}
6787	}
6788});
6789
6790function evalScript( i, elem ) {
6791	if ( elem.src ) {
6792		jQuery.ajax({
6793			url: elem.src,
6794			async: false,
6795			dataType: "script"
6796		});
6797	} else {
6798		jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
6799	}
6800
6801	if ( elem.parentNode ) {
6802		elem.parentNode.removeChild( elem );
6803	}
6804}
6805
6806
6807
6808
6809var ralpha = /alpha\([^)]*\)/i,
6810	ropacity = /opacity=([^)]*)/,
6811	rdashAlpha = /-([a-z])/ig,
6812	// fixed for IE9, see #8346
6813	rupper = /([A-Z]|^ms)/g,
6814	rnumpx = /^-?\d+(?:px)?$/i,
6815	rnum = /^-?\d/,
6816
6817	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
6818	cssWidth = [ "Left", "Right" ],
6819	cssHeight = [ "Top", "Bottom" ],
6820	curCSS,
6821
6822	getComputedStyle,
6823	currentStyle,
6824
6825	fcamelCase = function( all, letter ) {
6826		return letter.toUpperCase();
6827	};
6828
6829jQuery.fn.css = function( name, value ) {
6830	// Setting 'undefined' is a no-op
6831	if ( arguments.length === 2 && value === undefined ) {
6832		return this;
6833	}
6834
6835	return jQuery.access( this, name, value, true, function( elem, name, value ) {
6836		return value !== undefined ?
6837			jQuery.style( elem, name, value ) :
6838			jQuery.css( elem, name );
6839	});
6840};
6841
6842jQuery.extend({
6843	// Add in style property hooks for overriding the default
6844	// behavior of getting and setting a style property
6845	cssHooks: {
6846		opacity: {
6847			get: function( elem, computed ) {
6848				if ( computed ) {
6849					// We should always get a number back from opacity
6850					var ret = curCSS( elem, "opacity", "opacity" );
6851					return ret === "" ? "1" : ret;
6852
6853				} else {
6854					return elem.style.opacity;
6855				}
6856			}
6857		}
6858	},
6859
6860	// Exclude the following css properties to add px
6861	cssNumber: {
6862		"zIndex": true,
6863		"fontWeight": true,
6864		"opacity": true,
6865		"zoom": true,
6866		"lineHeight": true
6867	},
6868
6869	// Add in properties whose names you wish to fix before
6870	// setting or getting the value
6871	cssProps: {
6872		// normalize float css property
6873		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
6874	},
6875
6876	// Get and set the style property on a DOM Node
6877	style: function( elem, name, value, extra ) {
6878		// Don't set styles on text and comment nodes
6879		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
6880			return;
6881		}
6882
6883		// Make sure that we're working with the right name
6884		var ret, origName = jQuery.camelCase( name ),
6885			style = elem.style, hooks = jQuery.cssHooks[ origName ];
6886
6887		name = jQuery.cssProps[ origName ] || origName;
6888
6889		// Check if we're setting a value
6890		if ( value !== undefined ) {
6891			// Make sure that NaN and null values aren't set. See: #7116
6892			if ( typeof value === "number" && isNaN( value ) || value == null ) {
6893				return;
6894			}
6895
6896			// If a number was passed in, add 'px' to the (except for certain CSS properties)
6897			if ( typeof value === "number" && !jQuery.cssNumber[ origName ] ) {
6898				value += "px";
6899			}
6900
6901			// If a hook was provided, use that value, otherwise just set the specified value
6902			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {
6903				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
6904				// Fixes bug #5509
6905				try {
6906					style[ name ] = value;
6907				} catch(e) {}
6908			}
6909
6910		} else {
6911			// If a hook was provided get the non-computed value from there
6912			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
6913				return ret;
6914			}
6915
6916			// Otherwise just get the value from the style object
6917			return style[ name ];
6918		}
6919	},
6920
6921	css: function( elem, name, extra ) {
6922		// Make sure that we're working with the right name
6923		var ret, origName = jQuery.camelCase( name ),
6924			hooks = jQuery.cssHooks[ origName ];
6925
6926		name = jQuery.cssProps[ origName ] || origName;
6927
6928		// If a hook was provided get the computed value from there
6929		if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
6930			return ret;
6931
6932		// Otherwise, if a way to get the computed value exists, use that
6933		} else if ( curCSS ) {
6934			return curCSS( elem, name, origName );
6935		}
6936	},
6937
6938	// A method for quickly swapping in/out CSS properties to get correct calculations
6939	swap: function( elem, options, callback ) {
6940		var old = {};
6941
6942		// Remember the old values, and insert the new ones
6943		for ( var name in options ) {
6944			old[ name ] = elem.style[ name ];
6945			elem.style[ name ] = options[ name ];
6946		}
6947
6948		callback.call( elem );
6949
6950		// Revert the old values
6951		for ( name in options ) {
6952			elem.style[ name ] = old[ name ];
6953		}
6954	},
6955
6956	camelCase: function( string ) {
6957		return string.replace( rdashAlpha, fcamelCase );
6958	}
6959});
6960
6961// DEPRECATED, Use jQuery.css() instead
6962jQuery.curCSS = jQuery.css;
6963
6964jQuery.each(["height", "width"], function( i, name ) {
6965	jQuery.cssHooks[ name ] = {
6966		get: function( elem, computed, extra ) {
6967			var val;
6968
6969			if ( computed ) {
6970				if ( elem.offsetWidth !== 0 ) {
6971					val = getWH( elem, name, extra );
6972
6973				} else {
6974					jQuery.swap( elem, cssShow, function() {
6975						val = getWH( elem, name, extra );
6976					});
6977				}
6978
6979				if ( val <= 0 ) {
6980					val = curCSS( elem, name, name );
6981
6982					if ( val === "0px" && currentStyle ) {
6983						val = currentStyle( elem, name, name );
6984					}
6985
6986					if ( val != null ) {
6987						// Should return "auto" instead of 0, use 0 for
6988						// temporary backwards-compat
6989						return val === "" || val === "auto" ? "0px" : val;
6990					}
6991				}
6992
6993				if ( val < 0 || val == null ) {
6994					val = elem.style[ name ];
6995
6996					// Should return "auto" instead of 0, use 0 for
6997					// temporary backwards-compat
6998					return val === "" || val === "auto" ? "0px" : val;
6999				}
7000
7001				return typeof val === "string" ? val : val + "px";
7002			}
7003		},
7004
7005		set: function( elem, value ) {
7006			if ( rnumpx.test( value ) ) {
7007				// ignore negative width and height values #1599
7008				value = parseFloat(value);
7009
7010				if ( value >= 0 ) {
7011					return value + "px";
7012				}
7013
7014			} else {
7015				return value;
7016			}
7017		}
7018	};
7019});
7020
7021if ( !jQuery.support.opacity ) {
7022	jQuery.cssHooks.opacity = {
7023		get: function( elem, computed ) {
7024			// IE uses filters for opacity
7025			return ropacity.test((computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "") ?
7026				(parseFloat(RegExp.$1) / 100) + "" :
7027				computed ? "1" : "";
7028		},
7029
7030		set: function( elem, value ) {
7031			var style = elem.style;
7032
7033			// IE has trouble with opacity if it does not have layout
7034			// Force it by setting the zoom level
7035			style.zoom = 1;
7036
7037			// Set the alpha filter to set the opacity
7038			var opacity = jQuery.isNaN(value) ?
7039				"" :
7040				"alpha(opacity=" + value * 100 + ")",
7041				filter = style.filter || "";
7042
7043			style.filter = ralpha.test(filter) ?
7044				filter.replace(ralpha, opacity) :
7045				style.filter + ' ' + opacity;
7046		}
7047	};
7048}
7049
7050jQuery(function() {
7051	// This hook cannot be added until DOM ready because the support test
7052	// for it is not run until after DOM ready
7053	if ( !jQuery.support.reliableMarginRight ) {
7054		jQuery.cssHooks.marginRight = {
7055			get: function( elem, computed ) {
7056				// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
7057				// Work around by temporarily setting element display to inline-block
7058				var ret;
7059				jQuery.swap( elem, { "display": "inline-block" }, function() {
7060					if ( computed ) {
7061						ret = curCSS( elem, "margin-right", "marginRight" );
7062					} else {
7063						ret = elem.style.marginRight;
7064					}
7065				});
7066				return ret;
7067			}
7068		};
7069	}
7070});
7071
7072if ( document.defaultView && document.defaultView.getComputedStyle ) {
7073	getComputedStyle = function( elem, newName, name ) {
7074		var ret, defaultView, computedStyle;
7075
7076		name = name.replace( rupper, "-$1" ).toLowerCase();
7077
7078		if ( !(defaultView = elem.ownerDocument.defaultView) ) {
7079			return undefined;
7080		}
7081
7082		if ( (computedStyle = defaultView.getComputedStyle( elem, null )) ) {
7083			ret = computedStyle.getPropertyValue( name );
7084			if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) {
7085				ret = jQuery.style( elem, name );
7086			}
7087		}
7088
7089		return ret;
7090	};
7091}
7092
7093if ( document.documentElement.currentStyle ) {
7094	currentStyle = function( elem, name ) {
7095		var left,
7096			ret = elem.currentStyle && elem.currentStyle[ name ],
7097			rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ],
7098			style = elem.style;
7099
7100		// From the awesome hack by Dean Edwards
7101		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
7102
7103		// If we're not dealing with a regular pixel number
7104		// but a number that has a weird ending, we need to convert it to pixels
7105		if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
7106			// Remember the original values
7107			left = style.left;
7108
7109			// Put in the new values to get a computed value out
7110			if ( rsLeft ) {
7111				elem.runtimeStyle.left = elem.currentStyle.left;
7112			}
7113			style.left = name === "fontSize" ? "1em" : (ret || 0);
7114			ret = style.pixelLeft + "px";
7115
7116			// Revert the changed values
7117			style.left = left;
7118			if ( rsLeft ) {
7119				elem.runtimeStyle.left = rsLeft;
7120			}
7121		}
7122
7123		return ret === "" ? "auto" : ret;
7124	};
7125}
7126
7127curCSS = getComputedStyle || currentStyle;
7128
7129function getWH( elem, name, extra ) {
7130	var which = name === "width" ? cssWidth : cssHeight,
7131		val = name === "width" ? elem.offsetWidth : elem.offsetHeight;
7132
7133	if ( extra === "border" ) {
7134		return val;
7135	}
7136
7137	jQuery.each( which, function() {
7138		if ( !extra ) {
7139			val -= parseFloat(jQuery.css( elem, "padding" + this )) || 0;
7140		}
7141
7142		if ( extra === "margin" ) {
7143			val += parseFloat(jQuery.css( elem, "margin" + this )) || 0;
7144
7145		} else {
7146			val -= parseFloat(jQuery.css( elem, "border" + this + "Width" )) || 0;
7147		}
7148	});
7149
7150	return val;
7151}
7152
7153if ( jQuery.expr && jQuery.expr.filters ) {
7154	jQuery.expr.filters.hidden = function( elem ) {
7155		var width = elem.offsetWidth,
7156			height = elem.offsetHeight;
7157
7158		return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "dis
7158play" )) === "none");
7159	};
7160
7161	jQuery.expr.filters.visible = function( elem ) {
7162		return !jQuery.expr.filters.hidden( elem );
7163	};
7164}
7165
7166
7167
7168
7169var r20 = /%20/g,
7170	rbracket = /\[\]$/,
7171	rCRLF = /\r?\n/g,
7172	rhash = /#.*$/,
7173	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
7174	rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
7175	// #7653, #8125, #8152: local protocol detection
7176	rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|widget):$/,
7177	rnoContent = /^(?:GET|HEAD)$/,
7178	rprotocol = /^\/\//,
7179	rquery = /\?/,
7180	rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
7181	rselectTextarea = /^(?:select|textarea)/i,
7182	rspacesAjax = /\s+/,
7183	rts = /([?&])_=[^&]*/,
7184	rucHeaders = /(^|\-)([a-z])/g,
7185	rucHeadersFunc = function( _, $1, $2 ) {
7186		return $1 + $2.toUpperCase();
7187	},
7188	rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,
7189
7190	// Keep a copy of the old load method
7191	_load = jQuery.fn.load,
7192
7193	/* Prefilters
7194	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
7195	 * 2) These are called:
7196	 *    - BEFORE asking for a transport
7197	 *    - AFTER param serialization (s.data is a string if s.processData is true)
7198	 * 3) key is the dataType
7199	 * 4) the catchall symbol "*" can be used
7200	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
7201	 */
7202	prefilters = {},
7203
7204	/* Transports bindings
7205	 * 1) key is the dataType
7206	 * 2) the catchall symbol "*" can be used
7207	 * 3) selection will start with transport dataType and THEN go to "*" if needed
7208	 */
7209	transports = {},
7210
7211	// Document location
7212	ajaxLocation,
7213
7214	// Document location segments
7215	ajaxLocParts;
7216
7217// #8138, IE may throw an exception when accessing
7218// a field from document.location if document.domain has been set
7219try {
7220	ajaxLocation = document.location.href;
7221} catch( e ) {
7222	// Use the href attribute of an A element
7223	// since IE will modify it given document.location
7224	ajaxLocation = document.createElement( "a" );
7225	ajaxLocation.href = "";
7226	ajaxLocation = ajaxLocation.href;
7227}
7228
7229// Segment location into parts
7230ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
7231
7232// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
7233function addToPrefiltersOrTransports( structure ) {
7234
7235	// dataTypeExpression is optional and defaults to "*"
7236	return function( dataTypeExpression, func ) {
7237
7238		if ( typeof dataTypeExpression !== "string" ) {
7239			func = dataTypeExpression;
7240			dataTypeExpression = "*";
7241		}
7242
7243		if ( jQuery.isFunction( func ) ) {
7244			var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),
7245				i = 0,
7246				length = dataTypes.length,
7247				dataType,
7248				list,
7249				placeBefore;
7250
7251			// For each dataType in the dataTypeExpression
7252			for(; i < length; i++ ) {
7253				dataType = dataTypes[ i ];
7254				// We control if we're asked to add before
7255				// any existing element
7256				placeBefore = /^\+/.test( dataType );
7257				if ( placeBefore ) {
7258					dataType = dataType.substr( 1 ) || "*";
7259				}
7260				list = structure[ dataType ] = structure[ dataType ] || [];
7261				// then we add to the structure accordingly
7262				list[ placeBefore ? "unshift" : "push" ]( func );
7263			}
7264		}
7265	};
7266}
7267
7268//Base inspection function for prefilters and transports
7269function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
7270		dataType /* internal */, inspected /* internal */ ) {
7271
7272	dataType = dataType || options.dataTypes[ 0 ];
7273	inspected = inspected || {};
7274
7275	inspected[ dataType ] = true;
7276
7277	var list = structure[ dataType ],
7278		i = 0,
7279		length = list ? list.length : 0,
7280		executeOnly = ( structure === prefilters ),
7281		selection;
7282
7283	for(; i < length && ( executeOnly || !selection ); i++ ) {
7284		selection = list[ i ]( options, originalOptions, jqXHR );
7285		// If we got redirected to another dataType
7286		// we try there if executing only and not done already
7287		if ( typeof selection === "string" ) {
7288			if ( !executeOnly || inspected[ selection ] ) {
7289				selection = undefined;
7290			} else {
7291				options.dataTypes.unshift( selection );
7292				selection = inspectPrefiltersOrTransports(
7293						structure, options, originalOptions, jqXHR, selection, inspected );
7294			}
7295		}
7296	}
7297	// If we're only executing or nothing was selected
7298	// we try the catchall dataType if not done already
7299	if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
7300		selection = inspectPrefiltersOrTransports(
7301				structure, options, originalOptions, jqXHR, "*", inspected );
7302	}
7303	// unnecessary when only executing (prefilters)
7304	// but it'll be ignored by the caller in that case
7305	return selection;
7306}
7307
7308jQuery.fn.extend({
7309	load: function( url, params, callback ) {
7310		if ( typeof url !== "string" && _load ) {
7311			return _load.apply( this, arguments );
7312
7313		// Don't do a request if no elements are being requested
7314		} else if ( !this.length ) {
7315			return this;
7316		}
7317
7318		var off = url.indexOf( " " );
7319		if ( off >= 0 ) {
7320			var selector = url.slice( off, url.length );
7321			url = url.slice( 0, off );
7322		}
7323
7324		// Default to a GET request
7325		var type = "GET";
7326
7327		// If the second parameter was provided
7328		if ( params ) {
7329			// If it's a function
7330			if ( jQuery.isFunction( params ) ) {
7331				// We assume that it's the callback
7332				callback = params;
7333				params = undefined;
7334
7335			// Otherwise, build a param string
7336			} else if ( typeof params === "object" ) {
7337				params = jQuery.param( params, jQuery.ajaxSettings.traditional );
7338				type = "POST";
7339			}
7340		}
7341
7342		var self = this;
7343
7344		// Request the remote document
7345		jQuery.ajax({
7346			url: url,
7347			type: type,
7348			dataType: "html",
7349			data: params,
7350			// Complete callback (responseText is used internally)
7351			complete: function( jqXHR, status, responseText ) {
7352				// Store the response as specified by the jqXHR object
7353				responseText = jqXHR.responseText;
7354				// If successful, inject the HTML into all the matched elements
7355				if ( jqXHR.isResolved() ) {
7356					// #4825: Get the actual response in case
7357					// a dataFilter is present in ajaxSettings
7358					jqXHR.done(function( r ) {
7359						responseText = r;
7360					});
7361					// See if a selector was specified
7362					self.html( selector ?
7363						// Create a dummy div to hold the results
7364						jQuery("<div>")
7365							// inject the contents of the document in, removing the scripts
7366							// to avoid any 'Permission Denied' errors in IE
7367							.append(responseText.replace(rscript, ""))
7368
7369							// Locate the specified elements
7370							.find(selector) :
7371
7372						// If not, just inject the full result
7373						responseText );
7374				}
7375
7376				if ( callback ) {
7377					self.each( callback, [ responseText, status, jqXHR ] );
7378				}
7379			}
7380		});
7381
7382		return this;
7383	},
7384
7385	serialize: function() {
7386		return jQuery.param( this.serializeArray() );
7387	},
7388
7389	serializeArray: function() {
7390		return this.map(function(){
7391			return this.elements ? jQuery.makeArray( this.elements ) : this;
7392		})
7393		.filter(function(){
7394			return this.name && !this.disabled &&
7395				( this.checked || rselectTextarea.test( this.nodeName ) ||
7396					rinput.test( this.type ) );
7397		})
7398		.map(function( i, elem ){
7399			var val = jQuery( this ).val();
7400
7401			return val == null ?
7402				null :
7403				jQuery.isArray( val ) ?
7404					jQuery.map( val, function( val, i ){
7405						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
7406					}) :
7407					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
7408		}).get();
7409	}
7410});
7411
7412// Attach a bunch of functions for handling common AJAX events
7413jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
7414	jQuery.fn[ o ] = function( f ){
7415		return this.bind( o, f );
7416	};
7417} );
7418
7419jQuery.each( [ "get", "post" ], function( i, method ) {
7420	jQuery[ method ] = function( url, data, callback, type ) {
7421		// shift arguments if data argument was omitted
7422		if ( jQuery.isFunction( data ) ) {
7423			type = type || callback;
7424			callback = data;
7425			data = undefined;
7426		}
7427
7428		return jQuery.ajax({
7429			type: method,
7430			url: url,
7431			data: data,
7432			success: callback,
7433			dataType: type
7434		});
7435	};
7436} );
7437
7438jQuery.extend({
7439
7440	getScript: function( url, callback ) {
7441		return jQuery.get( url, undefined, callback, "script" );
7442	},
7443
7444	getJSON: function( url, data, callback ) {
7445		return jQuery.get( url, data, callback, "json" );
7446	},
7447
7448	// Creates a full fledged settings object into target
7449	// with both ajaxSettings and settings fields.
7450	// If target is omitted, writes into ajaxSettings.
7451	ajaxSetup: function ( target, settings ) {
7452		if ( !settings ) {
7453			// Only one parameter, we extend ajaxSettings
7454			settings = target;
7455			target = jQuery.extend( true, jQuery.ajaxSettings, settings );
7456		} else {
7457			// target was provided, we extend into it
7458			jQuery.extend( true, target, jQuery.ajaxSettings, settings );
7459		}
7460		// Flatten fields we don't want deep extended
7461		for( var field in { context: 1, url: 1 } ) {
7462			if ( field in settings ) {
7463				target[ field ] = settings[ field ];
7464			} else if( field in jQuery.ajaxSettings ) {
7465				target[ field ] = jQuery.ajaxSettings[ field ];
7466			}
7467		}
7468		return target;
7469	},
7470
7471	ajaxSettings: {
7472		url: ajaxLocation,
7473		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
7474		global: true,
7475		type: "GET",
7476		contentType: "application/x-www-form-urlencoded",
7477		processData: true,
7478		async: true,
7479		/*
7480		timeout: 0,
7481		data: null,
7482		dataType: null,
7483		username: null,
7484		password: null,
7485		cache: null,
7486		traditional: false,
7487		headers: {},
7488		*/
7489
7490		accepts: {
7491			xml: "application/xml, text/xml",
7492			html: "text/html",
7493			text: "text/plain",
7494			json: "application/json, text/javascript",
7495			"*": "*/*"
7496		},
7497
7498		contents: {
7499			xml: /xml/,
7500			html: /html/,
7501			json: /json/
7502		},
7503
7504		responseFields: {
7505			xml: "responseXML",
7506			text: "responseText"
7507		},
7508
7509		// List of data converters
7510		// 1) key format is "source_type destination_type" (a single space in-between)
7511		// 2) the catchall symbol "*" can be used for source_type
7512		converters: {
7513
7514			// Convert anything to text
7515			"* text": window.String,
7516
7517			// Text to html (true = no transformation)
7518			"text html": true,
7519
7520			// Evaluate text as a json expression
7521			"text json": jQuery.parseJSON,
7522
7523			// Parse text as xml
7524			"text xml": jQuery.parseXML
7525		}
7526	},
7527
7528	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
7529	ajaxTransport: addToPrefiltersOrTransports( transports ),
7530
7531	// Main method
7532	ajax: function( url, options ) {
7533
7534		// If url is an object, simulate pre-1.5 signature
7535		if ( typeof url === "object" ) {
7536			options = url;
7537			url = undefined;
7538		}
7539
7540		// Force options to be an object
7541		options = options || {};
7542
7543		var // Create the final options object
7544			s = jQuery.ajaxSetup( {}, options ),
7545			// Callbacks context
7546			callbackContext = s.context || s,
7547			// Context for global events
7548			// It's the callbackContext if one was provided in the options
7549			// and if it's a DOM node or a jQuery collection
7550			globalEventContext = callbackContext !== s &&
7551				( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
7552						jQuery( callbackContext ) : jQuery.event,
7553			// Deferreds
7554			deferred = jQuery.Deferred(),
7555			completeDeferred = jQuery._Deferred(),
7556			// Status-dependent callbacks
7557			statusCode = s.statusCode || {},
7558			// ifModified key
7559			ifModifiedKey,
7560			// Headers (they are sent all at once)
7561			requestHeaders = {},
7562			// Response headers
7563			responseHeadersString,
7564			responseHeaders,
7565			// transport
7566			transport,
7567			// timeout handle
7568			timeoutTimer,
7569			// Cross-domain detection vars
7570			parts,
7571			// The jqXHR state
7572			state = 0,
7573			// To know if global events are to be dispatched
7574			fireGlobals,
7575			// Loop variable
7576			i,
7577			// Fake xhr
7578			jqXHR = {
7579
7580				readyState: 0,
7581
7582				// Caches the header
7583				setRequestHeader: function( name, value ) {
7584					if ( !state ) {
7585						requestHeaders[ name.toLowerCase().replace( rucHeaders, rucHeadersFunc ) ] = value;
7586					}
7587					return this;
7588				},
7589
7590				// Raw string
7591				getAllResponseHeaders: function() {
7592					return state === 2 ? responseHeadersString : null;
7593				},
7594
7595				// Builds headers hashtable if needed
7596				getResponseHeader: function( key ) {
7597					var match;
7598					if ( state === 2 ) {
7599						if ( !responseHeaders ) {
7600							responseHeaders = {};
7601							while( ( match = rheaders.exec( responseHeadersString ) ) ) {
7602								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
7603							}
7604						}
7605						match = responseHeaders[ key.toLowerCase() ];
7606					}
7607					return match === undefined ? null : match;
7608				},
7609
7610				// Overrides response content-type header
7611				overrideMimeType: function( type ) {
7612					if ( !state ) {
7613						s.mimeType = type;
7614					}
7615					return this;
7616				},
7617
7618				// Cancel the request
7619				abort: function( statusText ) {
7620					statusText = statusText || "abort";
7621					if ( transport ) {
7622						transport.abort( statusText );
7623					}
7624					done( 0, statusText );
7625					return this;
7626				}
7627			};
7628
7629		// Callback for when everything is done
7630		// It is defined here because jslint complains if it is declared
7631		// at the end of the function (which would be more logical and readable)
7632		function done( status, statusText, responses, headers ) {
7633
7634			// Called once
7635			if ( state === 2 ) {
7636				return;
7637			}
7638
7639			// State is "done" now
7640			state = 2;
7641
7642			// Clear timeout if it exists
7643			if ( timeoutTimer ) {
7644				clearTimeout( timeoutTimer );
7645			}
7646
7647			// Dereference transport for early garbage collection
7648			// (no matter how long the jqXHR object will be used)
7649			transport = undefined;
7650
7651			// Cache response headers
7652			responseHeadersString = headers || "";
7653
7654			// Set readyState
7655			jqXHR.readyState = status ? 4 : 0;
7656
7657			var isSuccess,
7658				success,
7659				error,
7660				response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,
7661				lastModified,
7662				etag;
7663
7664			// If successful, handle type chaining
7665			if ( status >= 200 && status < 300 || status === 304 ) {
7666
7667				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
7668				if ( s.ifModified ) {
7669
7670					if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) {
7671						jQuery.lastModified[ ifModifiedKey ] = lastModified;
7672					}
7673					if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) {
7674						jQuery.etag[ ifModifiedKey ] = etag;
7675					}
7676				}
7677
7678				// If not modified
7679				if ( status === 304 ) {
7680
7681					statusText = "notmodified";
7682					isSuccess = true;
7683
7684				// If we have data
7685				} else {
7686
7687					try {
7688						success = ajaxConvert( s, response );
7689						statusText = "success";
7690						isSuccess = true;
7691					} catch(e) {
7692						// We have a parsererror
7693						statusText = "parsererror";
7694						error = e;
7695					}
7696				}
7697			} else {
7698				// We extract error from statusText
7699				// then normalize statusText and status for non-aborts
7700				error = statusText;
7701				if( !statusText || status ) {
7702					statusText = "error";
7703					if ( status < 0 ) {
7704						status = 0;
7705					}
7706				}
7707			}
7708
7709			// Set data for the fake xhr object
7710			jqXHR.status = status;
7711			jqXHR.statusText = statusText;
7712
7713			// Success/Error
7714			if ( isSuccess ) {
7715				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
7716			} else {
7717				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
7718			}
7719
7720			// Status-dependent callbacks
7721			jqXHR.statusCode( statusCode );
7722			statusCode = undefined;
7723
7724			if ( fireGlobals ) {
7725				globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
7726						[ jqXHR, s, isSuccess ? success : error ] );
7727			}
7728
7729			// Complete
7730			completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] );
7731
7732			if ( fireGlobals ) {
7733				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s] );
7734				// Handle the global AJAX counter
7735				if ( !( --jQuery.active ) ) {
7736					jQuery.event.trigger( "ajaxStop" );
7737				}
7738			}
7739		}
7740
7741		// Attach deferreds
7742		deferred.promise( jqXHR );
7743		jqXHR.success = jqXHR.done;
7744		jqXHR.error = jqXHR.fail;
7745		jqXHR.complete = completeDeferred.done;
7746
7747		// Status-dependent callbacks
7748		jqXHR.statusCode = function( map ) {
7749			if ( map ) {
7750				var tmp;
7751				if ( state < 2 ) {
7752					for( tmp in map ) {
7753						statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
7754					}
7755				} else {
7756					tmp = map[ jqXHR.status ];
7757					jqXHR.then( tmp, tmp );
7758				}
7759			}
7760			return this;
7761		};
7762
7763		// Remove hash character (#7531: and string promotion)
7764		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
7765		// We also use the url parameter if available
7766		s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
7767
7768		// Extract dataTypes list
7769		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax );
7770
7771		// Determine if a cross-domain request is in order
7772		if ( s.crossDomain == null ) {
7773			parts = rurl.exec( s.url.toLowerCase() );
7774			s.crossDomain = !!( parts &&
7775				( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||
7776					( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
7777						( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
7778			);
7779		}
7780
7781		// Convert data if not already a string
7782		if ( s.data && s.processData && typeof s.data !== "string" ) {
7783			s.data = jQuery.param( s.data, s.traditional );
7784		}
7785
7786		// Apply prefilters
7787		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
7788
7789		// If request was aborted inside a prefiler, stop there
7790		if ( state === 2 ) {
7791			return false;
7792		}
7793
7794		// We can fire global events as of now if asked to
7795		fireGlobals = s.global;
7796
7797		// Uppercase the type
7798		s.type = s.type.toUpperCase();
7799
7800		// Determine if request has content
7801		s.hasContent = !rnoContent.test( s.type );
7802
7803		// Watch for a new set of requests
7804		if ( fireGlobals && jQuery.active++ === 0 ) {
7805			jQuery.event.trigger( "ajaxStart" );
7806		}
7807
7808		// More options handling for requests with no content
7809		if ( !s.hasContent ) {
7810
7811			// If data is available, append data to url
7812			if ( s.data ) {
7813				s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
7814			}
7815
7816			// Get ifModifiedKey before adding the anti-cache parameter
7817			ifModifiedKey = s.url;
7818
7819			// Add anti-cache in url if needed
7820			if ( s.cache === false ) {
7821
7822				var ts = jQuery.now(),
7823					// try replacing _= if it is there
7824					ret = s.url.replace( rts, "$1_=" + ts );
7825
7826				// if nothing was replaced, add timestamp to the end
7827				s.url = ret + ( (ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
7828			}
7829		}
7830
7831		// Set the correct header, if data is being sent
7832		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
7833			requestHeaders[ "Content-Type" ] = s.contentType;
7834		}
7835
7836		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
7837		if ( s.ifModified ) {
7838			ifModifiedKey = ifModifiedKey || s.url;
7839			if ( jQuery.lastModified[ ifModifiedKey ] ) {
7840				requestHeaders[ "If-Modified-Since" ] = jQuery.lastModified[ ifModifiedKey ];
7841			}
7842			if ( jQuery.etag[ ifModifiedKey ] ) {
7843				requestHeaders[ "If-None-Match" ] = jQuery.etag[ ifModifiedKey ];
7844			}
7845		}
7846
7847		// Set the Accepts header for the server, depending on the dataType
7848		requestHeaders.Accept = s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
7849			s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) :
7850			s.accepts[ "*" ];
7851
7852		// Check for headers option
7853		for ( i in s.headers ) {
7854			jqXHR.setRequestHeader( i, s.headers[ i ] );
7855		}
7856
7857		// Allow custom headers/mimetypes and early abort
7858		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
7859				// Abort if not done already
7860				jqXHR.abort();
7861				return false;
7862
7863		}
7864
7865		// Install callbacks on deferreds
7866		for ( i in { success: 1, error: 1, complete: 1 } ) {
7867			jqXHR[ i ]( s[ i ] );
7868		}
7869
7870		// Get transport
7871		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
7872
7873		// If no transport, we auto-abort
7874		if ( !transport ) {
7875			done( -1, "No Transport" );
7876		} else {
7877			jqXHR.readyState = 1;
7878			// Send global event
7879			if ( fireGlobals ) {
7880				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
7881			}
7882			// Timeout
7883			if ( s.async && s.timeout > 0 ) {
7884				timeoutTimer = setTimeout( function(){
7885					jqXHR.abort( "timeout" );
7886				}, s.timeout );
7887			}
7888
7889			try {
7890				state = 1;
7891				transport.send( requestHeaders, done );
7892			} catch (e) {
7893				// Propagate exception as error if not done
7894				if ( status < 2 ) {
7895					done( -1, e );
7896				// Simply rethrow otherwise
7897				} else {
7898					jQuery.error( e );
7899				}
7900			}
7901		}
7902
7903		return jqXHR;
7904	},
7905
7906	// Serialize an array of form elements or a set of
7907	// key/values into a query string
7908	param: function( a, traditional ) {
7909		var s = [],
7910			add = function( key, value ) {
7911				// If value is a function, invoke it and return its value
7912				value = jQuery.isFunction( value ) ? value() : value;
7913				s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
7914			};
7915
7916		// Set traditional to true for jQuery <= 1.3.2 behavior.
7917		if ( traditional === undefined ) {
7918			traditional = jQuery.ajaxSettings.traditional;
7919		}
7920
7921		// If an array was passed in, assume that it is an array of form elements.
7922		if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
7923			// Serialize the form elements
7924			jQuery.each( a, function() {
7925				add( this.name, this.value );
7926			} );
7927
7928		} else {
7929			// If traditional, encode the "old" way (the way 1.3.2 or older
7930			// did it), otherwise encode params recursively.
7931			for ( var prefix in a ) {
7932				buildParams( prefix, a[ prefix ], traditional, add );
7933			}
7934		}
7935
7936		// Return the resulting serialization
7937		return s.join( "&" ).replace( r20, "+" );
7938	}
7939});
7940
7941function buildParams( prefix, obj, traditional, add ) {
7942	if ( jQuery.isArray( obj ) && obj.length ) {
7943		// Serialize array item.
7944		jQuery.each( obj, function( i, v ) {
7945			if ( traditional || rbracket.test( prefix ) ) {
7946				// Treat each array item as a scalar.
7947				add( prefix, v );
7948
7949			} else {
7950				// If array item is non-scalar (array or object), encode its
7951				// numeric index to resolve deserialization ambiguity issues.
7952				// Note that rack (as of 1.0.0) can't currently deserialize
7953				// nested arrays properly, and attempting to do so may cause
7954				// a server error. Possible fixes are to modify rack's
7955				// deserialization algorithm or to provide an option or flag
7956				// to force array serialization to be shallow.
7957				buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add );
7958			}
7959		});
7960
7961	} else if ( !traditional && obj != null && typeof obj === "object" ) {
7962		// If we see an array here, it is empty and should be treated as an empty
7963		// object
7964		if ( jQuery.isArray( obj ) || jQuery.isEmptyObject( obj ) ) {
7965			add( prefix, "" );
7966
7967		// Serialize object item.
7968		} else {
7969			for ( var name in obj ) {
7970				buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
7971			}
7972		}
7973
7974	} else {
7975		// Serialize scalar item.
7976		add( prefix, obj );
7977	}
7978}
7979
7980// This is still on the jQuery object... for now
7981// Want to move this to jQuery.ajax some day
7982jQuery.extend({
7983
7984	// Counter for holding the number of active queries
7985	active: 0,
7986
7987	// Last-Modified header cache for next request
7988	lastModified: {},
7989	etag: {}
7990
7991}
7991);
7992
7993/* Handles responses to an ajax request:
7994 * - sets all responseXXX fields accordingly
7995 * - finds the right dataType (mediates between content-type and expected dataType)
7996 * - returns the corresponding response
7997 */
7998function ajaxHandleResponses( s, jqXHR, responses ) {
7999
8000	var contents = s.contents,
8001		dataTypes = s.dataTypes,
8002		responseFields = s.responseFields,
8003		ct,
8004		type,
8005		finalDataType,
8006		firstDataType;
8007
8008	// Fill responseXXX fields
8009	for( type in responseFields ) {
8010		if ( type in responses ) {
8011			jqXHR[ responseFields[type] ] = responses[ type ];
8012		}
8013	}
8014
8015	// Remove auto dataType and get content-type in the process
8016	while( dataTypes[ 0 ] === "*" ) {
8017		dataTypes.shift();
8018		if ( ct === undefined ) {
8019			ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
8020		}
8021	}
8022
8023	// Check if we're dealing with a known content-type
8024	if ( ct ) {
8025		for ( type in contents ) {
8026			if ( contents[ type ] && contents[ type ].test( ct ) ) {
8027				dataTypes.unshift( type );
8028				break;
8029			}
8030		}
8031	}
8032
8033	// Check to see if we have a response for the expected dataType
8034	if ( dataTypes[ 0 ] in responses ) {
8035		finalDataType = dataTypes[ 0 ];
8036	} else {
8037		// Try convertible dataTypes
8038		for ( type in responses ) {
8039			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
8040				finalDataType = type;
8041				break;
8042			}
8043			if ( !firstDataType ) {
8044				firstDataType = type;
8045			}
8046		}
8047		// Or just use first one
8048		finalDataType = finalDataType || firstDataType;
8049	}
8050
8051	// If we found a dataType
8052	// We add the dataType to the list if needed
8053	// and return the corresponding response
8054	if ( finalDataType ) {
8055		if ( finalDataType !== dataTypes[ 0 ] ) {
8056			dataTypes.unshift( finalDataType );
8057		}
8058		return responses[ finalDataType ];
8059	}
8060}
8061
8062// Chain conversions given the request and the original response
8063function ajaxConvert( s, response ) {
8064
8065	// Apply the dataFilter if provided
8066	if ( s.dataFilter ) {
8067		response = s.dataFilter( response, s.dataType );
8068	}
8069
8070	var dataTypes = s.dataTypes,
8071		converters = {},
8072		i,
8073		key,
8074		length = dataTypes.length,
8075		tmp,
8076		// Current and previous dataTypes
8077		current = dataTypes[ 0 ],
8078		prev,
8079		// Conversion expression
8080		conversion,
8081		// Conversion function
8082		conv,
8083		// Conversion functions (transitive conversion)
8084		conv1,
8085		conv2;
8086
8087	// For each dataType in the chain
8088	for( i = 1; i < length; i++ ) {
8089
8090		// Create converters map
8091		// with lowercased keys
8092		if ( i === 1 ) {
8093			for( key in s.converters ) {
8094				if( typeof key === "string" ) {
8095					converters[ key.toLowerCase() ] = s.converters[ key ];
8096				}
8097			}
8098		}
8099
8100		// Get the dataTypes
8101		prev = current;
8102		current = dataTypes[ i ];
8103
8104		// If current is auto dataType, update it to prev
8105		if( current === "*" ) {
8106			current = prev;
8107		// If no auto and dataTypes are actually different
8108		} else if ( prev !== "*" && prev !== current ) {
8109
8110			// Get the converter
8111			conversion = prev + " " + current;
8112			conv = converters[ conversion ] || converters[ "* " + current ];
8113
8114			// If there is no direct converter, search transitively
8115			if ( !conv ) {
8116				conv2 = undefined;
8117				for( conv1 in converters ) {
8118					tmp = conv1.split( " " );
8119					if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) {
8120						conv2 = converters[ tmp[1] + " " + current ];
8121						if ( conv2 ) {
8122							conv1 = converters[ conv1 ];
8123							if ( conv1 === true ) {
8124								conv = conv2;
8125							} else if ( conv2 === true ) {
8126								conv = conv1;
8127							}
8128							break;
8129						}
8130					}
8131				}
8132			}
8133			// If we found no converter, dispatch an error
8134			if ( !( conv || conv2 ) ) {
8135				jQuery.error( "No conversion from " + conversion.replace(" "," to ") );
8136			}
8137			// If found converter is not an equivalence
8138			if ( conv !== true ) {
8139				// Convert with 1 or 2 converters accordingly
8140				response = conv ? conv( response ) : conv2( conv1(response) );
8141			}
8142		}
8143	}
8144	return response;
8145}
8146
8147
8148
8149
8150var jsc = jQuery.now(),
8151	jsre = /(\=)\?(&|$)|\?\?/i;
8152
8153// Default jsonp settings
8154jQuery.ajaxSetup({
8155	jsonp: "callback",
8156	jsonpCallback: function() {
8157		return jQuery.expando + "_" + ( jsc++ );
8158	}
8159});
8160
8161// Detect, normalize options and install callbacks for jsonp requests
8162jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
8163
8164	var dataIsString = ( typeof s.data === "string" );
8165
8166	if ( s.dataTypes[ 0 ] === "jsonp" ||
8167		originalSettings.jsonpCallback ||
8168		originalSettings.jsonp != null ||
8169		s.jsonp !== false && ( jsre.test( s.url ) ||
8170				dataIsString && jsre.test( s.data ) ) ) {
8171
8172		var responseContainer,
8173			jsonpCallback = s.jsonpCallback =
8174				jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback,
8175			previous = window[ jsonpCallback ],
8176			url = s.url,
8177			data = s.data,
8178			replace = "$1" + jsonpCallback + "$2",
8179			cleanUp = function() {
8180				// Set callback back to previous value
8181				window[ jsonpCallback ] = previous;
8182				// Call if it was a function and we have a response
8183				if ( responseContainer && jQuery.isFunction( previous ) ) {
8184					window[ jsonpCallback ]( responseContainer[ 0 ] );
8185				}
8186			};
8187
8188		if ( s.jsonp !== false ) {
8189			url = url.replace( jsre, replace );
8190			if ( s.url === url ) {
8191				if ( dataIsString ) {
8192					data = data.replace( jsre, replace );
8193				}
8194				if ( s.data === data ) {
8195					// Add callback manually
8196					url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback;
8197				}
8198			}
8199		}
8200
8201		s.url = url;
8202		s.data = data;
8203
8204		// Install callback
8205		window[ jsonpCallback ] = function( response ) {
8206			responseContainer = [ response ];
8207		};
8208
8209		// Install cleanUp function
8210		jqXHR.then( cleanUp, cleanUp );
8211
8212		// Use data converter to retrieve json after script execution
8213		s.converters["script json"] = function() {
8214			if ( !responseContainer ) {
8215				jQuery.error( jsonpCallback + " was not called" );
8216			}
8217			return responseContainer[ 0 ];
8218		};
8219
8220		// force json dataType
8221		s.dataTypes[ 0 ] = "json";
8222
8223		// Delegate to script
8224		return "script";
8225	}
8226} );
8227
8228
8229
8230
8231// Install script dataType
8232jQuery.ajaxSetup({
8233	accepts: {
8234		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
8235	},
8236	contents: {
8237		script: /javascript|ecmascript/
8238	},
8239	converters: {
8240		"text script": function( text ) {
8241			jQuery.globalEval( text );
8242			return text;
8243		}
8244	}
8245});
8246
8247// Handle cache's special case and global
8248jQuery.ajaxPrefilter( "script", function( s ) {
8249	if ( s.cache === undefined ) {
8250		s.cache = false;
8251	}
8252	if ( s.crossDomain ) {
8253		s.type = "GET";
8254		s.global = false;
8255	}
8256} );
8257
8258// Bind script tag hack transport
8259jQuery.ajaxTransport( "script", function(s) {
8260
8261	// This transport only deals with cross domain requests
8262	if ( s.crossDomain ) {
8263
8264		var script,
8265			head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
8266
8267		return {
8268
8269			send: function( _, callback ) {
8270
8271				script = document.createElement( "script" );
8272
8273				script.async = "async";
8274
8275				if ( s.scriptCharset ) {
8276					script.charset = s.scriptCharset;
8277				}
8278
8279				script.src = s.url;
8280
8281				// Attach handlers for all browsers
8282				script.onload = script.onreadystatechange = function( _, isAbort ) {
8283
8284					if ( !script.readyState || /loaded|complete/.test( script.readyState ) ) {
8285
8286						// Handle memory leak in IE
8287						script.onload = script.onreadystatechange = null;
8288
8289						// Remove the script
8290						if ( head && script.parentNode ) {
8291							head.removeChild( script );
8292						}
8293
8294						// Dereference the script
8295						script = undefined;
8296
8297						// Callback if not abort
8298						if ( !isAbort ) {
8299							callback( 200, "success" );
8300						}
8301					}
8302				};
8303				// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
8304				// This arises when a base node is used (#2709 and #4378).
8305				head.insertBefore( script, head.firstChild );
8306			},
8307
8308			abort: function() {
8309				if ( script ) {
8310					script.onload( 0, 1 );
8311				}
8312			}
8313		};
8314	}
8315} );
8316
8317
8318
8319
8320var // #5280: next active xhr id and list of active xhrs' callbacks
8321	xhrId = jQuery.now(),
8322	xhrCallbacks,
8323
8324	// XHR used to determine supports properties
8325	testXHR;
8326
8327// #5280: Internet Explorer will keep connections alive if we don't abort on unload
8328function xhrOnUnloadAbort() {
8329	jQuery( window ).unload(function() {
8330		// Abort all pending requests
8331		for ( var key in xhrCallbacks ) {
8332			xhrCallbacks[ key ]( 0, 1 );
8333		}
8334	});
8335}
8336
8337// Functions to create xhrs
8338function createStandardXHR() {
8339	try {
8340		return new window.XMLHttpRequest();
8341	} catch( e ) {}
8342}
8343
8344function createActiveXHR() {
8345	try {
8346		return new window.ActiveXObject( "Microsoft.XMLHTTP" );
8347	} catch( e ) {}
8348}
8349
8350// Create the request object
8351// (This is still attached to ajaxSettings for backward compatibility)
8352jQuery.ajaxSettings.xhr = window.ActiveXObject ?
8353	/* Microsoft failed to properly
8354	 * implement the XMLHttpRequest in IE7 (can't request local files),
8355	 * so we use the ActiveXObject when it is available
8356	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
8357	 * we need a fallback.
8358	 */
8359	function() {
8360		return !this.isLocal && createStandardXHR() || createActiveXHR();
8361	} :
8362	// For all other browsers, use the standard XMLHttpRequest object
8363	createStandardXHR;
8364
8365// Test if we can create an xhr object
8366testXHR = jQuery.ajaxSettings.xhr();
8367jQuery.support.ajax = !!testXHR;
8368
8369// Does this browser support crossDomain XHR requests
8370jQuery.support.cors = testXHR && ( "withCredentials" in testXHR );
8371
8372// No need for the temporary xhr anymore
8373testXHR = undefined;
8374
8375// Create transport if the browser can provide an xhr
8376if ( jQuery.support.ajax ) {
8377
8378	jQuery.ajaxTransport(function( s ) {
8379		// Cross domain only allowed if supported through XMLHttpRequest
8380		if ( !s.crossDomain || jQuery.support.cors ) {
8381
8382			var callback;
8383
8384			return {
8385				send: function( headers, complete ) {
8386
8387					// Get a new xhr
8388					var xhr = s.xhr(),
8389						handle,
8390						i;
8391
8392					// Open the socket
8393					// Passing null username, generates a login popup on Opera (#2865)
8394					if ( s.username ) {
8395						xhr.open( s.type, s.url, s.async, s.username, s.password );
8396					} else {
8397						xhr.open( s.type, s.url, s.async );
8398					}
8399
8400					// Apply custom fields if provided
8401					if ( s.xhrFields ) {
8402						for ( i in s.xhrFields ) {
8403							xhr[ i ] = s.xhrFields[ i ];
8404						}
8405					}
8406
8407					// Override mime type if needed
8408					if ( s.mimeType && xhr.overrideMimeType ) {
8409						xhr.overrideMimeType( s.mimeType );
8410					}
8411
8412					// X-Requested-With header
8413					// For cross-domain requests, seeing as conditions for a preflight are
8414					// akin to a jigsaw puzzle, we simply never set it to be sure.
8415					// (it can always be set on a per-request basis or even using ajaxSetup)
8416					// For same-domain requests, won't change header if already provided.
8417					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
8418						headers[ "X-Requested-With" ] = "XMLHttpRequest";
8419					}
8420
8421					// Need an extra try/catch for cross domain requests in Firefox 3
8422					try {
8423						for ( i in headers ) {
8424							xhr.setRequestHeader( i, headers[ i ] );
8425						}
8426					} catch( _ ) {}
8427
8428					// Do send the request
8429					// This may raise an exception which is actually
8430					// handled in jQuery.ajax (so no try/catch here)
8431					xhr.send( ( s.hasContent && s.data ) || null );
8432
8433					// Listener
8434					callback = function( _, isAbort ) {
8435
8436						var status,
8437							statusText,
8438							responseHeaders,
8439							responses,
8440							xml;
8441
8442						// Firefox throws exceptions when accessing properties
8443						// of an xhr when a network error occured
8444						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
8445						try {
8446
8447							// Was never called and is aborted or complete
8448							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
8449
8450								// Only called once
8451								callback = undefined;
8452
8453								// Do not keep as active anymore
8454								if ( handle ) {
8455									xhr.onreadystatechange = jQuery.noop;
8456									delete xhrCallbacks[ handle ];
8457								}
8458
8459								// If it's an abort
8460								if ( isAbort ) {
8461									// Abort it manually if needed
8462									if ( xhr.readyState !== 4 ) {
8463										xhr.abort();
8464									}
8465								} else {
8466									status = xhr.status;
8467									responseHeaders = xhr.getAllResponseHeaders();
8468									responses = {};
8469									xml = xhr.responseXML;
8470
8471									// Construct response list
8472									if ( xml && xml.documentElement /* #4958 */ ) {
8473										responses.xml = xml;
8474									}
8475									responses.text = xhr.responseText;
8476
8477									// Firefox throws an exception when accessing
8478									// statusText for faulty cross-domain requests
8479									try {
8480										statusText = xhr.statusText;
8481									} catch( e ) {
8482										// We normalize with Webkit giving an empty statusText
8483										statusText = "";
8484									}
8485
8486									// Filter status for non standard behaviors
8487
8488									// If the request is local and we have data: assume a success
8489									// (success with no data won't get notified, that's the best we
8490									// can do given current implementations)
8491									if ( !status && s.isLocal && !s.crossDomain ) {
8492										status = responses.text ? 200 : 404;
8493									// IE - #1450: sometimes returns 1223 when it should be 204
8494									} else if ( status === 1223 ) {
8495										status = 204;
8496									}
8497								}
8498							}
8499						} catch( firefoxAccessException ) {
8500							if ( !isAbort ) {
8501								complete( -1, firefoxAccessException );
8502							}
8503						}
8504
8505						// Call complete if needed
8506						if ( responses ) {
8507							complete( status, statusText, responses, responseHeaders );
8508						}
8509					};
8510
8511					// if we're in sync mode or it's in cache
8512					// and has been retrieved directly (IE6 & IE7)
8513					// we need to manually fire the callback
8514					if ( !s.async || xhr.readyState === 4 ) {
8515						callback();
8516					} else {
8517						// Create the active xhrs callbacks list if needed
8518						// and attach the unload handler
8519						if ( !xhrCallbacks ) {
8520							xhrCallbacks = {};
8521							xhrOnUnloadAbort();
8522						}
8523						// Add to list of active xhrs callbacks
8524						handle = xhrId++;
8525						xhr.onreadystatechange = xhrCallbacks[ handle ] = callback;
8526					}
8527				},
8528
8529				abort: function() {
8530					if ( callback ) {
8531						callback(0,1);
8532					}
8533				}
8534			};
8535		}
8536	});
8537}
8538
8539
8540
8541
8542var elemdisplay = {},
8543	rfxtypes = /^(?:toggle|show|hide)$/,
8544	rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,
8545	timerId,
8546	fxAttrs = [
8547		// height animations
8548		[ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
8549		// width animations
8550		[ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
8551		// opacity animations
8552		[ "opacity" ]
8553	];
8554
8555jQuery.fn.extend({
8556	show: function( speed, easing, callback ) {
8557		var elem, display;
8558
8559		if ( speed || speed === 0 ) {
8560			return this.animate( genFx("show", 3), speed, easing, callback);
8561
8562		} else {
8563			for ( var i = 0, j = this.length; i < j; i++ ) {
8564				elem = this[i];
8565				display = elem.style.display;
8566
8567				// Reset the inline display of this element to learn if it is
8568				// being hidden by cascaded rules or not
8569				if ( !jQuery._data(elem, "olddisplay") && display === "none" ) {
8570					display = elem.style.display = "";
8571				}
8572
8573				// Set elements which have been overridden with display: none
8574				// in a stylesheet to whatever the default browser style is
8575				// for such an element
8576				if ( display === "" && jQuery.css( elem, "display" ) === "none" ) {
8577					jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
8578				}
8579			}
8580
8581			// Set the display of most of the elements in a second loop
8582			// to avoid the constant reflow
8583			for ( i = 0; i < j; i++ ) {
8584				elem = this[i];
8585				display = elem.style.display;
8586
8587				if ( display === "" || display === "none" ) {
8588					elem.style.display = jQuery._data(elem, "olddisplay") || "";
8589				}
8590			}
8591
8592			return this;
8593		}
8594	},
8595
8596	hide: function( speed, easing, callback ) {
8597		if ( speed || speed === 0 ) {
8598			return this.animate( genFx("hide", 3), speed, easing, callback);
8599
8600		} else {
8601			for ( var i = 0, j = this.length; i < j; i++ ) {
8602				var display = jQuery.css( this[i], "display" );
8603
8604				if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) {
8605					jQuery._data( this[i], "olddisplay", display );
8606				}
8607			}
8608
8609			// Set the display of the elements in a second loop
8610			// to avoid the constant reflow
8611			for ( i = 0; i < j; i++ ) {
8612				this[i].style.display = "none";
8613			}
8614
8615			return this;
8616		}
8617	},
8618
8619	// Save the old toggle function
8620	_toggle: jQuery.fn.toggle,
8621
8622	toggle: function( fn, fn2, callback ) {
8623		var bool = typeof fn === "boolean";
8624
8625		if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
8626			this._toggle.apply( this, arguments );
8627
8628		} else if ( fn == null || bool ) {
8629			this.each(function() {
8630				var state = bool ? fn : jQuery(this).is(":hidden");
8631				jQuery(this)[ state ? "show" : "hide" ]();
8632			});
8633
8634		} else {
8635			this.animate(genFx("toggle", 3), fn, fn2, callback);
8636		}
8637
8638		return this;
8639	},
8640
8641	fadeTo: function( speed, to, easing, callback ) {
8642		return this.filter(":hidden").css("opacity", 0).show().end()
8643					.animate({opacity: to}, speed, easing, callback);
8644	},
8645
8646	animate: function( prop, speed, easing, callback ) {
8647		var optall = jQuery.speed(speed, easing, callback);
8648
8649		if ( jQuery.isEmptyObject( prop ) ) {
8650			return this.each( optall.complete );
8651		}
8652
8653		return this[ optall.queue === false ? "each" : "queue" ](function() {
8654			// XXX 'this' does not always have a nodeName when running the
8655			// test suite
8656
8657			var opt = jQuery.extend({}, optall), p,
8658				isElement = this.nodeType === 1,
8659				hidden = isElement && jQuery(this).is(":hidden"),
8660				self = this;
8661
8662			for ( p in prop ) {
8663				var name = jQuery.camelCase( p );
8664
8665				if ( p !== name ) {
8666					prop[ name ] = prop[ p ];
8667					delete prop[ p ];
8668					p = name;
8669				}
8670
8671				if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
8672					return opt.complete.call(this);
8673				}
8674
8675				if ( isElement && ( p === "height" || p === "width" ) ) {
8676					// Make sure that nothing sneaks out
8677					// Record all 3 overflow attributes because IE does not
8678					// change the overflow attribute when overflowX and
8679					// overflowY are set to the same value
8680					opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];
8681
8682					// Set display property to inline-block for height/width
8683					// animations on inline elements that are having width/height
8684					// animated
8685					if ( jQuery.css( this, "display" ) === "inline" &&
8686							jQuery.css( this, "float" ) === "none" ) {
8687						if ( !jQuery.support.inlineBlockNeedsLayout ) {
8688							this.style.display = "inline-block";
8689
8690						} else {
8691							var display = defaultDisplay(this.nodeName);
8692
8693							// inline-level elements accept inline-block;
8694							// block-level elements need to be inline with layout
8695							if ( display === "inline" ) {
8696								this.style.display = "inline-block";
8697
8698							} else {
8699								this.style.display = "inline";
8700								this.style.zoom = 1;
8701							}
8702						}
8703					}
8704				}
8705
8706				if ( jQuery.isArray( prop[p] ) ) {
8707					// Create (if needed) and add to specialEasing
8708					(opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
8709					prop[p] = prop[p][0];
8710				}
8711			}
8712
8713			if ( opt.overflow != null ) {
8714				this.style.overflow = "hidden";
8715			}
8716
8717			opt.curAnim = jQuery.extend({}, prop);
8718
8719			jQuery.each( prop, function( name, val ) {
8720				var e = new jQuery.fx( self, opt, name );
8721
8722				if ( rfxtypes.test(val) ) {
8723					e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
8724
8725				} else {
8726					var parts = rfxnum.exec(val),
8727						start = e.cur();
8728
8729					if ( parts ) {
8730						var end = parseFloat( parts[2] ),
8731							unit = parts[3] || ( jQuery.cssNumber[ name ] ? "" : "px" );
8732
8733						// We need to compute starting value
8734						if ( unit !== "px" ) {
8735							jQuery.style( self, name, (end || 1) + unit);
8736							start = ((end || 1) / e.cur()) * start;
8737							jQuery.style( self, name, start + unit);
8738						}
8739
8740						// If a +=/-= token was provided, we're doing a relative animation
8741						if ( parts[1] ) {
8742							end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
8743						}
8744
8745						e.custom( start, end, unit );
8746
8747					} else {
8748						e.custom( start, val, "" );
8749					}
8750				}
8751			});
8752
8753			// For JS strict compliance
8754			return true;
8755		});
8756	},
8757
8758	stop: function( clearQueue, gotoEnd ) {
8759		var timers = jQuery.timers;
8760
8761		if ( clearQueue ) {
8762			this.queue([]);
8763		}
8764
8765		this.each(function() {
8766			// go in reverse order so anything added to the queue during the loop is ignored
8767			for ( var i = timers.length - 1; i >= 0; i-- ) {
8768				if ( timers[i].elem === this ) {
8769					if (gotoEnd) {
8770						// force the next step to be the last
8771						timers[i](true);
8772					}
8773
8774					timers.splice(i, 1);
8775				}
8776			}
8777		});
8778
8779		// start the next in the queue if the last step wasn't forced
8780		if ( !gotoEnd ) {
8781			this.dequeue();
8782		}
8783
8784		return this;
8785	}
8786
8787});
8788
8789function genFx( type, num ) {
8790	var obj = {};
8791
8792	jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
8793		obj[ this ] = type;
8794	});
8795
8796	return obj;
8797}
8798
8799// Generate shortcuts for custom animations
8800jQuery.each({
8801	slideDown: genFx("show", 1),
8802	slideUp: genFx("hide", 1),
8803	slideToggle: genFx("toggle", 1),
8804	fadeIn: { opacity: "show" },
8805	fadeOut: { opacity: "hide" },
8806	fadeToggle: { opacity: "toggle" }
8807}, function( name, props ) {
8808	jQuery.fn[ name ] = function( speed, easing, callback ) {
8809		return this.animate( props, speed, easing, callback );
8810	};
8811});
8812
8813jQuery.extend({
8814	speed: function( speed, easing, fn ) {
8815		var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : {
8816			complete: fn || !fn && easing ||
8817				jQuery.isFunction( speed ) && speed,
8818			duration: speed,
8819			easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
8820		};
8821
8822		opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
8823			opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default;
8824
8825		// Queueing
8826		opt.old = opt.complete;
8827		opt.complete = function() {
8828			if ( opt.queue !== false ) {
8829				jQuery(this).dequeue();
8830			}
8831			if ( jQuery.isFunction( opt.old ) ) {
8832				opt.old.call( this );
8833			}
8834		};
8835
8836		return opt;
8837	},
8838
8839	easing: {
8840		linear: function( p, n, firstNum, diff ) {
8841			return firstNum + diff * p;
8842		},
8843		swing: function( p, n, firstNum, diff ) {
8844			return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
8845		}
8846	},
8847
8848	timers: [],
8849
8850	fx: function( elem, options, prop ) {
8851		this.options = options;
8852		this.elem = elem;
8853		this.prop = prop;
8854
8855		if ( !options.orig ) {
8856			options.orig = {};
8857		}
8858	}
8859
8860});
8861
8862jQuery.fx.prototype = {
8863	// Simple function for setting a style value
8864	update: function() {
8865		if ( this.options.step ) {
8866			this.options.step.call( this.elem, this.now, this );
8867		}
8868
8869		(jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
8870	},
8871
8872	// Get the current size
8873	cur: function() {
8874		if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
8875			return this.elem[ this.prop ];
8876		}
8877
8878		var parsed,
8879			r = jQuery.css( this.elem, this.prop );
8880		// Empty strings, null, undefined and "auto" are converted to 0,
8881		// complex values such as "rotate(1rad)" are returned as is,
8882		// simple values such as "10px" are parsed to Float.
8883		return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
8884	},
8885
8886	// Start an animation from one number to another
8887	custom: function( from, to, unit ) {
8888		var self = this,
8889			fx = jQuery.fx;
8890
8891		this.startTime = jQuery.now();
8892		this.start = from;
8893		this.end = to;
8894		this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" );
8895		this.now = this.start;
8896		this.pos = this.state = 0;
8897
8898		function t( gotoEnd ) {
8899			return self.step(gotoEnd);
8900		}
8901
8902		t.elem = this.elem;
8903
8904		if ( t() && jQuery.timers.push(t) && !timerId ) {
8905			timerId = setInterval(fx.tick, fx.interval);
8906		}
8907	},
8908
8909	// Simple 'show' function
8910	show: function() {
8911		// Remember where we started, so that we can go back to it later
8912		this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
8913		this.options.show = true;
8914
8915		// Begin the animation
8916		// Make sure that we start at a small width/height to avoid any
8917		// flash of content
8918		this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
8919
8920		// Start by showing the element
8921		jQuery( this.elem ).show();
8922	},
8923
8924	// Simple 'hide' function
8925	hide: function() {
8926		// Remember where we started, so that we can go back to it later
8927		this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
8928		this.options.hide = true;
8929
8930		// Begin the animation
8931		this.custom(this.cur(), 0);
8932	},
8933
8934	// Each step of an animation
8935	step: function( gotoEnd ) {
8936		var t = jQuery.now(), done = true;
8937
8938		if ( gotoEnd || t >= this.options.duration + this.startTime ) {
8939			this.now = this.end;
8940			this.pos = this.state = 1;
8941			this.update();
8942
8943			this.options.curAnim[ this.prop ] = true;
8944
8945			for ( var i in this.options.curAnim ) {
8946				if ( this.options.curAnim[i] !== true ) {
8947					done = false;
8948				}
8949			}
8950
8951			if ( done ) {
8952				// Reset the overflow
8953				if ( this.options.overflow != null && !jQuery.support.shrinkWrapBlocks ) {
8954					var elem = this.elem,
8955						options = this.options;
8956
8957					jQuery.each( [ "", "X", "Y" ], function (index, value) {
8958						elem.style[ "overflow" + value ] = options.overflow[index];
8959					} );
8960				}
8961
8962				// Hide the element if the "hide" operation was done
8963				if ( this.options.hide ) {
8964					jQuery(this.elem).hide();
8965				}
8966
8967				// Reset the properties, if the item has been hidden or shown
8968				if ( this.options.hide || this.options.show ) {
8969					for ( var p in this.options.curAnim ) {
8970						jQuery.style( this.elem, p, this.options.orig[p] );
8971					}
8972				}
8973
8974				// Execute the complete function
8975				this.options.complete.call( this.elem );
8976			}
8977
8978			return false;
8979
8980		} else {
8981			var n = t - this.startTime;
8982			this.state = n / this.options.duration;
8983
8984			// Perform the easing function, defaults to swing
8985			var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop];
8986			var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear");
8987			this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration);
8988			this.now = this.start + ((this.end - this.start) * this.pos);
8989
8990			// Perform the next step of the animation
8991			this.update();
8992		}
8993
8994		return true;
8995	}
8996};
8997
8998jQuery.extend( jQuery.fx, {
8999	tick: function() {
9000		var timers = jQuery.timers;
9001
9002		for ( var i = 0; i < timers.length; i++ ) {
9003			if ( !timers[i]() ) {
9004				timers.splice(i--, 1);
9005			}
9006		}
9007
9008		if ( !timers.length ) {
9009			jQuery.fx.stop();
9010		}
9011	},
9012
9013	interval: 13,
9014
9015	stop: function() {
9016		clearInterval( timerId );
9017		timerId = null;
9018	},
9019
9020	speeds: {
9021		slow: 600,
9022		fast: 200,
9023		// Default speed
9024		_default: 400
9025	},
9026
9027	step: {
9028		opacity: function( fx ) {
9029			jQuery.style( fx.elem, "opacity", fx.now );
9030		},
9031
9032		_default: function( fx ) {
9033			if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
9034				fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
9035			} else {
9036				fx.elem[ fx.prop ] = fx.now;
9037			}
9038		}
9039	}
9040});
9041
9042if ( jQuery.expr && jQuery.expr.filters ) {
9043	jQuery.expr.filters.animated = function( elem ) {
9044		return jQuery.grep(jQuery.timers, function( fn ) {
9045			return elem === fn.elem;
9046		}).length;
9047	};
9048}
9049
9050function defaultDisplay( nodeName ) {
9051	if ( !elemdisplay[ nodeName ] ) {
9052		var elem = jQuery("<" + nodeName + ">").appendTo("body"),
9053			display = elem.css("display");
9054
9055		elem.remove();
9056
9057		if ( display === "none" || display === "" ) {
9058			display = "block";
9059		}
9060
9061		elemdisplay[ nodeName ] = display;
9062	}
9063
9064	return elemdisplay[ nodeName ];
9065}
9066
9067
9068
9069
9070var rtable = /^t(?:able|d|h)$/i,
9071	rroot = /^(?:body|html)$/i;
9072
9073if ( "getBoundingClientRect" in document.documentElement ) {
9074	jQuery.fn.offset = function( options ) {
9075		var elem = this[0], box;
9076
9077		if ( options ) {
9078			return this.each(function( i ) {
9079				jQuery.offset.setOffset( this, options, i );
9080			});
9081		}
9082
9083		if ( !elem || !elem.ownerDocument ) {
9084			return null;
9085		}
9086
9087		if ( elem === elem.ownerDocument.body ) {
9088			return jQuery.offset.bodyOffset( elem );
9089		}
9090
9091		try {
9092			box = elem.getBoundingClientRect();
9093		} catch(e) {}
9094
9095		var doc = elem.ownerDocument,
9096			docElem = doc.documentElement;
9097
9098		// Make sure we're not dealing with a disconnected DOM node
9099		if ( !box || !jQuery.contains( docElem, elem ) ) {
9100			return box ? { top: box.top, left: box.left } : { top: 0, left: 0 };
9101		}
9102
9103		var body = doc.body,
9104			win = getWindow(doc),
9105			clientTop  = docElem.clientTop  || body.clientTop  || 0,
9106			clientLeft = docElem.clientLeft || body.clientLeft || 0,
9107			scrollTop  = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop,
9108			scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,
9109			top  = box.top  + scrollTop  - clientTop,
9110			left = box.left + scrollLeft - clientLeft;
9111
9112		return { top: top, left: left };
9113	};
9114
9115} else {
9116	jQuery.fn.offset = function( options ) {
9117		var elem = this[0];
9118
9119		if ( options ) {
9120			return this.each(function( i ) {
9121				jQuery.offset.setOffset( this, options, i );
9122			});
9123		}
9124
9125		if ( !elem || !elem.ownerDocument ) {
9126			return null;
9127		}
9128
9129		if ( elem === elem.ownerDocument.body ) {
9130			return jQuery.offset.bodyOffset( elem );
9131		}
9132
9133		jQuery.offset.initialize();
9134
9135		var computedStyle,
9136			offsetParent = elem.offsetParent,
9137			prevOffsetParent = elem,
9138			doc = elem.ownerDocument,
9139			docElem = doc.documentElement,
9140			body = doc.body,
9141			defaultView = doc.defaultView,
9142			prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
9143			top = elem.offsetTop,
9144			left = elem.offsetLeft;
9145
9146		while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
9147			if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
9148				break;
9149			}
9150
9151			computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
9152			top  -= elem.scrollTop;
9153			left -= elem.scrollLeft;
9154
9155			if ( elem === offsetParent ) {
9156				top  += elem.offsetTop;
9157				left += elem.offsetLeft;
9158
9159				if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) {
9160					top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
9161					left += parseFloat( computedStyle.borderLeftWidth ) || 0;
9162				}
9163
9164				prevOffsetParent = offsetParent;
9165				offsetParent = elem.offsetParent;
9166			}
9167
9168			if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
9169				top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
9170				left += parseFloat( computedStyle.borderLeftWidth ) || 0;
9171			}
9172
9173			prevComputedStyle = computedStyle;
9174		}
9175
9176		if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
9177			top  += body.offsetTop;
9178			left += body.offsetLeft;
9179		}
9180
9181		if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
9182			top  += Math.max( docElem.scrollTop, body.scrollTop );
9183			left += Math.max( docElem.scrollLeft, body.scrollLeft );
9184		}
9185
9186		return { top: top, left: left };
9187	};
9188}
9189
9190jQuery.offset = {
9191	initialize: function() {
9192		var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.css(body, "marginTop") ) || 0,
9193			html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
9194
9195		jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
9196
9197		container.innerHTML = html;
9198		body.insertBefore( container, body.firstChild );
9199		innerDiv = container.firstChild;
9200		checkDiv = innerDiv.firstChild;
9201		td = innerDiv.nextSibling.firstChild.firstChild;
9202
9203		this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
9204		this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
9205
9206		checkDiv.style.position = "fixed";
9207		checkDiv.style.top = "20px";
9208
9209		// safari subtracts parent border width here which is 5px
9210		this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
9211		checkDiv.style.position = checkDiv.style.top = "";
9212
9213		innerDiv.style.overflow = "hidden";
9214		innerDiv.style.position = "relative";
9215
9216		this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
9217
9218		this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
9219
9220		body.removeChild( container );
9221		jQuery.offset.initialize = jQuery.noop;
9222	},
9223
9224	bodyOffset: function( body ) {
9225		var top = body.offsetTop,
9226			left = body.offsetLeft;
9227
9228		jQuery.offset.initialize();
9229
9230		if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
9231			top  += parseFloat( jQuery.css(body, "marginTop") ) || 0;
9232			left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
9233		}
9234
9235		return { top: top, left: left };
9236	},
9237
9238	setOffset: function( elem, options, i ) {
9239		var position = jQuery.css( elem, "position" );
9240
9241		// set position first, in-case top/left are set even on static elem
9242		if ( position === "static" ) {
9243			elem.style.position = "relative";
9244		}
9245
9246		var curElem = jQuery( elem ),
9247			curOffset = curElem.offset(),
9248			curCSSTop = jQuery.css( elem, "top" ),
9249			curCSSLeft = jQuery.css( elem, "left" ),
9250			calculatePosition = (position === "absolute" || position === "fixed") && jQuery.inArray('auto', [curCSSTop, curCSSLeft]) > -1,
9251			props = {}, curPosition = {}, curTop, curLeft;
9252
9253		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
9254		if ( calculatePosition ) {
9255			curPosition = curElem.position();
9256		}
9257
9258		curTop  = calculatePosition ? curPosition.top  : parseInt( curCSSTop,  10 ) || 0;
9259		curLeft = calculatePosition ? curPosition.left : parseInt( curCSSLeft, 10 ) || 0;
9260
9261		if ( jQuery.isFunction( options ) ) {
9262			options = options.call( elem, i, curOffset );
9263		}
9264
9265		if (options.top != null) {
9266			props.top = (options.top - curOffset.top) + curTop;
9267		}
9268		if (options.left != null) {
9269			props.left = (options.left - curOffset.left) + curLeft;
9270		}
9271
9272		if ( "using" in options ) {
9273			options.using.call( elem, props );
9274		} else {
9275			curElem.css( props );
9276		}
9277	}
9278};
9279
9280
9281jQuery.fn.extend({
9282	position: function() {
9283		if ( !this[0] ) {
9284			return null;
9285		}
9286
9287		var elem = this[0],
9288
9289		// Get *real* offsetParent
9290		offsetParent = this.offsetParent(),
9291
9292		// Get correct offsets
9293		offset       = this.offset(),
9294		parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
9295
9296		// Subtract element margins
9297		// note: when an element has margin: auto the offsetLeft and marginLeft
9298		// are the same in Safari causing offset.left to incorrectly be 0
9299		offset.top  -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
9300		offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
9301
9302		// Add offsetParent borders
9303		parentOffset.top  += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
9304		parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
9305
9306		// Subtract the two offsets
9307		return {
9308			top:  offset.top  - parentOffset.top,
9309			left: offset.left - parentOffset.left
9310		};
9311	},
9312
9313	offsetParent: function() {
9314		return this.map(function() {
9315			var offsetParent = this.offsetParent || document.body;
9316			while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
9317				offsetParent = offsetParent.offsetParent;
9318			}
9319			return offsetParent;
9320		});
9321	}
9322});
9323
9324
9325// Create scrollLeft and scrollTop methods
9326jQuery.each( ["Left", "Top"], function( i, name ) {
9327	var method = "scroll" + name;
9328
9329	jQuery.fn[ method ] = function(val) {
9330		var elem = this[0], win;
9331
9332		if ( !elem ) {
9333			return null;
9334		}
9335
9336		if ( val !== undefined ) {
9337			// Set the scroll offset
9338			return this.each(function() {
9339				win = getWindow( this );
9340
9341				if ( win ) {
9342					win.scrollTo(
9343						!i ? val : jQuery(win).scrollLeft(),
9344						i ? val : jQuery(win).scrollTop()
9345					);
9346
9347				} else {
9348					this[ method ] = val;
9349				}
9350			});
9351		} else {
9352			win = getWindow( elem );
9353
9354			// Return the scroll offset
9355			return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
9356				jQuery.support.boxModel && win.document.documentElement[ method ] ||
9357					win.document.body[ method ] :
9358				elem[ method ];
9359		}
9360	};
9361});
9362
9363function getWindow( elem ) {
9364	return jQuery.isWindow( elem ) ?
9365		elem :
9366		elem.nodeType === 9 ?
9367			elem.defaultView || elem.parentWindow :
9368			false;
9369}
9370
9371
9372
9373
9374// Create innerHeight, innerWidth, outerHeight and outerWidth methods
9375jQuery.each([ "Height", "Width" ], function( i, name ) {
9376
9377	var type = name.toLowerCase();
9378
9379	// innerHeight and innerWidth
9380	jQuery.fn["inner" + name] = function() {
9381		return this[0] ?
9382			parseFloat( jQuery.css( this[0], type, "padding" ) ) :
9383			null;
9384	};
9385
9386	// outerHeight and outerWidth
9387	jQuery.fn["outer" + name] = function( margin ) {
9388		return this[0] ?
9389			parseFloat( jQuery.css( this[0], type, margin ? "margin" : "border" ) ) :
9390			null;
9391	};
9392
9393	jQuery.fn[ type ] = function( size ) {
9394		// Get window width or height
9395		var elem = this[0];
9396		if ( !elem ) {
9397			return size == null ? null : this;
9398		}
9399
9400		if ( jQuery.isFunction( size ) ) {
9401			return this.each(function( i ) {
9402				var self = jQuery( this );
9403				self[ type ]( size.call( this, i, self[ type ]() ) );
9404			});
9405		}
9406
9407		if ( jQuery.isWindow( elem ) ) {
9408			// Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
9409			// 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat
9410			var docElemProp = elem.document.documentElement[ "client" + name ];
9411			return elem.document.compatMode === "CSS1Compat" && docElemProp ||
9412				elem.document.body[ "client" + name ] || docElemProp;
9413
9414		// Get document width or height
9415		} else if ( elem.nodeType === 9 ) {
9416			// Either scroll[Width/Height] or offset[Width/Height], whichever is greater
9417			return Math.max(
9418				elem.documentElement["client" + name],
9419				elem.body["scroll" + name], elem.documentElement["scroll" + name],
9420				elem.body["offset" + name], elem.documentElement["offset" + name]
9421			);
9422
9423		// Get or set width or height on the element
9424		} else if ( size === undefined ) {
9425			var orig = jQuery.css( elem, type ),
9426				ret = parseFloat( orig );
9427
9428			return jQuery.isNaN( ret ) ? orig : ret;
9429
9430		// Set the width or height on the element (default to pixels if value is unitless)
9431		} else {
9432			return this.css( type, typeof size === "string" ? size : size + "px" );
9433		}
9434	};
9435
9436});
9437
9438
9439window.jQuery = window.$ = jQuery;
9440})(window);
9441/*!
9442 * jQuery-ajaxTransport-XDomainRequest - v1.0.4 - 2015-03-05
9443 * https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest
9444 * Copyright (c) 2015 Jason Moon (@JSONMOON)
9445 * Licensed MIT (/blob/master/LICENSE.txt)
9446 */
9447(function(factory) {
9448  if (typeof define === 'function' && define.amd) {
9449    // AMD. Register as anonymous module.
9450    define(['jquery'], factory);
9451  } else if (typeof exports === 'object') {
9452    // CommonJS
9453    module.exports = factory(require('jquery'));
9454  } else {
9455    // Browser globals.
9456    factory(jQuery);
9457  }
9458}(function($) {
9459
9460// Only continue if we're on IE8/IE9 with jQuery 1.5+ (contains the ajaxTransport function)
9461if ($.support.cors || !$.ajaxTransport || !window.XDomainRequest) {
9462  return $;
9463}
9464
9465var httpRegEx = /^(https?:)?\/\//i;
9466var getOrPostRegEx = /^get|post$/i;
9467var sameSchemeRegEx = new RegExp('^(\/\/|' + location.protocol + ')', 'i');
9468
9469// ajaxTransport exists in jQuery 1.5+
9470$.ajaxTransport('* text html xml json', function(options, userOptions, jqXHR) {
9471
9472  // Only continue if the request is: asynchronous, uses GET or POST method, has HTTP or HTTPS protocol, and has the same scheme as the calling page
9473  if (!options.crossDomain || !options.async || !getOrPostRegEx.test(options.type) || !httpRegEx.test(options.url) || !sameSchemeRegEx.test(options.url)) {
9474    return;
9475  }
9476
9477  var xdr = null;
9478
9479  return {
9480    send: function(headers, complete) {
9481      var postData = '';
9482      var userType = (userOptions.dataType || '').toLowerCase();
9483
9484      xdr = new XDomainRequest();
9485      if (/^\d+$/.test(userOptions.timeout)) {
9486        xdr.timeout = userOptions.timeout;
9487      }
9488
9489      xdr.ontimeout = function() {
9490        complete(500, 'timeout');
9491      };
9492
9493      xdr.onload = function() {
9494        var allResponseHeaders = 'Content-Length: ' + xdr.responseText.length + '\r\nContent-Type: ' + xdr.contentType;
9495        var status = {
9496          code: 200,
9497          message: 'success'
9498        };
9499        var responses = {
9500          text: xdr.responseText
9501        };
9502        try {
9503          if (userType === 'html' || /text\/html/i.test(xdr.contentType)) {
9504            responses.html = xdr.responseText;
9505          } else if (userType === 'json' || (userType !== 'text' && /\/json/i.test(xdr.contentType))) {
9506            try {
9507              responses.json = $.parseJSON(xdr.responseText);
9508            } catch(e) {
9509              status.code = 500;
9510              status.message = 'parseerror';
9511              //throw 'Invalid JSON: ' + xdr.responseText;
9512            }
9513          } else if (userType === 'xml' || (userType !== 'text' && /\/xml/i.test(xdr.contentType))) {
9514            var doc = new ActiveXObject('Microsoft.XMLDOM');
9515            doc.async = false;
9516            try {
9517              doc.loadXML(xdr.responseText);
9518            } catch(e) {
9519              doc = undefined;
9520            }
9521            if (!doc || !doc.documentElement || doc.getElementsByTagName('parsererror').length) {
9522              status.code = 500;
9523              status.message = 'parseerror';
9524              throw 'Invalid XML: ' + xdr.responseText;
9525            }
9526            responses.xml = doc;
9527          }
9528        } catch(parseMessage) {
9529          throw parseMessage;
9530        } finally {
9531          complete(status.code, status.message, responses, allResponseHeaders);
9532        }
9533      };
9534
9535      // set an empty handler for 'onprogress' so requests don't get aborted
9536      xdr.onprogress = function(){};
9537      xdr.onerror = function() {
9538        complete(500, 'error', {
9539          text: xdr.responseText
9540        });
9541      };
9542
9543      if (userOptions.data) {
9544        postData = ($.type(userOptions.data) === 'string') ? userOptions.data : $.param(userOptions.data);
9545      }
9546      xdr.open(options.type, options.url);
9547      xdr.send(postData);
9548    },
9549    abort: function() {
9550      if (xdr) {
9551        xdr.abort();
9552      }
9553    }
9554  };
9555});
9556
9557return $;
9558
9559}));
9560/*!
9561 * jQuery Cookie Plugin v1.3.1
9562 * https://github.com/carhartl/jquery-cookie
9563 *
9564 * Copyright 2013 Klaus Hartl
9565 * Released under the MIT license
9566 */
9567(function (factory) {
9568	if (typeof define === 'function' && define.amd) {
9569		// AMD. Register as anonymous module.
9570		define(['jquery'], factory);
9571	} else {
9572		// Browser globals.
9573		factory(jQuery);
9574	}
9575}(function ($) {
9576
9577	var pluses = /\+/g;
9578
9579	function raw(s) {
9580		return s;
9581	}
9582
9583	function decoded(s) {
9584		return decodeURIComponent(s.replace(pluses, ' '));
9585	}
9586
9587	function converted(s) {
9588		if (s.indexOf('"') === 0) {
9589			// This is a quoted cookie as according to RFC2068, unescape
9590			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
9591		}
9592		try {
9593			return config.json ? JSON.parse(s) : s;
9594		} catch(er) {}
9595	}
9596
9597	var config = $.cookie = function (key, value, options) {
9598
9599		// write
9600		if (value !== undefined) {
9601			options = $.extend({}, config.defaults, options);
9602
9603			if (typeof options.expires === 'number') {
9604				var days = options.expires, t = options.expires = new Date();
9605				t.setDate(t.getDate() + days);
9606			}
9607
9608			value = config.json ? JSON.stringify(value) : String(value);
9609
9610			return (document.cookie = [
9611				config.raw ? key : encodeURIComponent(key),
9612				'=',
9613				config.raw ? value : encodeURIComponent(value),
9614				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
9615				'; path=/',
9616				options.domain  ? '; domain=' + (options.domain) : '',
9617				options.secure  ? '; secure' : ''
9618			].join(''));
9619		}
9620
9621		// read
9622		var decode = config.raw ? raw : decoded;
9623		var cookies = document.cookie.split('; ');
9624		var result = key ? undefined : {};
9625		for (var i = 0, l = cookies.length; i < l; i++) {
9626			var parts = cookies[i].split('=');
9627			var name = decode(parts.shift());
9628			var cookie = decode(parts.join('='));
9629
9630			if (key && key === name) {
9631				result = converted(cookie);
9632				break;
9633			}
9634
9635			if (!key) {
9636				result[name] = converted(cookie);
9637			}
9638		}
9639
9640		return result;
9641	};
9642
9643	config.defaults = {};
9644
9645	$.removeCookie = function (key, options) {
9646		if ($.cookie(key) !== undefined) {
9647			// Must not alter options, thus extending a fresh object...
9648			$.cookie(key, '', $.extend({}, options, { expires: -1 }));
9649			return true;
9650		}
9651		return false;
9652	};
9653
9654}));
9655/*! Copyright (c) 2009 Brandon Aaron (http://brandonaaron.net)
9656 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
9657 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
9658 * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
9659 * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
9660 *
9661 * Version: 3.0.2
9662 * 
9663 * Requires: 1.2.2+
9664 */
9665
9666(function($) {
9667
9668var types = ['DOMMouseScroll', 'mousewheel'];
9669
9670$.event.special.mousewheel = {
9671	setup: function() {
9672		if ( this.addEventListener )
9673			for ( var i=types.length; i; )
9674				this.addEventListener( types[--i], handler, false );
9675		else
9676			this.onmousewheel = handler;
9677	},
9678	
9679	teardown: function() {
9680		if ( this.removeEventListener )
9681			for ( var i=types.length; i; )
9682				this.removeEventListener( types[--i], handler, false );
9683		else
9684			this.onmousewheel = null;
9685	}
9686};
9687
9688$.fn.extend({
9689	mousewheel: function(fn) {
9690		return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
9691	},
9692	
9693	unmousewheel: function(fn) {
9694		return this.unbind("mousewheel", fn);
9695	}
9696});
9697
9698
9699function handler(event) {
9700	var args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true;
9701	
9702	event = $.event.fix(event || window.event);
9703	event.type = "mousewheel";
9704	
9705	if ( event.wheelDelta ) delta = event.wheelDelta/120;
9706	if ( event.detail     ) delta = -event.detail/3;
9707	
9708	// Add events and delta to the front of the arguments
9709	args.unshift(event, delta);
9710
9711	return $.event.handle.apply(this, args);
9712}
9713
9714})(jQuery);
9715/*
9716    json2.js
9717    2008-03-14
9718
9719    Public Domain
9720
9721    No warranty expressed or implied. Use at your own risk.
9722
9723    See http://www.JSON.org/js.html
9724
9725    This is a reference implementation. You are free to copy, modify, or
9726    redistribute.
9727
9728    Use your own copy. It is extremely unwise to load third party
9729    code into your pages.
9730*/
9731
9732if (!this.JSON) {
9733
9734    this.JSON = function() {
9735
9736        function f(n) {    // Format integers to have at least two digits.
9737            return n < 10 ? '0' + n : n;
9738        }
9739
9740        Date.prototype.toJSON = function() {
9741            return this.getUTCFullYear()   + '-' +
9742                 f(this.getUTCMonth() + 1) + '-' +
9743                 f(this.getUTCDate())      + 'T' +
9744                 f(this.getUTCHours())     + ':' +
9745                 f(this.getUTCMinutes())   + ':' +
9746                 f(this.getUTCSeconds())   + 'Z';
9747        };
9748
9749
9750        var m = {    // table of character substitutions
9751            '\b': '\\b',
9752            '\t': '\\t',
9753            '\n': '\\n',
9754            '\f': '\\f',
9755            '\r': '\\r',
9756            '"' : '\\"',
9757            '\\': '\\\\'
9758        };
9759
9760        function stringify(value, whitelist) {
9761            var a,          // The array holding the partial texts.
9762                i,          // The loop counter.
9763                k,          // The member key.
9764                l,          // Length.
9765                r = /["\\\x00-\x1f\x7f-\x9f]/g,
9766                v;          // The member value.
9767
9768            switch (typeof value) {
9769            case 'string':
9770
9771                return r.test(value) ?
9772                    '"' + value.replace(r, function (a) {
9773                        var c = m[a];
9774                        if (c) {
9775                            return c;
9776                        }
9777                        c = a.charCodeAt();
9778                        return '\\u00' + Math.floor(c / 16).toString(16) +
9779                                                   (c % 16).toString(16);
9780                    }) + '"' :
9781                    '"' + value + '"';
9782
9783            case 'number':
9784
9785                return isFinite(value) ? String(value) : 'null';
9786
9787            case 'boolean':
9788            case 'null':
9789                return String(value);
9790
9791            case 'object':
9792
9793                if (!value) {
9794                    return 'null';
9795                }
9796
9797                if (typeof value.toJSON === 'function') {
9798                    return stringify(value.toJSON());
9799                }
9800                a = [];
9801                if (typeof value.length === 'number' &&
9802                        !(value.propertyIsEnumerable('length'))) {
9803
9804                    l = value.length;
9805                    for (i = 0; i < l; i += 1) {
9806                        a.push(stringify(value[i], whitelist) || 'null');
9807                    }
9808
9809                    return '[' + a.join(',') + ']';
9810                }
9811                if (whitelist) {
9812                    l = whitelist.length;
9813                    for (i = 0; i < l; i += 1) {
9814                        k = whitelist[i];
9815                        if (typeof k === 'string') {
9816                            v = stringify(value[k], whitelist);
9817                            if (v) {
9818                                a.push(stringify(k) + ':' + v);
9819                            }
9820                        }
9821                    }
9822                } else {
9823
9824                    for (k in value) {
9825                        if (typeof k === 'string') {
9826                            v = stringify(value[k], whitelist);
9827                            if (v) {
9828                                a.push(stringify(k) + ':' + v);
9829                            }
9830                        }
9831                    }
9832                }
9833
9834                return '{' + a.join(',') + '}';
9835            }
9836        }
9837
9838        return {
9839            stringify: stringify,
9840            parse: function (text, filter) {
9841                var j;
9842
9843                function walk(k, v) {
9844                    var i, n;
9845                    if (v && typeof v === 'object') {
9846                        for (i in v) {
9847                            if (Object.prototype.hasOwnProperty.apply(v, [i])) {
9848                                n = walk(i, v[i]);
9849                                if (n !== undefined) {
9850                                    v[i] = n;
9851                                } else {
9852                                    delete v[i];
9853                                }
9854                            }
9855                        }
9856                    }
9857                    return filter(k, v);
9858                }
9859
9860                if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').
9861replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
9862replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
9863
9864                    j = eval('(' + text + ')');
9865
9866                    return typeof filter === 'function' ? walk('', j) : j;
9867                }
9868
9869                throw new SyntaxError('parseJSON');
9870            }
9871        };
9872    }();
9873};
9874/**
9875 * @fileOverview Migration workarounds to keep sitewide inline JS working with
9876 * bothm prototype and jQuery.
9877 * Mostly the functions ported from prototype or mapping of old function names to 
9878 * jQuery analogs.
9879 */
9880
9881jQuery.ajaxSetup({'traditional': true});
9882
9883addLoadEvent = $;
9884myDeco.readCookie = $.cookie;
9885myDeco.createCookie = function(name, value, days, path, domain) {
9886	return $.cookie(name, value, {expires: days, path: path, domain: domain});
9887};
9888
9889myDeco.eraseCookie = function(name, path, domain) {
9890	this.createCookie(name, "", -1, path, domain);
9891};
9892
9893// Some methods from prototype library implemented for jQ.
9894$A = $.makeArray;
9895
9896jQuery.extend({
9897	keys: function(object) {
9898		var k = [];
9899		for (var p in object)
9900			k.push(p);
9901		return k;
9902	},
9903	
9904	values: function(object) {
9905		var k = [];
9906		for (var p in object)
9907			k.push(object[p]);
9908		return k;
9909	},
9910	first: function(list, callback) {
9911		var ret, i,
9912			isFunction = typeof callback === 'function';
9913		
9914		for (i = list.length - 1; i >= 0; i--) {
9915			if (isFunction && callback(list[i], i) == true ||
9916					callback === list[i]) {
9917				ret = list[i];
9918				break;
9919			}
9920		}
9921		
9922		return ret;
9923	},
9924	any: function(list, callback) {
9925		return (typeof this.first(list, callback) !== 'undefined');
9926	},
9927	firstElementKey: function(object) {
9928		for (var first in object) break;
9929		return first;
9930	},
9931	/**
9932	 * Extract specified element from list
9933	 * @param {Any} element Element to extract
9934	 * @param {Array} list List extract from
9935	 * @returns Extracted object or undefined if not found
9936	 */
9937	extract: function(element, list) {
9938		var idx = $.inArray(element, list),
9939			ret;
9940		if (idx != -1)
9941			ret = list.splice(idx, 1).shift();
9942		
9943		return ret;
9944	},
9945	
9946	/**
9947	 * Returns common elements for both given arrays
9948	 * @param {Array} array1
9949	 * @param {Array} array2
9950	 * @returns {Array}
9951	 */
9952	intersect: function (array1, array2) {
9953		var i, result = [];
9954		
9955		for (i = array1.length - 1; i >= 0; i--) {
9956			if (array2.indexOf(array1[i]) !== -1) {
9957				result.push(array1[i]);
9958			}
9959		}
9960		
9961		return result;
9962	},
9963	
9964	/**
9965	 * Substract to lists
9966	 * @param {Array} list List to substract from
9967	 * @param {Array} toSubstract Items to substract
9968	 * @returns {Array} List without elements from toSubstract
9969	 */
9970	substract: function(list, toSubstract) {
9971		var ret = [],
9972			i, ilen = list.length;
9973		for (i = 0; i < ilen; i++)
9974			if ($.inArray(list[i], toSubstract) == -1)
9975				ret.push(list[i]);
9976		
9977		return ret;
9978	},
9979	
9980	/**
9981	 * Minify given HTML by removed spaces, tabs, CRLFs between tags and from beginning/ending
9982	 * @param {String} html HTML to minify
9983	 * @returns {String} Minified HTML
9984	 */
9985	minifyHtml: function(html) {
9986		return $.trim(html.replace(/>[\s\t\r\n]*</g, '><'));
9987	}
9988});
9989
9990Function.prototype.bindAsEventListener = function() {
9991	var __method = this, args = $.makeArray(arguments), object = args.shift();
9992	return function(event) {
9993		return __method.apply(object, [event || window.event].concat(args));
9994	}
9995};
9996
9997Function.prototype.bind = function() {
9998	if (arguments.length < 2 && typeof(arguments[0]) == "undefined")
9999		return this;
10000
10001	var __method = this, args = $.makeArray(arguments), object = args.shift();
10002	return function() {
10003		return __method.apply(object, args.concat($A(arguments)));
10004	}
10005};
10006
10007String.prototype.toQueryParams = function(separator, excludeEmpty) {
10008	var i, key, value, s, hash = {},
10009		str = $.trim(this),
10010		start = str.indexOf('?'),
10011		end = str.indexOf('#'),
10012		chunks = [];
10013	
10014	start = start === -1 ? 0 : start + 1;
10015	end = end === -1 ? str.length : end;
10016		
10017	if (end > start && (str = str.substring(start, end)).indexOf('=') !== -1) {
10018		chunks = str.replace(/\+/g, ' ').split(separator || '&');
10019		for (i = chunks.length - 1; i != -1; i--) {
10020			if (chunks[i].length === 0)
10021				continue;
10022			s = chunks[i].indexOf('=');
10023			key = decodeURIComponent(chunks[i].substring(0, s));
10024			value = decodeURIComponent(chunks[i].substring(s + 1));
10025			if (!excludeEmpty || excludeEmpty && value !== '') {
10026			if (key in hash) {
10027				if (typeof hash[key] === 'string')
10028					hash[key] = [hash[key]];
10029	
10030				hash[key].push(value);
10031			} else {
10032				hash[key] = value;
10033			}
10034			}
10035		}
10036	}
10037	
10038	return hash;
10039};
10040
10041// Simple and fast function for regexp based param extraction
10042String.prototype.getURIParam = function(param) {
10043	var uriRe = new RegExp('[\?&;]?(' + param + '\=)([^&]*)', 'g'),
10044		parse, 
10045		result = [];
10046	
10047	while ((parse = uriRe.exec(this)) != null)
10048		result.push(decodeURIComponent(parse[2].replace('+', ' ')));
10049
10050	return result;
10051};
10052
10053/**
10054 * Update params in URI
10055 * @param {Object} data Data to update
10056 * @param {Boolean} override Override current data in URI or not
10057 * @return {String} resulting URI
10058 */
10059String.prototype.updateURIParams = function(data, override) {
10060	// split given URI into parts [url, getParams, hash]
10061	var parts = this.split('#');
10062	parts[2] = parts[1] ? '#' + parts[1] : '';
10063	parts[0] = parts[0].split('?');
10064	parts[1] = parts[0][1] || '';
10065	parts[0] = parts[0][0];
10066	parts[1] = '?' + $.param(override ? 
10067		$.extend(parts[1].toQueryParams(), data) :
10068		$.extend($.extend({}, data), parts[1].toQueryParams())
10069	);
10070	return parts.join('');
10071};
10072
10073String.prototype.capitalize = function() {
10074	return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
10075};
10076
10077String.prototype.toHtml = function() {
10078	return this.replace(/&/g, '&amp;')
10079		.replace(/\'/g, '&apos;')
10080		.replace(/>/g, '&gt;')
10081		.replace(/</g, '&lt;')
10082		.replace(/"/g, '&quot;');
10083};
10084
10085String.prototype.fromHtml = function() {
10086	return this
10087		.replace(/&apos;/gi, "\'")
10088		.replace(/&gt;/gi, '>')
10089		.replace(/&lt;/gi, '<')
10090		.replace(/&quot;/g, '"')
10091		.replace(/&#(\d+);/g, function() {
10092			return decodeURIComponent('%' + parseInt(arguments[1], 10).toString(16));
10093		})
10094		.replace(/&amp;/g, '&');
10095};
10096
10097/**
10098 * Return parsed date from string
10099 */
10100String.prototype.toDate = function() {
10101	var match = this.match(/(\d{4})-(\d{1,2})-(\d{1,2})(\s(\d{1,2}):(\d{1,2}):(\d{1,2}))?/),
10102		date;
10103	if (match.length > 0)
10104		date = new Date(match[1], match[2] - 1, match[3],
10105				match[5] || 0, match[6] || 0, match[7] || 0);
10106	return date;
10107};
10108
10109/**
10110 * Returns human readable time diff with specified or current date
10111 * Strings should have "2010-01-12 22:12:44" notation
10112 * @param {Number} limit Maximum number of returned items like "3 month and 3 days ago" for limit = 2. Default is 1
10113 * @param {String} date Date compare to, default is now.
10114 */
10115String.prototype.toDateDiff = function(limit, date) {
10116	limit = limit || 1;
10117	date = date || new Date();
10118	
10119	var values = {
10120			year:	3600 * 24 * 365,
10121			month:	3600 * 24 * 30,
10122			day:	3600 * 24,
10123			hour:	3600,
10124			minute:	60,
10125			second:	1
10126		},
10127		str = 'unknown',
10128		from = this.toDate(),
10129		to = (date != undefined && date instanceof Date) ? date : (date + '').toDate();
10130
10131	if (from && to) {
10132		var sign = Math.round((to - from) / 1000),
10133			diff = Math.abs(sign), chunks = [], lvl = 0;
10134		
10135		for (var name in values) {
10136			var value = Math.floor(diff / values[name]);
10137			if (value > 0) {
10138				diff -= values[name] * value;
10139				chunks.push(value + ' ' + name + (value != 1 ? 's' : ''));
10140				lvl++;
10141			}
10142			if (lvl >= limit)
10143				break;
10144		}
10145		
10146		if (chunks.length != 0) {
10147			str = '';
10148			if (sign < 0)
10149				str += 'in ';
10150			
10151			for (var i = 0, ilen = chunks.length; i < ilen; i++)
10152				str += chunks[i] + (i < ilen - 2 ? ', ' : '') + (i == ilen - 2 ? ' and ' : '');
10153			
10154			if (sign > 0)
10155				str += ' ago';
10156		} else
10157			str = 'now'
10158	}
10159	return str;
10160};
10161
10162/**
10163 * Return string truncated with dots if current length more than specified
10164 * @param {Number} length Length truncate to, default is 30
10165 * @param {String} truncation Suffix added to string in case of truncate
10166 * @return {String}
10167 */
10168String.prototype.truncate = function(length, truncation) {
10169	length = length || 30;
10170	truncation = truncation || '...';
10171	return this.length > length ?
10172		this.slice(0, length - truncation.length) + truncation : String(this);
10173};
10174
10175/**
10176 * Normalizes string, converts to lowercase, removes non-alpha characters,
10177 * and converts spaces to hyphens.
10178 * @return {String} Slugified string
10179 */
10180String.prototype.slugify = function() {
10181	return this.toLowerCase().replace(/^\s+|[^\w\s-]|\s+$/g, '')
10182		.replace(/[\s-]+/g,'-');
10183}
10184
10185/**
10186 * Format date as string by specified format (PHP-like format syntax)
10187 * @param {String} format
10188 * @return {String}
10189 */
10190Date.prototype.format = function(format) {
10191	var month = 'January February March April May June July August September October November December'.split(' '),
10192		chunks = [];
10193	format = format || 'j F Y';
10194	
10195	// prepend values with zeroes to a specified length
10196	function zerofill(value, length) {
10197		// stringify value
10198		value = value + '';
10199		while (value.length < length)
10200			value = '0' + value;
10201		return value;
10202	}
10203	
10204	for (var i = 0, ilen = format.length; i < ilen; i++) {
10205		// IE doesn't want to return char as as format[i]
10206		var sym = format.substring(i, i + 1);
10207		switch (sym) {
10208		// day as 1..31
10209		case 'j':
10210		// day as 01..31
10211		case 'd':
10212			var day = this.getDate();
10213			if (sym == 'd')
10214				day = zerofill(day, 2);
10215			chunks.push(day);
10216			break;
10217		// month as 1..12
10218		case 'n':
10219		// month as 01..12
10220		case 'm':
10221			var month = this.getMonth() + 1;
10222			if (sym == 'm')
10223				month = zerofill(month, 2);
10224			chunks.push(month);
10225			break;
10226		// month as January..December
10227		case 'F':
10228			chunks.push(month[this.getMonth()]);
10229			break;
10230		// year as 2001
10231		case 'Y':
10232			chunks.push(this.getFullYear());
10233			break;
10234		default:
10235			chunks.push(sym);
10236		}
10237	}
10238	return chunks.join('');
10239}
10240
10241// Prototype-style shorthand for stopping event.
10242$.Event.prototype.stop = function() {
10243	this.stopPropagation();
10244	this.preventDefault();
10245};
10246
10247// Prototype-style shorthand for finding correct parent of event target.
10248$.Event.prototype.findElement = function(query) {
10249	return $(this.target).closest(query);
10250};
10251
10252// indexOf implementation for IE browsers.
10253if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
10254	i || (i = 0);
10255	var length = this.length;
10256	if (i < 0) i = length + i;
10257	for (; i < length; i++)
10258		if (this[i] === item) return i;
10259	return -1;
10260};
10261
10262// function with the similar behaviour as prototype's identify()
10263// return ID if is set, or generate, set and return new otherwise
10264// works for the fist element in set, as attr()
10265$.fn.identify = function() {
10266	var self = $(this),
10267		id = self.attr('id');
10268	if(id) return id;
10269	do { id = 'anonymous_element_' + jQuery.identify_counter++ } while ($(id).size() > 0);
10270	self.attr('id', id);
10271	return id;
10272}
10273
10274jQuery.identify_counter = 0;
10275
10276Array.prototype.reduce =	function() {
10277	return this.length > 1 ? this : this[0];
10278};
10279
10280// remove duplicated items from array and return new one
10281Array.prototype.unique = function() {
10282	var result = [], x, xlen = this.length;
10283	
10284	for (x = 0; x < xlen; x++)
10285		if (result.indexOf(this[x]) == -1)
10286			result.push(this[x]);
10287		
10288	return result;
10289};
10290
10291Array.prototype.get = function(index) {
10292	if (index < 0) {
10293		index = this.length + index;
10294	}
10295	return this[index];
10296};
10297
10298/**
10299 * jQuery monkey patch for events used for transparent proxying of mouse* to
10300 * touch* events in case of ipad devices
10301 */
10302$(function() {
10303	if (myDeco.browser.isMobileSafari) {
10304		var oldFix = jQuery.event.fix,
10305			oldAdd = jQuery.event.add,
10306			oldTrigger = jQuery.event.trigger,
10307			oldRemove = jQuery.event.remove,
10308			map = {
10309				'mousemove': 'touchmove',
10310				'mousedown': 'touchstart',
10311				'mouseup': 'touchend'
10312			},
10313			revMap = {
10314				'touchmove': 'mousemove',
10315				'touchstart': 'mousedown',
10316				'touchend': 'mouseup'
10317			};
10318		
10319		/**
10320		 * Event fix monkey patch will restore coordiante events in jQuery event
10321		 * instead of original event
10322		 */
10323		jQuery.event.fix = function(event) {
10324			var touches = event.touches,
10325				changedTouches = event.changedTouches,
10326				targetTouches = event.targetTouches,
10327				rotation = event.rotation,
10328				scale = event.scale;
10329			event = oldFix.call(jQuery.event, event);
10330			event.touches = touches;
10331			event.changedTouches = changedTouches;
10332			event.targetTouches = targetTouches;
10333			event.rotation = rotation;
10334			event.scale = scale;
10335			return event;
10336		};
10337		
10338		/**
10339		 * Event trigger monkey patch
10340		 * It will replace any mouse* events with corresponding touch* events
10341		 */
10342		jQuery.event.trigger = function(event, data, elem) {
10343			var parts = event.type ? [] : event.split('.'),
10344				type = event.type || parts.shift(),
10345				namespaces = event.namespace || parts.join('.');
10346				
10347			type = map[type] || type;
10348			
10349			if (typeof event === 'string') {
10350				oldTrigger.call(this, type + (namespaces ? '.' + namespaces : ''), data, elem);
10351			} else {
10352				event.type = type;
10353				oldTrigger.call(this, event, data, elem);
10354			}
10355			return this;
10356		};
10357		
10358		/**
10359		 * Event add monkeypatch function
10360		 * Event handler can be an object or callable
10361		 */
10362		jQuery.event.add = function(elem, types, handler, data) {
10363			var etypes = types.split(' '),
10364				callee = handler.handler || handler,
10365				type, normalEvents = [], touchEvents = [],
10366				i = 0, namespaces, namespace, live,
10367				// wrapped handler used for intercept touch events and replace
10368				// type, add coordinate events
10369				wrappedHandler = function(e) {
10370					var touches = (e.type === 'touchend')
10371									? e.changedTouches
10372									: e.touches,
10373						args = Array.prototype.slice.call(arguments);
10374					
10375					if (touches && touches.length === 1) {
10376						args[0] = $.extend({}, args[0], {
10377							type: revMap[e.type],
10378							clientX: touches[0].clientX,
10379							clientY: touches[0].clientY,
10380							pageX: touches[0].pageX,
10381							pageY: touches[0].pageY
10382						});
10383					}
10384					
10385					return callee ? callee.apply(this, args) : false;
10386				};
10387			
10388			// iterate all event types and replace needed with custom callback
10389			while ((type = etypes[i++])) {
10390				// Namespaced event handlers
10391				if (type.indexOf(".") !== -1) {
10392					namespaces = type.split(".");
10393					// live events starting from «live»
10394					live = namespaces[0] === 'live' ?
10395						namespaces.shift() + '.' : '';
10396						
10397					// event type
10398					type = namespaces.shift();
10399					
10400					// rest is namespace
10401					namespace = namespaces.length !== 0 ?
10402						'.' + namespaces.join('.') : '';
10403				
10404				// ordinal event, no live and namespaces
10405				} else {
10406					live = namespace = '';
10407				}
10408				
10409				// check if we need to replace with touch event
10410				if (map[type]) {
10411					touchEvents.push(live + map[type] + namespace);
10412				} else {
10413					normalEvents.push(live + type + namespace);
10414				}
10415			}
10416			
10417			// add replaced events using old jquery wrapper
10418			if (touchEvents.length !== 0) {
10419				callee._wrappedHandler = wrappedHandler;
10420				// handler can be object also with handler property
10421				if (jQuery.isPlainObject(handler)) {
10422					handler.handler = wrappedHandler;
10423					oldAdd.call(jQuery.event, elem, touchEvents.join(' '), handler, data);
10424				} else {
10425					oldAdd.call(jQuery.event, elem, touchEvents.join(' '), wrappedHandler, data);
10426				}
10427			}
10428			
10429			// rest of events
10430			if (normalEvents.length !== 0) {
10431				oldAdd.call(jQuery.event, elem, normalEvents.join(' '), handler, data);
10432			}
10433		};
10434		
10435		/**
10436		 * Remove monkeypatched method
10437		 */
10438		jQuery.event.remove = function(elem, types, handler, pos) {
10439			if (typeof types === 'object') {
10440				oldRemove.call(jQuery.event, elem, types, handler, pos);
10441				return;
10442			}
10443			
10444			var callee = handler ? (handler.handler || handler) : handler,
10445				etypes = types.split(' '),
10446				type, i = 0, live, namespaces,
10447				normalEvents = [], touchEvents = [];
10448			
10449			// walk through all requested types
10450			while ((type = etypes[i++])) {
10451				// Namespaced event handlers
10452				if (type.indexOf(".") !== -1) {
10453					namespaces = type.split(".");
10454					live = namespaces[0] === 'live' ?
10455						namespaces.shift() + '.' : '';
10456					type = namespaces.shift();
10457					namespace = namespaces.length === 0 ? '' : 
10458						'.' + namespaces.join('.');
10459				} else {
10460					live = namespace = '';
10461				}
10462				
10463				if (revMap[type]) {
10464					touchEvents.push(live + revMap[type] + namespace);
10465				} else {
10466					normalEvents.push(live + type + namespace);
10467				}
10468				
10469				if (touchEvents.length !== 0) {
10470					oldRemove.call(jQuery.event, elem, touchEvents.join(' '),
10471						callee ? (callee._wrappedHandler || callee) : callee,
10472						pos);
10473				}
10474				
10475				if (normalEvents.length !== 0) {
10476					oldRemove.call(jQuery.event, elem, normalEvents.join(' '),
10477						handler, pos);
10478				}
10479			}
10480		};
10481	}
10482});
10483
10484/*!
10485 * jQuery each2 - v0.2 - 8/02/2010
10486 * http://benalman.com/projects/jquery-misc-plugins/
10487 * 
10488 * Inspired by James Padolsey's quickEach
10489 * http://gist.github.com/500145
10490 * 
10491 * Copyright (c) 2010 "Cowboy" Ben Alman
10492 * Dual licensed under the MIT and GPL licenses.
10493 * http://benalman.com/about/license/
10494 */
10495(function($) {
10496	var jq = $([1]);
10497	$.fn.each2 = function( fn ) {
10498		var i = -1;
10499		while ((jq.context = jq[0] = this[++i]) && fn.call(jq[0], i, jq) !== false) {}
10500		return this;
10501	};
10502})(jQuery);
10503
10504/**
10505 * jQuery :focus filter
10506 */
10507(function ($) {
10508	var filters = $.expr[":"];
10509	if (!filters.focus) {
10510		filters.focus = function(elem) {
10511			return elem === document.activeElement &&
10512				(elem.type || elem.href);
10513		};
10514	}
10515})(jQuery);
10516/*global window, navigator, alert, $, encodeURIComponent, document */
10517/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, bitwise: true, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
10518/**
10519 * @fileOverview This file contains functions and objects that used sitewide with jQuery
10520 */
10521
10522// no-op placeholder for django gettext functions
10523if (typeof window.gettext === 'undefined') {
10524	var gettext = function(msgid) {
10525		return msgid;
10526	},
10527	ngettext = function(msgid) {
10528		return msgid;
10529	},
10530	interpolate = function(fmt, obj, named) {
10531		if (named) {
10532    			return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
10533  		} else {
10534    			return fmt.replace(/%s/g, function(match){return String(obj.shift())});
10535  		}
10536	}
10537}
10538
10539/**
10540 * boolean XOR emulation
10541 * @param {Boolean} operand1
10542 * @param {Boolean} operand2
10543 * @return {Boolean}
10544 */
10545function xor(operand1, operand2) {
10546	// to satisfy jslint
10547	operand1 = !operand1;
10548	operand2 = !operand2;
10549	return operand1 !== operand2;
10550}
10551
10552/**
10553 * @namespace Global mydeco namespace.
10554 */
10555if (typeof window.myDeco === 'undefined') {
10556	var myDeco = {};
10557}
10558
10559/* container for all vars based in window object */
10560myDeco.vars = {};
10561
10562if (typeof window.console === 'undefined') {
10563	// In some caching scenarios FB 1.2 console doesn't get initialized by that time.
10564	// This situation leads to errors further in code. That's why we need to load FB
10565	// manually if it exists.
10566	if (typeof window.loadFirebugConsole === 'function') {
10567		window.loadFirebugConsole();
10568	} else {
10569		var console = {
10570			log: function(message) {},
10571			info: function(message) {},
10572			warn: function(message) {},
10573			profile: function() {},
10574			profileEnd: function() {},
10575			error: function(message) {}
10576		};
10577	}
10578}
10579
10580myDeco.browser = {
10581	isIE: navigator.userAgent.indexOf('MSIE ') >= 0,
10582	isIE6: navigator.userAgent.indexOf('MSIE 6.0') >= 0,
10583	isIE8: navigator.userAgent.indexOf('MSIE 8.0') >= 0,
10584	isMobileSafari: navigator.platform.slice(0, 2) === "iP",
10585	isChrome: navigator.userAgent.indexOf('Chrome') >= 0,
10586	isIOSWebView: /ip[ao][dn]e?.*webkit(?!.*safari)/i.test(navigator.userAgent)
10587};
10588
10589/**
10590 * Add a random value parameter to the given link to force a cache miss.
10591 * If the link refers to an <a/> element, then the href property is updated.
10592 */
10593myDeco.addCacheMissParam = function(link, paramName) {
10594	paramName = paramName || myDeco.settings.NO_CACHE_PARAM || '_nc';
10595	var param = paramName + '=' + new Date().valueOf(), 
10596		url = link.href || link,
10597		regexp = new RegExp('([&?])' + paramName + '=[0-9]+');
10598	if(regexp.test(url)) {
10599		url = url.replace(regexp, '$1' + param);
10600	} else {
10601		url += (url.indexOf('?') >= 0 ? '&' : '?') + param;
10602	}
10603	if (link.href) {
10604		link.href = url;
10605	}
10606	return url;
10607};
10608
10609/**
10610 * Returns true if no-cache cookie exists - which should be the case
10611 * if the user recently posted content.
10612 */
10613myDeco.userHasCacheMissCookie = function() {
10614	return $.cookie(myDeco.settings.NO_CACHE_COOKIE_NAME);
10615};
10616
10617/**
10618 * Function that fire event from child window on parent
10619 * @param {String} element_selector Element CSS selector
10620 * @param {String} event_name Event name
10621 * @param {String} event_namespace Event namespace, empty if not needed
10622 * @param {Mixed} event_params Event parameters
10623 */
10624function fireEventFromChild(element_selector, event_name, event_namespace, event_params) {
10625	event_name = event_name + (event_namespace ? '.' + event_namespace : '');
10626	if(typeof event_params === 'undefined') {
10627		$(element_selector).trigger(event_name);
10628	} else {
10629		$(element_selector).trigger(event_name, [event_params]);
10630	}
10631}
10632
10633/**
10634 * Format a number as a price including currency sumbol
10635 * @param {Number} value price value
10636 * @param {Number} locale the formatting locale
10637 * @param {Number} digits the number of digits after the decimal point
10638 */
10639myDeco.formatPrice = function(value, locale, digits) {
10640	//if value couldn't be converted into number: e.g ["4"] or "on request"
10641	if (isNaN(+value)) {
10642		return gettext('Price on request');
10643	}
10644	locale = locale || myDeco.settings.LOCALE;
10645	digits = digits || 2;
10646	
10647	var decimal = Number(value).toFixed(digits).slice(-digits);
10648	
10649	var language = locale.split('-x-')[0],
10650		conv = myDeco.settings.LANGUAGE_LOCALECONV_MAP[language],
10651		separator = conv.p_sep_by_space ? ' ' : '';
10652
10653    var strnum = Math.floor(value).toString();
10654    var number_parts = [];
10655    while (strnum) {
10656        number_parts.unshift(strnum.slice(-3));
10657        strnum = strnum.slice(0,-3);
10658    };
10659
10660	if ((typeof(number_parts.slice(-1)[0]) === "undefined") || (number_parts.slice(-1)[0] === "")) {
10661		number_parts.pop();
10662	}
10663	
10664	var s = number_parts.join(conv.mon_thousands_sep) + conv.decimal_point + decimal;
10665	
10666	if(myDeco.settings.WL) {
10667		if(myDeco.settings.WL.WL_PARTNER_NAME.toLowerCase() === 'falabella') {
10668			conv.currency_symbol = '$';
10669			s = number_parts.join('.');
10670		}
10671		if(myDeco.settings.WL.WL_PARTNER_NAME.toLowerCase() === 'molenaar') {
10672			conv.currency_symbol = '€';
10673		}
10674	}
10675
10676	return conv.p_cs_precedes ?
10677		conv.currency_symbol + separator + s :
10678		s + separator + conv.currency_symbol;
10679};
10680
10681/**
10682 * Convert location to coordintes
10683 * @param {String} location Text notation
10684 * @param {Function} callback Function if response succeeded
10685 * @param {Function} errback Function if response failed
10686 */
10687myDeco.location2coords = function(location, callback, errback) {
10688	$.ajax({
10689		type: 'get',
10690		data: 'location=' + encodeURIComponent(location),
10691		dataType: 'json',
10692		url: myDeco.urls.ws.location_to_coords,
10693		success: function(data) {
10694			if(data.error && $.isFunction(errback)) {
10695				errback(data.error);
10696			} else if($.isFunction(callback)) {
10697				callback(data.shift().coord);
10698			}
10699		},
10700		error: function(xhr, a, b) {
10701			var data = JSON.parse(xhr.responseText),
10702				error = data && data.error ? data.error : 'unexpected error';
10703			if($.isFunction(errback)) {
10704				errback(error);
10705			}
10706		}
10707	});
10708};
10709
10710
10711//enable background images caching for IE
10712//needed to avoid reloading of carousel backgrounds
10713try {
10714	document.execCommand('BackgroundImageCache', false, true); 
10715} catch(e) {}
10716
10717/**
10718 * Check of secure connection
10719 * @return {Boolean} true in case of secure connection
10720 */
10721myDeco.isSecure = function() {
10722	return window.location.protocol === 'https:';
10723};
10724
10725/**
10726 * Joins base and relative url to form an absolute URL.
10727 *
10728 * @param {String} base Base of the URL
10729 * @param {String} url Relative URL
10730 * @return {String} Absolute URL
10731 */
10732myDeco.urljoin = function(base, url) {
10733	if (!base) {
10734		return url;
10735	}
10736	if (!url) {
10737		return base;
10738	}
10739
10740	if (base.charAt(base.length - 1) === '/') {
10741		base = base.substr(0, base.length - 1);
10742	}
10743	if (url.charAt(0) === '/') {
10744		url = url.substr(1, url.length);
10745	}
10746
10747	return base + '/' + url;
10748};
10749
10750/**
10751 * global vars for body, window and document
10752 */
10753$(function(){
10754	$.extend(myDeco, {
10755		body: $('body'),
10756		window: $(window),
10757		document: $(document)
10758	});
10759});
10760
10761/**
10762 * plugin for toggling element attribute
10763 * @param {string} name - name of attr
10764 * @param {string} value - value of attr
10765 * @param {boolean} state - state of toggling
10766 */
10767$.fn.toggleAttr = function(name, value, state) {
10768	return this.each(function() {
10769		var element = $(this);
10770		if (typeof state === 'undefined') {
10771			state = !!element.attr(name);
10772		}
10773		
10774		if (state) {
10775			element.attr(name, value);
10776		} else {
10777			element.removeAttr(name);
10778		}
10779	});
10780
10781};
10782
10783/**
10784 * Load js or css resources, insert them to DOM and fire callback if needed
10785 * @param {String} file,
10786 * @param {String} file,
10787 * @param {String} file,
10788 * ...
10789 * [@param {Function} callback]
10790 */
10791myDeco.loadResources = function() {
10792	var args = Array.prototype.slice.call(arguments, 0),
10793		items = 0,
10794		head = $('head'),
10795		callback = $.isFunction(args[args.length - 1]) ? args.pop() : $.noop,
10796		itemLoaded = function() {
10797			if (args.length === ++items) {
10798				callback.call();
10799			}
10800		};
10801	
10802	$.each(args, function(i, file) {
10803		if (file.indexOf('.css') !== -1) {
10804			head.append(document.createStyleSheet ?
10805				document.createStyleSheet(file) :
10806				$('<link rel="stylesheet" type="text/css" />').attr('href', file));
10807			// Just assume css will load OK.  Not all browsers have an onload for
10808			// css resources.  We could create a timer check if we can access the
10809			// loaded rules, but that won't work if the css is loaded from another
10810			// domain.
10811			itemLoaded();
10812		} else if (file.indexOf('.js') !== -1) {
10813			// use ajax() instead of getScript() so that browser cache is used
10814			$.ajax({url: file, dataType: "script", cache: true, success: itemLoaded});
10815
10816		}
10817	});
10818};
10819
10820
10821/* enable HTML5 tags for IE */
10822myDeco.vars.elements  = ('article:aside:audio:canvas:figure:footer:header:hgroup:nav:section:video:output:detalis:keygen:meter:progress:figcaption:mark:summary:time:wbr').split(':');   
10823for (var c = 0; c < myDeco.vars.elements.length; c++) {
10824	document.createElement(myDeco.vars.elements[c]);
10825}
10826
10827/**
10828 * sort facebook friends to those who have mydeco account and the rest;
10829 * modifying main object
10830 * @param {object} object with all fb friends
10831 * using in austin save form and site register
10832 */
10833myDeco.sortFacebookFriends = function(json) {
10834	var i, friend, md_friends=[], fb_friends=[], arr;
10835	for (i = json.friends.length-1; i > -1; i--) {
10836		friend = json.friends[i];
10837		if (friend.mydeco_user) {
10838			friend.user_id = friend.mydeco_user.user_id;
10839			arr = md_friends;
10840		} else {
10841			arr = fb_friends;
10842		}
10843		arr.unshift(friend);
10844	}
10845	json.friends_not_on_mydeco = fb_friends;
10846	json.friends = md_friends;
10847};
10848/*globals myDeco, Persist, window, Object */
10849/*jslint forin: true, fragment: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, newcap: true, regexp: false, white: false, plusplus: false */
10850
10851/**
10852 * @fileOverview initialize local storage on load
10853 */
10854
10855myDeco.storage = {
10856	/**
10857	 * @var {Persist.Store} store storage holder
10858	 */
10859	store: null,
10860	ready: false,
10861	callbacks: [],
10862	init_counter: 0,
10863
10864	/**
10865	 * Get value from storage
10866	 * @param {String} name
10867	 * @param {Function} callback Returning function
10868	 */
10869	get: function(name, callback) {
10870		var self = this;
10871		this.onready(function() {
10872			self.store.get(name, callback);
10873		});
10874	},
10875	
10876	/**
10877	 * Set value to storage
10878	 * @param {String} name
10879	 * @param {String} value
10880	 */
10881	set: function(name, value) {
10882		var self = this;
10883		this.onready(function() {
10884			self.store.set(name, value);
10885		});
10886	},
10887
10888	/**
10889	 * Remove values from LS
10890	 * @param {String|Array}
10890 name Single name or array of names to remove
10891	 */
10892	remove: function(name) {
10893		var self = this, i, ilen;
10894
10895		name = Object.prototype.toString.call(name) === '[object Array]' ? name : [name];
10896		
10897		this.onready(function() {
10898			for (i = 0, ilen = name.length; i < ilen; i++) {
10899				self.store.remove(name[i]);
10900			}
10901		});
10902	},
10903/**
10904 * Initializes store, and check if it's ready to operate,
10905 * runs previously stored callbacks.
10906 */
10907	init: function() {
10908		var self = this, i, len;
10909		if(!this.store) {
10910			this.store = new Persist.Store('MYD_'+(window.location.hostname).replace(/(\.)/g, '_'), {
10911				swf_path: '/media/img/persist.swf'
10912			});
10913		}
10914		try {
10915			this.store.set('storage_status','ready');
10916			this.store.get('storage_status', function(ok, data) {
10917				if (ok && data === 'ready') {
10918					self.ready = true;
10919				}
10920			});
10921		} catch (e) {  // can raise errors when isnt ready yet
10922		}
10923		if (this.ready) {
10924			this.store.remove('storage_status');
10925			for (i = 0, len = this.callbacks.length; i < len; i++) {
10926				this.callbacks[i]();
10927			}
10928		} else {
10929			// Limit to 200 loops, ~15 sec
10930			if (this.init_counter++ < 300) {
10931				window.setTimeout(this.init.bind(this), 50);
10932			}
10933		}
10934	},
10935	
10936	/**
10937	 * Remember callback to run when storage will be ready
10938	 * @param {Function} callback
10939	 */
10940	onready: function(callback) {
10941		if (typeof callback === 'function') {
10942			if (this.ready) {
10943				callback();
10944			} else {
10945				this.callbacks.push(callback);
10946			}
10947		}
10948	}
10949};
10950
10951window.addLoadEvent(myDeco.storage.init.bind(myDeco.storage));
10952/*globals window, document, $, myDeco */
10953/*jslint sloppy: true, white: true, eqeq: true, nomen: true */
10954
10955myDeco.tracking = {
10956	tags: myDeco.settings.TRACKING_TAGS || [],
10957	cookiesEnabled: false,
10958	prefix: '',
10959
10960	init: function() {
10961		// set tracking cookie if tracking tag present in URL:
10962		var self = this,
10963			queryParams = document.location.href.toQueryParams(),
10964			cookieValue = (new Date()).valueOf();
10965
10966		$.each(this.tags, function(i, tag) {
10967			var value = queryParams[tag.paramName];
10968			if (value) {
10969				self.setTrackingCookie(tag.cookieName, value, tag.type == 'internal');
10970				return false;
10971			}
10972		});
10973
10974		// test user cookies support
10975		$.cookie('test', cookieValue);
10976		if ($.cookie('test') == cookieValue) {
10977			$.cookie('test', "", {expires: -1});
10978			this.cookiesEnabled = true;
10979		}
10980
10981		// Track click events on tagged elements
10982		// example html: <a href="" data-tracker="<action>;<category>;<label>"></a>
10983		$('.custom-tracker').click(function(e) {
10984			// XXX: If the browser loads the link target quickly then there may not be
10985			// time for the event request to complete.  Ideally we should stop the click
10986			// event and wait some time before explicitly setting a new location.
10987			// However such a delay isn't a great user experience and may cause unforseen
10988			// issues with browsers' popup blocker & click-jacking mitigation efforts.
10989			// See http://support.google.com/googleanalytics/bin/answer.py?hl=en&answer=55527
10990			var params = $(this).data('tracker');
10991			if (params) {
10992				params = params.split(';');
10993				myDeco.tracking.trackEvent(params[0], params[1] || 'none', params[2] || this.toString());
10994			}
10995		});
10996
10997		// Track clicks on site nav
10998		$('nav a').click(function(e) {
10999			// XXX: Stop event and redirect on timeout. See custom-tracker above.
11000			var menuId = $(this).closest('.popup-menu').attr('id'),
11001				category = menuId ? menuId.split('-')[1] : 'top',
11002				label = $(this).text().slugify();
11003			myDeco.tracking.trackEvent('siteNavClick', category, label);
11004		});
11005
11006		myDeco.body.bind('roomSaved', function(event, roomType, roomId) {
11007			self.trackEvent(event.type, roomType, String(roomId));
11008		});
11009		
11010		myDeco.body.bind('mydeco.login', function(e, div) {
11011			if (div.view === 'register') {
11012				self.trackStruq("g/fddexTjKfkKRhVu74dmRyA/?v=2&qs=registration");
11013			}
11014		});
11015	},
11016
11017	/**
11018	 * Set a cookie for the specified tracking tag name & value.
11019	 * Any existing cookies are removed unless retainExisting=True.
11020	 */
11021	setTrackingCookie: function(name, value, retainExisting) {
11022		$.each(this.tags, function(i, tag) {
11023			var cookieDomain = tag.cookieDomain || myDeco.settings.USER_DATA_COOKIE_DOMAIN;
11024			if (tag.paramName == name || tag.cookieName == name) {
11025				$.cookie(tag.cookieName, value, {expires: tag.cookieAge, domain: cookieDomain});
11026			} else {
11027				if (!retainExisting) {
11028					$.cookie(tag.cookieName, "", {expires: -1, domain: cookieDomain});
11029				}
11030			}
11031		});
11032	},
11033
11034	/**
11035	 * Send a command to the tracking system.
11036	 */
11037	send: function() {
11038		if (!myDeco.settings.GOOGLE_ANALYTICS_SKIP_TRACKING) {
11039			var command = Array.prototype.slice.call(arguments); // convert arguments to native array
11040			window._gaq = window._gaq || [];
11041			window._gaq.push(command);
11042		}
11043	},
11044
11045	/**
11046	 * Track a page view.
11047	 * @param {String} url Optional url.  Use current document url if not specified.
11048	 */
11049	trackPageview: function(url) {
11050		return this.send(this.prefix + '_trackPageview', url);
11051	},
11052
11053	/**
11054	 * Track a user action.
11055	 *
11056	 * @param {String} action The action for the event.
11057	 * @param {String} category The general event category.
11058	 * @param {String} label An optional descriptor for the event.
11059	 * @param {Int} value An optional value associated with the event.
11060	 */
11061	trackEvent: function(action, category, label, value) {
11062		return this.send(this.prefix + '_trackEvent',
11063			category, action, label, value);
11064	},
11065
11066	/**
11067	 * Set a tracking variable
11068	 *
11069	 * @param {String} name The tracking variable name.
11070	 * @param {String} value The variable value.
11071	 */
11072	setVar: function(name, value) {
11073		return this.send(this.prefix + '_set' + name, value);
11074	},
11075
11076	/**
11077	 * Set a custom tracking variable by slot index.
11078	 *
11079	 * @param {Int} index The slot for this variable..
11080	 * @param {String} name The name for this custom variable.
11081	 * @param {String} value The variable value.
11082	 * @param {Int} scope Optional scope id.
11083	 */
11084	setCustomVar: function(index, name, value, scope) {
11085		return this.send(this.prefix + '_setCustomVar', index, name, value, scope);
11086	},
11087
11088	/**
11089	 * Add tracking information for a sale
11090	 *
11091	 * @param {Order}  order      The order information
11092	 *
11093	 * Where order is a struct containing:
11094	 *
11095	 *  order_id        The order ID
11096	 *  order_total     Total transaction value of the sale
11097	 *  order_tax       Taxable component of order_total
11098	 *  shipping_cost   Shipping cost component of order_total
11099	 *  billing_city    City from billing address for order
11100	 *  billing_state   State from billiing address for order
11101	 *  billing_country Country from billing address for order
11102	 *  items           An array of the items purchased, each array
11103	 *                  being a structure of:
11104	 *      item_id       The item ID
11105	 *      item_name     The name of the item
11106	 *      category      The category the item is in
11107	 *      item_price    The price for a single item
11108	 *      item_quantity How many items we bought
11109	 */
11110	trackOrder: function(order) {
11111		var struq_vars = [],
11112			marin_vars = [],
11113			phg_vars = [],
11114			peerius_vars = {"products": [], "currency": "GBP"};
11115		
11116		// Order tracking with Google
11117		this.send(this.prefix + '_addTrans',
11118			order.order_id, 'MyDeco2 [' + myDeco.settings.LOCALE + ']', order.order_total,
11119			order.order_tax, order.shipping_cost, order.billing_city,
11120			order.billing_state, order.billing_country
11121		);
11122
11123		// Collect order-level information
11124		$.extend(peerius_vars, {
11125			"orderNo": order.order_id,
11126			"shipping": order.shipping_cost,
11127			"total": order.order_total,
11128			"subtotal": order.order_subtotal
11129		});
11130
11131		// Collect information about the various order items
11132		$.each(order.items, $.proxy(function (i, item) {
11133			this.send(this.prefix + '_addItem',
11134				order.order_id, item.item_id, item.item_name,
11135				'', item.item_price, item.item_qty
11136			);
11137			
11138			struq_vars.push('li=pid=' + encodeURIComponent(item.guid) +
11139				'|qty=' + encodeURIComponent(item.item_qty) + '|tv=' +
11140				encodeURIComponent(item.item_netprice));
11141
11142			marin_vars.push('UTM:I|' + order.order_id +'|BASKET|||' +
11143				item.item_price + '|');
11144
11145			phg_vars.push('[category:' + encodeURIComponent(item.cat) +
11146				'/sku:' + item.guid + '/value:' + item.item_netprice +
11147				'/quantity:' + item.item_qty + ']');
11148			
11149			peerius_vars.products.push({
11150				"refcode": item.guid,
11151				"qty": item.item_qty,
11152				"price": item.item_netprice
11153			});
11154		}, this));
11155
11156		this.send(this.prefix + '_trackTrans');
11157
11158		// Order tracking with Struq
11159		struq_vars.push('summary=tot=' + encodeURIComponent(order.order_total) +
11160			'|dis=' + encodeURIComponent(order.discount) + '|oid=' +
11161			encodeURIComponent(order.order_id));
11162
11163		this.trackStruq('cd/Tj3m0Nmp1kqvtKCB_KPyOg/' + '?v=2&qs=' +
11164			encodeURIComponent(struq_vars.join('&')));
11165
11166		// Google Adwords tracking.
11167		if (myDeco.settings.GOOGLE_ADWORDS_TRACKING_ID) {
11168			myDeco.tracking.trackingImage('https://www.googleadservices.com/pagead/conversion/'
11169				+ myDeco.settings.GOOGLE_ADWORDS_TRACKING_ID +'/?value='
11170				+ order.order_total + '&label='
11171				+ myDeco.settings.GOOGLE_ADWORDS_TRACKING_LABEL + '&script=0');
11172		}
11173
11174		// Marin tracking
11175		if (myDeco.settings.MARIN_TRANSACTION_SCRIPT) {
11176			myDeco.tracking.trackingImage(myDeco.settings.MARIN_TRANSACTION_SCRIPT +
11177				'&trans=UTM:T|' + [
11178					order.order_id, '', order.order_total, order.order_tax, 
11179					order.shipping_cost, order.billing_city, '', 
11180					order.billing_country
11181				].join('|') + '%0A' + marin_vars.join('%0A'));
11182		}
11183
11184		// PHG tracking
11185		if (myDeco.settings.PHG_TRACKING_URI) {
11186			myDeco.tracking.trackingImage(myDeco.settings.PHG_TRACKING_URI +
11187				'currency:GBP/conversionref:' + order.order_id +'/' + 
11188				phg_vars.join(''));
11189		}
11190
11191		// Peerius tracking
11192		myDeco.externalUi.initPeerius("en-gb", "order", peerius_vars);
11193	},
11194
11195	/**
11196	 * Add tracking information for a checkout basket
11197	 *
11198	 * @param {Cart}  cart      The cart information
11199	 *
11200	 * Where cart is a struct containing the same information as order
11201	 * from trackOrder() [see above]
11202	 */
11203	trackCheckoutBasket: function (data) {
11204		var peerius_vars = {"products": [], "currency": "GBP"};
11205		
11206		$.each(data.cart.items, function (i, item) {
11207			peerius_vars.products.push({
11208				"refcode": item.guid,
11209				"qty": item.item_qty,
11210				"price": item.item_price
11211			});
11212		});
11213		
11214		$.extend(peerius_vars, {
11215			"subtotal": data.cart.sub_totals,
11216			"total": data.cart.totals,
11217			"shipping": data.shippings[0].price
11218		});
11219		
11220		myDeco.externalUi.initPeerius("en-gb", "checkout", peerius_vars);
11221	},
11222
11223	/**
11224	 * Function for adding tracking custom images for misc objects
11225	 * @param {String} src Tracking image source, use [timestamp] for replace
11226	 * with current timestamp in url
11227	 * @param {Function} callback Callback called when image is loaded
11228	 */
11229	trackingImage: function(src, callback) {
11230		src = src.replace('[timestamp]', (new Date()).valueOf());
11231
11232		$('<img />')
11233			.load(function() {
11234				if ($.isFunction(callback)) {
11235					callback.call(this);
11236				}
11237				$(this).remove();
11238			})
11239			.error(function() {
11240				$(this).remove();
11241			})
11242			.attr({
11243				'src': src,
11244				'class': 'seo-hidden'
11245			})
11246			.appendTo('body');
11247	},
11248	
11249	/**
11250	 * Track something with Struq - remarketing product views, orders, etc.
11251	 */
11252	trackStruq: function (url) {
11253		var rnd = parseInt(Math.random() * 10000000000000000, 10),
11254			protocol = (document.location.protocol == "https:" ? "https:" : "http:");
11255
11256		if (myDeco.settings.STRUQ_TRACKING) {
11257			myDeco.body.append('<ifr'+'ame src="' + protocol + 
11258				myDeco.settings.STRUQ_TRACKING + url + "&rnd=" + rnd + 
11259				'" style="width:0; height:0; border: 0;"></ifr'+'ame>');
11260		}
11261	}
11262};
11263
11264// track all ajax errors
11265$(document).ajaxError(function(e, xhr, options, message) {
11266	if (options.url && options.url.indexOf('/ws/render') > -1) {
11267		return;		// ignore render errors
11268	}
11269	myDeco.tracking.trackEvent('ajaxError', message, options.url);
11270});
11271
11272$(function(){myDeco.tracking.init();});
11273/*globals myDeco, $, window, escape, s_gi, s_account, s_channel, s_pagename, Image */
11274/*jslint forin: true, fragment: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, newcap: true, regexp: false, white: false, plusplus: false */
11275myDeco.buynow = {
11276	popup_id: 0,
11277	omniture: function(obj, guid, price) {
11278		try {
11279			var chan = 'GoToShop',
11280					gts = chan,
11281					s_gts = s_gi(s_account);
11282
11283			if (s_account.indexOf('wl-')) {
11284				chan = s_channel;
11285			}
11286
11287			s_gts.pageName = gts + ':' + s_pagename;
11288			s_gts.channel = chan;
11289			s_gts.prop1 = gts;
11290			s_gts.prop2 = gts;
11291			s_gts.prop3 = gts; 
11292
11293			s_gts.events = 'purchase';
11294			s_gts.products = ';'+guid+';1;'+price;
11295			s_gts.linkTrackVars = 'pageName,channel,prop1,prop2,prop3,events,products,eVar5';
11296			s_gts.linkTrackEvents = 'purchase'; 
11297			s_gts.tl(obj,'o',gts); 
11298		}
11299		catch (e) {}
11300	},
11301
11302	/**
11303	 * Track a go-to-shop click with google as both an event and a page view.
11304	 */
11305	googleAnalytics: function(guid, price, category, src) {
11306		try {
11307			category = category || 'unknown';
11308			src = src || 'unknown';
11309			myDeco.tracking.trackEvent('GTS', category, src, parseInt(price, 10) || 0);
11310			myDeco.tracking.trackPageview('/goal/gts/' + category.slugify() + '/' + src + '/' + guid);
11311		} catch (e) {}
11312	},
11313
11314	clickHandler : function(event, guid, price) {
11315		var button = event.findElement('A');
11316		this.buynowClick(button.get(0), guid, price);
11317
11318		if (button.hasClass('external')) {
11319			event.stop();
11320			this.openWindow();
11321		}
11322	},
11323
11324	buynowClick: function(button, guid, price, category, src, gwoSetting, e) {
11325		this.button_url = $(button).data('url') || button.href;
11326		this.googleAnalytics(guid, price, category, src);
11327		this.omniture(button, guid, price);
11328		//if it is emulation in IE, just open new page after all trackers;
11329		if (myDeco.browser.isIE && myDeco.buynow.eclick) {
11330				this.openWindow(true);
11331		}
11332		//else run experiment
11333		if (!myDeco.browser.isIE) {
11334			var gts_ex = ['gts_new', 'gts_same'], i, value;
11335			for (i = 0; i < gts_ex.length; i++) {
11336				value = gts_ex[i];
11337				if (($.cookie('sv') && $.cookie('sv').indexOf(value) > 0) || myDeco.buynow.eclick) {
11338					if (e && e.stopPropagation) {
11339						e.stopPropagation();
11340						e.preventDefault(); 
11341					}
11342				this.openWindow(value === 'gts_same' || myDeco.buynow.eclick);
11343				myDeco.buynow.eclick = false;
11344				}
11345			}
11346		}
11347	},
11348	
11349	openWindow: function(sameTab) {
11350			var popup = window.open(myDeco.buynow.button_url, 'MD_GoToShop' + myDeco.buynow.popup_id,
11351				       		!sameTab ? 'width=900, height=600, scrollbars=no, toolbars=no, resizable=yes': '').focus();
11352			myDeco.buynow.popup_id += (!sameTab);
11353
11354	},
11355
11356/**
11357 * emulating go-to-shop behaviour for links to usuall product pages
11358 */
11359	emulate: function() {
11360		$('.search-product-link').click(function(e) {
11361			var guid = this.href.split('/').reverse()[1],
11362				price = this.rel;
11363	
11364			//we need to avoid emulation for buttons that already have href="/aff/?product=..."
11365			if (guid === 'aff') {
11366				return;
11367			}
11368	
11369			e.stop();
11370			myDeco.buynow.eclick = true;
11371			myDeco.buynow.buynowClick(this, guid, price, null, 'title', '');
11372		});
11373	}
11374	
11375};
11376/*globals myDeco, window, document, FB, addLoadEvent */
11377/*jslint sloppy: true, nomen: true */
11378
11379/**
11380 * @fileOverview This file contains some wrapper functions around the facebook
11381 * connect library that will load the scripts and iframe resources on demand.
11382 */
11383
11384myDeco.facebook = {
11385
11386	/**
11387	 * Expression to check for existence of facebook cookie.
11388	 * @type {RegExp}
11389	 */
11390	cookiePattern: new RegExp('\\bfbs_' + myDeco.settings.FACEBOOK_APP_ID + '='),
11391
11392	/**
11393	 * Facebook permissions to request on login.
11394	 * @type {String}
11395	 */
11396	permissions: 'email',
11397
11398	/**
11399	 * Flag to record when API is initialised
11400	 */
11401	_initialized: false,
11402
11403	/**
11404	 * Callbacks to fire on init
11405	 */
11406	_loadCallbacks: [],
11407
11408	/**
11409	 * Initialise facebook config.
11410	 */
11411	init: function() {
11412		// If user has auto_share enabled but we don't have a facebook cookie
11413		// try and get one.
11414		if (myDeco.userData && myDeco.userData.auto_share &&
11415					!this.cookiePattern.test(document.cookie)) {
11416			this.getLoginStatus();
11417		}
11418	},
11419
11420	/**
11421	 * Load facebook connect script and initialise library.
11422	 * @param {Function} callback Function to call when FB library is ready.
11423	 */
11424	loadConnect: function (callback) {
11425		if (this._initialized) {
11426			if (callback) {
11427				callback();
11428			}
11429			return;
11430		}
11431
11432		var self = this,
11433			fbRoot = document.getElementById('fb-root'),
11434			script = document.createElement('script'),
11435			src = window.location.protocol +
11436				'//connect.facebook.net/' + myDeco.settings.POSIX_LOCALE + '/sdk.js';
11437
11438		if (callback) {
11439			this._loadCallbacks.push(callback);
11440		}
11441		window.fbAsyncInit = function() {
11442			FB.init({
11443				appId: myDeco.settings.FACEBOOK_APP_ID,
11444				channelUrl: myDeco.urls.facebookProxy,
11445				oauth: true,
11446				cookie: true,
11447				version: 'v19.0'
11448			});
11449			self._initialized = true;
11450			fbRoot.removeChild(script);
11451			while (self._loadCallbacks.length) {
11452				self._loadCallbacks.pop()();
11453			}
11454		};
11455		script.src = src;
11456		// fbRoot.appendChild(script);
11457	},
11458
11459	/**
11460	 * Check if user is currently logged in to facebook.
11461	 * Will call loadConnect if necessary.
11462	 * @param {Function} callback Function to call when check is ready.
11463	 */
11464	getLoginStatus: function (callback) {
11465		this.loadConnect(function() {
11466			FB.getLoginStatus(callback);
11467		});
11468	},
11469
11470	/**
11471	 * Check if a user is already logged in to facebook and if not then
11472	 * display the facebook login popup window.
11473	 * @param {Function} callback Function to call when login status is known.
11474	 */
11475	login: function (callback) {
11476		var options = {scope: this.permissions};
11477		// If we need to wait for the connect script to load then the login
11478		// popup window will be blocked because the call will not be associated
11479		// with a click event.  So if FB is not initialised we instead redirect
11480		// to the standalone login page.
11481		if (this._initialized) {
11482			if (FB.getAuthResponse()) {	// can't use async getLoginStatus
11483				callback({authResponse: FB.getAuthResponse()});
11484			} else {
11485				FB.login(callback, options);
11486			}
11487		} else {
11488			window.location = myDeco.urls.userLogin + '#form=facebook';
11489		}
11490	}
11491};
11492
11493addLoadEvent(myDeco.facebook.init.bind(myDeco.facebook));
11494/*globals myDeco, window, $ */
11495/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
11496          sloppy: true, white: true */
11497/**
11498 * @fileOverview This file contains user object (reading user data from cookies, ajax login/logout, updating header)
11499 */
11500
11501myDeco.user = {
11502	/**
11503	 * Initialise page elements with user data.
11504	 */
11505	//need for pass form element through iframe-proxy callback
11506	parentForm: false,
11507
11508	/**
11509	 * Login template cached data
11510	 * @type {object}
11511	 */
11512	loginTemplate: {
11513		'login': false,
11514		'register': false
11515	},
11516
11517	/**
11518	 * Action performed after successful login
11519	 * @type {Function}
11520	 */
11521	_delayedAction: $.noop,
11522
11523	/**
11524	 * Default login form variant used for loginRequired
11525	 * @type {String}
11526	 */
11527	defaultLoginVariant: 'login',
11528
11529	/**
11530	 * User care module init
11531	 */
11532	init: function() {
11533		this.updateLoginArea();
11534		this.updateCounters();
11535		this.checkAuth();
11536		this.newsLetter();
11537		if (myDeco.showApplePopup) {
11538			this.applePopup();
11539		}
11540
11541		if (myDeco.settings.LOCALE === 'en-gb-x-retail') {
11542			this.cookiesNotify();
11543		}
11544
11545		// Running in Facebook canvas:
11546		if (myDeco.settings.FACEBOOK_CANVAS) {
11547			$('#guest_section').remove();
11548			this.defaultLoginVariant = 'register';
11549			if (!myDeco.username) {
11550				this.checkFacebookStatus();
11551			}
11552		}
11553
11554		$('.ajax-login-link').click(this.ajaxLoginClickHandler.bind(this));
11555		$('.ajax-logout-link').click(this.ajaxLogoutClickHandler.bind(this));
11556		//tracking for search for from header
11557		$('#global-search').find('form').submit(function() {
11558			myDeco.tracking.trackEvent('searchbox', 'Header', $('#query').val(), 1);
11559		});
11560		$('.toggle-link').click(this.toggleVisibleElements.bind(this));
11561	},
11562
11563	newsLetter: function(div) {
11564		div = div ||  myDeco.body;
11565		div.find('.newsletter-form').submit(function(e) {
11566			e.stop();
11567			var form = $(this),
11568				errorFields = form.find('.hideme').hide(),
11569				unsubscribe = window.location.pathname.indexOf('unsubscribe') > -1;
11570
11571			$.ajax({
11572				url: (unsubscribe ? myDeco.urls.newsletter_unsubscribe_ajax :
11573									myDeco.urls.newsletter_subscribe_ajax),
11574				data: form.serialize(),
11575				type: 'post',
11576				success: function() {
11577					myDeco.tracking.trackEvent(unsubscribe ? 'unsubscribe' : 'subscribe', 'newsletter');
11578					form.find('.success-replace').toggle();
11579				},
11580				error: function(xhr) {
11581					var messages = JSON.parse(xhr.responseText).error.message;
11582					for (var key in messages) {
11583						errorFields.filter('.error-' + key).text(messages[key][0]).show();
11584					}
11585				}
11586			});
11587		});
11588	},
11589
11590	/**
11591	 * Defines that current user is the owner of the current page
11592	 */
11593	isCurrentUser: function() {
11594		return typeof myDeco.username !== 'undefined' &&
11595			typeof myDeco.ownername !== 'undefined' &&
11596			myDeco.username === myDeco.ownername;
11597	},
11598
11599	/**
11600	 * Update the content of the top-right profile/login area with relevant user data.
11601	 */
11602	updateLoginArea: function() {
11603		var guest_section = $('#guest_section'),
11604			user_section = $('#user_section');
11605
11606		if (myDeco.username) {
11607			$('.replace-username-link').each(function() {
11608				var link = this,
11609					attr = (link.href || $(link).data('href')).replace('userid', myDeco.userid);
11610				if (link.href) {
11611					$(link).attr('href', attr);
11612				} else {
11613					$(link).data('href', attr);
11614				}
11615			});
11616
11617			$('.top-avatar').css({'background-image': 'url(' + myDeco.avatar + ')'});
11618
11619			$('.login-info').show();
11620
11621			guest_section.hide();
11622			user_section.show();
11623		} else {
11624			guest_section.show();
11625			user_section.hide();
11626			$('.logout-info').show();
11627			//no need to replace links in top nav as it broke ajax login
11628			$('.replace-username-link:not(.unchanged)').attr('href', myDeco.urls.userLogin + '?' + $.param({'next': window.location}));
11629		}
11630	},
11631
11632	/**
11633	 * Update alert & shortlist counter elements.
11634	 */
11635	updateCounters: function(reload) {
11636		if (reload) {
11637			myDeco.setUserData();
11638		}
11639
11640		var link = $('#alerts-link')[0];
11641		if (!myDeco.alert_count && link && !$('.active:first-child', link)) {
11642			link.addClass('grey-colored');
11643		}
11644	},
11645
11646	/**
11647	 * Check if user can be silently logged in with facebook credentials
11648	 */
11649	checkFacebookStatus: function() {
11650		myDeco.facebook.getLoginStatus(function (response) {
11651			if (response.authResponse) {
11652				$.ajax({
11653					url: myDeco.urls.userLoginFacebookAjax,
11654					type: 'post',
11655					dataType: 'json',
11656					success: function (data) {
11657						if (data.success) {
11658							myDeco.user.ajaxLoginSuccessHandler(data);
11659						}
11660					}
11661				});
11662			}
11663		});
11664	},
11665
11666	ajaxLoginClickHandler: function(event, destination, variant) {
11667		if (event) {
11668			event.stop();
11669		}
11670
11671		if (myDeco.username) {
11672			return;
11673		}
11674
11675		var self = this,
11676			isLogin,
11677			_variant = (variant || (event ? $(event.target).data('logintype') : 'login')).split('-'),
11678			form = _variant[1],
11679			context = myDeco.vars.reg_context,
11680			process = function(div) {
11681				myDeco.buttons.initSubmitButtons(div.find('input:submit'));
11682				div.find('a.popup-login-link')
11683					.click(self.ajaxLoginClickHandler.bind(self)).end()
11684					.find('form').submit(function (e) {
11685						var form = $(this);
11686						if (myDeco.buttons.isDisabled(form)) {
11687							e.stop();
11688						} else {
11689							self.parentForm = myDeco.buttons.disable(form);
11690						}
11691						if ($('input[name="reg_context"]').size() > 0) {
11692							if (context) {
11693								$('input[name="reg_context"]').val(context);
11694							}
11695						}
11696						// looks like we have a bug in mobile safari
11697						if (myDeco.browser.isMobileSafari) {
11698							this.submit();
11699						}
11700					});
11701				myDeco.login.init(div);
11702				if (form && form === 'mydeco') {
11703					div.find('li.register-mydeco').click();
11704				}
11705			},
11706			success = function(data) {
11707				if (destination) {
11708					process(destination.text('').append(data));
11709				} else {
11710					myDeco.popin.open({
11711						template: data,
11712						tracking: 'login',
11713						closeText: '',
11714						height: 'auto',
11715						width: $('<div class="login-content-holder seo-hidden">').appendTo(myDeco.body).width(),
11716						className: 'login-popin',
11717						holderClassName: 'login-content-holder',
11718						backgroundClassName: 'mdtwo-alert-background',
11719						afterOpen: process
11720					});
11721				}
11722			},
11723			templateLoad = function() {
11724				$.ajax({
11725					url: isLogin ? myDeco.urls.userLoginAjax : myDeco.urls.userRegisterAjax,
11726					type: 'get',
11727					success: function(data) {
11728						self.loginTemplate[variant] = data;
11729						success(data);
11730					}
11731				});
11732			};
11733
11734		variant = _variant[0];
11735		isLogin = (variant === 'login');
11736
11737		// login template is present only if we have loaded merges previously
11738		if (this.loginTemplate[variant]) {
11739			success(this.loginTemplate[variant]);
11740		} else {
11741			if (typeof myDeco.login === 'undefined') {
11742				myDeco.loadResources(myDeco.urls.merges.login_css, myDeco.urls.merges.login_js, templateLoad);
11743			} else {
11744				templateLoad.call();
11745			}
11746		}
11747	},
11748
11749	/**
11750	 * Callback used from hidden login iframe proxy for login-register form
11751	*/
11752	ajaxLoginSuccessHandler: function(result) {
11753		var form = this.parentForm ?
11754				myDeco.buttons.enable(this.parentForm) : false,
11755			view = result && result.view_id || 'login',
11756			i, error;
11757		if (result && result.error && form ) {
11758			form.find('p.error').remove();
11759			for (i = 0; i < result.error.message.length; i++) {
11760				error = result.error.message[i];
11761				if (error.field == 'global') {
11762					form.prepend('<p class="error">' + error.error + '</p>');
11763				} else {
11764					form.find(':input[name=' + error.field + ']')
11765						.addClass('error-border')
11766						.before('<p class="error">' + error.error + '</p>')
11767						.filter('input')
11768						.closest('p.name-field').addClass('not-available');
11769				}
11770				// track first error only
11771				if (i == 0) {
11772					myDeco.tracking.trackEvent('login-error', view + '-' + error.field, error.error);
11773				}
11774			}
11775			myDeco.body.trigger('mydeco.login-error');
11776		} else {
11777			myDeco.popin.close();
11778			myDeco.setUserData();
11779			this.updateLoginArea();
11780			myDeco.body.trigger("mydeco.login", [{username: myDeco.username, view: view}]);
11781			myDeco.tracking.trackEvent('login',
11782							view.indexOf('register') >= 0 ? 'register' : 'login',
11783							view.indexOf('facebook') >= 0 ? 'facebook' : 'mydeco');
11784			this._delayedAction.call();
11785			this.checkAuth();
11786		}
11787	},
11788
11789	/**
11790	 * Remove user cookies and update user data & DOM elements.
11791	 */
11792	ajaxLogoutClickHandler: function(event) {
11793		if (event) {
11794			event.stop();
11795		}
11796		var self = this,
11797			updateUserData = function() {
11798				myDeco.username = null;
11799
11800				myDeco.setUserData();
11801				self.updateLoginArea();
11802				if (event && $(event.target).closest('a').hasClass('redirect-to-root')) {
11803					myDeco.document.one('mydeco.login', function() {
11804						window.location = '/';
11805					});
11806				}
11807				myDeco.body.trigger("mydeco.login", [{}]);
11808				myDeco.tracking.trackEvent('logout', 'logout');
11809			};
11810
11811		$.ajax({
11812			url: myDeco.urls.ws.logout,
11813			type: 'post',
11814			success: updateUserData,
11815			error: function() {
11816				// remove cookies only if backend fails
11817				$.cookie(myDeco.settings.AUTH_COOKIE_NAME,		null, {path: '/', domain: myDeco.settings.AUTH_COOKIE_DOMAIN});
11818				$.cookie(myDeco.settings.USER_DATA_COOKIE_NAME,	null, {path: '/', domain: myDeco.settings.USER_DATA_COOKIE_DOMAIN});
11819				updateUserData();
11820			}
11821		});
11822
11823	},
11824
11825	/**
11826	 * Close iframe callback
11827	 */
11828	ajaxLoginCloseHandler: function() {
11829		this.iframe.remove();
11830	},
11831
11832	/**
11833	 * Initializes elements that depend on user's auth state.
11834	 */
11835	checkAuth: function() {
11836		$('.authorized').toggle(this.isCurrentUser());
11837		$('.unauthorized').toggle(!this.isCurrentUser());
11838
11839		var forms = $('.user-rating-form'), socialLink;
11840
11841		// If user is logged in use logged in text and
11842		// attach handlers to remove it.
11843		if (myDeco.username) {
11844			$('.login-info').show();
11845			$('.logout-info').hide();
11846
11847			forms.find('fieldset:first').each(function() {
11848				var fieldset = $(this),
11849					textarea = fieldset.find('textarea:first'),
11850					info = fieldset.find('.login-placeholder');
11851				fieldset.click(function() {
11852					info.hide();
11853					textarea.focus();
11854				});
11855				// if textarea not empty in case of ordinal form POST with errors
11856				// hide info and set cursor to textarea
11857				if(textarea.val() !== '') {
11858					fieldset.click();
11859				}
11860			});
11861		} else {
11862			// If user is not logged in use redirect it to login form,
11863			//remove all observers from button to avoid validation and error bubble
11864			if (!forms.hasClass('ajax-login-form')) {
11865				forms.find('a.review-button').unbind().attr('href', myDeco.urls.userLogin + '?next=' + window.location.href);
11866			}
11867			$('.logout-info').show();
11868			$('.login-info').hide();
11869		}
11870	},
11871
11872	/**
11873	 * Wrapper for actions which require login state
11874	 * @param {Function} successHandler Callback which will be executed after successful login. Very usable for actions which require login for it
11875	 * @param {String} variant - variant of login form
11876	 */
11877	loginRequired: function(successHandler, variant) {
11878		this._delayedAction = $.isFunction(successHandler) ? successHandler : $.noop;
11879
11880		if (!myDeco.username) {
11881            if (myDeco.settings.WL && myDeco.settings.WL.WL_CUSTOM_LOGIN) {
11882                var msg = {name: "login required"};
11883                parent.postMessage(msg.name, "*"); // backwards compatibility
11884                parent.postMessage(msg, "*");
11885                parent.postMessage(JSON.stringify(msg), "*"); // IE9
11886            } else {
11887			    this.ajaxLoginClickHandler(null, null, variant || this.defaultLoginVariant);
11888            }
11889		} else {
11890			this._delayedAction.call();
11891		}
11892	},
11893
11894	/**
11895	 * toggling elements visibility, used on PP and on /c/ pages
11896	 * @param {Event} e - click on element
11897	 */
11898	toggleVisibleElements: function(e) {
11899		e.stop();
11900		var menu = $(e.target).closest('.toggle-element-container').toggleClass('opened'),
11901			popup = menu.find('.toggle-element:not(.full-desr)').toggle();
11902
11903		myDeco.formDispatcher.setPosition(popup, {
11904			'control': menu,
11905			'position': 'bottom',
11906			'relative': true,
11907			//10 and 20 is random negative margin, need to overflow triangle in bg
11908			'vrow': menu.outerHeight() - 20,
11909			'rparent': popup.outerWidth() - 10
11910		});
11911		//click on any place except Add-to-folder popup should collapce right-hand menu
11912		 //why except Add-to-folder: see SITE-7351
11913		myDeco.body.click(function(e) {
11914			if (menu.hasClass('opened') && !$(e.target).closest('.object-action-popup').size()) {
11915				menu.removeClass('opened').find('.toggle-element:not(.no-autoclose)').toggle();
11916			}
11917		});
11918	},
11919
11920	/**
11921	* showing AppStore popup on apple devices
11922	*/
11923	applePopup: function() {
11924	  // Define Apple device
11925		var deviceAgent = navigator.userAgent.toLowerCase();
11926		var devices = [
11927			{ 'name': 'iPhone', 'matches': deviceAgent.match(/iphone|ipod/) },
11928			{ 'name': 'iPad', 'matches': deviceAgent.match(/ipad/) }
11929		];
11930
11931		if ($.cookie("app") == null){
11932			for (i = 0; i < devices.length;i++ ){
11933				if(devices[i].matches) {
11934					if(confirm('Did you know that you can find inspiration on the go with the mydeco.com ' + devices[i].name +' app? Browse 3D rooms and create your own inspiration gallery on your phone. Download it from the app store b
11934y searching `mydeco`?')) {
11935						window.location.href = 'http://itunes.apple.com/gb/app/mydeco.com/id478304722';
11936					}
11937					$.cookie("app", "seen", { expires: 30 });
11938				}
11939			}
11940		}
11941	  },
11942
11943	/** 
11944	* Notify our users that we use cookies
11945	*/
11946	cookiesNotify: function() {
11947		if (!myDeco.storage) {
11948			return;
11949		}
11950
11951		myDeco.storage.get('notified', function(check, saved) {
11952			if (!saved) {
11953				myDeco.body.addClass('notifier-on');
11954
11955				$('.close-noty').click(function(e) {
11956					e.stop();
11957					$(this).parent().hide();
11958				});
11959
11960				// save into storage
11961				$('.save-noty').click(function() {
11962					myDeco.storage.set('notified', true);
11963					myDeco.body.removeClass('notifier-on');
11964				});
11965			}
11966		});
11967	}
11968
11969};
11970
11971
11972// Update DOM elements:
11973$(function(){myDeco.user.init();});
11974/*globals myDeco, window, $ */
11975/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: true, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
11976
11977if (typeof myDeco.sidebarMenu === 'undefined') {
11978	myDeco.sidebarMenu = {
11979		/**
11980		 * Active class name
11981		 * @var {String}
11982		 */
11983		activeClass: 'active',
11984		paneHiddenClass: 'hidden',
11985		
11986		/**
11987		 * Init function
11988		 */
11989		init: function(root) {
11990			(typeof root !== 'undefined' ? root.find('.sidebar-menu li') : $('.sidebar-menu li'))
11991				.click(this.menuTab.bind(this));
11992		},
11993		
11994		/**
11995		 * switcher for active/inactive tabs
11996		 * @param {Object} e - event if this function was binded; but it can be a jquery object;
11997		 *(it is jq object for cases we need to extract menu variant from url(see /account/login/)
11998		 */
11999		menuTab: function(e) {
12000			
12001			//no need stop event if it is checkbox;
12002			if(e.target && e.target.tagName !== 'INPUT') {
12003				e.stop();
12004			}
12005			
12006			var self = this,
12007				newtab = e.target ? $(e.target).closest('li') : e,
12008				newinput = newtab.find('input'),
12009				menu = newtab.closest('.sidebar-menu'),
12010				oldtab = menu.find('li.' + this.activeClass),
12011				oldinput = oldtab.find('input'),
12012				root = newtab.closest('.sidebar-menu-root');
12013			
12014			if(newtab.get(0) === oldtab.get(0)) {
12015				return;
12016			}
12017			
12018			oldtab.removeClass(this.activeClass);
12019			oldinput.removeAttr('checked');
12020			root.find('.' + oldtab.data('name')).addClass(this.paneHiddenClass);
12021			
12022			newtab.addClass(this.activeClass);
12023			newinput.attr('checked', 'checked');
12024			
12025			root.find('.' + newtab.data('name')).removeClass(this.paneHiddenClass);
12026			// resize popin if it already open
12027			if(myDeco.popin && myDeco.popin.isOpen()) {
12028				myDeco.popin.resize();
12029			}
12030			
12031			if(root.hasClass('has-history')) {
12032				//set variant url in a history for non-popin elements, e.g login page
12033				$.history.add('form=' + newtab.attr('id').split('-')[1]);
12034			}
12035			
12036			root.trigger('switched');
12037		}
12038	};
12039	
12040	// No need to init at loading yet
12041	//addLoadEvent(myDeco.sidebarMenu.init.bind(myDeco.sidebarMenu));
12042};
12043/*globals $, myDeco, window, tmpl, gettext */
12044/*jslint sloppy: true, plusplus: true */
12045/**
12046 * @fileOverview login observers
12047 */
12048myDeco.login = {
12049	callback: null,
12050
12051	staticLogin: false,
12052
12053	staticLoginInvite: false,
12054
12055	alreadyBound: false,
12056
12057	customCallback: 'followStep',
12058
12059	//init login elements
12060	init: function (root) {
12061		root = root || $('div.sidebar-menu-root');
12062		var self = this, fbConnect = $('#fb-connect');
12063		if (myDeco.login.staticLogin) {
12064			$('a.ajax-login-link').unbind();
12065		}
12066
12067		if ($.isFunction(myDeco.login.callback)) {
12068			myDeco.login.callback(root);
12069		}
12070
12071		if (!myDeco.settings.AUSTIN_DETECT) {
12072			root.find('.default-value-js').defaultValue('');
12073		}
12074
12075		myDeco.sidebarMenu.init(root);
12076
12077		this.showPassword(root);
12078
12079		//prevent double-init events bind
12080		if (!self.alreadyBound) {
12081			myDeco.body
12082				.bind('mydeco.login', function (e, data) {
12083					self[self.customCallback](data.view);
12084				})
12085				.bind('mydeco.fbusername-generated', function (e, data) {
12086					self.callback = function (div) {
12087						self.buildUsernameLayout(div, data.data);
12088						self.killDaddy(div.find('div.removeme:first'), $('#fb-call'));
12089					};
12090
12091					myDeco.user.ajaxLoginClickHandler(null, null, 'register');
12092				});
12093			self.alreadyBound = true;
12094		}
12095
12096		if (myDeco.settings.FACEBOOK_CANVAS) {
12097			// It would be nice to call facebookConnect() here but since this
12098			// is probably a callback from loading login form the oauth popup
12099			// would be blocked by the browser.
12100			root.find('#register-area').addClass('fb-only');
12101		}
12102
12103		if (fbConnect.length) {
12104			// Load facebook connect code early so that we don't run into
12105			// browser popup blocker in myDeco.facebook.login callbacks,
12106			myDeco.facebook.getLoginStatus();
12107			fbConnect.click(this.facebookConnect.bind(this));
12108		}
12109
12110		// reset password form init
12111		root.find('.reset-password-form').submit(this.initResetPassword.bind(this));
12112
12113		//switch facebook new/returning forms
12114		root.find('a.form-switcher').click(function (e) {
12115			e.stop();
12116			$(e.target).closest('div.form-container')
12117				.find('form').toggleClass('hidden').end()
12118				.find('form:not(.hidden) input:first')
12119				.focus();
12120		});
12121
12122		// put it to a delayed action because it flicker
12123		// screen on appearing
12124		window.setTimeout(function() {
12125			root.find('.returning-user input[name=username]').focus();
12126		}, 100);
12127
12128		myDeco.body.trigger('mydeco.login-shown', [root]);
12129
12130		// Show register form after registrations error
12131		if (myDeco.vars.regError) {
12132			$('li.register-mydeco').trigger('click');
12133		}
12134	},
12135
12136	/**
12137	* Generates username based on user's Facebook account data.
12138	* Sends an Ajax request and forms an options list from returned values.
12139	*/
12140	facebookGenerateUsername: function (root) {
12141		var self = this;
12142		$.ajax({
12143			url: myDeco.urls.generateFacebookUsername,
12144			dataType: 'json',
12145			success: function (data) {
12146				if (root.length) {
12147					self.buildUsernameLayout(root, data);
12148				} else {
12149					myDeco.body.trigger('mydeco.fbusername-generated', {data: data});
12150				}
12151			}
12152		});
12153	},
12154
12155	buildUsernameLayout: function (root, data) {
12156		var select = root.find('select'), p = select.closest('div.username'), i, self = this;
12157		//if we have username variants, put them into select as <options>
12158		if (data.length > 0) {
12159			for (i = 0; i < data.length; i = i + 1) {
12160				select.find('option:last').before($('<option value="' + data[i] + '">' + data[i] + '</option>'));
12161			}
12162			select.find('option:first').attr('selected', 'selected')
12163				.end()
12164				.change(function (e) {
12165					//replace select with input if user want to choose his own username
12166					if ($(e.target).val() === 'own') {
12167						self.usernameInput(p);
12168					}
12169				});
12170		} else {
12171			//the same replacing if there is no username variants
12172			self.usernameInput(p);
12173		}
12174	},
12175
12176	/**
12177	 * build list of user's friends from json
12178	 * @param {object} container for template
12179	 * @param {object} json string with params for template
12180	 */
12181	fillUsersList: function (container, params) {
12182		container.html(tmpl('flist-tmpl', params)).find('div.face-friends-on').slideDown(300);
12183	},
12184
12185	/**
12186	 * The "Facebook invitation" step
12187	 */
12188	facebookInvitationStep: function (form) {
12189		var self = this;
12190		form = self.killDaddy(form, $('#facebook-invitation-step'));
12191		$.ajax({
12192			url: myDeco.urls.facebookInvitationStep,
12193			dataType: 'json',
12194			success: function (json) {
12195				myDeco.sortFacebookFriends(json);
12196				self.fillUsersList($('#fbshare'), {
12197					peoples: json.friends_not_on_mydeco,
12198					mode: 'share'
12199				});
12200				$('#afterinvite').click(function (e) {
12201					e.stop();
12202					$.ajax({
12203						url: myDeco.urls.facebookInvitationStepPost,
12204						type: 'post',
12205						dataType: 'json',
12206						data: $(e.target).closest('form').serialize(),
12207						success: function() {
12208							if (myDeco.settings.WL_PROFILE) {
12209								myDeco.popin.close();
12210							} else {
12211								self.killDaddy(form, $('#invite-form'));
12212							}
12213						},
12214						error: function() {
12215							myDeco.popin.close();
12216						}
12217					});
12218				});
12219
12220				$('#fbfriends-autocomplete').defaultValue('')
12221					.autocomplete({
12222						param: 'q',
12223						ajax: myDeco.urls.facebookFriendsAutocomplete,
12224						list: $.map(json.friends_names, function (name) {return name; }),
12225						insertText: function (obj) {return obj.name; },
12226						template: function (obj) {
12227							return '<li><span class="image" style="background-image: url(http://graph.facebook.com/'
12228								+ obj.id + '/picture/)"></span>'
12229								+ '<span class="face-name">' + obj.name + '</span></li>';
12230						},
12231						wrapper: '<div class="autocomplete super-top"><ul class="share-list"></ul></div>'
12232					});
12233			}
12234		});
12235	},
12236
12237	/**
12238	 * function that toggle password characters
12239	 * @param {Object} root container
12240	 */
12241	showPassword: function (div) {
12242		if (div.hasClass('inited')) {
12243			return;
12244		}
12245		var pswrd = div.addClass('inited').find('input.password').change(function (e) {pswrd.val($(this).val()); });
12246
12247		//show password toggle
12248		div.find('input.show_password')
12249			.toggleAttr('checked', 'checked', false)
12250			.change(function (e) {
12251				pswrd.toggleClass('hidden');
12252			});
12253	},
12254
12255	/**
12256	 * remove previous form
12257	 * @param {Object} previous form
12258	 */
12259	killDaddy: function (daddy, decent) {
12260		daddy.closest('.removeme').remove();
12261		if (decent) {
12262			return decent.slideDown(300);
12263		}
12264	},
12265
12266	/**
12267	 * Init the "follow" step of the registration
12268	 */
12269	followStep: function (view) {
12270		/* Disable invite and follow steps for 3d-tool login */
12271		if (!((view === 'register' || view === 'facebook_register') && myDeco.user.loginTemplate.register) || myDeco.vars.reg_context === 'community') {
12272			return;
12273		}
12274
12275		var popin = myDeco.popin.open({
12276				template: myDeco.user.loginTemplate.register,
12277				width: 568,
12278				className: 'login-popin steps',
12279				holderClassName: 'login-content-holder',
12280				backgroundClassName: 'mdtwo-alert-background'
12281			}),
12282			isFacebook = (view === 'facebook_register'),
12283			content = $('#follow-step').find('div.fbfriends').toggle(isFacebook).end(),
12284			form = this.killDaddy(content.prevAll(), content),
12285			list = form.find('div.updateme:visible'),
12286			self = this,
12287			i;
12288
12289		$.ajax({
12290			url: isFacebook ? myDeco.urls.userFollowStepFacebook : myDeco.urls.userFollowStep,
12291			dataType: 'json',
12292			success: function (json) {
12293				for (i = 0; i < list.length; i++) {
12294					var mode = $(list[i]).data('mode');
12295					if (json.friends) {
12296						myDeco.sortFacebookFriends(json);
12297					}
12298					self.fillUsersList($(list[i]), {
12299						peoples: json[mode],
12300						mode: mode
12301					});
12302				}
12303				$('#afterfollow').click(function (e) {
12304					e.stop();
12305					$.ajax({
12306						type: 'post',
12307						data: $(e.target).closest('form').serialize(),
12308						dataType: 'json',
12309						url: myDeco.urls.userFollowStepPost,
12310						success: function() {
12311							if (isFacebook) {
12312								self.facebookInvitationStep(form);
12313							} else {
12314								myDeco.popin.close();
12315							}
12316						}
12317					});
12318				});
12319			}
12320		});
12321	},
12322
12323	/**
12324	 * replace select with username variants with input for custom username
12325	 * @param {Object} p - <p> that contain our select
12326	 */
12327	usernameInput: function (p) {
12328		p.before($("<p class='username reg-form'>" +
12329			"<label>" + gettext('Choose a mydeco username') + "</label>" +
12330			"<label class='tip username_error'>" + gettext("This is how you'll be known on the site") + "</label>" +
12331			"<label class='username-icon'></label>" +
12332			"<input type='text' title='" + gettext('This is how you will be known on mydeco') + "' name='username' maxlength='35' id='id_username'></p>"))
12333			.closest('form')
12334			.find('.username input')
12335			.keyup(this.checkAvailability.bind(this));
12336		p.remove();
12337	},
12338
12339	/**
12340	* reset password observer
12341	* @param {event} e - click on reset password link
12342	*/
12343	initResetPassword: function (e) {
12344		e.stop();
12345		var form = $(e.target),
12346			parent = form.closest('div.form-container');
12347
12348		$.ajax({
12349			url: myDeco.urls.requestPasswordReset,
12350			dataType: 'json',
12351			type: 'post',
12352			data: form.serialize(),
12353			success: function (data) {
12354				if (data.error) {
12355					form.find('.error').text(data.message);
12356				} else {
12357					form.addClass('hidden');
12358					parent
12359						.find('.error').empty()
12360						.end()
12361						.find('.reset-password-success')
12362						.show()
12363						.prepend('<p>' + data.message + '</p>');
12364
12365						$('.reset-password-success').delegate('.close', 'click', function() {
12366							$('.reset-password-success').hide().parent().find('form:eq(0)').removeClass('hidden');
12367						});
12368
12369				}
12370			}
12371		});
12372	},
12373
12374	/**
12375	 * observer for facebook connect forms
12376	 * @param {event} e - click on facebook connect button
12377	 */
12378	facebookConnect: function (e) {
12379		e.stop();
12380		var self = this,
12381			form = $(e.target).closest('div.form-fb').addClass('processed');
12382
12383		function handleError(reason) {
12384			form.removeClass('processed');
12385			$('#fb-connect').prev('.error').text(
12386				'There was an error checking your Facebook details.' +
12387					'Please ensure that you are logged in to Facebook and try again.'
12388			);
12389			if (reason.responseText) {
12390				try {
12391					reason = JSON.parse(reason.responseText);
12392					reason = reason.error.message;
12393				} catch (e) {
12394				}
12395			}
12396			myDeco.tracking.trackEvent('login-error', 'facebook-connect', reason.toString());
12397		}
12398
12399		myDeco.facebook.login(function (response) {
12400			if (!response.authResponse) {
12401				handleError('unauthorized');
12402				return;
12403			}
12404			$.ajax({
12405				url: myDeco.urls.userLoginFacebookAjax,
12406				dataType: 'json',
12407				type: 'post',
12408				success: function (data) {
12409					// if user already has account...
12410					if (data.redirect_url) {
12411						if (self.staticLogin && !self.staticLoginInvite) {
12412							window.location.href = data.redirect_url;
12413						} else {
12414							myDeco.user.ajaxLoginSuccessHandler(data);
12415						}
12416					// ...else user needs to register
12417					} else {
12418						if (data.mydeco_user) {
12419							$.ajax({
12420								url: myDeco.urls.facebookAssociateAccount,
12421								type: 'post',
12422								success: function (data) {
12423									if (data.success) {
12424										myDeco.user.ajaxLoginSuccessHandler(data);
12425									}
12426								},
12427								error: handleError
12428							});
12429						} else {
12430							self.facebookGenerateUsername(self.killDaddy(form, $('#fb-call')));
12431						}
12432					}
12433				},
12434				error: handleError
12435			});
12436		});
12437	}
12438};
12439
12440$(function() {
12441	if (myDeco.login.staticLogin) {
12442		myDeco.login.init();
12443	}
12444});
12445/**
12446 * @fileOverview Get userdata info from cookies
12447 */
12448
12449myDeco.userData = {};
12450
12451/**
12452 * Take data from user_data cookie and add it to myDeco object.
12453 */
12454myDeco.setUserData = function() {
12455	var cookie = myDeco.readCookie(myDeco.settings.USER_DATA_COOKIE_NAME);
12456	if (cookie) {
12457		var userData = myDeco.userData;
12458		try {
12459			try {
12460				userData = eval('(' + cookie + ')');
12461			} catch (e) {
12462				// trick to get rid of extra " symbols
12463				cookie = cookie.slice(1, -1);
12464				userData = eval('(' + cookie + ')');
12465			}
12466
12467			if (typeof userData != 'object')
12468				throw "Not an object";
12469
12470		} catch (e) {
12471			console.log('Invalid user_data cookie value: ' + e);
12472		}
12473
12474		for (var prop in userData)
12475			myDeco[prop] = userData[prop];
12476		
12477		myDeco.userData = userData;
12478	} else {
12479		//zero-fill mydeco properties getting from cookies (need for ajax-logout)
12480		for (var prop in myDeco.userData)
12481			myDeco[prop] = '';
12482			myDeco.userData = {};
12483	}
12484	
12485	myDeco.urls.updateUserUrls(myDeco.username);
12486}
12487// Dont need to wait for DOM ready event
12488myDeco.setUserData();
12489/**
12490 * @fileOverview Class inheritance prototype
12491 */
12492
12493(function(){
12494	var initializing = false,
12495		fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
12496
12497	// The base Class implementation (does nothing)
12498	this.Class = function(){};
12499
12500	// Create a new Class that inherits from this class
12501	Class.extend = function(prop) {
12502		var _super = this.prototype;
12503
12504		// Instantiate a base class (but only create the instance,
12505		// don't run the init constructor)
12506		initializing = true;
12507		var prototype = new this();
12508		initializing = false;
12509
12510		// The dummy class constructor
12511		function Class() {
12512			// All construction is actually done in the init method
12513			if ( !initializing && this.init )
12514				this.init.apply(this, arguments);
12515		}
12516
12517		// Copy the properties over onto the new prototype
12518		for (var name in prop) {
12519			// Check if we're overwriting an existing function
12520			prototype[name] = typeof prop[name] == "function" &&
12521					typeof _super[name] == "function" && fnTest.test(prop[name])
12522				? (function(name, fn){
12523					return function() {
12524						var tmp = this._super;
12525
12526						// Add a new ._super() method that is the same method
12527						// but on the super-class
12528						this._super = _super[name];
12529
12530						// The method only need to be bound temporarily, so we
12531						// remove it when we're done executing
12532						var ret = fn.apply(this, arguments);
12533						this._super = tmp;
12534
12535						return ret;
12536					};
12537				})(name, prop[name])
12538				: prop[name];
12539
12540			if (typeof prop[name] != 'function' && name.substring(0, 1) == '$')
12541				Class[name] = prototype[name];
12542		}
12543		
12544		for (var name in _super)
12545			if (prop[name] == undefined && _super[name] != undefined && typeof _super[name] == 'object') {
12546				switch (Object.prototype.toString.call(_super[name])) {
12547					case '[object Array]':
12548						prototype[name] = jQuery.extend(true, [], _super[name]);
12549						break;
12550					case '[object Object]':
12551						prototype[name] = jQuery.extend(true, {}, _super[name]);
12552						break;
12553				}
12554			}
12555
12556		// Populate our constructed prototype object
12557		Class.prototype = prototype;
12558
12559		// Enforce the constructor to be what we expect
12560		Class.constructor = Class;
12561
12562		// And make this class extendable
12563		Class.extend = arguments.callee;
12564
12565		return Class;
12566	};
12567})();
12568myDeco.tabSwitcher = {
12569	initTabSwitcher: function(switcher, options) {
12570		var options = $.extend(true, {
12571				// use to add possibility to hide all tabs 
12572				always: !switcher.hasClass('not-always'),
12573				useVisibility: switcher.hasClass('use-visibility')
12574			}, options || {}),
12575			tabs = switcher.find('.tab'),
12576			activeTab = tabs.filter('.active'),
12577			index = tabs.index(activeTab) || (options.always ? 0 : -1),
12578			navButtons = switcher.find('.tabs-navigation .next-link, .tabs-navigation .prev-link').click(function(e) {
12579				e.stop();
12580				var element = $(this);
12581				element.get(0).blur();
12582				
12583				if (element.hasClass('disabled'))
12584					return;
12585				
12586				if (element.hasClass('next-link'))
12587					$(tabs.get(index + 1)).click();
12588				else 
12589					$(tabs.get(index - 1)).click();
12590			}),
12591			
12592			dummyCallback = function(tab, callback) { callback(tab); };
12593			
12594			updateNavLinks();
12595		
12596		options.onShow = options.onShow || dummyCallback;
12597		options.onHide = options.onHide || dummyCallback;
12598		
12599		function updateNavLinks() {
12600			navButtons.removeClass('disabled prev-link-disabled next-link-disabled');
12601			if (index == 0)
12602				navButtons.filter('.prev-link').addClass('disabled prev-link-disabled');
12603
12604			if (index == tabs.length - 1)
12605				navButtons.filter('.next-link').addClass('disabled next-link-disabled');
12606		}
12607					
12608		function getContainer(tab) {
12609			var container = tab.data('container');
12610			if (!container) {
12611				container = $(tab.data('hash') + '-container');
12612				tab.data('container', container);
12613			};
12614			
12615			return container;
12616		}
12617		
12618		function hideTab(t) {
12619			var container = getContainer(t.removeClass('active'));
12620			if (options.useVisibility) {
12621				container.css({visibility: 'hidden'});
12622			} else {
12623				container.hide();
12624			}
12625			activeTab = $();
12626		}
12627		
12628		function showTab(t) {
12629			activeTab = t.addClass('active');
12630
12631			var container = getContainer(activeTab),
12632				profile = myDeco.settings.WL_PROFILE;
12633
12634			if (options.useVisibility) {
12635				container.css({visibility: 'visible'});
12636			} else {
12637				container.show();
12638			}
12639
12640			if (profile) {
12641				showDefaultCategory();
12642			}
12643		}
12644
12645		function showDefaultCategory() {
12646			var cat = myDeco.settings.WL.WL_DEFAULT_TAB_ID;
12647
12648			// We don't have a reference to the SearchController here so instead a search is forced by simulating a click on the appropriate category facet item.
12649			if (cat) {
12650				$('#' + cat).click();
12651			}
12652		}
12653		
12654		tabs.click(function(e, nohide) {
12655			var tab = $(this),
12656				tabHash = (tab.data('hash') || '').substring(1),
12657				selectedItem;
12658			e.stop();
12659			
12660			// don't want to hide pane if always and tab is active
12661			if (tab[0] == activeTab[0] || (!options.always && tab.hasClass('tab-hide'))) {
12662				if (options.always || nohide)
12663					return;
12664				else
12665					options.onHide(activeTab, hideTab);
12666			} else {
12667				if (activeTab.size() > 0) {
12668					hideTab(activeTab);
12669					showTab(tab);
12670				} else
12671					options.onShow(tab, showTab)
12672			}
12673			
12674			// update global tab index;
12675			index = tabs.index(this);
12676			
12677			switcher.trigger('tab', [tab]);
12678			
12679			updateNavLinks();
12680		});
12681		
12682		switcher
12683			.bind('show', function(e, tabClass) {
12684				tabs.filter('.' + tabClass).trigger('click', [true]);
12685			})
12686			.bind('hide', function(e) {
12687				options.onHide(activeTab, hideTab);
12688			});
12689	},
12690
12691	initTabSwitchers: function(switchers) {
12692		var self = this;
12693		return switchers.each(function() {
12694			self.initTabSwitcher($(this));
12695		});
12696	}
12697};
12698/**
12699 * @fileOverview List of keycodes
12700 */
12701var KEY = {
12702	Esc:		27,
12703	Delete:		46,
12704	Space:		32,
12705	BackSpace:	8,
12706
12707	Left:		37,
12708	Up:			38,
12709	Right:		39,
12710	Down:		40,
12711
12712	// keypad buttons
12713	Plus:		107,
12714	Minus:		109,
12715	// plus + equal button
12716	PlusEq:		61,
12717
12718	A:			65,
12719	B:			66,
12720	C:			67,
12721	D:			68,
12722	E:			69,
12723	F:			70,
12724	G:			71,
12725	H:			72,
12726	I:			73,
12727	J:			74,
12728	K:			75,
12729	L:			76,
12730	M:			77,
12731	N:			78,
12732	O:			79,
12733	P:			80,
12734	Q:			81,
12735	R:			82,
12736	S:			83,
12737	T:			84,
12738	U:			85,
12739	V:			86,
12740	W:			87,
12741	X:			88,
12742	Y:			89,
12743	Z:			90
12744};
12745
12746//same but in backward compability
12747var _KEY = {"65":"A","66":"B","67":"C","68":"D","69":"E","70":"F","71":"G","72":"H","73":"I","74":"J","75":"K","76":"L","77":"M","78":"N","79":"O","80":"P","81":"Q","82":"R","83":"S","84":"T","85":"U","86":"V", "87": "W", "88":"X","89":"Y","90":"Z"};
12748/*globals myDeco, $, Class, Matrix, T */
12749/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
12750          sloppy: true, white: true */
12751/**
12752 * @fileOverview Abstract point class for XYZ transformations
12753 */
12754var Point = Class.extend({
12755	/**
12756	 * Init function
12757	 * @param {Number|Array} x X coordinate or Point instance
12758	 * @param {Number} y Y coordinate
12759	 * @param {Number} z Z coordinate
12760	 */
12761	init: function(x) {
12762		var coords = typeof x === 'object' ? x : arguments;
12763		
12764		this.cache = {};
12765		this.x = coords[0] || 0;
12766		this.y = coords[1] || 0;
12767		this.z = coords[2] || 0;
12768	},
12769	
12770	/**
12771	 * Drop all or particular cached value
12772	 * @param {String} id Cache id
12773	 */
12774	dropCache: function(id) {
12775		if (id) {
12776			delete this.cache[id];
12777		} else {
12778			this.cache = {};
12779		}
12780		return this;
12781	},
12782
12783	/**
12784	 * Get point from the cache
12785	 * @param {String} id Cache id
12786	 * @returns {Point} Point from cache.
12787	 */
12788	getPointFromCache: function(id) {
12789		if (typeof id !== 'undefined' && this.cache[id]) {
12790			return new Point(this.cache[id]);
12791		} else {
12792			return false;
12793		}
12794	},
12795	
12796	/**
12797	 * Apply transformations from the matrix to this point
12798	 * @param {Transform} t Transform to apply.
12799	 * @param {String} cacheId Apply to point and store into cache with id = cacheId.
12800	 */
12801	applyTransformation: function(t, cacheId, dropCache) {
12802		if (typeof cacheId === 'undefined') {
12803			this.fromVector(t.matrix.multiply(this.toVector()));
12804			this.dropCache();
12805			
12806			return this;
12807		} else {
12808			if (!dropCache && this.cache[cacheId]) {
12809				return this.getPointFromCache(cacheId);
12810			}
12811
12812			var vector = t.matrix.multiply(this.toVector());
12813			this.cache[cacheId] = [vector[0][0], vector[1][0], vector[2][0]];
12814
12815			return this.getPointFromCache(cacheId);
12816		}
12817	},
12818	
12819	/**
12820	 * Convert to 4x1 vector, usable for matrix transformations. 
12821	 * @returns {Matrix}
12822	 */
12823	toVector: function() {
12824		var vector = new Matrix(4, 1);
12825		vector[0][0] = this.x;
12826		vector[1][0] = this.y;
12827		vector[2][0] = this.z;
12828		vector[3][0] = 1;
12829
12830		return vector;
12831	},
12832	
12833	/**
12834	 * Convert from 4x1 vector
12835	 * @param {Matrix} vector Vector to convert from
12836	 */
12837	fromVector: function(vector) {
12838		this.x = vector[0][0];
12839		this.y = vector[1][0];
12840		this.z = vector[2][0];
12841
12842		return this;
12843	},
12844
12845	/**
12846	 * Clone current point
12847	 * @returns {Point} cloned point
12848	 */
12849	clone: function() {
12850		return new Point([this.x, this.y, this.z]);
12851	},
12852
12853	/**
12854	 * Stringify point
12855	 * @returns {String}
12856	 */
12857	toString: function() {
12858		return ['Point(', this.x.toFixed(2), ', ', this.y.toFixed(2), ', ', this.z.toFixed(2), ')'].join('');
12859	},
12860
12861	/**
12862	 * Return point as array of XYZ values
12863	 * @param {Boolean} TwoD return only x & y coord
12864	 * @returns {Array}
12865	 */
12866	toArray: function(TwoD) {
12867		if (TwoD) {
12868			return [this.x, this.y];
12869		} else {
12870			return [this.x, this.y, this.z];
12871		}
12872	},
12873	
12874	/**
12875	 * Rotate point CW using basePoint as rotation center
12876	 * xoz is for x0y plane
12877	 * yoz is for y0z plane
12878	 * xoz is for x0z plane
12879	 * 
12880	 * @param {Point} basePoint Rotating center
12881	 * @param {Number} xoy x0y rotating angle
12882	 * @param {Number} yoz y0z rotating angle
12883	 * @param {Number} xoz x0z rotating angle
12884	 * @returns {Point} this
12885	 */
12886	rotate: function(basePoint, xoy, yoz, xoz) {
12887		basePoint = basePoint.clone();
12888		// translate point to 0,0,0
12889		this.translate(-basePoint.x, -basePoint.y, -basePoint.z);
12890		
12891		if (typeof yoz !== 'undefined' && yoz !== 0) {
12892			this.rotateAroundAxis('x', yoz);
12893		}
12894		
12895		if (typeof xoz !== 'undefined' && xoz !== 0) {
12896			this.rotateAroundAxis('y', xoz);
12897		}
12898		
12899		if (typeof xoy !== 'undefined' && xoy !== 0) { 
12900			this.rotateAroundAxis('z', xoy);
12901		}
12902
12903		// translate point back to source coordinates
12904		this.translate(basePoint);
12905		return this;
12906	},
12907
12908
12909	/**
12910	 * Rotating point around given axis (using 0,0,0 as rotation center)
12911	 * @param {String} axis Axis to rotate around (x, y, z);
12912	 * @param {Number} angle Angle in degrees to rotate on.
12913	 * @returns {Point} this
12914	 */
12915	rotateAroundAxis: function(axis, angle) {
12916		this.dropCache();
12917		var _cos = T.cos(angle), 
12918			_sin = T.sin(angle),
12919			temp,
12920			_t = {
12921				'z': {c1: 'x', c2: 'y'},
12922				'x': {c1: 'y', c2: 'z'},
12923				'y': {c1: 'x', c2: 'z'}
12924			}[axis];
12925			
12926		temp		= this[_t.c1] * _cos - this[_t.c2] * _sin;
12927		this[_t.c2] = this[_t.c1] * _sin + this[_t.c2] * _cos;
12928		this[_t.c1] = temp;
12929		
12930		return this;
12931	},
12932
12933	/**
12934	 * Scale point using basePoint as scale center
12935	 * @param {Point} basePoint Scaling center
12936	 * @param {Number} scaleX X Scaling factor
12937	 * @param {Number} scaleY Y Scaling factor
12938	 * @param {Number} scaleZ Z Scaling factor
12939	 * @returns {Point} this
12940	 */
12941	scale: function(basePoint, scaleX, scaleY, scaleZ) {
12942		basePoint = basePoint.clone();
12943		this.translate(-basePoint.x, -basePoint.y, -basePoint.z);
12944		this.scaleC(scaleX, scaleY, scaleZ);
12945		this.translate(basePoint);
12946		return this;
12947	},
12948
12949	/**
12950	 * Scale point using (0, 0, 0) as scale center
12951	 * @param {Number} scaleX X Scaling factor
12952	 * @param {Number} scaleY Y Scaling factor
12953	 * @param {Number} scaleZ Z Scaling factor
12954	 * @returns {Point} this
12955	 */
12956	scaleC: function(scaleX, scaleY, scaleZ) {
12957		this.dropCache();
12958		
12959		this.x *= scaleX;
12960		this.y *= (typeof scaleY === 'undefined' ? scaleX : scaleY);
12961		this.z *= (typeof scaleZ === 'undefined' ? scaleX : scaleZ);
12962
12963		return this;
12964	},
12965	
12966	/**
12967	 * Translate point using delta coords from another point or 3 delta numbers
12968	 * @param {Number|Point} delta X delta or delta object
12969	 * @param {Number} y Y delta
12970	 * @param {Number} z Z delta
12971	 * @returns {Point} this
12972	 */
12973	translate: function(delta, y, z) {
12974		this.dropCache();
12975		
12976		if (typeof delta === 'number' || typeof delta === 'undefined') {
12977			this.x += delta || 0;
12978			this.y += y || 0;
12979			this.z += z || 0;
12980		} else {
12981			this.x += delta.x;
12982			this.y += delta.y;
12983			this.z += delta.z;
12984		}
12985
12986		return this;
12987	},
12988	
12989	/**
12990	 * Get isometric projection of point (http://www.gamedev.net/reference/articles/article1269.asp)
12991	 * @param {Number} angle Angle of projection
12992	 * @returns {Point} this.
12993	 */
12994	isometric: function(isoAngle, rotAngle) {
12995		this.dropCache();
12996		
12997		this.scaleC(1, -1, T.cos(isoAngle)).rotateAroundAxis('z', rotAngle - 45).rotateAroundAxis('x', 90 - isoAngle);
12998		
12999		return this;
13000	},
13001
13002	/**
13003	 * Calculate distance between point to a point or line (2D, not absolute value)
13004	 * @param {Point|Array} point Point or array (for line) distance calculate to
13005	 * @param {String} coord Coordinate for measuring distance, will calculate distance
13006	 *                       in 3D if omitted. Can by any combination of x, y or z.
13007	 * @returns {Number} distance
13008	 */
13009	distance: function(point, coord) {
13010		var result, dx, dy;
13011		if (point instanceof Point) {
13012			coord = coord || 'xyz';
13013			result = Math.sqrt(
13014				(this.x - point.x) * (this.x - point.x) * +(coord.indexOf('x') !== -1) +
13015				(this.y - point.y) * (this.y - point.y) * +(coord.indexOf('y') !== -1) +
13016				(this.z - point.z) * (this.z - point.z) * +(coord.indexOf('z') !== -1)
13017			);
13018		} else {
13019			dx = point[1].x - point[0].x;
13020			dy = point[0].y - point[1].y;
13021			result = (dy * this.x + dx * this.y + point[0].x * point[1].y - point[1].x * point[0].y) / Math.sqrt(dx * dx + dy * dy);
13022		}
13023		return result;
13024	},
13025	
13026	/**
13027	 * Distance to segment in a x-y plane
13028	 * @param {Array} p Array of points for segment
13029	 * @returns {Number} distance
13030	 */
13031	distance2segment: function(p) {
13032		var t,
13033			dx = p[1].x - p[0].x,
13034			dy = p[1].y - p[0].y,
13035			dpx1 = this.x - p[0].x,
13036			dpx2 = this.x - p[1].x,
13037			dpy1 = this.y - p[0].y,
13038			dpy2 = this.y - p[1].y;
13039		
13040		// if point isn't between given calculate minimal distance to the points
13041		if ( (dpx1 * dx + dpy1 * dy) * (dpx2 * dx + dpy2 * dy) > 0) {
13042			t = Math.min(Math.pow(dpx1, 2) + Math.pow(dpy1, 2), Math.pow(dpx2, 2) + Math.pow(dpy2, 2));
13043		// else calculate distance to the line
13044		} else {
13045			t = Math.pow(dpx1 * dy - dpy1 * dx, 2) / (Math.pow(dx, 2) + Math.pow(dy, 2));
13046		}
13047		return Math.sqrt(t);
13048	},
13049	
13050	/**
13051	 * Find projection of a point to a line
13052	 * @param {Array} p Array of points representing line
13053	 * @returns {Point}
13054	 */
13055	projection2line: function (p) {
13056		var angle = p[0].angleTo(p[1]);
13057		return new Point(this.clone().rotate(p[0], -angle).x,
13058			p[0].y).rotate(p[0], angle);
13059	},
13060	
13061	/**
13062	 * Check if the point (with applied transform) is in the poly (with applied transform)
13063	 * @param {Array of Point} poly Polygon to check. 
13064	 * @param {Transform} transform Transform to apply to both (optional).
13065	 * @param {String} tName Transform name (optional).
13066	 * @returns {Boolean} presence
13067	 */
13068	inPoly: function(poly, transform, tName) {
13069		var p = this, polygon = poly, i, j, c = false, len = polygon.length;
13070
13071		if (typeof transform !== 'undefined') {
13072			polygon = [];
13073			
13074			for (i = 0; i < len; i++) {
13075				polygon.push(poly[i].applyTransformation(transform, tName));
13076			}
13077		}
13078		
13079		for (i = 0, j = len - 1; i < len; j = i++) {
13080			if (((polygon[i].y > p.y) !== (polygon[j].y > p.y)) && 
13081				(p.x < (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) /
13082					(polygon[j].y - polygon[i].y) + polygon[i].x)) {
13083				c = !c;
13084			}
13085		}
13086		return c;
13087	},
13088	
13089	/**
13090	 * Calculate and return normalized angle to given point
13091	 * @param {Point} point
13092	 * @param {Boolean} rough Return rough value without normalizing
13093	 *                        to 0...360 scope
13094	 */
13095	angleTo: function(point, rough) {
13096		var angle = T.atan2(this.y - point.y, this.x - point.x);
13097		return rough ? angle : (angle + 360) % 360;
13098	},
13099
13100	/**
13101	 * Replace coordinates with coordinates of given point
13102	 * @param {Point} point
13103	 */
13104	copy: function(point) {
13105		this.x = point.x;
13106		this.y = point.y;
13107		this.z = point.z;
13108	},
13109	
13110	/**
13111	 * Update point coordinates from given
13112	 * @param {Number} x
13113	 * @param {Number} y
13114	 * @param {Number} z
13115	 * @returns {Point}
13116	 */
13117	update: function(x, y, z) {
13118		this.x = x === undefined ? this.x : x;
13119		this.y = y === undefined ? this.y : y;
13120		this.z = z === undefined ? this.z : z;
13121		return this;
13122	}
13123});
13124/*globals myDeco, $, Point */
13125/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
13126          sloppy: true, white: true */
13127/**
13128 * @fileOverview Common drawing objects library 
13129 */
13130var ObjectsLibrary = {
13131	
13132	/**
13133	 * Drawing canvas
13134	 * @type {Object}
13135	 */
13136	_canvas: undefined,
13137	
13138	/**
13139	 * Default object settings
13140	 * @type {Object}
13141	 */
13142	_settings: {
13143		closeButton: {
13144			size: 20,
13145			img: '/delete-button.png'
13146		},
13147		infoButton: {
13148			size: 20,
13149			img: '/default.png'
13150		},
13151		duplicateButton: {
13152			size: 20,
13153			img: '/duplicate-button.png'
13154		},
13155		directionButton: {
13156			size: 20,
13157			img: '/direction-button.png'
13158		},
13159		hingeButton: {
13160			size: 20,
13161			img: '/hinge-button.png'
13162		},
13163		splitButton: {
13164			size: 20,
13165			img: '/split-button.png'
13166		},
13167		drawButton: {
13168			size: 20,
13169			img: '/draw-button.png'
13170		},
13171		objectRotate: {
13172			distance: 65
13173		},
13174		camera: {
13175			fov: 180 / 3.0,
13176			scaleFactor: 0.6,
13177			distance: 350
13178		},
13179		dragNotice: {
13180			'font-family': 'Georgia, Times New Roman',
13181			'fill': '#ffffff'
13182		}
13183	},
13184	
13185	/**
13186	 * Already initiated objects
13187	 * @type {Object}
13188	 */
13189	_objects: {},
13190	
13191	/**
13192	 * Init function
13193	 * @param {Object} canvas Drawing canvas
13194	 * @param {Object} settings Objects settings
13195	 * @returns {Object} this 
13196	 */
13197	init: function(canvas, settings) {
13198		this._settings.camera.fov = myDeco.App.settings.model.cameraFov;
13199		this._canvas = canvas;
13200		this._settings = $.extend(true, this._settings, settings || {});
13201		return this;
13202	},
13203	
13204	/**
13205	 * Get object by name
13206	 * @param {String} name Object name
13207	 * @param {Boolean} clone Will object copy insetead of referencing
13208	 * @returns {Object}
13209	 */
13210	get: function(name, clone) {
13211		clone = clone || false;
13212		name = '__' + name;
13213		
13214		if (typeof this._objects[name] !== 'undefined' && !clone) {
13215			return this._objects[name];
13216		}
13217		
13218		var object = this[name]();
13219		
13220		if (typeof this._objects[name] === 'undefined') {
13221			this._objects[name] = object;
13222		}
13223		
13224		return object;
13225	},
13226	
13227	
13228	// objects drawing functions
13229	
13230	_renderButton: function(settings) {
13231		// added 1 as the part of the image url as easiest way to cache them
13232		// on the CDN
13233		return this._canvas.image(myDeco.settings.STATIC_URL + '..' +
13234			settings.img, 0, 0, settings.size, settings.size).hide();
13235	},
13236
13237	__infoButton: function() {
13238		return this._renderButton(this._settings.infoButton);
13239	},
13240
13241	__closeButton: function() {
13242		return this._renderButton(this._settings.closeButton);
13243	},
13244
13245	__duplicateButton: function() {
13246		return this._renderButton(this._settings.duplicateButton);
13247	},
13248	
13249	__hingeButton: function() {
13250		return this._renderButton(this._settings.hingeButton);
13251	},
13252	
13253	__directionButton: function() {
13254		return this._renderButton(this._settings.directionButton);
13255	},
13256	
13257	__splitButton: function() {
13258		return this._renderButton(this._settings.splitButton);
13259	},
13260	
13261	__drawButton: function() {
13262		return this._renderButton(this._settings.drawButton);
13263	},
13264	
13265	__objectRotate: function() {
13266		var settings = this._settings.objectRotate,
13267			circleRadius = 30,
13268			arrowsRadius = 90,
13269			arrowStartX = 35,
13270			arrowStartY = 6,
13271			arrowEndX = 78,
13272			arrowEndY = 38,
13273			triangle = 45,
13274			tsin = Math.sin(Math.PI/3),
13275			tcos = Math.cos(Math.PI/3),
13276			triangleDY = triangle * tsin,
13277			baseDistance = settings.distance,
13278			baseArrow = 18,
13279			dist1 = 12,
13280			dist2 = 30,
13281			dist3 = 60,
13282			dist4 = 40;
13283	
13284		return this._canvas.set().push(
13285			// 0
13286			this._canvas.path({'stroke': 'white', 'stroke-width': 4}).hold()
13287				.moveTo(-arrowStartX, -arrowStartY)
13288				.arcTo(arrowsRadius, arrowsRadius, 0, 1, -arrowEndX, -arrowEndY).render().hide(),
13289			// 1
13290			this._canvas.path({'stroke': '#6b6b6b', 'stroke-width': 2}).hold()
13291				.moveTo(-arrowStartX, -arrowStartY)
13292				.arcTo(arrowsRadius, arrowsRadius, 0, 1, -arrowEndX, -arrowEndY).render().hide(),
13293			// 2
13294			this._canvas.circle(0, 0, circleRadius).attr({'stroke': '#ffffff', 'stroke-width': 4}).hide(),
13295			// 3
13296			this._canvas.circle(0, 0, circleRadius).attr({'stroke': '#6b6b6b', 'fill': '#dfe1e1', 'stroke-width': 2}).hide(),
13297			// 4
13298			this._canvas.path({'stroke': 'white', 'stroke-width': 4}).hold()
13299				.moveTo(arrowStartX, -arrowStartY)
13300				.arcTo(arrowsRadius, arrowsRadius, 0, 0, arrowEndX, -arrowEndY).render().hide(),
13301			// 5
13302			this._canvas.path({'stroke': '#6b6b6b', 'stroke-width': 2}).hold()
13303				.moveTo(arrowStartX, -arrowStartY)
13304				.arcTo(arrowsRadius, arrowsRadius, 0, 0, arrowEndX, -arrowEndY).render().hide(),
13305			// 6
13306			this._canvas.path({'stroke': 'white', 'stroke-width': 1, 'fill': '#6b6b6b'}).hold()
13307				.moveTo(arrowEndX - triangle / 2 * tsin, -arrowEndY - triangle / 2 * tcos)
13308				.lineTo(arrowEndX + triangle / 2 * tsin, -arrowEndY + triangle / 2 * tcos)
13309				.lineTo(arrowEndX + triangleDY * tcos, -arrowEndY - triangleDY * tsin)
13310				.andClose()
13311				.render().hide(),
13312			// 7
13313			this._canvas.path({'stroke': 'white', 'stroke-width': 1, 'fill': '#6b6b6b'}).hold()
13314				.moveTo(-arrowEndX + triangle / 2 * tsin, -arrowEndY - triangle / 2 * tcos)
13315				.lineTo(-arrowEndX - triangle / 2 * tsin, -arrowEndY + triangle / 2 * tcos)
13316				.lineTo(-arrowEndX - triangleDY * tcos, -arrowEndY - triangleDY * tsin)
13317				.andClose()
13318				.render().hide(),
13319			// 8
13320			this._canvas.path({'stroke': 'white', 'stroke-width': 4}).hold()
13321				.moveTo(- baseArrow, - baseDistance - baseArrow)
13322				.lineTo(0, - baseDistance)
13323				.lineTo(baseArrow, - baseDistance - baseArrow)
13324				.render().hide(),
13325			// 9
13326			this._canvas.path({'stroke': 'white', 'stroke-width': 4}).hold()
13327				.moveTo(0, -baseDistance - dist1)
13328				.lineTo(0, -baseDistance - dist1 - dist2)
13329				.render().hide(),
13330			// 10
13331			this._canvas.path({'stroke': 'white', 'stroke-width': 4}).hold()
13332				.moveTo(-dist4 / 2, -baseDistance - dist3)
13333				.lineTo( dist4 / 2, -baseDistance - dist3)
13334				.render().hide(),
13335			// 11
13336			this._canvas.path({'stroke': '#6b6b6b', 'stroke-width': 2}).hold()
13337				.moveTo(- baseArrow, - baseDistance - baseArrow)
13338				.lineTo(0, - baseDistance)
13339				.lineTo(baseArrow, - baseDistance - baseArrow)
13340				.render().hide(),
13341			// 12
13342			this._canvas.path({'stroke': '#6b6b6b', 'stroke-width': 2}).hold()
13343				.moveTo(0, -baseDistance - dist1)
13344				.lineTo(0, -baseDistance - dist1 - dist2)
13345				.render().hide(),
13346			// 13
13347			this._canvas.path({'stroke': '#6b6b6b', 'stroke-width': 2}).hold()
13348				.moveTo(-dist4 / 2, -baseDistance - dist3)
13349				.lineTo( dist4 / 2, -baseDistance - dist3)
13350				.render().hide()
13351		);
13352	},
13353
13354    camera_cone: function(vfov) {
13355		var baseP = new Point(),
13356			distance = this._settings.camera.distance,
13357            aspect = 16/9,
13358            hfov = 2 * Math.atan(aspect * Math.tan(vfov * T.d2r/2)) * T.r2d,
13359			fov1 = new Point(0, distance, 0).rotate(baseP, - hfov / 2),
13360			fov2 = fov1.clone().rotate(baseP , hfov);
13361
13362			return this._canvas.path({stroke: 'white', fill: 'white', opacity: 0.5})
13363				.hold()
13364				.lineTo(fov1.x, fov1.y)
13365				.lineTo(fov2.x, fov2.y)
13366				.andClose().render();
13367    },
13368	
13369	__camera: function() {
13370		// Camera dimensions (base, lens, lens hood, shutter button)
13371		var settings = this._settings.camera,
13372			factor = settings.scaleFactor,
13373			baseW = 175 * factor,
13374			baseH = 60  * factor,
13375			lensW = 57  * factor,
13376			lensH = 53  * factor,
13377			hoodW = 95  * factor,
13378			hoodH = 82  * factor,
13379			shutterD = 35  * factor,
13380			shutterO = 30  * factor,
13381			speckO = 3,
13382            
13383            hover = this.camera_cone(settings.fov),
13384			pseudo_base = this._canvas.rect(-25 * factor, -25 * factor, 50 * factor, 50 * factor)
13385                .attr({stroke: 'none', fill: 'none'}),
13386			base = this._canvas.set().push(
13387				// white outline for lens and lenshood
13388				this._canvas.path({stroke: 'white', 'stroke-width': 4}).hold()
13389					.moveTo(0, 0)
13390					.lineTo(-lensW / 2.0, 0)
13391					.lineTo(-lensW / 2.0, baseH / 2.0 + lensH)
13392					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH)
13393		
13394					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13395					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13396		
13397					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH)
13398					.lineTo( lensW / 2.0, baseH / 2.0 + lensH)
13399					.lineTo( lensW / 2.0, 0)
13400					.andClose()
13401					.render(),
13402					
13403				// Camera lens + hood
13404				this._canvas.path({stroke: '#5bb9e8', 'fill': '#5bb9e8'}).hold()
13405					.moveTo(0, 0)
13406					.lineTo(-lensW / 2.0, 0)
13407					.lineTo(-lensW / 2.0, baseH / 2.0 + lensH)
13408					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH)
13409
13410					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13411					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13412
13413					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH)
13414					.lineTo( lensW / 2.0, baseH / 2.0 + lensH)
13415					.lineTo( lensW / 2.0, 0)
13416					.andClose()
13417					.render(),
13418					
13419				// Speck on lens & hood
13420				this._canvas.path({'stroke': 'none', 'fill': '#b8e1ec'}).hold()
13421					.moveTo(-speckO, 0)
13422					.lineTo(-lensW / 2.0, 0)
13423					.lineTo(-lensW / 2.0, baseH / 2.0 + lensH)
13424					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH)
13425					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13426					.lineTo(-speckO, baseH / 2.0 + lensH + hoodH)
13427					.andClose()
13428					.render(),
13429
13430				// Black outline around lens & hood
13431				this._canvas.path({stroke: '#010101', 'fill': 'none'}).hold()
13432					.moveTo(0, 0)
13433					.lineTo(-lensW / 2.0, 0)
13434					.lineTo(-lensW / 2.0, baseH / 2.0 + lensH)
13435					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH)
13436
13437					.lineTo(-hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13438					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH + hoodH)
13439
13440					.lineTo( hoodW / 2.0, baseH / 2.0 + lensH)
13441					.lineTo( lensW / 2.0, baseH / 2.0 + lensH)
13442					.lineTo( lensW / 2.0, 0)
13443					.andClose()
13444					.render(),
13445					
13446				// white stroke around body
13447				this._canvas.rect(-baseW / 2.0, -baseH / 2.0, baseW, baseH)
13448					.attr({stroke:"#FFFFFF", 'stroke-width': "3", fill: 'none'}),
13449				
13450				// Body of the camera
13451				this._canvas.rect(-baseW / 2.0, -baseH / 2.0, baseW, baseH)
13452					.attr({fill: "#9bddf2", stroke:"#0A0B0A", 'stroke-width': "1"}),
13453					
13454				// shutter button
13455				this._canvas.circle(-baseW / 2.0 + shutterO, 0, shutterD / 2.0).attr({fill: "#ffffff", 'stroke': "none"})
13456			);
13457		return this._canvas.set().push(hover, pseudo_base, base.translate(0,-30));
13458	},
13459	
13460	__dragNotice: function() {
13461		return this._canvas.text(0, 0, gettext("Drag\nand drop\nfurniture\nhere"))
13462							.attr(this._settings.dragNotice)
13463							.attr('font-size', Math.round(18 / (this._canvas.raphael.type === 'SVG' ? this._canvas.zoom : 1)) + 'px')
13464							.hide();
13465	}
13466};
13467/*globals myDeco, $ */
13468/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
13469          sloppy: true, white: true */
13470var Stack = (function() {
13471	/**
13472	 * Subclassed JavaScript 1.5 Array for every browser.
13473	 * 
13474	 * @author Andrea Giammarchi
13475	 * @site devpro.it
13476	 * @blog webreflection.blogspot.com
13477	 * @date 2009/05/07
13478	 * @requires IE 5.5+ , FF 1+ , Opera 8+ , Safari 2+
13479	 * @license Mit Style
13480	 */
13481	function Stack() {
13482		this.push.apply(this, Array.apply(null, arguments));
13483	}
13484	
13485	Stack.prototype = [];
13486	Stack.prototype.length = 0;
13487	
13488	var split, length,
13489		toString = Object.prototype.toString,
13490		slice = Array.prototype.slice,
13491		concat = Array.prototype.concat;
13492		
13493	
13494	if (!new Stack(1).length) {
13495		Stack.prototype = {
13496			length : 0
13497		};
13498		for (split = ['join', 'pop', 'push', 'reverse', 'shift', 'slice',
13499				'sort', 'splice', 'unshift'], length = split.length; length !== 0; length--) {
13500			Stack.prototype[split[length - 1]] = Array.prototype[split[length - 1]];
13501		}
13502	}
13503	
13504	Stack.prototype.concat = function() {
13505		var args = Array.prototype.slice(arguments, 0),
13506			array = this.slice(0),
13507			length = arguments.length, i;
13508		for (i = 0; i < length; ++i) {
13509			if (toString.call(args[i]) !== "[object Array]") {
13510				args[i] = typeof args[i] === "object" ? slice.call(args[i]) : [ args[i] ];
13511			}
13512		}
13513		array.push.apply(array, concat.apply([], args));
13514		return array;
13515	};
13516	
13517	Stack.prototype.toString = Stack.prototype.join;
13518	Stack.prototype.constructor = Stack;
13519	return Stack;
13520	
13521}());
13522/*globals myDeco, $, Stack */
13523/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
13524          sloppy: true, white: true */
13525/**
13526 * Cloud type
13527 * 
13528 * @param {FpeView} view
13529 * @return {Cloud}
13530 */
13531var Cloud = function(view) {
13532	this._view = view;
13533};
13534
13535Cloud.prototype = new Stack();
13536
13537/**
13538 * Empty callback used when no callback passed to append method
13539 * @param {Object} value
13540 * @returns {Object}
13541 */
13542Cloud.prototype.emptyCallback = function(value) {
13543	return value;
13544};
13545
13546/**
13547 * Append a new instance to a cloud
13548 * @param {Array} source Array of source item arguments
13549 * @param {String} ClassName Item constructor class name
13550 * @param {Function} callback Callback for post-processing of item
13551 * @param {String} filterBy name of the field to avoid adding duplicates
13552 * 
13553 * @return {Cloud}
13554 */
13555Cloud.prototype.append = function(source, ClassName, callback, filterBy) {
13556	var i, ilen;
13557	
13558	callback = callback || this.emptyCallback;
13559	
13560	for (i = 0, ilen = source.length; i < ilen; i++) {
13561		if (!filterBy || this.findByField(filterBy, [source[i]]).length === 0) {
13562			Array.prototype.push.apply(this,
13563				[callback(new ClassName(this._view, source[i]))]);
13564		}
13565	}
13566	
13567	return this;
13568};
13569
13570/**
13571 * Grep cloud using custom function
13572 * @param {Function} func
13573 * 
13574 * @return {Cloud}
13575 */
13576Cloud.prototype.grep = function(func) {
13577	var result = new Cloud(this._view);
13578	
13579	Array.prototype.push.apply(result, $.grep(this, func));
13580	return result;
13581};
13582
13583/**
13584 * Find cloud items by particular value of the field
13585 * @param {String} field Name of the field
13586 * @param {Array} values List of values to compare with
13587 * 
13588 * @return {Cloud}
13589*/ 
13590
13591Cloud.prototype.findByField = function(field, values) {
13592	return this.grep(function(element) {
13593		return values.indexOf(element[field]) !== -1;
13594	});
13595};
13596
13597/**
13598 * Remove item from cloud
13599 * @param {Object} item Cloud item
13600 * 
13601 * @return {Array} List of removed items
13602 */
13603Cloud.prototype.remove = function(item) {
13604	var idx = Array.prototype.indexOf.call(this, item);
13605	return (idx !== -1) ?
13606		Array.prototype.splice.call(this, idx, 1) :
13607		[];
13608};
13609
13610/**
13611 * Batch processing on cloud. Pass parameters to method as a next to method n
13611ame arguments.
13612 * 
13613 * @param {Any} fname Function reference or cloud item method name
13614 * 
13615 * @return {Cloud}
13616 */
13617Cloud.prototype.batch = function(fname) {
13618	var params = Array.prototype.splice.call(arguments, 1, arguments.length - 1),
13619		i;
13620	
13621	for (i = this.length - 1; i > -1; i--) {
13622		// execute callback
13623		if ($.isFunction(fname)) {
13624			// insert item as a first element
13625			fname.apply(this[i], $.merge([this[i]], params));
13626		} else if ($.isFunction(this[i][fname])) {
13627			this[i][fname].apply(this[i], params);
13628		}
13629	}
13630	
13631	return this;
13632};
13633/*globals myDeco, $, jQuery, window */
13634/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
13635          sloppy: true, white: true */
13636/**
13637 * @fileOverview Misc functions
13638 */
13639
13640/**
13641 * Clone object properties except functions
13642 * @param {Object} obj Object to clone
13643 * @returns {Object} cloned object
13644 */
13645jQuery.cloneObject = function(obj) {
13646	var destination = {}, property, val;
13647	for (property in obj) {
13648		val = obj[property];
13649		if (!$.isFunction(val)) {
13650			destination[property] = $.isArray(val) ?
13651				val.slice(0, val.length) : val;
13652		}
13653	}
13654
13655	return destination;
13656};
13657
13658/**
13659 * Build thumbnail url based on image and settings
13660 * @param {String} image Source image
13661 * @param {Object} settings Converting settings
13662 * @returns {String} thumbnail url
13663 */
13664function buildThumbnailLink(image, settings) {
13665	if (!settings || !image) {
13666		return image;
13667	}
13668
13669	var postfix = '_',
13670		decodingTable = {
13671			'width': 'w',
13672			'height': 'h',
13673			'percent': 'c',
13674			'border': 'b',
13675			'shadow': 's',
13676			'offset': 'o',
13677			'mirror': 'm',
13678			'rotate': 'r',
13679			'shear': 'e',
13680			'y_wrapped_shear': 'f',
13681			'filled_rotation': 'g',
13682			'y_scale': 'y'
13683		},
13684		name;
13685
13686	// remove unnesessary options
13687	if (settings.filled_rotation === 0) {
13688		delete settings.filled_rotation;
13689	}
13690
13691	for (name in decodingTable) {
13692		if (typeof settings[name] !== 'undefined') {
13693      if (name == 'width' || name == 'height') {
13694        postfix += decodingTable[name] + "270";
13695      } else {
13696			  postfix += decodingTable[name] + settings[name];
13697      }
13698		}
13699	}
13700
13701	return image.replace(/cdb\/(renders|textures)/, 'cdb/scaled/$1') +
13702		 postfix + '.png';
13703}
13704
13705/**
13706 * Convert color hex representation to a array of rgb values
13707 * @param {String} hex Hex string like '#ff0044'
13708 * @return {Array}
13709 */
13710function hex2RGB(hex) {
13711	var rgb = [], offset = hex[0] === '#' ? 1 : 0;
13712	while (offset < hex.length) {
13713		rgb.push(parseInt(hex.substring(offset, offset += 2), 16));
13714	}
13715	return rgb;
13716}
13717
13718/**
13719 * Convert color hex representation to a array of intensivity values
13720 * @param {String} hex Hex string like '#ff0044'
13721 * @return {Array}
13722 */
13723function hex2Diffuse(hex) {
13724	if (!hex) {
13725		return false;
13726	}
13727	return $.map(hex2RGB(hex), function(val) { return val / 255 });
13728}
13729
13730/**
13731 * Convert decimal color representation to diffuse values
13732 * @param {Number} dec
13733 * @return {Array}
13734 */
13735function dec2Diffuse(dec) {
13736	return hex2Diffuse(parseInt(dec, 10).toString(16));
13737}
13738
13739/**
13740 * Convert array of intensivity values (0...1) to an array of rgb-values (0...255,0...255,0...255)
13741 * @param {Array} arr
13742 * @return {Array}
13743 */
13744function diffuse2RGBa(arr) {
13745	return $.map(arr, function(item) {
13746		return Math.floor(item * 255);
13747	});
13748}
13749
13750/**
13751 * Convert array of intensivity values (0...1) to a string of rgb-values (0...255,0...255,0...255)
13752 * @param {Array} arr
13753 * @return {String}
13754 */
13755function diffuse2RGB(arr) {
13756	return diffuse2RGBa(arr).join(',');
13757}
13758
13759/**
13760 * Convert array of intensivity values (0...1) to a string of hex color (00...FF,00...FF,00...FF)
13761 * @param {Array} arr
13762 * @return {String}
13763 */
13764function diffuse2Hex(arr) {
13765	return $.map(arr, function(item) {
13766		var v = Math.floor(item * 255).toString(16);
13767		if (v.length < 2) {
13768			v = '0' + v;
13769		}
13770		return v;
13771	}).join('');
13772}
13773
13774/**
13775 * Convert cm to feet and inches
13776 * @param {Boolean} flat Return flat representation in inches without units
13777 * @returns {String}
13778 */
13779Number.prototype.toFeet = function(flat) {
13780	var inches = Math.round(this / 2.54),
13781		feet = Math.floor(inches / 12);
13782	inches -= feet * 12;
13783	return flat ? feet * 12 + inches + '"': feet + "'" + inches + '"';
13784};
13785
13786/**
13787 * Converts cm² to square feet
13788 * @returns string
13789 */
13790Number.prototype.toSquareFeet = function() {
13791	var inches = Math.round(this / 6.4516),
13792		feet = Math.floor(inches / 144);
13793	inches -= feet * 144;
13794	return feet + "." + inches + ' sq.ft.';
13795};
13796
13797/**
13798 * Converts feet string representation to cm
13799 * @returns {Number}
13800 */
13801String.prototype.fromFeet = function() {
13802	var value = this
13803		.replace(/[^0-9'"]/g, '')
13804		.match(/(?:(\d+)')?(?:(\d+)")?/) || [];
13805	return (parseInt(value[1] || '0', 10) * 12 +
13806		parseInt(value[2] || '0', 10)) * 2.54;
13807};
13808
13809/**
13810 * Convert cm to meter notation
13811 * @param {Boolean} flat Return flat representation in meters without units
13812 * @returns {String}
13813 */
13814Number.prototype.toMeters = function(flat) {
13815	return (this / 100).toFixed(2) + (flat ? 0 : ' m');
13816};
13817
13818/**
13819 * Converts cm² to square feet
13820 * @returns {String}
13821 */
13822Number.prototype.toSquareMeters = function() {
13823	return (this / 10000).toFixed(2) + ' m²';
13824};
13825
13826/**
13827 * Convert meters back to cm
13828 * @returns {String}
13829 */
13830String.prototype.fromMeters = function() {
13831	return parseFloat(this.replace(/[^0-9.,]/g, '').replace(/,/, '.')) * 100;
13832};
13833
13834/**
13835 * Convert value to a currently selected unit
13836 * @param {Boolean} flat Return flat representation without units
13837 * @returns {String}
13838 */
13839Number.prototype.toViewUnits = function(flat) {
13840	return this['to' + myDeco.settings.UNITS_LIST[myDeco.settings.UNITS][1]]
13841		.call(this, flat);
13842};
13843
13844/**
13845 * Parse string using currently selected unit
13846 * @returns {Number}
13847 */
13848String.prototype.fromViewUnits = function() {
13849	return this['from' + myDeco.settings.UNITS_LIST[myDeco.settings.UNITS][1]]
13850		.call(this);
13851};
13852
13853/**
13854 * Convert square cm to square units
13855 * @returns {String}
13856 */
13857Number.prototype.toSquareUnits = function() {
13858	return this['toSquare' + myDeco.settings.UNITS_LIST[myDeco.settings.UNITS][1]]
13859		.call(this);
13860};
13861
13862/**
13863 * Parse units string according selected units to a list of values in cm
13864 * @returns {Array}
13865 */
13866String.prototype.fromUnits = function() {
13867	var func = 'from' +
13868		myDeco.settings.UNITS_LIST[myDeco.settings.UNITS][1];
13869	return $.map(this.split(/\s*x\s*/), function(value) {
13870		return value[func].call(value);
13871	});
13872
13873};
13874
13875/**
13876 * Convert array of values in cm to a string in current units
13877 * @param {Array} prefixes Prefixes before each value in string
13878 * @param {Boolean} flat Return flat representation in inches without units
13879 * @returns {String}
13880 */
13881Array.prototype.toUnits = function(prefixes, flat) {
13882	prefixes = prefixes || [];
13883
13884	return $.map(this, function(value, index) {
13885		return (typeof prefixes[index] !== 'undefined' ? prefixes[index] : '') +
13886			Number(value)['to' + myDeco.settings.UNITS_LIST[myDeco.settings.UNITS][1]]
13887				.call(value, flat);
13888	}).join(' x ');
13889
13890};
13891
13892/**
13893 * Function returning string representation of component dimensions
13894 * @param {Object} component
13895 * @returns {String}
13896 */
13897function toDimensionsString(component) {
13898	return [component.componentData.height, component.componentData.width,
13899		component.componentData.depth].toUnits(['H', 'W', 'D'], myDeco.settings.UNITS == 'ft');
13900}
13901
13902/**
13903 * Remove arrays in the response from WS (inplace)
13904 * @param {Object} data
13905 */
13906function unArray(data) {
13907	var name;
13908	for (name in data) {
13909		if (name !== 'bounding_box' && $.isArray(data[name])) {
13910			data[name] = data[name].shift();
13911		}
13912	}
13913	return data;
13914}
13915
13916/**
13917 * Get thumbnail URL from a image url
13918 * @param {String} image Image URL
13919 * @param {Number} width Desired thumbnail width, default is 70
13920 * @param {Number} height Desired thumbnail height, default is 70
13921 * @param {Number} quality Desired quality of jpeg image, default is 85
13922 * @returns {String}
13923 */
13924function appImage2Thumbnail(image, width, height, quality) {
13925	width = width || 70;
13926	height = height || 70;
13927	quality = quality || 85;
13928
13929  return myDeco.settings.MEDIA_URL + image;
13930	//var matches = image.match(/^.*?(scrapbook_images\/|rooms\/images\/)(.*)$/);
13931	//return matches ?
13932	//	myDeco.settings.MEDIA_URL + matches[1] + '_thumbs/' + matches[2] + '_' +
13933	//		width + 'x' + height + '_q' + quality + '.jpg' :
13934	//	image;
13935}
13936
13937/**
13938 * Generate object for jQ css method from the material entity
13939 * @param material
13940 * @param {Boolean} plain Return plain string if true
13941 * @returns {Object}
13942 */
13943function material2Style(material, plain) {
13944	var thumbnail = material.thumbnail || 'none',
13945		value;
13946
13947	if (thumbnail !== 'none') {
13948		if (!thumbnail.match(/^http/i)) {
13949			thumbnail = myDeco.settings.MATERIAL_MEDIA_URL + thumbnail;
13950		}
13951		thumbnail = 'url(' + thumbnail + ')';
13952	}
13953
13954	value = {
13955		'background-color': material.diffuse ?
13956			'rgb(' + diffuse2RGB(material.diffuse) + ')' : 'transparent',
13957		'background-image': thumbnail
13958	};
13959
13960	if (plain) {
13961		$.each(value, function(name, v) {
13962			value[name] = name + ': ' + v;
13963		});
13964		value = $.values(value).join('; ');
13965	}
13966
13967	return value;
13968}
13969
13970/**
13971 * Returns X coordinate of lines projection to OX coordinate
13972 * @param {Wall} a
13973 * @param {Wall} b
13974 * @returns {Number}
13975 */
13976function getIntersection(a, b) {
13977	var ax0 = Math.min(a[0].x, a[1].x),
13978		ax1 = Math.max(a[0].x, a[1].x),
13979		bx0 = Math.min(b[0].x, b[1].x),
13980		bx1 = Math.max(b[0].x, b[1].x);
13981
13982	if (ax1 > bx0 && ax0 < bx1) {
13983		return (Math.min(ax1, bx1) + Math.max(ax0, bx0)) / 2;
13984	}
13985}
13986
13987/**
13988 * Return Y coordinate for specified line and X coordinate
13989 * @param {Array} points Points for line
13990 * @param {Numher}
13991 */
13992function getY(points, x) {
13993	return points[0].y + (x - points[0].x) * (points[1].y - points[0].y) /
13994		(points[1].x - points[0].x);
13995}
13996
13997/**
13998 * Construct texture fill depending on given options
13999 * @param {Object} material Used material
14000 * @param {Object} options
14001 *                 Options are:
14002 *                 scale: 0...100
14003 *                 vskew: decimal
14004 *                 hskew: decimal
14005 *                 rotate: 0...359
14006 *                 defaultColor: [0.5, 0.5, 0.5]
14007 *                 noTexture: fill only with diffuse
14008 *
14009 * @return {String|Boolean} url if possible or false
14010 */
14011function textureFill(material, options) {
14012	options = options || {};
14013
14014	return (!options.noTexture && material.diffuse_texture) ?
14015		'url(' + buildThumbnailLink(myDeco.settings.MATERIAL_MEDIA_URL +
14016			material.diffuse_texture, options) + ')' :
14017		'#' + diffuse2Hex(material.diffuse || options.defaultColor ||
14018			[0.5, 0.5, 0.5]);
14019}
14020
14021/**
14022 * Extract root from absolute url
14023 * @param {String} url
14024 * @returns {String}
14025 */
14026function extractRootUrl(url) {
14027	return url.substring(0, url.indexOf('/', 8) + 1);
14028}
14029
14030/**
14031 * Function for updating material info in html blocks
14032 * @param {Object} material
14033 * @param {jQuery} holder Holder where material data is
14034 * @returns {jQuery}
14035 */
14036function updateMaterialInfo(material, holder) {
14037	var hasSupplier = !!material.supplier && material.supplier !== 'mydeco',
14038		supplier = holder.toggleClass('branded', !!material.branded).toggleClass('color', !!material.isColor)
14039			.find('.name').text(material.name || '').end()
14040			.find('.texture div').css(material2Style(material)).end()
14041			.find('.gts').attr('href', material.url || '').toggle(!!material.url).end()
14042			.find('.delete, .change').hide().filter(material.isColor ? '.change' : '.delete').show().end().end()
14043			.find('.supplier').toggle(hasSupplier);
14044
14045	if (hasSupplier) {
14046		supplier.find('a, span').text(material.supplier || '')
14047			.filter('a').toggle(!!material.supplier_url).attr('href', material.supplier_url || '').end()
14048			.filter('span').toggle(!material.supplier_url);
14049	}
14050
14051	return holder;
14052}
14053
14054/**
14055 * Function for check input values on input time
14056 * for numberInput plugin
14057 * @param {Event} e
14058 */
14059function numberInputKeyPress(e) {
14060	var input = $(this),
14061		value = input.val();
14062
14063	switch (e.which) {
14064		// special keys
14065		case 0:
14066			break;
14067
14068		// minus
14069		case 45:
14070			e.preventDefault();
14071			break;
14072
14073		// dot
14074		case 46:
14075			if (myDeco.settings.UNITS !== 'm' ||
14076					(e.which === 46 && value.indexOf('.') !== -1)) {
14077				e.preventDefault();
14078			}
14079			break;
14080
14081		// ' and "
14082		case 39:
14083		case 34:
14084			if (myDeco.settings.UNITS !== 'ft' || (
14085					(e.which === 39 && value.indexOf("'") !== -1) ||
14086					(e.which === 34 && value.indexOf('"') !== -1))) {
14087				e.preventDefault();
14088			}
14089			break;
14090
14091		// digits
14092		case 48: case 49: case 50: case 51: case 52: case 53:
14093		case 54: case 55: case 56: case 57:
14094
14095		// backspace
14096		case 8:
14097			break;
14098
14099		// deny any changes by default
14100		default:
14101			e.preventDefault();
14102	}
14103}
14104
14105/**
14106 * from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
14107 * Converts an RGB color value to HSL. Conversion formula
14108 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
14109 * Assumes r, g, and b are contained in the set [0, 255] and
14110 * returns h, s, and l in the set [0, 1].
14111 *
14112 * @param   color hex representation of an RGB color (RRGGBB)
14113 * @return  Array           The HSL representation
14114 */
14115function rgb2hsl(hex_color) {
14116    var diffuse = hex2Diffuse(hex_color);
14117    var r = diffuse[0], g = diffuse[1], b = diffuse[2];
14118    var max = Math.max(r, g, b), min = Math.min(r, g, b);
14119    var h, s, l = (max + min) / 2;
14120
14121    if(max == min){
14122        h = s = 0; // achromatic
14123    }else{
14124        var d = max - min;
14125        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
14126        switch(max){
14127            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
14128            case g: h = (b - r) / d + 2; break;
14129            case b: h = (r - g) / d + 4; break;
14130        }
14131        h /= 6;
14132    }
14133
14134    return [h, s, l];
14135}
14136
14137/**
14138 * Converts an HSL color value to RGB. Conversion formula
14139 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
14140 * Assumes h, s, and l are contained in the set [0, 1] and
14141 * returns r, g, and b in the set [0, 1].
14142 *
14143 * @param   Number  h       The hue
14144 * @param   Number  s       The saturation
14145 * @param   Number  l       The lightness
14146 * @return  Array           The RGB representation
14147 */
14148function hsl2rgb(h, s, l){
14149    var r, g, b;
14150
14151    if(s == 0){
14152        r = g = b = l; // achromatic
14153    }else{
14154        function hue2rgb(p, q, t){
14155            if(t < 0) t += 1;
14156            if(t > 1) t -= 1;
14157            if(t < 1/6) return p + (q - p) * 6 * t;
14158            if(t < 1/2) return q;
14159            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
14160            return p;
14161        }
14162
14163        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
14164        var p = 2 * l - q;
14165        r = hue2rgb(p, q, h + 1/3);
14166        g = hue2rgb(p, q, h);
14167        b = hue2rgb(p, q, h - 1/3);
14168    }
14169
14170    return [r, g, b];
14171}
14172
14173/* palette index number (in hex) to hsl [0..1]. rows, cols - number of colors in the whole palette */
14174function pal2hsl(number, rows, cols) {
14175    var h,s,l,p=parseInt(number, 16);
14176    var rows = 9, cols = 12;
14177    var hstep = 1/cols, vstep = 1/rows;
14178
14179    if (p < rows * cols) {
14180        h = (p % cols) * hstep + hstep / 2;
14181        s = Math.floor(p / cols) * vstep + vstep / 2;
14182        l = 0.5;
14183    } else { // the last row is grayscale
14184        h = s = 0;
14185        l = (p % cols) * hstep + hstep / 2;
14186    }
14187
14188    return [h,s,l];
14189};
14190/*globals myDeco, $ */
14191/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
14192          sloppy: true, white: true */
14193/**
14194 * @fileOverview Trigonometry lib for dealing with degrees not radians
14195 */
14196
14197var T = {
14198	/**
14199	 * Degrees to radoans converting factor
14200	 * @type {Number}
14201	 */
14202	d2r: Math.PI / 180.0,
14203	
14204	/**
14205	 * Radians to degrees converting factor
14206	 * @type {Number}
14207	 */
14208	r2d: 180.0 / Math.PI,
14209	
14210	/**
14211	 * Angle cosinus
14212	 * @param {Number} a Angle
14213	 * @returns {Number} Value
14214	 */
14215	cos: function(a) {
14216		return Math.cos(a * this.d2r);
14217	},
14218	
14219	/**
14220	 * Angle sinus
14221	 * @param {Number} a Angle
14222	 * @returns {Number} Value
14223	 */
14224	sin: function(a) {
14225		return Math.sin(a * this.d2r);
14226	},
14227	
14228	/**
14229	 * Angle tangens
14230	 * @param {Number} a Angle
14231	 * @returns {Number} Value
14232	 */
14233	tan: function(a) {
14234		return Math.tan(a * this.d2r);
14235	},
14236	
14237	/**
14238	 * Angle arcsinus
14239	 * @param {Number} a Angle
14240	 * @returns {Number} Value
14241	 */
14242	asin: function(a) {
14243		return Math.asin(a) * this.r2d;
14244	},
14245	
14246	/**
14247	 * Angle arccosinus
14248	 * @param {Number} a Value
14249	 * @returns {Number} Angle
14250	 */
14251	acos: function(a) {
14252		return Math.acos(a) * this.r2d;
14253	},
14254	
14255	/**
14256	 * Angle arctangens
14257	 * @param {Number} a Value
14258	 * @returns {Number} Angle
14259	 */
14260	atan: function(a) {
14261		return Math.atan(a) * this.r2d;
14262	},
14263	
14264	/**
14265	 * Angle arctangens with sector support
14266	 * @param {Number} a Value
14267	 * @returns {Number} Angle
14268	 */
14269	atan2: function(a, b) {
14270		return Math.atan2(a, b) * this.r2d;
14271	}
14272};
14273/*
14274 * Raphael 0.8.6 - JavaScript Vector Library
14275 *
14276 * Copyright (c) 2008 - 2009 Dmitry Baranovskiy (http://raphaeljs.com)
14277 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
14278 */
14279
14280
14281window.Raphael = (function() {
14282	// indexOf implementation for IE browsers.
14283	if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
14284	  i || (i = 0);
14285	  var length = this.length;
14286	  if (i < 0) i = length + i;
14287	  for (; i < length; i++)
14288	    if (this[i] === item) return i;
14289	  return -1;
14290	};
14291	
14292    var separator = /[, ]+/,
14293        doc = document,
14294        win = window,
14295        oldRaphael = {
14296            was: "Raphael" in window,
14297            is: window.Raphael
14298        },
14299        R = function() {
14300            return create.apply(R, arguments);
14301        },
14302        paper = {},
14303        availableAttrs = {cx: 0, cy: 0, fill: "#fff", "fill-opacity": 1, font: '10px "Arial"', "font-family": '"Arial"', "font-size": "10", "font-style": "normal", "font-weight": 400, gradient: 0, height: 0, href: "http://raphaeljs.com/", opacity: 1, path: "M0,0", r: 0, rotation: 0, rx: 0, ry: 0, scale: "1 1", src: "", stroke: "#000", "stroke-dasharray": "", "stroke-linecap": "butt", "stroke-linejoin": "butt", "stroke-miterlimit": 0, "stroke-opacity": 1, "stroke-width": 1, target: "_blank", "text-anchor": "middle", title: "Raphael", translation: "0 0", width: 0, x: 0, y: 0},
14304        availableAnimAttrs = {cx: "number", cy: "number", fill: "colour", "fill-opacity": "number", "font-size": "number", height: "number", opacity: "number", path: "path", r: "number", rotation: "csv", rx: "number", ry: "number", scale: "csv", stroke: "colour", "stroke-opacity": "number", "stroke-width": "number", translation: "csv", width: "number", x: "number", y: "number"},
14305        events = ["click", "dblclick", "mousedown", "mousemove", "mouseout", "mouseover", "mouseup", "touchstart", "touchmove", "touchend", "orientationchange", "touchcancel", "gesturestart", "gesturechange", "gestureend"],
14306        supportsTouch = "createTouch" in doc,
14307        isGecko = navigator.userAgent.indexOf('Gecko') >= 0,
14308        isChrome = navigator.userAgent.indexOf('Chrome') !== -1;
14309
14310
14311    R.version = "0.8.6";
14312    R.type = (window.SVGAngle || document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
14313    R.svg = !(R.vml = R.type == "VML");
14314    R.idGenerator = 0;
14315    R.fn = {};
14316    R.isArray = function (arr) {
14317        return Object.prototype.toString.call(arr) == "[object Array]";
14318    };
14319    R.setWindow = function (newwin) {
14320        win = newwin;
14321        doc = win.document;
14322    };
14323    R.extend = function(destination, source) {
14324        for (var property in source)
14325            destination[property] = source[property];
14326        return destination;
14327    };
14328    R.formatCoord =function(num) {
14329        return  R.svg
14330                ? num.toFixed(3)
14331                : Math.round(num) - 1;
14332    };
14333    
14334    var getRGBcache = {},
14335        getRGBcount = [];
14336    R.getRGB = function (colour) {
14337        if (colour in getRGBcache) {
14338            return getRGBcache[colour];
14339        }
14340        var htmlcolors = {aliceblue: "#f0f8ff", amethyst: "#96c", antiquewhite: "#faebd7", aqua: "#0ff", aquamarine: "#7fffd4", azure: "#f0ffff", beige: "#f5f5dc", bisque: "#ffe4c4", black: "#000", blanchedalmond: "#ffebcd", blue: "#00f", blueviolet: "#8a2be2", brown: "#a52a2a", burlywood: "#deb887", cadetblue: "#5f9ea0", chartreuse: "#7fff00", chocolate: "#d2691e", coral: "#ff7f50", cornflowerblue: "#6495ed", cornsilk: "#fff8dc", crimson: "#dc143c", cyan: "#0ff", darkblue: "#00008b", darkcyan: "#008b8b", darkgoldenrod: "#b8860b", darkgray: "#a9a9a9", darkgreen: "#006400", darkkhaki: "#bdb76b", darkmagenta: "#8b008b", darkolivegreen: "#556b2f", darkorange: "#ff8c00", darkorchid: "#9932cc", darkred: "#8b0000", darksalmon: "#e9967a", darkseagreen: "#8fbc8f", darkslateblue: "#483d8b", darkslategray: "#2f4f4f", darkturquoise: "#00ced1", darkviolet: "#9400d3", deeppink: "#ff1493", deepskyblue: "#00bfff", dimgray: "#696969", dodgerblue: "#1e90ff", firebrick: "#b22222", floralwhite: "#fffaf0", forestgreen: "#228b22", fuchsia: "#f0f", gainsboro: "#dcdcdc", ghostwhite: "#f8f8ff", gold: "#ffd700", goldenrod: "#daa520", gray: "#808080", green: "#008000", greenyellow: "#adff2f", honeydew: "#f0fff0", hotpink: "#ff69b4", indianred: "#cd5c5c", indigo: "#4b0082", ivory: "#fffff0", khaki: "#f0e68c", lavender: "#e6e6fa", lavenderblush: "#fff0f5", lawngreen: "#7cfc00", lemonchiffon: "#fffacd", lightblue: "#add8e6", lightcoral: "#f08080", lightcyan: "#e0ffff", lightgoldenrodyellow: "#fafad2", lightgreen: "#90ee90", lightgrey: "#d3d3d3", lightpink: "#ffb6c1", lightsalmon: "#ffa07a", lightsalmon: "#ffa07a", lightseagreen: "#20b2aa", lightskyblue: "#87cefa", lightslategray: "#789", lightsteelblue: "#b0c4de", lightyellow: "#ffffe0", lime: "#0f0", limegreen: "#32cd32", linen: "#faf0e6", magenta: "#f0f", maroon: "#800000", mediumaquamarine: "#66cdaa", mediumblue: "#0000cd", mediumorchid: "#ba55d3", mediumpurple: "#9370db", mediumseagreen: "#3cb371", mediumslateblue: "#7b68ee", mediumslateblue: "#7b68ee", mediumspringgreen: "#00fa9a", mediumturquoise: "#48d1cc", mediumvioletred: "#c71585", midnightblue: "#191970", mintcream: "#f5fffa", mistyrose: "#ffe4e1", moccasin: "#ffe4b5", navajowhite: "#ffdead", navy: "#000080", oldlace: "#fdf5e6", olive: "#808000", olivedrab: "#6b8e23", orange: "#ffa500", orangered: "#ff4500", orchid: "#da70d6", palegoldenrod: "#eee8aa", palegreen: "#98fb98", paleturquoise: "#afeeee", palevioletred: "#db7093", papayawhip: "#ffefd5", peachpuff: "#ffdab9", peru: "#cd853f", pink: "#ffc0cb", plum: "#dda0dd", powderblue: "#b0e0e6", purple: "#800080", red: "#f00", rosybrown: "#bc8f8f", royalblue: "#4169e1", saddlebrown: "#8b4513", salmon: "#fa8072", sandybrown: "#f4a460", seagreen: "#2e8b57", seashell: "#fff5ee", sienna: "#a0522d", silver: "#c0c0c0", skyblue: "#87ceeb", slateblue: "#6a5acd", slategray: "#708090", snow: "#fffafa", springgreen: "#00ff7f", steelblue: "#4682b4", tan: "#d2b48c", teal: "#008080", thistle: "#d8bfd8", tomato: "#ff6347", turquoise: "#40e0d0", violet: "#ee82ee", wheat: "#f5deb3", white: "#fff", whitesmoke: "#f5f5f5", yellow: "#ff0", yellowgreen: "#9acd32"},
14341        res;
14342        if ((colour + "").toLowerCase() in htmlcolors) {
14343            colour = htmlcolors[colour.toString().toLowerCase()];
14344        }
14345        if (!colour) {
14346            return {r: 0, g: 0, b: 0, hex: "#000"};
14347        }
14348        if (colour == "none") {
14349            return {r: -1, g: -1, b: -1, hex: "none"};
14350        }
14351        var red,
14352            green,
14353            blue,
14354            rgb = (colour + "").match(/^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgb\(\s*([\d\.]+\s*,\s*[\d\.]+\s*,\s*[\d\.]+)\s*\)|rgb\(\s*([\d\.]+%\s*,\s*[\d\.]+%\s*,\s*[\d\.]+%)\s*\)|hsb\(\s*([\d\.]+\s*,\s*[\d\.]+\s*,\s*[\d\.]+)\s*\)|hsb\(\s*([\d\.]+%\s*,\s*[\d\.]+%\s*,\s*[\d\.]+%)\s*\))\s*$/i);
14355        if (rgb) {
14356            if (rgb[2]) {
14357                blue = parseInt(rgb[2].substring(5), 16);
14358                green = parseInt(rgb[2].substring(3, 5), 16);
14359                red = parseInt(rgb[2].substring(1, 3), 16);
14360            }
14361            if (rgb[3]) {
14362                blue = parseInt(rgb[3].substring(3) + rgb[3].substring(3), 16);
14363                green = parseInt(rgb[3].substring(2, 3) + rgb[3].substring(2, 3), 16);
14364                red = parseInt(rgb[3].substring(1, 2) + rgb[3].substring(1, 2), 16);
14365            }
14366            if (rgb[4]) {
14367                rgb = rgb[4].split(/\s*,\s*/);
14368                red = parseFloat(rgb[0]);
14369                green = parseFloat(rgb[1]);
14370                blue = parseFloat(rgb[2]);
14371            }
14372            if (rgb[5]) {
14373                rgb = rgb[5].split(/\s*,\s*/);
14374                red = parseFloat(rgb[0]) * 2.55;
14375                green = parseFloat(rgb[1]) * 2.55;
14376                blue = parseFloat(rgb[2]) * 2.55;
14377            }
14378            var rgb = {r: red, g: green, b: blue},
14379                r = Math.round(red).toString(16),
14380                g = Math.round(green).toString(16),
14381                b = Math.round(blue).toString(16);
14382            (r.length == 1) && (r = "0" + r);
14383            (g.length == 1) && (g = "0" + g);
14384            (b.length == 1) && (b = "0" + b);
14385            rgb.hex = "#" + r + g + b;
14386            res = rgb;
14387        } else {
14388            res = {r: -1, g: -1, b: -1, hex: "none"};
14389        }
14390        if (getRGBcount.length > 20) {
14391            delete getRGBcache[getRGBcount.unshift()];
14392        }
14393        getRGBcount.push(colour);
14394        getRGBcache[colour] = res;
14395        return res;
14396    };
14397
14398    // path utilities
14399    
14400    var paramsMap = {
14401        'm': ['x', 'y'],
14402        'l': ['x', 'y'],
14403        'z': [],
14404        'a': ['rx', 'ry', 'large_arc_flag', 'sweep_flag', 'x', 'y'],
14405        'cp': ['x', 'y', 'w'],
14406        'c': ['x1', 'y1', 'x2', 'y2', 'x', 'y'],
14407        's': ['x2', 'y2', 'x', 'y'],
14408        'q': ['x1', 'y1', 'x', 'y'],
14409        't': ['x', 'y']
14410    };
14411    var vmlToSvgMap = {
14412        'M': 'm',
14413        'm': 't',
14414        'L': 'l',
14415        'l': 'r',
14416        'z': 'x',
14417        'CP': 'c',
14418        'cp': 'v',
14419        'C': 'c',
14420        'c': 'v',
14421        'S': 'c',
14422        's': 'v',
14423        'Q': 'c',
14424        'q': 'v',
14425        'T': 'c',
14426        't': 'v'
14427    };
14428
14429    var PathPoint = function(path, type) {
14430        this.type = path.isAbsolute && type !='z' ? type.toUpperCase() : type;
14431        this.path = path;
14432        this._cached = false;
14433        
14434        this.dropCache = function() {
14435            this._cached = false;
14436            return this;
14437        };
14438        
14439        var map = paramsMap[type.toLowerCase()],
14440            data = Array.prototype.splice.call(arguments, 2, arguments.length-2),
14441            len = data.length,
14442            i;
14443
14444        for (i = 0; i < len; i++)
14445            this[map[i]] = parseFloat(data[i]);
14446
14447        this.toString = function() {
14448            var type = R.svg ? this.type : vmlToSvgMap[this.type];
14449            
14450            if (!this._cached)
14451                switch(this.type.toLowerCase()) {
14452                    case 'm':
14453                    case 'l':
14454                        this._cached = type + [R.formatCoord(this.x), R.formatCoord(this.y)].join(' ');
14455                    break;
14456                    // Workaround for cpline shorthand.
14457                    case 'cp':
14458                        if (!this.prev)
14459                            path.pathPoints._getAbsoluteCoords(this, true);
14460
14461                        var type = R.svg ? type[0] : type;
14462                            
14463                        this._cached = type + [ R.formatCoord(this.prev.x + this.w), R.formatCoord(this.prev.y), 
14464                                                R.formatCoord(this.x - this.w), R.formatCoord(this.y), 
14465                                                R.formatCoord(this.x), R.formatCoord(this.y)].join(' ');
14466                    break;
14467                    case 'c':
14468                        this._cached = type + [ R.formatCoord(this.x1), R.formatCoord(this.y1), 
14469                                                R.formatCoord(this.x2), R.formatCoord(this.y2), 
14470                                                R.formatCoord(this.x), R.formatCoord(this.y)].join(' ');
14471                    break;
14472                    case 's':
14473                        if (!this.prev)
14474                            path.pathPoints._getAbsoluteCoords(this, true);
14475
14476                        this._cached = type + (R.vml 
14477                                                    ? R.formatCoord(this.prev.x * (path.isAbsolute + 1) - this.prev.bx) + " " + 
14478                                                      R.formatCoord(this.prev.y * (path.isAbsolute + 1) - this.prev.by) + " "
14479                                                    : '') +
14480                                            [ R.formatCoord(this.x2), R.formatCoord(this.y2), 
14481                                              R.formatCoord(this.x), R.formatCoord(this.y)].join(' ');
14482                    break;
14483                    case 'q':
14484                        if (!this.prev)
14485                            path.pathPoints._getAbsoluteCoords(this, true);
14486
14487                        this._cached = type + (R.svg
14488                             ? [R.formatCoord(this.x1), R.formatCoord(this.y1), 
14489                                                    R.formatCoord(this.x), R.formatCoord(this.y)].join(' ')
14490                             : [R.formatCoord(2 / 3 * this.x1 + 1 / 3 * this.prev.x),
14491                                R.formatCoord(2 / 3 * this.y1 + 1 / 3 * this.prev.y),
14492                                R.formatCoord(2 / 3 * this.x1 + 1 / 3 * this.x),
14493                                R.formatCoord(2 / 3 * this.y1 + 1 / 3 * this.y),
14494                                R.formatCoord(this.x),
14495                                R.formatCoord(this.y)].join(' '));
14496                    break;
14497                    case 't':
14498                        if (!this.prev)
14499                            path.pathPoints._getAbsoluteCoords(this, true);
14500                        // TBD: correct implementation for IE
14501                        this._cached = type + (R.svg
14502                            ? [R.formatCoord(this.x), R.formatCoord(this.y)].join(' ')
14503                            : [ R.formatCoord(2 / 3 * (this.prev.x - this.prev.qx) + 1 / 3 * this.prev.x),
14504                                R.formatCoord(2 / 3 * (this.prev.y - this.prev.qy) + 1 / 3 * this.prev.y),
14505                                R.formatCoord(2 / 3 * this.prev.qx + 1 / 3 * this.x),
14506                                R.formatCoord(2 / 3 * this.prev.qy + 1 / 3 * this.y),
14507                                R.formatCoord(this.x),
14508                                R.formatCoord(this.y)].join(' '));
14509                    break;
14510                    
14511                    case 'z':
14512                        this._cached = type;
14513                    break;
14514                    case 'a':
14515                        if (R.svg)
14516                            this._cached = type + [ R.formatCoord(this.rx), R.formatCoord(this.ry),
14517                                                    0, this.large_arc_flag, this.sweep_flag,
14518                                                    R.formatCoord(this.x), R.formatCoord(this.y)].join(' ');
14519                        else {
14520                            if (!this.prev)
14521                                path.pathPoints._getAbsoluteCoords(this, true);
14522
14523                            type = this.sweep_flag ? (path.isAbsolute ? "wa" : "wa") : (path.isAbsolute ? "at" : "at");
14524
14525                            var x22 = (path.isAbsolute ? 0 : this.prev.x) + this.x,
14526                                y22 = (path.isAbsolute ? 0 : this.prev.y) + this.y,
14527                                x1 = this.prev.x,
14528                                y1 = this.prev.y,
14529                                x = (x1 - x22) / 2,
14530                                y = (y1 - y22) / 2,
14531                                rxS = this.rx * this.rx,
14532                                ryS = this.ry * this.ry,
14533                                xS = x * x,
14534                                yS = y * y,
14535                                k = (this.large_arc_flag == this.sweep_flag ? -1 : 1) *
14536                                    Math.sqrt(Math.abs(rxS * ryS - rxS * yS - ryS * xS) / (rxS * yS + ryS * xS)),
14537                                cx = k * this.rx * y / this.ry + (x1 + x22) / 2,
14538                                cy = k * -this.ry * x / this.rx + (y1 + y22) / 2,
14539                                left = cx - this.rx,
14540                                top = cy - this.ry;
14541
14542                                this._cached = type + [ R.formatCoord(left), R.formatCoord(top), 
14543                                                        R.formatCoord(left + this.rx * 2), R.formatCoord(top + this.ry * 2), 
14544                                                        R.formatCoord(x1), R.formatCoord(y1), 
14545                                                        R.formatCoord(x22), R.formatCoord(y22)].join(" ");
14546                        }
14547                    break;
14548                }
14549
14550            return this._cached;
14551        }
14552    };
14553    
14554
14555    var pointsMethods = {
14556        getPathString: function() {
14557            return this.join(' ');
14558        },
14559
14560        _checkFirstPoint: function() {
14561			if (!this.bb)
14562				this.bb = {x1: Number.MAX_VALUE, y1: Number.MAX_VALUE, x2: -Number.MAX_VALUE, y2: -Number.MAX_VALUE};
14563            
14564            if (this.length == 0)
14565                this.push(new PathPoint(this.path, 'M', 0, 0));
14566            else if (this[0].type.toUpperCase() != 'M')
14567                this.unshift(new PathPoint(this.path, 'M', 0, 0));
14568        },
14569
14570        _indexInBounds: function(index) {
14571            return (index >= 0 && index < this.length ||
14572                    index < 0 && -index <= this.length);
14573        },
14574
14575        find: function(point) {
14576            if (typeof Array.prototype.indexOf != 'undefined')
14577                return this.indexOf(point);
14578            else {
14579                var i, len = this.length;
14580                for (i = 0; i < len; i++)
14581                    if (this[i] === point)
14582                        return i;
14583            }
14584            return -1;
14585        },
14586
14587        deletePoint: function(point) {
14588            if (typeof point == 'object') {
14589                point = this.find(point);
14590
14591                if (point == -1)
14592                    return false;
14593            }
14594            
14595            if (this._indexInBounds(point)) {
14596                var deleted = this.splice(point, 1).shift(),
14597                    path = deleted.path;
14598
14599                deleted.path = ''; // preventing some memleaks (I hope).
14600                this._checkFirstPoint();
14601                this._updateCoords();
14602                path.render();
14603                return deleted;
14604            }
14605            
14606            return false;
14607        },
14608
14609        insertBefore: function(point, newPoint, after) {
14610            if (typeof point == 'object') {
14611                point = this.find(point);
14612
14613                if (point == -1)
14614                    return false;
14615            }
14616            
14617            if (this._indexInBounds(point)) {
14618                this.splice(point + (typeof after == 'undefined' ? 0 : 1), 0, newPoint);
14619                this._checkFirstPoint();
14620                this._updateCoords();
14621                path.render();
14622            }
14623        },
14624
14625        insertAfter: function(point, newPoint) {
14626            this.insertBefore(point, newPoint, true);
14627        },
14628
14629        add: function(newPoint) {
14630            var num = this.push(newPoint);
14631        	this._checkFirstPoint();
14632            this._getAbsoluteCoords(num - 1, true);
14633        },
14634
14635        _getAbsoluteCoords: function(point, recalculate) {
14636            if (typeof point == 'object') {
14637                point = this.find(point);
14638
14639                if (point == -1)
14640                    return false;
14641            }
14642            // Dummy for cases when first point aren't M type.
14643            if (point == -1)
14644                return {'x': 0, 'y': 0, 'bx': 0, 'by': 0, 'qx': 0, 'qy': 0};
14645
14646            if (this._indexInBounds(point)) {
14647                if (typeof this[point].abs !='undefined' && !recalculate)
14648                    return this[point].abs;
14649                else {
14650                    var curr = this[point];
14651                    var prev = 1;
14652                    if (this[point - prev] && this[point - prev].type.toLowerCase() === 'z') {
14653                        prev++;
14654                    }
14655                    
14656                    curr.prev = this._getAbsoluteCoords(point - prev);
14657
14658                    // if we have relative point
14659                    var relative = (curr.type != curr.type.toUpperCase() && curr.type != 'z');
14660                    
14661                    switch (curr.type.toLowerCase()) {
14662                        case 's':
14663                        case 'c':
14664                            curr.abs = {'x': curr.prev.x * relative + curr.x, 'y': curr.prev.y * relative + curr.y, 
14665                                        'bx': curr.prev.x * R.vml * relative + curr.x2, 'by': curr.prev.y * R.vml * relative + curr.y2, 
14666                                        'qx': 0, 'qy': 0};
14667                        break;
14668
14669                        case 'q':
14670                            curr.abs = {'x': curr.prev.x * relative + curr.x, 'y': curr.prev.y * relative + curr.y, 
14671                                        'bx': 0, 'by': 0, 
14672                                        'qx': curr.prev.x * R.vml * relative + curr.x1, 'qy': curr.prev.y * R.vml * relative + curr.y1};
14673                        break;
14674
14675                        case 'a':
14676                        case 't':
14677                        case 'm':
14678                        case 'l':
14679                            curr.abs = {'x': curr.prev.x * relative + curr.x, 'y': curr.prev.y * relative + curr.y, 
14680                                        'bx': 0, 'by': 0, 'qx': 0, 'qy': 0};
14681                        break;
14682                        case 'z':
14683                            return;
14684                        break
14685                        default:
14686                            // dummy
14687                            curr.abs = {'x': 0, 'y': 0, 'bx': 0, 'by': 0, 'qx': 0, 'qy': 0};
14688                        break;
14689                    }
14690					
14691                    // Updating bounding box
14692                    this.bb.x1 = Math.min(this.bb.x1, curr.abs.x);
14693                    this.bb.y1 = Math.min(this.bb.y1, curr.abs.y);
14694                    this.bb.x2 = Math.max(this.bb.x2, curr.abs.x);
14695                    this.bb.y2 = Math.max(this.bb.y2, curr.abs.y);
14696                    return curr.abs;
14697                }
14698            }
14699        },
14700
14701        _updateCoords: function() {
14702            var i, len = this.length;
14703            this.bb = {x1: Number.MAX_VALUE, y1: Number.MAX_VALUE, x2: -Number.MAX_VALUE, y2: -Number.MAX_VALUE};
14704            for (i = 0; i < len; i++)
14705                this._getAbsoluteCoords(i, true);
14706        }
14707    };
14708
14709	var pathMethods = {
14710		absolutely: function() {
14711			this.isAbsolute = true;
14712			return this;
14713		},
14714
14715		relatively: function() {
14716			this.isAbsolute = false;
14717			return this;
14718		},
14719
14720		render: function() {
14721			this.freeze = false;
14722			this.attrs.path = this.pathPoints.getPathString() ||
14723				(R.svg ? 'M 0 0' : '');
14724			this.node.setAttribute((R.svg ? 'd' : 'path'), this.attrs.path);
14725			
14726			return this;
14727		},
14728
14729		getBBox: function() {
14730			var yshift = 0, height = 0;
14731			
14732			this.pathPoints._updateCoords();
14733			if (typeof this.pathPoints.bb == 'undefined' ||
14734				(this.pathPoints.bb.x1 == Number.MAX_VALUE && this.pathPoints.bb.y1 == Number.MAX_VALUE))
14735				return {x: 0, y: 0, width: 0, height: 0};
14736			else {
14737				if (this.isCircle) {
14738					height = this.pathPoints.bb.x2 - this.pathPoints.bb.x1;
14739					yshift = height / 2;
14740				}
14741				return {x: this.pathPoints.bb.x1, 
14742						y: this.pathPoints.bb.y1 - yshift, 
14743						width: this.pathPoints.bb.x2 - this.pathPoints.bb.x1, 
14744						height: height || (this.pathPoints.bb.y2 - this.pathPoints.bb.y1)};
14745			}
14746		},
14747
14748		hold: function() {
14749			this.freeze = true;
14750			return this;
14751		},
14752
14753		moveTo: function (x, y) {
14754			this.pathPoints.add(new PathPoint(this, 'm', x, y));
14755			return this.freeze ? this : this.render();
14756		},
14757
14758		lineTo: function (x, y) {
14759			this.pathPoints.add(new PathPoint(this, 'l', x, y));
14760			return this.freeze ? this : this.render();
14761		},
14762
14763		arcTo: function (rx, ry, large_arc_flag, sweep_flag, x, y) {
14764			this.pathPoints.add(new PathPoint(this, 'a', rx, ry, large_arc_flag, sweep_flag, x, y));
14765			return this.freeze ? this : this.render();
14766		},
14767
14768		transform: function(callback) {
14769			var i, len = this.pathPoints.length;
14770			
14771			for (i = 0; i < len; i++)
14772				this.pathPoints[i] = callback(this.pathPoints[i].dropCache());
14773
14774			this.pathPoints._updateCoords();
14775			this.render();
14776		},
14777
14778		cplineTo: function (x1, y1, w1) {
14779			if (!w1) {
14780				return this.lineTo(x1, y1);
14781			} else {
14782				this.pathPoints.add(new PathPoint(this, 'cp', x1, y1, w1));
14783				return this.freeze ? this : this.render();
14784			}
14785		},
14786
14787		curveTo: function() {
14788			if (arguments.length == 6)
14789				this.pathPoints.add(new PathPoint(this, 'c', arguments[0], arguments[1]
14790														   , arguments[2], arguments[3] 
14791														   , arguments[4], arguments[5]));
14792			else
14793				this.pathPoints.add(new PathPoint(this, 's', arguments[0], arguments[1]
14794														   , arguments[2], arguments[3]));
14795			return this.freeze ? this : this.render();
14796		},
14797
14798		qcurveTo: function() {
14799			if (arguments.length == 4)
14800				this.pathPoints.add(new PathPoint(this, 'q', arguments[0], arguments[1]
14801														   , arguments[2], arguments[3]));
14802			else
14803				this.pathPoints.add(new PathPoint(this, 't', arguments[0], arguments[1]));
14804			return this.freeze ? this : this.render();
14805		},
14806
14807		andClose: function() {
14808		  this.pathPoints.add(new PathPoint(this, 'z'));
14809		  return this.freeze ? this : this.render();
14810		},
14811
14812		clear: function() {
14813			this.pathPoints.splice(0, this.pathPoints.length);
14814			this.pathPoints._updateCoords();
14815			return this.freeze ? this : this.render();
14816		},
14817		
14818		// Austin-specific functions, works with Point objects
14819		// with negative Y coordinate
14820		update: function(points, positive) {
14821			var p = 0, plen = points.length;
14822			for (; p < plen; p++) {
14823				this.pathPoints[p].dropCache();
14824				this.pathPoints[p].x = points[p].x;
14825				this.pathPoints[p].y = (positive ? 1 : -1) * points[p].y;
14826			}
14827			return this.freeze ? this : this.render();
14828		},
14829		
14830		poly: function(points, positive) {
14831			var i = 1, ilen = points.length;
14832            if (ilen == 0)
14833                return this;
14834			this.clear().moveTo(points[0].x, (positive ? 1 : -1) * points[0].y);
14835			for (; i < ilen; i++) {
14836				this.lineTo(points[i].x, (positive ? 1 : -1) * points[i].y);
14837			}
14838			this.andClose();
14839			return this.freeze ? this : this.render();
14840		}
14841    };
14842
14843    var getContainer = function() {
14844            var container,
14845                x,
14846                y,
14847                width,
14848                height;
14849            if (typeof arguments[0] == "string" || typeof arguments[0] == "object") {
14850                if (typeof arguments[0] == "string") {
14851                    container = doc.getElementById(arguments[0]);
14852                } else {
14853                    container = arguments[0];
14854                }
14855                if (container.tagName) {
14856                    if (arguments[1] == null) {
14857                        return {
14858                            container: container,
14859                            width: container.style.pixelWidth || container.offsetWidth,
14860                            height: container.style.pixelHeight || container.offsetHeight
14861                        };
14862                    } else {
14863                        return {container: container, width: arguments[1], height: arguments[2]};
14864                    }
14865                }
14866            }
14867        };
14868
14869    // SVG
14870    if (R.svg) {
14871        R.toString = function() {
14872            return  "Your browser supports SVG.\nYou are running Rapha\u00ebl " + this.version;
14873        };
14874
14875        var thePath = function (SVG, params, pathString) {
14876            params = R.extend({}, params || {});
14877            var el = doc.createElementNS(SVG.svgns, "path");
14878            if (SVG.canvas) {
14879                SVG.canvas.appendChild(el);
14880            }
14881            var p = new Element(el, SVG);
14882            p.isAbsolute = true;
14883            
14884            R.extend(p, pathMethods);
14885
14886            p.type = "path";
14887            p.pathPoints = new Array;
14888            R.extend(p.pathPoints, pointsMethods);
14889            p.pathPoints.path = p;
14890            
14891            if (params) {
14892                !params.gradient && (params.fill = params.fill || "none");
14893                params.stroke = params.stroke || "#000";
14894            } else {
14895                params = {fill: "none", stroke: "#000"};
14896            }
14897            setFillAndStroke(p, params);
14898            return p;
14899        };
14900        var updatePosition = function (o) {
14901            if (o.pattern) {
14902                var bbox = o.getBBox();
14903                o.pattern.setAttribute("patternTransform", "translate(".concat(bbox.x, ",", bbox.y, ")"));
14904            }
14905        };
14906        var setFillAndStroke = function (o, params) {
14907            var dasharray = {
14908                    "": [0],
14909                    "none": [0],
14910                    "-": [3, 1],
14911                    ".": [1, 1],
14912                    "-.": [3, 1, 1, 1],
14913                    "-..": [3, 1, 1, 1, 1, 1],
14914                    ". ": [1, 3],
14915                    "- ": [4, 3],
14916                    "--": [8, 3],
14917                    "- .": [4, 3, 1, 3],
14918                    "--.": [8, 3, 1, 3],
14919                    "--..": [8, 3, 1, 3, 1, 3]
14920                },
14921                node = o.node,
14922                attrs = o.attrs,
14923                rot = attrs.rotation,
14924                rcx = attrs.rcx,
14925                rcy = attrs.rcy,
14926                addDashes = function (o, value) {
14927                    value = dasharray[value.toString().toLowerCase()];
14928                    if (value) {
14929                        var width = o.attrs["stroke-width"] || "1",
14930                            butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
14931                            dashes = [];
14932                        for (var i = 0, ii = value.length; i < ii; i++) {
14933                            dashes.push(value[i] * width + ((i % 2) ? 1 : -1) * butt);
14934                        }
14935                        value = dashes.join(",");
14936                        node.setAttribute("stroke-dasharray", value);
14937                    }
14938                };
14939            // o.rotate(0, rcx, rcy);
14940            for (var att in params) {
14941                if (!(att in availableAttrs)) {
14942                    continue;
14943                }
14944                var value = params[att];
14945                attrs[att] = value;
14946                switch (att) {
14947                    // Hyperlink
14948                    case "href":
14949                    case "title":
14950                    case "target":
14951                        var pn = node.parentNode;
14952                        if (pn.tagName.toLowerCase() != "a") {
14953                            var hl = doc.createElementNS(o.paper.svgns, "a");
14954                            pn.insertBefore(hl, node);
14955                            hl.appendChild(node);
14956                            pn = hl;
14957                        }
14958                        pn.setAttributeNS(o.paper.xlink, att, value);
14959                      break;
14960                    case "path":
14961                        if (o.type == "path") {
14962                            node.setAttribute("d", "M0,0");
14963                            paper.pathfinder(o, value);
14964                        }
14965                    case "width":
14966                        node.setAttribute(att, value);
14967                        if (attrs.fx) {
14968                            att = "x";
14969                            value = attrs.x;
14970                        } else {
14971                            break;
14972                        }
14973                    case "x":
14974                        if (attrs.fx) {
14975                            value = -attrs.x - (attrs.width || 0);
14976                        }
14977                    case "rx":
14978                    case "cx":
14979                        node.setAttribute(att, value);
14980                        updatePosition(o);
14981                        break;
14982                    case "height":
14983                        node.setAttribute(att, value);
14984                        if (attrs.fy) {
14985                            att = "y";
14986                            value = attrs.y;
14987                        } else {
14988                            break;
14989                        }
14990                    case "y":
14991                        if (attrs.fy) {
14992                            value = -attrs.y - (attrs.height || 0
14992);
14993                        }
14994                    case "ry":
14995                    case "cy":
14996                        node.setAttribute(att, value);
14997                        updatePosition(o);
14998                        break;
14999                    case "r":
15000                        if (o.type == "rect") {
15001                            node.setAttribute("rx", value);
15002                            node.setAttribute("ry", value);
15003                        } else {
15004                            node.setAttribute(att, value);
15005                        }
15006                        break;
15007                    case "src":
15008                        if (o.type == "image") {
15009                            node.setAttributeNS(o.paper.xlink, "href", value);
15010                        }
15011                        break;
15012                    case "stroke-width":
15013                        node.style.strokeWidth = value;
15014                        // Need following line for Firefox
15015                        node.setAttribute(att, value);
15016                        if (attrs["stroke-dasharray"]) {
15017                            addDashes(o, attrs["stroke-dasharray"]);
15018                        }
15019                        break;
15020                    case "stroke-dasharray":
15021                        addDashes(o, value);
15022                        break;
15023                    case "rotation":
15024                        rot = value;
15025                        break;
15026                    case "translation":
15027                        var xy = (value + "").split(separator);
15028                        o.translate((+xy[0] + 1 || 2) - 1, (+xy[1] + 1 || 2) - 1);
15029                        break;
15030                    case "scale":
15031                        var xy = (value + "").split(separator);
15032                        o.scale(+xy[0] || 1, +xy[1] || +xy[0] || 1, +xy[2] || null, +xy[3] || null);
15033                        break;
15034                    case "fill":
15035                        var isURL = (value + "").match(/^url\(([^\)]+)\)$/i);
15036                        if (isURL) {
15037                            var el = doc.createElementNS(o.paper.svgns, "pattern"),
15038                                ig = doc.createElementNS(o.paper.svgns, "image"),
15039                                img = doc.createElement("img"),
15040                                updImage = function() {
15041                                    // Special workaround for FF browsers.
15042                                    if (isGecko && false) {
15043                                        el.setAttribute("width", img.offsetWidth - 2);
15044                                        el.setAttribute("height", img.offsetHeight - 2);
15045                                    } else {
15046                                        el.setAttribute("width", img.offsetWidth);
15047                                        el.setAttribute("height", img.offsetHeight);
15048                                    }
15049                                    ig.setAttribute("width", img.offsetWidth);
15050                                    ig.setAttribute("height", img.offsetHeight);
15051                                    doc.body.removeChild(img);
15052                                    paper.safari();
15053                                };
15054                            el.id = "raphael-pattern-" + R.idGenerator++;
15055                            el.setAttribute("x", 0);
15056                            el.setAttribute("y", 0);
15057                            el.setAttribute("patternUnits", "userSpaceOnUse");
15058                            ig.setAttribute("x", 0);
15059                            ig.setAttribute("y", 0);
15060                            ig.setAttributeNS(o.paper.xlink, "href", isURL[1]);
15061                            el.appendChild(ig);
15062                            
15063                            img.style.cssText = "position:absolute;left:-9999em;top:-9999em;";
15064                            if (isChrome) {
15065                                img.onload = function() {
15066                                    window.setTimeout(updImage, 1);
15067                                };
15068                            } else {
15069                                img.onload = updImage;
15070                            }
15071                            img.onerror = function() {
15072                                doc.body.removeChild(img);
15073                            }
15074                            doc.body.appendChild(img);
15075                            img.src = isURL[1];
15076                            o.paper.defs.appendChild(el);
15077                            node.style.fill = "url(#" + el.id + ")";
15078                            node.setAttribute("fill", "url(#" + el.id + ")");
15079                            o.pattern = el;
15080                            updatePosition(o);
15081                            break;
15082                        }
15083                        delete params.gradient;
15084                        delete attrs.gradient;
15085                        if (typeof attrs.opacity != "undefined" && typeof params.opacity == "undefined" ) {
15086                            node.style.opacity = attrs.opacity;
15087                            // Need following line for Firefox
15088                            node.setAttribute("opacity", attrs.opacity);
15089                        }
15090                        if (typeof attrs["fill-opacity"] != "undefined" && typeof params["fill-opacity"] == "undefined" ) {
15091                            node.style.fillOpacity = o.attrs["fill-opacity"];
15092                            // Need following line for Firefox
15093                            node.setAttribute("fill-opacity", attrs["fill-opacity"]);
15094                        }
15095                    case "stroke":
15096                        node.style[att] = R.getRGB(value).hex;
15097                        // Need following line for Firefox
15098                        node.setAttribute(att, R.getRGB(value).hex);
15099                        break;
15100                    case "opacity":
15101                    case "fill-opacity":
15102                        if (attrs.gradient) {
15103                            var gradient = doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, ""));
15104                            if (gradient) {
15105                                var stops = gradient.getElementsByTagName("stop");
15106                                stops[stops.length - 1].setAttribute("stop-opacity", value);
15107                            }
15108                            break;
15109                        }
15110                    default :
15111                        var cssrule = att.replace(/(\-.)/g, function (w) {
15112                            return w.substring(1).toUpperCase();
15113                        });
15114                        node.style[cssrule] = value;
15115                        // Need following line for Firefox
15116                        node.setAttribute(att, value);
15117                        break;
15118                }
15119            }
15120            
15121            tuneText(o, params);
15122            // o.rotate(rot, rcx, rcy);
15123        };
15124        var leading = 1.2;
15125        var tuneText = function (el, params) {
15126            if (el.type != "text" || !("text" in params || "font" in params || "font-size" in params || "x" in params || "y" in params)) {
15127                return;
15128            }
15129            var a = el.attrs,
15130                node = el.node,
15131                fontSize = node.firstChild ? parseInt(doc.defaultView.getComputedStyle(node.firstChild, "").getPropertyValue("font-size"), 10) : 10;
15132
15133            if ("text" in params) {
15134                while (node.firstChild) {
15135                    node.removeChild(node.firstChild);
15136                }
15137                var texts = (params.text + "").split("\n");
15138                for (var i = 0, ii = texts.length; i < ii; i++) {
15139                    var tspan = doc.createElementNS(el.paper.svgns, "tspan");
15140                    i && tspan.setAttribute("dy", fontSize * leading);
15141                    i && tspan.setAttribute("x", a.x);
15142                    tspan.appendChild(doc.createTextNode(texts[i]));
15143                    node.appendChild(tspan);
15144                }
15145            } else {
15146                var texts = node.getElementsByTagName("tspan");
15147                for (var i = 0, ii = texts.length; i < ii; i++) {
15148                    i && texts[i].setAttribute("dy", fontSize * leading);
15149                    i && texts[i].setAttribute("x", a.x);
15150                }
15151            }
15152            node.setAttribute("y", a.y);
15153            var bb = el.getBBox(),
15154                dif = a.y - (bb.y + bb.height / 2);
15155            dif && node.setAttribute("y", a.y + dif);
15156        };
15157        var Element = function (node, svg) {
15158            var X = 0,
15159                Y = 0;
15160            this[0] = node;
15161            this.node = node;
15162            this.paper = svg;
15163            this.attrs = this.attrs || {};
15164            this.transformations = []; // rotate, translate, scale
15165            this._ = {
15166                tx: 0,
15167                ty: 0,
15168                rt: {deg: 0, x: 0, y: 0},
15169                sx: 1,
15170                sy: 1
15171            };
15172        };
15173
15174        var combinedTrans = function(trans) {
15175           return trans.filter(function(t) { return t != '';}).join(" ");
15176        };
15177
15178        Element.prototype.rotate = function (deg, cx, cy) {
15179            if (deg == null) {
15180                return this._.rt.deg;
15181            }
15182            deg = deg.toString().split(separator);
15183            if (deg.length - 1) {
15184                cx = parseFloat(deg[1]);
15185                cy = parseFloat(deg[2]);
15186            }
15187            deg = parseFloat(deg[0]);
15188            if (cx != null) {
15189                this._.rt.deg = deg;
15190            } else {
15191                this._.rt.deg += deg;
15192            }
15193            if (cy == null) {
15194                cx = null;
15195                var bbox = this.getBBox();
15196            }
15197            
15198            cx = cx == null ? bbox.x + bbox.width / 2 : cx;
15199            cy = cy == null ? bbox.y + bbox.height / 2 : cy;
15200
15201            if (this._.rt.deg) {
15202                this.transformations[0] = ("rotate(" + this._.rt.deg + " " + cx + " " + cy + ")");
15203            } else {
15204                this.transformations[0] = "";
15205            }
15206            this.attrs.rotation = deg;
15207            this.attrs.rcx = cx;
15208            this.attrs.rcy = cy;
15209            this.node.setAttribute("transform", combinedTrans(this.transformations));
15210            return this;
15211        };
15212        Element.prototype.hide = function() {
15213            this.node.style.display = "none";
15214            return this;
15215        };
15216        Element.prototype.show = function() {
15217            this.node.style.display = "block";
15218            return this;
15219        };
15220        Element.prototype.remove = function() {
15221            this.node.parentNode.removeChild(this.node);
15222        };
15223        Element.prototype.getBBox = function() {
15224            if (this.node.style.display == "none") {
15225                this.show();
15226                var hide = true;
15227            }
15228            var bbox = {};
15229            try {
15230                bbox = this.node.getBBox();
15231            } catch(e) {
15232                // Firefox 3.0.x plays badly here
15233            } finally {
15234                bbox = bbox || {};
15235            }
15236            /*
15237            if (this.type == "text") {
15238                bbox = {x: bbox.x, y: Infinity, width: bbox.width, height: 0};
15239                for (var i = 0, ii = this.node.getNumberOfChars(); i < ii; i++) {
15240                    var bb = this.node.getExtentOfChar(i);
15241                    (bb.y < bbox.y) && (bbox.y = bb.y);
15242                    (bb.y + bb.height - bbox.y > bbox.height) && (bbox.height = bb.y + bb.height - bbox.y);
15243                }
15244            }
15245            */
15246            hide && this.hide();
15247            return bbox;
15248        };
15249        Element.prototype.attr = function() {
15250            if (arguments.length == 1 && typeof arguments[0] == "string") {
15251                if (arguments[0] == "translation") {
15252                    return this.translate();
15253                }
15254                return this.attrs[arguments[0]];
15255            }
15256            if (arguments.length == 1 && R.isArray(arguments[0])) {
15257                var values = {};
15258                for (var j in arguments[0]) {
15259                    values[arguments[0][j]] = this.attrs[arguments[0][j]];
15260                }
15261                return values;
15262            }
15263            if (arguments.length == 2) {
15264                var params = {};
15265                params[arguments[0]] = arguments[1];
15266                setFillAndStroke(this, params);
15267            } else if (arguments.length == 1 && typeof arguments[0] == "object") {
15268                setFillAndStroke(this, arguments[0]);
15269            }
15270            return this;
15271        };
15272        Element.prototype.toFront = function() {
15273            this.node.parentNode.appendChild(this.node);
15274            return this;
15275        };
15276        Element.prototype.toBack = function() {
15277            if (this.node.parentNode.firstChild != this.node) {
15278                this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
15279            }
15280            return this;
15281        };
15282
15283        Element.prototype.insertAfter = function (element) {
15284			if (element instanceof Set)
15285				element = element.getLast();
15286
15287			if (!element)
15288				return this;
15289				
15290			element = element.node || element;
15291	
15292            if (element.nextSibling)
15293                element.parentNode.insertBefore(this.node, element.nextSibling);
15294            else
15295                element.parentNode.appendChild(this.node);
15296
15297            return this;
15298        };
15299
15300        Element.prototype.insertBefore = function (element) {
15301			if (element instanceof Set)
15302				element = element.getFirst();
15303				
15304			if (!element)
15305				return this;
15306
15307            element = element.node || element;
15308            element.parentNode.insertBefore(this.node, element);
15309            return this;
15310        };
15311        var theCircle = function (svg, x, y, r) {
15312            var el = doc.createElementNS(svg.svgns, "circle");
15313            el.setAttribute("cx", x);
15314            el.setAttribute("cy", y);
15315            el.setAttribute("r", r);
15316            el.setAttribute("fill", "none");
15317            el.setAttribute("stroke", "#000");
15318            if (svg.canvas) {
15319                svg.canvas.appendChild(el);
15320            }
15321            var res = new Element(el, svg);
15322            res.attrs = res.attrs || {};
15323            res.attrs.cx = x;
15324            res.attrs.cy = y;
15325            res.attrs.r = r;
15326            res.attrs.stroke = "#000";
15327            res.type = "circle";
15328            return res;
15329        };
15330        var theRect = function (svg, x, y, w, h) {
15331            var el = doc.createElementNS(svg.svgns, "rect");
15332            el.setAttribute("x", x);
15333            el.setAttribute("y", y);
15334            el.setAttribute("width", w);
15335            el.setAttribute("height", h);
15336
15337            el.setAttribute("fill", "none");
15338            el.setAttribute("stroke", "#000");
15339            if (svg.canvas)
15340                svg.canvas.appendChild(el);
15341
15342            var res = new Element(el, svg);
15343            res.attrs = res.attrs || {};
15344            res.attrs.x = x;
15345            res.attrs.y = y;
15346            res.attrs.width = w;
15347            res.attrs.height = h;
15348            res.attrs.stroke = "#000";
15349
15350            res.type = "rect";
15351            return res;
15352        };
15353        var theImage = function (svg, src, x, y, w, h) {
15354            var el = doc.createElementNS(svg.svgns, "image");
15355            el.setAttribute("x", x);
15356            el.setAttribute("y", y);
15357            el.setAttribute("width", w);
15358            el.setAttribute("height", h);
15359            el.setAttribute("preserveAspectRatio", "none");
15360            el.setAttributeNS(svg.xlink, "href", src);
15361            if (svg.canvas) {
15362                svg.canvas.appendChild(el);
15363            }
15364            var res = new Element(el, svg);
15365            res.attrs = res.attrs || {};
15366            res.attrs.x = x;
15367            res.attrs.y = y;
15368            res.attrs.width = w;
15369            res.attrs.height = h;
15370            res.type = "image";
15371            return res;
15372        };
15373        var theText = function (svg, x, y, text) {
15374            var el = doc.createElementNS(svg.svgns, "text");
15375            el.setAttribute("x", x);
15376            el.setAttribute("y", y);
15377            el.setAttribute("text-anchor", "middle");
15378            if (svg.canvas) {
15379                svg.canvas.appendChild(el);
15380            }
15381            var res = new Element(el, svg);
15382            res.attrs = res.attrs || {};
15383            res.attrs.x = x;
15384            res.attrs.y = y;
15385            res.type = "text";
15386            setFillAndStroke(res, {font: availableAttrs.font, stroke: "none", fill: "#000", text: text});
15387            return res;
15388        };
15389        var setSize = function (width, height) {
15390            this.width = width || this.width;
15391            this.height = height || this.height;
15392            this.canvas.setAttribute("width", this.width);
15393            this.canvas.setAttribute("height", this.height);
15394
15395            if (this.webkitPan) {
15396                this.zoom = this._zFactor = 10;
15397                this._adjX = width / 2;
15398                this._adjY = height / 2;
15399                this.canvas.setAttribute('viewBox', [-width * this.zoom / 2, -height * this.zoom / 2,
15400                                                     width * this.zoom, height * this.zoom].join(' '));
15401            } else {
15402                this.zoom = paper._zFactor = 1;
15403                this._adjX = 0;
15404                this._adjY = 0;
15405            }
15406
15407            return this;
15408        };
15409        var create = function() {
15410            var con = getContainer.apply(null, arguments),
15411                container = con.container,
15412                x = con.x,
15413                y = con.y,
15414                width = con.width,
15415                height = con.height;
15416            if (!container) {
15417                throw new Error("SVG container not found.");
15418            }
15419
15420            paper.canvas = doc.createElementNS(paper.svgns, "svg");
15421            paper.webkitPan = arguments[3] === true;
15422
15423            paper.setSize(width, height);
15424            paper.x = 0;
15425            paper.y = 0;
15426
15427            paper._updateViewBox();
15428
15429            if (container == 1) {
15430                doc.body.appendChild(paper.canvas);
15431                paper.canvas.style.position = "absolute";
15432                paper.canvas.style.left = x + "px";
15433                paper.canvas.style.top = y + "px";
15434            } else {
15435                if (container.firstChild) {
15436                    container.insertBefore(paper.canvas, container.firstChild);
15437                } else {
15438                    container.appendChild(paper.canvas);
15439                }
15440            }
15441            container = {
15442                canvas: paper.canvas,
15443                clear: function() {
15444                    while (this.canvas.firstChild) {
15445                        this.canvas.removeChild(this.canvas.firstChild);
15446                    }
15447                    this.defs = doc.createElementNS(paper.svgns, "defs");
15448                    this.canvas.appendChild(this.defs);
15449                }
15450            };
15451            for (var prop in paper) {
15452                if (prop != "create") {
15453                    container[prop] = paper[prop];
15454                }
15455            }
15456            container.clear();
15457            container.raphael = R;
15458            return container;
15459        };
15460        paper.remove = function() {
15461            this.canvas.parentNode.removeChild(this.canvas);
15462        };
15463
15464        paper._updateViewBox = function() {
15465            if (this.webkitPan) {
15466                this.canvas.style.webkitTransform = 'translate(' + (-this.x) + 'px, ' + (-this.y) + 'px' + ') scale(' + this.zoom + ')';
15467            } else {
15468                this.canvas.setAttribute('viewBox', [this.x / this.zoom, this.y / this.zoom, 
15469                                                     this.width / this.zoom, this.height / this.zoom ].join(' '));
15470            }
15471        }
15472
15473        paper.pan = function(dx, dy, absolute) {
15474            if (absolute) {
15475                this.x = -dx + this._adjX;
15476                this.y = -dy + this._adjY;
15477            } else {
15478                this.x -= dx;
15479                this.y -= dy;
15480            }
15481
15482            this._updateViewBox();
15483        };
15484
15485        paper.scale = function(zoom) {
15486            this.zoom = this._zFactor * zoom;
15487            this._updateViewBox();
15488        };
15489
15490        paper.svgns = "http://www.w3.org/2000/svg";
15491        paper.xlink = "http://www.w3.org/1999/xlink";
15492
15493        // rest
15494        // WebKit rendering bug workaround method
15495        if ((navigator.vendor == "Apple Computer, Inc.") && (win.navigator.platform.slice(0, 2) == "iP")) {
15496            paper.safari = function() {
15497                var rect = this.rect(-this.width, -this.height, this.width * 3, this.height * 3).attr({stroke: "none"});
15498                win.setTimeout(function() {rect.remove();});
15499            };
15500        } else {
15501            paper.safari = function() {};
15502        }
15503    }
15504
15505	// VML
15506	if (R.vml) {
15507		R.toString = function() {
15508			return  "Your browser doesn\u2019t support SVG. Assuming it is Internet Explorer and falling down to VML.\nYou are running Rapha\u00ebl " + this.version;
15509		};
15510
15511		var thePath = function (vml, params, pathString) {
15512			if (params) {
15513				params = R.extend({}, params || {});
15514				params.fill = params.fill || "none";
15515				params.stroke = params.stroke || "none";
15516			} else {
15517				params = {fill: "none", stroke: "#000"};
15518			}
15519
15520			var node = createNode("shape"),
15521				style = node.style,
15522				element;
15523
15524			node.path = "";
15525
15526			if (params["class"])
15527				node.className = "rvml " + params["class"];
15528
15529			// node.coordsize = vml.coordsize;
15530			// node.coordorigin = vml.coordorigin;
15531			
15532			element = new Element(node, vml);
15533			element.isAbsolute = true;
15534			element.type = "path";
15535			element.path = [];
15536			R.extend(element, pathMethods);
15537
15538			element.pathPoints = [];
15539			R.extend(element.pathPoints, pointsMethods);
15540			element.pathPoints.path = element;
15541			
15542			style.position = "absolute";
15543			style.width = vml.width + "px";
15544			style.height = vml.height + "px";
15545			style.left = 0;
15546			style.top = 0;
15547			 
15548			setFillAndStroke(element, params);
15549
15550			element.attrs.x = 0;
15551			element.attrs.y = 0;
15552			element.attrs.w = vml.width;
15553			element.attrs.h = vml.height;
15554
15555			return element;
15556        };
15557
15558        var setFillAndStroke = function (o, params) {
15559            var node = o.node,
15560                s = node.style,
15561                xy, left, top, bbox = false,
15562                res = o;
15563            o.attrs = o.attrs || {};
15564
15565			if (params.x != undefined) {
15566				if (o.attrs.rcx == undefined) {
15567					bbox = bbox || o.getBBox();
15568
15569					o.attrs.rcx = params.x + (o.type != 'text' ? bbox.width / 2: 0);
15570				} 
15571
15572				left = o.attrs.rcx - paper.width / 2;
15573				o.holder.style.left = left + 'px';
15574				s.left = params.x - left + 'px';
15575			}
15576
15577			if (params.y != undefined) {
15578				if (o.attrs.rcy == undefined) {
15579					bbox = bbox || o.getBBox();
15580
15581					o.attrs.rcy = params.y + (o.type != 'text' ? bbox.height / 2: 0);
15582				} 
15583
15584				top = o.attrs.rcy - paper.height / 2;
15585
15586				o.holder.style.top = top + 'px';
15587				s.top = params.y - top + 'px';
15588			}
15589
15590            for (var par in params)
15591                o.attrs[par] = params[par];
15592			
15593			params.width != undefined && (node.style.width = params.width);
15594			params.height != undefined && (node.style.height = params.height);
15595
15596            params.href && (node.href = params.href);
15597            params.title && (node.title = params.title);
15598            params.target && (node.target = params.target);
15599            if (params.path && o.type == "path") {
15600                o.Path = "";
15601                o.path = [];
15602                o.attrs.path = "";
15603                paper.pathfinder(o, params.path);
15604            }
15605            if (params.rotation != null) {
15606                o.rotate(params.rotation, true);
15607            }
15608            if (params.translation) {
15609                xy = (params.translation + "").split(separator);
15610                o.translate(xy[0], xy[1]);
15611            }
15612            if (params.scale) {
15613                xy = (params.scale + "").split(separator);
15614                o.scale(+xy[0] || 1, +xy[1] || +xy[0] || 1, +xy[2] || null, +xy[3] || null);
15615            }
15616            if (o.type == "image" && params.src) {
15617                node.src = params.src;
15618            }
15619            if (o.type == "image" && params.opacity) {
15620                node.filterOpacity = " progid:DXImageTransform.Microsoft.Alpha(opacity=" + (params.opacity * 100) + ")";
15621                node.style.filter = (node.filterMatrix || "") + (node.filterOpacity || "");
15622            }
15623
15624            if (typeof params.opacity != "undefined" || typeof params["stroke-width"] != "undefined" || typeof params.fill != "undefined" || typeof params.stroke != "undefined" || params["stroke-width"] || params["stroke-opacity"] || params["fill-opacity"] || params["stroke-dasharray"] || params["stroke-miterlimit"] || params["stroke-linejoin"] || params["stroke-linecap"]) {
15625                o = o.shape || node;
15626                var fill = (o.getElementsByTagName("fill") && o.getElementsByTagName("fill")[0]) || createNode("fill");
15627                if ("fill-opacity" in params || "opacity" in params) {
15628                    var opacity = ((+params["fill-opacity"] + 1 || 2) - 1) * ((+params.opacity + 1 || 2) - 1);
15629                    opacity < 0 && (opacity = 0);
15630                    opacity > 1 && (opacity = 1);
15631                    fill.opacity = opacity;
15632                }
15633                params.fill && (fill.on = true);
15634                if (typeof fill.on == "undefined" || params.fill == "none") {
15635                    fill.on = false;
15636                }
15637                if (fill.on && params.fill) {
15638                    var isURL = params.fill.match(/^url\(([^\)]+)\)$/i);
15639                    if (isURL) {
15640                        fill.src = isURL[1];
15641                        fill.type = "tile";
15642                    } else {
15643                        fill.color = R.getRGB(params.fill).hex;
15644                        fill.src = "";
15645                        fill.type = "solid";
15646                    }
15647                }
15648                o.appendChild(fill);
15649                var stroke = (o.getElementsByTagName("stroke") && o.getElementsByTagName("stroke")[0]) || createNode("stroke");
15650                if ((params.stroke && params.stroke != "none") || params["stroke-width"] || typeof params["stroke-opacity"] != "undefined" || params["stroke-dasharray"] || params["stroke-miterlimit"] || params["stroke-linejoin"] || params["stroke-linecap"]) {
15651                    stroke.on = true;
15652                }
15653                if (params.stroke == "none" || typeof stroke.on == "undefined" || params.stroke == 0) {
15654                    stroke.on = false;
15655                }
15656                if (stroke.on && params.stroke) {
15657                    stroke.color = R.getRGB(params.stroke).hex;
15658                }
15659                var opacity = ((+params["stroke-opacity"] + 1 || 2) - 1) * ((+params.opacity + 1 || 2) - 1);
15660                opacity < 0 && (opacity = 0);
15661                opacity > 1 && (opacity = 1);
15662                stroke.opacity = opacity;
15663                params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
15664                stroke.miterlimit = params["stroke-miterlimit"] || 8;
15665                params["stroke-linecap"] && (stroke.endcap = {butt: "flat", square: "square", round: "round"}[params["stroke-linecap"]] || "miter");
15666                params["stroke-width"] && (stroke.weight = (parseFloat(params["stroke-width"]) || 1) * 12 / 16);
15667                if (params["stroke-dasharray"]) {
15668                    var dasharray = {
15669                        "-": "shortdash",
15670                        ".": "shortdot",
15671                        "-.": "shortdashdot",
15672                        "-..": "shortdashdotdot",
15673                        ". ": "dot",
15674                        "- ": "dash",
15675                        "--": "longdash",
15676                        "- .": "dashdot",
15677                        "--.": "longdashdot",
15678                        "--..": "longdashdotdot"
15679                    };
15680                    stroke.dashstyle = dasharray[params["stroke-dasharray"]] || "";
15681                }
15682                o.appendChild(stroke);
15683            }
15684            if (res.type == "text") {
15685                var s = res.textpath.style,
15686					a = res.attrs;
15687
15688	            params.font && (s.font = params.font);
15689	            params["font-family"] && (s.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, "") + '"');
15690	            params["font-size"] && (s.fontSize = parseInt(params["font-size"]) * paper.zoom + 'px');
15691	            params["font-weight"] && (s.fontWeight = params["font-weight"]);
15692	            params["font-style"] && (s.fontStyle = params["font-style"]);
15693				
15694                s = res.paper._span.style,
15695                a.font && (s.font = a.font);
15696                a["font-family"] && (s.fontFamily = a["font-family"]);
15697                a["font-size"] && (s.fontSize = a["font-size"]);
15698                a["font-weight"] && (s.fontWeight = a["font-weight"]);
15699                a["font-style"] && (s.fontStyle = a["font-style"]);
15700                res.paper._span.innerText = res.node.string;
15701
15702                a.width = res.paper._span.offsetWidth;
15703                a.height = res.paper._span.offsetHeight;
15704
15705                // text-anchor emulation
15706                switch (a["text-anchor"]) {
15707                    case "start":
15708                        res.node.style["v-text-align"] = "left";
15709                        res.bbx = Math.round(res.W / 2);
15710                    break;
15711                    case "end":
15712                        res.node.style["v-text-align"] = "right";
15713                        res.bbx = -Math.round(res.W / 2);
15714                    break;
15715                    default:
15716                        res.node.style["v-text-align"] = "center";
15717                    break;
15718                }
15719            }
15720        };
15721
15722		var Element = function (node, vml) {
15723			this.node = node;
15724			this.holder = createNode("group");
15725
15726			this.attrs = {};
15727			this.paper = vml;
15728
15729			var style = this.holder.style;
15730			style.position = "absolute";
15731			style.left = 0;
15732			style.top = 0;
15733			style.width = vml.width + "px";
15734			style.height = vml.height + "px";
15735			this.holder.coordsize = vml.coordsize;
15736			this.holder.coordorigin = vml.coordorigin;
15737
15738			this.holder.appendChild(node);
15739			this.paper.group.appendChild(this.holder);
15740
15741			this._ = {
15742				tx: 0,
15743				ty: 0,
15744				sx: 1,
15745				sy: 1
15746			};
15747		};
15748
15749		Element.prototype.rotate = function (deg, cx, cy) {
15750			if (deg == null)
15751				return this.attrs.rotation;
15752
15753			deg = parseFloat(deg);
15754
15755			if (cx != null)
15756				this.attrs.rotation = deg;
15757			else 
15758				this.attrs.rotation += deg;
15759
15760			if (cy == null) {
15761				var bbox = this.getBBox();
15762
15763				if (this.type != 'text') {
15764					cx = bbox.x + bbox.width / 2;
15765					cy = bbox.y + bbox.height / 2;
15766				} else {
15767					cx = bbox.x;
15768					cy = bbox.y;
15769				}
15770			}
15771			
15772			this.attrs.rcx = cx;
15773			this.attrs.rcy = cy;
15774			var left = cx - this.paper.width / 2,
15775				top = cy - this.paper.height / 2;
15776
15777			this.holder.style.left = left + 'px';
15778			this.holder.style.top = top + 'px';
15779			
15780			this.node.style.left = (this.attrs.x || 0) - left + 'px';
15781			this.node.style.top =  (this.attrs.y || 0) - top + 'px';
15782			
15783			this.holder.style.rotation = this.attrs.rotation;
15784			return this;
15785		};
15786
15787		Element.prototype.hide = function() {
15788			this.holder.style.display = "none";
15789			return this;
15790		};
15791
15792		Element.prototype.show = function() {
15793			this.holder.style.display = "block";
15794			return this;
15795		};
15796
15797		Element.prototype.getBBox = function() {
15798			return {
15799				x: this.attrs.x + (this.bbx || 0),
15800				y: this.attrs.y,
15801				width: this.attrs.width,
15802				height: this.attrs.height
15803			};
15804		};
15805
15806		Element.prototype.remove = function() {
15807			this.node.parentNode.removeChild(this.node);
15808			this.holder.parentNode.removeChild(this.holder);
15809			this.shape && this.shape.parentNode.removeChild(this.shape);
15810		};
15811
15812        Element.prototype.attr = function() {
15813            if (arguments.length == 1 && typeof arguments[0] == "string") {
15814                if (arguments[0] == "translation") {
15815                    return this.translate();
15816                }
15817                return this.attrs[arguments[0]];
15818            }
15819            if (this.attrs && arguments.length == 1 && R.isArray(arguments[0])) {
15820                var values = {};
15821                for (var i = 0, ii = arguments[0].length; i < ii; i++) {
15822                    values[arguments[0][i]] = this.attrs[arguments[0][i]];
15823                };
15824                return values;
15825            }
15826
15827            var params;
15828            if (arguments.length == 2) {
15829                params = {};
15830                params[arguments[0]] = arguments[1];
15831            }
15832            if (arguments.length == 1 && typeof arguments[0] == "object") {
15833                params = arguments[0];
15834            }
15835            if (params) {
15836                if (params.text && this.type == "text")
15837					this.textpath.string = params.text;
15838                setFillAndStroke(this, params);
15839            }
15840            return this;
15841        };
15842
15843        Element.prototype.toFront = function() {
15844            this.holder.parentNode.appendChild(this.holder);
15845            return this;
15846        };
15847        Element.prototype.toBack = function() {
15848            if (this.holder.parentNode.firstChild != this.holder) {
15849                this.holder.parentNode.insertBefore(this.holder, this.holder.parentNode.firstChild);
15850            }
15851            return this;
15852        };
15853        Element.prototype.insertAfter = function (element) {
15854			if (element instanceof Set)
15855				element = element.getLast();
15856
15857			if (!element)
15858				return this;
15859
15860			element = element.holder || element;
15861
15862            if (element.nextSibling)
15863                element.parentNode.insertBefore(this.holder, element.nextSibling);
15864            else
15865                element.parentNode.appendChild(this.holder);
15866
15867            return this;
15868        };
15869
15870        Element.prototype.insertBefore = function (element) {
15871			if (element instanceof Set)
15872				element = element.getFirst();
15873
15874			if (!element)
15875				return this;
15876			
15877			// getFirst will return DOM node, not raphael object
15878			element = element.holder || element;
15879
15880            element.parentNode.insertBefore(this.holder, element);
15881            return this;
15882        };
15883
15884		var theCircle = function (vml, x, y, r) {
15885			var element = thePath(vml)
15886							.hold()
15887							.moveTo(x - r, y)
15888							.arcTo(r, r, 0, 1, x + r, y)
15889							.arcTo(r, r, 0, 1, x - r, y)
15890							.render();
15891			element.isCircle = true;
15892			return element;
15893        };
15894
15895		var theRect = function (vml, x, y, w, h) {
15896			var element = thePath(vml)
15897							.hold()
15898							.moveTo(x, y)
15899							.lineTo(x + w, y)
15900							.lineTo(x + w, y + h)
15901							.lineTo(x, y + h)
15902							.andClose()
15903							.render();
15904							
15905			return element;
15906        };
15907
15908        var theImage = function (vml, src, x, y, w, h) {
15909			var node = createNode("image"),
15910				style = node.style;
15911
15912			node.src = src;
15913			var element = new Element(node, vml);
15914			element.type = "image";
15915
15916			element.attrs.x = x;
15917			element.attrs.y = y;
15918			element.attrs.width = w;
15919			element.attrs.height = h;
15920
15921			style.left = x + 'px';
15922			style.top = y + 'px';
15923			style.width = w;
15924			style.height = h;
15925			
15926			return element;
15927		};
15928		var theText = function (vml, x, y, text) {
15929		   var	node = createNode("shape"),
15930				style = node.style,
15931				path = createNode("path"),
15932				ps = path.style,
15933				o = createNode("textpath");
15934
15935			path.v = ["m0,0 l1,0"].join("");
15936			path.textpathok = true;
15937
15938			o.string = text;
15939			o.on = true;
15940			node.appendChild(o);
15941			node.appendChild(path);
15942			var element = new Element(node, vml);
15943			element.shape = node;
15944			element.textpath = o;
15945			element.type = "text";
15946
15947			element.attrs.x = x;
15948			element.attrs.y = y;
15949
15950			setFillAndStroke(element, {font: availableAttrs.font, stroke: "none", fill: "#000"});
15951
15952			style.left = x + 'px';
15953			style.top = y + 'px';
15954			style.width = element.attrs.width;
15955			style.height = element.attrs.height;
15956
15957			return element;
15958		};
15959
15960        var setSize = function (width, height) {
15961        	width = width || this.width;
15962        	height = height || this.height;
15963        	
15964			this.zoom = 1;
15965			
15966			if (width / height < this.ratio)
15967				width = parseInt(height * this.ratio);
15968			else
15969				height = parseInt(width / this.ratio);
15970			
15971			var cs = this.canvas.style;
15972			cs.width = width + "px";
15973			cs.height = height + "px";
15974			cs.clip = "rect(0 " + width + "px " + height + "px 0)"
15975			return this;
15976		};
15977        doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
15978        try {
15979            if (!doc.namespaces.rvml) {
15980                doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
15981            }
15982            var createNode = function (tagName) {
15983                return doc.createElement('<rvml:' + tagName + ' class="rvml">');
15984            };
15985        } catch (e) {
15986            var createNode = function (tagName) {
15987                return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
15988            };
15989        }
15990        var create = function() {
15991            var con = getContainer.apply(null, arguments),
15992                container = con.container,
15993                x = con.x,
15994                y = con.y,
15995                width = con.width,
15996                s,
15997                height = con.height;
15998            if (!container) {
15999                throw new Error("VML container not found.");
16000            }
16001            var c = paper.canvas = doc.createElement("div"),
16002                cs = c.style,
16003				group = paper.group = createNode('group'),
16004				style = group.style;
16005				
16006            width = parseFloat(width) || 512;
16007            height = parseFloat(height) || 342;
16008            
16009            paper.ratio = width / height;
16010
16011            paper.width = width;
16012            paper.height = height;
16013            paper.coordsize = width + " " + height;
16014
16015			paper.x = 0;
16016			paper.y = 0;
16017			
16018			paper.coordorigin = "0 0";
16019			group.coordorigin = "0 0";
16020
16021			paper.scale(1);
16022
16023            c.appendChild(paper.group);
16024
16025            cs.width  = width + "px";
16026            cs.height = height + "px";
16027            cs.position = "absolute";
16028            cs.clip = "rect(0 " + width + "px " + height + "px 0)";
16029
16030            style.width  = width + "px";
16031            style.height = height + "px";
16032            style.position = "absolute";
16033
16034            if (container == 1) {
16035                doc.body.appendChild(c);
16036                cs.left = x + "px";
16037                cs.top = y + "px";
16038                container = {
16039                    style: {
16040                        width: width,
16041                        height: height
16042                    }
16043                };
16044            } else {
16045                container.style.width = width;
16046                container.style.height = height;
16047                if (container.firstChild) {
16048                    container.insertBefore(c, container.firstChild);
16049                } else {
16050                    container.appendChild(c);
16051                }
16052            }
16053            for (var prop in paper) {
16054                container[prop] = paper[prop];
16055            }
16056            container.clear = function() {
16057                while (c.firstChild) {
16058                    c.removeChild(c.firstChild);
16059                }
16060            };
16061            container.raphael = R;
16062            container._span = doc.createElement("span");
16063            s = container._span.style;
16064            s.position = "absolute";
16065            s.left = "-99999px";
16066            s.top = "-99999px";
16067            s.padding = 0;
16068            s.margin = 0;
16069            s.lineHeight = 1;
16070            s.display = "inline";
16071
16072            c.appendChild(container._span);
16073            return container;
16074        };
16075
16076		paper.pan = function(dx, dy, absolute) {
16077			if (absolute) {
16078				this.x = -dx;
16079				this.y = -dy;
16080			} else {
16081				this.x -= dx;
16082				this.y -= dy;
16083			}
16084
16085			this.group.coordorigin = this.x / this.zoom + ' ' + this.y / this.zoom;
16086		};
16087
16088		paper.scale = function(zoom) {
16089			this.zoom = zoom;
16090			this.group.coordorigin = this.x / this.zoom + ' ' + this.y / this.zoom;
16091			this.group.coordsize = this.width / this.zoom + " " + this.height / this.zoom;
16092		};
16093
16094        paper.remove = function() {
16095            this.group.parentNode.removeChild(this.group);
16096        };
16097        paper.safari = function() {};
16098    }
16099
16100    // rest
16101
16102    // Events
16103    var addEvent = (function() {
16104		var map = {
16105			'mousemove': 'touchmove',
16106			'mousedown': 'touchstart',
16107			'mouseup': 'touchend'
16108		},
16109		revMap = {
16110			'touchmove': 'mousemove',
16111			'touchstart': 'mousedown',
16112			'touchend': 'mouseup'
16113		};
16114
16115        if (doc.addEventListener) {
16116            return function (obj, type, fn, element) {
16117                var realName = supportsTouch && map[type] ? map[type] : type;
16118                var f = function (e) {
16119                    if (typeof jQuery != undefined)
16120                        e = jQuery.event.fix(e);
16121
16122					if (supportsTouch && typeof map[type] != 'undefined') {
16123						if (e.type == 'touchend') {
16124							if (e.changedTouches.length == 1) {
16125								R.extend(e, {
16126									type: revMap[e.type],
16127									clientX: e.changedTouches[0].clientX,
16128									clientY: e.changedTouches[0].clientY,
16129									pageX: e.changedTouches[0].pageX,
16130									pageY: e.changedTouches[0].pageY
16131								});
16132							}
16133						} else if (e.touches.length == 1) {
16134							R.extend(e, {
16135								type: revMap[e.type],
16136								clientX: e.touches[0].clientX,
16137								clientY: e.touches[0].clientY,
16138								pageX: e.touches[0].pageX,
16139								pageY: e.touches[0].pageY
16140							});
16141						}
16142					}
16143
16144                    return fn.call(element, e);
16145                };
16146
16147                obj.addEventListener(realName, f, false);
16148                return function() {
16149                    obj.removeEventListener(realName, f, false);
16150                    return true;
16151                };
16152            };
16153        } else if (doc.attachEvent) {
16154            return function (obj, type, fn, element) {
16155                var f = function (e) {
16156                    if (typeof jQuery != undefined)
16157                        e = jQuery.event.fix(e);
16158                    return fn.call(element, e || win.event);
16159                };
16160                var detacher = function() {
16161                    obj.detachEvent("on" + type, f);
16162                    return true;
16163                };
16164                if (type == "mouseover") {
16165                    obj.attachEvent("onmouseenter", f);
16166                    return function() {
16167                        obj.detachEvent("onmouseenter", f);
16168                        return detacher;
16169                    };
16170                } else if (type == "mouseout") {
16171                    obj.attachEvent("onmouseleave", f);
16172                    return function() {
16173                        obj.detachEvent("onmouseleave", f);
16174                        return detacher;
16175                    };
16176                } else
16177	                obj.attachEvent("on" + type, f);
16178                return detacher;
16179            };
16180        }
16181    })();
16182    for (var i = events.length; i--;) {
16183        (function (eventName) {
16184            Element.prototype[eventName] = function (fn, trackDefault) {
16185                if (typeof fn == "function") {
16186                    this.events = this.events || {};
16187                    this.events[eventName] = this.events[eventName] || {};
16188                    this.events[eventName][fn] = this.events[eventName][fn] || [];
16189                    this.events[eventName][fn].push(addEvent(this.shape || this.node, eventName, trackDefault && jQuery && jQuery._transparentBindListener ? jQuery._transparentBindListener(eventName, fn) : fn, this));
16190                }
16191                return this;
16192            };
16193            Element.prototype["un" + eventName] = function (fn) {
16194                this.events &&
16195                this.events[eventName] &&
16196                this.events[eventName][fn] &&
16197                this.events[eventName][fn].length &&
16198                this.events[eventName][fn].shift()() &&
16199                !this.events[eventName][fn].length &&
16200                delete this.events[eventName][fn];
16201            };
16202
16203        })(events[i]);
16204    }
16205    paper.circle = function (x, y, r) {
16206        return theCircle(this, x, y, r);
16207    };
16208    paper.rect = function (x, y, w, h) {
16209        return theRect(this, x, y, w, h);
16210    };
16211    paper.path = function (params, pathString) {
16212        return thePath(this, params, pathString);
16213    };
16214    paper.image = function (src, x, y, w, h) {
16215        return theImage(this, src, x, y, w, h);
16216    };
16217    paper.text = function (x, y, text) {
16218        return theText(this, x, y, text);
16219    };
16220
16221    paper.set = function (itemsArray) {
16222        return new Set(itemsArray);
16223    };
16224    paper.setSize = setSize;
16225
16226    Element.prototype.scale = function (x, y, cx, cy) {
16227        y = y || x;
16228
16229        var dx, dy, dcx, dcy,
16230            a = this.attrs;
16231
16232        if (x != 0) {
16233            var rot = this.attrs.rotation || 0;
16234            this.attrs.rcx = this.attrs.rcx || 0;
16235            this.attrs.rcy = this.attrs.rcy || 0;
16236
16237            if (rot != 0)
16238                this.rotate(0, this.attrs.rcx, this.attrs.rcy);
16239
16240            var bb = this.getBBox(),
16241                rcx = bb.x + bb.width / 2,
16242                rcy = bb.y + bb.height / 2,
16243                kx = x / this._.sx,
16244                ky = y / this._.sy;
16245				
16246            cx = (+cx || cx == 0) ? cx : rcx;
16247            cy = (+cy || cy == 0) ? cy : rcy;
16248
16249            var dirx = Math.round(x / Math.abs(x)),
16250                diry = Math.round(y / Math.abs(y)),
16251                s = this.node.style,
16252                ncx = cx + (rcx - cx) * dirx * kx,
16253                ncy = cy + (rcy - cy) * diry * ky;
16254
16255
16256            switch (this.type) {
16257                case "rect":
16258                case "image":
16259					var neww = a.width * dirx * kx,
16260						newh = a.height * diry * ky;
16261
16262					this.attr({
16263						width: neww,
16264						height: newh,
16265						x: ncx - neww / 2,
16266						y: ncy - newh / 2
16267					});
16268                    break;
16269                case "circle":
16270                case "ellipse":
16271                    this.attr({
16272                        rx: a.rx * kx,
16273                        ry: a.ry * ky,
16274                        r: a.r * kx,
16275                        cx: ncx,
16276                        cy: ncy
16277                    });
16278                    break;
16279                case "path":
16280	                this.transform(function(point) {
16281						var u = point.type.toUpperCase();
16282                        if (point.type == u) {
16283                            point.x = cx + (point.x - cx) * kx;
16284                            point.y = cy + (point.y - cy) * ky;
16285                        } else {
16286                            point.x *= kx;
16287                            point.y *= ky;
16288                        }
16289
16290						if (u == "A") {
16291	                        point.rx *= kx;
16292	                        point.ry *= ky;
16293						}
16294	                    return point;
16295	                });
16296                break;
16297            }
16298            if (this.type in {text: 1, image:1} && (dirx != 1 || diry != 1)) {
16299                if (this.transformations) {
16300                    this.transformations[2] = "scale(".concat(dirx, ",", diry, ")");
16301                    this.node.setAttribute("transform", combinedTrans(this.transformations));
16302                    dx = (dirx == -1) ? -a.x - (neww || 0) : a.x;
16303                    dy = (diry == -1) ? -a.y - (newh || 0) : a.y;
16304                    this.attr({x: dx, y: dy});
16305                    a.fx = dirx - 1;
16306                    a.fy = diry - 1;
16307                } else {
16308                    this.node.filterMatrix = " progid:DXImageTransform.Microsoft.Matrix(M11=".concat(dirx,
16309                        ", M12=0, M21=0, M22=", diry,
16310                        ", Dx=0, Dy=0, sizingmethod='auto expand', filtertype='bilinear')");
16311
16312                    s.filter = (this.node.filterMatrix || "") + (this.node.filterOpacity || "");
16313                }
16314            } else {
16315                if (this.transformations) {
16316                    this.transformations[2] = "";
16317                    this.node.setAttribute("transform", combinedTrans(this.transformations));
16318                    a.fx = 0;
16319                    a.fy = 0;
16320                } else {
16321                    this.node.filterMatrix = "";
16322                    s.filter = (this.node.filterMatrix || "") + (this.node.filterOpacity || "");
16323                }
16324            }
16325            a.scale = [x, y, cx, cy].join(" ");
16326            this._.sx = x;
16327            this._.sy = y;
16328
16329            this.attrs.rcx = cx + (this.attrs.rcx - cx) * kx;
16330            this.attrs.rcy = cy + (this.attrs.rcy - cy) * ky;
16331
16332            if (rot != 0)
16333                this.rotate(rot, this.attrs.rcx, this.attrs.rcy);
16334        }
16335        return this;
16336    };
16337
16338    Element.prototype.translate = function (x, y) {
16339        if (x == null) {
16340            return {x: this._.tx, y: this._.ty};
16341        }
16342        this._.tx += +x;
16343        this._.ty += +y;
16344
16345        var rot = this.attrs.rotation || 0;
16346        this.attrs.rcx = this.attrs.rcx || 0;
16347        this.attrs.rcy = this.attrs.rcy || 0;
16348        
16349        if (rot != 0 || R.vml)
16350            this.rotate(0, this.attrs.rcx, this.attrs.rcy);
16351
16352        switch (this.type) {
16353			case "circle":
16354			case "ellipse":
16355				this.attr({cx: +x + this.attrs.cx, cy: +y + this.attrs.cy});
16356			break;
16357			case "rect":
16358			case "image":
16359			case "text":
16360				this.attr({x: +x + this.attrs.x, y: +y + this.attrs.y});
16361			break;
16362			case "path":
16363				this.transform(function(point) {
16364					if (point.type == point.type.toUpperCase()) {
16365						point.x += +x;
16366						point.y += +y;
16367					}
16368					return point;
16369				});
16370			break;
16371        }
16372
16373        this.attrs.rcx = (this.attrs.rcx || 0) + x;
16374        this.attrs.rcy = (this.attrs.rcy || 0) + y;
16375        
16376        if (rot != 0 || R.vml)
16377            this.rotate(rot, this.attrs.rcx, this.attrs.rcy);
16378
16379        return this;
16380    };
16381
16382    // Set
16383    var Set = function (items) {
16384        this.items = [];
16385        this.length = 0;
16386        if (items) {
16387            for (var i = 0, ii = items.length; i < ii; i++) {
16388                if (items[i] && (items[i].constructor == Element || items[i].constructor == Set)) {
16389                    this[this.items.length] = this.items[this.items.length] = items[i];
16390                    this.length++;
16391                }
16392            }
16393        }
16394    };
16395    Set.prototype.push = function() {
16396        var item,
16397            len;
16398        for (var i = 0, ii = arguments.length; i < ii; i++) {
16399            item = arguments[i];
16400            if (item && (item.constructor == Element || item.constructor == Set &&
16401                    this.items.indexOf(item) === -1)) {
16402                len = this.items.length;
16403                this[len] = this.items[len] = item;
16404                this.length++;
16405            }
16406        }
16407        return this;
16408    };
16409    Set.prototype.pop = function (id) {
16410        if (typeof id == 'object') {
16411            var i = 0;
16412            while (i < this.items.length && this.items[i] != id)
16413                i++;
16414            id = i;
16415        }
16416        var res = this.items.splice(id, 1)[0];
16417        for (var j = id, jj = this.items.length; j < jj; j++)
16418            this[j] = this[j + 1];
16419
16420        delete this[jj + 1];
16421        this.length--;
16422        return res;
16423    };
16424    for (var method in Element.prototype) {
16425        Set.prototype[method] = (function (methodname) {
16426            return function() {
16427                for (var i = 0, ii = this.items.length; i < ii; i++) {
16428                    this.items[i][methodname].apply(this.items[i], arguments);
16429                }
16430                return this;
16431            };
16432        }
16432)(method);
16433    }
16434    Set.prototype.attr = function (name, value) {
16435        if (name && R.isArray(name) && typeof name[0] == "object") {
16436            for (var j = 0, jj = name.length; j < jj; j++) {
16437                this.items[j].attr(name[j]);
16438            }
16439        } else {
16440            for (var i = 0, ii = this.items.length; i < ii; i++) {
16441                this.items[i].attr.apply(this.items[i], arguments);
16442            }
16443        }
16444        return this;
16445    };
16446
16447	Set.prototype.insertAfter = function(element, before) {
16448		if (this.items.length == 0)
16449			return this;
16450		
16451		var i, length = this.items.length, 
16452			sorted = this.items.slice(0, length),
16453			nodesArray = this.getAllSiblings();
16454
16455		for (i = 0; i < length; i++)
16456			sorted[i]._position = nodesArray.indexOf(this.items[i].node || this.items[i].holder);
16457			
16458		sorted.sort(function(a, b) {
16459			return a._position - b._position;
16460		});
16461		
16462		if (before)
16463			for (i = length - 1; i >= 0 ; i--) {
16464				sorted[i].insertBefore(element);
16465				delete sorted._position;
16466				element = sorted[i];
16467			}
16468		else
16469			for (i = 0; i < length; i++) {
16470				sorted[i].insertAfter(element);
16471				delete sorted._position;
16472				element = sorted[i];
16473			}
16474
16475		return this;
16476	};
16477
16478	Set.prototype.insertBefore = function(element) {
16479		this.insertAfter(element, true);
16480	};
16481
16482	Set.prototype.getAllSiblings = function() {
16483		var nodesCollection = (this.items[0].node || this.items[0].holder).parentNode.childNodes,
16484			nodesArray = [], i, length = nodesCollection.length;
16485
16486		for (i = 0; i < length; i++)
16487			nodesArray.push(nodesCollection[i]);
16488			
16489		return nodesArray;
16490	};
16491
16492	Set.prototype.getFirst = function(last) {
16493		if (this.items.length == 0)
16494			return false;
16495		
16496		var pos = last ? -1 : Number.MAX_VALUE, 
16497			i, length = this.items.length,
16498			nodesArray = this.getAllSiblings();
16499			compare = last ? Math.max : Math.min;
16500			
16501		for (i = 0; i < length; i++)
16502			pos = compare(nodesArray.indexOf(this.items[i].node || this.items[i].holder));
16503			
16504		return nodesArray[pos];
16505	};
16506
16507	Set.prototype.getLast = function() {
16508		return this.getFirst(true);
16509	};
16510    
16511    Set.prototype.getBBox = function() {
16512        var x = [],
16513            y = [],
16514            w = [],
16515            h = [];
16516        for (var i = this.items.length; i--;) {
16517            var box = this.items[i].getBBox();
16518            x.push(box.x);
16519            y.push(box.y);
16520            w.push(box.x + box.width);
16521            h.push(box.y + box.height);
16522        }
16523        x = Math.min.apply(Math, x);
16524        y = Math.min.apply(Math, y);
16525        return {
16526            x: x,
16527            y: y,
16528            width: Math.max.apply(Math, w) - x,
16529            height: Math.max.apply(Math, h) - y
16530        };
16531    };
16532
16533    R.ninja = function() {
16534        var r = window.Raphael;
16535        if (oldRaphael.was) {
16536            window.Raphael = oldRaphael.is;
16537        } else {
16538            try {
16539                delete window.Raphael;
16540            } catch (e) {
16541                window.Raphael = void(0);
16542            }
16543        }
16544        return r;
16545    };
16546    R.el = Element.prototype;
16547    return R;
16548})();
16549/*globals myDeco, $, Stack */
16550/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
16551          sloppy: true, white: true */
16552/**
16553 * @fileOverview Lib for matrix operations
16554 */
16555
16556/**
16557 * Constructor to create rows x cols matrix and fill it with zeros
16558 * @param {Number} rows Number of rows
16559 * @param {Number} cols Number of cols
16560 * @param {Number} identity Initialize diagonal (identity when true or 1) matrix
16561 */
16562function Matrix(rows, cols, identity) {
16563	var i, j, 
16564		row = [];
16565	
16566	for (j = 0; j < cols; j++) {
16567		row[j] = 0;
16568	}
16569		
16570	for (i = 0; i < rows; i++) {
16571		this.push(row.slice(0, cols));
16572	}
16573		
16574	if (identity) {
16575		// Converting identity value to number.
16576		identity *= 1;
16577		for (i = 0; i < Math.min(rows, cols); i++) { 
16578			this[i][i] = identity;
16579		}
16580	}
16581		
16582	this.rows = rows;
16583	this.cols = cols;
16584	
16585	$.extend(this, {
16586		/**
16587		 * Multiply this matrix on given one.
16588		 * @param {Matrix} m Second matrix to multiply on
16589		 * @returns {Matrix} New matrix with result of multiplication or false, if it's impossible to multiply
16590		 */
16591		multiply: function(m) {
16592			if (this.cols !== m.rows) {
16593				return false;
16594			}
16595
16596			var result = new Matrix(this.rows, m.cols), i, j, k;
16597
16598			if (this.rows === 4 && m.cols === 4) {
16599				result[0][0] = this[0][0] * m[0][0] + this[0][1] * m[1][0] + this[0][2] * m[2][0] + this[0][3] * m[3][0];
16600				result[0][1] = this[0][0] * m[0][1] + this[0][1] * m[1][1] + this[0][2] * m[2][1] + this[0][3] * m[3][1];
16601				result[0][2] = this[0][0] * m[0][2] + this[0][1] * m[1][2] + this[0][2] * m[2][2] + this[0][3] * m[3][2];
16602				result[0][3] = this[0][0] * m[0][3] + this[0][1] * m[1][3] + this[0][2] * m[2][3] + this[0][3] * m[3][3];
16603				result[1][0] = this[1][0] * m[0][0] + this[1][1] * m[1][0] + this[1][2] * m[2][0] + this[1][3] * m[3][0];
16604				result[1][1] = this[1][0] * m[0][1] + this[1][1] * m[1][1] + this[1][2] * m[2][1] + this[1][3] * m[3][1];
16605				result[1][2] = this[1][0] * m[0][2] + this[1][1] * m[1][2] + this[1][2] * m[2][2] + this[1][3] * m[3][2];
16606				result[1][3] = this[1][0] * m[0][3] + this[1][1] * m[1][3] + this[1][2] * m[2][3] + this[1][3] * m[3][3];
16607				result[2][0] = this[2][0] * m[0][0] + this[2][1] * m[1][0] + this[2][2] * m[2][0] + this[2][3] * m[3][0];
16608				result[2][1] = this[2][0] * m[0][1] + this[2][1] * m[1][1] + this[2][2] * m[2][1] + this[2][3] * m[3][1];
16609				result[2][2] = this[2][0] * m[0][2] + this[2][1] * m[1][2] + this[2][2] * m[2][2] + this[2][3] * m[3][2];
16610				result[2][3] = this[2][0] * m[0][3] + this[2][1] * m[1][3] + this[2][2] * m[2][3] + this[2][3] * m[3][3];
16611				result[3][0] = this[3][0] * m[0][0] + this[3][1] * m[1][0] + this[3][2] * m[2][0] + this[3][3] * m[3][0];
16612				result[3][1] = this[3][0] * m[0][1] + this[3][1] * m[1][1] + this[3][2] * m[2][1] + this[3][3] * m[3][1];
16613				result[3][2] = this[3][0] * m[0][2] + this[3][1] * m[1][2] + this[3][2] * m[2][2] + this[3][3] * m[3][2];
16614				result[3][3] = this[3][0] * m[0][3] + this[3][1] * m[1][3] + this[3][2] * m[2][3] + this[3][3] * m[3][3];
16615			} else if (this.rows === 4 && m.cols === 1) {
16616				result[0][0] = this[0][0] * m[0][0] + this[0][1] * m[1][0] + this[0][2] * m[2][0] + this[0][3] * m[3][0];
16617				result[1][0] = this[1][0] * m[0][0] + this[1][1] * m[1][0] + this[1][2] * m[2][0] + this[1][3] * m[3][0];
16618				result[2][0] = this[2][0] * m[0][0] + this[2][1] * m[1][0] + this[2][2] * m[2][0] + this[2][3] * m[3][0];
16619				result[3][0] = this[3][0] * m[0][0] + this[3][1] * m[1][0] + this[3][2] * m[2][0] + this[3][3] * m[3][0];				
16620			} else {
16621				for (i = 0; i < result.rows; i++) {
16622					for (j = 0; j < result.cols; j++) {
16623						for (k = 0; k < this.cols; k++) {
16624							result[i][j] += this[i][k] * m[k][j];
16625						}
16626					}
16627				}
16628			}
16629
16630			return result;
16631		},
16632
16633		/**
16634		 * Get particular row of the this matrix
16635		 * @param {Integer} n Number of row to extract
16636		 * @returns {Matrix} New 1 x cols matrix with data from n'th row of this one
16637		 */
16638		row: function(n) {
16639			var row = new Matrix(1, this.cols), i;
16640
16641			for (i = 0; i < this.cols; i++) {
16642				row[0][i] = this[n][i];
16643			}
16644
16645			return row;
16646		},
16647
16648		/**
16649		 * Get particular column of the this matrix
16650		 * @param {Integer} n Number of col to extract
16651		 * @returns {Matrix} New rows x 1 matrix with data from n'th col of this one
16652		 */
16653		col: function(n) {
16654			var col = new Matrix(this.rows, 1), i;
16655
16656			for (i = 0; i < this.rows; i++) {
16657				col[i][0] = this[i][n];
16658			}
16659
16660			return col;
16661		},
16662
16663		/**
16664		 * Get string representation of the matrix
16665		 * @returns {String}
16666		 */
16667		toString: function() {
16668			var i, result = '';
16669
16670			for (i = 0; i < this.rows; i++) {
16671				result += this[i].join(",\t") + "\n";
16672			}
16673
16674			return result;
16675		}
16676	});
16677}
16678
16679Matrix.prototype = new Stack();
16680/*globals myDeco, $, T, Matrix */
16681/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
16682          sloppy: true, white: true */
16683/**
16684 * @fileOverview Lib for matrix transformations
16685 */
16686
16687/**
16688 * Constructor to create transform object
16689 */
16690function Transform() {
16691	this.reset();
16692}
16693
16694/**
16695 * Add rotate transformation (using 0,0,0 as rotation center)
16696 * @param {String} axis Axis to rotate around
16697 * @param {Number} angle Angle in degrees to rotate on.
16698 * @returns {Transform} this
16699 */
16700Transform.prototype.rotate = function(axis, angle) {
16701	var _cos = T.cos(angle), 
16702		_sin = T.sin(angle),
16703		_t = {
16704			'z': {m00: [0, 0], m01: [0, 1], m10: [1, 0], m11: [1, 1]},
16705			'x': {m00: [1, 1], m01: [1, 2], m10: [2, 1], m11: [2, 2]},
16706			'y': {m00: [0, 0], m01: [2, 0], m10: [0, 1], m11: [2, 2]}
16707		}[axis],
16708		rMatrix = new Matrix(4, 4, true);
16709
16710	rMatrix[_t.m00[0]][_t.m00[1]] = _cos;
16711	rMatrix[_t.m01[0]][_t.m01[1]] = - _sin;
16712	rMatrix[_t.m10[0]][_t.m10[1]] = _sin;
16713	rMatrix[_t.m11[0]][_t.m11[1]] = _cos;
16714
16715	this.matrix = rMatrix.multiply(this.matrix);
16716
16717	return this;
16718};
16719
16720/**
16721 * Add isometric transformation (http://www.gamedev.net/reference/articles/article1269.asp)
16722 * @param {Number} angle Angle of projection
16723 * @returns {Transform} this.
16724 */
16725Transform.prototype.isometric = function(isoAngle, rotAngle) {
16726	this.scale(1, -1, T.cos(isoAngle)).rotate('z', rotAngle - 45).rotate('x', 90 - isoAngle);
16727	return this;
16728};
16729
16730/**
16731 * Apply translate transformation
16732 * @param {Number|Point} delta X delta or delta object
16733 * @param {Number} y Y delta
16734 * @param {Number} z Z delta
16735 * @returns {Transform} this
16736 */
16737Transform.prototype.translate = function(delta, y, z) {
16738	var dx, dy, dz, tMatrix;
16739	
16740	if (typeof delta === 'number') {
16741		dx = delta || 0;
16742		dy = y || 0;
16743		dz = z || 0;
16744	} else {
16745		dx = delta.x;
16746		dy = delta.y;
16747		dz = delta.z;
16748	}
16749
16750	tMatrix = new Matrix(4, 4, true);
16751	tMatrix[0][3] = dx;
16752	tMatrix[1][3] = dy;
16753	tMatrix[2][3] = dz;
16754
16755	this.matrix = tMatrix.multiply(this.matrix);
16756	
16757	return this;
16758};
16759
16760/**
16761 * Apply scale transformation
16762 * @param {Number} scaleX X Scaling factor
16763 * @param {Number} scaleY Y Scaling factor
16764 * @param {Number} scaleZ Z Scaling factor
16765 * @returns {Transform} this
16766 */
16767Transform.prototype.scale = function(scaleX, scaleY, scaleZ) {
16768	scaleY = (typeof scaleY === 'undefined' ? scaleX : scaleY);
16769	scaleZ = (typeof scaleZ === 'undefined' ? scaleX : scaleZ);
16770
16771	var sMatrix = new Matrix(4, 4, true);
16772	sMatrix[0][0] = scaleX;
16773	sMatrix[1][1] = scaleY;
16774	sMatrix[2][2] = scaleZ;
16775	
16776	this.matrix = sMatrix.multiply(this.matrix);
16777	return this;
16778};
16779
16780
16781/**
16782 * Reset transformation matrix
16783 * @returns {Transform} this
16784 */
16785Transform.prototype.reset = function() {
16786	this.matrix = new Matrix(4, 4, true);
16787	return this;
16788};
16789/*globals myDeco, $, Point */
16790/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
16791          sloppy: true, white: true */
16792/**
16793 * Utility function to find intersection point of two sections (walls).
16794 * @param {Array of Point} wall1 First wall (array of two Points)
16795 * @param {Array of Point} wall2 Second wall
16796 * @param {Boolean} real check if intersection point is in bound of sections.
16797 * @param {Boolean} relative Return relative coords on both lines or real Point.
16798 * @return {Point}
16799 */
16800function findWallIntersection (wall1, wall2, real, relative) {
16801	// Math from http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/
16802	var dx1 = wall1[1].x - wall1[0].x,
16803		dy1 = wall1[1].y - wall1[0].y,
16804		dx2 = wall2[1].x - wall2[0].x,
16805		dy2 = wall2[1].y - wall2[0].y,
16806		d = dx2 * dy1 - dx1 * dy2,
16807		point, di, dx, dy, s, t;
16808
16809	if (Math.abs(d) < 0.001) {
16810		return false;
16811	}
16812
16813	di = 1.0 / d;
16814	dx = wall2[0].x - wall1[0].x;
16815	dy = wall2[0].y - wall1[0].y;
16816	s = (dx2 * dy - dy2 * dx) * di;
16817
16818	if (real && (s > 1 || s < 0)) {
16819		return false;
16820	}
16821
16822	t = (dx1 * dy - dy1 * dx) * di;
16823
16824	if (real && (t > 1 || t < 0)) {
16825		return false;
16826	}
16827		
16828	point = new Point(wall1[0].x + dx1 * s,
16829					  wall1[0].y + dy1 * s,
16830					  0);
16831
16832	if (relative) {
16833		return {
16834			s: s,
16835			t: t, 
16836			point: point
16837		};
16838	} else {
16839		return point;
16840	}
16841}
16842
16843function weilerAtherton(poly1, poly2, operation) {
16844	// Algo from http://cs1.bradley.edu/public/jcm/weileratherton.html
16845	// http://www.anirudh.net/practical_training/main/node10.html
16846
16847	// convert array to doubly circular linked list
16848	function arrayToLinkList(array) {
16849		var i, length = array.length, item;
16850		
16851		for (i = 0; i < length; i++) {
16852			item = array[i];
16853			
16854			item.prev = i === 0 ? array[length - 1] : array[i - 1];
16855			item.next = i === length - 1 ? array[0] : array[i + 1];
16856		}
16857		
16858		return array[0];
16859	}
16860
16861	// convert doubly circular linked list to array
16862	function linkListToArray(item) {
16863		var result = [],
16864			curr = item, first = true;
16865		
16866		while (curr !== item || first) {
16867			first = false;
16868			result.push(curr);
16869			curr = curr.next;
16870		}
16871		
16872		return result;
16873	}
16874	
16875	// Break linked list apart
16876	function breakLinkList(item) {
16877		var temp, curr = item;
16878		
16879		while (curr !== null) {
16880			curr.prev = null;
16881			curr.link = null;
16882			temp = curr.next;
16883			curr.next = null;
16884			curr = temp;
16885		}
16886	}
16887	
16888	// Sort intersection points added between points of original poly
16889	// by their position on edge to keep original order of points in poly 
16890	function sortAndInject(orig, inter) {
16891		var i, len1 = orig.length,
16892			j, len2, result = [],
16893			sortFunc = function (a, b) {
16894				return b.pos - a.pos;
16895			};
16896			
16897		for (i = 0; i < len1; i++) {
16898			result.push(orig[i]);
16899
16900			if (inter[i]) {
16901				inter[i].sort(sortFunc);
16902				
16903				for (j = 0, len2 = inter[i].length; j < len2; j++) {
16904					result.push(inter[i][j]);
16905				}
16906			}
16907		}
16908		
16909		return result;
16910	}
16911
16912	var poly1Intersections = [], poly2Intersections = [], enterings = [],
16913		i1, i2, j1, j2,
16914		len1 = poly1.length, len2 = poly2.length,
16915		intersection, pointInPoly1, pointinPoly2, enterTest, result,
16916		currPoint, startPoint, clippedPolys = [], poly, first;
16917	
16918	
16919	// Find intersections between any edge of poly1ect poly and any edge of poly2 poly.
16920	for (i1 = 0, i2 = len1 - 1; i1 < len1; i2 = i1++) {
16921		for (j1 = 0, j2 = len2 - 1; j1 < len2; j2 = j1++) {
16922			intersection = findWallIntersection([poly1[i1], poly1[i2]], [poly2[j1], poly2[j2]], true, true);
16923			if (intersection) {
16924
16925				if (!poly1Intersections[i2]) {
16926					poly1Intersections[i2] = [];
16927
16928					poly1Intersections[i2].from = poly1[i2];
16929					poly1Intersections[i2].to = poly1[i1];
16930				}
16931					
16932				if (!poly2Intersections[j2]) {
16933					poly2Intersections[j2] = [];
16934
16935					poly2Intersections[j2].from = poly2[j2];
16936					poly2Intersections[j2].to = poly2[j1];
16937				}
16938					
16939				// Check, if edge of poly1 poly is entering to poly2 poly.
16940				enterTest = poly1[i2].clone().rotate(poly2[j1], -poly2[j2].angleTo(poly2[j1]));
16941					
16942				pointInPoly1 = intersection.point.clone();
16943				pointinPoly2 = intersection.point.clone();
16944
16945				pointinPoly2.entering = (enterTest.y >= poly2[j1].y);
16946
16947				if (pointinPoly2.entering) {
16948					enterings.push(pointinPoly2);
16949				}
16950				
16951				// create links between same intersection points inserted into poly1 and poly2 polys
16952				pointInPoly1.link = pointinPoly2;
16953				pointInPoly1.pos = intersection.s;
16954				pointinPoly2.link = pointInPoly1;
16955				pointinPoly2.pos = intersection.t;
16956				pointinPoly2.inPoly2 = true;
16957
16958				// add found intersection points into poly1 and poly2 polygons for further sorting and expanding
16959				poly1Intersections[i2].push(pointInPoly1);
16960				poly2Intersections[j2].push(pointinPoly2);
16961			}
16962		}
16963	}
16964	
16965	// Escape if no intersections found.
16966	if (enterings.length === 0) {
16967		return false;
16968	}
16969	
16970	// Expand stored intersection points in poly1 and poly2 polygons and convert polygons to linked list
16971	poly1 = sortAndInject(poly1, poly1Intersections);
16972
16973	if (operation === 'clip') {
16974		poly1 = poly1.reverse();
16975	}
16976	
16977	poly1 = arrayToLinkList(poly1);
16978	poly2 = arrayToLinkList(sortAndInject(poly2, poly2Intersections));
16979	
16980	// Find shapes which was cut from poly1 polygon by poly2 polygon
16981	for (i1 = 0, len1 = enterings.length; i1 < len1; i1++) {
16982		if (enterings[i1]) {
16983			poly = [];
16984			clippedPolys.push(poly);
16985
16986			startPoint = currPoint = enterings[i1].link;
16987			first = true;
16988			delete(enterings[i1]);
16989			
16990			while (currPoint !== startPoint || first) {
16991				first = false;
16992				
16993				// Collecting only points from poly2 polygon in our resulting polygons, not
16994				// their twines from poly1
16995				poly.push((!currPoint.link || currPoint.inPoly2) ? currPoint : currPoint.link);
16996				
16997				if (currPoint.link) {
16998					for (i2 = 0; i2 < len1; i2++) {
16999						if (enterings[i2] === currPoint ||
17000							enterings[i2] === currPoint.link) {
17001							delete enterings[i2];
17002							break;
17003						}
17004					}
17005					currPoint = currPoint.link.next;
17006				} else {
17007					currPoint = currPoint.next;
17008				}
17009			}
17010			poly.reverse();
17011		}
17012	}
17013	
17014	if (operation === 'clip') { 
17015		result = {
17016			// new area with corners added
17017			clip: linkListToArray(poly2),
17018			// new areas clipped from old one
17019			clipped: clippedPolys,
17020			// points were added
17021			added: {
17022				subj: poly1Intersections,
17023				clip: poly2Intersections
17024			}
17025		};
17026	} else {
17027		result = clippedPolys;
17028	}
17029	
17030	// Prevent possible memleaks.
17031	breakLinkList(poly1);
17032	breakLinkList(poly2);
17033	
17034	return result;
17035}
17036
17037/**
17038 * Function for determining of closed shape area
17039 * @param {Array} points
17040 * @returns {Number}
17041 */
17042function findShapeArea(points) {
17043	var i,
17044		n = points.length,
17045		s = 0;
17046
17047    if (n == 0)
17048        return 0;
17049	
17050	// Classical math from http://en.wikipedia.org/wiki/Polygon#Area_and_centroid
17051	for (i = 0; i < n - 1; i++) {
17052		s += points[i].x * points[i + 1].y - points[i + 1].x * points[i].y;
17053	}
17054	
17055	s += points[n - 1].x * points[0].y - points[0].x * points[n - 1].y;
17056	
17057	return s / 2.0;
17058}
17059
17060/**
17061 * Function to determine shape center
17062 * @param {Array} points
17063 * @returns {Point}
17064 */
17065function findPointInShape(points) {
17066	var i, j, ilen = points.length, jlen, x = 0, y = 0, jn, point;
17067	
17068	// calculate center point for triangle as middle point
17069	if (points.length < 4) {
17070		for (i = 0, ilen = points.length; i < ilen; i++) {
17071			x += points[i].x;
17072			y += points[i].y;
17073		}
17074		
17075		return new Point(x / ilen, y / ilen);
17076	} else {
17077		// in other case try to find diagonal
17078		for (i = 0; i < ilen; i++) {
17079			for (j = i + 2, jlen = i + ilen - 1; j < jlen; j++) {
17080				jn = j % ilen;
17081				point = new Point(
17082					(points[i].x + points[jn].x) / 2,
17083					(points[i].y + points[jn].y) / 2
17084				);
17085				if (point.inPoly(points)) {
17086					return point;
17087				}
17088			}
17089		}
17090	}
17091};
17092/*globals myDeco, $, FpeModel */
17093/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
17094          sloppy: true, white: true */
17095/**
17096 * @fileOverview FPE dispatcher
17097 */
17098myDeco.App = {
17099	/**
17100	 * views/plugins settings
17101	 */
17102	settings: {
17103		views: {},
17104		plugins: {}
17105	},
17106	/**
17107	 * List of registered controllers
17108	 * @type {Array}
17109	 */
17110	controllers: [],
17111	
17112	/**
17113	 * List of registered views
17114	 * @type {Array}
17115	 */
17116	views: [],
17117	
17118	/**
17119	 * List of registered plugins
17120	 * @type {Array}
17121	 */
17122	plugins: [],
17123	
17124	/**
17125	 * Dispatcher object
17126	 */
17127	dispatcher: {
17128		/**
17129		 * Started flag
17130		 * @type {Boolean}
17131		 * @private
17132		 */
17133		_started: false,
17134		
17135		/**
17136		 * List of callbacks that will executed after app initialization
17137		 * @type {Array}
17138		 * @private
17139		 */
17140		_callOnInit: [],
17141		
17142		/**
17143		 * Init function
17144		 * @param {Object} options Dispatcher startup options
17145		 */
17146		init: function(options) {
17147			// default layout and app holder
17148			options.app = options.app || $('#app');
17149
17150			// check for layout
17151			options.layout = options.layout || options.app;
17152			
17153			// instantiate model
17154			switch (options.model.type) {
17155				case '3d': 
17156					myDeco.App.model = new FpeModel(options.model, myDeco.App.settings.model);
17157					break;
17158				case 'montage': 
17159					myDeco.App.model = new MontageModel(options.model, {});
17160					break;
17161				default:
17162					myDeco.App.model = new AppModel(options.model, myDeco.App.settings.model);
17163			}
17164
17165			// fill options with empty arrays if needed
17166			options = $.extend({
17167				controllers: [],
17168				views: [],
17169				plugins: []
17170			}, options);
17171			
17172			var i, ilen, type, typeOptions, ControllerClass, controller,
17173				viewPlugins, canvas, viewOptions, view,
17174				controllers = $.grep(myDeco.App.controllers, function(c) {
17175					return $.inArray(c, options.controllers.ignore || []) < 0;
17176				}),
17177				plugins = $.grep(myDeco.App.plugins, function(c) {
17178					return $.inArray(c, options.plugins.ignore || []) < 0;
17179				}),
17180				views = $.grep(myDeco.App.views, function(c) {
17181					return $.inArray(c, options.views.ignore || []) < 0;
17182				}),
17183				typeFilter = function(item) {
17184					return item.$type === type;
17185				},
17186				convertPlugins = function(plugin) {
17187					return {
17188						'class': plugin,
17189						'settings': myDeco.App.settings.plugins[plugin.$name]
17190					};
17191				};
17192			
17193			// iterate views list and init found items
17194			for (i = 0, ilen = views.length; i < ilen; i++) {
17195				// view type name
17196				type = views[i].$type;
17197				
17198				// view default options
17199				typeOptions = options.views && options.views[type] ? options.views[type] : {};
17200						
17201				// default controller constructor for this view
17202				ControllerClass = $.grep(controllers, typeFilter).shift();
17203				
17204				// controller if exists
17205				controller = ControllerClass ? new ControllerClass(typeOptions.model || myDeco.App.model, $.extend(true, {},
17206						myDeco.App.settings.controllers.common || {}
17206, myDeco.App.settings.controllers[type] || {})) : undefined;
17207						
17208				// list of plugin classes for current view
17209				viewPlugins = $.grep(plugins, typeFilter);
17210				canvas = options.layout.find(typeOptions.canvasSelector || '#view-' + type);
17211				
17212				// finally view options
17213				viewOptions = $.extend({
17214					model: myDeco.App.model,
17215					controller: controller,
17216					
17217					// merge plugin classes with its options
17218					plugins: $.map(viewPlugins, convertPlugins),
17219					
17220					// default canvas should have ID like "view-viewType"
17221					canvas: canvas.size() === 0 ? options.app : canvas,
17222					app: options.app,
17223					settings: myDeco.App.settings.views[type]
17224				}, typeOptions);
17225				
17226				// view itself
17227				view = new views[i](viewOptions);
17228				// register view in controller if exists
17229				if (controller) {
17230					controller.registerView(view);
17231				}
17232			}
17233			
17234			// check for stored callbacks
17235			this._started = true;
17236			$.each(this._callOnInit, function() {
17237				this.call(null);
17238			});
17239		},
17240		
17241		/**
17242		 * Lazy loaded callback execution
17243		 * @param {Function} callback
17244		 */
17245		callOnInit: function(callback) {
17246			if (!$.isFunction(callback)) {
17247				return;
17248			}
17249			
17250			if (this._started) {
17251				callback();
17252			} else {
17253				this._callOnInit.push(callback);
17254			}
17255		}
17256	}
17257};
17258(function() {
17259  myDeco.new_search = {
17260    apply_wl_settings: function(search) {
17261      var wl_filter, wl_sort;
17262      if (wl_filter = myDeco.settings.WL.WL_NEW_SEARCH_FILTER) {
17263        search.must.push(JSON.parse(wl_filter));
17264      }
17265      if (wl_sort = myDeco.settings.WL.WL_NEW_SEARCH_SORT) {
17266        return search.sort.unshift(JSON.parse(wl_sort));
17267      }
17268    },
17269    apply_color_filter: function(search, color) {
17270      var cols, functions, h, hstep, l, ref, rows, s, vstep;
17271      rows = myDeco.settings.PALETTE_ROWS;
17272      cols = myDeco.settings.PALETTE_COLS;
17273      hstep = 0.5 / cols;
17274      vstep = 0.5 / rows;
17275      ref = pal2hsl(color, rows, cols), h = ref[0], s = ref[1], l = ref[2];
17276      functions = [];
17277      if (h === 0 && s === 0) {
17278        functions.push({
17279          exp: {
17280            color_l: {
17281              origin: l,
17282              scale: 1 / 72
17283            }
17284          }
17285        });
17286        search.must.push({
17287          range: {
17288            color_l: {
17289              gte: l - hstep,
17290              lte: l + hstep
17291            }
17292          }
17293        }, {
17294          term: {
17295            color_h: 0
17296          }
17297        }, {
17298          term: {
17299            color_s: 0
17300          }
17301        });
17302      } else {
17303        functions.push({
17304          exp: {
17305            color_h: {
17306              origin: h,
17307              scale: 5
17308            }
17309          }
17310        }, {
17311          exp: {
17312            color_s: {
17313              origin: s,
17314              scale: 1 / 60
17315            }
17316          }
17317        }, {
17318          exp: {
17319            color_l: {
17320              origin: l,
17321              scale: 1 / 9
17322            }
17323          }
17324        });
17325        search.must.push({
17326          range: {
17327            color_h: {
17328              gte: h - hstep,
17329              lte: h + hstep
17330            }
17331          }
17332        }, {
17333          range: {
17334            color_s: {
17335              gte: s - vstep,
17336              lte: s + vstep
17337            }
17338          }
17339        });
17340      }
17341      return search.queries.push({
17342        function_score: {
17343          score_mode: "multiply",
17344          functions: functions
17345        }
17346      });
17347    },
17348    apply_dimensions_filter: function(search, dims) {
17349      var functions;
17350      search.must.push({
17351        range: {
17352          width: {
17353            lte: dims.w
17354          }
17355        }
17356      }, {
17357        range: {
17358          depth: {
17359            lte: dims.d
17360          }
17361        }
17362      });
17363      search.must_not.push({
17364        term: {
17365          level: -1
17366        }
17367      });
17368      functions = [
17369        {
17370          exp: {
17371            width: {
17372              origin: dims.w,
17373              scale: dims.w / 2
17374            }
17375          }
17376        }, {
17377          exp: {
17378            depth: {
17379              origin: dims.d,
17380              scale: dims.d / 2
17381            }
17382          }
17383        }
17384      ];
17385      return search.queries.push({
17386        function_score: {
17387          score_mode: "multiply",
17388          functions: functions,
17389          boost: 10
17390        }
17391      });
17392    },
17393    build_fetch_query: function(ids, kind) {
17394      var fields, filter, id, size, source;
17395      fields = '*';
17396      source = false;
17397      size = ids.length;
17398      filter = {
17399        ids: {
17400          values: ids
17401        }
17402      };
17403      if (kind === 'product') {
17404        size = size * 50;
17405        filter = {
17406          terms: {
17407            parent: ids
17408          }
17409        };
17410      }
17411      if (kind === 'material') {
17412        filter = {
17413          ids: {
17414            values: (function() {
17415              var i, len, results;
17416              results = [];
17417              for (i = 0, len = ids.length; i < len; i++) {
17418                id = ids[i];
17419                results.push(id.toString());
17420              }
17421              return results;
17422            })()
17423          }
17424        };
17425        fields = false;
17426        source = true;
17427      }
17428      return JSON.stringify({
17429        filter: filter,
17430        fields: fields,
17431        _source: source,
17432        size: size
17433      });
17434    },
17435    build_materials_query: function(params) {
17436      var expr, must, ref, wl_filter;
17437      must = [];
17438      expr = {
17439        _source: true,
17440        from: params.from,
17441        size: params.size,
17442        filter: {
17443          bool: {
17444            must: must
17445          }
17446        },
17447        sort: {
17448          created_at: "desc"
17449        }
17450      };
17451      if (wl_filter = (ref = myDeco.settings.WL) != null ? ref.MATERIALS_SEARCH_FILTER : void 0) {
17452        must.push(JSON.parse(wl_filter));
17453      }
17454      if (params.material_class) {
17455        must.push({
17456          term: {
17457            class_name: params.material_class
17458          }
17459        });
17460      }
17461      if (params.name) {
17462        expr.query = {
17463          match: {
17464            name: params.name
17465          }
17466        };
17467      }
17468      return JSON.stringify(expr);
17469    },
17470    build_query: function(params) {
17471      var expr, filter, i, len, ref, search;
17472      search = {
17473        sort: params.sort || [
17474          {
17475            completed_at: 'desc'
17476          }
17477        ],
17478        must: [],
17479        must_not: [],
17480        queries: []
17481      };
17482      if (myDeco.settings.WL && !params.skip_wl_filters) {
17483        this.apply_wl_settings(search);
17484      }
17485      if (params.name) {
17486        search.queries.push({
17487          multi_match: {
17488            query: params.name,
17489            fields: ["name", "brand^3", "supplier_name^3"]
17490          }
17491        });
17492      }
17493      if (params.category_id) {
17494        search.must.push({
17495          term: {
17496            tags: parseInt(params.category_id)
17497          }
17498        });
17499      }
17500      if (params.filters) {
17501        ref = params.filters;
17502        for (i = 0, len = ref.length; i < len; i++) {
17503          filter = ref[i];
17504          search.must.push(filter);
17505        }
17506      }
17507      if (params.color) {
17508        this.apply_color_filter(search, params.color);
17509      }
17510      if (params.dimensions) {
17511        this.apply_dimensions_filter(search, params.dimensions);
17512      }
17513      expr = {
17514        filter: {
17515          bool: {
17516            must: search.must,
17517            must_not: search.must_not
17518          }
17519        },
17520        fields: "*",
17521        from: params.from,
17522        size: params.size,
17523        sort: search.sort
17524      };
17525      if (search.queries.length > 0) {
17526        expr.sort.unshift({
17527          _score: "desc"
17528        });
17529        expr.query = {
17530          bool: {
17531            must: search.queries
17532          }
17533        };
17534      }
17535      return JSON.stringify(expr);
17536    },
17537    mydeco_path: function(model) {
17538      return (model.substr(0, 2)) + "/" + model;
17539    },
17540    render_path: function(model, kind) {
17541      return "https://fp-media-img-cdn.floorplanner.com/cdb/renders/" + (this.mydeco_path(model)) + "." + kind + ".png";
17542    },
17543    scaled_render_path: function(model, kind, side) {
17544      return "https://fp-media-img-cdn.floorplanner.com/cdb/scaled/renders/" + (this.mydeco_path(model)) + "." + kind + ".png_w" + side + "h" + side + ".png";
17545    },
17546    product_image_thumb: function(base, kind) {
17547      if (kind === '110') {
17548        return "https://dnkstn0pmvirh.cloudfront.net/product_images/thumbs/110/" + base + ".jpg";
17549      } else {
17550        return "https://dnkstn0pmvirh.cloudfront.net/product_images/nobkg/270/" + base + ".png";
17551      }
17552    },
17553    texture_thumb_url: function(filename) {
17554      return "https://fp-media-img-cdn.floorplanner.com/cdb/textures/floor_and_wall/thumbn
17554ails/" + filename;
17555    },
17556    texture_path: function(filename) {
17557      return "floor_and_wall/original/" + filename;
17558    },
17559    es_to_legacy_item: function(item) {
17560      var attributes, i, len, model, ref, view;
17561      model = item.fields.model[0];
17562      attributes = item.fields;
17563      attributes.model = ["models/" + (this.mydeco_path(model)) + ".mi"];
17564      attributes.bounding_box = [attributes.bbox_min.slice(0), attributes.bbox_max.slice(0)];
17565      attributes.renders = {};
17566      ref = ['top', 'front', 'vir270w1', 'perspective'];
17567      for (i = 0, len = ref.length; i < len; i++) {
17568        view = ref[i];
17569        attributes.renders[view] = this.render_path(model, view);
17570      }
17571      attributes.render_path = [attributes.renders.perspective];
17572      return attributes;
17573    },
17574    es_to_legacy: function(data, kind) {
17575      var i, item, j, k, len, len1, len2, ref, ref1, ref2, result;
17576      switch (kind) {
17577        case 'component_search':
17578          return {
17579            results: {
17580              component: {
17581                estmatches: data.hits.total,
17582                items: (function() {
17583                  var i, len, ref, results;
17584                  ref = data.hits.hits;
17585                  results = [];
17586                  for (i = 0, len = ref.length; i < len; i++) {
17587                    item = ref[i];
17588                    results.push([item._id, this.es_to_legacy_item(item)]);
17589                  }
17590                  return results;
17591                }).call(this)
17592              }
17593            }
17594          };
17595        case 'fetch':
17596          result = {};
17597          ref = data.hits.hits;
17598          for (i = 0, len = ref.length; i < len; i++) {
17599            item = ref[i];
17600            result[item._id] = this.es_to_legacy_item(item);
17601          }
17602          return {
17603            products: result
17604          };
17605        case 'product_search':
17606          return {
17607            results: {
17608              product: {
17609                estmatches: data.hits.total,
17610                items: (function() {
17611                  var j, len1, ref1, results;
17612                  ref1 = data.hits.hits;
17613                  results = [];
17614                  for (j = 0, len1 = ref1.length; j < len1; j++) {
17615                    item = ref1[j];
17616                    results.push([item.fields.parent[0], this.es_to_product(item)]);
17617                  }
17618                  return results;
17619                }).call(this)
17620              }
17621            }
17622          };
17623        case 'product_fetch':
17624          result = {};
17625          ref1 = data.hits.hits;
17626          for (j = 0, len1 = ref1.length; j < len1; j++) {
17627            item = ref1[j];
17628            result[item.fields.parent[0]] = this.es_to_product(item);
17629          }
17630          return {
17631            products: result
17632          };
17633        case 'material_fetch':
17634          result = {};
17635          ref2 = data.hits.hits;
17636          for (k = 0, len2 = ref2.length; k < len2; k++) {
17637            item = ref2[k];
17638            result[item._id] = this.es_to_material(item._source);
17639          }
17640          return {
17641            docs: result
17642          };
17643        case 'material_search':
17644          return {
17645            results: {
17646              materials: {
17647                estmatches: data.hits.total,
17648                items: (function() {
17649                  var len3, m, ref3, results;
17650                  ref3 = data.hits.hits;
17651                  results = [];
17652                  for (m = 0, len3 = ref3.length; m < len3; m++) {
17653                    item = ref3[m];
17654                    results.push([item._id, this.es_to_material(item._source)]);
17655                  }
17656                  return results;
17657                }).call(this)
17658              }
17659            }
17660          };
17661      }
17662    },
17663    es_to_material: function(source) {
17664      var shader_settings;
17665      shader_settings = {
17666        texture_width: source.width,
17667        texture_height: source.height,
17668        diffuse_texture: this.texture_path(source.texture),
17669        diffuse: hex2Diffuse(source.color)
17670      };
17671      $.extend(shader_settings, source.material_properties);
17672      return {
17673        description: source.description,
17674        supplier: source.supplier_name,
17675        "class": source.class_name,
17676        texture_thumbnail: this.texture_thumb_url(source.texture),
17677        shader_settings: shader_settings,
17678        name: source.name
17679      };
17680    },
17681    es_to_product: function(item) {
17682      var attributes, model, product_image;
17683      attributes = item.fields;
17684      attributes.available = [true];
17685      attributes.description || (attributes.description = ['']);
17686      product_image = attributes.product_image;
17687      if (product_image) {
17688        attributes.thumbnail_nobkg_270 = [this.product_image_thumb(product_image[0], 'nobkg_270')];
17689        attributes.thumbnail_110 = [this.product_image_thumb(product_image[0], '110')];
17690      } else {
17691        model = item.fields.model[0];
17692        attributes.thumbnail_nobkg_270 = [this.render_path(model, 'perspective')];
17693        attributes.thumbnail_110 = [this.scaled_render_path(model, 'perspective', 110)];
17694      }
17695      return attributes;
17696    },
17697    get_used_categories: function(callback) {
17698      var expr, wl_filter;
17699      expr = {
17700        aggs: {
17701          filtered: {
17702            filter: {
17703              match_all: {}
17704            },
17705            aggs: {
17706              categories: {
17707                terms: {
17708                  field: "tags",
17709                  size: 0
17710                }
17711              }
17712            }
17713          }
17714        }
17715      };
17716      if (myDeco.settings.WL && (wl_filter = myDeco.settings.WL.WL_NEW_SEARCH_FILTER)) {
17717        expr.aggs.filtered.filter = JSON.parse(wl_filter);
17718      }
17719      return $.ajax({
17720        type: 'post',
17721        url: myDeco.urls.ws.new_search + '?search_type=count',
17722        dataType: 'json',
17723        data: JSON.stringify(expr),
17724        success: function(data) {
17725          var bucket, keys;
17726          keys = (function() {
17727            var i, len, ref, results;
17728            ref = data.aggregations.filtered.categories.buckets;
17729            results = [];
17730            for (i = 0, len = ref.length; i < len; i++) {
17731              bucket = ref[i];
17732              results.push(bucket.key);
17733            }
17734            return results;
17735          })();
17736          return callback(keys);
17737        }
17738      });
17739    }
17740  };
17741
17742}).call(this);
17743/*globals $, hex2Diffuse, unArray, myDeco, getArchitecturalPath */
17744/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
17745          sloppy: true, white: true */
17746
17747if (!myDeco.App.ws) {
17748	myDeco.App.ws = {};
17749}
17750
17751/**
17752 * Proxy module for locaStorage or flash storage.
17753 * Will work as session local storage if locaStorage or flash storage isn't
17754 * available
17755 */
17756myDeco.App.ws.storage = {
17757	
17758	/**
17759	 * Data cache until storage isn't ready
17760	 * @type {Object}
17761	 */
17762	_cache: {},
17763	
17764	/**
17765	 * Init function, will copy local content to storage when it is ready
17766	 */
17767	init: function() {
17768		var self = this;
17769		
17770		myDeco.storage.onready(function() {
17771			var key;
17772			
17773			for (key in self._cache) {
17774				myDeco.storage.set(key, self._cache[key]);
17775			}
17776			
17777			self.get = $.proxy(myDeco.storage.get, myDeco.storage);
17778			self.set = $.proxy(myDeco.storage.set, myDeco.storage);
17779		});
17780	},
17781	
17782	/**
17783	 * Get value routine, has the same signature as storage
17784	 * @param {String} key
17785	 * @param {Function} callback
17786	 */
17787	get: function(key, callback) {
17788		callback.call(null, false, this._cache[key]);
17789	},
17790	
17791	/**
17792	 * Set value routine, has the same signature as storage
17793	 * @param {String} key
17794	 * @param {String} value
17795	 */
17796	set: function(key, value) {
17797		this._cache[key] = value;
17798	}
17799};
17800
17801myDeco.App.ws.storage.init();
17802
17803myDeco.App.ws.proxy = {
17804	/**
17805	 * Memory components cache
17806	 * @type {Object}
17807	 */
17808	_cache: {},
17809	
17810	/**
17811	 * List of failed components from ws
17812	 * @type {Object}
17813	 */
17814	_failed: {},
17815	
17816	/**
17817	 * LS cache expire timeout in days, default is to 2 weeks
17818	 * @type {Number}
17819	 */
17820	_expire: 14,
17821	
17822	/**
17823	 * List of available backends
17824	 * @type {Object}
17825	 */
17826	_backends: {
17827		component: {
17828			// urls for fetch by id and search
17829			urls: {
17830				fetch: myDeco.urls.ws.new_search,
17831				search: myDeco.urls.ws.new_search
17832			},
17833			// list of fields to fetch from backend
17834			display: ['bounding_box', 'level', 'surface_height', 'renders', 'name', 'price',
17835						'3ddepth', '3dheight', '3dwidth', 'labels', 'price_before_discount',
17836						'parent', 'model', 'supplier', 'supplier_id', 'render_path', 'locale',
17837						'url', 'product_available', 'description', 'category', 'category_id'],
17838			
17839			fetchReqKey: 'id',
17840			fetchResKey: 'products',
17841			
17842			validateKey: /^[a-z0-9]{40}$/,
17843
17844			// normalize component got from backend
17845			normalize: function(id, data) {
17846				var renders, level, bbox, result = {}, i, conf,
17847					// originally width: 0X, height: 0Y, depth: 0Z
17848					map = {width: 0, height: 1, depth: 2};
17849
17850				if (data._normalized) {
17851					return data;
17852				}
17853				
17854				// set some default values for components with configuration
17855				// and preprocess them
17856				if (data.configuration) {
17857					conf = data.configuration;
17858					result.configurations = {};
17859					result.configuration = {};
17860					data.bounding_box = [[0, 0, 0], [0, 0, 0]];
17861					for (i = conf.length - 1; i >= 0; i--) {
17862						result.configurations[conf[i].name] = conf[i];
17863						switch (conf[i].name) {
17864							case 'width':
17865							case 'height':
17866							case 'depth':
17867								data['3d' + conf[i].name] = conf[i]['default'];
17868								data.bounding_box[0][map[conf[i].name]] =
17869									- conf[i]['default'] / 2;
17870								data.bounding_box[1][map[conf[i].name]] =
17871									- data.bounding_box[0][map[conf[i].name]];
17872								break;
17873							default:
17874								result[conf[i].name] = conf[i]['default'];
17875						}
17876					}
17877				}
17878				
17879				renders = data.renders || {};
17880				bbox = data.bounding_box;
17881				level = +data.level || 0;
17882				result = $.extend(true, result, {
17883					_normalized: true,
17884					component_id: id || '',
17885					componentData: {
17886						name: data.name || '',
17887						price: +data.price || 0,
17888						price_before_discount: +data.price_before_discount || 0,
17889                        has_special_price: data.has_special_price || false,
17890                        out_of_stock: data.out_of_stock || false,
17891						stock: parseInt(data.stock || "0", 10),
17892						supplier: data.supplier || '',
17893						supplier_id: data.supplier_id || '',
17894						supplier_url: (data.supplier_id && data.supplier) ? '/suppliers/' + data.supplier.slugify() + '/' + data.supplier_id + '/'  : '',
17895						locale: data.locale || myDeco.settings.BASE_LOCALE,
17896						//FIXME - mdm replacement only for mydeco products, wl products urls remain the same
17897						url: data.source_url || data.url || '',
17898						available: data.product_available,
17899					    width: Math.abs(bbox[0][0] - bbox[1][0]),
17900					    depth: Math.abs(bbox[0][1] - bbox[1][1]),
17901					    height: Math.abs(bbox[0][2] - bbox[1][2]),
17902						description: data.description || '',
17903						category: data.category || '',
17904						category_id: data.category_id || 0,
17905						'3d': {
17906							level: level,
17907							render_back: renders.back,
17908							render_bottom: renders.bottom,
17909							render_front: renders.front,
17910							render_left: renders.left,
17911							render_perspective: renders.perspective,
17912							render_right: renders.right,
17913							render_top: renders.top
17914						},
17915						labels: data.labels ? [data.labels] : []
17916					},
17917					type: data.type ? "ArchitecturalComponent" : undefined,
17918					category: data.type,
17919					aperture: data.aperture,
17920					level: level,
17921					model: data.model,
17922					position: [0, 0, 0],
17923					rotation: [0, 0, 0],
17924					render_path: data.render_path,
17925					bounding_box: bbox,
17926					width: Math.abs(bbox[0][0] - bbox[1][0]),
17927					depth: Math.abs(bbox[0][1] - bbox[1][1]),
17928					height: Math.abs(bbox[0][2] - bbox[1][2]),
17929					product_id: data.parent || id || '',
17930					surface_height: +data.surface_height || 0
17931				});
17932
17933                if (data["light.wattage"]) {
17934                    result.light = {
17935                        on: true,
17936                        color: hex2RGB(data["light.color"]), 
17937                        wattage: data["light.wattage"],
17938                        color_fixed: data["light.color_fixed"],
17939                        wattage_fixed: data["light.wattage_fixed"]}
17940                }
17941
17942/*	Turned off on Oct 23th to make Falabella WL work
17943
17944				// change url for affiliates
17945				if (result.product_id && result.componentData.locale !== 'en-gb-x-retail') {
17946					result.componentData.url = '/aff/?product=' + result.product_id;
17947				}
17948*/
17949				if (typeof result.revision === 'undefined' &&
17950						typeof result.model === 'string') {
17951					result.revision =
17952						(result.model.match(/_(\d*)\.mi/) || ['00']).pop();
17953				}
17954				return result;
17955			},
17956
17957			get: function(key) {
17958				if (myDeco.App.ws.components && myDeco.App.ws.components[key]) {
17959					return this.normalize('component', key, $.extend(true, {}, myDeco.App.ws.components[key]));
17960
17961				}
17962			}
17963		},
17964		
17965		material: {
17966			urls: {
17967				fetch: myDeco.urls.ws.new_materials,
17968				search: myDeco.urls.ws.new_materials
17969			},
17970			
17971			display: ['class', 'name', 'colour_value', 'texture_thumbnail', 'shader_settings', 'product_id',
17972						'texture_width', 'product_price', 'supplier', 'product_supplier_id', 'description', 'product_available'],
17973			
17974			fetchReqKey: 'docid',
17975			fetchResKey: 'docs',
17976			
17977			validateKey: /^[0-9]+$/,
17978			
17979			normalize: function(id, data) {
17980				if (typeof data.shader_settings === 'string') {
17981					data.shader_settings = $.parseJSON(data.shader_settings);
17982				}
17983				
17984				var supplier = data.supplier || '',
17985					urlReg = /http.*?cdb\/.*?\//,
17986					result = {
17987						component_id: data.component_id || id,
17988						ID: 'decor-' + (data.component_id || id),
17989						name: data.name,
17990						diffuse_texture: data.shader_settings.diffuse_texture || data.texture_thumbnail,
17991						// FIXME: needs backend support
17992						average_colour: data.shader_settings.diffuse || hex2Diffuse(data.colour_value) || [0.5, 0.5, 0.5],
17993						guid: data.product_id,
17994						product_id: data.product_id,
17995						thumbnail: data.texture_thumbnail || data.shader_settings.diffuse_texture,
17996						shader: "combi_material",
17997						texture_width: data.shader_settings.texture_width,
17998						texture_height: data.shader_settings.texture_height,
17999						ambient: data.shader_settings.ambient,
18000						use_diffusemap: data.shader_settings.use_diffusemap,
18001						shiny: data.shader_settings.shiny,
18002						diffuse: data.shader_settings.diffuse,
18003						url: data.product_url,
18004						locale: myDeco.settings.LOCALE,
18005						supplier: supplier,
18006						supplier_url: data.product_supplier_id ? '/suppliers/' + supplier.slugify() + '/' + data.product_supplier_id + '/'  : '',
18007						price: data.product_price || 0,
18008						// TODO: fix this
18009						available: data.product_available,
18010						description: data.description,
18011						'class': $.inArray(data['class'], ['flooring', 'tile', 'tiles', 'wood', 'carpet']) >= 0 ? 'flooring' : 'general'
18012					};
18013				
18014				// remove prefix for textures, will generate automatically later
18015				if (typeof result.diffuse_texture === 'string' && result.diffuse_texture.indexOf('http') === 0) {
18016					result.diffuse_texture = result.diffuse_texture.replace(urlReg, '');
18017				}
18018				
18019				return result;
18020			},
18021			
18022			get: function(key) {
18023				if (typeof key === 'string' && key.substring(0, 1) === '#') {
18024					return myDeco.App.palette.getMaterial(key);
18025				}
18026			}
18027		},
18028		// moodboard products 
18029		product: {
18030			// urls for fetch by id and search
18031			urls: {
18032				fetch: myDeco.urls.ws.new_search,
18033                search: myDeco.urls.ws.new_search
18034			},
18035			// list of fields to fetch from backend
18036			display: ['name', 'url', 'product_page_url', 'thumbnail_110', 'thumbnail_nobkg_270', 'description', 'available'],
18037			
18038			fetchReqKey: 'id',
18039			fetchResKey: 'products',
18040			
18041			validateKey: /^[A-Z0-9]{40}$/,
18042
18043			// normalize data from backend
18044			normalize: function(id, data) {
18045				var result = {}, i, conf;
18046
18047				if (data._normalized) {
18048					return data;
18049				}
18050
18051				result = $.extend(true, result, {
18052					_normalized: true,
18053					productData: {
18054						id: id,
18055						name: data.name || '',
18056						thumbnail: data.thumbnail_110,
18057						url: data.url || data.product_page_url,
18058						description: data.description,
18059						selectable: true,
18060						available: data.available
18061					},
18062					thumbnail: data.thumbnail_110,
18063					thumbnailWidth: 110,
18064					thumbnailHeight: 110,
18065					image: data.thumbnail_nobkg_270 || data.thumbnail_110,
18066					// no thumbnail_nobkg_270 for marketplace products
18067					imageWidth: 270,
18068					imageHeight: 270,
18069					component_id: id	// needed for compatibility with ItemsListView
18070				});
18071
18072				return result;
18073			}
18074		}
18075	},
18076	
18077	/**
18078	 * Returns is this backend type supported or not
18079	 * @param {String} type Backend type
18080	 * @return {Boolean}
18081	 */
18082	has: function(type) {
18083		return typeof this._backends[type] !== 'undefined';
18084	},
18085	
18086	/**
18087	 * Fetch options
18088	 * @param {String} type Database type
18089	 * @return {Object} 
18090	 */
18091	_type: function(type) {
18092		if (!this.has(type)) {
18093			throw "Unknown backend type " + type;
18094		}
18095		
18096		if (!this._cache[type]) {
18097			this._cache[type] = {};
18098		}
18099
18100		if (!this._failed[type]) {
18101			this._failed[type] = {};
18102		}
18103		
18104		return this._backends[type];
18105	},
18106	
18107	/**
18108	 * Get entity structure by type and key
18109	 * @param {String} type Database type
18110	 * @param {String} key
18111	 * @param {Function} callback to fire when data available
18112	 */
18113	get: function(type, key, callback) {
18114		callback = callback || $.noop;
18115		// check for backend
18116		var self = this,
18117			options = this._type(type),
18118			result,
18119			response = function(result) {
18120				result = result || false;
18121				if (options.fix && result) {
18122					result = options.fix(result);
18123				}
18124				callback.call(self, result);
18125			};
18126		
18127		if ($.isFunction(options.get)) {
18128			result = options.get.call(this, key);
18129		}
18130
18131		if (!result) {
18132			if (this._cache[type][key]) {
18133				response(this._cache[type][key]);
18134			} else {
18135				myDeco.App.ws.storage.get(type + key, function(dummy, value) {
18136					if (value) {
18137						value = JSON.parse(value);
18138						if (value[0] + self._expire * 60 * 60 * 24 * 1000 > $.now()) {
18139							value = value[1];
18140						} else {
18141							value = false;
18142						}
18143					}
18144					response(value || false);
18145				});
18146			}
18147		} else {
18148			response(result);
18149		}
18150	},
18151	
18152	/**
18153	 * Store key/value pair for specified type
18154	 * @param {String} type Database type
18155	 * @param {String} key Data key
18156	 * @param {Object} value Data structure
18157	 * @param {Boolean} ls Store value in LS cache (true) or memory
18158	 */
18159	set: function(type, key, value, ls) {
18160		// check for backend
18161		this._type(type);
18162		if (ls) {
18163			try {
18164				// there is no working mechanism to obtain list of all elements
18165				// in LS, so in case of exception throw just put them to memory
18166				// cache. Later it will have much smalles size since we'll have
18167				// a resizables and will be able to stop all thses caches)
18168				myDeco.App.ws.storage.set(type + key, JSON.stringify([$.now(), value]));
18169			} catch(e) {
18170				this._cache[type][key] = value;
18171			}
18172		} else {
18173			this._cache[type][key] = value;
18174		}
18175	},
18176	
18177	/**
18178	 * Fetch entity structure by id(s)
18179	 * @param {String} type Database type
18180	 * @param {Array} list List of ID to fetch
18181	 * @param {Function} callback Response callback, passed param is id=>value dictionary
18182	 * @param {Boolean} ls set local (true)/memory (falce) storage, default is
18183	 *                  false
18184	 */
18185	fetch: function(type, list, callback, ls) {
18186		list = typeof list === 'string' ? [list] : list;
18187		callback = callback || $.noop;
18188		
18189		var self = this,
18190			options = this._type(type),
18191			requests = 0,
18192			data = {
18193				db: type === 'material' ? 'materials' : type,
18194				display: options.display
18195			},
18196			result = {},
18197			dataPreProcessed = false,
18198			processRequest = function() {
18199				if (data[options.fetchReqKey].length === 0) {
18200					return callback.call(null, result);
18201				}
18202
18203                var request_url = options.urls.fetch;
18204                var request_data = myDeco.new_search.build_fetch_query(data[options.fetchReqKey], type);
18205
18206				$.ajax({
18207					type: 'post',
18208					url: request_url,
18209					dataType: 'json',
18210					data: request_data,
18211					success: function(data) {
18212                        switch (type) {
18213                            case 'component':
18214                            data = myDeco.new_search.es_to_legacy(data, 'fetch');
18215                            break;
18216                            case 'product':
18217                            data = myDeco.new_search.es_to_legacy(data, 'product_fetch');
18218                            break;
18219                            case 'material':
18220                            data = myDeco.new_search.es_to_legacy(data, 'material_fetch');
18221                        }
18222						var id;
18223						for (id in data[options.fetchResKey]) {
18224							if (data[options.fetchResKey][id]) {
18225								unArray(data[options.fetchResKey][id]);
18226								result[id] = self.normalize(type, id, data[options.fetchResKey][id]);
18227								self.set(type, id, result[id], ls);
18228							} else {
18229								self._failed[type][id] = true;
18230							}
18231						}
18232						callback.call(null, result);
18233					}
18234				});
18235			};
18236		
18237		data[options.fetchReqKey] = [];
18238		
18239		if (myDeco.settings.SEARCH_SOURCE) {
18240			data.sv = myDeco.settings.SEARCH_SOURCE;
18241		}
18242		
18243		$.each(list.unique(), function(i, id) {
18244			if (!self._failed[type][id]) {
18245				requests++;
18246				
18247				self.get(type, id, function(struct) {
18248					if (struct) {
18249						result[id] = struct;
18250					} else if (options.validateKey.test(id)) {
18251						data[options.fetchReqKey].push(id);
18252					}
18253					requests--;
18254					
18255					if (dataPreProcessed && requests === 0) {
18256						processRequest();
18257					}
18258				});
18259			}
18260		});
18261		
18262		dataPreProcessed = true;
18263		
18264		if (requests === 0) {
18265			processRequest();
18266		}
18267	},
18268	
18269	/**
18270	 * Clear memory storage. If type isn't specified it will clear all caches
18271	 * @param {String} type Cache to clear, defaults to undefined
18272	 */
18273	clear: function(type) {
18274		var i, ilen;
18275		
18276		type = type === 'materials' ? 'material' : type;
18277		if (typeof type !== 'undefined') {
18278			this._type(type);
18279			type = [type];
18280		} else {
18281			type = $.keys(this._cache);
18282		}
18283		
18284		for (i = 0, ilen = type.length; i < ilen; i++) {
18285			this._cache[type[i]] = {};
18286		}
18287	},
18288	
18289	/**
18290	 * Make a search request to a backend
18291	 * @param {Object}
18291 params URL params
18292	 * @param {Function} callback Response callback, passed params are:
18293	 * - response object (excluding result items)
18294	 * - items dictionary as key=>value
18295	 * - list of id of fetched entities
18296	 */
18297	search: function(params, callback) {
18298		var self = this,
18299			type = params.db === 'materials' ? 'material' : params.db,
18300			options = this._type(type);
18301		
18302		if (myDeco.settings.SEARCH_SOURCE) {
18303			params.sv = myDeco.settings.SEARCH_SOURCE;
18304		}
18305		
18306		params.display = options.display;
18307
18308        var facets = params.facet;
18309        var ranges = params.ranges;
18310        var request_url = options.urls.search;
18311        var request_data = params;
18312        var category_id = null;
18313        var dimensions = null;
18314        var query = params.query;
18315        var color = null;
18316        var material_class = null;
18317
18318        if (type == 'material') {
18319            // extract material class from facets
18320            if (facets && facets.length > 0) {
18321                material_class = facets[0].split(",")[1];
18322            }
18323
18324            request_data = myDeco.new_search.build_materials_query({from: params.startrank, 
18325                                                                    size: params.count, 
18326                                                                    material_class: material_class,
18327                                                                    name: params.query});
18328        } else {
18329            // extract category_id when 'new' facets are used
18330            if (facets && facets.length > 0) {
18331                for(var i = 0; i < params.facet.length; i++) {
18332                    var new_facet = facets[i].match(/^new,([0-9]+)$/);
18333                    if (new_facet) {
18334                        category_id = new_facet[1];
18335                        break;
18336                    }
18337                }
18338            }
18339
18340            // extract color from the color facet, if used
18341            if (facets && facets.length > 0) {
18342                for(var i = 0; i < params.facet.length; i++) {
18343                    var color_facet = facets[i].match(/^manual_colour,([0-9a-fA-F]+)$/);
18344                    if (color_facet) {
18345                        color = color_facet[1];
18346                        break;
18347                    }
18348                }
18349            }
18350
18351            // extract dimensions if there is a filter by size
18352            if (ranges && (ranges.length == 2) && ranges[0].match(/^3dwidth/)) {
18353                dimensions = {"w": ranges[0].split(',')[2],
18354                              "d": ranges[1].split(',')[2] };
18355            }
18356
18357            if (type == 'product') {
18358                params.skip_wl_filters = true;
18359                params.filters = params.filters || [];
18360                params.filters.push({exists: {field: "parent"}});
18361            };
18362
18363            request_data = myDeco.new_search.build_query({from: params.startrank, 
18364                                                          size: params.count, 
18365                                                          name: query, 
18366                                                          category_id: category_id,
18367                                                          dimensions: dimensions,
18368                                                          filters: params.filters,
18369                                                          sort: params.sort,
18370                                                          color: color,
18371                                                          skip_wl_filters: params.skip_wl_filters});
18372        }
18373
18374		$.ajax({
18375			type: 'post',
18376			url: request_url,
18377			dataType: 'json',
18378			data: request_data,
18379			success: function(data) {
18380				var dictionary = {}, list = [];
18381
18382                var legacy_type = null;
18383                switch (type) {
18384                    case 'product':
18385                    legacy_type = 'product_search';
18386                    break;
18387                    case 'component':
18388                    legacy_type = 'component_search';
18389                    break;
18390                    case 'material':
18391                    legacy_type = 'material_search';
18392                }
18393                data = myDeco.new_search.es_to_legacy(data, legacy_type);
18394
18395				$.each(data.results[params.db].items, function() {
18396					var id = this.shift(),
18397						model = this.shift();
18398					
18399					unArray(model);
18400					model = self.normalize(type, id, model);
18401					dictionary[id] = model;
18402					list.push(id);
18403				});
18404
18405				delete data.results[params.db].items;
18406				
18407				if ($.isFunction(callback)) {
18408					callback.call(null, data, dictionary, list);
18409				}
18410			}
18411		});
18412	},
18413	
18414	/**
18415	 * Normalize passed entity according to database type
18416	 * @param {String} type Database type
18417	 * @param {String} id Entity ID
18418	 * @param {Object} data Entity structure
18419	 */
18420	normalize: function(type, id, data) {
18421		var options = this._type(type),
18422			struct = options.normalize.call(this, id, data);
18423			
18424		return struct;
18425	},
18426	
18427	/**
18428	 * Main init function
18429	 */
18430	init: function() {
18431		if (myDeco.settings.WL) {
18432			this._backends.component.display.push('source_url');
18433		}
18434	}
18435};
18436
18437$(function() {
18438	myDeco.App.ws.proxy.init();
18439});
18440/*globals Class, $, myDeco, emptyRoom */
18441/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
18442          sloppy: true, white: true */
18443/*
18444 * Abstract room model class for apps (FPE, montage, DMP)
18445 */
18446var AppModel = Class.extend({
18447	/**
18448	 * Room loaded flag
18449	 * @type {Boolean}
18450	 */
18451	loaded: false,
18452	
18453	/**
18454	 * Model settings
18455	 * @type {Object}
18456	 * @public
18457	 */
18458	settings: {
18459		// max depth of undo
18460		historyLimit: 100
18461	},
18462	
18463	/**
18464	 * Editing history object
18465	 */
18466	history: {
18467		changes: [],
18468		position: -1,
18469		counter: 0
18470	},
18471	
18472	/**
18473	 * Room metadata
18474	 * @type {Object}
18475	 */
18476	_metadata: {},
18477	
18478	/**
18479	 * Is current room already saved or a brand new one
18480	 * @type {Boolean}
18481	 */
18482	isNew: true,
18483	
18484	/**
18485	 * URL to a web page of this model
18486	 * @type {String}
18487	 */
18488	weburl: '',
18489	
18490	/**
18491	 * Room WS url
18492	 * @type {String}
18493	 */
18494	_ws_url: '',
18495	
18496	/**
18497	 * Constructor
18498	 * @param {Object} rawModel Room model
18499	 */
18500	init: function(rawModel, settings) {
18501		var self = this;
18502		
18503		this.settings = $.extend(this.settings, settings || {});
18504	
18505		if (rawModel) {
18506			this.load(rawModel);
18507		} else {
18508			this.reset();
18509		}
18510		
18511		myDeco.body
18512			.bind('loadRoom', function(e, urlOrModel) {
18513				myDeco.body.trigger('clearRoom');
18514				self.loadRoom(urlOrModel);
18515			})
18516			.bind('roomMetadata', this.metadata.bind(this))
18517			.bind('saveRoom', function(e, options) {
18518				self.saveRoom(options);
18519			});
18520	},
18521	
18522	metadata: function(e, callbackOrData) {
18523		if ($.isFunction(callbackOrData)) {
18524			callbackOrData(this._metadata, this._room_id);
18525		} else {
18526			this._metadata = $.extend(true, this._metadata, callbackOrData);
18527		}
18528	},
18529	
18530	/**
18531	 * Reset the model to initial state
18532	 */
18533	reset: function() {
18534		this._backup = false;
18535		this._room_id = 0;
18536		this.loaded = false;
18537		this.weburl = '';
18538	},
18539	
18540	/**
18541	 * Stup for method for making web url
18542	 */
18543	_makeWebUrl: function() {
18544		this.weburl = this.isNew ? '' : myDeco.urls.roomDetails.replace(/test-room-0/, this._metadata.name.slugify() + '-' + this._room_id);
18545	},
18546
18547	/**
18548	 * Undo the last action
18549	 */
18550	undo: function() {
18551		this.history.counter = Math.max(0, this.history.counter - 1);
18552	},
18553
18554	/**
18555	 * Redo the last action
18556	 */
18557	redo: function() {
18558		this.history.counter++;
18559	},
18560	
18561	/**
18562	 * Base method for room loading
18563	 * @oparam {Object} rawModel
18564	 */
18565	load: function(rawModel) {
18566		var name, value;
18567		this.isNew = !rawModel.id || !myDeco.username || myDeco.username !== rawModel.author;
18568		
18569		this._ws_url = rawModel.parent || this._ws_url;
18570		this._room_id = (this.isNew ? '' : rawModel.id) || '';
18571		
18572		// metadata
18573		for (name in rawModel) {
18574			if ($.inArray(name, ['model', 'products']) < 0) {
18575				value = rawModel[name];
18576				if ($.inArray(name, ['name', 'category', 'style']) !== -1) {
18577					value = value.fromHtml();
18578				}
18579				this._metadata[name] = value;
18580			}
18581		}
18582		this._makeWebUrl();
18583	},
18584
18585	/**
18586	 * Load room from given url
18587	 * @param {String|Model} url Url to room ws or it's model.
18588	 */
18589	loadRoom: function(urlOrModel) {
18590		var self = this;
18591		
18592		myDeco.body.trigger('roomLoading', [urlOrModel]);
18593		if (typeof(urlOrModel) === 'string') {
18594			$.ajax({
18595				type: 'get',
18596				dataType: 'json',
18597				url: urlOrModel,
18598				success: function(data) {
18599					self._ws_url = self._fixUrl(urlOrModel);
18600					self.load(data);
18601				},
18602				error: function() {
18603					myDeco.body.trigger('roomLoadError');
18604				}
18605			});
18606		} else {
18607			self._ws_url = '';
18608			self.load(urlOrModel);
18609		}
18610		
18611		this.history = {
18612			changes: [],
18613			position: -1,
18614			counter: 0
18615		};
18616	},
18617
18618	/**
18619	 * Helper to fix room url if needed
18620	 * @param {String} url Incoming url
18621	 * @returns {String}
18622	 */
18623	_fixUrl: function(url) {
18624		return url;
18625	},
18626	
18627	/**
18628	 * Save current model to backend
18629	 * 
18630	 * @param {Object} options Save options, expected keys/values are:
18631	 *    {Boolean} forceNew Create new room or replace old one
18632	 *    {Object} settings Top-level settings of model
18633	 *    {Object} data Already wrapped room data
18634	 *    {Function} successCallback Callback which will be executed after success of ajax request
18635	 *    {String} urlParams Params which will be added to the save url
18636	 */
18637	saveRoom: function(options) {
18638		// TODO: implement login
18639		if (!myDeco.username) {
18640			return false;
18641		}
18642		options = options || {};
18643
18644		options.forceNew = options.forceNew || this.isNew ||
18645			this._ws_url === '' || this.restoreOwner;
18646
18647		options.isNew = options.forceNew || !this._room_id;
18648
18649		var self = this,
18650			url = options.forceNew ? myDeco.urls.ws.userRooms : this._ws_url,
18651			roomData = options.data || this._wrapRoom(options.settings);
18652
18653		if (options.restoreOwner) {
18654            options.urlParams = (options.urlParams ? options.urlParams + '&' : '') + 'restore_owner=1';
18655		}
18656		
18657		if (options.urlParams) {
18658			url += (url.indexOf('?') === -1 ? '?' : '&') + options.urlParams;
18659		}
18660
18661        // let plugins add data to the room before saving it
18662        for (var i=0; i < myDeco.settings.SAVE_CALLBACKS.length; i++) {
18663            myDeco.settings.SAVE_CALLBACKS[i](roomData);
18664        }
18665		
18666		$.ajax({
18667			type: options.isNew ? 'POST' : 'PUT',
18668			url: url,
18669			contentType: "application/json",
18670			dataType: "json",
18671			data: JSON.stringify(roomData),
18672			beforeSend: function() {
18673				myDeco.body.trigger('roomSaving',
18674					[roomData.type, self._room_id, options]);
18675			},
18676			success: function(data, textStatus) {
18677				if (data !== null) { 
18678					self._ws_url = self._fixUrl(data.location);
18679					self._room_id = data.id;
18680					self.isNew = false;
18681					self._makeWebUrl();
18682					
18683					if ($.isFunction(options.successCallback)) {
18684						options.successCallback(data, textStatus, roomData);
18685					}
18686					
18687					self.history.counter = 0;
18688					
18689					myDeco.body.trigger('roomSaved',
18690						[roomData.type, data.id, options]);
18691
18692                    if (parent) {
18693                        var msg = {name: "saved", id: data.id};
18694                        parent.postMessage(msg, "*");
18695                        parent.postMessage(JSON.stringify(msg), "*"); // IE9
18696                    }
18697				} else {
18698					myDeco.body.trigger('roomSavingError',
18699						[504, gettext('Gateway Timeout'), gettext('Connection is broken')]);
18700				}
18701			},
18702			error: function(xhr) {
18703				var response,
18704					message;
18705				try {
18706					response = JSON.parse(xhr.responseText);
18707				} catch(ex) {
18708					response = {};
18709				}
18710				if (response.error) {
18711					message = response.error.message;
18712					if (message.__all__) {
18713						message = message.__all__;
18714					} else {
18715						message = JSON.stringify(message);
18716					}
18717				} else {
18718					message = xhr.status + ': ' + xhr.statusText;
18719				}
18720				myDeco.body.trigger('roomSavingError', [
18721					xhr.status,
18722					xhr.statusText,
18723					message
18724				]);
18725			}
18726		});
18727		return true;
18728	},
18729
18730	/**
18731	 * Delete active room
18732	 * @param {Object} options
18733	 */
18734	deleteRoom: function(options) {
18735		var self = this,
18736			params = '';
18737
18738		options = options || {};
18739
18740		if (this._ws_url) {
18741			if (options.urlParams) {
18742				params = (this._ws_url.indexOf('?') === -1 ? '?' : '&') +
18743					options.urlParams;
18744			}
18745
18746			$.ajax({
18747				// DELETE method don't accept params, so add them directly to URL
18748				url: this._ws_url + params,
18749				type: 'DELETE',
18750				success: function() {
18751					myDeco.body.trigger('roomDeleted');
18752					self.loadRoom(emptyRoom);
18753				},
18754				error: function() {
18755					myDeco.body.trigger('roomDeletionError');
18756				}
18757			});
18758		}
18759	},
18760	
18761	/**
18762	 * Discard all changes made by user and start again with this room
18763	 */
18764	revert: function() {
18765		this.load($.extend(true, {}, this._backup));
18766	},
18767	
18768	// Stub method for get the room ready to save.
18769	/**
18770	 * Wrap room model with some settings from template.
18771	 * @param {Object} template Default room settings
18772	 * @return {Object} Room ready for saving
18773	 */
18774	 
18775	_wrapRoom: function(template) {
18776		if (this.isNew && this._metadata.author &&
18777				// keep original author
18778				!this._metadata.original_author) {
18779			this._metadata.original_author = this._metadata.author;
18780		}
18781
18782		if (this.isNew && this._metadata.image) {
18783			this._metadata.original_image = this._metadata.image;
18784		}
18785
18786		$.extend(true, this._metadata, template);
18787		this._metadata.author = myDeco.username;
18788		
18789		// remove original parent
18790		delete this._metadata.parent;
18791		this._metadata.name = (this._metadata.name || '').substring(0, 64);
18792
18793		return this._metadata;
18794	},
18795	
18796	/**
18797	 * Add room to one or more competitions.
18798	 * @param {Array} competitions array of competitions id (or just one id)
18799	 */
18800	addToCompetitions: function(competitions) {
18801		if(!this._room_id) {
18802			return;
18803		}
18804		var success = true,
18805			data = {room_id: this._room_id};
18806		if(!$.isArray(competitions)) {
18807			competitions = [competitions];
18808		}
18809		$.each(competitions, function() {
18810			$.ajax({
18811				type: 'POST',
18812				url: myDeco.urls.ws.room_competitions + this.id + '/',
18813				data: data,
18814				async: false,
18815				error: function() {
18816					success = false;
18817				}
18818			});
18819		});
18820		return success;
18821	},
18822	
18823	/**
18824	 * Store some changes over the model to history
18825	 * @param {Any} Data to store
18826	 */
18827	operationEnd: function(data) {
18828		// populate change to history
18829		if (this.history.changes.length - 1 > this.history.position) {
18830			this.history.changes = this.history.changes.slice(0, this.history.position);
18831		}
18832		
18833		this.history.changes.push(data);
18834		
18835		if (this.history.changes.length > this.settings.historyLimit) {
18836			this.history.changes.shift();
18837		}
18838		
18839		this.history.position = this.history.changes.length - 1;
18840		
18841		this.history.counter++;
18842	},
18843	
18844	/**
18845	 * Return string representation for latest changes.
18846	 * @param {Number} latest Count of latest changes, leave empty for all
18847	 */
18848	changeLog: function(latest) {
18849		return $.map(typeof latest === 'undefined' ? 
18850						this.history.changes :
18851						this.history.changes.slice(-latest), 
18852					function(item) {
18853						return item[0];
18854					});
18855	}
18856
18857});
18858/*globals Class, $, myDeco */
18859/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
18860          sloppy: true, white: true */
18861/**
18862 * @fileOverview App base controller
18863 */
18864var AppController = Class.extend({
18865	/**
18866	 * Controller type
18867	 * @type {String}
18868	 * @static
18869	 */
18870	$type: 'app',
18871	
18872	/**
18873	 * Model instance
18874	 * @type {Object}
18875	 */
18876	_model: false,
18877	
18878	/**
18879	 * View instance
18880	 * @type {FpeView}
18881	 */
18882	_view: false,
18883	
18884	/**
18885	 * Controller settings
18886	 */
18887	settings: {},
18888	
18889	/**
18890	 * Operation info
18891	 * @type: {Object}
18892	 */
18893	_operation: {},
18894	
18895	/**
18896	 * Current controller state
18897	 * @type {Boolean}
18898	 */
18899	state: true,
18900
18901	/**
18902	 * Init function
18903	 * @param {Object} model FPE model
18904	 */
18905	init: function(model, settings) {
18906		this._model = model;
18907		this.settings = $.extend(true, this.settings, settings || {});
18908	},
18909	
18910	/**
18911	 * Register view
18912	 * @param {Object} view FPE view
18913	 */
18914	registerView: function(view) {
18915		this._view = view;
18916		
18917		myDeco.body
18918			.bind('operationEnd', this.operationEnd.bind(this))
18919			.bind('operationStart', this.operationStart.bind(this))
18920			.bind('operationReset', this.operationReset.bind(this))
18921			.bind('viewToggle', this.toggle.bind(this));
18922	},
18923	
18924	/**
18925	 * End operation handler, will fire 'endOperation' event with custom parameters
18926	 * @param {Array} eventData Custom event data
18927	 */
18928	operationEnd: function(e, data) {
18929		return (data || []);
18930	},
18931
18932	/**
18933	 * Callback for resetting current operation state
18934	 */
18935	operationReset: function() {
18936	},
18937	
18938	/**
18939	 * Event operation start event listener
18940	 * Hide info and prepare data passed to event
18941	 * @param {Event} e
18942	 * @param {Array} data Passed data 
18943	 */
18944	operationStart: function(e, data) {
18945		data = $.isArray(data) ? data : [];
18946		
18947		return data;
18948	},
18949	
18950	/**
18951	 * Toggle view to a visible/nonvsible state
18952	 * @param {Event} e
18953	 * @param {String} type Destination type to enable
18954	 */
18955	toggle: function(e, type) {
18956		var state = this.state;
18957		
18958		if (type === this.$type && this.state !== true) {
18959			this.state = true;
18960		} else if (this.state === true && $.inArray(type, (this.settings.linked || [])) !== -1) {
18961			this.state = false;
18962		}
18963		
18964		if (state !== this.state) {
18965			this._view.toggle(this.state);
18966			return true;
18967		}
18968		
18969		return false;
18970	}
18971});
18972/*globals Class, $, myDeco */
18973/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
18974          sloppy: true, white: true */
18975/**
18976 * @fileOverview base class for FPE view
18977 */
18978var AppView = Class.extend({
18979	/**
18980	 * View type, should be static
18981	 * @type {String}
18982	 * @static
18983	 */
18984	$type: 'base',
18985	
18986	/**
18987	 * Real view holder, will find closest with #view-$type
18988	 * @type {jQset}
18989	 */
18990	viewHolder: false,
18991	
18992	/**
18993	 * Canvas DOM element
18994	 * @type {Object}
18995	 */
18996	canvasElement: false,
18997	
18998	/**
18999	 * Canvas width
19000	 * @type {Number}
19001	 */
19002	canvasHeight: 0,
19003	
19004	/**
19005	 * Canvas height
19006	 * @type {Number}
19007	 */
19008	canvasWidth: 0,
19009	
19010	/**
19011	 * Canvas offset
19012	 * @type {Object}
19013	 */
19014	_canvasOffset: false,
19015	
19016	/**
19017	 * Application layout
19018	 */
19019	app: false,
19020
19021	/**
19022	 * View state
19023	 */
19024	state: false,
19025	
19026	/**
19027	 * App width and height
19028	 * @type {Object}
19029	 */
19030	_appDimensions: false,
19031	
19032	/**
19033	 * App offset
19034	 * @type {Object}
19035	 */
19036	_appOffset: false,
19037	
19038	/**
19039	 * View settings
19040	 * @type {Object}
19041	 */
19042	settings: {},
19043
19044	/**
19045	 * FPE model
19046	 * @type {Object}
19047	 */
19048	_model: undefined,
19049	
19050	/**
19051	 * FPE controller
19052	 * @type {Object}
19053	 */
19054	_controller: undefined,
19055	
19056	/**
19057	 * Should view fire 'viewStarted' event after init
19058	 * Usable for plugins
19059	 * @type true
19060	 */
19061	autostart: true,
19062	
19063	/**
19064	 * Flag for loaded state
19065	 * @type {Boolean}
19066	 */
19067	loaded: false,
19068	
19069	/**
19070	 * Init function
19071	 * Required options are:
19072	 * 
19073	 * @param {Object} options Init options
19074	 */
19075	init: function(options) {
19076		var i, ilen, instance;
19077		
19078		options = options || {};
19079		options.settings = options.settings || {};
19080		this.settings = $.extend(true, this.settings, options.settings || {});
19081		options.canvas = options.canvas || $('#view-' + this.$type);
19082		
19083		if (!options.canvas || options.canvas.size() === 0) {
19084			throw "No canvas found in the document";
19085		}
19086			
19087		// canvas
19088		this.canvasElement = options.canvas;
19089		this.canvasWidth = this.canvasElement.innerWidth();
19090		this.canvasHeight = this.canvasElement.innerHeight();
19091		
19092		// view holder
19093		this.viewHolder = this.canvasElement.closest('#view-' + this.$type);
19094		
19095		// app
19096		this.app = options.app;
19097		
19098		this._model = options.model;
19099		this._controller = options.controller;
19100		
19101		// init plugins
19102		for (i = 0, ilen = (options.plugins || []).length; i < ilen; i++) {
19103			// to satisfy jsLint
19104			instance = new options.plugins[i]['class'](this, options.plugins[i].settings);
19105		}
19106		
19107		myDeco.body.trigger('runPlugins.' + this.$type);
19108		
19109		// tell to plugins that we started
19110		if (this.autostart) { 
19111			myDeco.body.trigger('viewStarted.' + this.$type);
19112		}
19113
19114		// load different sprite image for different locales
19115		if (myDeco.settings.I18N_LOCALE === 'de') {
19116			$('.decorate-target').css('background-image', 'url(/assets/austin/sprite-de.png)')
19117		} else if (myDeco.settings.I18N_LOCALE === 'fr') {
19118			$('.decorate-target').css('background-image', 'url(/assets/austin/sprite-fr.png)')
19119		} else if (myDeco.settings.I18N_LOCALE === 'es') {
19120			$('.decorate-target').css('background-image', 'url(/assets/austin/sprite-es.png)')
19121		}
19122	},
19123	
19124	/**
19125	 * Get canvas offset
19126	 * 
19127	 * @return {Object}
19128	 */
19129	getCanvasOffset: function() {
19130		if (!this._canvasOffset) {
19131			this._canvasOffset = this.canvasElement.offset();
19132		}
19133		
19134		return this._canvasOffset;
19135	},
19136
19137	/**
19138	 * Get App offset
19139	 * 
19140	 * @return {Object}
19141	 */
19142	getAppOffset: function(){
19143		if (!this._appOffset) {
19144			this._appOffset = this.app.offset();
19145			this._appOffset.left = Math.ceil(this._appOffset.left);
19146			this._appOffset.top = Math.ceil(this._appOffset.top);
19147		}
19148		
19149		return this._appOffset;
19150	},
19151
19152	/**
19153	 * Get app width and height as object
19154	 * 
19155	 * @return {Object}
19156	 */
19157	getAppDimensions: function() {
19158		if (!this._appDimensions ||
19159				(!this._appDimensions.width && !this._appDimensions.height)) {
19160			this._appDimensions = {
19161				width: this.app.innerWidth(),
19162				height: this.app.innerHeight()
19163			};
19164		}
19165		return this._appDimensions;
19166	},
19167	
19168	/**
19169	 * Set cursor style depend on current element hover
19170	 * 
19171	 * @param {String} cur Body className
19172	 */
19173	cursor: function(cur) {
19174		var body = myDeco.body.get(0);
19175		if (typeof cur === 'undefined') {
19176			return body.cursor || '';
19177		} else if (!this.disableCursors) {
19178			myDeco.body
19179				.removeClass(body.cursor || '')
19180				.addClass(cur);
19181
19182			body.cursor = cur;
19183		}
19184	},
19185	
19186	/**
19187	 * Toggle view
19188	 * @param {Boolean} state
19189	 */
19190	toggle: function(state) {
19191		this.viewHolder
19192			.toggle(state)
19193			.trigger(state === true ? 'maximized' : 'minimized');
19194		myDeco.body.trigger('view-toggle',
19195			[this, state]);
19196	}
19197});
19198/*jslint white: false, forin: true, fragment: true, undef: true, nomen: true, bitwise: false, regexp: false, devel: true, sloppy: true, plusplus: true, bitwise: true */
19199
19200/**
19201 * banners delay loading
19202 */
19203myDeco.advertiseBanners = {
19204
19205	/**
19206	 * Init banner
19207	 */
19208	init: function (banner) {
19209		if (!banner.size()) {
19210			return;
19211		}
19212
19213		var prop = banner.data(),
19214			cachebuster = Math.floor(Math.random() * 10000000000),
19215			dcopt = null,
19216			tile = 0;
19217
19218		if (document.body.clientWidth > prop.cwidth) {
19219			dcopt = (dcopt !== null) ? "dcopt=ist" : '';
19220			tile++;
19221
19222			banner.append('<iframe src="http://ad.doubleclick.net/adi/'
19223				+ prop.site + '/' + prop.zone
19224				+ ';pos=' + prop.pos + ';' + dcopt + ';tile=' + tile
19225				+ ';sz=' + prop.sz + ';ord=' + cachebuster + '?" marginwidth="0" marginheight="0"'
19226				+ 'hspace="0" vspace="0" frameborder="0" scrolling="no" allowtransparency="true"'
19227				+ prop.attrs + '></iframe>');
19228		}
19229	},
19230
19231	/**
19232	 * Init all base banners
19233	 */
19234	initAll: function() {
19235		var banners = $('#mpu-banner, #sky-banner, #leaderboard-search-banner, #leaderboard-banner.ad2one'),
19236			i,
19237			len = banners.length,
19238			banner;
19239
19240		if (myDeco.settings.ad2one) {
19241			for (i = len; i > -1; i--) {
19242				this.init($(banners[i]));
19243			}
19244		}
19245	}
19246};
19247
19248$(function() {
19249	myDeco.advertiseBanners.initAll();
19250});
19251/*globals $ */
19252/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
19253          sloppy: true, white: true */
19254/*
19255 * Common "ancestor" for model entities such Corner, Wall, Camera, etc
19256 */
19257var commonMethods  = {
19258	/**
19259	 * type of the entity
19260	 * @type {String}
19261	 */
19262	type: 'entity',
19263	
19264	/**
19265	 * pointer to the model
19266	 * @type {FpeModel}
19267	 */
19268	_model: false,
19269
19270	/**
19271	 * way to wrap all private data into the one field (which can be easily dropped) before model exporting
19272	 * @type {Object}
19273	 */
19274	_internals: {},
19275	
19276	/**
19277	 * Selected flag
19278	 * @type {Boolean}
19279	 */
19280	selected: false,
19281
19282	/**
19283	 * Emulation of the constructor. Needed to store pointer to the model
19284	 */
19285	init: function(model) {
19286		this._model = model;
19287	},
19288
19289	/**
19290	 * fire event and pass there params which was passed to this function
19291	 */
19292	fireEvent: function() {
19293		// first param - callback name
19294		var args = $.makeArray(arguments),
19295			eventName = args.shift(),
19296			i, ilen, group;
19297		
19298		for (group in this._internals) {
19299			if (this._internals[group][eventName]) {
19300				for (i = 0, ilen = this._internals[group][eventName].length; i < ilen; i++) {
19301					this._internals[group][eventName][i].apply(this, args);
19302				}
19303			}
19304		}
19305	},
19306	
19307	/**
19308	 * Add callback to the queue
19309	 * @type {String} eventName (onMove or onRotate)
19310	 * @type {Function} observer Callback function
19311	 * @type {String} group Event group
19312	 */
19313	attachEvent: function(eventName, observer, group) {
19314		if (!this._internals[group]) {
19315			this._internals[group] = {};
19316		}
19317		
19318		if (!this._internals[group][eventName]) {
19319			this._internals[group][eventName] = [];
19320		}
19321		
19322		this._internals[group][eventName].push(observer);
19323		
19324		return this;
19325	},
19326	
19327	/**
19328	 * Detach event listener from model
19329	 * @type {String} eventName (onMove or onRotate)
19330	 * @type {Function} observer Callback function
19331	 * @type {String}
19331 group Event group
19332	 */
19333	detachEvent: function(eventName, observer, group) {
19334		var idx;
19335		
19336		if (this._internals[group] && this._internals[group][eventName]) {
19337			idx = this._internals[group][eventName].indexOf(observer);
19338			if (idx !== -1) {
19339				this._internals[group][eventName].splice(idx, 1);
19340			}
19341		}
19342	},
19343	
19344	/**
19345	 * Drop event observers group from list
19346	 * @param {String} group Event observers group name 
19347	 */
19348	dropEvents: function(group) {
19349		delete this._internals[group];
19350	},
19351
19352	/**
19353	 * Abstract move to the new position method
19354	 * @param {Number} dx
19355	 * @param {Number} dy
19356	 */
19357	move: function(dx, dy, dz) {
19358		if (typeof this.position !== 'undefined') {
19359			this.position.translate(dx, dy, dz);
19360		}
19361		this.fireEvent('onMove', dx, dy, dz);
19362	},
19363	
19364	/**
19365	 * Return cached angle of this object (if applicable)
19366	 * @returns Number
19367	 */
19368	getAngle: function() {
19369		return this.angle || 0;
19370	},
19371	
19372	/**
19373	 * Return cached x coord of entity center (if applicable)
19374	 * @returns Number
19375	 */
19376	getCX: function() {
19377		return (typeof this.position !== 'undefined' ? this.position.x : 0);
19378	},
19379
19380	/**
19381	 * Return cached y coord of entity center (if applicable)
19382	 * @returns Number
19383	 */
19384	getCY: function() {
19385		return (typeof this.position !== 'undefined' ? this.position.y : 0);
19386	},
19387	
19388	/**
19389	 * Select entity
19390	 * @returns {Object}
19391	 */
19392	_select: function() {
19393		if (!this.selected) {
19394			this.selected = true;
19395			this.fireEvent('onSelect', this._model._selected);
19396			return true;
19397		}
19398		return false;
19399	},
19400	
19401	/**
19402	 * Unselect entity
19403	 * @returns {Object}
19404	 */
19405	_unselect: function() {
19406		if (this.selected) {
19407			this.selected = false;
19408			this.fireEvent('onUnselect', this._model._selected);
19409			return true;
19410		}
19411		return false;
19412	},
19413	
19414	/**
19415	 * Event method executed on remove
19416	 */
19417	remove: function() {
19418		this.fireEvent('onRemove', true);
19419		return this;
19420	},
19421	
19422	/**
19423	 * Event method executed on highlighting
19424	 */
19425	highlightOn: function() {
19426		this.fireEvent('onHightlightOn');
19427		return this;
19428	},
19429	
19430	/**
19431	 * Event method executed on dishighlighting
19432	 */
19433	highlightOff: function() {
19434		this.fireEvent('onHightlightOff');
19435		return this;
19436	},
19437	
19438	/**
19439	 * Event method executed on operation end
19440	 * @param {String} name Operation name
19441	 */
19442	operationEnd: function(name) {
19443		this.fireEvent('onOperationEnd', name);
19444		return this;
19445	},
19446	
19447	/**
19448	 * Event method executed on operation start
19449	 * @param {String} name Operation name
19450	 */
19451	operationStart: function(name) {
19452		this.fireEvent('onOperationStart', name);
19453		return this;
19454	},
19455	
19456	/**
19457	 * Reset operation event
19458	 */
19459	operationReset: function() {
19460		this.fireEvent('onOperationReset');
19461		return this;
19462	},
19463	
19464	/**
19465	 * Default action for entity
19466	 */
19467	defaultAction: function() {
19468		return this;
19469	}
19470
19471};
19472/*globals $, commonMethods, findShapeArea */
19473/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
19474          sloppy: true, white: true */
19475/*
19476 * Shape class for the model (derived from common ancestor from entity.js)
19477 */
19478
19479var shapeMethods = $.extend({}, commonMethods, {
19480	/**
19481	 * type of the entity
19482	 * @type string
19483	 */
19484	type: 'shape',
19485	
19486	/**
19487	 * Materials
19488	 * DO NOT DEFINE THIS PROPERTIES!
19489	 */
19490	// ceilingMaterial: false,
19491	// floorMaterial: false,
19492	
19493	/**
19494	 * Walls attached to shape
19495	 * @type {Array}
19496	 */
19497	_walls: [],
19498	
19499	/**
19500	 * Flag for decora acceptance
19501	 * @type {Boolean}
19502	 */
19503	acceptDecor: true,
19504	
19505	/**
19506	 * Emulation of the constructor. Needed to register corner point in snapping array.
19507	 * @param {FpeModel} model
19508	 * @param {Array} walls Walls to attach
19509	 */
19510	init: function(model, walls) {
19511		var i, ilen;
19512		
19513		this._model = model;
19514		this.ID = 'area' + this._model._areaCounter++;
19515		
19516		this._model.shapes.push(this);
19517		this._walls = [];
19518		walls = walls || [];
19519		
19520		for (i = 0, ilen = this.length; i < ilen; i++) {
19521			this[i].attachShape(this);
19522		}
19523		
19524		for (i = 0, ilen = walls.length; i < ilen; i++) {
19525			walls[i].attachShape(this);
19526		}
19527
19528		var defaultFloorMaterial = this._model.settings.defaultMaterials.floor;
19529		if(myDeco.settings.WL && myDeco.settings.WL.WL_DEFAULT_FLOOR_MATERIAL)
19530			defaultFloorMaterial = eval('('+ myDeco.settings.WL.WL_DEFAULT_FLOOR_MATERIAL +')');
19531		
19532		// materials
19533		this._model.assignMaterial(this,
19534			typeof this.floorMaterial === 'object' ? this.floorMaterial :
19535			defaultFloorMaterial, 'floor');
19536		this._model.assignMaterial(this,
19537			typeof this.ceilingMaterial === 'object' ? this.ceilingMaterial :
19538			this._model.settings.defaultMaterials.ceiling, 'ceiling');
19539		
19540		this.restoreCorrectOrder();
19541		return this;
19542	},
19543	
19544	/**
19545	 * Calculate area of the given room shape.
19546	 */
19547	getArea: function() {
19548		var s = findShapeArea(this);
19549		this.area = Math.abs(s);
19550		return s;
19551	},
19552	
19553	/**
19554	 * Restore correct order for shape points
19555	 */
19556	restoreCorrectOrder: function() {
19557		if (this.getArea() > 0) {
19558			this.reverse();
19559		}
19560	},
19561	
19562	/**
19563	 * Append corner to this shape
19564	 * @param {Corner} corner
19565	 * @param {Array} points Two points from shape for insertion between them
19566	 * @returns {Shape}
19567	 */
19568	addCorner: function(corner, points) {
19569		var index = points ? (function(shape) {
19570			var i1 = shape.indexOf(points[0]),
19571				i2 = shape.indexOf(points[1]);
19572				
19573			return i1 !== -1 && i2 !== -1 && Math.abs(i1 - i2) === 1 ?
19574				Math.max(i1, i2) : shape.length;
19575		}(this)) : this.length;
19576		
19577		corner.attachShape(this);
19578		
19579		this.splice(index, 0, corner);
19580		this.fireEvent('onCornerAdd', corner, index);
19581		return this;
19582	},
19583	
19584	/**
19585	 * Get attached to this shape walls
19586	 * @returns {Array} 
19587	 */
19588	getWalls: function() {
19589		return this._walls;
19590	},
19591	
19592	/**
19593	 * Attach wall to this shape
19594	 * @param {Wall} wall
19595	 * @returns {Shape}
19596	 */
19597	attachWall: function(wall) {
19598		if (this._walls.indexOf(wall) === -1) {
19599			this._walls.push(wall);
19600			wall.attachShape(this);
19601		}
19602		return this;
19603	},
19604	
19605	/**
19606	 * Detach wall from this shape
19607	 * @param {Wall} wall
19608	 * @returns {Shape}
19609	 */
19610	detachWall: function(wall) {
19611		if ($.extract(wall, this._walls)) {
19612			wall.detachShape(this);
19613		}
19614		return this;
19615	},
19616	
19617	/**
19618	 * Remove shape form model. Will remove single walls/corners which isn't
19619	 * connected to any other walls/shapes
19620	 * @param {Boolean} removeWalls remove walls when removing shape
19621	 */
19622	remove: function(keepWalls) {
19623		var i, wall;
19624		
19625		this._model.unselect(this);
19626		
19627		// unassign materials
19628		this._model.assignMaterial(this, undefined, 'ceiling');
19629		this._model.assignMaterial(this, undefined, 'floor');
19630		
19631		// dereference self from walls and remove walls if they do not have
19632		// any shapes
19633		for (i = this._walls.length - 1; i >= 0; i--) {
19634			wall = this._walls[i].detachShape(this);
19635			if (!keepWalls && wall.getShapes().length === 0) {
19636				wall.remove();
19637			}
19638		}
19639		
19640		for (i = this.length - 1; i >= 0; i--) {
19641			this[i].detachShape(this);
19642		}
19643		
19644		// deregister self from model
19645		$.extract(this, this._model.shapes);
19646		
19647		this.fireEvent('onRemove', true);
19648	},
19649	
19650	/**
19651	 * String representation of shape
19652	 * @returns {String}
19653	 */
19654	toString: function() {
19655		var ret = 'ID: ' + this.ID,
19656			i, ilen = this.length;
19657			
19658		for (i = 0; i < ilen; i++) {
19659			ret += ', ' + this[i].toString();
19660		}
19661		
19662		return ret;
19663	},
19664	
19665	/**
19666	 * Return opposite corner from shape
19667	 * @param {Point} point Center point
19668	 * @param {Point} existing Point opposite to
19669	 * @returns {Point}
19670	 */
19671	getOpposite: function(point, existing) {
19672		var neighbors = this.getNeighbors(point);
19673		return  neighbors[0] === existing ? neighbors[1] : neighbors[0];
19674	},
19675	
19676	/**
19677	 * Get neighbors points for provided point in shape
19678	 * @param {Corner}
19679	 * @returns {Array}
19680	 */
19681	getNeighbors: function(point) {
19682		var idx = this.indexOf(point);
19683		
19684		return [this[(idx + 1) % this.length],
19685			this[(idx - 1 + this.length) % this.length]];
19686	},
19687
19688	getBBox: function() {
19689		var xs = $.map(this, function(p) {
19690				return p.x;
19691			}),
19692			x = Math.min.apply(null, xs),
19693			ys = $.map(this, function(p) {
19694				return p.y;
19695			}),
19696			y = Math.min.apply(null, ys);
19697
19698		return {
19699			x: x, y: y,
19700			width: Math.max.apply(null, xs) - x,
19701			depth: Math.max.apply(null, ys) - y
19702		};
19703	}
19704});
19705/*globals $, myDeco, commonMethods, T */
19706/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
19707          sloppy: true, white: true */
19708/*
19709 * Camera class for the model (derived from common ancestor from entity.js)
19710 */
19711
19712var cameraMethods = $.extend({}, commonMethods, {
19713	/**
19714	 * type of the entity
19715	 * @type {String}
19716	 */
19717	type: 'camera',
19718	
19719	/**
19720	 * Camera angle in horizontal plane
19721	 * @type {Number}
19722	 */
19723	_angleH: undefined,
19724	
19725	/**
19726	 * Camera angle in vertical plane
19727	 * @type {Number}
19728	 */
19729	_angleV: undefined,
19730	
19731	/**
19732	 * Focal length calculated value
19733	 * @type {Number}
19734	 */
19735	// fieldofview: 180 / 3.0,
19736	
19737	/**
19738	 * Camera init
19739	 * @param {Object} model Camera model
19740	 */
19741	init: function(model) {
19742		this._model = model;
19743		this.setFov(model.camera.fieldofview);
19744	},
19745
19746    setFov: function(fov) {
19747        this.fieldofview = fov;
19748        this.fireEvent('onFov');
19749    },
19750	
19751	/**
19752	 * Rotate the camera
19753	 * @param {Number} angle new camera angle in x0y plane
19754	 * @param {Number} angleV new camera angle relative to z axis
19755	 * @returns {Camera}
19756	 */
19757	rotate: function(angleH, angleV) {
19758		this._angleH = typeof angleH === 'undefined' ? this.getAngle() : angleH;
19759		this._angleV = typeof angleV === 'undefined' ? this.getVAngle() : angleV;
19760		
19761		this.focal_point = this.position.clone()
19762			// Magical number involved
19763			.translate(318.826, 0, 0)
19764			.rotate(this.position, - this._angleH - 90, undefined, this._angleV);
19765		this.fireEvent('onRotate', this._angleH, this._angleV);
19766		return this;
19767	},
19768	
19769	/**
19770	 * Move camera to the new position
19771	 * @param {Number} dx
19772	 * @param {Number} dy
19773	 * @param {Number} dz
19774	 * @returns {Camera}
19775	 */
19776	move: function(dx, dy, dz) {
19777		this.position.translate(dx, dy, dz);
19778		this.focal_point.translate(dx, dy, dz);
19779		this.fireEvent('onMove', dx, dy, dz);
19780		return this;
19781	},
19782	
19783	/**
19784	 * Return angle between camera's base and focal point in x0y plane
19785	 * @returns {Number}
19786	 */
19787	getAngle: function() {
19788		if (typeof this._angleH === 'undefined') {
19789			this._angleH = T.atan2(this.position.y - this.focal_point.y, this.focal_point.x - this.position.x) - 90.0;
19790		}
19791		
19792		return this._angleH;
19793	},
19794	
19795	/**
19796	 * Return angle between camera base and focal point relative to Z axis
19797	 * @return {Number}
19798	 */
19799	getVAngle: function() {
19800		if (typeof this._angleV === 'undefined') {
19801			this._angleV = T.asin((this.focal_point.z - this.position.z) / Math.abs(this.position.distance(this.focal_point)));
19802		}
19803		return this._angleV;
19804	}
19805	
19806});
19807/*globals $, commonMethods, Point, myDeco, T, findWallIntersection */
19808/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
19809          sloppy: true, white: true */
19810/*
19811 * Wall class for the model (derived from common ancestor from entity.js)
19812 */
19813
19814var wallMethods = $.extend({}, commonMethods, {
19815	/**
19816	 * type of the entity
19817	 * @type {String}
19818	 */
19819	type: 'wall',
19820
19821	/**
19822	 * attached components (doors and windows)
19823	 * @type {Array} of Object 
19824	 */
19825	components: [],
19826
19827	/**
19828	 * Point at the center of the wall.
19829	 * @type {Point} 
19830	 */
19831	middle: false,
19832	
19833	/**
19834	 * Shapes attached to wall
19835	 * @type {Array}
19836	 */
19837	_shapes: [],
19838	
19839	/**
19840	 * Flag for decora acceptance
19841	 * @type {Boolean}
19842	 */
19843	acceptDecor: true,
19844
19845	/**
19846	 * Will this instance accept photo or not
19847	 * @type {Boolean}
19848	 */
19849	acceptPicture: true,
19850	
19851	/**
19852	 * Emulation of the constructor. Needed to register wall's middle point in snapping array.
19853	 * @param {FpeModel} model
19854	 * @param {Array} shapes Shapes attached to wall
19855	 */
19856	init: function(model, shapes) {
19857		var i, ilen;
19858		this._model = model;
19859		if (typeof this.ID === 'undefined') {
19860			this.ID = 'wall' + this._model._wallCounter++;
19861		} else {
19862			i = +this.ID.replace('wall', '');
19863			if (!isNaN(i)) {
19864				this._model._wallCounter =
19865					Math.max(this._model._wallCounter, i + 1);
19866			}
19867		}
19868		
19869		// for old models without thickness
19870		if (typeof this.thickness === 'undefined') {
19871			this.thickness = this._model.settings
19872				.wallThickness[this._model.settings.wallThicknessDefault].value;
19873		}
19874		
19875		this._model.walls.push(this);
19876		
19877		// reference self to points
19878		for (i = 0; i < 2; i++) {
19879			this[i].attachWall(this);
19880		}
19881		
19882		// avoid of "shared" shapes
19883		this._shapes = [];
19884		shapes = shapes || [];
19885		for (i = 0, ilen = shapes.length; i < ilen; i++) {
19886			shapes[i].attachWall(this);
19887		}
19888		
19889		this._corners = {};
19890		this.middle = new Point();
19891		this._updateMiddle();
19892		
19893		// used as flag for snapping rules
19894		this.middle.type = 'wall';
19895		this._model._snapPoints.push(this.middle);
19896		
19897		// material
19898		this._model.assignMaterial(this, typeof this.material === 'object' ?
19899			this.material : this._model.settings.defaultMaterials.wall);
19900		
19901		return this;
19902	},
19903	
19904	/**
19905	 * Recalculate pre-cached value of the middle of the wall
19906	 */
19907	_updateMiddle: function() {
19908		this.middle.x = (this[1].x + this[0].x) / 2;
19909		this.middle.y = (this[1].y + this[0].y) / 2;
19910		this.middle.z = (this[1].z + this[0].z) / 2;
19911	},
19912
19913	/**
19914	 * Move Wall to the new position (if possible)
19915	 * @param {Number} dx
19916	 * @param {Number} dy
19917	 */
19918	move: function(dx, dy) {
19919		var self = this,
19920			pdx = [],
19921			pdy = [],
19922			// Finding an angle and distance between point to which we are
19923			// trying to move the wall sand current wall position.
19924			dx2 = this[1].x - this[0].x,
19925			dy2 = this[1].y - this[0].y,
19926			d1 = (dx * dy2 - dx2 * dy) / Math.sqrt(dx2 * dx2 + dy2 * dy2),
19927			angle = T.atan2(dy2, dx2) - 90,
19928			pretender, i, wall, neighbourAngle, point;
19929			
19930		// Finding real position of the wall after movement
19931		pdx[0] = pdx[1] = dx = d1 * T.cos(angle);
19932		pdy[0] = pdy[1] = dy = d1 * T.sin(angle);
19933		
19934		// New corners of the wall
19935		pretender = [self[0].clone().translate(dx, dy, 0),
19936			self[1].clone().translate(dx, dy, 0)];
19937
19938		// Checking new corners for constraints.
19939		for (i = 0; i < 2; i++) {
19940			wall = this[i].getWalls(this).shift();
19941			
19942			if (wall) {
19943				neighbourAngle = wall.getAngle();
19944				
19945				// if new wall and it's neighbor has almost no angle between
19946				if (Math.abs((neighbourAngle - angle + 90) % 180) <=
19947						this._model.settings.minAngle) {
19948					// block wall movement when wal is almost parallel to
19949					// neighbor
19950					return;
19951				} else {
19952					// finding new position as intersection point between new
19953					// wall and it's neighbor
19954					point = findWallIntersection(pretender, wall);
19955					if (!point) {
19956						return;
19957					}
19958					pdx[i] = point.x - this[i].x;
19959					pdy[i] = point.y - this[i].y;
19960				}
19961				// Checking if wall move isn't blocked by it's components and
19962				// components of
19963				// walls-neighbours
19964				if (!this[i].checkConstraints(pdx[i], pdy[i])) {
19965					return;
19966				}
19967			}
19968		}
19969		
19970		// forbid movement if walls create intersection
19971		if (findWallIntersection(
19972				[this[0], new Point(this[0].x + pdx[0], this[0].y + pdy[0])],
19973				[this[1], new Point(this[1].x + pdx[1], this[1].y + pdy[1])],
19974				true
19975		)) {
19976			return;
19977		}
19978		
19979		// Walls components will be moved automatically by corner method
19980		this[0].move(pdx[0], pdy[0], true);
19981		this[1].move(pdx[1], pdy[1], true);
19982		
19983		this._updateMiddle();
19984
19985		this.fireEvent('onMove', dx, dy, 0);
19986	},
19987
19988	/**
19989	 * Get wall attribute
19990	 * @param {String} name
19991	 * @returns {Mixed}
19992	 */
19993	getAttribute: function(name) {
19994		var value;
19995
19996		switch(name) {
19997			case 'width':
19998				value = this[0].distance(this[1]);
19999				break;
20000			case 'height':
20001				value = this._model.ceilingHeight;
20002				break;
20003		}
20004
20005		return value;
20006	},
20007
20008	/**
20009	 * Set wall attribute
20010	 * @param {String} name Attribute name
20011	 * @param {Mixed} value Attribute value
20012	 * @returns {Object} this
20013	 */
20014	setAttribute: function(name, value) {
20015		var length, half, angle, dx, dy, corners, i, ilen, components;
20016
20017		switch(name) {
20018			case 'height':
20019				this._model.setCeilingHeight(value);
20020				this._model.operationEnd(['wallHeight', this, this.getAttribute('height')]);
20021				break;
20022			case 'width':
20023				length = Math.max(10, value);
20024				half = (length - this.getAttribute('width')) / 2;
20025				angle = this.getAngle();
20026				dx = half * T.cos(angle + 180);
20027				dy = half * T.sin(angle);
20028				corners = this.getCorners();
20029				corners[0].move(-dx, -dy);
20030				corners[1].move(dx, dy);
20031				this._model.operationEnd(['wallWidth', this, this.getAttribute('width')]);
20032				break;
20033			case 'thickness':
20034				this.thickness = value;
20035				for (i = 0; i < 2; i++) {
20036					this[i].jointPoints();
20037				}
20038				
20039				components = this.getComponents();
20040				for (i = 0, ilen = components.length; i < ilen; i++) {
20041					if (components[i].type === 'component') {
20042						components[i].placeToWall(components[i].parent, components[i]._shape);
20043					} else {
20044						components[i].setAttribute('depth', value);
20045					}
20046				}
20047				this.fireEvent('onThickness');
20048				this._model.operationEnd(['wallThickness', this, value]);
20049				break;
20050		}
20051
20052		return this;
20053	},
20054	
20055	/**
20056	 * Return angle between wall and 0X axis
20057	 * @param {Object} shape
20058	 * @returns {Number}
20059	 */
20060	getAngle: function(shape) {
20061		var corners = this.getCorners(shape);
20062		return corners ? ((360 - corners[0].angleTo(corners[1])) % 360) : false;
20063	},
20064	
20065	/**
20066	 * Return coeffs of line which represents wall
20067	 * @returns {Object}
20068	 */
20069	getCoeffs: function() {
20070		var dy = (this[1].y - this[0].y),
20071			dx = (this[1].x - this[0].x);
20072		
20073		return {a: -dy,
20074				b: dx, 
20075				c: -dx * this[0].y + dy * this[0].x};
20076	},
20077	
20078	/**
20079	 * Return wall corners in a proper order relative to passed shape
20080	 * First shape attached to wall is a default if no shape passed
20081	 * @param {Shape} shape
20082	 * @returns {Array}
20083	 */
20084	getCorners: function(shape) {
20085		if (shape && !this._corners[shape.ID]) {
20086			var i1, i2, len = shape.length;
20087			
20088			for (i2 = 0, i1 = len - 1; i2 < len; i1 = i2++) {
20089				if (this.indexOf(shape[i1]) !== -1 &&
20090						this.indexOf(shape[i2]) !== -1) {
20091					this._corners[shape.ID] = [shape[i1], shape[i2]];
20092					break;
20093				}
20094			}
20095		}
20096
20097		return shape ? this._corners[shape.ID] : this;
20098	},
20099	
20100	/**
20101	 * Return opposite point to given one
20102	 * @param {Number|Point} point Point or point index
20103	 */
20104	getOpposite: function(point) {
20105		if (typeof point !== 'number') {
20106			return this[0] === point ? this[1] : this[0];
20107		} else {
20108			return this[(point + 1) % 2];
20109		}
20110	},
20111	
20112	/**
20113	 * Return attached walls with the current one
20114	 * @returns {Array}
20115	 */
20116	getWalls: function() {
20117		return $.merge($.merge([], this[0].getWalls(this)), this[1].getWalls());
20118	},
20119	
20120	/**
20121	 * Return array of points for current wall including base points
20122	 * in format where p1 and p4 are base points
20123	 *       p0                   p5
20124	 *   p1                           p4
20125	 *       p2                   p3
20126	 * @returns {Array}
20127	 */
20128	getJoints: function() {
20129		var p0 = this[0].getJoints(this),
20130			p1 = this[1].getJoints(this);
20131		
20132		return [
20133			p0[0], this[0], p0[1],
20134			p1[0], this[1], p1[1]
20135		];
20136	},
20137	
20138	/**
20139	 * Proxy method to redraw measurer
20140	 * @param {Boolean} state Show/hide/update measurer; [true/hide/undefined] respectively
20141	 * @returns {Wall}
20142	 */
20143	updateMeasurer: function(state) {
20144		this.fireEvent('onUpdateMeasurer', state);
20145		return this;
20146	},
20147	
20148	/**
20149	 * Attach shape to current wall
20150	 * @param {Shape} shape
20151	 * @returns {Wall}
20152	 */
20153	attachShape: function(shape) {
20154		if (this._shapes.indexOf(shape) === -1) {
20155			this._shapes.push(shape);
20156			shape.attachWall(this);
20157		}
20158		return this;
20159	},
20160	
20161	/**
20162	 * Detach shape from current wall
20163	 * @param {Shape} shape
20164	 * @returns {Wall}
20165	 */
20166	detachShape: function(shape) {
20167		if ($.extract(shape, this._shapes)) {
20168			shape.detachWall(this);
20169		}
20170		return this;
20171	},
20172	
20173	/**
20174	 * Get list of currently attached shapes
20175	 * @returns {Array}
20176	 */
20177	getShapes: function() {
20178		return this._shapes;
20179	},
20180	
20181	/**
20182	 * Attach on-wall component to current wall
20183	 * @param {Component} component
20184	 * @returns {Wall}
20185	 */
20186	addComponent: function(component) {
20187		this.components.push(component);
20188		component.parent = this;
20189		return this;
20190	},
20191	
20192	/**
20193	 * Deattach component from wall
20194	 * @param {Component} component
20195	 * @returns {Wall}
20196	 */
20197	removeComponent: function(component) {
20198		$.extract(component, this.components);
20199		delete component.parent;
20200		return this;
20201	},
20202	
20203	/**
20204	 * Return list of attached components
20205	 * @returns {Array}
20206	 */
20207	getComponents: function() {
20208		return this.components;
20209	},
20210	
20211	/**
20212	 * Remove wall from model
20213	 */
20214	remove: function(keepPoints) {
20215		var i;
20216		
20217		this._model.unselect(this);
20218		
20219		// extract self from snap points
20220		$.extract(this.middle, this._model._snapPoints);
20221		
20222		// dereference material
20223		this._model.assignMaterial(this, undefined, this.type);
20224		
20225		// dereference self from points/shapes
20226		for (i = this._shapes.length - 1; i >= 0; i--) {
20227			this._shapes[i].detachWall(this);
20228		}
20229		
20230		for (i = 0; i < 2; i++) {
20231			this[i].detachWall(this);
20232			if (!keepPoints && this[i].getShapes().length === 0 &&
20233					this[i].getWalls().length === 0) {
20234				this[i].remove();
20235			}
20236		}
20237		
20238		// remove on-wall components attached to current wall
20239		for (i = this.components.length - 1; i >= 0; i--) {
20240			this.components[i].remove();
20241		}
20242		
20243		// extract self from model
20244		$.extract(this, this._model.walls);
20245		
20246		this.fireEvent('onRemove', true);
20247	},
20248	
20249	/**
20250	 * String representation of a wall
20251	 * @returns {String}
20252	 */
20253	toString: function() {
20254		var ret, i;
20255		
20256		ret = 'ID: ' + this.ID;
20257		
20258		for (i = 0; i < 2; i++) {
20259			ret += ", " + this[i].toString();
20260		}
20261		
20262		return ret;
20263	}
20264});
20265/*globals $, commonMethods, findWallIntersection, T */
20266/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
20267          sloppy: true, white: true */
20268/*
20269 * Corner class for the model (derived from common ancestor from entity.js)
20270 */
20271var cornerMethods = $.extend({}, commonMethods, {
20272	/**
20273	 * type of the entity
20274	 * @type string
20275	 */
20276	type: 'corner',
20277	
20278	/**
20279	 * Walls attached to this corner
20280	 * @type {Array}
20281	 */
20282	_walls: [],
20283	
20284	/**
20285	 * Shapes attached to this corner
20286	 * @type {Array}
20287	 */
20288	_shapes: [],
20289	
20290	/**
20291	 * Walls joint points with neighbor walls
20292	 * @type {Object}
20293	 */
20294	_joints: {},
20295	
20296	/**
20297	 * Emulation of the constructor. Needed to register corner point in snapping array.
20298	 * @param {FpeModel}
20299	 */
20300	init: function(model) {
20301		this._model = model;
20302		
20303		this._model.corners.push(this);
20304
20305		// avoid of "shared" walls
20306		this._walls = [];
20307		this._shapes = [];
20308		this._joints = {};
20309
20310		this._model._snapPoints.push(this);
20311		
20312		this.jointPoints();
20313		
20314		return this;
20315	},
20316	
20317	/**
20318	 * Check if we can move the current corner from it's current position
20319	 * to x0 + dx, y0 + dy without breaking the room.
20320	 * @param {Number} dx
20321	 * @param {Number} dy
20322	 * @returns {Boolean} can the corner be moved
20323	 */
20324	checkConstraints: function(dx, dy) {
20325		var self = this,
20326			pretender = this.clone().translate(dx, dy, 0),
20327			hasIntersection = false,
20328			willIntersect = false,
20329			i, wall, nextPoint, w, n1, n2, n3, testWall, s, newWallLength,
20330			component, angle,
20331			angles = $.map(this.getWalls(), function(wall) {
20332				return pretender.angleTo(wall.getOpposite(self));
20333			}),
20334			checkAngle = function(angle1, angle2) {
20335				var diff = Math.abs(angle1 - angle2);
20336				return diff > self._model.settings.minAngle && diff <
20337					360 - self._model.settings.minAngle;
20338			},
20339			invokeCheckAngle = function(w) {
20340				return !checkAngle(nextPoint.angleTo(w.getOpposite(nextPoint)),
20341					angle);
20342			};
20343		
20344		if (angles.length > 1) {
20345			// check for walls angles
20346			angles.sort(function(a, b) {
20347				return a < b ? -1 : (a > b ? 1 : 0);
20348			});
20349			
20350			for (n1 = 0, i = angles.length, n2 = i - 1; n1 !== i; n2 = n1++) {
20351				if (!checkAngle(angles[n1], angles[n2])) {
20352					return false;
20353				}
20354			}
20355		}
20356		
20357		// for each wall, which creates corner.
20358		for (i = 0, n1 = this._walls.length; i < n1; i++) {
20359			wall = this._walls[i];
20360			
20361			// Finding neighbour corner to current one 
20362			nextPoint = wall.getOpposite(this);
20363			
20364			// check for minimal angle to connected walls
20365			angle = nextPoint.angleTo(pretender);
20366			if ($.any(nextPoint.getWalls(wall), invokeCheckAngle)) {
20367				return false;
20368			}
20369			
20370			// And checking, if our new wall intersecting with other room walls.
20371			// but only for those walls which assigned to any shape 
20372			for (w = 0, n2 = this._model.walls.length; w < n2 && !hasIntersection; w++) {
20373				testWall = this._model.walls[w];
20374				
20375				// Check only walls which assigned to the same shape
20376				if (testWall.indexOf(self) === -1 && testWall.indexOf(nextPoint) === -1) {
20377					for (s = 0, n3 = wall._shapes.length; s < n3; s++) {
20378						if (testWall._shapes.indexOf(wall._shapes[s]) !== -1) {
20379							if (findWallIntersection([this, nextPoint], testWall, true)) {
20380								// check if the walls linked to this corner already has some intersections.
20381								// (possible with old rooms)
20382								hasIntersection = true;
20383								break;
20384							}
20385							
20386							if (findWallIntersection([nextPoint, pretender], testWall, true)) {
20387								willIntersect = true;
20388							}
20389						}
20390					}
20391				}
20392			}
20393			
20394			newWallLength = nextPoint.distance(pretender);
20395			// minimal wall length
20396			if (newWallLength < this._model.settings.minDistance &&
20397					wall.getAttribute('width') > newWallLength) {
20398				return false;
20399			}
20400			
20401			// checking if new wall length is enough to keep all doors/windows
20402			for (s = 0, n2 = wall.components.length; s < n2; s++) {
20403				component = wall.components[s];
20404				if (component.wallX + component.width / 2 > newWallLength) {
20405					return false;
20406				}
20407			}
20408		}
20409		
20410		// disable intersection constraint if walls has intersections before the change.
20411		// (to make able to edit old rooms with intersections).
20412		if (willIntersect && !hasIntersection) {
20413			return false;
20414		}
20415
20416		return true;
20417	},
20418
20419	/**
20420	 * Move corner to the new position
20421	 * @param {Number} dx
20422	 * @param {Number} dy
20423	 * @param {Boolean} dontCheck don't check new constraints for the new position
20424	 */
20425	move: function(dx, dy, dontCheck) {
20426		if (!dontCheck && !this.checkConstraints(dx, dy)) {
20427			return;
20428		}
20429		
20430		var i, ilen, wall, length, j, jlen, component, corners, angle, coeff, x, y,
20431			self = this,
20432			c = this._model.corners;
20433		
20434		this.translate(dx, dy, 0);
20435		// For each wall, which creating the corner
20436		for (i = 0, ilen = this._walls.length; i < ilen; i++) {
20437			wall = this._walls[i];
20438			length = wall.getAttribute('width');
20439			angle = wall.getAngle();
20440			corners = wall.getCorners();
20441			
20442			// ...update position of components (windows/doors) which belongs to this wall
20443			for (j = 0, jlen = wall.components.length; j < jlen; j++) {
20444				component = wall.components[j];
20445				coeff = component.wallX / length;
20446				x = (corners[1].x - corners[0].x) * coeff + corners[0].x;
20447				y = (corners[1].y - corners[0].y) * coeff + corners[0].y;
20448				component.rotate(angle);
20449				component.move( x - component.position.x,
20450								y - component.position.y, 
20451								0, true);
20452                                if (component.level === -1 && component.type === 'component') {
20453                                        component.rotation[2] = component.rotation[2] - 180;
20454                                }
20455
20456			}
20457			wall._updateMiddle();
20458		}
20459		
20460		this.jointPoints();
20461		
20462		$.each(this._walls, function() {
20463			this.getOpposite(self).jointPoints();
20464		});
20465		
20466		this.fireEvent('onMove', dx, dy, 0);
20467	},
20468	
20469	/**
20470	 * Get bounding box for this corner
20471	 * @param {Transform} transform Transform to apply
20472	 * @param {String} transformName Transform name
20473	 * @returns {Object} bounding box hash.
20474	 */
20475	getBBox: function(transform, transformName) {
20476		if (typeof transform === 'undefined') {
20477			return {
20478				x1: this.x,
20479				y1: this.y,
20480				z1: 0,
20481				x2: this.x,
20482				y2: this.y,
20483				z2: this._model.ceilingHeight
20484			};
20485		} else {
20486			var p1 = this.applyTransformation(transform, transformName),
20487				p2 = this.clone().translate(0, 0, this._model.ceilingHeight).applyTransformation(transform);
20488			
20489			return {
20490				x1: p1.x,
20491				y1: p1.y,
20492				z1: p1.z,
20493				x2: p2.x,
20494				y2: p2.y,
20495				z2: p2.z
20496			};
20497		}
20498	},
20499	
20500	/**
20501	 * Return walls connected to current point except given
20502	 * @param {Wall} except Wall to discard from response
20503	 */
20504	getWalls: function(except) {
20505		return except ?
20506			$.grep(this._walls, function(wall) { return except !== wall; }) :
20507			this._walls.slice(0);
20508	},
20509	
20510	/**
20511	 * Attach wall to corner
20512	 * @param {Wall} wall
20513	 * @returns {Corner}
20514	 */
20515	attachWall: function(wall) {
20516		if (this._walls.indexOf(wall) === -1) {
20517			this._walls.push(wall);
20518			this._sortAttachedWalls();
20519			this.jointPoints();
20520			this.fireEvent('neighborsUpdated');
20521		}
20522		return this;
20523	},
20524	
20525	/**
20526	 * Detach wall from corner
20527	 * @param {Wall} wall
20528	 * @returns {Corner}
20529	 */
20530	detachWall: function(wall) {
20531		if ($.extract(wall, this._walls)) {
20532			this.jointPoints();
20533			this.fireEvent('neighborsUpdated');
20534		}
20535		return this;
20536	},
20537	
20538	/**
20539	 * Get shapes from corner
20540	 * @returns {Array}
20541	 */
20542	getShapes: function() {
20543		return this._shapes;
20544	},
20545	
20546	/**
20547	 * Attach shape to this corner
20548	 * @param {Shape} shape
20549	 * @returns {Corner}
20550	 */
20551	attachShape: function(shape) {
20552		if (this._shapes.indexOf(shape) === -1) {
20553			this._shapes.push(shape);
20554		}
20555		return this;
20556	},
20557	
20558	/**
20559	 * Detach shape from this corner
20560	 * @param {Shape} shape
20561	 * @returns {Corner}
20562	 */
20563	detachShape: function(shape) {
20564		if ($.extract(shape, this._shapes)) {
20565			// remove orphaned corner
20566			if (this._walls.length === 0 && this._shapes.length === 0) {
20567				this.remove();
20568			}
20569		}
20570		return this;
20571	},
20572	
20573	/**
20574	 * Sort attached walls by angle
20575	 * @returns {Corner}
20576	 */
20577	_sortAttachedWalls: function() {
20578		var self = this;
20579		this._walls.sort(function(a, b) {
20580			var aangle = self.angleTo(a.getOpposite(self), true),
20581				bangle = self.angleTo(b.getOpposite(self), true);
20582			if (aangle > bangle) {
20583				return -1;
20584			} else if (aangle < bangle) {
20585				return 1;
20586			}
20587			return 0;
20588		});
20589		return this;
20590	},
20591	
20592	/**
20593	 * Calculate joint points for all attached walls
20594	 * @returns {Corner}
20595	 */
20596	jointPoints: function() {
20597		var i, ilen = this._walls.length,
20598			w, n, wT, nT, wA, nA, jA;
20599		
20600		this._joints = {};
20601		
20602		for (i = 0; i < ilen; i++) {
20603			w = this._walls[i];
20604			wT = w.thickness / 2;
20605			wA = this.angleTo(w.getOpposite(this));
20606			
20607			if (!this._joints[w.ID]) {
20608				this._joints[w.ID] = [];
20609			}
20610			
20611			if (ilen === 1) {
20612				// special case when there is only one wall attached
20613				this._joints[w.ID] = [
20614					this.clone().translate(wT, 0).rotate(this, wA + 90),
20615					this.clone().translate(wT, 0).rotate(this, wA + 270)
20616				];
20617			} else {
20618				n = this._walls[(i + 1) % ilen];
20619				nT = n.thickness / 2;
20620				nA = this.angleTo(n.getOpposite(this));
20621				jA = ((nA - wA + 360) % 360);
20622				
20623				// common case for walls with the same thickness and walls with
20624				// different thickness and angle between them in range 160° — 200°
20625				if (wT === nT || (jA > 160 && jA < 200)) {
20626					jA /= 2;
20627					
20628					if (!this._joints[n.ID]) {
20629						this._joints[n.ID] = [];
20630					}
20631				
20632					this._joints[w.ID][0] = this.clone()
20633						.translate(Math.abs(wT / T.sin(jA)), 0)
20634						.rotate(this, jA + wA);
20635					
20636					// if the wall has the same thickness just reference
20637					// original point
20638					if (nT === nA) {
20639						this._joints[n.ID][1] = this._joints[w.ID][0];
20640					} else {
20641						this._joints[n.ID][1] = this.clone()
20642							.translate(Math.abs(nT / T.sin(jA)), 0)
20643							.rotate(this, jA + wA);
20644					}
20645					
20646				// different thickness and angle not in range 160° — 200°
20647				} else {
20648					if (!this._joints[n.ID]) {
20649						this._joints[n.ID] = [];
20650					}
20651					
20652					jA = T.atan2(wT * T.sin(jA), nT + wT * T.cos(jA));
20653					
20654					this._joints[w.ID][0] = this._joints[n.ID][1] = this.clone()
20655						.translate(wT / T.sin(jA), 0)
20656						.rotate(this, jA + wA);
20657				}
20658				
20659			}
20660		}
20661		
20662		return this;
20663	},
20664	
20665	/**
20666	 * Return joints array for specified wall in order where specified wall is
20667	 * first in the list
20668	 * @param {Object} wall
20669	 * @returns {Array}
20670	 */
20671	getJoints: function(wall) {
20672		return this._joints[wall.ID] || [];
20673	},
20674	
20675	/**
20676	 * Returns list of connected walls in joint relative to given wall
20677	 * @param {Object} wall
20678	 * @returns {Array}
20679	 */
20680	getJointWalls: function(wall) {
20681		var i = this._walls.indexOf(wall),
20682			ilen = this._walls.length,
20683			ret = [];
20684		
20685		if (i !== -1) {
20686			ret = [
20687				this._walls[(i + 1) % ilen],
20688				this._walls[(i - 1 + ilen) % ilen]
20689			];
20690		}
20691		return ret;
20692	},
20693	
20694	/**
20695	 * Remove corner from model
20696	 */
20697	remove: function() {
20698		// var i, ilen;
20699		
20700		this._model.unselect(this);
20701		
20702		// find shapes which have those point and extract it
20703		// for (i = 0, ilen = this.shapes.length; i < ilen; i++) {
20704			// if (this._model.shapes[i].indexOf(this) !== -1) {
20705				// this.
20706			// }
20707		// }
20708		// this._model.shapes.
20709		
20710		// extract self from snap points
20711		$.extract(this, this._model._snapPoints);
20712		// deregister self from model
20713		$.extract(this, this._model.corners);
20714		
20715		this.fireEvent('onRemove', true);
20716	},
20717	
20718	/**
20719	 * Returns list of directly attached points detected by attached shapes
20720	 * except given one
20721	 * @param {Array} except Array of corners to exclude
20722	 * @returns {Array} array of attached points
20723	 */
20724	getCorners: function(except, includeSelf, onlyWalls) {
20725		var i, points = [];
20726		
20727		// from  shapes
20728		if (!onlyWalls) {
20729			for (i = this._shapes.length - 1; i >= 0; i--) {
20730				points = points.concat(this._shapes[i].getNeighbors(this));
20731			}
20732		}
20733		
20734		// from walls
20735		for (i = this._walls.length - 1; i >= 0; i--) {
20736			points.push(this._walls[i].getOpposite(this));
20737		}
20738		
20739		if (includeSelf) {
20740			points.push(this);
20741		}
20742		
20743		points = points.unique();
20744		
20745		if ($.isArray(except)) {
20746			points = $.grep(points, function(point) {
20747				return except.indexOf(point) === -1;
20748			});
20749		}
20750		
20751		return points;
20752	}
20753});
20754/*globals $, myDeco, commonMethods, Point, Transform, T */
20755/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
20756          sloppy: true, white: true */
20757/*
20758 * Components class for the model (derived from common ancestor from entity.js)
20759 */
20760
20761var componentMethods = $.extend({}, commonMethods, {
20762	/**
20763	 * type of the entity
20764	 * @type string
20765	 */
20766	type: 'component',
20767
20768	/**
20769	 * Cached points of component's bounding box.
20770	 * @type Array of Points.
20771	 */
20772	_bbox: [],
20773
20774	/**
20775	 * Top surface of component.
20776	 * @type Array of Points.
20777	 */
20778	_surface: [],
20779	
20780	/**
20781	 * Component type, values are:
20782	 * -2 Ceiling fixtures such as lights
20783	 * -1 Windows, doors, pictures, mirrors and other objects which would be attached to a wall
20784	 *  0 Rugs, carpet and other floor coverings
20785	 *  1 Chairs, tables, sofas, beds and most other furniture
20786	 *  2 Desk lamps, cushions, ornaments and other objects which wouldn't normally rest on the floor
20787	 * @type {Number}
20788	 */
20789	// DO NOT DEFINE THIS PROPERTY!
20790	// level: 0,
20791	
20792	/**
20793	 * Emulation of the constructor. Needed to store pointer to the model and precalculate Transform object.
20794	 */
20795	init: function(model) {
20796		var i, p, self = this;
20797		
20798		this._model = model;
20799			
20800		// default level
20801		this.level = this.level || 0;
20802
20803		if (this.category === 'Door' || this.category === 'Window') {
20804			this.type = this.category.toLowerCase();
20805			this.acceptDecor = true;
20806			this.level = -1;
20807		}
20808
20809		if (this.type === 'door') {
20810			// put door to floor
20811			this.position.z = this.height / 2;
20812		}
20813		this._bbox = [];
20814		for (i = 0; i < 8; i++) {
20815			p = new Point();
20816			this._bbox.push(p);
20817		}
20818
20819		// check for configuration given
20820		if (this.configuration) {
20821			this.setAttribute(this.configuration, undefined, true);
20822		}
20823		
20824		// for on-wall components
20825		switch(this.level) {
20826			case -2:
20827				this.position.z = this._model.ceilingHeight;
20828				break;
20829			case -1:
20830				this.parent = this.parent || this._model.findNearestWall(this.position, false, this.width);
20831				if (this.parent) {
20832					this.placeToWall(this.parent, $.grep(this._model.shapes, function() {
20833						return this.ID === self.area;
20834					}).shift());
20835					this.updateInWallCoords();
20836				}
20837				break;
20838			case 0:
20839				this.position.z = 0;
20840				break;
20841			case 1:
20842			case 2:
20843				this.position.z = this._calculateZposition(this.position.x, this.position.y);
20844				break;
20845		}
20846
20847		if (this.type === 'component') {
20848			this.position.type = 'component';
20849		}
20850		// SITE-8241
20851		// this._model._snapPoints.push(this.position);
20852
20853		this._calculateTransform();
20854		
20855		this._surface.push(this._bbox[1]);
20856		this._surface.push(this._bbox[3]);
20857		this._surface.push(this._bbox[7]);
20858		this._surface.push(this._bbox[5]);
20859
20860		if (!this.category && (this.level === 1 || this.level === 0)) {
20861			this._surface.surface_height = this.surface_height;
20862			this._surface.level = this.level;
20863			this._surface.ID = this.ID;
20864			this._surface.position = this.position;
20865			this._surface.zDiff = this.height - this.surface_height;
20866			this._model._surfaces.push(this._surface);
20867		}
20868		
20869		this._model[this.type + 's'].push(this);
20870		
20871		if (this.material && $.isPlainObject(this.material)) {
20872			this._model.assignMaterial(this, this.material);
20873		}
20874
20875		return this;
20876	},
20877
20878	/**
20879	 * Move to the new position method
20880	 * @param {Number} dx
20881	 * @param {Number} dy
20882	 * @param {Number} dz
20883	 * @param {Any} notUsed to skip parameter passed to doors and windows
20884	 */
20885	move: function(dx, dy, dz, disableRestrictions) {
20886		var halfH, halfW, corners, dx2, dy2, dl, angle, cosA, sinA, wallLength,
20887			wx, i;	
20888		dx = +(dx || 0);
20889		dy = +(dy || 0);
20890
20891		if (!disableRestrictions && this.parent) {
20892			// Calculating projection of the new position of the component to it's wall
20893			corners = this.parent.getCorners();
20894			dx2 = corners[1].x - corners[0].x;
20895			dy2 = corners[1].y - corners[0].y;
20896			dl = (dy * dy2 + dx2 * dx) / Math.sqrt(dx2 * dx2 + dy2 * dy2);
20897			angle = T.atan2(dy2, dx2);
20898			cosA = T.cos(angle);
20899			sinA = T.sin(angle);
20900			halfW = this.width / 2;
20901			
20902			// Use this projection as new dx/dy to kept component in plane of the wall.
20903			dx = dl * cosA;
20904			dy = dl * sinA;
20905			
20906			// Checking if our new position of the window is in bounds of the wall
20907			wallLength = this.parent.getAttribute('width');
20908			wx = this._calculateWallX(dx, dy);
20909			// on-wall components outside the walls aren't allowed :)
20910			if (wx < halfW) {
20911				dl = halfW - wx;
20912				dx += dl * cosA;
20913				dy += dl * sinA;
20914			} else if (wx + halfW > wallLength) {
20915
20916				dl = wx - wallLength + halfW;
20917				dx -= dl * cosA;
20918				dy -= dl * sinA;
20919			}
20920		}
20921		
20922		if (this.level >= 1 && dz === undefined) {
20923			dz = this._calculateZposition((this.position.x || 0) + dx, (this.position.y || 0) + dy) - this.position.z;
20924
20925		} else if (dz === undefined) {
20926			dz = 0;
20927		}
20928		
20929		if (!disableRestrictions) {
20930			halfH = this.height / 2;
20931
20932			if (this.type === 'door (DISABLED)') {
20933				// door should be on ground
20934				dz = halfH - this.position.z;
20935			} else if (this.level === 0) {
20936				// ground components, should be always on floor
20937				dz = -this.position.z;
20938			} else if (this.level === -1) {
20939				// on-wall components, should be at least under ceiling
20940				dz = Math.max(Math.min(this.position.z + dz,
20941					this._model.ceilingHeight - halfH), halfH) -
20942						this.position.z;
20943			} else if (this.level === -2) {
20944				// ceiling components
20945				dz = this._model.ceilingHeight - this.position.z;
20946			}
20947		}
20948		
20949		this.position.translate(dx, dy, dz);
20950
20951		for (i = 0; i < 8; i++) {
20952			this._bbox[i].translate(dx, dy, dz);
20953		}
20954		
20955		// for on-wall components
20956		if (this.parent) {
20957			this.updateInWallCoords();
20958		}
20959		
20960		this.fireEvent('onMove', dx, dy, dz);
20961	},
20962
20963	/**
20964	 * Rotate the component
20965	 * @param {Number} angle new component angle in x0y plane
20966	 */
20967	rotate: function(angle) {
20968		var oldAngle = this.rotation[2], i;
20969		this.rotation[2] = (angle + (this.x0yRotation || 0)) % 360;
20970		for (i = 0; i < 8; i++) {
20971			this._bbox[i].rotate(this.position, this.rotation[2] - oldAngle);
20972		}
20973		this._calculateTransform();
20974		this.fireEvent('onRotate', this.rotation[2]);
20975	},
20976
20977	/**
20978	 * Get bounding box for the component
20979	 * @param {Transform} transform Transform to apply
20980	 * @param {String} transformName Transform name
20981	 * @returns {Object} bounding box hash.
20982	 */
20983	getBBox: function(transform, transformName) {
20984		var x = [], y = [], z = [],
20985			q1, q2, q3, p, p1, p2;
20986			
20987		for (q1 = 0; q1 <= 1; q1++) {
20988			for (q2 = 0; q2 <= 1; q2++) {
20989				for (q3 = 0; q3 <= 1; q3++) {
20990					p = new Point(this.bounding_box[q1].x, this.bounding_box[q2].y, this.bounding_box[q3].z)
20991						.translate(this.position.x, this.position.y, this.position.z)
20992						.rotate(this.position, this.rotation[2], this.rotation[0], this.rotation[1]);
20993
20994					if (transform) {
20995						p.applyTransformation(transform);
20996					}
20997						
20998					x.push(p.x);
20999					y.push(p.y);
21000					z.push(p.z);
21001				}
21002			}
21003		}
21004
21005		// Main goal of this function - calculation of the bbox for doors, which opening outside and may affect bbox of the full room
21006		if (this.open_direction === 'o') {
21007			p1 = this.position
21008				.clone()
21009				.translate(this.width / 2, this.bounding_box[1].z, 0);
21010			p2 = p1.clone()
21011				.translate( - this.width * (this.open_side === 'b' ? 0.5 : 1), 0, 0)
21012				// Magic numbers
21013				.rotate(p1, -30 * (this.open_side === 'b' ? 0.5 : 1))
21014				.rotate(this.position, this.rotation[2]);
21015
21016			if (transform) {
21017				p2.applyTransformation(transform);
21018			}
21019			
21020			x.push(p2.x);
21021			y.push(p2.y);
21022			z.push(p2.z);
21023		}		
21024
21025		return {
21026			x1: Math.min.apply(Math, x),
21027			y1: Math.min.apply(Math, y),
21028			z1: Math.min.apply(Math, z),
21029			x2: Math.max.apply(Math, x),
21030			y2: Math.max.apply(Math, y),
21031			z2: Math.max.apply(Math, z)
21032		};
21033	},
21034	
21035	/**
21036	 * Update cached points for bbox and transformation matrix
21037	 */
21038	_calculateTransform: function() {
21039		if (this._transform) {
21040			this._transform.reset();
21041		} else {
21042			this._transform = new Transform();
21043		}
21044
21045		var q1 = (this.bounding_box[1].x + this.bounding_box[0].x),
21046			q2 = (this.bounding_box[1].y + this.bounding_box[0].y),
21047			q3 = (this.bounding_box[1].z + this.bounding_box[0].z),
21048			i = 0;
21049
21050		this._transform
21051			.translate(new Point(-q1, -q2, -q3))
21052			.rotate('x',  this.rotation[0])
21053			.rotate('y',  this.rotation[1])
21054			.rotate('z', -this.rotation[2])
21055			.translate(new Point(q1, q2, q3))
21056			.translate(this.position);
21057
21058		for (q1 = 0; q1 <= 1; q1++) {
21059			for (q2 = 0; q2 <= 1; q2++) {
21060				for (q3 = 0; q3 <= 1; q3++) {
21061					this._bbox[i].x = this.bounding_box[q1].x;
21062					this._bbox[i].y = this.bounding_box[q2].y;
21063					this._bbox[i].z = this.bounding_box[q3].z;
21064					
21065					this._bbox[i].applyTransformation(this._transform);
21066					i++;
21067				}
21068			}
21069		}
21070	},
21071	
21072	/**
21073	 * Return angle between of the component in x0y plane
21074	 * @returns Number
21075	 */
21076	getAngle: function() {
21077		return this.rotation[2];
21078	},
21079
21080	/**
21081	 * Return new in-wall x coord after moving the component 
21082	 * @param {Number} dx
21083	 * @param {Number} dy
21084	 * @returns {Boolean} new in-wall x coord 
21085	 */
21086	_calculateWallX: function(dx, dy) {
21087		return this.position.clone()
21088			.translate(dx || 0, dy || 0)
21089			.distance(this.parent.getCorners()[0], 'xy');
21090	},
21091
21092	/**
21093	 * Update in-wall coords from real world ones.
21094	 */
21095	updateInWallCoords: function() {
21096		this.wallX = this._calculateWallX();
21097		this.wallY = this.position.z;
21098	},
21099	
21100	/**
21101	 * Calculate Z position of element
21102	 * @param {Number} x
21103	 * @param {Number} y
21104	 * @returns {Number} z
21105	 */
21106	_calculateZposition: function(x, y) {
21107		var z = 0,
21108			self = this,
21109			surfaces, p, i, len;
21110		
21111		switch (this.level || 0) {
21112			case -2:
21113				z = this._model.ceilingHeight;
21114			break;
21115			case 0:
21116				z = this.position.z || 0;
21117			break;
21118			case -1:
21119				z = this.position.z || this._model.ceilingHeight / 2;
21120			break;
21121			case 1:
21122			case 2:
21123				p = new Point(x, y, 0);
21124				
21125				// Trying to find intersection of the component position with other component's surfaces
21126				surfaces = $.grep(this._model._surfaces, function(surface) {
21127					return (self.level > surface.level) && p.inPoly(surface);
21128				});
21129				
21130				if (surfaces.length > 0) {
21131					z = Number.MIN_VALUE;
21132					for (i = 0, len = surfaces.length; i < len; i++) {
21133						z = Math.max(surfaces[i].surface_height + surfaces[i].position.z, z);
21134					}
21135				}
21136			break;
21137		}
21138		return z;
21139	},
21140
21141	/**
21142	 * Get component attribute
21143	 * @param {String} name Attribute name
21144	 * @returns {Mixed}
21145	 */
21146	getAttribute: function(name) {
21147		return this[name];
21148	},
21149	
21150	/**
21151	 * Set component attribute
21152	 * @param {String} name Attribute name
21153	 * @param {Mixed}
21153 name Attribute value
21154	 * @param {Boolean} noevent Skip event propagation
21155	 * @returns {Object} this
21156	 */
21157	setAttribute: function(name, value, noevent) {
21158		var self = this,
21159			map = {width: 'x', height: 'y', depth: 'z'},
21160			limit;
21161
21162		if ($.isPlainObject(name)) {
21163			$.each(name, function(key, value) {
21164				self.setAttribute(key, value, noevent);
21165			});
21166			return;
21167		}
21168
21169		if (!this.configurations || !this.configurations[name]) {
21170			return;
21171		}
21172
21173		switch(name) {
21174			case 'width':
21175				limit = this.parent ? this.parent.getAttribute('width') : value;
21176				break;
21177			case 'height':
21178				limit = this.parent ? this.parent.getAttribute('height') : value;
21179				break;
21180			case 'depth':
21181				limit = value;
21182				/*if (this.parent && (this.type === 'door' || this.type === 'window')) {
21183					limit = value = this.parent.thickness;
21184				}*/
21185				break;
21186			case 'open_side':
21187			case 'open_direction':
21188				limit = $.map(this.configurations[name].values, function(v) {
21189					return v[0];
21190				});
21191				if (limit.indexOf(value) === -1) {
21192					// keep old value
21193					value = this[name];
21194				}
21195				break;
21196			default:
21197				return;
21198		}
21199
21200		if (map[name]) {
21201			value = Math.max(this.configurations[name].minimum,
21202				Math.min(value, this.configurations[name].maximum, limit));
21203			// correct bbox
21204			this.bounding_box[0][map[name]] = -value / 2;
21205			this.bounding_box[1][map[name]] = value / 2;
21206			this._calculateTransform();
21207		}
21208
21209		this[name] = value;
21210
21211		// force model to align inside wall
21212		if (map[name] && this.parent) {
21213			this.move();
21214		}
21215
21216		if (!noevent) {
21217			this.fireEvent('onAttributeChange', name, value);
21218			this._model.operationEnd(['attributeChanged', this]);
21219		}
21220
21221		return this;
21222	},
21223
21224	/**
21225	 * Move component to a specified wall and place it properly
21226	 * @param {Object} wall Wall attach to
21227	 * @param {Object} shape Shape which wall attached to
21228	 */
21229	placeToWall: function(wall, shape) {
21230		// find shape if it not given
21231		this._shape = shape = shape || (function() {
21232			var shapes = wall.getShapes(),
21233				i, ilen;
21234			for (i = 0, ilen = shapes.length; i < ilen; i++) {
21235				if (this.position.inPoly(shapes[i])) {
21236					return shapes[i];
21237				}
21238			}
21239			// didn't find shape, use first one
21240			return shapes[0];
21241		}.call(this));
21242
21243		// place it to a correct position
21244		var d = this.position.distance(wall),
21245			angle = wall.getAngle(),
21246			dx = -d * T.cos(angle + 90),
21247			dy = d * T.sin(angle + 90),
21248			dz = 0,
21249			wallX = 0,
21250			wallL = wall.getAttribute('width'),
21251			halfW = this.width / 2,
21252			halfH = this.height / 2;
21253
21254		// remove component from old wall if exist
21255		if (this.parent) {
21256			this.parent.removeComponent(this);
21257		}
21258		
21259		this.x0yRotation = wall.getAngle(shape) ? (wall.getAngle(shape) - angle) : 180;
21260		// add component to a new wall
21261		wall.addComponent(this);
21262		wallX = this._calculateWallX(dx, dy);
21263		
21264		if (this.position.z - halfH < 0) {
21265			dz = halfH - this.position.z;
21266		} else if (this.position.z + halfH > this._model.ceilingHeight) {
21267			dz = this._model.ceilingHeight - this.position.z - halfH;
21268		}
21269
21270		this.rotate(angle, false);
21271
21272		// disable restrictions for doors and windows
21273		this.move(dx, dy, dz, true);
21274		if (this.parent && (this.type === 'door' || this.type === 'window')) {
21275			this.setAttribute('depth', 6, true);
21276		}
21277		
21278		this.updateInWallCoords();
21279	},
21280	
21281	/**
21282	 * Event method executed on operation end
21283	 * @param {String} name Operation name
21284	 */
21285	operationEnd: function(name) {
21286		if ((name === 'drag' || name === 'add') && 
21287				this.level === -1 &&
21288				this.type !== 'door' && 
21289				this.type !== 'window') {
21290			if(name === 'drag') {
21291				// Degrading gratefuly if there are no wall of desired size for the component;
21292				var wall = this._model.findNearestWall(this.position, false, this.width) ||
21293					   this._model.findNearestWall(this.position);
21294
21295				this.placeToWall(wall);
21296			}
21297			this.rotation[2] -= 180;
21298		}
21299
21300		this.fireEvent('onOperationEnd', name);
21301	},
21302	
21303	/**
21304	 * Get distance from floor to component base
21305	 * @return {Number}
21306	 */
21307	getFloorDistance: function() {
21308		return this.position.z - this.height / 2;
21309	},
21310	
21311	/**
21312	 * Set new distance from floor to component base
21313	 * @param {Number} distance
21314	 * @return {Number}
21315	 */
21316	setFloorDistance: function(distance) {
21317		this.rotation[2] += 180;
21318		this.move(0, 0, distance + this.height / 2 - this.position.z);
21319		this._model.operationEnd(['componentFloorDistance', this]);
21320		this.rotation[2] -= 180;
21321		return this.getFloorDistance();
21322	},
21323	
21324	/**
21325	 * Remove entity from model
21326	 */
21327	remove: function() {
21328		this._model.unselect(this);
21329
21330		// remove material
21331		if (this.material) {
21332			this._model.assignMaterial(this);
21333		}
21334		
21335		// remove self from model
21336		$.extract(this, this._model[this.type + 's']);
21337		
21338		// remove self from snap points
21339		// SITE-8241
21340		// $.extract(this.position, this._model._snapPoints);
21341		
21342		// dereference self from parent
21343		if (this.parent) {
21344			this.parent.removeComponent(this);
21345			this.parent = false;
21346		}
21347		
21348		// deregister self from model surfaces
21349		$.extract(this._surface, this._model._surfaces);
21350		
21351		this.fireEvent('onRemove', true);
21352
21353		if (this.type === 'component') {
21354			myDeco.body.trigger('componentRemoved', [this]);
21355		}
21356	}
21357});
21358/*globals $, AppModel, myDeco, shapeMethods, Point, Image, componentMethods,
21359          cornerMethods, wallMethods, cameraMethods, diffuse2Hex,
21360          weilerAtherton, window, findWallIntersection, emptyRoom */
21361/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
21362          sloppy: true, white: true */
21363/**
21364 * FPE room model
21365 */
21366var FpeModel = AppModel.extend({
21367	/**
21368	 * Tolerancy value for finding common points of different polygons
21369	 * @type {Number}
21370	 */
21371	_tolerancy: 0.5,
21372	
21373	/**
21374	 * Collection of anchor points for snapping
21375	 * all points with type defined will work for draw/corner movement
21376	 * @type {Array}
21377	 */
21378	_snapPoints: [],
21379	
21380	/**
21381	 * List of registered top surfaces, grouped by level
21382	 * @type {Array}
21383	 */
21384	_surfaces: [],
21385
21386	/**
21387	 * Model, stored for simple, one step undo
21388	 * @type {Model}
21389	 */
21390	_undoModel: false,
21391	_currModel: false,
21392	_redoModel: false,
21393	
21394	/**
21395	 * Object used for storage all used components in a clear state
21396	 * @type {Object}
21397	 * @private
21398	 */
21399	_componentsCache: {},
21400	
21401	/**
21402	 * Object used for storage all inexistent components
21403	 * @type {Object}
21404	 * @private
21405	 */
21406	_componentsFailed: {},
21407	
21408	/**
21409	 * List of selected entities
21410	 * @type {Array}
21411	 * @private
21412	 */
21413	_selected: [],
21414	
21415	/**
21416	 * Selection box, third point is a center of selection
21417	 * @type {Array}
21418	 */
21419	_selectionCenter: new Point(),
21420	rotationAvailable: false,
21421	
21422	/**
21423	 * Is floorplan locked for change or not
21424	 * @type {Boolean}
21425	 */
21426	_locked: false,
21427	
21428	/**
21429	 * Masking mode flag
21430	 * @type {Boolean}
21431	 */
21432	_masking: false,
21433	
21434	/**
21435	 * Model settings
21436	 * @type {Object}
21437	 */
21438	settings: {
21439		defaultMaterials: {
21440			'wall': {
21441				"shader": "combi_material",
21442				"ID": "materialWallDefault",
21443				"use_diffuse_map": false,
21444				"ambient": [0, 0, 0],
21445				"shiny": 0,
21446				"diffuse": [0.93333333333333333, 0.9372549019607843, 0.9176470588235294]
21447			},
21448			'floor': {
21449				"diffuse_texture": "floor_and_wall/original/Pine_Wood_Floor_Planks2.jpg",
21450				"shader": "combi_material",
21451				"diffuse": [1, 1, 1],
21452				"ambient": [0.050000000000000003, 0.050000000000000003, 0.050000000000000003],
21453				"use_diffusemap": "on",
21454				"shiny": 0.20000000000000001,
21455				"ID": "decor-2542", // materialFloorDefault
21456                "texture_width": 90,
21457                "texture_height": 179
21458			},
21459			'ceiling': {
21460				"shader": "combi_material",
21461				"ID": "materialCeilingDefault",
21462				"use_diffuse_map": false,
21463				"ambient": [0, 0, 0],
21464				"shiny": 0,
21465				"diffuse": [0.97599999999999998, 0.97599999999999998, 0.97599999999999998]
21466			}
21467		},
21468		// http request timeout in seconds
21469		renderRequestTimeout: 60
21470	},
21471
21472	_metadata: {
21473		"rating": 0,
21474		"description": "",
21475		"category": "",
21476		"view_count": 0,
21477		"background_image": null,
21478		"name": "",
21479		"public": 1,
21480		"comment_count": 0,
21481		"type": "3d",
21482		"image": ""
21483	},
21484
21485	/**
21486	 * Model initialization
21487	 */
21488	init: function() {
21489		var self = this;
21490
21491		this._super.apply(this, arguments);
21492
21493		myDeco.body
21494			.bind('roomSaved', function(e, roomType, roomId, options) {
21495				if (self._masking) {
21496                    self._ws_url = options.originalURL;
21497					self.deleteRoom();
21498				}
21499			});	
21500	},
21501
21502	// TBD: remove when properties panel are ready
21503	/**
21504	 * Fill room with metadada
21505	 * @param {Event} e
21506	 * @param {Function|Object} callbackOrData
21507	 */
21508	metadata: function(e, callbackOrData) {
21509		if ($.isFunction(callbackOrData)) {
21510			var match = this._ws_url.match(/\/(\d+)\/?/),
21511				parent_id = match ? parseInt(match[1], 10) : this._room_id || 0;
21512			
21513			callbackOrData(
21514				$.extend({
21515						hq_renderer: this.camera.hq_renderer,
21516						exposure: this.camera.exposure
21517					}, this._metadata),
21518				parent_id, e);
21519		} else {
21520			this._metadata = $.extend(true, this._metadata, callbackOrData);
21521			this.camera.hq_renderer = this._metadata.hq_renderer || myDeco.settings.DEFAULT_HQ_RENDERER;
21522			this.camera.exposure = this._metadata.exposure || 0;
21523			delete this._metadata.hq_renderer;
21524			delete this._metadata.exposure;
21525		}
21526	},
21527
21528	/**
21529	 * Reset the model to initial state
21530	 * @param {Boolean} keepInfo Keep room info
21531	 */
21532	reset: function(keepInfo) {
21533		keepInfo = keepInfo || false;
21534		
21535		if (!keepInfo) {
21536			this._super();
21537			this._renderUrl = myDeco.urls.renderws;
21538			
21539			// cleanup memory cache
21540			myDeco.App.ws.proxy.clear();
21541		}
21542		
21543		this.corners = [];
21544		this.walls = [];
21545		this.shapes = [];
21546		this.components = [];
21547		this.camera = {};
21548		this.materials = [];
21549		this.doors = [];
21550		this.windows = [];
21551		this.ceilingHeight = 265;
21552		this.environment = {};
21553		
21554		this._surfaces = [];
21555		this._snapPoints = [];
21556		this._currModel = false;
21557		this._selected = [];
21558		
21559		this._componentCounter = 0;
21560		this._wallCounter = 0;
21561		this._areaCounter = 0;
21562        this.light_env = undefined;
21563		
21564		myDeco.body.trigger('roomCleared');
21565	},
21566	
21567	/**
21568	 * Set new height for ceiling
21569	 * @param {Number} height New height to set
21570	 */ 
21571	setCeilingHeight: function(height) {
21572		var components = this.doors.concat(this.windows,
21573				$.grep(this.components, function(component) {
21574					return component.level === -1;
21575				})),
21576			heights = $.map(components, function(component) {
21577				return component.height;
21578			}),
21579			ceilingComponents = $.grep(this.components, function(component) {
21580				return component.level === -2;
21581			});
21582		// limit min wall height to 2m
21583		heights.push(height, 200);
21584		this.ceilingHeight = Math.max.apply(Math, heights);
21585		$.each(components, function() {
21586			this.move(0, 0, 0);
21587		});
21588		for (var i = 0; i < ceilingComponents.length; i++) {
21589			ceilingComponents[i].move(0, 0, ceilingComponents[i]._calculateZposition() - ceilingComponents[i].position.z);
21590		}
21591	},
21592	
21593	/**
21594	 * Find common wall which belongs to both corners
21595	 * @param {Corner} c1 First corner
21596	 * @param {Corner} c2 Second corner
21597	 * @return {Wall}
21598	 */
21599	_findWall: function(c1, c2) {
21600		return c1.getWalls && c2.getWalls ? $.intersect(c1.getWalls(), c2.getWalls()).shift() : false;
21601	},
21602	
21603	/**
21604	 * Generate component ID for a new one
21605	 * @return {String}
21606	 */
21607	generateComponentId: function() {
21608		return 'model-' + this._componentCounter++;
21609	},
21610	
21611	/**
21612	 * Process component to add/update some fields
21613	 * @param {Component} component
21614	 * @param {Boolean} preserveID
21615	 */
21616	processComponent: function(component, preserveID) {
21617		var j, jlen, side, rotation,
21618			sizeGrep = function(size) {
21619				return $.values(size[2]).indexOf(component.component_id) !== -1;
21620			};
21621		
21622		if (preserveID && component.ID) {
21623			j = +component.ID.match(/(\d+)/)[1];
21624			if (j >= this._componentCounter) {
21625				this._componentCounter = j + 1;
21626			}
21627		} else {
21628			component.ID = this.generateComponentId();
21629		}
21630		
21631		if (component._isProcessed) {
21632			return;
21633		}
21634			
21635		for (j = 0, jlen = component.bounding_box.length; j < jlen; j++) {
21636			component.bounding_box[j] = new Point(component.bounding_box[j]);
21637		}
21638
21639		for (j = 0, jlen = component.rotation.length; j < jlen; j++) {
21640			rotation = Math.round(component.rotation[j]) % 360;
21641			component.rotation[j] = (rotation < 0 ? 360 + rotation : rotation);
21642		}
21643		
21644		side = {
21645			'0':   {x: 'x', y: 'y', z: 'z', x0yRender: 'render_top'},
21646			'90':  {x: 'x', y: 'z', z: 'y', x0yRender: 'render_back'},
21647			'180': {x: 'x', y: 'y', z: 'z', x0yRender: 'render_bottom'},
21648			'270': {x: 'x', y: 'z', z: 'y', x0yRender: 'render_front'}
21649		}[component.rotation[0]];
21650
21651		// determining prerendered image for plan view.
21652		component.x0yRender = side.x0yRender;
21653
21654		if (!component.configuration) {
21655			component.width = Math.abs(component.bounding_box[0][side.x] - component.bounding_box[1][side.x]);
21656			component.depth = Math.abs(component.bounding_box[0][side.y] - component.bounding_box[1][side.y]);
21657			component.height = Math.abs(component.bounding_box[0][side.z] - component.bounding_box[1][side.z]);
21658			component.centerZ = -component.bounding_box[0][side.z];
21659		}	
21660
21661		// component shift from central point
21662		component.shift = {
21663			dx: component.bounding_box[1][side.x] - component.width / 2,
21664			dy: component.bounding_box[1][side.y] - component.depth / 2,
21665			dz: component.bounding_box[1][side.z] - component.height / 2
21666		};
21667
21668		// find aperture
21669		if (typeof component.aperture === 'string' &&
21670				(component.category === 'Door' || component.category === 'Window')) {
21671			component.aperture = {
21672				type: component.aperture,
21673				bounds: [component.width, component.height]
21674			};
21675		}
21676
21677		component._isProcessed = true;
21678	},
21679	
21680	/**
21681	 * Assign environment to a model
21682	 * @param {String} type Env type, 'image' is a good one
21683	 * @param {String} value
21684	 */
21685	assignEnvironment: function(type, value) {
21686		this.environment[type] = value;
21687	},
21688	
21689	/**
21690	 * Remove specified environment from model
21691	 * @param {String} type Env type
21692	 */
21693	removeEnvironment: function(type) {
21694		if (typeof this.environment[type] !== 'undefined') {
21695			delete this.environment[type];
21696		}
21697	},
21698	
21699	/**
21700	 * Assign photo to the wall
21701	 * @param {Object} target Object assign photo to, usually Wall
21702	 * @param {Object} photo Photo to assign, should have id and image in properties
21703	 *                 If not specified will change properties if photo is applied
21704	 * @param {Object} properties Photo positioning properties
21705	 */
21706	assignPhoto: function(target, photo, properties) {
21707		// duplicate material if refcounter more than one
21708		var material = target.material,
21709			self = this,
21710			image;
21711
21712		// no photo given and material hasn't image at all
21713		if (!photo && typeof material.photo === 'undefined') {
21714			return;
21715		}
21716		
21717		function setProperties(material, properties) {
21718			$.extend(true, material.photo, properties || {});
21719			var p = material.photo;
21720			// prevent of settings too small image
21721			p.width = p.width < 1 ? 1 : p.width;
21722			p.height = p.height < 1 ? 1 : p.height;
21723			
21724			material.decal_repeat_u = p.repeatX;
21725			material.decal_repeat_v = p.repeatY;
21726			material.decal_transform = [
21727				[100 / p.width, 0,              -(p.x / p.width) ],
21728				[0,             100 / p.height, -(p.y / p.height)]
21729			];
21730		}
21731		
21732		// fix ID if a new photo given
21733		if (photo) {
21734			material = $.extend(true, {}, material);
21735			delete material._refcounter;
21736			
21737			// remove complex material id if exist
21738			if (typeof material.photo !== 'undefined') {
21739				material.ID = material.ID.substring(material.ID.indexOf(':') + 1);
21740			}
21741				
21742			image = new Image();
21743			
21744			image.onload = function() {
21745				// construct unique ID
21746				material.ID = target.ID + '-' + photo.component_id + ':' + material.ID;
21747				
21748				material.photo = {
21749					orig: {
21750						width: image.width,
21751						height: image.height
21752					},
21753					x: (target.getAttribute('width') - image.width) / 2,
21754					y: (target.getAttribute('height') - image.height) / 2,
21755					width: image.width,
21756					height: image.height,
21757					repeatX: false,
21758					repeatY: false
21759				};
21760				material.decal_texture = photo.texture;
21761				setProperties(material);
21762				self.assignMaterial(target, material);
21763				self.operationEnd(['material']);
21764			};
21765			image.src = photo.image;
21766		} else {
21767			setProperties(material, properties);
21768			// notify target about changed properties
21769			target.fireEvent('onMaterial');
21770		}
21771	},
21772	
21773	/**
21774	 * Remove photo from wall
21775	 * @param {Object} target Source which have an photo attached
21776	 */
21777	removePhoto: function(target) {
21778		if (typeof target.material.photo !== 'undefined') {
21779			var material = $.extend(true, {}, target.material, {
21780				ID: target.material.ID.substring(target.material.ID.indexOf(':') + 1)
21781			});
21782			delete material.photo;
21783			delete material.decal_texture;
21784			delete material.decal_transform;
21785			delete material.decal_repeat_u;
21786			delete material.decal_repeat_v;
21787			this.assignMaterial(target, material, 'wall', true);
21788		}
21789	},
21790
21791	/**
21792	 * Assign material to the specified entity
21793	 * @param {Entity} target Target material assign to
21794	 * @param {Material} material
21795	 * @param {String} type Target destination type, allowed 'ceiling', 'floor' and 'wall'
21796	 * @param {Boolean}
21796 skipPhoto Skip photo value on assigning
21797	 */
21798	assignMaterial: function(target, material, type, skipPhoto) {
21799		var found = false, oldMaterial, i, len;
21800		type = type || target.type;
21801
21802		if (material) {
21803			if (target.material && target.material.photo && !skipPhoto) {
21804				material = $.extend(true, {}, material, material.photo ? {} : {
21805					photo: target.material.photo,
21806					decal_texture: target.material.decal_texture,
21807					decal_transform: target.material.decal_transform,
21808					decal_repeat_u: target.material.decal_repeat_u,
21809					decal_repeat_v: target.material.decal_repeat_v,
21810					ID: target.material.ID.substring(0, target.material.ID.indexOf(':')) + ':' + material.ID
21811				});
21812			} else {
21813				// Checking if we already have such material in model
21814				for (i = 0, len = this.materials.length; i < len; i++) {
21815					if (this.materials[i].ID === material.ID) {
21816						material = this.materials[i];
21817						found = true;
21818						break;
21819					}
21820				}
21821			}
21822
21823			if (!material._refcounter) {
21824				material._refcounter = 0;
21825			}
21826
21827			material._refcounter++;
21828			if (!found) {
21829				this.materials.push(material);
21830				myDeco.body.trigger('materialAdded', [material]);
21831			}
21832		}
21833
21834		switch (type) {
21835			case 'ceiling':
21836				oldMaterial = target.ceilingMaterial;
21837				target.ceilingMaterial = material;
21838				break;
21839				
21840			case 'floor':
21841				oldMaterial = target.floorMaterial;
21842				target.floorMaterial = material;
21843				break;
21844				
21845			default:
21846				oldMaterial = target.material;
21847				target.material = material;
21848		}
21849
21850		// Do not notify entities if material was removed from them 
21851		// (usually just before deletion of the entity)
21852		if (typeof material !== 'undefined') {
21853			target.fireEvent('onMaterial');
21854		}
21855
21856		// do not change order in conditional statement!
21857		if (oldMaterial && material && oldMaterial.ID !== material.ID && (--oldMaterial._refcounter) === 0) {
21858			myDeco.body.trigger('materialRemoved', this.materials.splice(this.materials.indexOf(oldMaterial), 1));
21859		}
21860		
21861		return material;
21862	},
21863	
21864	/**
21865	 * Check for entity is locked
21866	 * @param {Object} entity
21867	 * @returns {Boolean}
21868	 */
21869	isLocked: function(entity) {
21870		return this._locked && (entity.type === 'corner' ||
21871			entity.type === 'wall');
21872	},
21873	
21874	/**
21875	 * Toggle floorplan lock
21876	 * @param {Boolean} state
21877	 */
21878	toggleLock: function(state) {
21879		var selectedWalls;
21880		this._locked = typeof state !== 'undefined' ? state : !this._locked;
21881		
21882		// unselect selected walls
21883		if (this._locked) {
21884			selectedWalls = $.grep(this._selected, function(item) {
21885				return item.type === 'wall';
21886			});
21887			
21888			if (selectedWalls.length > 0) {
21889				this.unselect(selectedWalls);
21890			}
21891		}
21892	},
21893	
21894	/**
21895	 * Return list of unique material IDs in model
21896	 */
21897	uniqueMaterialIds: function() {
21898		return $.map(this.materials, function(material) {
21899			return material.component_id || null;
21900		}).unique();
21901	},
21902	
21903	/**
21904	 * Identify is given material or its id is in model or not
21905	 * @param {Object|String} id Material or its ID
21906	 */
21907	isMaterialInModel: function(id) {
21908		id = typeof id === 'object' ? id.component_id : id;
21909		return $.any(this.materials, function(material) {
21910			return id === material.component_id;
21911		});
21912	},
21913	
21914	/**
21915	 * Return list of unique component IDs in model
21916	 */
21917	uniqueComponentIds: function() {
21918		return $.map(this.components, function(component) {
21919			return component.component_id || null;
21920		}).unique();
21921	},
21922	
21923	/**
21924	 * Identify is selected component or its id is in model or not
21925	 * @param {Object|String} id Component or its ID
21926	 */
21927	isComponentInModel: function(id) {
21928		id = typeof id === 'object' ? id.component_id : id;
21929		return $.any(this.components, function(component) {
21930			return id === component.component_id;
21931		});
21932	},
21933	
21934	/**
21935	 * Add camera to room
21936	 * @param {Object}
21937	 * @returns {Camera}
21938	 */
21939	addCamera: function(camera) {
21940		this.camera = {};
21941		$.extend(true, this.camera, camera, cameraMethods).init(this);
21942		myDeco.body.trigger('cameraAdded', [this.camera]);
21943		return this.camera;
21944	},
21945	
21946	/**
21947	 * Add component to the room
21948	 * @param {Object} component Component to add
21949	 * @param {Number} x
21950	 * @param {Number} y
21951	 * @param {Number} z
21952	 * @param {Boolean} noselect Prevent of component selection
21953	 * @param {Boolean} preserveID
21954	 * @returns {Camera} Added component
21955	 */
21956	addComponent: function(component, x, y, z, noselect, preserveID) {
21957		// assign component position if specified
21958		component.position[0] = typeof x === 'undefined' ? (+component.position[0] || 0) : +x;
21959		component.position[1] = typeof y === 'undefined' ? (+component.position[1] || 0) : +y;
21960		component.position[2] = typeof z === 'undefined' ? (+component.position[2] || 0) : +z;
21961		
21962		component.position = new Point(component.position);
21963		
21964		this.processComponent(component, preserveID);
21965		
21966		var pID;
21967		
21968		// Searching for parent wall for the window/door
21969		if (!component.parent && component.parentID) {
21970			pID = 'wall' + component.parentID.replace('wall', '');
21971			
21972			component.parent = $.grep(this.walls, function(wall) {
21973				return wall.ID === pID;
21974			}).shift();
21975			
21976			delete component.parentID;
21977		}	
21978		// window or door
21979		$.extend(true, component, componentMethods).init(this);
21980		myDeco.body.trigger('componentAdded', [component]);
21981		
21982		if (this.loaded && !noselect) {
21983			this.select(component);
21984		}
21985
21986		return component;
21987	},
21988	
21989	/**
21990	 * Add room
21991	 * @param {String} type Type of room shape
21992	 * @param {Float} x X position of the center of the room
21993	 * @param {Float} y Y position of the center of the room
21994	 * @param {Float} width width of the room
21995	 * @param {Float} depth depth of the room
21996	 * @returns {Shape} added shape
21997	 */
21998	addRoom: function(type, x, y, width, depth) {
21999		var shape = [],
22000			i, ilen, j,
22001			dx = 0, dy = 0,
22002			// jlen, distance = Number.MAX_VALUE, d,
22003			model = this.settings.shapes[type];
22004		
22005		if (!model) {
22006			return;
22007		}
22008			
22009		width = width || model.width;
22010		depth = depth || model.depth;
22011		
22012		for (i = 0, ilen = model.shape.length; i < ilen; i++) {
22013			shape.push(new Point(x + (model.shape[i][0] - 0.5) * width, 
22014				y + (model.shape[i][1] - 0.5) * depth));
22015		}
22016		
22017		/* Snap to the nearest point
22018		for (i = 0, ilen = this.corners.length; i < ilen; i++) {
22019			for (j = 0, jlen = shape.length; j < jlen; j++) {
22020				d = this.corners[i].distance(shape[j]);
22021				if (d < distance && d < this.settings.searchDistance) {
22022					distance = d;
22023					dx = this.corners[i].x - shape[j].x;
22024					dy = this.corners[i].y - shape[j].y;
22025				}
22026			}
22027		}
22028		*/
22029		
22030		for (i = 0, ilen = shape.length; i < ilen; i++) {
22031			shape[i] = this.addCorner(shape[i].translate(dx, dy));
22032		}
22033		
22034		shape = this.addShape(shape);
22035		
22036		for (i = 0, j = shape.length - 1, ilen = shape.length; i < ilen; j = i++) {
22037			shape.attachWall(this.addWall([shape[i], shape[j]], [shape]));
22038		}
22039		
22040		this.select(shape);
22041		
22042		return shape;
22043	},
22044	
22045	/**
22046	 * Add corner to the model
22047	 * @param {Point} corner. Corner to add.
22048	 * @returns {Corner}
22049	 */
22050	addCorner: function(corner) {
22051		corner = $.extend(true, corner, cornerMethods).init(this);
22052		myDeco.body.trigger('cornerAdded', [corner]);
22053		return corner;
22054	},
22055
22056	/**
22057	 * Add wall to the model
22058	 * @param {Array of Point} wall. Wall to add.
22059	 * @param {Array of Shape} shape. Parent shapes of the wall
22060	 * @returns {Wall} Added wall.
22061	 */
22062	addWall: function(wall, shapes) {
22063		wall = $.extend(true, wall, wallMethods).init(this, shapes);
22064		myDeco.body.trigger('wallAdded', [wall]);
22065		return wall;
22066	},
22067	
22068	/**
22069	 * Add shape to the model
22070	 * @param {Array of Point} shape. Shape to add.
22071	 * @param {Array of Wall} walls List of walls connected to shape
22072	 * @param {Object} floor Floor material
22073	 * @oaram {Object} ceiling Ceiling material 
22074	 * @returns {Shape} Added shape.
22075	 */
22076	addShape: function(shape, walls, floor, ceiling) {
22077		if (floor) {
22078			shape.floorMaterial = floor;
22079		}
22080		
22081		if (ceiling) {
22082			shape.ceilingMaterial = ceiling;
22083		}
22084		shape = $.extend(true, shape, shapeMethods).init(this, walls);
22085		myDeco.body.trigger('shapeAdded', [shape]);
22086		return shape;
22087	},
22088
22089	/**
22090	 * Replace existing shape with new one (when combining rooms)
22091	 * — find and add newly created corners
22092	 * — 
22093	 * @param {Shape} oldShape. Old shape. It's ceiling/flooring materials will be used for new one.
22094	 * @param {Array of Point} newShape. Shape to add.
22095	 * @returns {Shape} Added shape.
22096	 */
22097	replaceShape: function(oldShape, newShape, restrictWalls) {
22098		var i1, i2, len1 = newShape.length, walls = [], wall, rShape = [],
22099			newCorners = [], corner;
22100		
22101		// Remove identical points from shape as well as short wall segments
22102		// and prepare new shape
22103		for (i1 = 0, i2 = len1 - 1; i1 < len1; i2 = i1++) {
22104			// remove small walls
22105			if (newShape[i1].distance(newShape[i2]) <
22106					this.settings.searchDistance) {
22107				wall = this._findWall(newShape[i1], newShape[i2]);
22108				if (wall) {
22109					wall.remove();
22110				}
22111			} else {
22112				// create corner if it doesn't exist
22113				if (!newShape[i1].type) {
22114					corner = this.findNearestCorner(newShape[i1],
22115						this.settings.searchDistance);
22116					newShape[i1] = corner || this.addCorner(newShape[i1]);
22117					newCorners.push(newShape[i1]);
22118				}
22119				rShape.push(newShape[i1]);
22120			}
22121		}
22122		
22123		this.addShape(rShape, [], oldShape.floorMaterial,
22124				oldShape.ceilingMaterial);
22125		
22126		len1 = rShape.length;
22127		walls = oldShape.getWalls();
22128		
22129		for (i1 = 0, i2 = len1 - 1; i1 < len1; i2 = i1++) {
22130			// Reuse wall if possible and was available on the old shape
22131			wall = this._findWall(rShape[i1], rShape[i2]);
22132			if (wall) {
22133				wall.attachShape(rShape);
22134			} else if(restrictWalls || (newCorners.indexOf(rShape[i1]) !== -1 || newCorners.indexOf(rShape[i2]) !== -1)) {
22135				this.addWall([rShape[i1], rShape[i2]], [rShape]);
22136			}
22137		}
22138		
22139		return rShape;
22140	},
22141	
22142	/**
22143	 * Save current model to backend
22144	 * 
22145	 * @param {Object} options Save options, expected keys/values are:
22146	 *    {Boolean} forceNew Create new room or replace old one
22147	 *    {Object} settings Top-level settings of model
22148	 *    {Object} data Already wrapped room data
22149	 *    {Function} successCallback Callback which will be executed after success of ajax request
22150	 *    {String} urlParams Params which will be added to the save url
22151	 *    {Boolean} hqRender Should backend render HQ render or not
22152	 */
22153	saveRoom: function(options) {
22154		//forceNew, settings, data, callback, urlParams
22155		var saveRoutine = this._super,
22156			self = this,
22157			width = 960,
22158			height = 540; 
22159		
22160		options = options || {};
22161		
22162		options.data = options.data || this._wrapRoom(options.settings);
22163		
22164		if (!options.urlParams) {
22165			options.urlParams = 'snapshot=1';
22166		}
22167
22168        if (this._metadata.force_thickness) {
22169            options.urlParams += '&with_thickness=1';
22170        }
22171		
22172		if (this._masking) {
22173			options.urlParams += '&' + $.param({
22174				send_alert: true,
22175				event_type: 'floor_plan_ready'
22176			});
22177			options.restoreOwner = true;
22178			options.forceNew = true;
22179            options.originalURL = this._ws_url;
22180		}
22181		
22182		options.hqRender = options.forceNew || options.hqRender;
22183		
22184		if (options.hqRender) {
22185			options.urlParams += '&hq_render=' + this.camera.hq_renderer;
22186		}
22187
22188		if (myDeco.settings.WL_PROFILE) {
22189			options.urlParams += '&profile=' + myDeco.settings.WL_PROFILE;
22190		}
22191		
22192		// retrieving rendered image
22193		this.render(function(result) {
22194			if (result) {
22195				options.data.image = result.data;
22196				options.urlParams += '&file=image';
22197			}
22198			saveRoutine.call(self, options);
22199		}, options.data.model, width, height, 'normal', '?', true);
22200	},
22201
22202	/**
22203	 * Delete active room
22204	 * @param {Object} options
22205	 */
22206	deleteRoom: function(options) {
22207		options = options || {};
22208		if (this._masking && options.reason) {
22209			options.urlParams = (options.urlParams ? options.urlParams + '&' : '') + 
22210				$.param({
22211					send_alert: true,
22212					event_type: 'floor_plan_rejected',
22213					reason: options.reason
22214				});
22215		}
22216		this._super.call(this, options);
22217	},
22218	
22219	/**
22220	 * Load room and convert low level (raw) model to high level
22221	 * @param {Room} rawModel Raw model of the room which comes from room WS
22222	 */
22223	load: function(rawModel) {
22224		this._masking = !rawModel.model;
22225		if (!rawModel.model) {
22226			rawModel.model = $.extend(true, {}, emptyRoom.model);
22227		}
22228		this.reset();
22229		this._backup = $.extend(true, {}, rawModel);
22230		this._super(rawModel);
22231		this.loadModel(rawModel.model);
22232        this._parent_created = rawModel.parent_created ? new Date(rawModel.parent_created) : new Date();
22233	},
22234	
22235	/**
22236	 * Strip snapshot ID from url
22237	 * @param {String} url
22238	 * @returns {String}
22239	 */
22240	_fixUrl: function(url) {
22241		return this._super(url).replace(/(\/\d+)\/\d+(\/)?$/, '$1$2');
22242	},
22243	
22244	/**
22245	 * Load raw model
22246	 * @param {Room} rawModel Raw model of the room which comes from room WS
22247	 * @param {Boolean} sameRoom Will model send event "roomLoaded" with
22248	 *                           sameRoom flag rised or not
22249	 */
22250	loadModel: function(rawModel, sameRoom) {
22251		if (!sameRoom) {
22252			this.loaded = false;
22253		}
22254		
22255		var self = this, x, image, w, m, width, height, name, i, id, component,
22256			materialRe = /(material|decor-)(\d+)$/,
22257			idToRequest = [], materials = [],
22258			rawComponents = rawModel.components || [],
22259			replaceComponents = this.settings.replacements || {},
22260			stage = 0,
22261			camera = {},
22262			onLoad = function() {
22263				m.photo.orig.width = image.width;
22264				m.photo.orig.height = image.height;
22265			},
22266			loadingFinished = function() {
22267				if (++stage === 2) {
22268					if (self.loaded === false) {
22269						self.loaded = true;
22270					}
22271					self._currModel = JSON.stringify(self.getModel(2, true));
22272					myDeco.body.trigger('roomLoaded', [!!sameRoom]);
22273				}
22274			};
22275
22276		materials = rawModel.version === 2 ?
22277			this._loadPlanV2(rawModel) :
22278			this._loadPlanV1(rawModel);
22279
22280        // use numerical ID for the default floor material
22281        // so that its settings are fetched from the index
22282		var defaultFloorMaterial = materials.materialFloorDefault;
22283
22284        if (defaultFloorMaterial) {
22285            var numericalFloorMaterialID = 'decor-' + (defaultFloorMaterial.diffuse_texture.match('Natural_Travertine_TILE.jpg') ? 3708 : 2542);
22286            materials.materialFloorDefault.ID = numericalFloorMaterialID;
22287            materials[numericalFloorMaterialID] = materials.materialFloorDefault;
22288            delete materials.materialFloorDefault;
22289        }
22290
22291		// update material positions
22292		// wall materials are already in model
22293		for (x = this.walls.length - 1; x >=0; x--) {
22294			if (this.walls[x].material.decal_texture && typeof this.walls[x].material.photo === 'undefined') {
22295				image = new Image();
22296				w = this.walls[x];
22297				m = w.material;
22298				width = 100 / m.decal_transform[0][0];
22299				height = 100 / m.decal_transform[1][1];
22300				m.photo = {
22301					orig: {
22302						width: width,
22303						height: height
22304					},
22305					x: -m.decal_transform[0][2] * width,
22306					y: -m.decal_transform[1][2] * height,
22307					width: width,
22308					height: height,
22309					repeatX: m.decal_repeat_u,
22310					repeatY: m.decal_repeat_v
22311				};
22312				image.onload = onLoad;
22313				image.src = myDeco.settings.MEDIA_URL + m.decal_texture;
22314			}
22315		}
22316
22317		camera.position = new Point(rawModel.camera.position);
22318		camera.focal_point = new Point(rawModel.camera.focal_point);
22319		camera.hq_renderer = rawModel.camera.hq_renderer || myDeco.settings.DEFAULT_HQ_RENDERER;
22320		camera.exposure = rawModel.camera.exposure || 0;
22321        camera.fieldofview = rawModel.camera.fieldofview || myDeco.App.settings.model.cameraFov;
22322		this.addCamera(camera);
22323
22324        // light environment settings
22325        this.light_env = rawModel.light_env;
22326
22327        if (!this.light_env) {
22328            this.light_env = {setting: 'day', intensity: 30, azimuth: 45, altitude: 60};
22329        }
22330
22331        // azimuth/altitude were introduced later, so there are rooms
22332        // with light env but without these two attributes
22333        if (!this.light_env.azimuth) {
22334            $.extend(this.light_env, {azimuth: 45, altitude: 60});
22335        }
22336		
22337		// assign environment
22338		if (typeof rawModel.environment === 'object') {
22339			for (name in rawModel.environment) {
22340				this.assignEnvironment(name, rawModel.environment[name]);
22341			}
22342		}
22343		
22344		// unique list of components
22345		idToRequest = $.map(rawComponents, function(component) {
22346			id = component.component_id || null;
22347			if (id && replaceComponents[id]) {
22348				id = replaceComponents[id];
22349			}
22350			return id;
22351		}).unique();
22352
22353		// Processing components
22354		myDeco.App.ws.proxy.fetch('component', idToRequest, function(result) {
22355			for (i = rawComponents.length - 1; i !== -1; i--) {
22356				component = rawComponents[i];
22357				id = component ? (replaceComponents[component.component_id] || component.component_id) : false;
22358				if (id && result[id]) {
22359					// result[...] is stored in cache, so extend its copy
22360					rawComponents[i] = $.extend(true,
22361						{},
22362						result[id] || {},
22363						component,
22364						{
22365							component_id: result[id].component_id,
22366							model: result[id].model,
22367							fromRoom: true
22368						});
22369					rawComponents[i].bounding_box = result[id].bounding_box.slice(0);
22370				} else {
22371					// component service notified about not existing component
22372					// TODO: show message
22373					rawComponents.splice(i, 1);
22374				}
22375			}
22376			
22377			if (rawComponents.length > 0) {
22378				rawComponents.sort(function(a, b) {
22379					var ret = 0;
22380					if (a.level < 0 || b.level < 0) {
22381						ret = b.level - a.level
22382					} else {
22383						ret = (a.level || 0) - (b.level || 0);
22384					}
22385					return ret;
22386				});
22387				
22388				$.each(rawComponents, function() {
22389					if (this.configuration && this.configuration.finish) {
22390						this.material = materials[this.configuration.finish];
22391					}
22392
22393                    var was_position = this.position[2];
22394
22395					var comp = self.addComponent(this, undefined, undefined,
22396						undefined, true, sameRoom);
22397
22398                    // restore z position for elevated doors
22399                    if (comp.type == 'door') {
22400                        comp.position.z = was_position;
22401                    }
22402				});
22403			}
22404			
22405			loadingFinished();
22406		});
22407
22408		// fetch materials
22409		idToRequest = [];
22410
22411		$.each(materials, function() {
22412			if (this.ID && this.ID.indexOf('Default') === -1) {
22413				var match = this.ID.match(materialRe);
22414				idToRequest.push(match ? match[2] : '#' + diffuse2Hex(this.diffuse));
22415			}
22416		});
22417
22418		idToRequest = idToRequest.unique();
22419		myDeco.App.ws.proxy.fetch('material', idToRequest, function(result) {
22420			$.each(materials, function() {
22421				if (this.ID) {
22422					var match = this.ID.match(materialRe), backup = $.extend(true, {}, this);
22423                    if (match && result[match[2]]) { // for materials with an ID we override all with values from the index
22424					    $.extend(true, this, result[match[2]], {ID: backup.ID})
22425                    } else if (this.diffuse) { // for other materials (such as colours) we mix in palette settings, but old values still have preference
22426                        $.extend(true, this, result['#' + diffuse2Hex(this.diffuse)], backup)
22427                    }
22428				}
22429			});
22430			loadingFinished();
22431		});
22432	},
22433
22434	/**
22435	 * Loading algo or how to obtain high level data from low-level
22436	 * @param {Room} rawModel Raw model of the room which comes from room WS
22437	 */
22438	_loadPlanV1: function(rawModel) {
22439		var self = this,
22440			height = 0,
22441			// normalize points
22442			points = $.map(rawModel.geometry.points, function(point) {
22443				if (point[2] < 0) {
22444					point[2] = 0;
22445				}
22446				height = Math.max(height, point[2]);
22447				return [point];
22448			}),
22449			shape = [],
22450			floorMaterial, ceilingMaterial,
22451			i, j, ilen, jlen, w, wlen, wall,
22452			materials = {},
22453			
22454			// get actual walls
22455			walls = $.map(rawModel.geometry.polygons, function(polygon) {
22456				// find indices based on floor (z === 0)
22457				var p = $.map(polygon.indices, function(index) {
22458					return points[index][2] === 0 ?
22459						new Point(points[index]) :
22460						null;
22461				}), d, distance = 0;
22462				
22463				// all indices on the floor - floor polygon
22464				if (p.length === polygon.indices.length) {
22465					shape = p;
22466					
22467					// remove last point from shape
22468					if (shape[0].distance(shape[shape.length - 1]) < self._tolerancy) {
22469						shape.pop();
22470					}
22471					
22472					floorMaterial = polygon.material;
22473					return null;
22474				// some indices on the floor - wall
22475				} else if (p.length !== 0) {
22476					// find points with max distance between them
22477					for (i = 0, ilen = p.length - 1; i < ilen; i++) {
22478						for (j = i, jlen = ilen + 1; j < jlen; j++) {
22479							d = p[i].distance(p[j]);
22480							if (d > distance) {
22481								distance = d;
22482								// double array because map is opening brackets
22483								wall = [p[i], p[j]];
22484							}
22485						}
22486					}
22487					wall.ID = polygon.ID;
22488					wall.material = polygon.material;
22489					
22490					// as array to satisfy $.map
22491					return [wall];
22492				// no one index on the floor - ceiling
22493				} else {
22494					// ceiling
22495					ceilingMaterial = polygon.material;
22496					return null;
22497				}
22498			});
22499		
22500		this.ceilingHeight = height;
22501		
22502		// init materials cache
22503		$.each(rawModel.materials, function() {
22504			materials[this.ID] = this;
22505		});
22506		
22507		for (i = 0, ilen = shape.length; i < ilen; i++) {
22508			this.addCorner(shape[i]);
22509		}
22510		
22511		// add shape
22512		shape = this.addShape(shape, [], materials[floorMaterial],
22513				materials[ceilingMaterial]);
22514		
22515		// sort walls according to shape
22516		ilen = shape.length;
22517		
22518		// walk thru shape, find corresponding walls and add them
22519		for (i = 0, j = ilen - 1; i < ilen; j = i++) {
22520			for (w = 0, wlen = walls.length; w < wlen; w++) {
22521				if ((shape[i].distance(walls[w][0]) < this._tolerancy &&
22522						shape[j].distance(walls[w][1]) < this._tolerancy) ||
22523						(shape[i].distance(walls[w][1]) < this._tolerancy &&
22524						shape[j].distance(walls[w][0]) < this._tolerancy)) {
22525					wall = [shape[i], shape[j]];
22526					wall.ID = walls[w].ID;
22527					wall.material = materials[walls[w].material];
22528					this.addWall(wall, [shape]);
22529					walls.splice(w, 1);
22530					break;
22531				}
22532			}
22533		}
22534
22535		return materials;
22536	},
22537
22538	/**
22539	 * Load plan part of the room
22540	 * @param {Room} rawModel Raw model of the room which comes from room WS (must have version=2)
22541	 */ 
22542	_loadPlanV2: function(rawModel) {
22543		var self = this, wall, shape,
22544			i, j, key, plan = rawModel.plan, len,
22545			materials = {},
22546			indexesToCorners = function(index) {
22547				return self.corners[index];
22548			};
22549			
22550		// init materials cache
22551        if (rawModel.materials) {
22552		    $.each(rawModel.materials, function() {
22553			    materials[this.ID] = this;
22554		    });
22555        }
22556
22557		this.ceilingHeight = plan.height;
22558
22559        if (plan.points) {
22560		    len = plan.points.length;
22561		    // Load/convert corners
22562		    for (i = 0; i < len; i++) {
22563			    this.addCorner(new Point(plan.points[i]));
22564		    }
22565        }
22566		
22567		// Load walls
22568		for (key in plan.walls) {
22569			wall = [
22570				this.corners[plan.walls[key].indices[0]],
22571				this.corners[plan.walls[key].indices[1]]
22572			];
22573			wall.ID = key;
22574			wall.thickness = plan.walls[key].thickness;
22575			wall.material = materials[plan.walls[key].material];
22576			this.addWall(wall);
22577		}
22578
22579		// Load areas
22580		for (key in plan.areas) {
22581            var indices = plan.areas[key].indices;
22582            if (!indices) continue;
22583
22584			shape = $.map(indices, indexesToCorners);
22585
22586			// shape
22587			shape = this.addShape(shape, [],
22588				materials[plan.areas[key].floor_material ||
22589					plan.areas[key]['floor-material']],
22590				materials[plan.areas[key].ceiling_material ||
22591					plan.areas[key]['ceiling-material']]);
22592			
22593			len = shape.length;
22594			
22595			// restore walls
22596			for (i = 0, j = len - 1; i < len; j = i++) {
22597				wall = this._findWall(shape[i], shape[j]);
22598				if (wall) {
22599					shape.attachWall(wall);
22600				}
22601			}
22602		}
22603
22604		return materials;
22605	},
22606	
22607	/**
22608	 * remove selected items from model
22609	 */
22610	remove: function(param) {
22611		var j;
22612		
22613		for (j = this._selected.length - 1; j !== -1; j--) {
22614			this._selected[j].remove(param);
22615		}
22616		
22617		this.operationEnd(['remove']);
22618	},
22619
22620	/**
22621	 * Get high-level (new) representation of geometry for room format ver. 2.0
22622	 */
22623	_getHighGeometry: function() {
22624		var plan = {
22625			height: this.ceilingHeight,
22626			points: [],
22627			areas: {},
22628			walls: {}
22629		}, 
22630		i, len, indices, 
22631		j, entity, elen,
22632		g, geometry = ['walls', 'shapes'], glen = geometry.length, gname;
22633		
22634		len = this.corners.length;
22635		
22636		for (i = 0; i < len; i++) {
22637			plan.points.push(this.corners[i].toArray(true));
22638		}
22639
22640		for (g = 0; g < glen; g++) {
22641			gname = geometry[g];
22642			
22643			len = this[gname].length;
22644			for (i = 0; i < len; i++) {
22645				entity = this[gname][i];
22646				elen = entity.length;
22647				indices = [];
22648				
22649				for (j = 0; j < elen; j++) {
22650					indices.push($.inArray(entity[j], this.corners));
22651				}
22652
22653				if (gname !== 'walls') {
22654					plan.areas[entity.ID] = {indices: indices, 
22655						'floor_material': entity.floorMaterial ? entity.floorMaterial.ID : null,
22656						'ceiling_material': (entity.ceilingMaterial ? entity.ceilingMaterial.ID : null)};
22657				} else {
22658					plan[gname][entity.ID] = {
22659						indices: indices,
22660						thickness: entity.thickness,
22661						material: entity.material.ID
22662					};
22663				}
22664			}
22665		}
22666		
22667		return plan;
22668	},
22669
22670	/**
22671	 * Get a fresh and clean copy of the component (without cached/precalculated values, methods, etc)
22672	 * @param {Boolean} dontRemoveMeta Do not remove meta and precalculated information from the components (needed for undo/redo and duplicate).
22673	 * @return {Component} Clean copy of the component
22674	 */
22675	_cleanComponent: function(component, dontRemoveMeta) {
22676		// Save the parentID before clone component object 
22677		// (cause clone operation is only 1 level deep).
22678		var parentID = component.parent ? component.parent.ID.replace('wall', '') : null,
22679			newComponent = {
22680				parentID: parentID,
22681				ID: component.ID,
22682				model: component.model,
22683				// Convert Points back to array of coords
22684				position: component.position.toArray(),
22685				bounding_box: [component.bounding_box[0].toArray(), component.bounding_box[1].toArray()],
22686				rotation: component.rotation,
22687				component_id: component.component_id,
22688				category: component.category,
22689				level: component.level,
22690				surface_height: component.surface_height
22691			};
22692		
22693		if (component._shape) {
22694			newComponent.area = component._shape.ID;
22695		}
22696
22697		if (component.aperture) {
22698			newComponent.aperture = {
22699				type: component.aperture.type,
22700				bounds: [component.width, component.height]
22701			};
22702		}
22703
22704		if (component.configurations) {
22705			newComponent.configuration = {};
22706			$.each(component.configurations, function(key) {
22707				newComponent.configuration[key] = component[key];
22708			});
22709			if (component.material) {
22710				newComponent.configuration.finish = component.material.ID;
22711			}
22712		}
22713
22714		// Preserve some highlevel data to satisfy undo/redo
22715		if (dontRemoveMeta) {
22716			$.extend(newComponent, {
22717				componentData: component.componentData,
22718				width: component.width,
22719				height: component.height,
22720				depth: component.depth,
22721				product_id: component.product_id,
22722				name: component.name,
22723				price: component.price,
22724				supplier: component.supplier,
22725				supplier_url: component.supplier_url,
22726				description: component.description,
22727				available: component.available,
22728				url: component.url,
22729				locale: component.locale
22730			});
22731		}
22732		
22733		return newComponent;
22734	},
22735		// Stub method for get the room ready to save.
22736	/**
22737	 * Wrap room model with some fpe-specific settings.
22738	 * @param {Object} template Default room settings
22739	 * @return {Object} Room ready for saving
22740	 */
22741	 
22742	_wrapRoom: function(template) {
22743		var data = this._super(template);
22744		
22745		// remove original parent
22746		delete this._metadata.parent;
22747		this._metadata.type = '3d';
22748		
22749		data = $.extend(true, {}, this._metadata, {
22750			"model": this.getModel(),
22751			"products": this.uniqueProducts()
22752		});
22753		
22754		if (this._masking) {
22755			data.author = data.original_author;
22756			delete data.thumbnail;
22757			delete data.image;
22758			delete data.last_updated;
22759			delete data.id;
22760		}
22761
22762        // remove wall thickness toggle
22763        delete data.force_thickness;
22764
22765		return data;
22766	},
22767
22768	/**
22769	 * Convert the room back from hi-level model to low-level, required by render, old ME and rooms WS
22770	 * @param {Integer} version Version of the room model (1 or 2).
22771	 * @param {Boolean} dontRemoveMeta Do not remove meta and precalculated information from the components (needed for undo/redo).
22772	 * @return {Object} Low level room model
22773	 */
22774	getModel: function(version, dontRemoveMeta, testMaterials) {
22775		if (!this.loaded) {
22776			return false;
22777		}
22778
22779		var model = {
22780				camera: {
22781					position: this.camera.position.toArray(),
22782					focal_point: this.camera.focal_point.toArray(),
22783					hq_renderer: this.camera.hq_renderer,
22784					exposure: this.camera.exposure,
22785                    fieldofview: this.camera.fieldofview
22786				},
22787				// Default values for lightning
22788				lights: [{
22789					position: [10, 0, 500],
22790					direction: [0, 0, -500],
22791					type: "sun"
22792				}],
22793				materials: [],
22794				room_id: this._room_id || 0,
22795				components: []
22796			},
22797			i, n, c, clen, component, material,
22798			src = ['components', 'doors', 'windows'],
22799			materialIds = [],
22800			errMaterials = [];
22801
22802		model.plan = this._getHighGeometry();
22803		model.version = version || 2;
22804
22805		// Saving components
22806		for (c = 0, clen = src.length; c < clen; c++) {
22807			for (i = 0, n = this[src[c]].length; i < n; i++) {
22808				// Make a copy of the current component
22809				component = this._cleanComponent(this[src[c]][i], dontRemoveMeta);
22810
22811				// Restore component type
22812				component.type = (src[c] === 'components' ? 'Component': 'ArchitecturalComponent');
22813				model.components.push(component);
22814			}
22815		}
22816
22817		if (dontRemoveMeta) {
22818			model.materials = this.materials;
22819		} else {
22820			for (i = 0, n = this.materials.length; i < n; i++) {
22821				material = {
22822					ID: this.materials[i].ID,
22823					diffuse_texture: this.materials[i].diffuse_texture,
22824					shader: this.materials[i].shader,
22825					texture_width: this.materials[i].texture_width,
22826					texture_height: this.materials[i].texture_height,
22827					ambient: this.materials[i].ambient,
22828					shiny: this.materials[i].shiny,
22829					diffuse: this.materials[i].diffuse
22830				};
22831				if (typeof this.materials[i].photo !== 'undefined') {
22832					$.extend(true, material, {
22833						decal_texture: this.materials[i].decal_texture,
22834						decal_repeat_u: this.materials[i].decal_repeat_u,
22835						decal_repeat_v: this.materials[i].decal_repeat_v,
22836						decal_transform: this.materials[i].decal_transform
22837					});
22838				}
22839					
22840				model.materials.push(material);
22841			}
22842		}
22843		
22844		//test if all the materials used are in model.materials
22845		if (testMaterials) {
22846			materialIds = $.map(model.materials, function (m) {
22847				return m.ID;
22848			});
22849			
22850			for (i in model.plan.walls) { 
22851				if ($.inArray(model.plan.walls[i].material, materialIds) === -1) {
22852					errMaterials.push(model.plan.walls[i].material);
22853					this.assignMaterial(model.plan.walls[i], this.settings.defaultMaterials['wall'], 'wall');
22854				}
22855			}
22856
22857			for (i in model.plan.areas) {
22858				for (c in {'ceiling_material': 1, 'floor_material': 1}) {
22859					if ($.inArray(model.plan.areas[i][c], materialIds) === -1) {
22860						errMaterials.push(model.plan.areas[i][c]);
22861						if (c === 'ceiling_material') {
22862							this.assignMaterial(model.plan.areas[i][c], this.settings.defaultMaterials['ceiling'] , 'ceiling');
22863						} else {
22864							this.assignMaterial(model.plan.areas[i][c], this.settings.defaultMaterials['floor'] , 'floor');
22865						}
22866					}
22867				}
22868			}
22869			
22870			if (errMaterials.length !== 0) {
22871				//trigger an error
22872				myDeco.body.trigger('materialsError', errMaterials.join(','));
22873			}
22874		}
22875		
22876		if ($.keys(this.environment).length !== 0) {
22877			model.environment = this.environment;
22878		}
22879		
22880		return model;
22881	},
22882	
22883	/**
22884	 * Get the list of unique products, used in room
22885	 * @return {Array of Strings} List of unique guids
22886	 */
22887	uniqueProducts: function() {
22888		var result = [],
22889			i, len = this.components.length;
22890		
22891		for (i = 0; i < len; i++) {
22892			if ($.inArray(this.components[i].product_id, result) === -1) {
22893				result.push(this.components[i].product_id);
22894			}
22895		}
22896
22897		return result;
22898	},
22899	
22900	/**
22901	 * Get bounding box of the room (including outside components and possible door opening)
22902	 * @param {Transform} transform Transform to apply
22903	 * @param {String} transformName Transform name
22904	 * @return {Object} BB object
22905	 */
22906	getBBox: function(transform, transformName) {
22907		var x1 = Number.MAX_VALUE, y1 = Number.MAX_VALUE, z1 = Number.MAX_VALUE,
22908			x2 = -Number.MAX_VALUE, y2 = -Number.MAX_VALUE, z2 = -Number.MAX_VALUE,
22909			src = ['corners', 'components', 'doors'], p, plen, i, ilen, bb;
22910
22911		// calculate components (can be outside of room) and doors (can has open_direction = o)
22912		
22913		for (p = 0, plen = src.length; p < plen; p++) {
22914			for (i = 0, ilen = this[src[p]].length; i < ilen; i++) {
22915				bb = this[src[p]][i].getBBox(transform, transformName);
22916
22917				x1 = Math.min(x1, bb.x1, bb.x2);
22918				x2 = Math.max(x2, bb.x1, bb.x2);
22919				y1 = Math.min(y1, bb.y1, bb.y2);
22920				y2 = Math.max(y2, bb.y1, bb.y2);
22921				z1 = Math.min(z1, bb.z1, bb.z2);
22922				z2 = Math.max(z2, bb.z1, bb.z2);
22923			}
22924		}
22925		
22926		
22927		// calculate camera
22928		p = typeof transform !== 'undefined' ?
22929			this.camera.position.applyTransformation(transform, transformName) :
22930			this.camera.position;
22931		
22932		x1 = Math.min(x1, p.x);
22933		y1 = Math.min(y1, p.y);
22934		z1 = Math.min(z1, p.z);
22935		x2 = Math.max(x2, p.x);
22936		y2 = Math.max(y2, p.y);
22937		z2 = Math.max(z2, p.z);
22938		
22939		return {
22940			p1: new Point([x1, y1, z1]),
22941			p2: new Point([x2, y2, z2]),
22942			width: x2 - x1,
22943			depth: y2 - y1,
22944			height: z2 - z1,
22945			cx: x1 + (x2 - x1) / 2,
22946			cy: y1 + (y2 - y1) / 2,
22947			cz: z1 + (z2 - z1) / 2
22948		};
22949	},
22950
22951	/**
22952	 * Render current model and run callback in a Model scope
22953	 * 
22954	 * @param {Function} callback Function is running after success render
22955	 * @param {Object} model Model to render, defaults to current
22956	 * @param {Integer} width Image width to render
22957	 * @param {Integer} height Image height to render
22958	 * @param {String} type Render type, 'preview' or 'normal',
22959	 * @param {String} renderCallback Callback name passed to render
22960	 * @param {Number} errorCounter Errors counter, used to break infinite loop
22961	 * 
22962	 * @return {Object} jQ image or response object if renderCallback is specified
22963	 */
22964	render: function(callback, model, width, height, type, renderCallback, errorCounter) {
22965		errorCounter = errorCounter || 0;
22966		type = type || 'preview';
22967		model = model || this.getModel();
22968		
22969		// don't want to serve empty model
22970		if (!model) {
22971			return;
22972		}
22973		// prevent problems with the render when float values are passed
22974		width = Math.ceil(width || 0);
22975		height = Math.ceil(height || 0);
22976		
22977		
22978		var self = this,
22979			renderC = renderCallback ? '&callback=' + renderCallback : '',
22980			// render success callback
22981			renderResponse = function(response) {
22982				if ($.isFunction(callback)) {
22983					callback.call(self, response);
22984				}
22985			},
22986			// callback for support render errors
22987			renderError = function() {
22988				// just re-request session if it already expired
22989				if (self._renderUrl !== myDeco.urls.renderws && errorCounter === 0) {
22990					myDeco.body.trigger('renderState', ['session']);
22991					self._renderUrl = myDeco.urls.renderws;
22992					self.render(callback, model, width, height, type, renderCallback);
22993				} else if (errorCounter > 0 || self._renderUrl === myDeco.urls.renderws) {
22994					renderResponse(false);
22995				} else {
22996					self.render(callback, model, width, height, type, renderCallback, ++errorCounter);
22997				}
22998			};
22999
23000		$.ajax({
23001			type: 'POST',
23002			url: this._renderUrl,
23003			contentType: "application/json",
23004            xhrFields: {withCredentials: true},
23005			dataType: "json",
23006			timeout: this.settings.renderRequestTimeout * 1000,
23007			data: JSON.stringify(model),
23008			
23009			success: function(data) {
23010				// handle disconnection
23011				if (!data) {
23012					return renderError();
23013				}
23014				
23015				// remove host from location because of ajax request limitations
23016				// self._renderUrl = data.location.replace(/https?:\/\/([^\/]+)/i, '');
23017        self._renderUrl = data.location.replace(/http?:/, window.location.protocol);
23018        // Force HTTPS
23019        // self._renderUrl = data.location.replace(/^http:\/\//i, 'https://');
23020
23021				if (width && height && $.isFunction(callback)) {
23022					var imageUrl = self._renderUrl + '/render/' + type +
23023						'?width=' + width +
23024						'&height=' + height +
23025						'&fieldofview=' + self.camera.fieldofview +
23026						renderC +
23027						/* Random param is needed to fool the browser, because if you are changing
23028						   img src to the same url, browser doesn't reload it (nothing changed)
23029						*/
23030						'&' + Math.random();
23031					
23032					// serve JSONp request
23033					if (renderCallback) {
23034						$.ajax({
23035							url: imageUrl,
23036							contentType: 'application/json',
23037							dataType: 'json',
23038							success: renderResponse,
23039							error: renderError
23040						});
23041					} else {
23042						// run it in a separite thread?
23043						window.setTimeout(function() {
23044							// serve typical image request
23045							$('<img src="'+ imageUrl +'" />')
23046								.load(function() {
23047									renderResponse($(this));
23048								})
23049								.error(renderError);
23050						}, 100);
23051					}
23052				}
23053			},
23054			error: renderError
23055		});
23056	},
23057
23058	/**
23059	 * Check for rooms intersection and combine rooms if any intersection
23060	 * were found.
23061	 * 
23062	 * @param {Object} initiator Object which initiate possible intersection
23063	 * (can be corner, wall or shape), for example - moved corner.
23064	 */ 
23065	checkForRoomsIntersection: function(initiator) {
23066		var clipShapes = [], i, j, k, shapesToCheck,
23067			clipLength, checkLength, clipResults, resultLength, cLength,
23068			self = this, gotIntersection = false;
23069		
23070		// Check if Weiler-Atherton added corners on walls common to different shapes
23071		function checkForCommonWalls(inter) {
23072			var i, interLength = inter.length, wall;
23073			
23074			for (i = 0; i < interLength; i++) {
23075				if (inter[i]) {
23076					wall = self._findWall(inter[i].from, inter[i].to);
23077					
23078					if (wall && wall._shapes.length > 1) {
23079						return true;
23080					}
23081				}
23082			}
23083			return false;
23084		}
23085		
23086		// One shape - nothing to intersect.
23087		if (this.shapes.length === 1) {
23088			return;
23089		}
23090		
23091		// Collect list of shapes, which can be used as clip polygons.
23092		// It's shapes linked to moved wall or corner
23093		switch (initiator.type) {
23094			case 'wall':
23095			case 'corner':
23096				clipShapes = initiator.getShapes();
23097				break;
23098			case 'shape':
23099				clipShapes = [initiator];
23100				break;
23101		}
23102		
23103		// Check if there are shapes available for intersection
23104		if (clipShapes.length === this.shapes.length) {
23105			return false;
23106		}
23107		
23108		// Collect shapes which we are going to clip with given list of clip polygons 
23109		shapesToCheck = $.grep(this.shapes, function(shape) {
23110			if (clipShapes.indexOf(shape) !== -1) {
23111				return false;
23112			}
23113			
23114			// avoid intersection with shapes, which already linked to one of clip shapes.
23115			for (i = 0, clipLength = clipShapes.length; i < clipLength; i++) { 
23116				for (j = 0, cLength = clipShapes[i].length; j < cLength; j++) {
23117					if (shape.indexOf(clipShapes[i][j]) !== -1) {
23118						return false;
23119					}
23120				}
23121			}
23122			return true;
23123		});
23124		
23125		for (i = 0, clipLength = clipShapes.length; i < clipLength; i++) {
23126			for (j = 0, checkLength = shapesToCheck.length; j < checkLength; j++) {
23127				clipResults = weilerAtherton(shapesToCheck[j], clipShapes[i], 'clip');
23128				
23129				// Intersections found?
23130				if (clipResults && !checkForCommonWalls(clipResults.added.subj) && !checkForCommonWalls(clipResults.added.clip)) {
23131					resultLength = clipResults.clipped.length;
23132					
23133					if (resultLength !== 0) {
23134						// add new obtained shapes
23135						for (k = 0; k < resultLength; k++) {
23136							this.replaceShape(shapesToCheck[j], clipResults.clipped[k]);
23137						}
23138						
23139						this.replaceShape(clipShapes[i], clipResults.clip, true);
23140						
23141						shapesToCheck[j].remove();
23142						clipShapes[i].remove();
23143						
23144						gotIntersection = true;
23145					}
23146				}
23147			}
23148		}
23149		
23150		if (gotIntersection) {
23151			myDeco.body.trigger('shapesIntersected');
23152		}
23153	},
23154
23155	/**
23156	 * Return string representation for latest changes.
23157	 * @param {Number} latest Count of latest changes, leave empty for all
23158	 */
23159	changeLog: function(latest) {
23160		return $.map(typeof latest === 'undefined' ? 
23161						this.history.changes :
23162						this.history.changes.slice(-latest), 
23163					function(item) {
23164						return item[0] + (item[1] ? ' ' + item[1].type : '');
23165					});
23166	},
23167	
23168	/**
23169	 * Undo the last action.
23170	 */
23171	undo: function() {
23172		if (this._undoModel !== false && this._undoModel !== 'false') {
23173			this._redoModel = this._currModel;
23174			this.reset(true);
23175			this.loadModel(JSON.parse(this._undoModel), true);
23176			myDeco.body.trigger('undo');
23177			this._currModel = this._undoModel;
23178			this._undoModel = false;
23179			this._super();
23180		}
23181	},
23182
23183	/**
23184	 * Redo the last action.
23185	 */
23186	redo: function() {
23187		if (this._redoModel !== false && this._redoModel !== 'false') {
23188			this._undoModel = this._currModel;
23189			this.reset(true);
23190			this.loadModel(JSON.parse(this._redoModel), true);
23191			myDeco.body.trigger('redo');
23192			this._currModel = this._redoModel;
23193			this._redoModel = false;
23194			this._super();
23195		}
23196	},
23197	
23198	/**
23199	 * Locate nearest corner to specified coordinates
23200	 * @param {Point} point
23201	 * @param {Numner} maxDistance
23202	 * @param {Array} except
23203	 * @returns {Corner} nearest corner found
23204	 */
23205	findNearestCorner: function(point, maxDistance, except) {
23206		var i, d,
23207			candidate,
23208			distance = Number.MAX_VALUE;
23209		
23210		for (i = this.corners.length - 1; i >= 0; i--) {
23211			if (!except || except.indexOf(this.corners[i]) === -1) {
23212				d = this.corners[i].distance(point);
23213				if (d < maxDistance && d < distance) {
23214					distance = d;
23215					candidate = this.corners[i];
23216				}
23217			}
23218		}
23219		
23220		return candidate;
23221	},
23222	
23223	/**
23224	 * Find point which can stick to already existing entites
23225	 * @param {Point} newPoint
23226	 * @param {Point} lastCorner
23227	 * @param {Boolean} denyShapes Deny points inside existing shapes
23228	 * @returns {Object}
23229	 */
23230	findAllowedPoint: function(newPoint, lastCorner, denyShapes) {
23231		var corner, wall, length = Number.MAX_VALUE, len, i, ilen, pretender,
23232			point, walls, angle, angleDiff;
23233		
23234		if (!lastCorner ||
23235				lastCorner.distance(newPoint) > this.settings.minDistance) {
23236			
23237			// check angles with attached walls
23238			if (lastCorner) {
23239				angle = lastCorner.angleTo(newPoint);
23240				walls = lastCorner.getWalls();
23241				
23242				for (i = 0, ilen = walls.length; i < ilen; i++) {
23243					angleDiff = Math.abs(
23244						lastCorner.angleTo(walls[i].getOpposite(lastCorner)) -
23245						angle
23246					);
23247					
23248					if (angleDiff < this.settings.minAngle ||
23249							angleDiff > 360 - this.settings.minAngle) {
23250						return false;
23251					}
23252				}
23253			}
23254			
23255			if (lastCorner) {
23256				for (i = 0, ilen = this.walls.length; i < ilen; i++) {
23257					pretender = findWallIntersection(
23258						[lastCorner, newPoint],
23259						this.walls[i],
23260						true);
23261					
23262					len = pretender ?
23263						pretender.distance(lastCorner) :
23264						Number.MAX_VALUE;
23265					
23266					if (len > this.settings.minDistance && len < length) {
23267						length = len;
23268						wall = this.walls[i];
23269						point = pretender;
23270					}
23271				}
23272				
23273				// check for nearest corners in case of wall intersection
23274				if (point && wall) {
23275					// look for nearest point around point of intersection
23276					corner = this.findNearestCorner(point,
23277						this.settings.searchDistance);
23278				}
23279			}
23280			
23281			if (!corner && !point && !wall) {
23282				corner = this.findNearestCorner(newPoint,
23283					this.settings.searchDistance, lastCorner ?
23284						lastCorner.getCorners(false, true, true) : false);
23285			}
23286			
23287			// if not corner is found, lets find nearest wall to attach
23288			if (!corner && !point) {
23289				wall = this.findNearestWall(newPoint,
23290					this.settings.searchDistance, 0, lastCorner ?
23291						lastCorner.getWalls() : false);
23292				
23293				if (wall) {
23294					point = newPoint.projection2line(wall);
23295				}
23296			}
23297			
23298			// check for point inside existing shape
23299			if (!corner && !wall && !point && denyShapes) {
23300				if ($.grep(this.shapes, function(shape) {
23301					return newPoint.inPoly(shape);
23302				}).length !== 0) {
23303					return false;
23304				}
23305			}
23306			
23307			return {
23308				corner: corner,
23309				point: corner ? undefined : (point || newPoint),
23310				wall: wall
23311			};
23312		}
23313	},
23314	
23315	/**
23316	 * Locate nearest wall to a specified coordinates
23317	 * @param {Point} point
23318	 * @param {Number} maxDistance
23319	 * @param {Number} minWidth Minimal wall width
23320	 * @param {Array} except Walls to exclude
23321	 * @returns {Wall} nearest wall found
23322	 */
23323	findNearestWall: function(point, maxDistance, minWidth, except) {
23324		maxDistance = +(maxDistance || Number.MAX_VALUE);
23325		minWidth = +(minWidth || 0);
23326		
23327		if ($.isArray(point)) {
23328			point = new Point(point);
23329		}
23330		
23331		var wall, distance = Number.MAX_VALUE, d;
23332		$.each(this.walls, function() {
23333			// filter by minimal width
23334			if ((minWidth && this.getAttribute('width') < minWidth) ||
23335					(except && $.inArray(this, except) !== -1)) {
23336				return;
23337			}
23338			
23339			d = Math.abs(point.distance2segment(this));
23340			if (d < distance && d <= maxDistance) {
23341				distance = d;
23342				wall = this;
23343			}
23344		});
23345		
23346		return wall;
23347	},
23348	
23349	/**
23350	 * Select entity in model
23351	 * @param {Object} entity Entity to select
23352	 * @param {Boolean} append Will entity append to already selected items
23353	 * @returns {Boolean} selection result, successful or not
23354	 */
23355	select: function(entity, append) {
23356		var ret = false;
23357		
23358		if (!append) {
23359			this.unselect();
23360		}
23361		
23362		if (entity && !entity.selected) {
23363			this._selected.push(entity);
23364			ret = entity._select();
23365
23366			// update measurers on walls
23367			if (ret && (entity.type === 'wall' || entity.type === 'corner' || entity.type == 'shape')) {
23368				$.each(entity.getWalls(), function() {
23369					this.updateMeasurer(true);
23370				});
23371			}
23372		}
23373		
23374		this._updateSelection();
23375		myDeco.body.trigger('selectEnd');
23376		
23377		return ret;
23378	},
23379	
23380	/**
23381	 * Unselect all or specified entity
23382	 * @param {Object} entity Entity to unselect. If not specified, all selected items will be unselected.
23383	 */
23384	unselect: function(entity) {
23385		var entities = entity ?
23386			// entity SHOULD have type defined, in other case this is an array
23387			// of entities
23388			(entity.type ? [entity] : entity) : this._selected,
23389			i, before = [], after = [];
23390		
23391		// collect walls with enabled measurers
23392		// if any of entities are corner or wall
23393		if ($.any(entities, function(element) { return element.type === 'corner' || element.type === 'wall' || element.type == 'shape'; })) {
23394			for (i = this._selected.length - 1; i >= 0; i--) {
23395				if (this._selected[i].type === 'wall' || this._selected[i].type === 'corner' || this._selected[i].type === 'shape') {
23396					$.merge(before, this._selected[i].getWalls());
23397				}
23398			}
23399			
23400			before = before.unique();
23401		}
23402		
23403		for (i = entities.length - 1; i >= 0; i--) {
23404			if (entities[i].selected) {
23405				$.extract(entities[i], this._selected)._unselect();
23406			}
23407		}
23408		
23409		// if have previously selected walls with measurers
23410		// disable no more affected
23411		if (before.length > 0) {
23412			// gather walls which should have measurers
23413			for (i = this._selected.length - 1; i >= 0; i--) {
23414				if (this._selected[i].type === 'wall' || this._selected[i].type === 'corner' || this._selected[i].type == 'shape') {
23415					$.merge(after, this._selected[i].getWalls());
23416				}
23417			}
23418			
23419			after = after.unique();
23420			
23421			// compare both lists and hide measurers for not afftected walls
23422			$.each($.substract(before, after), function() { this.updateMeasurer(false); });
23423		}
23424		
23425		this._updateSelection();
23426		
23427		myDeco.body.trigger('unselectEnd');
23428	},
23429	
23430	_updateSelection: function() {
23431		var x1 = Number.MAX_VALUE, x2 = -Number.MAX_VALUE,
23432			y1 = Number.MAX_VALUE, y2 = -Number.MAX_VALUE,
23433			z1 = Number.MAX_VALUE, z2 = -Number.MAX_VALUE,
23434			selected = this.selected(), i, ilen = selected.length,
23435			j, jlen, p, found = false;
23436		
23437		for (i = 0; i < ilen; i++) {
23438			if (selected[i]._bbox) {
23439				for (j = 0, jlen = selected[i]._bbox.length; j < jlen; j++) {
23440					p = selected[i]._bbox[j];
23441					x1 = Math.min(x1, p.x);
23442					x2 = Math.max(x2, p.x);
23443					y1 = Math.min(y1, p.y);
23444					y2 = Math.max(y2, p.y);
23445					z1 = Math.min(z1, p.z);
23446					z2 = Math.max(z2, p.z);
23447				}
23448				found = true;
23449			} else if (selected[i].position) {
23450				x1 = Math.min(x1, selected[i].position.x);
23451				x2 = Math.max(x2, selected[i].position.x);
23452				y1 = Math.min(y1, selected[i].position.y);
23453				y2 = Math.max(y2, selected[i].position.y);
23454				z1 = Math.min(z1, selected[i].position.z);
23455				z2 = Math.max(z2, selected[i].position.z);
23456				found = true;
23457			}
23458		}
23459		
23460		if (found) {
23461			this._selectionCenter.update((x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2);
23462		}
23463		
23464		this.rotationAvailable = selected.length !== 0 &&
23465			!$.any(selected, function(entity) {
23466				return !!(entity.parent ||
23467					['component', 'camera'].indexOf(entity.type) === -1);
23468			});
23469	},
23470	
23471	/**
23472	 * Returns list of selected entities
23473	 * @param {Boolean} first Return only the first item in selected list
23474	 * @returns {Array||Object}
23475	 */
23476	selected: function(first) {
23477		return first ? this._selected[0] : this._selected;
23478	},
23479	
23480	/**
23481	 * Batch move of entity or selected entities
23482	 * @param {Number} dx
23483	 * @param {Number} dy
23484	 * @param {Number} dz
23485	 * @param {Object} entity Entity to operate, default are selected entities
23486	 * @param {Boolean} forceRestrictions Force restrictions for on-wall components
23487	 * @returns {Object} this
23488	 */
23489	move: function(dx, dy, dz, entity, forceRestrictions) {
23490		var entities = entity ? [entity] : this._selected,
23491			i;
23492		
23493		for (i = entities.length - 1; i >= 0; i--) {
23494			entities[i].move(dx, dy, dz, !forceRestrictions &&
23495				entities[i].level === -1 && !entities[i].category);
23496		}
23497		
23498		this._updateSelection();
23499		
23500		return this;
23501	},
23502	
23503	/**
23504	 * Batch rotate entities
23505	 * @param dangle 
23506	 * @returns {Object} this
23507	 */
23508	rotate: function(dangle) {
23509		var i, ilen, point, multiple = this._selected.length > 1;
23510		
23511		if (this.rotationAvailable) {
23512			for (i = 0, ilen = this._selected.length; i < ilen; i++) {
23513				this._selected[i].rotate(this._selected[i].getAngle() + dangle,
23514					multiple);
23515				if (ilen !== 1) {
23516					point = this._selected[i].position.clone()
23517						.rotate(this._selectionCenter, -dangle);
23518					this._selected[i].move(
23519						point.x - this._selected[i].position.x,
23520						point.y - this._selected[i].position.y);
23521				}
23522			}
23523		}
23524		return this;
23525	},
23526	
23527	/**
23528	 * Operation end notify method
23529	 * @param {Array} data Data to notify, first item usually is operation name 
23530	 */
23531	operationEnd: function(data) {
23532		var name = data[0], i, item;
23533		
23534		$.each(this._selected, function() {
23535			this.operationEnd(name);
23536		});
23537		
23538		if (name === 'remove' || ((name === 'drag' || name === 'add') && 
23539				$.any(this._selected, function(element) {
23540					return element.level === 0 || element.level === 1;
23541				}))) {
23542			$.each(this.components, function() {
23543				// each on-top components shold relocate after movement of any of component
23544				// move method by default make all checks
23545				if (this.level >= 1) {
23546					this.move(0, 0);
23547				}
23548			});
23549		}
23550		
23551		this._super(data);
23552		
23553		if (name !== 'material') {
23554			// Check for rooms intersections
23555			for (i = this._selected.length - 1; i >= 0; i--) {
23556				item = this._selected[i];
23557					
23558				if ((name === 'add' && item.type === 'shape') ||
23559						(name === 'drag' && (item.type === 'wall' || item.type === 'corner'))) {
23560					this.checkForRoomsIntersection(item);
23561				}
23562			}
23563		}
23564		
23565		// store undo/redo information
23566		this._undoModel = this._currModel;
23567		this._currModel = JSON.stringify(this.getModel(2, true));
23568		this._redoModel = false;
23569		
23570		myDeco.body.trigger('operationEnd', [data]);
23571		
23572		// fire operation reset
23573		this.operationReset();
23574	},
23575	
23576	/**
23577	 * Operation start notify method
23578	 * @param {Array} data Data to notify, first item usually is operation name
23579	 */
23580	operationStart: function(data) {
23581		data = $.isArray(data) ? data : (typeof data === 'undefined' ? [] : [data]);
23582		
23583		$.each(this._selected, function() {
23584			this.operationStart(data[0]);
23585		});
23586		
23587		myDeco.body.trigger('operationStart', [data]);
23588	},
23589	
23590	/**
23591	 * Operation reset notify method
23592	 */
23593	operationReset: function() {
23594		$.each(this._selected, function() {
23595			this.operationReset();
23596		});
23597		
23598		myDeco.body.trigger('operationReset');
23599	},
23600	
23601	/**
23602	 * Duplicate method
23603	 * Operates on already selected items
23604	 */
23605	duplicate: function() {
23606		this.operationStart('duplicate');
23607		
23608		var self = this, x, y,
23609			entities = $.grep(this._selected, function(entity) {
23610				return entity.type === 'component' || entity.type === 'window' ||
23611					entity.type === 'door';
23612			}),
23613			// gather component IDs
23614			ids = $.map(entities, function(entity) {
23615				return entity.component_id;
23616			}).unique();
23617		
23618		myDeco.App.ws.proxy.fetch('component', ids, function(result) {
23619			var components = [];
23620			
23621			$.each(entities, function() {
23622				// shift new entity position
23623				x = this.position.x + 50;
23624				y = this.position.y + 50;
23625				
23626				var component = $.cloneObject(result[this.component_id]),
23627					_self = this;
23628				
23629				// make component identical by original by position, parent (for on-wall components) and rotation
23630				$.extend(component, {
23631					parent: this.parent,
23632					rotation: $.merge([], this.rotation),
23633					category: this.category
23634				});
23635				
23636				if (this.configuration) {
23637					$.each(this.configuration, function(key) {
23638						component[key] = _self[key];
23639					});
23640					component.bounding_box = [
23641						$.merge([], component.bounding_box[0]),
23642						$.merge([], component.bounding_box[1])
23643					];
23644				}
23645				
23646				components.push(self.addComponent(component, x, y, this.position.z));
23647			});
23648			
23649			// notify related entities etc about new items
23650			self.operationEnd(['add']);
23651			
23652			// unselect all entities
23653			self.unselect();
23654			
23655			$.each(components, function() {
23656				self.select(this, true);
23657			});
23658		});
23659	},
23660	
23661	/**
23662	 * Split wall by corner
23663	 * @param {Wall} wall Wall to split
23664	 * @param {Point} point by split with, will split in the middle if omitted
23665	 * @returns {Corner}
23666	 */
23667	splitWall: function(wall, point) {
23668		// TODO: check for wall components, store them and restore
23669		// on the new walls
23670		point = point || wall.middle;
23671		
23672		var self = this,
23673			// add corner
23674			corner = this.addCorner(point.projection2line(wall)),
23675			wallX,
23676			thickness = wall.thickness,
23677			material = wall.material,
23678			// copy shapes instead of linking
23679			shapes = wall.getShapes().slice(),
23680			nwall, i, halfW, wallsC = [[], []],
23681			components = wall.getComponents(),
23682			restoreWallProps = function(wall, components) {
23683				wall.material = material;
23684				wall.thickness = thickness;
23685				self.addWall(wall, shapes);
23686				for (i = components.length - 1; i >= 0; i--) {
23687					components[i].placeToWall(wall, components[i]._shape);
23688				}
23689			};
23690		
23691		// filter components which should remain on walls
23692		for (i = components.length - 1; i >= 0; i--) {
23693			wallX = wall.getCorners()[0].distance(point);
23694			halfW = components[i].width / 2;
23695			
23696			if (components[i].wallX + halfW < wallX) {
23697				wallsC[0].push(components[i]);
23698				wall.removeComponent(components[i]);
23699			} else if (components[i].wallX - halfW > wallX) {
23700				wallsC[1].push(components[i]);
23701				wall.removeComponent(components[i]);
23702			}
23703		}
23704		
23705		// remove old wall with remaining components
23706		// leave all points
23707		wall.remove(true);
23708		
23709		// insert point to a shape
23710		$.each(shapes, function() {
23711			this.addCorner(corner, wall);
23712		});
23713		
23714		// create new walls
23715		nwall = [wall[0], corner];
23716		restoreWallProps(nwall, wallsC[0]);
23717		
23718		nwall = [corner, wall[1]];
23719		restoreWallProps(nwall, wallsC[1]);
23720		
23721		return corner;
23722	},
23723
23724	/**
23725	 * Place components inside first shape
23726	 * @param {Array} components List of components
23727	 * @param {Shape} shape Target shape
23728	 * @param {Function} callback
23729	 */
23730	dropComponents: function(components, shape, callback) {
23731		var self = this;
23732
23733		if ($.isFunction(shape)) {
23734			callback = shape;
23735			shape = this.shapes[0];
23736		}
23737
23738		if (!shape && !Boolean(shape = this.shapes[0])) {
23739			if ($.isFunction(callback)) {
23740				callback.call(null);
23741			}
23742			return;
23743		}
23744
23745		myDeco.App.ws.proxy.fetch('component', components,
23746			function(components) {
23747				var shapeBox = shape.getBBox(),
23748					walls = shape.getWalls(),
23749					// variable holding list of walls allowed for onwall
23750					// components
23751					wallSet = walls.slice(),
23752
23753					i, wall, width, depth, component, x,
23754					corners, angle, componentSet,
23755					
23756					wallThickness = self.walls[0].thickness,
23757					
23758					// function for placing component on wall
23759					placeComponent = function(component, x, y, z) {
23760						var wall = component.parent;
23761						
23762						component = $.cloneObject(component);
23763						
23764						if (wall) {
23765							// restore parent as it was destroyed by clone
23766							component.parent = wall;
23767							if (typeof x === 'undefined' ||
23768									typeof y === 'undefined') {
23769								x = (wall[0].x + wall[1].x) / 2;
23770								y = (wall[0].y + wall[1].y) / 2;
23771							}
23772							
23773							if (typeof z === 'undefined') {
23774								z = component.category === 'Door' ? 0 :
23775									wall.getAttribute('height') / 2;
23776							}
23777							
23778							component.rotation[0] = 270;
23779							
23780							if (component.category === 'Window' || component.category === 'Door') {
23781								component.configuration = {
23782									'width': component.width,
23783									'height': component.height,
23784									'depth': component.depth
23785								};
23786							}
23787						}
23788						self.addComponent(component, x, y, z);
23789					},
23790					
23791					sortFunction = function(a, b) {
23792						return b.width - a.width;
23793					},
23794					
23795					isComponentInShape = function(point, component) {
23796						var i, j, p;
23797						
23798						for (i = 0; i < 2; i++) {
23799							for (j = 0; j < 2; j++) {
23800								p = new Point(
23801									point.x - component.width / 2 + component.width * i,
23802									point.y - component.depth / 2 + component.depth * j
23803								);
23804								if (!p.inPoly(self.shapes[0])) {
23805									return false;
23806								}
23807							}
23808						}
23809						
23810						return true;
23811					},
23812					putRandomly = function(component) {
23813						var i, n = 0, x, y, h, points, edge, intersection,
23814							found = false,
23815							v = [new Point(), new Point()],
23816							cwidth = component.width / 2,
23817							cdepth = component.depth / 2,
23818							// by default place component in the center of
23819							// the shape
23820							point = new Point(
23821								shapeBox.x + shapeBox.width / 2,
23822								shapeBox.y + shapeBox.depth / 2
23823							),
23824							x1 = shapeBox.x + cwidth,
23825							x2 = shapeBox.x + shapeBox.width - cwidth,
23826							y1 = shapeBox.y + cdepth,
23827							y2 = shapeBox.y + shapeBox.depth - cdepth,
23828							width = x2 - x1,
23829							depth = y2 - y1;
23830						// first of all check possibility of component fit
23831						if (width > 0 && depth > 0) {
23832							do {
23833								// get a random value for x between x1 and x2
23834								x = x1 + width * Math.random();
23835								// 10 is additional for sure to intersect
23836								v[0].update(x, y1 - cdepth - 10);
23837								v[1].update(x, y2 + cdepth + 10);
23838								points = [];
23839
23840								// find closest y1 and y2 values where vertical
23841								// line crosses shape edges
23842								for (i = shape.length; i !== 0; i--) {
23843									edge = [shape[i % shape.length], shape[i - 1]];
23844									intersection = findWallIntersection(edge, v, true);
23845									if (intersection) {
23846										points.push(intersection);
23847									}
23848								}
23849
23850								for (i = Math.floor(points.length / 2) - 1; i >= 0; i--) {
23851									y = Math.min(points[i * 2].y, points[i * 2 + 1].y);
23852									h = Math.max(points[i * 2].y, points[i * 2 + 1].y) - y;
23853									if (h > component.depth) {
23854										y = y + (h - component.depth) * Math.random();
23855										point.update(x, y);
23856										if (isComponentInShape(point, component)) {
23857											found = true;
23858											break;
23859										}
23860									}
23861								}
23862
23863								n++;
23864
23865							// try for first 50 attemts
23866							} while(!found && n < 50);
23867						}
23868
23869						$.extract(component, components);
23870						placeComponent(component, point.x, point.y);
23871					},
23872					wallGrep = function(size) {
23873						return function(wall) {
23874							return wall.getAttribute('width') > size;
23875						};
23876					};
23877				
23878				components = $.values(components);
23879
23880				/* ok, let's roll
23881				   1. extract from product list components which should place on
23882				      walls and apply them
23883				   2. find components which have big difference between width and
23884				      depth assuming to place it near walls; sort walls and
23885				      components by length and place them near that walls
23886				   3. place reamined components randomly in the room 
23887				*/
23888
23889				// add onwall components including windows/doors
23890				for (i = components.length - 1; i >= 0; i--) {
23891					component = components[i];
23892					if (component.level === -1) {
23893						wall = $.grep(wallSet, wallGrep(component.width));
23894						wall = wall[Math.floor(Math.random() * wall.length)];
23895
23896						if (wall && wall.getAttribute('height') > component.depth) {
23897							// don't allow to place any component alongside this wall
23898							if (component.category === 'Door') {
23899								$.extract(wall, walls);
23900							}
23901							$.extract(wall, wallSet);
23902
23903							component.parent = wall;
23904							//in the center of the wall
23905							placeComponent(component);
23906						}
23907
23908						$.extract(component, components);
23909					}
23910				}
23911
23912				// sort components by width/depth ratio
23913				components.sort(function(a, b) {
23914					if (!a.ratio) {
23915						a.ratio = a.width / a.depth;
23916						if (a.ratio < 1) {
23917							a.ratio = 1 / a.ratio;
23918						}
23919					}
23920					
23921					if (!b.ratio) {
23922						b.ratio = b.width / b.depth;
23923						if (b.ratio < 1) {
23924							b.ratio = 1 / b.ratio;
23925						}
23926					}
23927					
23928					return a.ratio - a.ratio;
23929				});
23930
23931				// place components (not onwall ones) along the walls
23932				for (i = components.length - 1; i >= 0; i--) {
23933					component = components[i];
23934					if (components[i].level !== 2) {
23935						if (component.ratio >= 2) {
23936							// find wall with length equal or more than max size
23937							wallSet = $.grep(walls, wallGrep(Math.max(component.width, component.depth)));
23938							
23939							if (wallSet.length > 0) {
23940								// yes, we have walls with desired length
23941								wall = wallSet[Math.floor(Math.random() *
23942									wallSet.length)];
23943									
23944								// extract wall from list of available walls
23945								$.extract(wall, walls);
23946								
23947								corners = wall.getCorners();
23948								angle = corners[0].angleTo(corners[1]);
23949								
23950								width = component.width;
23951								depth = component.depth;
23952								component.rotation[2] = 180 - angle;
23953								
23954								// rotate component in case of depth more that width
23955								if (component.depth > component.width) {
23956									component.rotation[2] += 90;
23957									width = component.depth;
23958									depth = component.width;
23959								}
23960								
23961								x = Math.random() * (wall.getAttribute('width') - width - wallThickness) +
23962									width / 2 + wall.thickness / 2;
23963									
23964								x = corners[0].clone().translate(-x, 0).rotate(corners[0], angle);
23965								
23966								x = x.clone().translate(- depth / 2 - wallThickness / 2, 0).rotate(x, angle + 90);
23967								$.extract(component, components);
23968								placeComponent(component, x.x, x.y);
23969							}
23970							// else we don't want to put this component at all
23971						} else {
23972							putRandomly(component);
23973						}
23974					}
23975				}
23976				
23977				// get wall components (level === 1) with height less than 1m
23978				componentSet = $.grep(self.components.slice(), function(component) {
23979					return component.level === 1 && component.height <= 100;
23980				});
23981				
23982				// and sort them by size
23983				componentSet.sort(sortFunction);
23984				components.sort(sortFunction);
23985				
23986				
23987				// place 2nd level components over other components with level === 1
23988				for (i = components.length - 1; i >= 0; i--) {
23989					if (componentSet[i]) {
23990						components[i].rotation = componentSet[i].rotation.slice();
23991						placeComponent(components[i],
23992							componentSet[i].position.x,
23993							componentSet[i].position.y,
23994							componentSet[i].height + components[i].height / 2
23995						);
23996					} else {
23997						putRandomly(components[i]);
23998					}
23999				}
24000
24001				if ($.isFunction(callback)) {
24002					callback.call(null);
24003				}
24004			});
24005	},
24006	
24007	/**
24008	 * Check availability of floorplan tab and floor button: 
24009	 */
24010	checkFloorPermissions: function (showPopin, callback) {
24011		var self = this,
24012			tabs = $('#sidebar').find('.build-tabs-selector'),
24013			user = myDeco.username,
24014			buttonAvailable = false,
24015			popin = showPopin ? true : false,
24016			systemUsers = new Array(
24017				'home3d_properties',
24018				'mask_monkey'
24019			);
24020			
24021		if (user) {
24022			$.ajax({
24023				url: '/3dplanner/can_upload_floorplans',
24024				success: function (data) {					
24025					if (data && data['can_upload_floorplans']) {
24026						if ($.isFunction(callback)) {
24027							callback();
24028						}	
24029					} else {
24030						self.disableFloorPlanUpload(popin);	
24031					}
24032				},
24033				error: function (request, status, error) {
24034				    self.disableFloorPlanUpload(popin);
24035				}
24036			});
24037		} 
24038		
24039		if (myDeco.settings.WL_PROFILE === 166 && $.inArray(user, systemUsers) === -1) {
24040			$('#sidebar').find('.build-walls-container, .build-floor-container').toggle(); 
24041		} else {
24042			buttonAvailable = true;
24043		}
24044
24045		tabs.html(tmpl('build-tabs', {
24046			available: buttonAvailable
24047		}));
24048
24049		// always start with walls (Gert-Jan)
24050		$('#sidebar').find('.build-walls-container, .build-floor-container').toggle();
24051		
24052		myDeco.tabSwitcher.initTabSwitcher(tabs.find('.tabs'));
24053	},
24054	
24055	/**
24056	 * Disable floor plan button and show popin if it's needed
24057	 */
24058	disableFloorPlanUpload: function (showPopin) {
24059		$('#sidebar')
24060			.find('.build-floor-container .upload-container')
24061			.html(tmpl('floor-plan-disabled'));	
24062			
24063		if (showPopin) {
24064			myDeco.popin.open({
24065				template: gettext("Sorry, you have no free floor plan upload allowance. We'll launch soon the ability to buy them."),
24066				className: 'login-popin',
24067				holderClassName: 'login-content-holder'				
24068			});
24069		}
24070	}	
24071	
24072});
24073/*globals $, AppView, myDeco, Cloud, Raphael, Point, window */
24074/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
24075          sloppy: true, white: true */
24076/**
24077 * @fileOverview Base class for all model-editor based views which uses canvas and cloud
24078 */
24079
24080var FpeView = AppView.extend({
24081	/**
24082	 * View type, should be static
24083	 * @type {String}
24084	 * @static
24085	 */
24086	$type: 'me-view',
24087	
24088	/**
24089	 * Canvas SVG element, Raphael instance or something else
24090	 * @type {Object}
24091	 */
24092	canvas: false,
24093	
24094	/**
24095	 * Entity cloud, used for batch processing
24096	 * @type {Array}
24097	 */
24098	_cloud: undefined,
24099	
24100	/**
24101	 * Canvas zoom factor
24102	 * @type {Number}
24103	 */
24104	zoom: 1,
24105	
24106	/**
24107	 * Full (not limited) zoom factor
24108	 * @type {Number}
24109	 */
24110	_fullZoom: 1,
24111	
24112	/**
24113	 * Canvas offset
24114	 * @type {Object}
24115	 */
24116	_offset: false,
24117
24118	/**
24119	 * Button-like targets for decorating all walls/ceiling
24120	 * @type {Object}
24121	 */
24122	_decorateTargets: false,
24123	
24124	/**
24125	 * Cache for bound listeners
24126	 * @type {Object}
24127	 */
24128	_listenersCache: {},
24129
24130	/**
24131	 * Flag indicating initial zoom of view is needed
24132	 * @type {Boolean}
24133	 */
24134	_initialZoom: true,
24135	
24136	/**
24137	 * Init function
24138	 * 
24139	 * @param {Object} options Init options
24140	 * 
24141	 * required are:
24142	 * - {FpeModel} model
24143	 * - {FpeController} controller
24144	 */
24145	init: function(options) {
24146		var self = this;
24147		
24148		this._draggablesObserver = $.proxy(options.controller.registerDraggables,
24149			options.controller);
24150		
24151		this._droppablesObserver = $.proxy(options.controller.registerDroppables,
24152			options.controller);
24153		
24154		this._super(options);
24155		this._cloud = new Cloud(this);
24156		this.canvas = new Raphael(this.canvasElement.get(0), this.canvasWidth, this.canvasHeight, myDeco.browser.isMobileSafari);
24157		this._initCanvasRelated();
24158		this._decorateTargets = this.viewHolder.find('.decorate-target');
24159		
24160		this._listenersCache = {
24161			load:	$.proxy(function(e, sameRoom) {
24162						this.load(sameRoom);
24163					}, this),
24164			add:	$.proxy(function(e, entity) {
24165						this['add' + e.type.replace('Added', '')
24166							.capitalize()].call(this, entity);
24167					}, this),
24168			pan:	$.proxy(function(e, dx, dy) {
24169						this.pan(dx * this.zoom, dy * this.zoom);
24170					}, this),
24171			scale:	$.proxy(function(e, factor) {
24172						this.scale(factor);
24173					}, this),
24174			clear:	$.proxy(function() {
24175						this.clear();
24176					}, this),
24177			zoom:	$.proxy(function(e, coeff) {
24178						coeff = coeff || 0.05;
24179						this.scale((e.type === 'zoomIn' ? +1 : -1) * coeff);
24180					}, this),
24181			show:	$.proxy(function(e) {
24182						this.showAll();
24183					}, this)
24184		};
24185			
24186		myDeco.body.bind('loadRoom', function() {
24187			self._initialZoom = !self._controller._viewActive;
24188		});
24189
24190		this.processEvents(true);
24191		this.load();
24192	},
24193	
24194	processEvents: function(type) {
24195		if (type) {
24196			myDeco.body
24197				.bind('roomLoaded', this._listenersCache.load)
24198				.bind('componentAdded shapeAdded wallAdded cornerAdded cameraAdded', this._listenersCache.add)
24199				.bind('pan', this._listenersCache.pan)
24200				.bind('scale', this._listenersCache.scale)
24201				.bind('roomCleared', this._listenersCache.clear)
24202				.bind('zoomIn zoomOut', this._listenersCache.zoom);
24203			
24204			if (myDeco.browser.isMobileSafari) {
24205				$(window).bind('orientationchange', this._listenersCache.show);
24206			}
24207			
24208		} else {
24209			myDeco.body
24210				.unbind('roomLoaded', this._listenersCache.load)
24211				.unbind('componentAdded shapeAdded wallAdded cornerAdded cameraAdded', this._listenersCache.add)
24212				.unbind('pan', this._listenersCache.pan)
24213				.unbind('scale', this._listenersCache.scale)
24214				.unbind('roomCleared', this._listenersCache.clear)
24215				.unbind('zoomIn zoomOut', this._listenersCache.zoom);
24216			
24217			if (myDeco.browser.isMobileSafari) {
24218				$(window).unbind('orientationchange', this._listenersCache.show);
24219			}
24220		}
24221	},
24222	
24223	/**
24224	 * Function to init something canvas related, objects, arrows etc
24225	 */
24226	_initCanvasRelated: function() {
24227		
24228	},
24229	
24230	/**
24231	 * Load from model and draw room representation on canvas
24232	 * @param {Boolean} sameRoom Flag for calls for room load from undo/redo routines. Disables room auto positioning
24233	 */
24234	load: function(sameRoom) {
24235		this.clear();
24236		if (!sameRoom) {
24237			this.reset();
24238		}
24239	},
24240	
24241	/**
24242	 * Reset room
24243	 */
24244	reset: function() {
24245		this.zoom = 1;
24246		this._fullZoom = 1;
24247		this._offset = {x: 0, y: 0};
24248		this.showAll();
24249	},
24250
24251	/**
24252	 * Clear canvas, keep any objects with Grid and Camera type
24253	 */
24254	clear: function() {
24255		this._cloud.batch('erase');
24256	},
24257	
24258	/**
24259	 * Abstract function for cloud sorting
24260	 */
24261	sortCloud: function() {
24262	},
24263
24264	/**
24265	 * Load the given entities asynchronously to the cloud and exec callback
24266	 * when done.
24267	 * 
24268	 * @param {Array} data Cloud append arguments list
24269	 * @param {Function} callback Callback run when all items are populated
24270	 */
24271	populateCloud: function(data, callback) {
24272		var loadQueue = $({}),
24273			self = this,
24274			items, sets;
24275		
24276		$.map(data, function(args) {
24277			// split items to a sets
24278			items = args.shift().slice();
24279			sets = [];
24280			
24281			if (items.length !== 0) {
24282				do {
24283					sets.push(items.splice(0, 30));
24284				} while (items.length !== 0);
24285				
24286				$.map(sets, function(set) {
24287					loadQueue.queue(function(next) {
24288						window.setTimeout(function() {
24289							self._cloud.append.apply(self._cloud,
24290								[set].concat(args));
24291							next();
24292						}, 0);
24293					});
24294				});
24295			}
24296		});
24297		
24298		loadQueue.queue(function() {
24299			callback.apply(self);
24300		});
24301	},
24302	
24303	/**
24304	 * Pan all items on canvas
24305	 * 
24306	 * @param {Number} dx DeltaX
24307	 * @param {Number} dy DeltaY
24308	 * @param {Boolean} absolute Use DeltaX/DeltaY as absolute coords
24309	 */
24310	pan: function(dx, dy, absolute) {
24311		if (absolute) {
24312			this._offset.x = dx;
24313			this._offset.y = dy;
24314		} else {
24315			this._offset.x += dx;
24316			this._offset.y += dy;
24317		}
24318		
24319		this.canvas.pan(dx, dy, absolute);
24320	},
24321
24322	/**
24323	 * Scale all item on canvas
24324	 * 
24325	 * @param {Number} zoom
24326	 */
24327	scale: function(zoom, absolute) {
24328		this._fullZoom = (absolute ? 0 : this._fullZoom) + zoom;
24329		zoom = Math.min(Math.max(this._fullZoom, 0.05), 5);
24330
24331		if (this.zoom !== zoom) {
24332			// this.processing is used for preventing race conditions
24333			// on zoom using wheel
24334			if (!this.processing) {
24335				this.processing = true;
24336				this.zoom = zoom;
24337				this.canvas.scale(zoom);
24338				this._cloud.batch('scale', zoom);
24339				myDeco.body.trigger('view-' + this.$type + '-scaled');
24340				this.processing = false;
24341			}
24342		}
24343	},
24344
24345	/**
24346	 * Highlight off all items on canvas
24347	 */
24348	highlightOff: function() {
24349		this._cloud.batch('highlightOff');
24350	},
24351	
24352	/**
24353	 * Add camera stub
24354	 * @param {Camera}
24355	 */
24356	addCamera: function() {
24357	},
24358	
24359	/**
24360	 * Add component stub
24361	 * @param {Object} entity Component which has been added
24362	 */
24363	addShape: function() {
24364	},
24365	
24366	/**
24367	 * Add wall stub
24368	 * @param {Wall} wall
24369	 */
24370	addWall: function() {
24371	},
24372	
24373	/**
24374	 * Add corner stub
24375	 * @param {Corner}
24376	 */
24377	addCorner: function() {
24378	},
24379	
24380	/**
24381	 * Add component stub
24382	 * @param {Component} component Component which has been added
24383	 */
24384	addComponent: function() {
24385	},
24386
24387	/**
24388	 * Auto scale/pan current room to fit view area
24389	 */
24390	showAll: function() {
24391		this.zoom = 1;
24392		this._offset = {x: 0, y: 0};
24393		
24394		var parent = this.canvasElement.parent();
24395		this.canvasWidth = parent.innerWidth();
24396		this.canvasHeight = parent.innerHeight();
24397		this.canvas.setSize(this.canvasWidth, this.canvasHeight);
24398		
24399		delete this._canvasOffset;
24400	},
24401
24402	/**
24403	 * Show/hide decorate target buttons
24404	 * @param {String} state Type of targets to show (decor for now)
24405	 * @param {String} type Subtype of a targets (photo or all other)
24406	 */
24407	toggleDecorateTargets: function(state, type) {
24408		this._decorateTargets.hide();
24409		
24410		var filter = state === 'decor' ?
24411				(type === 'photo' ? '.scenery, .walls' : '.ceiling, .walls') :
24412				false,
24413			targets = filter ? this._decorateTargets.filter(filter) : false,
24414			positions,
24415			listener;
24416		
24417		if (targets) {
24418			targets.show();
24419		}
24420		
24421		// ipad mouseover workaround
24422		if (myDeco.browser.isMobileSafari) {
24423			listener = function(e) {
24424				$.each(positions, function() {
24425					var active = e.pageX >= this[0] && e.pageX <= this[1] &&
24426								 e.pageY >= this[2] && e.pageY <= this[3],
24427						dataActive = this[4].data('active');
24428					
24429					if (!active && dataActive) {
24430						this[4].data('active', false);
24431						this[4].mouseout();
24432					} else if (active && !dataActive) {
24433						this[4].data('active', true);
24434						this[4].mouseover();
24435					}
24436				});
24437			};
24438			
24439			if (targets) {
24440				positions = $.map(targets, function(target) {
24441					target = $(target).show();
24442					var pos = target.offset();
24443					
24444					return [[pos.left, pos.left + target.width(),
24445							pos.top, pos.top + target.height(),
24446							target]];
24447				});
24448				
24449				myDeco.body.bind('mousemove', listener);
24450			} else {
24451				myDeco.body.unbind('mousemove', listener);
24452			}
24453		}
24454	},
24455
24456	/**
24457	 * Get bounds of decorated targets on screeen (required for mousemove emulation in mobile safari)
24458	 * @returns {Array} Array decorated targets or null
24459	 */
24460	getDecorateTargets: function() {
24461		if (this.state) {
24462			return $.map(this._decorateTargets, function(target) {
24463				target = $(target);
24464				var offset = target.offset();
24465				
24466				return {
24467					x1: offset.left,
24468					y1: offset.top,
24469					x2: offset.left + target.width(),
24470					y2: offset.top + target.height(),
24471					target: target.eq(0)
24472				};
24473			});
24474		} else {
24475			return null;
24476		}
24477	},
24478
24479	/**
24480	 * Toggle view visibility state
24481	 * @param {Boolean} state View state
24482	 */
24483	toggle: function(state) {
24484		this._super(state);
24485		if (state) {
24486			// just load the same room and add listeners
24487			this.load(true);
24488			if (this._initialZoom === true) {
24489				this.showAll();
24490				this._initialZoom = false;
24491			}
24492		} else {
24493			this.clear();
24494		}
24495		this.processEvents(state);
24496	}
24497});
24498/*globals myDeco, $, FpeView, ObjectsLibrary, Grid, Shape, Wall, Corner, Window,
24499          Door, Component, Camera, window, buildThumbnailLink, Point, T */
24500/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
24501          sloppy: true, white: true */
24502/**
24503 * @fileOverview FPE Floor-view 
24504 */
24505
24506var FloorView = FpeView.extend({
24507	/**
24508	 * View type, should be static
24509	 * @type {String}
24510	 * @static
24511	 */
24512	$type: 'floor',
24513	
24514	/**
24515	 * Cursor disability flag
24516	 * @type {Boolean}
24517	 */
24518	disableCursors: false,
24519	
24520	/**
24521	 * Canvas bounds, limited to size in 20x20 metzers
24522	 * @type {Number}
24523	 */
24524	bounds: 20 * 100,
24525	
24526	/**
24527	 * Common drawings library
24528	 * @type {Object}
24529	 */
24530	library: false,
24531	
24532	/**
24533	 * Processing flag 
24534	 * @type {Boolean}
24535	 */
24536	processing: false,
24537	
24538	/**
24539	 * Line used for drawing walls
24540	 * @type {Object}
24541	 */
24542	_drawLine: false,
24543	
24544	/**
24545	 * Corner for visual representation of the destination point
24546	 * @type {Object}
24547	 */
24548	_drawCorner: false,
24549
24550	/**
24551	 * Points which drawn when setscale in progress
24552	 * @type {Array}
24553	 */
24554	_scalePoints: [],
24555	
24556	/**
24557	 * Measurer shown while drawing
24558	 * @type {Object}
24559	 */
24560	_drawMeasurer: false,
24561	
24562	/**
24563	 * Rotating arrow holder
24564	 * @type {Object}
24565	 */
24566	_rotateArrow: null,
24567
24568	/**
24569	 * Masking backgound data holder
24570	 * @type {Object}
24571	 */
24572	_background: {
24573		src: '',
24574		x: 0,
24575		y: 0,
24576		width: 0,
24577		height: 0,
24578		scale: 1
24579	},
24580
24581	/**
24582	 * Reference to view's Grid object
24583	 * @type {Object}
24584	 */
24585	_grid: null,
24586
24587	/**
24588	 * View init
24589	 * @param {Object} options
24590	 */
24591	init: function(options) {
24592		var self = this;
24593		
24594		this.measurers = [];
24595		
24596		this._rotablesObserver = options.controller.registerRotatables
24597			.bind(options.controller);
24598		
24599		this._super(options);
24600		
24601		// allow drawing of ruler and accept point only for ipad
24602		if (!myDeco.browser.isMobileSafari) {
24603			// drawing line
24604			this._drawLine = this.canvas.path({
24605					'stroke': '#000',
24606					'stroke-width': 2
24607				})
24608				.moveTo(0, 0)
24609				.lineTo(10, 10)
24610				.hide();
24611			
24612			// drawing corner
24613			this._drawCorner = this.canvas.circle(0, 0, 30).attr({
24614				'stroke': '#000',
24615				'fill': '#469CDA',
24616				'opacity': 0.5
24617			}).hide();
24618			
24619			myDeco.body.bind('backgroundChange', function(e, action, multiplier) {
24620				multiplier = multiplier || 1;
24621				switch (action) {
24622					case 'left':
24623						self._background.x -= multiplier;
24624						break;
24625					case 'up':
24626						self._background.y -= multiplier;
24627						break;
24628					case 'right':
24629						self._background.x += multiplier;
24630						break;
24631					case 'down':
24632						self._background.y += multiplier;
24633						break;
24634					case 'zoom-in':
24635						self._background.scale += 0.005 * multiplier;
24636						break;
24637					case 'zoom-out':
24638						self._background.scale -= 0.005 * multiplier;
24639						break;
24640				}
24641				self._grid.drawBackground();
24642			});
24643		}
24644		
24645		// remove possibility of drawing measurers
24646		if (window.location.hash.indexOf('norulers') !== -1) {
24647			this.settings.measurer = false;
24648		}
24649		
24650		// drawing measurer
24651		this._drawMeasurer = this.registerMeasurer([new Point(), new Point()]);
24652		
24653		this.sortCloud();
24654		
24655		myDeco.body
24656			.bind('operationEnd shapesIntersected', function(e, operation) {
24657				operation = operation ? operation[0] : false;
24658				if (!operation || operation === 'add' ||
24659						operation === 'remove') {
24660					self._cloud.findByField('type', 'Wall')
24661						.batch('updateEdges');
24662					self.sortCloud();
24663				}
24664			})
24665			.bind('debug', function(e, type, index) {
24666				var model = self._model[type + 's'][index],
24667					visual = self._cloud.findByField('_model', [model]).shift(),
24668					circle, bbox;
24669				
24670				if (visual) {
24671					bbox = visual.getBBox();
24672					circle = self.canvas.circle(bbox.cx, bbox.cy, 20)
24673						.attr({fill: '#f00', 'stroke-color': '#000'});
24674					window.setTimeout(function() {
24675						circle.remove();
24676					}, 1000);
24677				}
24678			});
24679	},
24680	
24681	/**
24682	 * Init canvas related stuff
24683	 */
24684	_initCanvasRelated: function() {
24685		this.library = ObjectsLibrary.init(this.canvas, this.settings.library);
24686		this._rotateArrow = this.library.get('objectRotate');
24687	},
24688	
24689	/**
24690	 * Load room to view
24691	 * @param {Boolean} sameRoom
24692	 */
24693	load: function(sameRoom) {
24694		this._super(sameRoom);
24695		var self = this,
24696			bg = this._background;
24697
24698		bg.src = this._model._metadata.image;
24699		this._grid = new Grid(this);
24700
24701		if (this._model._masking && bg.src) {
24702			if (!sameRoom) {
24703				bg.x = bg.y = bg.width = bg.height = 0;
24704				bg.scale = 1;
24705
24706				myDeco.tracking.trackingImage(bg.src, function() {
24707					var image = $(this),
24708						width = self.canvasWidth,
24709						height = self.canvasHeight,
24710						scale;
24711
24712					bg.width = image.width();
24713					bg.height = image.height();
24714
24715					scale = bg.width / bg.height;
24716					
24717					if (scale > width / height) {
24718						bg.scale = width / bg.width;
24719						height = width / scale;
24720					} else {
24721						bg.scale = height / bg.height;
24722						width = height * scale;
24723					}
24724
24725					bg.scale /= self.zoom;
24726
24727					bg.x = (self.canvasWidth / 2 - self._offset.x - width / 2) / self.zoom;
24728					bg.y = (self.canvasHeight / 2 - self._offset.y - height / 2) / self.zoom;
24729
24730					self._grid.drawBackground();
24731				});
24732			} else {
24733				this._grid.drawBackground();
24734			}
24735		}
24736
24737		this._cloud.push(this._grid);
24738
24739        var floor_items = [
24740			[[this._model.camera], Camera, this._rotablesObserver],
24741			[this._model.shapes, Shape, this._droppablesObserver],
24742			[this._model.walls, Wall, this._draggablesObserver],
24743			[this._model.corners, Corner, this._draggablesObserver],
24744			[this._model.windows, Window, this._draggablesObserver],
24745			[this._model.doors, Door, this._draggablesObserver],
24746			[this._model.components, Component, this._rotablesObserver] ];
24747
24748        if (myDeco.settings.WL && myDeco.settings.WL.HIDE_CAMERA) {
24749            floor_items.shift();
24750        };
24751		
24752		this.populateCloud(floor_items,
24753        $.proxy(function() {
24754			this.sortCloud();
24755			$.each(this._model.components, function(i, component) {
24756				if (component.level === -1 && component.type === 'component') {
24757		        		this.rotation[2] = this.rotation[2] - 180;
24758				}
24759			})
24760		}, this));
24761	},
24762	
24763	/**
24764	 * Add camera to view
24765	 * @param {Camera}
24766	 */
24767	addCamera: function(camera) {
24768		this._cloud.append([camera], Camera, this._rotablesObserver, '_model');
24769	},
24770	
24771	/**
24772	 * Add shape to view
24773	 * @param {Shape}
24774	 */
24775	addShape: function(shape) {
24776		this._cloud.append([shape], Shape, this._droppablesObserver, '_model');
24777	},
24778	
24779	/**
24780	 * Add wall to view
24781	 * @param {Wall}
24782	 */
24783	addWall: function(wall) {
24784		this._cloud.append([wall], Wall, this._draggablesObserver, '_model');
24785	},
24786	
24787	/**
24788	 * Add corner to view
24789	 */
24790	addCorner: function(corner) {
24791		this._cloud.append([corner], Corner, this._draggablesObserver,
24792			'_model');
24793	},
24794	
24795	/**
24796	 * Add component to cloud and canvas
24797	 * @param {Component} component
24798	 */
24799	addComponent: function(entity) {
24800		this._cloud.append([entity], window[entity.type.capitalize()],
24801			this._rotablesObserver);
24802	},
24803
24804	/**
24805	 * Calculate zoom for fit room to canvas and pan/scale all items
24806	 */
24807	showAll: function() {
24808		this._super();
24809
24810		var settings = this.settings.offset || {},
24811			top = settings.top || 0,
24812			bottom = settings.bottom || 0,
24813			bb = this._model.getBBox(),
24814			wzoom = (bb.width || 500) / (this.canvasWidth),
24815			hzoom = (bb.depth || 500) / (this.canvasHeight - top - bottom),
24816			// set some space around model
24817			zoom = 0.80 / Math.max(wzoom, hzoom),
24818			cx = this.canvasWidth / 2,
24819			cy = (this.canvasHeight + top - bottom) / 2;
24820
24821		this.pan(cx - bb.cx * zoom, cy + bb.cy * zoom, true);
24822		this.scale(zoom, true);
24823	},
24824	
24825	/**
24826	 * Scale view
24827	 * @param {Number} zoom
24828	 * @param {Boolean} absolute
24829	 */
24830	scale: function(zoom, absolute) {
24831		this._super(zoom, absolute);
24832		
24833		var abox = this._rotateArrow.getBBox(),
24834			ax = abox.x + abox.width / 2,
24835			ay = abox.y + abox.height / 2;
24836			
24837		zoom = Math.max(this.settings.rotateArrow.scale, 
24838			this.settings.rotateArrow.scale /
24839				(this.zoom * this.settings.rotateArrow.zoom));
24840		
24841		this._rotateArrow.scale(zoom, zoom, ax, ay);
24842	},
24843	
24844	/**
24845	 * Generate draggable HTML element for display while dragging
24846	 * 
24847	 * @param {Object} component Display component
24848	 * @param {String} kind Kind of entity: furniture, decor, build element
24849	 */
24850	generateElementForDrag: function(component, kind) {
24851		var result,
24852			side = 'front',
24853			dimension = 'height',
24854			w, h;
24855			
24856		switch (kind) {
24857			case 'furniture':
24858			case 'element':
24859				if (kind === 'furniture' && component.level !== -1) {
24860					side = 'top';
24861					dimension = 'depth';
24862				}
24863			
24864				if (component.type !== 'room') {
24865					w = Math.round(component.width * this.zoom);
24866					h = Math.round(component[dimension] * this.zoom);
24867					result = {	
24868						'ox': Math.round(w * 0.5),
24869						'oy': Math.round(h * 0.5),
24870						'visual': $('<img src="' + buildThumbnailLink(
24871							component.componentData['3d']['render_' + side]) +
24872							'" style="width:' + w + 'px;height:' + h +
24873							'px;display:none" class="floor" />')
24874					};
24875				} else {
24876					result = {	
24877						'ox': Math.round(component.width * this.zoom * 0.5),
24878						'oy': Math.round(component.depth * this.zoom * 0.5),
24879						'visual': $('<div class="floor ' + component.subtype +
24880									'" style="display: none">' +
24881										'<span class="room-lt"></span>' +
24882										'<span class="room-rt"></span>' +
24883										'<span class="room-lb"></span>' +
24884										'<span class="room-rb"></span>' +
24885									'</div>')
24886									.width(component.width * this.zoom)
24887									.height(component.depth * this.zoom)
24888						};
24889				}
24890				break;
24891			case 'decor':
24892				result = {	
24893					'ox': -10,
24894					'oy': -10,
24895					'visual': false
24896				};
24897				break;	
24898		}
24899		
24900		return result;
24901	},
24902	
24903	/**
24904	 * Sort cloud and reposition components by Z index on view whenever the scene is changed, 
24905	 * for example when a component is added to the scene.
24906	 */
24907	sortCloud: function() {
24908		var map = {'Grid': 0, 'Shape': 1, 'Wall': 2, 'Corner': 3, 'Window': 4,
24909				   'Door': 5, 'Component': 6, 'Camera': 7},
24910				i, ilen, drawFlag = true;
24911				
24912		
24913		this._cloud.sort(function(a, b) {
24914			var ret = 0;
24915			if (a.type === 'Component' && b.type === 'Component') {
24916				if (a._model.level !== b._model.level) {
24917					if (a._model.level < 0 || b._model.level < 0) {
24918						ret = b._model.level - a._model.level
24919					} else {
24920						ret = a._model.level - b._model.level;
24921					}
24922				} else {
24923					ret = a._model._surface[0].z - b._model._surface[0].z;
24924				}
24925			} else {
24926				ret = map[a.type] - map[b.type];
24927			}
24928			return ret;
24929		});
24930		
24931		for (i = 0, ilen = this._cloud.length; i < ilen; i++) {
24932			if (drawFlag && this._drawLine && this._drawCorner &&
24933					this._controller._operation.type === 'draw' &&
24934					this._cloud[i].type === 'Corner') {
24935				// keep drawLine under corners
24936				drawFlag = false;
24937				this._drawLine.toFront();
24938				this._drawCorner.toFront();
24939			}
24940			this._cloud[i].element.toFront();
24941		}
24942			
24943		// active measurers
24944		for (i = 0, ilen = this.measurers.length; i < ilen; i++) {
24945			if (this.measurers[i].measurer) {
24946				this.measurers[i].measurer.rulers.toFront();
24947				this.measurers[i].measurer.text.toFront();
24948			}
24949		}
24950		
24951		// no walls
24952		if (this._drawLine && this._drawCorner && drawFlag) {
24953			this._drawLine.toFront();
24954			this._drawCorner.toFront();
24955		}
24956		
24957		if (this._grid.image) {
24958			this._grid.image.toBack();
24959		}
24960		this._rotateArrow.toFront();
24961	},
24962	
24963	/**
24964	 * Calculate real model coordinates from given screen coordinates
24965	 * @param {Float} screenX X screen coord
24966	 * @param {Float} screenY Y screen coord
24967	 * @param {Float} sceneZ height of the plane, parallel to the floor, on which coords should be projected (used for isometry)
24968	 * @param {Wall} wall Parent wall to which screen coordinates should be projected (used for isometry)
24969	 * @returns {Object} in format {x: ..., y: ...}
24970	 */
24971	toModelPosition: function(screenX, screenY) {
24972		var offset = this.getCanvasOffset();
24973		screenX = screenX || 0;
24974		screenY = screenY || 0;
24975		
24976		return new Point((screenX - offset.left - this._offset.x) / this.zoom,
24977			(- (screenY - offset.top) + this._offset.y) / this.zoom);
24978	},
24979	
24980	/**
24981	 * Toggle view to maximize/minimize state
24982	 * @param {Boolean} state View state, will toggle if not specified
24983	 * 
24984	 * If state is not specified, will maximize if viewType eq this one and
24985	 * minimize if not.
24986	 * Will hide search by dimension facet on switch 
24987	 */
24988	toggle: function(state) {
24989		this._super(state);
24990		if (!this.state) {
24991			myDeco.body.trigger('controller-facet-toggle',
24992				[false, 'dimension', false]);
24993		}
24994	},
24995	
24996	/**
24997	 * Register measurer in the view stack
24998	 * @param {Array} corners Array of corners
24999	 * @param {Number} distance Distance from baseline to measurer
25000	 * @returns {Array}
25001	 */
25002	registerMeasurer: function(corners, distance) {
25003		var measurer = corners.slice();
25004			
25005		if (this.settings.measurer) {
25006			measurer._view = this;
25007			measurer._distance = typeof distance !== 'undefined' ? distance : 17;
25008			measurer.update = this.updateMeasurer;
25009			this.measurers.push(measurer);
25010		} else {
25011			measurer.update = $.noop;
25012		}
25013		
25014		return measurer;
25015	},
25016	
25017	/**
25018	 * Remove and pop measurer from view stack
25019	 * @param {Array} measurer 
25020	 */
25021	deregisterMeasurer: function(measurer) {
25022		measurer.update(false);
25023		$.extract(measurer, this.measurers);
25024	},
25025	
25026	/**
25027	 * Draw/remove/update measurer method
25028	 * @param {Boolean} state Show/hide measurer. In case of undefined and
25029	 *                        enabled measurer just update its position
25030	 */
25031	updateMeasurer: function(state) {
25032		var style = this._view.settings.measurer,
25033			distance, fontSize, rulersStyle,
25034			m0, m1, text, angle, textAngle, l0, p0, l1, p1, tp;
25035			
25036		if ((typeof state === 'undefined' && this.measurer) || state) {
25037			distance = this._distance + style.distance / this._view.zoom;
25038			fontSize = parseInt(+(style.text['font-size'] || 12) / 
25039				(this._view.canvas.raphael.type === 'SVG' ?
25040					this._view.zoom : 1), 10) + 'px';
25041			
25042			// rulers thickness
25043			rulersStyle = $.extend({}, style.rulers, {
25044				'stroke-width': (style.rulers['stroke-width'] || 1) / 
25045					(this._view.canvas.raphael.type === 'SVG' ?
25046						this._view.zoom : 1)
25047			});
25048			
25049			m0 = this[0];
25050			m1 = this[1];
25051			text = [m0.distance(m1)].toUnits();
25052			angle = T.atan2(this[1].y - this[0].y, this[1].x - this[0].x);
25053			textAngle = (angle + 360) % 360;
25054			
25055			l0 = m0.clone().translate(0, distance).rotate(m0, angle);
25056			p0 = l0.clone().translate(0, distance * 0.2).rotate(l0, angle);
25057			
25058			l1 = m1.clone().translate(0, distance).rotate(m1, angle);
25059			p1 = l1.clone().translate(0, distance * 0.2).rotate(l1, angle);
25060			
25061			tp = new Point((p0.x + p1.x) / 2, (p0.y + p1.y) / 2, 0);
25062			
25063			// correct text rotation for better reading
25064			textAngle = textAngle >= 91 && textAngle <= 271 ?
25065				textAngle - 180 : textAngle;
25066				
25067			if (!this.measurer) {
25068				this.measurer = {
25069					text: this._view.canvas.text(tp.x, -tp.y, text),
25070					rulers: this._view.canvas.set().push(
25071						this._view.canvas.path(rulersStyle)
25072							.moveTo(l0.x, -l0.y).lineTo(l1.x, -l1.y),
25073						this._view.canvas.path(rulersStyle)
25074							.moveTo(m0.x, -m0.y).lineTo(p0.x, -p0.y),
25075						this._view.canvas.path(rulersStyle)
25076							.moveTo(m1.x, -m1.y).lineTo(p1.x, -p1.y)
25077					)
25078				};
25079			}
25080			
25081			// reposition measurer and update text
25082			this.measurer.rulers[0].update([l0, l1]);
25083			this.measurer.rulers[1].update([m0, p0]);
25084			this.measurer.rulers[2].update([m1, p1]);
25085			this.measurer.rulers.attr(rulersStyle);
25086			
25087			if (this.measurer.text.attr('font-size') !== fontSize) {
25088				this.measurer.text.attr('font-size', fontSize);
25089			}
25090			
25091			this.measurer.text.attr($.extend({}, style.text, {
25092				text: text,
25093				x: tp.x,
25094				y: -tp.y
25095			})).rotate(-textAngle, true);
25096			
25097			if (state) {
25098				this.measurer.text.toFront().show();
25099				this.measurer.rulers.toFront().show();
25100			}
25101		} else if (this.measurer) {
25102			this.measurer.text.remove();
25103			this.measurer.rulers.remove();
25104			this.measurer = null;
25105		}
25106	},
25107	
25108	/**
25109	 * Updates rotation arrow position and angle
25110	 */
25111	updateRotationArrow: function() {
25112		var distance, entity, selected,
25113			abox, ax, ay, angle = 0;
25114		
25115		if (this._controller._viewActive && this._model.rotationAvailable) {
25116			abox = this._rotateArrow.getBBox();
25117			ax = abox.x + abox.width / 2;
25118			ay = abox.y + abox.height / 2;
25119			distance = this.settings.rotateArrow.distance;
25120			selected = this._model.selected();
25121			
25122			if (selected.length === 1) {
25123				entity = selected[0];
25124				entity = this._cloud.findByField('_model', [entity]).shift();
25125				distance = entity._settings.arrowDistance ||
25126					this.settings.rotateArrow.distance;
25127			}
25128			
25129			switch (this._controller._operation.type) {
25130				case 'rotate':
25131					this._rotateArrow.rotate(this._controller._rotationAngle,
25132							this._model._selectionCenter.x,
25133							-this._model._selectionCenter.y);
25134					break;
25135				case 'drag':
25136					this._rotateArrow
25137						.translate(this._model._selectionCenter.x - ax,
25138							-this._model._selectionCenter.y - ay + distance);
25139					break;
25140				default:
25141					this._rotateArrow
25142						.rotate(0, ax, ay)
25143						.translate(this._model._selectionCenter.x - ax,
25144							-this._model._selectionCenter.y - ay + distance)
25145						.rotate(this._controller._rotationAngle,
25146							this._model._selectionCenter.x,
25147							-this._model._selectionCenter.y);
25148			}
25149		}
25150	},
25151
25152	/**
25153	 * Draw point for setScale method
25154	 * @param {Point} point
25155	 */
25156	drawScalePoint: function(point) {
25157		this._scalePoints.push(this.canvas.circle(point.x, -point.y, 10).attr({
25158			'stroke': '#f00'
25159		}).toFront());
25160	}
25161});
25162
25163// register view in dispatched views list
25164myDeco.App.views.push(FloorView);
25165/*globals $, myDeco, AppController, document, KEY, xor, Point */
25166/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
25167          sloppy: true, white: true */
25168/**
25169 * @fileOverview FPE abstract room controller
25170 */
25171
25172var FpeController = AppController.extend({
25173	/**
25174	 * Controller type
25175	 * @type {String}
25176	 * @static
25177	 */
25178	$type: 'base',
25179	
25180	/**
25181	 * Flag which representing that cursor is inside of associated view and
25182	 * only over it (since other views can flying over this view)
25183	 * @type {Boolean}
25184	 * @private
25185	 */
25186	_viewActive: false,
25187	
25188	/**
25189	 * Dict of currently active snaps
25190	 * @type {Object}
25191	 */
25192	_snaps: {},
25193	
25194	/**
25195	 * Init function
25196	 * @param {Object} model FPE model
25197	 */
25198	init: function(model, settings) {
25199		this._super(model, settings);
25200		this._model.operationReset();
25201		
25202		myDeco.body
25203			.bind('entity-drag-started', this.entityDrag.bind(this))
25204			.mousemove(this.globalMouseMove.bind(this))
25205			.bind('mouseup', this.entityDrop.bind(this))
25206			.bind('actionRequest', this.keyboardAction.bind(this));
25207	},
25208	
25209	/**
25210	 * Entity (room shape/door/window/component/material) drop observer
25211	 * @param {Event} e Mouseup event
25212	 */
25213	entityDrop: function(e) {
25214		if (this._viewActive && this._operation.type === 'add') {
25215			e.preventDefault();
25216			
25217			var entity, operationName, decorateTarget, i, z,
25218				pos = this._view.toModelPosition(e.pageX, e.pageY, 0);
25219			
25220			operationName = 'add';
25221			entity = $.cloneObject(this._operation.data.entity);
25222			
25223			if (this._operation.data.parent) {
25224				z = this._model.ceilingHeight / 2;
25225			}
25226			
25227			// fix for ipad which touchend doesn't support real target
25228			if (myDeco.browser.isMobileSafari &&
25229					this._operation.data.kind === 'decor') {
25230				decorateTarget = $.grep(this._view._decorateTargets, function(element) {
25231					element = $(element);
25232					var pos = element.offset();
25233					return  e.pageX >= pos.left &&
25234							e.pageX <= pos.left + element.width() &&
25235							e.pageY >= pos.top &&
25236							e.pageY <= pos.top + element.height() &&
25237							element.is(':visible');
25238				}).shift();
25239				
25240				if (decorateTarget) {
25241					decorateTarget = $(decorateTarget);
25242				}
25243			}
25244			
25245			switch (this._operation.data.kind) {
25246				case 'furniture':
25247					delete entity.ID;
25248					
25249					// so, if this component has parent
25250					if (this._operation.data.parent) {
25251						// rotate component over oX axis
25252						entity.rotation[0] = -90;
25253					
25254						// blur selected wall
25255						this._operation.data.parent.highlightOff();
25256					
25257						// set parent for it
25258						entity.parent = this._operation.data.parent;
25259					}
25260					
25261					// add on-wall components only if parent is defined
25262					// all other components will add also
25263					if (xor(this._operation.data.parent, entity.level !== -1)) {
25264						entity = this._model.addComponent(entity, pos.x, pos.y, z);
25265					}
25266					break;
25267				case 'decor':
25268					if (this._operation.data.type === 'photo') {
25269						if (this._operation.data.target) {
25270							if (this._operation.data.target.type) {
25271								this._model.assignPhoto(this._operation.data.target, this._operation.data.entity);
25272								operationName = 'material';
25273							} else {
25274								decorateTarget = $(this._operation.data.target);
25275							}
25276						} else {
25277							decorateTarget = decorateTarget || $(e.target);
25278						}
25279							
25280						if (decorateTarget && decorateTarget.hasClass('decorate-target')) {
25281							if (decorateTarget.hasClass('walls')) {
25282								for (i = this._model.walls.length - 1; i >= 0; i = i - 1) {
25283									this._model.assignPhoto(this._model.walls[i], entity);
25284								}
25285							} else {
25286								myDeco.App.model.assignEnvironment('image', entity.texture);
25287							}
25288							operationName = 'material';
25289						}
25290					} else {
25291						if (this._operation.data.target &&
25292								this._checkDecorAcceptance(this._operation.data.target)) {
25293							if (this._operation.data.target.type === 'shape') {
25294								this._model.assignMaterial(this._operation.data.target, entity, 'floor');
25295							} else if (this._operation.data.target.type) {
25296								this._model.assignMaterial(this._operation.data.target, entity);
25297							} else {
25298								decorateTarget = $(this._operation.data.target);
25299							}
25300							
25301							operationName = 'material';
25302						} else {
25303							decorateTarget = decorateTarget || $(e.target);
25304						}
25305						
25306						if (decorateTarget && decorateTarget.hasClass('decorate-target')) {
25307							if (decorateTarget.hasClass('walls')) {
25308								for (i = this._model.walls.length - 1; i >= 0; i = i - 1) {
25309									this._model.assignMaterial(this._model.walls[i], entity, 'wall');
25310								}
25311							} else {
25312								for (i = this._model.shapes.length - 1; i >= 0; i = i - 1) {
25313									this._model.assignMaterial(this._model.shapes[i], entity, 'ceiling');
25314								}
25315							}
25316							
25317							operationName = 'material';
25318						}
25319					}
25320					break;
25321				case 'element':
25322					switch (entity.type) {
25323						case 'room':
25324							entity = this._model.addRoom(entity.subtype,
25325									pos.x,
25326									pos.y,
25327									entity.width,
25328									entity.depth);
25329							break;
25330						case 'ArchitecturalComponent': 
25331							delete entity.ID;
25332							
25333							// so, if this component has parent
25334							if (this._operation.data.parent) {
25335								// rotate component over oX axis
25336								entity.rotation[0] = -90;
25337							
25338								// blur selected wall
25339								this._operation.data.parent.highlightOff();
25340							
25341								// set parent for it
25342								entity.parent = this._operation.data.parent;
25343								
25344								entity = this._model.addComponent(entity, pos.x, pos.y, z);
25345							} else {
25346								operationName = '';
25347							}
25348						
25349							break;
25350					}
25351					break;
25352			}
25353			
25354			if (operationName) {
25355				this._model.operationEnd([operationName]);
25356			} else {
25357				this._model.operationReset();
25358			}
25359			
25360			this._view.toggleDecorateTargets(false);
25361		
25362		}
25363	},
25364	
25365	/**
25366	 * Global mouse move observer for tracking nearest walls for onwall components
25367	 * @param {Event} e Mousemove event
25368	 */
25369	globalMouseMove: function(e) {
25370		var wall, i, len, point, currTarget;
25371		
25372		if (this.state &&
25373				this._viewActive &&
25374				typeof this._operation.data !== 'undefined') {
25375			if (this._operation.data.addingComponentOnWall) {
25376				wall = this.findNearestWall(e, false, this._operation.data.entity.width);
25377			
25378				if (wall && wall !== this._operation.data.parent) {
25379					if (this._operation.data.parent) {
25380						this._operation.data.parent.highlightOff();
25381					}
25382				
25383					this._operation.data.parent = wall;
25384					wall.highlightOn();
25385				}
25386			} 
25387			// Special case for iPad, since there are impossible to track mouseover events in some cases
25388			else if (myDeco.browser.isMobileSafari && 
25389					this._operation.type === 'add' &&
25390					this._operation.data.kind === 'decor') {
25391				
25392				// Searching for decorate targets (buttons on canvas) under the finger
25393				if (this._operation.data.targets) {
25394					for (i = 0, len = this._operation.data.targets.length; i < len; i = i + 1) {
25395						if (e.pageX >= this._operation.data.targets[i].x1 &&
25396							e.pageX <= this._operation.data.targets[i].x2 &&
25397							e.pageY >= this._operation.data.targets[i].y1 &&
25398							e.pageY <= this._operation.data.targets[i].y2) {
25399							
25400							currTarget = this._operation.data.targets[i].target;
25401							break;
25402						}
25403					}
25404				}
25405				
25406				// Searching for wall nearest to finger to decorate
25407				if (typeof currTarget === 'undefined') {
25408					point = this._view.toModelPosition(e.pageX, e.pageY);
25409					currTarget = this._model.findNearestWall(point, 22);
25410				}
25411				
25412				// Searching for floor under the finger to decorate
25413				if (typeof currTarget === 'undefined' && this._operation.data.type !== 'photo') {
25414					for (i = 0, len = this._model.shapes.length; i < len; i = i + 1) {
25415						if (point.inPoly(this._model.shapes[i])) {
25416							currTarget = this._model.shapes[i];
25417							break;
25418						}
25419					}
25420				}
25421				
25422				if (currTarget !== this._operation.data.target) {
25423					if (this._operation.data.target && this._operation.data.target.highlightOff) {
25424						this._operation.data.target.highlightOff();
25425					}
25426					
25427					this._operation.data.target = currTarget;
25428					if (currTarget) {
25429						if (currTarget.highlightOn) {
25430							currTarget.highlightOn();
25431						}
25432						
25433						this._operation.data.visual.addClass('yes').removeClass('no');
25434					} else {
25435						this._operation.data.visual.addClass('no').removeClass('yes');
25436					}
25437				}
25438			}
25439		}
25440	},
25441	
25442	/**
25443	 * Register view
25444	 * @param {Object} view View associated to controller
25445	 */
25446	registerView: function(view) {
25447		var self = this;
25448		this._super(view);
25449		
25450		this.toggle(this._view.viewHolder.is(':visible'), this.$type);
25451		
25452		this._view._decorateTargets
25453			.mouseup(this.entityDrop.bind(this))
25454			.click(function(e) {
25455				e.preventDefault();
25456			})
25457			.bind('mouseover mouseout', function(e) {
25458				if (self._operation.type === 'add' &&
25459						(self._operation.data.kind === 'decor')) {
25460					if (e.type === 'mouseover') {
25461						self._operation.data.visual.removeClass('no').addClass('yes');
25462					} else {
25463						self._operation.data.visual.removeClass('yes').addClass('no');
25464					}
25465				}
25466			});
25467		
25468		this._view.viewHolder
25469			.bind('canvasIn', this.canvasIn.bind(this))
25470			.bind('canvasOut', this.canvasOut.bind(this));
25471		
25472		// register observers
25473		this._view.canvasElement
25474			.mousedown(this.mouseDown.bind(this))
25475			.mousewheel(this.wheelScroll.bind(this))
25476			.mousemove(this.mouseMove.bind(this))
25477			.mouseup(this.mouseUp.bind(this))
25478			.bind('gesturestart', function() {
25479					self._operation = {
25480						type: 'scale',
25481						data: {
25482							zoom: self._view.zoom
25483						}
25484					};
25485			})
25486			.bind('gesturechange', function(e) {
25487				if (self._operation.type === 'scale') {
25488					self._view.scale(self._operation.data.zoom * e.scale, true);
25489				}
25490			})
25491			.bind('gestureend', function() {
25492				self._model.operationReset();
25493			});
25494		
25495		myDeco.body
25496			.bind('setscaleStart', this.setscaleStart.bind(this))
25497			.bind('drawStart', this.drawStart.bind(this))
25498			.bind('dblclick', this.doubleClick.bind(this));
25499	},
25500	
25501	/**
25502	 * Check for locked visual entity
25503	 * @param {Object} visual Visual to check
25504	 * @param {Boolean} triggerEvent Trigger event on locked item
25505	 * @returns {Boolean}
25506	 */
25507	isLocked: function(visual, triggerEvent) {
25508		var locked = this._model.isLocked(visual._model);
25509		if (locked && triggerEvent) {
25510			myDeco.body.trigger('locked-move', [visual._model]);
25511		}
25512		return locked;
25513	},
25514
25515	/**
25516	 * Shorthand for registering draggable elements
25517	 * @param {Object} visual Visual element to register
25518	 * @returns {Object} input visual entity
25519	 */
25520	registerDraggables: function(visual) {
25521		var self = this;
25522		// we want to drag only visual base
25523		visual.getBase()
25524			.mouseover(function(e) {
25525				self.highlightStart(e, visual);
25526			})
25527			.mouseout(function(e) {
25528				self.highlightEnd(e, visual);
25529			});
25530		visual.element
25531			.mousedown(function(e) {
25532				if (!self.isLocked(visual, true)) {
25533					self.dragStart(e, visual);
25534				} else {
25535					e.preventDefault();
25536				}
25537			}, true)
25538			.click(function(e) {
25539				e.preventDefault();
25540			})
25541			.dblclick(function(e) {
25542				e.preventDefault();
25543				self.doubleClick(e, visual);
25544			});
25545		return visual;
25546	},
25547
25548	/**
25549	 * Shorthand for registering droppable elements
25550	 * @param {Object} visual Visual element to register
25551	 * @returns {Object} input visual element
25552	 */
25553	registerDroppables: function(visual) {
25554		var self = this;
25555		visual.element
25556			.mouseover(function(e) {
25557				self.activateDroppable(e, visual);
25558			})
25559			.mouseout(function(e) {
25560				self.deactivateDroppable(e, visual);
25561			})
25562			.mousedown(function(e) {
25563				self.mouseDown(e, visual);
25564			}, true);
25565		return visual;
25566	},
25567	
25568	/**
25569	 * Callback for resetting current operation
25570	 */
25571	operationReset: function() {
25572		if (this._view) {
25573			switch (this._operation.type) {
25574				case 'drag':
25575				case 'pan':
25576					this._view.cursor('move');
25577					break;
25578					
25579				case 'draw':
25580					$('.draggable, #sidebar').fadeIn();
25581					this._model.unselect();
25582					this._view._cloud.findByField('type', 'Component')
25583						.batch('toggle', true);
25584					
25585					// find all corners which do not connected to any wall or shape
25586					// and remove it
25587					$.each(this._model.corners, function() {
25588						if (this.getWalls().length === 0 &&
25589							this.getShapes().length === 0) {
25590								this.remove();
25591							}
25592					});
25593					myDeco.body.trigger('status');
25594					break;
25595
25596				case 'setscale':
25597					myDeco.body.trigger('status');
25598					break;
25599
25600				default:
25601					this._view.cursor('default');
25602				
25603			}
25604			
25605			this._view.toggleDecorateTargets(false);
25606			
25607			if (myDeco.browser.isMobileSafari) {
25608				this._view.canvas.safari();
25609			}
25610		}
25611		
25612		this._operation = {type: '', data: {}};
25613		this._snaps = {};
25614		myDeco.body.trigger('snappingOff');
25615	},
25616	
25617	/**
25618	 * Sort cloud on each operation
25619	 * @param {Event} e
25620	 * @param {Array} data
25621	 */
25622	operationEnd: function(e, data) {
25623		if (data && data[0] === 'add') {
25624			this._view.sortCloud();
25625		}
25626		return this._super(e, data);
25627	},
25628	
25629	/**
25630	 * Event listener for pan elements
25631	 * @param {Event} e Event
25632	 * @param {Object} visual Visual part of entity which started pan
25633	 */
25634	mouseDown: function(e, visual) {
25635		if (e.isDefaultPrevented()) {
25636			return;
25637		}
25638		
25639		e.preventDefault();
25640		
25641		this._operation = {
25642			type: 'pan',
25643			data: {
25644				timestamp: +(new Date()),
25645				timeDifference: myDeco.browser.isIE ? 300 : 150,
25646				x: e.pageX,
25647				y: e.pageY,
25648				visual: visual
25649			}
25650		};
25651	},
25652
25653	/**
25654	 * Mouse move event listener
25655	 * @param {Event} e Mousemove event
25656	 */
25657	mouseMove: function(e) {
25658		if (this._operation.type === 'pan') {
25659			this._view.pan((e.pageX - this._operation.data.x), (e.pageY - this._operation.data.y));
25660			this._operation.data.x = e.pageX;
25661			this._operation.data.y = e.pageY;
25662		}
25663	},
25664
25665	/**
25666	 * Mouse up button listener
25667	 * @param {Event} e Mouseup event
25668	 */
25669	mouseUp: function(e) {
25670		var shortClick = false;
25671		// Add operation will be processed by body mouseUp watcher
25672		if (this.state && this._operation.type !== 'add') {
25673			e.preventDefault();
25674			
25675			if (this._operation.type) {
25676				if (this._operation.type === 'pan' ||
25677					this._operation.type === 'drag') {
25678				
25679					// Time difference between mousedown and up was too short
25680					// so we can call it click
25681					shortClick = +(new Date()) -
25682						this._operation.data.timestamp <=
25683							this._operation.data.timeDifference;
25684					
25685					if (shortClick) {
25686						this._model.unselect();
25687						if (this._operation.data.visual) {
25688							this.select(this._operation.data.visual._model);
25689							myDeco.body.trigger('showInfo', [this._operation.data.visual._model]);
25690						}
25691					}
25692				}
25693				
25694				if (this._operation.type === 'pan' || shortClick) {
25695					this._model.operationReset();
25696				} else {
25697					this._model.operationEnd([this._operation.type]);
25698				}
25699			}
25700		}
25701	},
25702
25703	/**
25704	 * Visual element highlighting observer
25705	 * @param {Event} e Mouseover event
25706	 * @param {Object} visual Visual element to highlight
25707	 */
25708	highlightStart: function(e, visual) {
25709		this._view.cursor('default');
25710		visual._model.highlightOn();
25711		myDeco.body.trigger('highlightOn', [e, visual]);
25712	},
25713
25714	/**
25715	 * Visual element dishighliting observer
25716	 * @param {Event} e Mouseout event
25717	 * @param {Object} visual Visual element to dishighlight
25718	 */
25719	highlightEnd: function(e, visual) {
25720		visual._model.highlightOff();
25721	},
25722
25723	/**
25724	 * Visual element drag observer
25725	 * @param {Event} e Mousedown event
25726	 * @param {Object} entity operating on
25727	 */
25728	dragStart: function(e, visual) {
25729		if (e.isDefaultPrevented()) {
25730			return;
25731		}
25732		e.preventDefault();
25733		
25734		if (this._operation.type) {
25735			return;
25736		}
25737		
25738		if (this.select(visual._model, e.shiftKey)) {
25739			var selected = this._model.selected();
25740			
25741			this._operation = {
25742				type: 'drag',
25743				data: {
25744					timestamp: +(new Date()),
25745					timeDifference: myDeco.browser.isIE ? 300 : 150,
25746					x: e.pageX,
25747					y: e.pageY,
25748					// custom data storage, used in isometry controller at least
25749					d: {},
25750					// set up parent only for on-wall component
25751					parent: selected.length === 1 && !selected[0].category ?
25752							selected[0].parent : undefined,
25753					visual: visual
25754				}
25755			};
25756			
25757			this._model.operationStart(this._operation.type);
25758		}
25759	},
25760	
25761	/**
25762	 * Start draw function
25763	 * @param {Event} e
25764	 */
25765	drawStart: function(e) {
25766	},
25767	
25768	/**
25769	 * Start set scale function
25770	 * @param {Event} e
25771	 */
25772	setscaleStart: function(e) {
25773	},
25774
25775	/**
25776	 * Global document observer, used for misc keyboard operations
25777	 * @param {Event} e
25778	 * @param {Array} action
25779	 */
25780	keyboardAction: function(e, action) {
25781		var entity;
25782		
25783		// don't want to serve not fullscreen windows
25784		if (!this._view || !this.state) {
25785			return;
25786		}
25787		if (!this._operation || !this._operation.type ||
25788				action[0] === 'reset') {
25789		
25790			switch(action[0]) {
25791				case 'remove':
25792    					// delete some element
25793					if (!$.any(this._model.selected(),
25794							function(element) {
25795								return ['component', 'window', 'door', 'wall', 'shape']
25796									.indexOf(element.type) === -1;
25797							})
25798						) {
25799                        // action[2] is always false for the 'del' key but can be manually set
25800						this._model.remove(action[2]); 
25801					}
25802					break;
25803				case 'move':
25804					// move components
25805					this.moveByKey(action);
25806					break;
25807				case 'rotate':
25808					this.rotateByKey(action);
25809					break;
25810				case 'redo':
25811					this._model.redo();
25812					break;
25813				case 'undo':
25814					this._model.undo();
25815					break;
25816				case 'duplicate':
25817					this._model.duplicate();
25818					break;
25819				case 'split':
25820					entity = $.grep(this._model.selected(), function(entity) {
25821						return entity.type === 'wall';
25822					});
25823					if (entity.length !== 0) {
25824						this._model.splitWall(entity[0]);
25825						this._model.operationEnd(['add']);
25826						this._model.unselect();
25827					}
25828					break;
25829				case 'find':
25830					$('#search-query').focus();
25831					break;
25832				case 'draw':
25833					this.drawStart();
25834					break;
25835				case 'setscale':
25836					this.setscaleStart();
25837					break;
25838				case 'info':
25839					// show only for the first selected component
25840					if (this._model.selected().length === 1) {
25841						myDeco.body.trigger('showInfo', [this._model.selected(true)]);
25842					}
25843					break;
25844				case 'reset':
25845					this._model.operationReset();
25846					break;
25847				case 'debug':
25848                    alert('user_id:' + myDeco.userid + ', room_id:' + myDeco.App.model._room_id);
25849					break;
25850			}
25851		}
25852	},
25853	
25854	/**
25855	 * Move items by keypress
25856	 * @param {Object} action
25857	 */
25858	moveByKey: function(action) {
25859		var shift = this._view.settings.keyboardShiftValue || 10,
25860			dx = -1 * (action[1] === KEY.Left) + (action[1] === KEY.Right),
25861			dy = -1 * (action[1] === KEY.Down) + (action[1] === KEY.Up);
25862		
25863		if (action[2]) {
25864			shift /= 10;
25865		}
25866		
25867		this._model.move(dx * shift, dy * shift);
25868		this._model.operationEnd(['drag']);
25869	},
25870	
25871	/**
25872	 * Rotate items by keypress
25873	 * @param {Object} action
25874	 */
25875	rotateByKey: function(action) {
25876		var shift = this._view.settings.keyboardRotateValue || 15;
25877		
25878		if (action[2]) {
25879			shift /= 3;
25880		}
25881
25882		if (action[1] === KEY.L) {
25883			shift *= -1;
25884		}
25885		
25886		this._model.rotate(shift);
25887		
25888		this._model.operationEnd(['rotate']);
25889	},
25890
25891	/**
25892	 * Observer for mouse wheel scrolling, bound to canvas zooming
25893	 * @param {Event} e Wheel event
25894	 * @param {Number} delta Scrolling delta
25895	 */
25896	wheelScroll: function(e, delta) {
25897		this._view.scale(delta / 50.0);
25898		e.stop();
25899	},
25900	
25901	/**
25902	 * Observer for canvas in event
25903	 */
25904	canvasIn: function(e, ee) {
25905		this._viewActive = true;
25906		switch (this._operation.type) {
25907			case 'add':
25908				if (this._operation.data.kind === 'decor') {
25909					// no dropables under canvas
25910					this._operation.data.visual.removeClass('yes').addClass('no');
25911				} else {
25912					this._operation.data.visual
25913						.css({
25914							left: ee.pageX - (this._operation.data.ox || 0) + 'px',
25915							top: ee.pageY - (this._operation.data.oy || 0) + 'px'
25916						})
25917						.data('position', {	x: this._operation.data.ox, 
25918											y: this._operation.data.oy});
25919					
25920					var current = this._operation.data.visual.find('.' + this.$type);
25921							
25922					if (!current.length) {
25923						current = this._operation.data.visual.find('.original');
25924					}
25925					
25926					current
25927						.show()
25928						.siblings().hide();
25929				}
25930				break;
25931				
25932			case 'draw':
25933			case 'setscale':
25934				this._view.cursor('crosshair');
25935				break;
25936				
25937			default:
25938				this._view.cursor('move');
25939		}
25940	},
25941
25942	/**
25943	 * Observer for canvas out event
25944	 */
25945	canvasOut: function() {
25946		this._view.cursor('default');
25947		this._viewActive = false;
25948		
25949		if (this._operation.type === 'add') {
25950			if (this._operation.data.kind === 'decor') {
25951				delete this._operation.data.target;
25952				// do not show any acceptance icons out of canvas
25953			} else {
25954				this._operation.data.visual
25955					.removeData('position')
25956					.find('.original').show()
25957					.siblings().hide();
25958			}
25959		}
25960	},
25961
25962	/**
25963	 * Select entity
25964	 * @param {Object} entity Entity to select
25965	 * @param {Object} toggle Will entity toggled in selected list or not
25966	 */
25967	select: function(entity, toggle) {
25968		var ret = true,
25969			exclusive = ['camera', 'shape'],
25970			selected = $.grep(this._model.selected(), function(entity) {
25971				return exclusive.indexOf(entity.type) !== -1;
25972			}).length !== 0;
25973			
25974		// check for exclusive selection
25975		if (selected || exclusive.indexOf(entity.type) !== -1) {
25976			this._model.unselect();
25977		}
25978		
25979		if (toggle) {
25980			if (entity.selected) {
25981				this._model.unselect(entity);
25982			} else {
25983				this._model.select(entity, true);
25984			}
25985			ret = false;
25986		} else if (!entity.selected) {
25987			ret = this._model.select(entity);
25988		}
25989		
25990		return ret;
25991	},
25992
25993	/**
25994	 * Entity (room shape/door/window/component/material) start dragging observer
25995	 * @param {Event} e Click event
25996	 */
25997	entityDrag: function(e, draggable) {
25998		var res = this._view.generateElementForDrag(draggable.entity, draggable.kind),
25999			addingComponentOnWall = 
26000				draggable.entity.level === -1 && 
26001					(draggable.kind === 'furniture' ||
26002						(draggable.kind === 'element' && draggable.entity.type === 'ArchitecturalComponent'));
26003		
26004		if (res.visual) {
26005			draggable.visual.append(res.visual);
26006		}
26007			
26008		this._operation = { type: 'add',
26009							data: { visual: draggable.visual,
26010									entity: draggable.entity,
26011									kind: draggable.kind,
26012									ox: res.ox,
26013									oy: res.oy,
26014									addingComponentOnWall: addingComponentOnWall,
26015									type: draggable.type || '' }};
26016
26017		if (draggable.kind === 'decor') {
26018			this._view.toggleDecorateTargets(draggable.kind, draggable.type);
26019			this._operation.data.targets = this._view.getDecorateTargets();
26020		}
26021	},
26022	
26023	/**
26024	 * Locate nearest wall to a specified coordinates
26025	 * @param {Event} e
26026	 * @param {Number} x
26027	 * @param {Number} y
26028	 * @parma {Number} minWidth Minimal width of the wall
26029	 */
26030	findNearestWall: function(e, minWidth) {
26031		return this._model.findNearestWall(this._view.toModelPosition(e.pageX, e.pageY), false, minWidth);
26032	},
26033	
26034	/**
26035	 * Funtion for finding nearest coordinates to given
26036	 * @param {Array} points Point snap to
26037	 * @param {Array} types Look only for specified item types
26038	 * @param {Array} except Points to omit from search result
26039	 * @returns {Object} object with x, y and z (if given) coordinates
26040	 */
26041	findSnappingCoords: function(snapPoints, types, exclude) {
26042		var tolerancy = this._model.settings.snappingMoveTolerancy,
26043			// list of axes by which find snaps
26044			coords = ['x', 'y'],
26045			response = {}, distance, distances = {},
26046			p = 0, plen = this._model._snapPoints.length, point, points = {},
26047			s = 0, slen = snapPoints.length, snap, snaps = {},
26048			c = 0, clen = coords.length, coord;
26049		
26050		for (p = 0; p < plen; p++) {
26051			point = this._model._snapPoints[p];
26052			
26053			// filter by point type and exlude list
26054			if ((!types || types.indexOf(point.type) !== -1) &&
26055					(!exclude || exclude.indexOf(point) === -1)) {
26056				for (s = 0; s < slen; s++) {
26057					snap = snapPoints[s];
26058					
26059					for (c = 0; c < clen; c++) {
26060						coord = coords[c];
26061						distance = point.distance(snap, coord);
26062						
26063						// filter by distance
26064						if ((!snaps[coord] && distance < tolerancy) ||
26065								distances[coord] > distance) {
26066							snaps[coord] = snap;
26067							points[coord] = point;
26068							distances[coord] = distance;
26069						}
26070					}
26071				}
26072			}
26073		}
26074		
26075		for (c = 0; c < clen; c++) {
26076			coord = coords[c];
26077			response['d' + coord] = snaps[coord] ?
26078				points[coord][coord] - snaps[coord][coord] : 0;
26079			if (this._snaps[coord] && !snaps[coord]) {
26080				this._snaps[coord] = false;
26081				myDeco.body.trigger('snappingOff', [coord]);
26082			} else if (snaps[coord] && (!this._snaps[coord] ||
26083					this._snaps[coord] !== points[coord][coord])) {
26084				this._snaps[coord] = points[coord][coord];
26085				myDeco.body.trigger('snappingOn', [coord, this._snaps[coord]]);
26086			}
26087		}
26088		
26089		return response;
26090	},
26091	
26092	/**
26093	 * Switch cotroller state
26094	 * @param {Event} e
26095	 * @param {String} type Controller to enable
26096	 */
26097	toggle: function(e, type) {
26098		this._viewActive = type === this.$type;
26099		this._model.unselect();
26100		this._super(e, type);
26101	},
26102	
26103	/**
26104	 * Execute default action by dblclick
26105	 */
26106	doubleClick: function() {
26107	},
26108	
26109	/**
26110	 * Check for acceptance of the currect operation and meterial to the specified visual element
26111	 * @param {Object} visual Visual or Entity element
26112	 * @returns {Boolean}
26113	 */
26114	_checkDecorAcceptance: function(visual) {
26115		var model = visual._view ? visual._model : visual;
26116		return this._operation.type === 'add' &&
26117			this._operation.data.kind === 'decor' &&
26118			model.acceptDecor &&
26119			(this._operation.data.type !== 'photo' ||
26120				(this._operation.data.type === 'photo' &&
26121				!!model.acceptPicture));
26122	},
26123
26124	/**
26125	 * Change acceptance icon on mouseover for droppable components/decor etc
26126	 * @param {Event} e Mouseover event
26127	 * @param {Object} visual Visual element operating on
26128	 */
26129	activateDroppable: function(e, visual) {
26130		if (this._checkDecorAcceptance(visual)) {
26131			this._operation.data.target = visual._model;
26132			this._operation.data.visual.addClass('yes').removeClass('no');
26133		}
26134	},
26135
26136	/**
26137	 * Change acceptance icon on mouseout for droppable components/decor etc
26138	 * @param {Event} e Mouseout event
26139	 * @param {Object} visual Visual element operating on
26140	 */
26141	deactivateDroppable: function(e, visual) {
26142		if (this._checkDecorAcceptance(visual)) {
26143			delete this._operation.data.target;
26144			this._operation.data.visual.removeClass('yes').addClass('no');
26145		}
26146	}
26147});
26148/*globals myDeco, $, FpeController, T, findShapeArea, findPointInShape, Point,
26149          numberInputKeyPress */
26150/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
26151          sloppy: true, white: true */
26152/**
26153 * @fileOverview FPE floor plan controller
26154 */
26155
26156var FloorController = FpeController.extend({
26157	/**
26158	 * Controller type
26159	 * @type {String}
26160	 * @static
26161	 */
26162	$type: 'floor',
26163	
26164	/**
26165	 * Initial rotation angle for selected components
26166	 * @type {Number}
26167	 */
26168	_rotationAngle: 0,
26169	
26170	/**
26171	 * Drag start listener
26172	 * @param {Event} e
26173	 * @param {Object} visual
26174	 */
26175	dragStart: function(e, visual) {
26176		if (this._operation.type !== 'draw' &&
26177				this._operation.type !== 'setscale') {
26178			this._super(e, visual);
26179			if (this._operation.type === 'drag' &&
26180					visual._model.type === 'corner') {
26181				this._operation.data.pointsToExclude =
26182					$.map(visual._model.getWalls(), function(wall) {
26183						return wall.middle;
26184					}).concat([visual._model]);
26185			}
26186		}
26187	},
26188	
26189	/**
26190	 * View registering function
26191	 * @param {Object} view
26192	 */
26193	registerView: function(view) {
26194		this._super(view);
26195		var self = this;
26196		
26197		// Initial support for component dropping outside of austin
26198		view.canvasElement
26199			.bind('dragover', function(e) {
26200				e.stop();
26201				e.originalEvent.dataTransfer.dropEffect = 'copy';
26202				self.canvasIn(false, e);
26203			})
26204			.bind('drop', function(e) {
26205				e.stop();
26206				
26207				var info, component,
26208					data = e.originalEvent.dataTransfer.getData('URL') ||
26209						e.originalEvent.dataTransfer.getData('Text');
26210				if (data) {
26211					info = data.match(/([0-9a-z]{40})/i);
26212					if (info) {
26213						myDeco.App.ws.proxy.fetch('component', info[1],
26214							function(result) {
26215								component = result[info[1]];
26216								if (component) {
26217									self._operation = {
26218										type: 'add',
26219										data: {
26220											entity: component,
26221											visual: $(),
26222											kind: 'furniture'
26223										}
26224									};
26225									self.entityDrop(e);
26226								}
26227							});
26228					}
26229				}
26230			});
26231		
26232		this.initDomElements();
26233		
26234		this._view._rotateArrow
26235			.mouseover(function() {
26236				self._view.cursor('default');
26237			})
26238			.mouseout(function() {
26239				self._view.cursor('move');
26240			})
26241			.mousedown(function(e) {
26242				self.rotateStart(e);
26243			}, true);
26244			
26245		myDeco.body.bind('selectEnd unselectEnd',
26246			this.toggleSelection.bind(this));
26247	},
26248
26249	/**
26250	 * Init various dom elements and attach observers
26251	 */
26252	initDomElements: function() {
26253		var self = this;
26254		
26255		// panel remove button
26256		this.registerButtons(this._view.library.get('closeButton'),
26257			function(e) {
26258				if (e.isDefaultPrevented()) {
26259					return;
26260				}
26261				e.preventDefault();
26262				self._model.remove();
26263			});
26264		
26265		// panel info button
26266		this.registerButtons(this._view.library.get('infoButton'),
26267			function(e) {
26268				if (e.isDefaultPrevented()) {
26269					return;
26270				}
26271				e.preventDefault();
26272				var entity = self._model.selected(true);
26273				if (entity && entity.componentData) {
26274					myDeco.body.trigger('showInfo', [entity]);
26275				}
26276			});
26277		
26278		// duplicate button
26279		this.registerButtons(this._view.library.get('duplicateButton'),
26280			function(e) {
26281				if (e.isDefaultPrevented()) {
26282					return;
26283				}
26284				e.preventDefault();
26285				self._model.duplicate();
26286			});
26287		
26288		// split button
26289		this.registerButtons(this._view.library.get('splitButton'),
26290			function(e) {
26291				if (e.isDefaultPrevented()) {
26292					return;
26293				}
26294				e.preventDefault();
26295				var entity = self._model.selected(true);
26296				if (entity && entity.type === 'wall') {
26297					self._model.splitWall(entity);
26298					self._model.operationEnd(['add']);
26299				}
26300			});
26301		
26302		// draw button
26303		this.registerButtons(this._view.library.get('drawButton'),
26304			function(e) {
26305				if (e.isDefaultPrevented()) {
26306					return;
26307				}
26308				e.preventDefault();
26309				var entity = self._model.selected(true);
26310				if (entity && entity.type === 'corner') {
26311					myDeco.body.trigger('preventOperationReset');
26312					self.drawStart(e, entity);
26313				}
26314			});
26315	},
26316	
26317	/**
26318	 * Shorthand for registering rotatable elements
26319	 * @param {Object} visual Visual element to register
26320	 * @returns {Object} input visual element
26321	 */
26322	registerRotatables: function(visual) {
26323		var self = this;
26324		visual.getBase()
26325			.mouseover(function(e) {
26326				if (self._checkDecorAcceptance(visual) ||
26327						self._operation.type !== 'add') {
26328					self.highlightStart(e, visual);
26329				}
26330			})
26331			.mouseout(function(e) {
26332				if (self._checkDecorAcceptance(visual) ||
26333						self._operation.type !== 'add') {
26334					self.highlightEnd(e, visual);
26335				}
26336			});
26337		visual.element
26338			.mousedown(function(e) {
26339				self.dragStart(e, visual);
26340			}, true)
26341			.click(function(e) {
26342				e.preventDefault();
26343			});
26344
26345		return visual;
26346	},
26347	
26348	/**
26349	 * Shorthand for registering button click listeners
26350	 * @param {Object} button DOM element
26351	 * @param {Function} clickCallback 
26352	 */
26353	registerButtons: function(button, clickCallback) {
26354		var self = this;
26355		button
26356			.mouseover(function() {
26357				self._view.cursor('default');
26358			})
26359			.mouseout(function() {
26360				self._view.cursor('move');
26361			})
26362			// remove component callback
26363			.mousedown(clickCallback, true);
26364	},
26365	
26366	/**
26367	 * Mousedown observer
26368	 * @param {Event} e
26369	 * @param {Object} visual Visual element
26370	 */
26371	mouseDown: function(e, visual) {
26372		var self = this,
26373			data, lastCorner, firstCorner, corner, corners, shapes, i, ilen, j,
26374			ceilingMaterial, floorMaterial, walls, walls1, wall,
26375			model = this._model,
26376			
26377			/**
26378			 * Function for check closed paths
26379			 * @param {Array} wall
26380			 * @returns {Array} array of shapes included given wall
26381			 */
26382			findShapes = function(path) {
26383				var shapes = [],
26384					lastPoint = path.get(-1),
26385					idx, newPath, points;
26386				
26387				while (lastPoint) {
26388					idx = path.indexOf(lastPoint);
26389					if (idx === 0) {
26390						// remove last from array because it's identical to the
26391						// first one
26392						path.pop();
26393						shapes.push(path);
26394						break;
26395					} else if (idx > 0 && idx < path.length - 1) {
26396						// closed with point from the path
26397						break;
26398					}
26399					
26400					// get attached walls except current
26401					points = lastPoint.getCorners([path.get(-2)], false, true);
26402					
26403					// fork searching in case of walls.length > 1
26404					while (points.length > 1) {
26405						newPath = path.slice();
26406						newPath.push(points.shift());
26407						shapes = shapes.concat(findShapes(newPath));
26408					}
26409					
26410					// continue for the first wall
26411					lastPoint = points.shift();
26412					path.push(lastPoint);
26413				}
26414				
26415				return shapes;
26416			},
26417			grepWalls = function(point, j) {
26418				var wall = model._findWall(point,
26419					shapes[i][(j + 1) % shapes[i].length]);
26420				return wall ? [wall] : null;
26421			};
26422		
26423		if (e.isDefaultPrevented()) {
26424			return;
26425		}
26426		
26427		if (this._operation.type === 'draw') {
26428			e.preventDefault();
26429			lastCorner = this._operation.data.corners.get(-1);
26430			firstCorner = this._operation.data.corners[0];
26431			
26432			// ipad have no mouseevents, so we need to specify last point
26433			// in it exact position
26434			if (myDeco.browser.isMobileSafari) {
26435				this._operation.data.point.copy(this._view.toModelPosition(e.pageX, e.pageY));
26436			}
26437			
26438			data = this._model.findAllowedPoint(
26439				this._operation.data.point ? new Point(
26440					this._operation.data.point.x,
26441					this._operation.data.point.y) : this._view.toModelPosition(e.pageX, e.pageY),
26442				lastCorner, this._operation.data.n === 0);
26443			
26444			// forbid adding a point in restricted area
26445			if (!data) {
26446				return;
26447			}
26448			
26449			if (data.point && data.wall && !data.corner) {
26450				data.corner = this._model.splitWall(data.wall, data.point);
26451			} else if (data.point) {
26452				corner = this._model.addCorner(data.point);
26453			}
26454			
26455			if (data.corner || corner) {
26456				this._operation.data.corners.push(corner || data.corner);
26457				this._model.select(this._operation.data.corners.get(-1));
26458				
26459				this._view._drawMeasurer.update(false);
26460				
26461				// add wall
26462				if (lastCorner && lastCorner !== (corner || data.corner)) {
26463					wall = this._model.addWall([lastCorner, corner || data.corner]);
26464					
26465					// show measurer for recently created wall
26466					$.each(this._view.measurers, function() {
26467						if ((this[0] === wall[0] && this[1] === wall[1]) ||
26468								(this[0] === wall[1] && this[1] === wall[0])) {
26469							this.update(true);
26470						}
26471					});
26472				}
26473				
26474				// we have a last corner already defined on plan
26475				// stop drawing
26476				if (this._operation.data.n !== 0 && data.corner) {
26477					// detect and create shape only if we have a closed path,
26478					if (data.corner) {
26479						// find all present shapes starting from last wall in
26480						// reverse order
26481						corners = this._operation.data.corners.slice();
26482						corners.reverse();
26483						shapes = findShapes(corners.slice(0, 2));
26484						
26485						// sort shapes by area size, usage of shortest path is
26486						// incorrect there
26487						shapes.sort(function(a, b) {
26488							var a1 = Math.abs(findShapeArea(a)),
26489								b1 = Math.abs(findShapeArea(b));
26490							
26491							if (a1 < b1) {
26492								return -1;
26493							} else if (a1 > b1) {
26494								return 1;
26495							} else {
26496								return 0;
26497							}
26498						});
26499						
26500						// 2 is the maximum number of produced shapes
26501						shapes = shapes.slice(0, 2);
26502						
26503						for (i = 0, ilen = shapes.length; i < ilen; i++) {
26504							// check if this shape is not intersected with other
26505							// new shape
26506							// if so that means this shape is combined from
26507							// new shape and old one, skip it
26508							// check only for second one
26509							if (i === 1) {
26510								// corner for the first shape is already defined
26511								// there!
26512								if (corner.inPoly(shapes[i])) {
26513									break;
26514								}
26515							}
26516							
26517							// find point in the new shape to check for
26518							// intersections with other shapes
26519							corner = findPointInShape(shapes[i]);
26520							
26521							// remove old orphaned shapes (under new one)
26522							for (j = model.shapes.length - 1; j >= 0; j--) {
26523								if (corner.inPoly(model.shapes[j])) {
26524									ceilingMaterial =
26525										$.cloneObject(model.shapes[j].ceilingMaterial);
26526									floorMaterial =
26527										$.cloneObject(model.shapes[j].floorMaterial);
26528								}
26529							}
26530							
26531							// gather all walls available for given points
26532							walls = $.map(shapes[i], grepWalls);
26533							
26534							// finally we add a new shape
26535							shapes[i] = model.addShape(shapes[i], walls,
26536								floorMaterial, ceilingMaterial);
26537							
26538							// find shape which already have all walls from current
26539							// and remove it, updating current with material
26540							for (j = this._model.shapes.length - 1; j >= 0; j--) {
26541								walls1 = this._model.shapes[j].getWalls();
26542								
26543								if (this._model.shapes[j] !== shapes[i] &&
26544										$.intersect(walls, walls1).length === walls1.length) {
26545									// set shape mateiral from old one
26546									this._model.assignMaterial(shapes[i],
26547										this._model.shapes[j].ceilingMaterial,
26548										'ceiling');
26549									this._model.assignMaterial(shapes[i],
26550										this._model.shapes[j].floorMaterial,
26551										'floor');
26552									
26553									// remove it finally
26554									this._model.shapes[j].remove();
26555								}
26556							}
26557						}
26558					}
26559					
26560					this._model.operationEnd(['add']);
26561					if (this._view._drawLine && this._view._drawCorner) {
26562						this._view._drawLine.hide();
26563						this._view._drawCorner.hide();
26564					}
26565				} else {
26566					this._operation.data.n += 1;
26567					myDeco.body.trigger('preventOperationReset');
26568					this.updateDrawLine(data.corner || corner);
26569				}
26570				
26571				this._view.sortCloud();
26572			}
26573		} else if (this._operation.type === 'setscale') {
26574			if (this._operation.data.points.length < 2) {
26575				myDeco.body.trigger('preventOperationReset');
26576				this._operation.data.points
26577					.push(this._view.toModelPosition(e.pageX, e.pageY));
26578				this._view.drawScalePoint(this._operation.data.points.get(-1));
26579
26580				if (this._operation.data.points.length === 2) {
26581					// current distance between points
26582					data = this._operation.data.points[0]
26583						.distance(this._operation.data.points[1]);
26584
26585					myDeco.popin.open({
26586						url: '#view-build-setscale-popup',
26587						afterOpen: function(div) {
26588							var form = div.find('form'),
26589								value = data,
26590								input = form.find('input:text').numberInput({
26591									formatFunction: function() {
26592										return Array.prototype.slice.call(arguments).toUnits();
26593									},
26594									parseFunction: function(v) {
26595										return v.fromUnits().shift();
26596									},
26597									positive: true,
26598									increment: function(shift) {
26599										var a = myDeco.settings
26600											.UNITS_LIST[myDeco.settings.UNITS][+shift + 2]
26601												.fromViewUnits();
26602										return a;
26603									},
26604									keyPress: numberInputKeyPress,
26605									value: data
26606								})
26607								.change(function() {
26608									value = $(this).data('value');
26609								});
26610
26611							myDeco.buttons.initSubmitButtons(form.find('input:submit'));
26612							form
26613								.find('.units').unitsSelector().end()
26614								.submit(function(e) {
26615									e.stop();
26616                                    var  input = $(e.target).find('input:text');
26617                                    input.blur();
26618                                    value = input.data('value');
26619									if (value !== 0) {
26620										self._view._background.scale *= (value / data);
26621										self._view._grid.drawBackground();
26622									}
26623									myDeco.popin.close();
26624								});
26625
26626							myDeco.body.bind('unitsChanged', function() {
26627								input.trigger('setValue');
26628							});
26629						}
26630					});
26631
26632					// we don't want have this in history
26633					this._model.operationReset();
26634				}
26635			}
26636		} else {
26637			this._super(e, visual);
26638		}
26639	},
26640	
26641	/**
26642	 * Method for updating draw line after point placing
26643	 * @param {Point} point
26644	 */
26645	updateDrawLine: function(point) {
26646		var line = this._view._drawLine, i;
26647		if (line) {
26648			line.hold();
26649			for (i = 0; i < 2; i++) {
26650				line.pathPoints[i].dropCache();
26651				line.pathPoints[i].x = point.x;
26652				line.pathPoints[i].y = -point.y;
26653			}
26654			line.render().show();
26655		}
26656	},
26657	
26658	/**
26659	 * Mouse move event listener
26660	 * @param {Event} e Mousemove event
26661	 */
26662	mouseMove: function(e) {
26663		var visual = this._operation.data.visual, model,
26664			pos, angle, evenAngle, dx, dy, wall, data, tolerancy,
26665			lastCorner;
26666		switch (this._operation.type) {
26667			case 'rotate':
26668				tolerancy = this._model.settings.snappingRotationTolerancy;
26669				pos = this._view.toModelPosition(e.pageX, e.pageY);
26670				angle = -T.atan2(
26671					this._model._selectionCenter.y -pos.y,
26672					this._model._selectionCenter.x - pos.x
26673				) + 90;
26674				
26675				evenAngle = Math.abs(angle % 90);
26676				if (evenAngle < tolerancy || 90 - evenAngle < tolerancy) {
26677					angle = Math.round(angle / 90) * 90;
26678				}
26679				
26680				this._model.rotate(angle - this._rotationAngle);
26681				this._rotationAngle = angle;
26682				this._view.updateRotationArrow();
26683				break;
26684				
26685			case 'drag':
26686				dx = (e.pageX - this._operation.data.x) / this._view.zoom;
26687				dy = -(e.pageY - this._operation.data.y) / this._view.zoom; 
26688				
26689				// parent is set only for one on-wall non-architectural
26690				// selected component
26691				if (this._operation.data.parent) {
26692					wall = this.findNearestWall(e);
26693					
26694					if (wall) {
26695						if (wall !== this._operation.data.parent) {
26696							this._operation.data.parent.highlightOff();
26697							this._operation.data.parent = wall;
26698						}
26699						
26700						wall.highlightOn();
26701					}
26702				}
26703				
26704											// SITE-8241
26705				if (visual.type === 'Corner'/* || visual.type === 'Component'*/) {
26706					model = visual._model;
26707					switch (visual.type) {
26708						case 'Corner':
26709							data = this.findSnappingCoords(
26710								[new Point(model.x + dx, model.y + dy)],
26711								['corner', 'wall'],
26712								this._operation.data.pointsToExclude
26713							);
26714							break;
26715						
26716						case 'Component':
26717							data = this.findSnappingCoords(
26718								[new Point(model.position.x + dx, model.position.y + dy)],
26719								['component'],
26720								[model.position]
26721							);
26722							break;
26723					}
26724					
26725					dx += data.dx;
26726					dy += data.dy;
26727					
26728					if (!data.dx) {
26729						this._operation.data.x = e.pageX;
26730					}
26731					if (!data.dy) {
26732						this._operation.data.y = e.pageY;
26733					}
26734				} else {
26735					this._operation.data.x = e.pageX;
26736					this._operation.data.y = e.pageY;
26737				}
26738				
26739				this._model.move(dx, dy);
26740				this._view.updateRotationArrow();
26741				break;
26742				
26743			case 'draw':
26744				pos = this._view.toModelPosition(e.pageX, e.pageY);
26745				lastCorner = this._operation.data.corners.get(-1);
26746				
26747				if (lastCorner) {
26748					if (e.shiftKey) {
26749						angle = pos.angleTo(lastCorner);
26750						evenAngle = Math.round(angle / 15) * 15;
26751						if ((angle > 45 && angle < 135) ||
26752								(angle > 225 && angle < 315)) {
26753							pos.x = lastCorner.x + (pos.y - lastCorner.y) / T.tan(evenAngle);
26754						} else {
26755							pos.y = lastCorner.y + (pos.x - lastCorner.x) * T.tan(evenAngle);
26756						}
26757					} else if (!e.ctrlKey) {
26758						data = this.findSnappingCoords([pos], ['corner', 'wall']);
26759						pos.x += data.dx;
26760						pos.y += data.dy;
26761					}
26762				}
26763				
26764				this._operation.data.point.copy(pos);
26765				data = this._model.findAllowedPoint(pos, lastCorner,
26766					this._operation.data.n === 0);
26767				
26768				if (this._view._drawLine && this._view._drawCorner) {
26769					if (lastCorner) {
26770						if (this._view._drawMeasurer[0] !== lastCorner ||
26771								this._view._drawMeasurer[1] !== this._operation.data.point) {
26772							this._view._drawMeasurer[0] = lastCorner;
26773							this._view._drawMeasurer[1] = this._operation.data.point;
26774							this._view._drawMeasurer.update(true);
26775						} else {
26776							this._view._drawMeasurer.update();
26777						}
26778					}
26779					
26780					this._view._drawLine.hold();
26781					this._view._drawLine.pathPoints[1].dropCache();
26782					this._view._drawLine.pathPoints[1].x = pos.x;
26783					this._view._drawLine.pathPoints[1].y = -pos.y;
26784					this._view._drawLine.render();
26785					
26786					if (data) {
26787						if (data.corner || data.wall) {
26788							pos = this._view._drawCorner.getBBox();
26789							pos = {
26790								x: pos.x + pos.width / 2,
26791								y: pos.y + pos.height / 2
26792							};
26793							this._view._drawCorner.translate(
26794								 (data.corner || data.point).x - pos.x,
26795								-(data.corner || data.point).y - pos.y
26796							).show();
26797						} else {
26798							this._view._drawCorner.hide();
26799						}
26800						
26801						if (!this._operation.data.allowed) {
26802							this._view._drawLine.attr('stroke', '#000');
26803							this._operation.data.allowed = true;
26804						}
26805					} else if (this._operation.data.allowed) {
26806						this._view._drawLine.attr('stroke', '#f00');
26807						this._operation.data.allowed = false;
26808					}
26809				}
26810				
26811				break;
26812		}
26813		
26814		if (this._operation.type !== 'add') {
26815			this._super(e);
26816		}
26817	},
26818	
26819	/**
26820	 * Mouse up button listener
26821	 * @param {Event} e Mouseup event
26822	 */
26823	mouseUp: function(e) {
26824		if (this._operation.type !== 'draw' &&
26825				this._operation.type !== 'setscale') {
26826			this._super(e);
26827		}
26828	},
26829	
26830	/**
26831	 * Operation reset function
26832	 */
26833	operationReset: function() {
26834		if (this._operation.type === 'draw' &&
26835				this._view._drawLine &&
26836				this._view._drawCorner) {
26837			
26838			this._view._drawMeasurer.update(false);
26839			this._view._drawLine.hide();
26840			this._view._drawCorner.hide();
26841			
26842			$.each(this._view.measurers, function() {
26843				this.update(false);
26844			});
26845		} else if (this._operation.type === 'setscale') {
26846			$.each(this._view._scalePoints, function() {
26847				this.remove();
26848			});
26849			this._view._scalePoints = [];
26850		}
26851		
26852		this._view.cursor('move');
26853		this._super();
26854	},
26855	
26856	/**
26857	 * Entity rotating observer
26858	 * @param {Event} e Mousedown event
26859	 */
26860	rotateStart: function(e) {
26861		if (!e.isDefaultPrevented()) {
26862			e.preventDefault();
26863			
26864			var self = this,
26865				selected = this._model.selected(),
26866				multiple = selected.length !== 1;
26867			
26868			if (!this._operation.type) {
26869				this._operation = {
26870					type: 'rotate',
26871					data: {}
26872				};
26873				
26874				$.each(selected, function() {
26875					this.operationStart(self._operation.type);
26876				});
26877			}
26878		}
26879	},
26880	
26881	/**
26882	 * Set Scale start function
26883	 */
26884	setscaleStart: function() {
26885		if (this._model._masking) {
26886			// if drawing requested switch view to floor
26887			if (!this.state) {
26888				myDeco.body.trigger('viewToggle', [this.$type]);
26889			}
26890			
26891			this._view.cursor('crosshair');
26892			
26893			this._operation = {
26894				type: 'setscale',
26895				data: {
26896					points: []
26897				}
26898			};
26899			myDeco.body.trigger('status', [$('<span>' + gettext('Point two corners to define real distance between them and set the scale. Press ESC key to escape') + '</span>'), -1]);
26900			this._model.operationStart(this._operation.type);
26901		}
26902	},
26903
26904	/**
26905	 * Start draw function
26906	 * @param {Event} e
26907	 * @param {Corner} corner Starting corner
26908	 */
26909	drawStart: function(e, corner) {
26910		var text;
26911
26912		// if drawing requested switch view to floor
26913		if (!this.state) {
26914			myDeco.body.trigger('viewToggle', [this.$type]);
26915		}
26916		
26917		this._operation = {
26918			type: 'draw',
26919			data: {
26920				n: 0,
26921				point: new Point(),
26922				corners: []
26923			}
26924		};
26925		
26926		if (corner) {
26927			this._operation.data.corners.push(corner);
26928			this._operation.data.n++;
26929			this.updateDrawLine(corner);
26930		}
26931		
26932		// this part can be common for both floor and iso views
26933		this._model.unselect();
26934		
26935		this._view.cursor('crosshair');
26936		
26937		// hide all components
26938		this._view._cloud.findByField('type', 'Component').batch('toggle', false);
26939		
26940		// show message
26941		text = '<span class="info">' + gettext('Drawing mode enabled:') + '</span><ul>' +
26942			'<li>' + gettext('1. Draw your shape point by point. ');
26943		
26944		if (this._operation.data.n !== 0) {
26945			text += gettext('Click to add a wall.');
26946		} else {
26947			text += gettext('Click on the grid to start.');
26948		}
26949		
26950		text += '</li><li>' + gettext('2. Complete your drawing by closing your shape or ');
26951		if (myDeco.browser.isMobileSafari) {
26952			text += gettext('long tap (1s)');
26953		} else {
26954			text += gettext('double clicking');
26955		}
26956		text += '</li></ul>';
26957		text = '<span>
26957' + text + '</span>';
26958		myDeco.body.trigger('status', [$(text), -1]);
26959		
26960		$('.draggable, #sidebar').fadeOut();
26961		
26962		this._model.operationStart(this._operation.type);
26963	},
26964	
26965	/**
26966	 * Entity highlighting observer
26967	 * @param {Event} e Mouseover event
26968	 * @param {Object} visual Visual element to highlight
26969	 */
26970	highlightStart: function(e, visual) {
26971		if (this._operation.type === 'drag' ||
26972				this._operation.type === 'rotate' ||
26973				this._operation.type === 'draw') {
26974			return;
26975		}
26976			
26977		// Hack to don't change cursor when dragging material over the wall
26978		if (this._operation.type === 'add') {
26979			this._view.disableCursors = true;
26980		}
26981
26982		this._super(e, visual);
26983		this._view.disableCursors = false;
26984		
26985		// change acceptance icon
26986		if (this._checkDecorAcceptance(visual)) {
26987			this._operation.data.target = visual._model;
26988			this._operation.data.visual.addClass('yes').removeClass('no');
26989		}
26990	},
26991
26992	/**
26993	 * Entity dishighliting observer
26994	 * @param {Event} e Mouseout event
26995	 * @param {Object} visual Visual element to dishighlight
26996	 */
26997	highlightEnd: function(e, visual) {
26998		this._super(e, visual);
26999		
27000		// change acceptance icon
27001		if (this._checkDecorAcceptance(visual)) {
27002			delete this._operation.data.target;
27003			this._operation.data.visual.removeClass('yes').addClass('no');
27004		}
27005		
27006		if (this._operation.type !== 'draw' &&
27007				$.inArray(this._operation.type,
27008					['drag', 'rotate', 'add']) === -1) {
27009			this._view.cursor('move');
27010		}
27011	},
27012	
27013	/**
27014	 * default action on doubleclick: slipt wall under cursor
27015	 * @param {Event} e
27016	 * @param {Wall} visual
27017	 */
27018	doubleClick: function(e, visual) {
27019		if (!this._operation && visual && visual.type === 'Wall' &&
27020				!this.isLocked(visual)) {
27021			this._model.splitWall(visual._model,
27022				this._view.toModelPosition(e.pageX, e.pageY));
27023			this._model.operationEnd(['add']);
27024		} else if (this._operation && this._operation.type === 'draw') {
27025			this._model.operationReset();
27026		}
27027	},
27028	
27029	toggleSelection: function(state) {
27030		var selected = this._model.selected(),
27031			multiple = selected.length > 1,
27032			camera = this._view._cloud.findByField('type', ['Camera']).shift();
27033			
27034		if (camera) {
27035			camera._decorate(multiple ? 'hidden' : 'normal');
27036		}
27037		
27038		if (this._model.rotationAvailable) {
27039			this._rotationAngle = multiple ? 0 : selected[0].getAngle();
27040			this._view.updateRotationArrow();
27041			this._view._rotateArrow.show();
27042		} else {
27043			this._view._rotateArrow.hide();
27044		}
27045	},
27046	
27047	operationEnd: function(data) {
27048		this._super(data);
27049		this._view.updateRotationArrow();
27050	}
27051	
27052});
27053
27054myDeco.App.controllers.push(FloorController);
27055/*globals myDeco, $, Class, T, ObjectsLibrary, Draggable, Rotatable */
27056/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27057          sloppy: true, white: true */
27058/**
27059 * @fileOverview Base class for drawable entities
27060 */
27061var Drawable = Class.extend({
27062	/**
27063	 * Instance type
27064	 * @type {String}
27065	 */
27066	type: 'Drawable',
27067
27068	/**
27069	 * Highlighted flag
27070	 * @type {Boolean}
27071	 */
27072	_highlighted: false,
27073	
27074	/**
27075	 * Operation flag
27076	 * @type {Boolean}
27077	 */
27078	_operation: false,
27079	
27080	/**
27081	 * Controls panel flag
27082	 * @type {Boolean}
27083	 */
27084	_panel: false,
27085	
27086	/**
27087	 * Is controls visible on plan
27088	 * @type {Boolean}
27089	 */
27090	_controlsVisible: false,
27091
27092	/**
27093	 * List of controls to show in control panel
27094	 * @type {Object}
27095	 */
27096	controls: {},
27097	
27098	/**
27099	 * View object
27100	 * @type {FpeView}
27101	 */
27102	_view: false,
27103	
27104	/**
27105	 * Model object
27106	 * @type {FpeModel}
27107	 */
27108	_model: false,
27109	
27110	/**
27111	 * Visibility state
27112	 * @type {Boolean}
27113	 */
27114	state: true,
27115	
27116	/**
27117	 * Component type settings
27118	 * @type {Object}
27119	 */
27120	_settings: {},
27121	
27122	/**
27123	 * Init function
27124	 * @param {Object} view FPE view
27125	 */
27126	init: function(view, model) {
27127		this._view = view;
27128		this._model = model;
27129		
27130		this.settings();
27131		
27132		if (this._model) {
27133			this._model
27134				.attachEvent('onSelect', this.select.bind(this),
27135					this._view.$type)
27136				.attachEvent('onUnselect', this.unselect.bind(this),
27137					this._view.$type)
27138				.attachEvent('onRemove', this.erase.bind(this),
27139					this._view.$type)
27140				.attachEvent('onHightlightOn', this.highlightOn.bind(this),
27141					this._view.$type)
27142				.attachEvent('onHightlightOff', this.highlightOff.bind(this),
27143					this._view.$type)
27144				.attachEvent('onOperationStart', this.operationStart.bind(this),
27145					this._view.$type)
27146				.attachEvent('onOperationEnd', this.operationEnd.bind(this),
27147					this._view.$type)
27148				.attachEvent('onOperationReset', this.operationReset.bind(this),
27149					this._view.$type);
27150		}
27151		this.draw();
27152	},	
27153
27154	/**
27155	 * Instance drawing function
27156	 */
27157	draw: function() {
27158		if (this.element) {
27159			this.updateMaterial();
27160			
27161			if ($.keys(this.controls).length === 0) {
27162				return;
27163			}
27164			
27165			// draw control panel
27166			var settings = this._settings.controlPanel || {},
27167				w = settings.padding || 0,
27168				h = 0,
27169				element, bbox, control;
27170
27171			for (control in this.controls) {
27172				element = this._view.library.get((control === 'remove' ?
27173					'close' : control) + 'Button');
27174				
27175				// scale button back to original size
27176				bbox = element.scale(1, 1, 0, 0).getBBox();
27177				
27178				this.controls[control] = {
27179						x: w,
27180						y: settings.padding,
27181						element: element,
27182						target: 'panel'
27183				};
27184				
27185				// panel width
27186				w += settings.padding + bbox.width;
27187				// panel height
27188				h = Math.max(h, settings.padding * 2 + bbox.height);
27189			}
27190			
27191			// create control panel
27192			this._panel = this._view.canvas.set()
27193				.push(this._view.canvas.path(settings.bg)
27194					.hold()
27195					.moveTo(-w/2.0 + settings.curveOffset, 0)
27196					.lineTo( w/2.0 - settings.curveOffset, 0)
27197					.arcTo(h/2.0, h/2.0, 0, 1, w/2.0 - settings.curveOffset, h)
27198					.lineTo(-w/2.0 + settings.curveOffset, h)
27199					.arcTo(h/2.0, h/2.0, 0, 1, -w/2.0 + settings.curveOffset, 0)
27200					.andClose()
27201					.render());
27202			
27203			this.element.push(this._panel.hide());
27204		}
27205	},
27206
27207	/**
27208	 * Pan instance
27209	 * @param {Number} dx DeltaX
27210	 * @param {Number} dy DeltaY
27211	 */
27212	pan: function(dx, dy) {
27213		if (this.element) {
27214			this.element.translate(dx, dy);
27215		}
27216	},
27217
27218	/**
27219	 * Scale current instance
27220	 * @param {Number} coeff Scaling factor
27221	 */
27222	scale: function() {
27223		this.toggleControls();
27224	},
27225
27226	/**
27227	 * Erase drawing instance from canvas
27228	 */
27229	erase: function() {
27230		this.unselect();
27231		
27232		// remove from cloud
27233		if (this._view._cloud.remove(this).length !== 0) {
27234			// unlink events
27235			if (this._model) {
27236				this._model.dropEvents(this._view.$type);
27237			}
27238			this.element.remove();
27239		}
27240	},
27241
27242	/**
27243	 * Toggle controls for entity
27244	 * @param {Boolean} state [true||false||undefined] for show/hide/update
27245	 * respectively
27246	 */
27247	toggleControls: function(state) {
27248		var name, control, ebb, pbb, zoom, angle,
27249			btn, btnbox;
27250		
27251		// Do we have panel at all?
27252		if (this._panel) {
27253			// hide panel
27254			if (state === false && this._controlsVisible) {
27255				this._panel.hide();
27256				
27257				for (name in this.controls) {
27258					control = this.controls[name];
27259					if (control.target === 'panel') {
27260						this._panel.pop(control.element);
27261					} else {
27262						this.element.pop(control.element);
27263					}
27264				}
27265			}
27266			
27267			// do not want to reposition hidden element
27268			if (this._controlsVisible || state === true) {
27269				// reposition panel
27270				ebb = this.getBBox();
27271				
27272				zoom = 1 / this._view.zoom;
27273				angle = this._model.getAngle();
27274		
27275				this._panel.rotate(0, ebb.cx, ebb.cy);
27276				this._panel.scale(zoom, zoom, ebb.cx, ebb.cy);
27277				
27278				pbb = this._panel.getBBox();
27279				
27280				this._panelPosition(ebb, pbb, angle, zoom).toFront();
27281			}
27282			
27283			// show panel
27284			if (state === true && !this._controlsVisible) {
27285				pbb = this._panel.getBBox();
27286				pbb.width += this._settings.controlPanel.curveOffset * 2 * zoom;
27287				pbb.x -= this._settings.controlPanel.curveOffset * zoom;
27288				
27289				for (name in this.controls) {
27290					btn = this.controls[name];
27291					btnbox = btn.element.scale(zoom, zoom).getBBox();
27292					
27293					if (btn.target === 'panel') {
27294						this._panel.push(btn.element
27295							.translate(pbb.x + btn.x * zoom - btnbox.x,
27296								pbb.y + btn.y * zoom - btnbox.y).toFront());
27297					} else {
27298						this.element.push(btn);
27299					}
27300				}
27301				
27302				this._panel.show();
27303				this._controlsVisible = true;
27304			}
27305		}
27306		
27307		if (typeof state !== 'undefined') {
27308			this._controlsVisible = state;
27309		}
27310	},
27311	
27312	/**
27313	 * Calculate panel position for visual
27314	 * @param {Object} componentBBox
27315	 * @param {Object} panelBBox
27316	 * @param {Number} angle
27317	 * @param {Number} zoom
27318	 */
27319	_panelPosition: function(componentBBox, panelBBox, angle, zoom) {
27320		return this._panel.translate(componentBBox.cx - panelBBox.width / 2.0 -
27321			panelBBox.x, componentBBox.cy -
27322			(Math.abs(componentBBox.width / 2.0  * T.sin(angle)) + 
27323			Math.abs(componentBBox.height / 2.0 * T.cos(angle))) -
27324			this._settings.controlPanel.distance * zoom - panelBBox.y
27325		);
27326		
27327	},
27328	
27329	/**
27330	 * Select current instance
27331	 * @param {Array} selected List of currently selected entities
27332	 * @returns {Object|Boolean===false}
27333	 */
27334	select: function(selected) {
27335		if (selected) {
27336			// hide controls on multiple selection
27337			if (selected.length > 1) {
27338				this._view._cloud
27339					.findByField('_model', selected)
27340					.batch('toggleControls', false);
27341			} else if (selected.length === 1) {
27342				this.toggleControls(true);
27343			}
27344		}
27345		
27346		this._decorate('selected');
27347	},
27348
27349	/**
27350	 * Unselect current instance
27351	 * @param {Array} selected List of currently selected entities
27352	 * @returns {Object|Boolean===false}
27353	 */
27354	unselect: function(selected) {
27355		if (selected) {
27356			// show panel for the last selected element
27357			if (selected.length === 1) {
27358				this._view._cloud
27359					.findByField('_model', selected)
27360					.batch('toggleControls', true);
27361			}
27362		}
27363		
27364		this.toggleControls(false);
27365		
27366		this._operation = false;
27367		this._decorate('normal');
27368	},
27369
27370	/**
27371	 * Highlight current instance
27372	 * @returns {Object|Boolean===false}
27373	 */
27374	highlightOn: function() {
27375		if (this._highlighted) {
27376			return false;
27377		}
27378		
27379		this._decorate('highlighted');
27380		this._highlighted = true;
27381		return this;
27382	},
27383
27384	/**
27385	 * Dishighlight current instance
27386	 * @returns {Object|Boolean===false}
27387	 */
27388	highlightOff: function() {
27389		if (!this._highlighted) {
27390			return false;
27391		}
27392		
27393		this._decorate(this._model.selected ? 'selected' : 'normal');
27394		this._highlighted = false;
27395		return this;
27396	},
27397	
27398	/**
27399	 * Return current highlighting state
27400	 * @returns {Boolean}
27401	 */
27402	highlighted: function() {
27403		return this._highlighted;
27404	},
27405	
27406	/**
27407	 * Set status to operating for current instance
27408	 * @param {String} operation Operation type
27409	 * @returns {Object|Boolean===false}
27410	 */
27411	operationStart: function(operation) {
27412		if (this._model.selected && !this._operation) {
27413			this._operation = operation;
27414			this._decorate('operation');
27415			return this;
27416		}
27417		return false;
27418	},
27419	
27420	/**
27421	 * Remove operation status
27422	 * @returns {Object|Boolean===false} 
27423	 */
27424	operationEnd: function() {
27425		if (this._operation) {
27426			this._operation = false;
27427			this._decorate(this._highlighted ? 'highlighted' : 'selected');
27428			return this;
27429		}
27430		return false;
27431	},
27432	
27433	/**
27434	 * Set status for current instance in case of operation reset
27435	 * @returns {Object|Boolean===false}
27436	 */
27437	operationReset: function() {
27438		if (this._operation) {
27439			this._operation = false;
27440			this._decorate(this._highlighted ? 'highlighted' :
27441				(this._model.selected ? 'selected' : 'normal'));
27442			return this;
27443		}
27444		return false;
27445	},
27446	
27447	/**
27448	 * Return current operation
27449	 * @returns {String}
27450	 */
27451	operation: function() {
27452		return this._operation;
27453	},
27454
27455	/**
27456	 * Return element base
27457	 * @returns {Object}
27458	 */
27459	getBase: function() {
27460		return this._base || this.element;
27461	},
27462	
27463	/**
27464	 * Return bounding box for element
27465	 * @returns {Object}
27466	 */
27467	getBBox: function() {
27468		var bb = this.getBase().getBBox();
27469		bb.cx = bb.x + bb.width / 2.0;
27470		bb.cy = bb.y + bb.height / 2.0;
27471		return bb;
27472	},
27473
27474	/**
27475	 * Move instance
27476	 * @param {Number} dx DeltaX
27477	 * @param {Number} dy DeltaY
27478	 * @returns {Object} this
27479	 */
27480	move: function(dx, dy) {
27481		this.element.translate(dx, -dy);
27482		this.updateRender();
27483		return this;
27484	},
27485
27486	/**
27487	 * Rotate instance
27488	 * @param {Number} angle Rotating angle in degrees
27489	 * @returns {Object} this
27490	 */
27491	rotate: function(angle) {
27492		var bb = this.getBBox();
27493		this.element.rotate(angle, bb.cx, bb.cy);
27494		this.toggleControls();
27495		return this;
27496	},
27497
27498	/**
27499	 * Stub for update displaying of element material
27500	 */
27501	updateMaterial: function() {
27502	},
27503	
27504	/**
27505	 * Detect event type settings depend on event type
27506	 * @param {String} type Event type
27507	 * @returns {Object} event settings
27508	 */
27509	_decorate: function(state) {
27510		if (!this._settings) {
27511			return false;
27512		}
27513		
27514		state = state || 'normal';
27515		// check for entity specific settings depend on z-level
27516		var settings = typeof this._model !== 'undefined' &&
27517			typeof this._model.level !== 'undefined' &&
27518			this._settings[this._model.level] ?
27519				this._settings[this._model.level] :
27520				this._settings;
27521							
27522		return settings[state] || false;
27523	},
27524	
27525	/**
27526	 * Update settings for current type
27527	 */
27528	settings: function() {
27529		this._settings = {};
27530		$.extend(true, this._settings, this._view.settings.Drawable || {});
27531		if (this instanceof Draggable) {
27532			$.extend(true, this._settings, this._view.settings.Draggable || {});
27533		}
27534		if (this instanceof Rotatable) {
27535			$.extend(true, this._settings, this._view.settings.Rotatable || {});
27536		}
27537		$.extend(true, this._settings, this._view.settings[this.type] || {});
27538	},
27539	
27540	/**
27541	 * Misc functions for render view update
27542	 */
27543	updateRender: function() {
27544	},
27545	
27546	/**
27547	 * Toggle element visibility
27548	 * @param {Boolean} state Visual state. If omitted visual changes to a
27549	 * semitransparent state described in settings
27550	 */
27551	toggle: function(state) {
27552		this.state = typeof state === 'undefined' || state;
27553		if (this.state) {
27554			this.element
27555				.attr('opacity', typeof state === 'undefined' &&
27556					this._settings.visual &&
27557						typeof this._settings.visual.opacity !== 'undefined' ?
27558							this._settings.visual.opacity : 1)
27559				.show();
27560			if (this._panel && this.state && !this._model.selected) {
27561				this._panel.hide();
27562			}
27563			
27564			this.updateMaterial();
27565		} else {
27566			this.element.hide();
27567		}
27568		return this.state;
27569	}
27570	
27571});
27572/*globals myDeco, $, Drawable */
27573/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27574          sloppy: true, white: true */
27575/**
27576 * @fileOverview Base class for draggable instances
27577 */
27578var Draggable = Drawable.extend({
27579	/**
27580	 * Instance type
27581	 * @type {String}
27582	 */
27583	type: 'Draggable',
27584	
27585	/**
27586	 * Init function
27587	 */
27588	init: function(view, model) {
27589		this._super(view, model);
27590		this._model.attachEvent('onMove', this.move.bind(this),
27591			this._view.$type);
27592	},
27593	
27594	/**
27595	 * Decorate draggable visual
27596	 * @param {Boolean} state
27597	 * @returns {Object} settings
27598	 */
27599	_decorate: function(state) {
27600		var settings = this._super(state);
27601		
27602		if (this._panel) {
27603			switch (this.operation()) {
27604				case 'drag':
27605				case 'rotate':
27606					this._panel.hide();
27607					break;
27608				default:
27609					if (this._controlsVisible) {
27610						// reposition panel
27611						this.toggleControls(true);
27612						// and show it finally
27613						this._panel.show();
27614					}
27615			}
27616		}
27617		
27618		return settings;
27619	}
27620});
27621/*globals myDeco, $, Draggable */
27622/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27623          sloppy: true, white: true */
27624/**
27625 * @fileOverview Base class for rotatable instances
27626 */
27627var Rotatable = Draggable.extend({
27628	/**
27629	 * Instance type
27630	 * @type {String}
27631	 */
27632	type: 'Rotatable',
27633	/**
27634	 * init function
27635	 * @param {Object} view FPE view
27636	 * @param {Object} model Model entity
27637	 */
27638	init: function(view, model) {
27639		this._super(view, model);
27640		
27641		model.attachEvent('onRotate', this.rotate.bind(this), this._view.$type);
27642	},
27643	
27644	/**
27645	 * Drawing function
27646	 */
27647	draw: function() {
27648		this._super();
27649		this.rotate(this._model.getAngle());
27650	}
27651});
27652/*globals myDeco, $, Drawable */
27653/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27654          sloppy: true, white: true */
27655/**
27656 * @fileOverview Canvas grid class
27657 */
27658var Grid = Drawable.extend({
27659	/**
27660	 * Intsance type
27661	 * @type {String}
27662	 */
27663	type: 'Grid',
27664	
27665	/**
27666	 * Grid drawing function
27667	 */
27668	draw: function() {
27669		var view = this._view,
27670			self = this,
27671			settings, gridNo, grid, i, image;
27672		
27673		if (this.element) {
27674			this.element.remove();
27675			// image is reamoved here as it part of element
27676			this.image = false;
27677			this.element = false;
27678		}
27679			
27680		this.element = view.canvas.set();
27681		
27682		for (gridNo = 0; gridNo < 2; gridNo++) {
27683			settings = this._settings[gridNo];
27684			grid = view.canvas.path({
27685				stroke: settings.strokeColor,
27686				'stroke-width': settings.strokeWidth
27687			}).hold();
27688			
27689			// draw both vertical & horizontal lines
27690			for (i = -view.bounds; i <= view.bounds; i = i + settings.step) {
27691				grid.moveTo(-view.bounds + settings.offset,
27692					i + settings.offset);
27693				grid.lineTo(view.bounds + settings.offset, i + settings.offset);
27694				
27695				grid.moveTo(i + settings.offset, -view.bounds +
27696					settings.offset);
27697				grid.lineTo(i + settings.offset, view.bounds + settings.offset);
27698			}
27699			
27700			this.element.push(grid.render());
27701		}
27702
27703		this._super();
27704	},
27705
27706	/**
27707	 * Draw masking background, references to view data
27708	 */
27709	drawBackground: function() {
27710		var bg = this._view._background;
27711		
27712		if (bg.src) {
27713			if (!this.image) {
27714				this.image = this._view.canvas
27715					.image(bg.src, bg.x, bg.y, bg.width * bg.scale, bg.height * bg.scale);
27716				this.element.push(this.image);
27717			} else {
27718				this.image.attr({
27719					x: bg.x,
27720					y: bg.y,
27721					width: bg.width * bg.scale,
27722					height: bg.height * bg.scale
27723				});
27724			}
27725			this.image.toBack();
27726		}
27727	}
27728});
27729/*globals myDeco, $, Drawable, location, diffuse2RGB, textureFill */
27730/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27731          sloppy: true, white: true */
27732/**
27733 * @fileOverview Base class for all shape-based objects (floor, ceiling etc)
27734 */
27735var Shape = Drawable.extend({
27736	/**
27737	 * Instance type
27738	 * @type {String}
27739	 */
27740	type: 'Shape',
27741	
27742	/**
27743	 * Measurer holder
27744	 * @type {Object}
27745	 */
27746	measurer: null,
27747	
27748	/**
27749	 * Init function
27750	 * @param {FpeView} view FPE view
27751	 * @param {FpeModel} model Model entity
27752	 */
27753	init: function(view, model) {
27754		this._super(view, model);
27755
27756		var i, len = this._model.length,
27757			self = this;
27758			
27759		this._cornerMoveFunc = function(dx, dy) {
27760			self.cornerMove(this, dx, dy);
27761		};
27762
27763		this._model.attachEvent('onMaterial', function() {
27764			self.updateMaterial();
27765			self._decorate();
27766		}, this._view.$type);
27767
27768		// attach listeners on corners moving
27769		for (i = 0; i < len; i++) {
27770			this._model[i].attachEvent('onMove', this._cornerMoveFunc,
27771				this._view.$type);
27772		}
27773		
27774		this._model.attachEvent('onCornerAdd', function(corner) {
27775			corner.attachEvent('onMove', self._cornerMoveFunc,
27776				self._view.$type);
27777			self.element.clear();
27778			self.draw();
27779		}, this._view.$type);
27780	},
27781
27782	/**
27783	 * Drawing function
27784	 */
27785	draw: function() {
27786		if (!this.element) {
27787			this.element = this._view.canvas.path({
27788				'stroke-width': 0,
27789				'stroke-opacity': 0,
27790				'fill-opacity': this._model._model._masking ? 0.5 : 1
27791			}).hold();
27792		}
27793		this.element.poly(this._model).render();
27794		this._super();
27795	},
27796
27797	/**
27798	 * Change drawing on corner moving
27799	 * @param {Object} point Point to move
27800	 * @param {Number} dx DeltaX
27801	 * @param {Number} dy DeltaY
27802	 */
27803	cornerMove: function(point, dx, dy) {
27804		point = this._model.indexOf(point);
27805		if (point === -1) {
27806			return;
27807		}
27808		
27809		this.element.pathPoints[point].dropCache();
27810		this.element.pathPoints[point].x += dx;
27811		this.element.pathPoints[point].y += -dy;
27812		this.element.render();
27813	},
27814	
27815	/**
27816	 * Shape material update
27817	 * @param {String} type ceiling or floor
27818	 */
27819	updateMaterial: function(type) {
27820		if (type !== 'ceiling') {
27821            var floormat = this._model.floorMaterial;
27822            var scaling = $.extend(true, {}, 
27823                                   this._view.settings.Shape.options,
27824                                   {width: floormat.texture_width, height: floormat.texture_height}
27825                                  );
27826            delete scaling.percent;
27827			this.element.attr({fill: textureFill(floormat, scaling)});
27828		}
27829	},
27830	
27831	/**
27832	 * End operation callback
27833	 * @param {String} operation
27834	 */
27835	operationEnd: function(operation) {
27836		this._super(operation);
27837		this.element.pathPoints._updateCoords();
27838	},
27839	
27840	erase: function() {
27841		var i, len = this._model.length;
27842		for (i = 0; i < len; i++) {
27843			this._model[i].detachEvent('onMove', this._cornerMoveFunc,
27844				this._view.$type);
27845		}
27846		
27847		this._super();
27848	}
27849});
27850/*globals myDeco, $, Draggable */
27851/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27852          sloppy: true, white: true */
27853/**
27854 * @fileOverview Corner class
27855 */
27856var Corner = Draggable.extend({
27857	/**
27858	 * Instance type
27859	 * @type {String}
27860	 */
27861	type: 'Corner',
27862	
27863	controls: {
27864		draw: false
27865	},
27866	/**
27867	 * Drawing function
27868	 */
27869	draw: function() {
27870		this._base = this._view.canvas.circle(this._model.x, - this._model.y,
27871				this._view.settings.Corner.radius)
27872			.attr(this._view.settings.Corner.normal.attr).toFront();
27873		this.element = this._view.canvas.set([this._base]);
27874		
27875		this._super();
27876	},
27877	
27878	/**
27879	 * Decorate corner
27880	 * @param {String} settings Event type
27881	 * @returns {Object} Event settings
27882	 */
27883	_decorate: function(settings) {
27884		if (settings === 'highlighted') {
27885			this.element.toFront();
27886		}
27887			
27888		settings = this._super(settings);
27889		this._base.attr(settings.attr);
27890		return settings;
27891	},
27892	
27893	/**
27894	 * Toggle controls for corners
27895	 * @param {Boolean} state
27896	 */
27897	toggleControls: function(state) {
27898		if (this._view._controller._operation &&
27899				this._view._controller._operation.type === 'draw') {
27900			state = false;
27901		}
27902		
27903		this._super(state);
27904	}
27905});
27906/*globals myDeco, $, Draggable, T, diffuse2RGB, Point, window */
27907/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
27908          sloppy: true, white: true */
27909/**
27910 * @fileOverview Wall instance class
27911 */
27912var Wall = Draggable.extend({
27913	/**
27914	 * Instance type
27915	 * @type {String}
27916	 */
27917	type: 'Wall',
27918	
27919	/**
27920	 * Measurer
27921	 * @type {Object}
27922	 */
27923	measurer: null,
27924	
27925	/**
27926	 * Used controls
27927	 * @type {Object}
27928	 */
27929	controls: {
27930		remove: false,
27931		split: false
27932	},
27933	
27934	/**
27935	 * Init function
27936	 * @param {Object} view FPE view
27937	 * @param {Object} model Model entity
27938	 */
27939	init: function(view, model) {
27940		var i, self = this;
27941		
27942		this._super(view, model);
27943		
27944		this._cornerMoveListener = function(dx, dy) {
27945			self.cornerMove(this, dx, dy);
27946		};
27947		
27948		this._updateMeasurerListener = this.updateMeasurer.bind(this);
27949		
27950		this._model.attachEvent('onMaterial', function() {
27951			self.updateMaterial();
27952			self._decorate();
27953		}, this._view.$type);
27954		
27955		this._model.attachEvent('onThickness', function() {
27956			self.updateEdges();
27957			for (i = 0; i < 2; i++) {
27958				self.updateNeighborEdges(self._model[i]);
27959			}
27960		}, this._view.$type);
27961		
27962		this.updateEdges();
27963		
27964		// attach events to wall corners and update neighbor walls
27965		for (i = 0; i < 2; i++) {
27966			this.updateNeighborEdges(this._model[i]);
27967			this._model[i].attachEvent('onMove', this._cornerMoveListener,
27968				this._view.$type);
27969		}
27970		
27971		this.measurer = this._view.registerMeasurer(
27972			this._model.getCorners(this._model.getShapes()[0]),
27973			this._model.thickness
27974		);
27975		
27976		// Can't use bound function here because fireEvent passes parameter,
27977		// and unitsChanged don't.
27978		myDeco.body.bind('unitsChanged', function() {
27979			self.measurer.update();
27980		});
27981		
27982		this._model.attachEvent('onUpdateMeasurer', this._updateMeasurerListener);
27983	},
27984
27985	/**
27986	 * Instance draw function 
27987	 */
27988	draw: function() {
27989		// update wall decoration depend on entity data
27990		this.updateMaterial();
27991
27992		var attr = $.extend({}, this._settings.normal, {
27993			'stroke-width': 0.5,
27994			'fill': this._fill
27995		});
27996		
27997		this._base = this._view.canvas.path(attr).hold()
27998			.poly(this._model.getJoints()).render();
27999		this.element = this._view.canvas.set([this._base]);
28000		this._super();
28001
28002		this._decorate('normal');
28003	},
28004	
28005	/**
28006	 * Move wall function
28007	 * @param {Number} dx DeltaX
28008	 * @param {Number} dy DeltaY
28009	 * @returns {Object} this object
28010	 */
28011	move: function() {
28012		this.toggleControls();
28013		return this;
28014	},
28015	
28016	/**
28017	 * Corner move function, recalculate self points and trigger to recalculate
28018	 * points of neighbor walls
28019	 * @param {Object} point Draggable point
28020	 * @param {Number} dx DeltaX
28021	 * @param {Number} dy DeltaY
28022	 */
28023	cornerMove: function(point) {
28024		this.updateEdges();
28025		this.updateNeighborEdges(point);
28026		this.measurer.update();
28027	},
28028	
28029	/**
28030	 * Function for update already drawn points
28031	 */
28032	updateEdges: function() {
28033		this._base.hold().update(this._model.getJoints()).render();
28034	},
28035	
28036	/**
28037	 * Update edges for neighbor walls
28038	 * @param {Corner} point Opposite point
28039	 */
28040	updateNeighborEdges: function(point) {
28041		// get opposite point to current one
28042		var p = this._model.getOpposite(point);
28043		// update attached points on the attached walls
28044		this._view._cloud.findByField('_model', p.getWalls(this._model))
28045			.batch('updateEdges', p);
28046	},
28047	
28048	/**
28049	 * Update already defined measurer
28050	 * @param {Boolean} state
28051	 */
28052	updateMeasurer: function(state) {
28053		if (this._view._controller._operation &&
28054				this._view._controller._operation.type === 'draw' &&
28055				state === false) {
28056			return;
28057		}
28058		
28059		this.measurer.update(state);
28060	},
28061	
28062	/**
28063	 * Update wall material display
28064	 */
28065	updateMaterial: function() {
28066		this._fill = this._settings.normal.fill || 
28067			'rgb(' + diffuse2RGB(this._model.material.average_colour ||
28068			this._model.material.diffuse || [0.5, 0.5, 0.5]) + ')';
28069	},
28070	
28071	/**
28072	 * Decorate wall depending on event type
28073	 * @param {String} type Event type
28074	 * @returns {Object} Decorate settings for current event type
28075	 */
28076	_decorate: function(type) {
28077		var settings = this._super(type);
28078		if (settings) {
28079            // Wall top should be drawn with the color wall surface
28080            // has. But there is one exeption from this rule: if wall
28081            // color is default, we draw it in dark grey instead of
28082            // white.
28083			var fill = this._fill;
28084			if (this._model && this._model.material && this._model.material.ID == 'materialWallDefault') fill = "#222";
28085			this._base.attr($.extend({'fill': fill}, settings));
28086		}
28087		
28088		return settings;
28089	},
28090	
28091	/**
28092	 * Scale wall
28093	 * @param {Number} scale Scale factor
28094	 */
28095	scale: function(scale) {
28096		this._super(scale);
28097		this.measurer.update();
28098	},
28099	
28100	/**
28101	 * Erase current wall
28102	 */
28103	erase: function() {
28104		this._view.deregisterMeasurer(this.measurer);
28105		this._model[0].detachEvent('onMove', this._cornerMoveListener,
28106			this._view.$type);
28107		this._model[1].detachEvent('onMove', this._cornerMoveListener,
28108			this._view.$type);
28109		this._model.detachEvent('onUpdateMeasurer', this._updateMeasurerListener);
28110		this._super();
28111	},
28112	
28113	/**
28114	 * Calculate panel position for wall
28115	 * @param {Object} componentBBox
28116	 * @param {Object} panelBBox
28117	 * @param {Number} angle
28118	 * @param {Number} zoom
28119	 */
28120	_panelPosition: function(componentBBox, panelBBox, angle, zoom) {
28121		return this._panel.translate(componentBBox.cx - panelBBox.width / 2.0 -
28122			panelBBox.x, componentBBox.cy - panelBBox.height / 2.0 -
28123			panelBBox.y - this._settings.controlPanel.distance * zoom
28124		);
28125	}
28126});
28127/*globals myDeco, $, Rotatable, buildThumbnailLink, T */
28128/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28129          sloppy: true, white: true */
28130/**
28131 * @fileOverview Class for components
28132 */
28133var Component = Rotatable.extend({
28134	/**
28135	 * Instance type
28136	 * @type {String}}
28137	 */
28138	type: 'Component',
28139	
28140	/**
28141	 * Available controls for instance
28142	 * @type {Object}
28143	 */
28144	controls: {
28145		remove: false,
28146		duplicate: false
28147	},
28148
28149	/**
28150	 * Draw instance
28151	 */
28152	draw: function() {
28153		var src = buildThumbnailLink(this._model.componentData['3d'][this._model.x0yRender],
28154				this._view.settings.furniture.componentThumbnail),
28155			minSize = this._settings.minsize,
28156			width = Math.max(this._model.width, minSize),
28157			depth = Math.max(this._model.depth, minSize),
28158			x0 = this._model.position.x - width / 2,
28159			y0 = -this._model.position.y - depth / 2;
28160		
28161		this.element = this._view.canvas.set();
28162		this._base  = this._view.canvas.image(src, x0, y0, width, depth);
28163		this._outline = this._view.canvas.rect(x0, y0, width, depth).toBack();
28164		
28165		this.element.push(this._base, this._outline);
28166		
28167		this.updateRender();
28168		this._decorate();
28169		this._super();
28170	},
28171	
28172	/**
28173	 * Update render view on canvas
28174	 */
28175	updateRender: function() {
28176		if (this._model.parent) {
28177			var bbox = this.getBBox(),
28178				dx = this._model.position.x - bbox.cx,
28179				dy = this._model.position.y + bbox.cy,
28180				angle = this._model.getAngle(),
28181				th = this._model.parent.thickness / 2 +
28182					this._model.shift.dy;
28183			
28184			this.element.translate(th * T.sin(angle) + dx,
28185				-th * T.cos(angle) - dy);
28186			this.toggleControls();
28187		}
28188	},
28189
28190	/**
28191	 * Decorate component depend on event type
28192	 * @param {String} type Event type
28193	 * @returns {Object} Event settings
28194	 */
28195	_decorate: function(type) {
28196		var settings = this._super(type);
28197
28198		if (this._settings.hideOutline) {
28199			switch (this.operation()) {
28200				case 'drag':
28201				case 'rotate':
28202					this._outline.hide();
28203					break;
28204				default:
28205					this._outline.show();
28206			}
28207		}
28208		
28209		if (settings.outline) {
28210			this._outline.attr(settings.outline).insertAfter(this._base);
28211		}
28212		
28213		return settings;
28214	}
28215});
28216/*globals myDeco, $, Draggable, T, Point */
28217/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28218          sloppy: true, white: true */
28219/**
28220 * @fileOverview Base class for all draggable items
28221 */
28222var Window = Draggable.extend({
28223	/**
28224	 * Instance type
28225	 * @type {String}
28226	 */
28227	type: 'Window',
28228	
28229	/**
28230	 * Used controls
28231	 * @type {Object}
28232	 */
28233	controls: {
28234		remove: false,
28235		duplicate: false,
28236		info: false
28237	},
28238	
28239	/**
28240	 * Init function
28241	 * @param {Object} view FPE view
28242	 * @param {Object} model Model entity
28243	 */
28244	init: function(view, model) {
28245		this._super(view, model);
28246		this._model
28247			.attachEvent('onRotate', this.rotate.bind(this), this._view.$type)
28248			.attachEvent('onAttributeChange', this.draw.bind(this), this._view.$type);
28249	},
28250
28251	/**
28252	 * Draw function for window instance
28253	 */
28254	draw: function() {
28255		var position = this._model.position,
28256			attr = this._settings.normal,
28257			median = this._model.parent.thickness / 3,
28258			width = this._model.width / 2,
28259			thickness = this._model.parent.thickness / 2;
28260			
28261		if (!this.element) {
28262			this._rect = this._view.canvas
28263				.path($.extend({}, attr, {'stroke-width': 0.5}))
28264				.poly([
28265					new Point( width,  thickness),
28266					new Point(-width,  thickness),
28267					new Point(-width, -thickness),
28268					new Point( width, -thickness)
28269				]);
28270			this._median = this._view.canvas.path()
28271				.attr($.extend({}, attr, {'stroke-width': median})).hold()
28272				.moveTo(-width, 0)
28273				.lineTo( width, 0).render();
28274			this._base = this._view.canvas.set([this._rect, this._median]);
28275			this.element = this._view.canvas.set([this._base])
28276				.translate(this._model.position.x, -this._model.position.y)
28277				.rotate(this._model.getAngle());
28278			this._super();
28279		} else {
28280			this._rect.hold().update([
28281				new Point(position.x + width, position.y + thickness),
28282				new Point(position.x - width, position.y + thickness),
28283				new Point(position.x - width, position.y - thickness),
28284				new Point(position.x + width, position.y - thickness)
28285			]).render();
28286			this._median.hold().attr('stroke-width', median).update([
28287				new Point(position.x - width, position.y),
28288				new Point(position.x + width, position.y)
28289			]).render();
28290		}
28291	},
28292	
28293	/**
28294	 * Decorate function
28295	 * @param {String} type Event type
28296	 * @returns {Object} Event settings
28297	 */
28298	_decorate: function(type) {
28299		var settings = this._super(type);
28300		if (settings) {
28301			this._base.attr(settings);
28302		}
28303		return settings;
28304	}
28305});
28306/*globals myDeco, $, Draggable, T, Point, xor */
28307/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28308          sloppy: true, white: true */
28309/**
28310 * @fileOverview Door class
28311 */
28312var Door = Draggable.extend({
28313	/**
28314	 * Instance type
28315	 * @type {String}
28316	 */
28317	type: 'Door',
28318	
28319	/**
28320	 * Used controls
28321	 * @type {Object}
28322	 */
28323	controls: {
28324		remove: false,
28325		duplicate: false,
28326		info: false
28327	},
28328	
28329	/**
28330	 * Drawing function
28331	 * @param {Object} view FPE view
28332	 * @param {Object} model Model entity
28333	 */
28334	init: function(view, model) {
28335		this._super(view, model);
28336		var self = this;
28337		
28338		this._model
28339			.attachEvent('onRotate', this.rotate.bind(this), this._view.$type)
28340			.attachEvent('onAttributeChange', function(name) {
28341				switch (name) {
28342					case 'width':
28343					case 'height':
28344					case 'depth':
28345						self.draw();
28346						break;
28347					case 'open_side':
28348					case 'open_direction':
28349						self.drawHinge();
28350						break;
28351				}
28352			}, this._view.$type);
28353		
28354	},
28355
28356	/**
28357	 * Drawing function
28358	 */
28359	draw: function() {
28360		var position = this._model.position,
28361			attr = this._settings.normal,
28362			width = this._model.width / 2,
28363			thickness = this._model.parent.thickness / 2;
28364			
28365		if (!this.element) {
28366			this._rect = this._view.canvas
28367				.path($.extend({}, attr, {'stroke-width': 0.5}))
28368				.poly([
28369					new Point( width,  thickness),
28370					new Point(-width,  thickness),
28371					new Point(-width, -thickness),
28372					new Point( width, -thickness)
28373				]);
28374			this._base = this._view.canvas.set([this._rect]);
28375			this.element = this._view.canvas.set([this._base])
28376				.translate(this._model.position.x, -this._model.position.y)
28377				.rotate(this._model.getAngle());
28378			this._super();
28379		} else {
28380			this._rect.hold().update([
28381				new Point(position.x + width, position.y + thickness),
28382				new Point(position.x - width, position.y + thickness),
28383				new Point(position.x - width, position.y - thickness),
28384				new Point(position.x + width, position.y - thickness)
28385			]).render();
28386		}
28387		
28388		this.drawHinge();
28389	},
28390	
28391	/**
28392	 * Draw hinge for door
28393	 */
28394	drawHinge: function() {
28395		if (!this._model.open_side) {
28396			return;
28397		}
28398		
28399		if (this._hinge) {
28400			this.element.pop(this._hinge);
28401			this._hinge.remove();
28402		}
28403		
28404		if (this._clearance) {
28405			this.element.pop(this._clearance);
28406			this._clearance.remove();
28407		}
28408		
28409		var settings = this._settings,
28410			hingeAngle = settings.openedAngle,
28411			thickness = this._model.parent.thickness,
28412			th = thickness / 2, 
28413			// base points
28414			angle = this._model.getAngle(),
28415			x0 = this._model.position.x - this._model.width / 2 -
28416				th * T.sin(angle),
28417			y0 = -this._model.position.y - th + th * T.cos(angle),
28418			bbox = this.getBBox(),
28419			side_correction, direction_correction,
28420			hingeX0, hingeY0, hingeX1, hingeY1, hingeX2, dx, dy;
28421		
28422		// door with one hingle
28423		if (this._model.open_side === 'l' || this._model.open_side === 'r') {
28424			side_correction = +(this._model.open_side === 'l');
28425			direction_correction = +(this._model.open_direction === 'o');
28426			
28427			hingeX0 = x0 + this._model.width * side_correction;
28428			hingeY0 = y0 + direction_correction * thickness;
28429			hingeX1 = hingeX0 - (side_correction || -1) *
28430				this._model.width * T.cos(hingeAngle);
28431			hingeY1 = hingeY0 + (direction_correction || -1) *
28432				this._model.width * T.sin(hingeAngle);
28433			hingeX2 = x0 + this._model.width * +(!side_correction);
28434			
28435			this._hinge = this._view.canvas.path(settings.normal)
28436				.moveTo(hingeX0, hingeY0)
28437				.lineTo(hingeX1, hingeY1);
28438		
28439			this._clearance = this._view.canvas.path(settings.clearance)
28440				.moveTo(hingeX0, hingeY0)
28441				.lineTo(hingeX1, hingeY1)
28442				.arcTo(this._model.width, this._model.width,
28443					0, +!xor(direction_correction, side_correction),
28444					hingeX2, hingeY0)
28445				.andClose()
28446				.translate(th * T.sin(angle), -th * T.cos(angle))
28447				.rotate(angle, bbox.cx, bbox.cy);
28448			
28449			this.element.push(this._clearance);
28450		} else if (this._model.open_side === 'b') {
28451			// door with two hingles
28452			hingeAngle *= 0.5;
28453			dx = this._model.width * T.cos(hingeAngle) * 0.5;
28454			dy = this._model.width * T.sin(hingeAngle) * 0.5;
28455			
28456			y0 += thickness;
28457			
28458			this._hinge = this._view.canvas.path(settings.normal)
28459				.moveTo(x0, y0)
28460				.lineTo(x0 + dx, y0 + dy)
28461				.moveTo(x0 + this._model.width, y0)
28462				.lineTo(x0 + this._model.width - dx, y0 + dy);
28463		}
28464		
28465		this._hinge.translate(
28466				 th * T.sin(angle),
28467				-th * T.cos(angle));
28468		
28469		this._hinge.rotate(angle, bbox.cx, bbox.cy);
28470		
28471		this.element.push(this._hinge);
28472	},
28473	
28474	/**
28475	 * Decorate door
28476	 * @param {String} type Event type
28477	 * @returns {Object} Event settings 
28478	 */
28479	_decorate: function(type) {
28480		var settings = this._super(type);
28481		if (settings) {
28482			this._base.attr(settings);
28483			
28484			if (this._hinge) {
28485				this._hinge.attr(settings);
28486			}
28487		}
28488		return settings;
28489	}
28490});
28491/*globals myDeco, $, Rotatable */
28492/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28493          sloppy: true, white: true */
28494/**
28495 * @fileOverview Camera class
28496 */
28497var Camera = Rotatable.extend({
28498	/**
28499	 * Instance type
28500	 * @type {String}
28501	 */
28502	type: 'Camera',
28503
28504	init: function(view, model) {
28505		this._super(view, model);
28506		model.attachEvent('onFov', this.update_cone.bind(this), this._view.$type);
28507    },
28508	
28509	/**
28510	 * Drawing function
28511	 */
28512	draw: function() {
28513		this.element = this._view.library.get('camera', true)
28514			.translate(this._model.getCX(), -this._model.getCY());
28515		this._hover = this.element[0];
28516        this._base = this.element[1];
28517		this._bbase = this.element[2][5];
28518		this._lens = this.element[2][1];
28519		this._speck = this.element[2][2];
28520
28521        this.update_cone();
28522        this._hover.hide();
28523
28524		this._super();
28525	},
28526
28527    update_cone: function() {
28528        var fov = this._model.fieldofview,
28529            hover = this._view.library.camera_cone(fov),
28530		    bb = this.getBBox();
28531        hover.translate(this._model.getCX(), -this._model.getCY());
28532		hover.rotate(this._model.getAngle(), bb.cx, bb.cy);
28533
28534        var idx = this.element.items.indexOf(this._hover);
28535        this._hover.remove();
28536        this.element.items[idx] = hover;
28537        hover.insertBefore(this._lens);
28538        this._hover = hover;
28539    },
28540
28541	/**
28542	 * Decorate camera
28543	 * @param {String} type Event type
28544	 * @returns {Object} Event settings
28545	 */
28546	_decorate: function(type) {
28547		var settings = this._super(type);
28548		
28549		if (type === 'hidden') {
28550			this.element.hide();
28551		} else {
28552			this.element.show();
28553			if (type !== 'normal') {
28554                this._hover.show();
28555			} else {
28556				this._hover.hide();
28557			}
28558			
28559			if (!(type === 'highlighted' && this._model.selected) && settings) {
28560				this._bbase.attr(settings.base);
28561				this._lens.attr(settings.lens);
28562				this._speck.attr(settings.speck);
28563			}
28564			
28565			switch (this.operation()) {
28566				case 'drag': 
28567					this._hover.hide();
28568					break;
28569				case 'rotate':
28570					this._hover.hide();
28571					break;
28572				default:
28573					if (this._controlsVisible) {
28574						this._hover.show();
28575					}
28576			}
28577		}
28578			
28579		return settings;
28580	}
28581});
28582/*globals myDeco, $, Class */
28583/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28584          sloppy: true, white: true */
28585/**
28586 * @fileOverview FPE competiton support plugin
28587 */
28588
28589var FpePlugin = Class.extend({
28590	/**
28591	 * Default plugin settings
28592	 * @type {Object}
28593	 */
28594	_settings: {
28595		// plugin doesn't block view autostart
28596		autostart: true
28597	},
28598	
28599	/**
28600	 * Associated view
28601	 * @type {FpeView}
28602	 */
28603	_view: false,
28604	
28605	/**
28606	 * Plugin init method
28607	 * @param {FpeView} view Associated view
28608	 * @param {Object} settings Plugin settings
28609	 */
28610	init: function(view, settings) {
28611		this._view = view;
28612		this._settings = $.extend(true, this._settings, settings || {});
28613		
28614		// merge view autostart view plugin settings
28615		this._view.autostart = this._view.autostart && this._settings.autostart !== false;
28616		
28617		// bind runPlugin event with run method
28618		myDeco.body.bind('runPlugins', this.run.bind(this));
28619	},
28620	
28621	/**
28622	 * Run plugin method, will run before finishing of view starting
28623	 * @param {Event} e runPlugin event
28624	 */
28625	run: function() {
28626	}
28627});
28628/*globals myDeco, $ */
28629/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
28630          sloppy: true, white: true */
28631/**
28632 * @fileOverview Settings for FPE project
28633 */
28634myDeco.App.settings = $.extend(true, myDeco.App.settings, {
28635	className: 'fpe',
28636	model: {
28637		shapes: {},
28638		cameraFov: 78,
28639		minDistance: 10,
28640		searchDistance: 30,
28641		minAngle: 10,
28642		snappingRotationTolerancy: 10,
28643		snappingMoveTolerancy: 10
28644	},
28645	views: {
28646		isometric: {
28647			decor: {
28648				listingThumbnail: {postfix: '_50.jpg'}
28649			},
28650
28651			IsoWall: {
28652				visual: {
28653					'fill': '#eeeeee',
28654					'stroke': '#dddddd',
28655					'stroke-width': 2
28656				},
28657				options: {
28658					defaultColor: [0.5, 0.5, 0.5],
28659					noTexture: false,
28660					percent: 30
28661				}
28662			},
28663			IsoFloor: {
28664				visual: {
28665					'stroke-opacity': 0
28666				},
28667				options: {
28668					defaultColor: [0.5, 0.5, 0.5],
28669					noTexture: false,
28670					percent: 30
28671				}
28672			},
28673			IsoComponent: {
28674				visual: {
28675					opacity: 0.2,
28676					active: {
28677						stroke: '#459fda'
28678					},
28679					inactive: {
28680						stroke: 'none'
28681					}
28682				}
28683			}
28684		},
28685		floor: {
28686			penWidth: 7,
28687
28688			rotateArrow: {
28689				distance: 100,
28690				scale: 0.25,
28691				zoom: 2,
28692				decoration: {
28693					normal: {
28694						rotatingArrows: {
28695							fill: '#6b6b6b',
28696							stroke: '#6b6b6b'
28697						}
28698					},
28699					highlighted: {
28700						rotatingArrows: {
28701							fill: '#6b6b6b',
28702							stroke: '#6b6b6b'
28703						}
28704					},
28705					selected: {
28706						rotatingArrows: {
28707							fill: '#6b6b6b',
28708							stroke: '#6b6b6b'
28709						}
28710					},
28711					operation: {
28712						rotatingArrows: {
28713							fill: '#29a6e6',
28714							stroke: '#29a6e6'
28715						}
28716					}
28717				}
28718			},
28719
28720			offset: {
28721				top: 0
28722			},
28723
28724			furniture: {
28725				perPage: 4,
28726				canvasThumbnail: false,
28727				dragThumbnail: {percent: 30, shadow: 70, offset: 2}
28728			},
28729
28730			decor: {
28731				// pagination settings for different kind of materials
28732				'wall-colour': {perPage: 24, maxWidth: 70, maxHeight: 70},
28733				wallpaper: {perPage: 4, maxWidth: 70, maxHeight: 70},
28734				flooring: {perPage: 4, maxWidth: 70, maxHeight: 70},
28735				listingThumbnail: {postfix: '_50.jpg'},
28736				dragThumbnail: {width: 50, height: 50, postfix: '_50.jpg'}
28737			},
28738
28739			render: {
28740				width: 640,
28741				height: 480
28742			},
28743
28744			Drawable: {
28745				controlPanel: {
28746					distance: 30,
28747					padding: 1,
28748					curveOffset: 10,
28749					bg: {
28750						fill: '#fff',
28751						stroke: '#fff',
28752						opacity: 0.5
28753					}
28754				}
28755			},
28756			Wall: {
28757				normal: {
28758					'stroke': '#222'
28759				},
28760				highlighted: {
28761					'fill': '#3eb3de',
28762					'stroke': '#3eb3de'
28763				},
28764				selected: {
28765					'fill': '#3eb3de',
28766					'stroke': '#3eb3de'
28767				}
28768			},
28769			Window: {
28770				thickness: 17,
28771				normal: {
28772					'fill': '#fff',
28773					'stroke': '#000'
28774				},
28775				highlighted: {
28776					'fill': '#67afdf',
28777					'stroke': '#000'
28778				},
28779				selected: {
28780					'fill': '#39617b',
28781					'stroke': '#000'
28782				}
28783			},
28784			Door: {
28785				openedAngle: 30,
28786				normal: {
28787					'fill': '#fff',
28788					'stroke': '#000'
28789				},
28790				highlighted: {
28791					'fill': '#67afdf',
28792					'stroke': '#000'
28793				},
28794				selected: {
28795					'fill': '#39617b',
28796					'stroke': '#000'
28797				},
28798				clearance: {
28799					'fill': '#fff',
28800					'opacity': 0.4,
28801					'stroke-width': 1
28802				}
28803			},
28804			floor: {
28805				fill: 'red',
28806				fillOpacity: 0.2
28807			},
28808			Corner: {
28809				radius: 9,
28810				normal: {
28811					attr: {
28812						'stroke': 'black',
28813						'fill': '#469CDA',
28814						'opacity': 0
28815					}
28816				},
28817				highlighted: {
28818					attr: {
28819						'opacity': 1
28820					}
28821				},
28822				selected: {
28823					attr: {
28824						'opacity': 1
28825					}
28826				}
28827			},
28828			Grid: [
28829				{
28830					strokeColor: '#C8C8C8',
28831					strokeWidth: .5,
28832					step: 200,
28833					offset: 0
28834				},
28835				{
28836					strokeColor: '#eaeaea',
28837					strokeWidth: 0.5,
28838					step: 50,
28839					offset: 50
28840				}
28841			],
28842			Camera: {
28843				arrowDistance: 200,
28844				normal: {
28845					base: {
28846						fill: '#9bddf2'
28847					},
28848					lens: {
28849						fill: '#5bb9e8',
28850						stroke: '#5bb9e8'
28851					},
28852					speck: {
28853						fill: '#b8e1ec',
28854						stroke: '#b8e1ec'
28855					}
28856				},
28857				highlighted: {
28858					base: {
28859						fill: '#35b4e9'
28860					},
28861					lens: {
28862						fill: '#1e88bd',
28863						stroke: '#1e88bd'
28864					},
28865					speck: {
28866						fill: '#1e88bd',
28867						stroke: '#28a6e5'
28868					}
28869				},
28870				// TODO: renamed to unused state to avaiod of change style on select
28871				selected_: {
28872					base: {
28873						fill: '#9fa03d'
28874					},
28875					lens: {
28876						fill: '#9fa03d',
28877						stroke: '#aaad35'
28878					},
28879					speck: {
28880						fill: '#ced021',
28881						stroke: '#ced021'
28882					}
28883				},
28884				fov: 180 / 3.0
28885			},
28886			Component: {
28887				arrowDistance: 100,
28888				minsize: 4,
28889				hideOutline: false,
28890				normal: {
28891					outline: {
28892						stroke: '#999',
28893						'stroke-width': 2,
28894						opacity: 0
28895					}
28896				},
28897				highlighted: {
28898					outline: {
28899						stroke: '#459fda',
28900						'stroke-width': 2,
28901						opacity: 1
28902					}
28903				},
28904				selected: {
28905					outline: {
28906						stroke: '#459fda',
28907						'stroke-width': 2,
28908						opacity: 1
28909					}
28910				},
28911				// settings for on-wall components
28912				'-1': {
28913					normal: {
28914						outline: {
28915							stroke: '#333',
28916							'stroke-width': 1,
28917							opacity: 1
28918						}
28919					},
28920					highlighted: {
28921						outline: {
28922							stroke: '#459fda',
28923							'stroke-width': 2,
28924							opacity: 1
28925						}
28926					},
28927					selected: {
28928						outline: {
28929							stroke: '#459fda',
28930							'stroke-width': 2,
28931							opacity: 1
28932						}
28933					}
28934				}
28935			},
28936			Shape: {
28937				options: {
28938					defaultColor: [0.5, 0.5, 0.5],
28939					percent: 30
28940				}
28941			},
28942			// will be merged with ObjectsLibrary._settings
28943			library: { }
28944		}
28945	},
28946
28947	plugins: {
28948		advertisement: {
28949			enabled: false,
28950			timeout: 2
28951		},
28952
28953		competition: {
28954			enabled: false,
28955			// seconds
28956			timeout: 120
28957		}
28958	},
28959
28960	controllers: {}
28961});
28962/*
28963 * WARNING !!!
28964 * original code was improved!
28965 * it has more precise regexp for id attribute
28966 * and dramatically faster (up to 10x times)
28967 * when str contain a template already, not just
28968 * its element id, due to caching in this mode.
28969 */
28970
28971// Simple JavaScript Templating
28972// John Resig - http://ejohn.org/ - MIT Licensed
28973(function(){
28974	var cache = {};
28975	this.tmpl = function tmpl(str, data){
28976		// Figure out if we're getting a template, or if we need to
28977		// load the template - and be sure to cache the result.
28978		var fn = cache[str] || (/^[A-z][\w-]*$/.test(str) ? 
28979					// we have element id as str, so will read from tag,
28980					// setting data as false will prevent caching for this case
28981					tmpl((document.getElementById(str) ?
28982						document.getElementById(str)
28983							.innerHTML
28984							.replace(/^\s*\/\/\s*<!--\s*/, '')
28985							.replace(/\s*\/\/\s*-->$/, '')
28986						: ''), 
28987						false) :
28988					// str contain template
28989					new Function("obj",
28990					"var p=[];" +
28991					
28992					// Introduce the data as local variables using with(){}
28993					"with(obj){p.push('" +
28994					
28995					// Convert the template into pure JavaScript
28996					str
28997						.replace(/[\r\t\n]/g, " ")
28998						.split("<%").join("\t")
28999						.replace(/((^|%>)[^\t]*)'/g, "$1\r")
29000						.replace(/\t=(.*?)%>/g, "',$1,'")
29001						.split("\t").join("');")
29002						.split("%>").join("p.push('")
29003						.split("\r").join("\\'")
29004					
29005					+ "');}return p.join('');"));
29006		
29007		// Do not cache in self recall mode
29008		if (data !== false)
29009			cache[str] = fn;
29010		// Provide some basic currying to the user
29011		return data ? fn( data ) : fn;
29012	};
29013})();
29014/**
29015 * @fileOverview Common animation routines
29016 */
29017
29018myDeco.animate = {
29019	
29020	/**
29021	 * Visualize transition from one element to other by blue bordered rectangle,
29022	 * used to add item into favbar for example.
29023	 * @param {jQuery element} start Starting element
29024	 * @param {jQuery element} finish Finish element
29025	 * @param {function} callback Will be called when animation is complete
29026	 * @return {Boolean} False in case of wrong arguments
29027	 */
29028	transition: function(start, finish, callback) {
29029		if (!start['length'] || !finish['length']) return false;
29030		var border = 4,
29031				startDims = $.extend(start.offset(), {
29032					width: start.outerWidth(),
29033					height: start.outerHeight(),
29034					border: parseInt(start.css('borderLeftWidth')) || 0
29035				}),
29036				finishDims = $.extend(finish.offset(),{
29037					width: finish.outerWidth(), 
29038					height: finish.outerHeight()
29039				});
29040		$('<div></div>')
29041			.css({
29042				position: 'absolute',
29043				zIndex: 5000,
29044				left: startDims.left + startDims.border - border + 'px',
29045				top: startDims.top + startDims.border - border + 'px',
29046				border: border + 'px solid #459FDB',
29047				width: startDims.width + 'px',
29048				height: startDims.height + 'px'
29049			})
29050			.appendTo('body')
29051			.animate({
29052				width: finishDims.width + 'px',
29053				height: finishDims.height + 'px',
29054				left: finishDims.left - border + 'px',
29055				top: finishDims.top - border + 'px'
29056			}, {
29057				duration: 500,
29058				complete: function(){
29059					$(this).remove();
29060					if ($.isFunction(callback)) callback();
29061				}
29062			});
29063	}
29064	
29065};
29066/* globals $, myDeco, window */
29067/* jslint white: false, forin: true, fragment: true, undef: true, nomen: true, 
29068   plusplus: true, bitwise: false, regexp: false, newcap: true, sloppy: true */
29069
29070/**
29071* @fileOverview Wishlisting functionality
29072*/
29073myDeco.shortlist = {
29074	guids : [],
29075	buttons: {},
29076	data : [],
29077	folders : [],
29078	sending : false,
29079	removing : false,
29080	storeName: 'wishlist',
29081	storeFolders: 'user_folders',
29082	wrapper: myDeco.body,
29083
29084	init: function (root) {
29085		if ($('.wishlist-block').length || $('#wishlist-items').length) {
29086			var self = this;
29087			self.wrapper = root || myDeco.body;
29088			if (myDeco.username) {
29089				this.load(true, true);
29090				this.periodicalSync();
29091				this.checkShortlistOwner();
29092				this.initWishlistPageHandlers();
29093				this.checkState();
29094			} else {
29095				self.wrapper.find('.add-to-wishlist').click(function (e) {
29096					myDeco.user.ajaxLoginClickHandler(e, null, 'login');
29097				});
29098				self.removeGroupsPopup();
29099			}
29100			myDeco.body.bind('mydeco.login', function() {
29101				self.init();
29102			});
29103		}
29104	},
29105
29106	/**
29107	* Load products and folders from localStorage
29108	* @param {Boolean} products (true - to load products)
29109	* @param {Boolean} folders (true - to load folders)
29110	*/
29111	load: function (products, folders) {
29112		var self = this;
29113
29114		if (products) {
29115			myDeco.storage.get(this.storeName, function (ok, data) {
29116				self.data = JSON.parse(data || 'null') || {};
29117			});
29118		}
29119
29120		if (folders) {
29121			myDeco.storage.get(this.storeFolders, function (ok, data) {
29122				self.folders = JSON.parse(data || 'null') || {};
29123				self.loadGroups();
29124			});
29125		}
29126	},
29127
29128	update: function (item) {
29129		var self = this, i,
29130			guid = item.data('product'),
29131			groups = $(self.data[guid]);
29132
29133		for (i = 0; i < groups.length; i++) {
29134			self.updateButton(item.find('a[data-group="' + groups[i] + '"]'), true);
29135		}
29136	},
29137
29138	updateButton: function (button, action) {
29139		button.toggleClass('inactive', action);
29140		this.checkState(button.closest('.wishlist-block'));
29141	},
29142
29143	checkState: function (button) {
29144		var items, i, guid, item, self = this;
29145
29146		items = button || this.wrapper.find('.wishlist-block');
29147
29148		for (i = 0; i < items.length; i++) {
29149			item = $(items[i]);
29150			guid = item.data('product');
29151			item.toggleClass('wishlisted', !!self.data[guid]);
29152		}
29153	},
29154
29155	save: function (guid, group) {
29156		// guid can be an array of guids or one guid
29157		var self = this, i, arr = [];
29158		if (typeof guid === 'string') {
29159			guid = [guid];
29160		}
29161
29162		for (i = 0; i < guid.length; i++) {
29163			arr = self.data[guid[i]] || [];
29164			if (arr.indexOf(group) < 0) {
29165				arr.push(group);
29166				self.data[guid[i]] = arr;
29167			}
29168		}
29169
29170		myDeco.storage.set(self.storeName, JSON.stringify(self.data));
29171	},
29172
29173	remove: function (guid, group) {
29174		// guid can be an array of guids or one guid
29175		var self = this, i, arr = [], index;
29176		if (typeof guid === 'string') {
29177			guid = [guid];
29178		}
29179
29180		for (i = 0; i < guid.length; i++) {
29181			arr = self.data[guid[i]] || [];
29182			index = arr.indexOf(group);
29183			arr.splice(index, 1);
29184			if (arr.length <= 0) {
29185				delete self.data[guid[i]];
29186			}
29187		}
29188
29189		myDeco.storage.set(self.storeName, JSON.stringify(self.data));
29190	},
29191
29192	removeGroup: function (group) {
29193		var self = this, guid, data;
29194
29195		for (guid in self.data) {
29196			data = self.data[guid];
29197			index = data.indexOf(group);
29198			if (index !== -1) {
29199				data.splice(index, 1);
29200			}
29201			if (data.length <= 0) {
29202				delete (self.data[guid]);
29203			}
29204		}
29205
29206		delete self.folders[group];
29207		myDeco.storage.set(self.storeName, JSON.stringify(self.data));
29208		myDeco.storage.set(self.storeFolders, JSON.stringify(self.folders));
29209	},
29210
29211	sendOne: function (button, product) {
29212		// Add item to wishlist group
29213		var self = this,
29214			guid = product || button.closest('.wishlist-block').data('product'),
29215			group_id = button.data('group'),
29216			data = {
29217				'wishlist': guid || '',
29218				'group_id': group_id || ''
29219			},
29220			firstKey;
29221
29222		if (!self.sending) {
29223			$.ajax({
29224				url: myDeco.urls.addShortlistProduct,
29225				type: 'POST',
29226				dataType: 'json',
29227				data: $.param(data),
29228				beforeSend: function() { self.sending = true; },
29229				complete: function() { self.sending = false; },
29230				success: function (data) {
29231					if (data.wishlisted) {
29232						// get first element from object
29233						firstKey = $.firstElementKey(data.wishlisted);
29234						// result has map of guid to list of group ids
29235						self.save(guid, data.wishlisted[firstKey][0]);
29236						if (button.length) {
29237							self.updateButton(button, true);
29238						}
29239					}
29240				}
29241			});
29242
29243			myDeco.tracking.trackEvent('shortlist', 'product', guid);
29244		}
29245	},
29246
29247	removeOne: function (button) {
29248		// Delete item from wishlist group
29249		var self = this,
29250			guid = button.closest('.wishlist-block').data('product'),
29251			group_id = button.data('group'),
29252			data = {
29253				'guid': guid,
29254				'group_id': group_id,
29255				'current_group_id': group_id
29256			};
29257		if (!self.removing) {
29258			$.ajax({
29259				url: myDeco.urls.removeShortlistedProduct,
29260				type: 'POST',
29261				dataType: 'json',
29262				data: $.param(data),
29263				beforeSend: function() { self.removing = true; },
29264				complete: function() { self.removing = false; },
29265				success: function (data) {
29266					if (data.success) {
29267						self.remove(guid, group_id);
29268						self.updateButton(button, false);
29269					}
29270				}
29271			});
29272		}
29273	},
29274
29275	checkShortlistOwner: function() {
29276		var self = this;
29277		myDeco.storage.get('wishlist_last_username', function (ok, data) {
29278			if (data !== myDeco.username) {
29279				myDeco.storage.remove(self.storeName);
29280				myDeco.storage.remove(self.storeFolders);
29281				self.sync(true, true);
29282			}
29283		});
29284	},
29285
29286	periodicalSync: function() {
29287		var self = this, hours;
29288		myDeco.storage.get('wishlist_last_sync', function (ok, data) {
29289			hours = Math.round(new Date() / (1000 * 60 * 60));
29290			if ((!data || hours > +data + myDeco.settings.SHORTLIST_SYNC_INTERVAL)) {
29291				self.sync(true, true);
29292			}
29293		});
29294	},
29295
29296	/**
29297	* Retrieve shortlisted product guids and user folders from server.
29298	* @param {Boolean} products (true - to load products)
29299	* @param {Boolean} folders (true - to load folders)
29300	*/
29301	sync: function (folders, products) {
29302		var self = this;
29303
29304		if (myDeco.username) {
29305			if (folders) {
29306				// Get list of folders
29307				$.ajax({
29308					url: myDeco.urls.wishlistGroupPopinList,
29309					type: 'GET',
29310					data: $.param({'username': myDeco.username}),
29311					dataType: 'html',
29312					success: function (data) {
29313						myDeco.storage.remove(self.storeFolders);
29314						myDeco.storage.set(self.storeFolders, data);
29315						self.load(false, true);
29316					}
29317				});
29318			}
29319			if (products) {
29320				// Get wishlisted products
29321				$.ajax({
29322					url: myDeco.urls.addShortlistProduct,
29323					type: 'GET',
29324					dataType: 'json',
29325					success: function (data) {
29326						if (data.wishlisted) {
29327							myDeco.storage.remove(self.storeName);
29328							myDeco.storage.set(self.storeName, JSON.stringify(data.wishlisted));
29329							self.load(true, false);
29330						}
29331					}
29332				});
29333			}
29334			// Set last user name
29335			myDeco.storage.set('wishlist_last_username', myDeco.username);
29336		} else {
29337			myDeco.storage.remove(self.storeFolders);
29338			myDeco.storage.remove(self.storeName);
29339		}
29340
29341		// Set last time sync
29342		myDeco.storage.set('wishlist_last_sync', Math.round(new Date() / (1000 * 60 * 60)));
29343	},
29344
29345	reset: function() {
29346		myDeco.storage.remove(self.storeName);
29347		myDeco.storage.remove(self.storeFolders);
29348		myDeco.storage.remove('wishlist_last_sync');
29349	},
29350
29351	loadGroups: function() {
29352		var self = this, folders_list = '', wishlist_form;
29353		self.removeGroupsPopup();
29354
29355		for (folder_id in self.folders) {
29356			folders_list += '<a class="wishlist-action" data-group="' + folder_id + '" href="#">' + self.folders[folder_id] + '</a>';
29357		}
29358
29359		wishlist_form = '<div id="wislist-links-data" style="display: none;">'
29360			+ '  <div class="wishlist-links">'
29361			+ '    <div class="wishlist-inner">'
29362			+ '      <div class="wishlist-items">'
29363			+ '        <div class="add-title">Add to wishlist</div>'
29364			+ '        <div class="move-title">Move to wishlist</div>'
29365			+ '        <div class="list">'
29366			+            folders_list
29367			+ '        </div>'
29368			+ '        <div class="loader"></div>'
29369			+ '      </div>'
29370			+ '      <a href="" class="wishlist-create">Create new wishlist</a>'
29371			+ '      <div class="wishlist-form-add">'
29372			+ '        <form method="POST" class="wishlistFormAdd">'
29373			+ '          <input type="text" name="name" class="text-field wishlist-name" autocomplete="off" value="" placeholder="Wishlist name" />'
29374			+ '          <input type="submit" class="mdtwo-button no-decorate" value="Create" />'
29375			+ '        </form>'
29376			+ '      </div>'
29377			+ '    </div>'
29378			+ '  </div>'
29379			+ '</div>';
29380
29381		myDeco.body.append(wishlist_form);
29382		self.buttonHover();
29383	},
29384
29385	/*
29386	* Add new folder and include or move product to it
29387	*/
29388	wishlistAddGroup: function (e) {
29389		e.stop();
29390		var self = this, params,
29391			form = $(e.target),
29392			name = form.find('.wishlist-name').val(),
29393			itemsWrap = form.closest('.wishlist-links').find('.wishlist-items'),
29394			itemsList = itemsWrap.find('.list'),
29395			itemsLoader = itemsWrap.find('.loader'),
29396			block = form.closest('.wishlist-block'),
29397			_data = block.data(),
29398			guid = _data.product,
29399			wishlist_product = _data.wishlist_product,
29400			current_group_id = _data.current_group_id,
29401			moving = block.hasClass('wishlist-move'),
29402			data = moving ? {'name': name} : {'name': name, 'guid': guid},
29403			error_message = 'Please type Name of wishlist';
29404
29405		if (!name) {
29406			myDeco.popin.error({
29407				message: error_message
29408			});
29409		} else {
29410			itemsList.fadeTo('fast', 0);
29411			itemsLoader.show();
29412
29413			$.ajax({
29414				url: myDeco.urls.wishlistGroupEdit,
29415				type: 'POST',
29416				dataType: 'json',
29417				data: $.param(data),
29418				success: function (data) {
29419					if (data.success) {
29420						// Save folder to localStorage
29421						self.saveGroup(data.folder_id, name);
29422						if (moving) {
29423							// Move item to new folder
29424							params = {
29425								'shortlist_items': wishlist_product,
29426								'group_id': data.folder_id,
29427								'current_group_id': current_group_id
29428							};
29429							self.moveItem(params);
29430						} else {
29431							// Add item to new folder
29432							self.save(guid, data.folder_id);
29433							itemsLoader.hide();
29434							itemsList
29435								.prepend('<a class="wishlist-action inactive" data-group="' + data.folder_id + '" href="">' + name + '</a>')
29436								.fadeTo('fast', 1);
29437						}
29438
29439						self.wrapper = myDeco.body;
29440						self.checkState();
29441
29442						$('.wishlist-block').removeClass('active');
29443					} else {
29444						myDeco.popin.error({
29445							message: data.message.name[0]
29446						});
29447						itemsLoader.hide();
29448						itemsList.fadeTo('fast', 1);
29449					}
29450				}
29451			});
29452		}
29453	},
29454
29455	/*
29456	* Save folder to localStorage
29457	*/
29458	saveGroup: function (id, name) {
29459		this.folders[id] = name;
29460		myDeco.storage.set(this.storeFolders, JSON.stringify(this.folders));
29461		this.load(false, true);
29462	},
29463
29464	/*
29465	* Remove popup
29466	*/
29467	removeGroupsPopup: function() {
29468		$('#wislist-links-data').remove();
29469	},
29470
29471	wishlistPopupInit: function (button) {
29472		// Init popup links for wishlist popup
29473		if (!button.hasClass('active')) {
29474			var self = this,
29475				data = $('#wislist-links-data').html();
29476
29477			button
29478				.addClass('active')
29479				.find('.wishlist-links-wrapper')
29480				.html(data);
29481
29482			self.initHandlers(button);
29483			self.update(button);
29484			self.setActiveWishlist(button);
29485		}
29486	},
29487
29488	buttonHover: function() {
29489		var self = this, button;
29490		// Show/hide wishlist popup
29491		self.wrapper.find('.wishlist-block')
29492			.hover(function() {
29493				self.wishlistPopupInit($(this).addClass('hover'));
29494			}, function() {
29495				$(this)
29496					.removeClass('hover')
29497					.find('.wishlist-create')
29498					.show()
29499					.next('.wishlist-form-add')
29500					.hide();
29501			});
29502	},
29503
29504	initHandlers: function (button) {
29505		// Add events handlers for wishlist popup
29506		var self = this, item, block, _data, wishlist_product, current_group_id, new_group_id;
29507
29508		button
29509			.find('.wishlist-action')
29510			.click(function (e) {
29511				e.stop();
29512				item = $(this);
29513				if (button.hasClass('wishlist-add')) {
29514					// Handler for adding new item to wishlist group
29515					if (item.hasClass('inactive')) {
29516						self.removeOne(item);
29517					} else {
29518						self.sendOne(item);
29519					}
29520				} else {
29521					// Handler for moving item to another wishlist
29522					block = item.closest('.wishlist-block');
29523					_data = block.data();
29524					wishlist_product = _data.wishlist_product;
29525					current_group_id = _data.current_group_id;
29526					new_group_id = item.data('group');
29527
29528					if (item.hasClass('inactive')) {
29529						return;
29530					}
29531
29532					params = {
29533						'shortlist_items': wishlist_product,
29534						'group_id': new_group_id,
29535						'current_group_id': current_group_id
29536					};
29537					self.moveItem(params);
29538				}
29539			});
29540
29541		// Show new group form and adding new wishlist group
29542		button
29543			.find('.wishlist-create')
29544			.click(function (e) {
29545				e.stop();
29546				$(this)
29547					.hide()
29548					.next('.wishlist-form-add')
29549					.show();
29550			})
29551			.end()
29552			.find('.wishlistFormAdd')
29553			.submit(self.wishlistAddGroup.bind(self));
29554	},
29555
29556	/**
29557	* My wishlist page functions
29558	*/
29559	initWishlistPageHandlers: function() {
29560		var self = this, button,
29561			popupProp = {
29562				yesButtonText: 'Confirm',
29563				noButtonText: 'No thanks!',
29564				className: 'mdtwo-alert-popin',
29565				holderClassName: 'mdtwo-alert-content-holder',
29566				backgroundClassName: 'mdtwo-alert-background',
29567				width: 440
29568			};
29569
29570		// Handler for delete product from wishlist
29571		$('.delete-product')
29572			.click(function (e) {
29573				e.stop();
29574				button = $(this);
29575				myDeco.popin.confirm($.extend({}, popupProp, {
29576					message: 'Are you sure you want to remove this product from wishlist?',
29577					yesButton: function (e) {
29578						self.deleteWishlistItem(button);
29579					}
29580				}));
29581			});
29582
29583		// Handler for delete wishlist
29584		$('.wishlist-del')
29585			.click(function (e) {
29586				e.stop();
29587				button = $(this);
29588				myDeco.popin.confirm($.extend({}, popupProp, {
29589					message: 'Are you sure you want to delete the entire wishlist?',
29590					yesButton: function (e) {
29591						self.deleteWishlist(button);
29592					}
29593				}));
29594			});
29595
29596		// Callback for edit title of wishlist
29597		if (myDeco.editable) {
29598			myDeco.editable.callback = function (form) {
29599				if (form.hasClass('resync')) {
29600					self.sync(true, false);
29601					$('.wishlist-block').removeClass('active');
29602				}
29603			};
29604		}
29605	},
29606
29607	setActiveWishlist: function (block) {
29608		var scope = $(block).find('.wishlist-action'),
29609			wrapper = $('#wishlist-items'),
29610			current_group_id = wrapper.data('group');
29611
29612		if (wrapper.length > 0) {
29613			scope.each(function() {
29614				var button = $(this);
29615				if (button.data('group') === current_group_id) {
29616					button.addClass('inactive');
29617				}
29618			});
29619		}
29620	},
29621
29622	deleteWishlistItem: function (item) {
29623		var self = this,
29624			_data = item.data(),
29625			guid = _data.product,
29626			group_id = _data.group,
29627			container = $('#wishlist-items'),
29628			data = {
29629				'guid': guid,
29630				'group_id': group_id,
29631				'current_group_id': group_id
29632			};
29633
29634		$.ajax({
29635			url: myDeco.urls.removeShortlistedProduct,
29636			type: 'POST',
29637			dataType: 'json',
29638			data: $.param(data),
29639			success: function (data) {
29640				if (data.success) {
29641					self.remove(guid, group_id);
29642					container.html(data.wishlist_products_content);
29643					self.initButtons();
29644				}
29645			}
29646		});
29647	},
29648
29649	moveItem: function (params) {
29650		var self = this,
29651			container = $('#wishlist-items');
29652
29653		$.ajax({
29654			url: myDeco.urls.wishlistGroupMoveItems,
29655			type: 'POST',
29656			data: $.param(params),
29657			dataType: 'json',
29658			success: function (data) {
29659				if (data.success) {
29660					container.html(data.wishlist_products_content);
29661					self.sync(false, true);
29662					self.initButtons();
29663				} else {
29664					myDeco.popin.error({
29665						message: data.message
29666					});
29667				}
29668			},
29669			error: function() {
29670				myDeco.popin.error({
29671					message: 'Error of relocation of a product'
29672				});
29673			}
29674		});
29675	},
29676
29677	deleteWishlist: function (item) {
29678		var self = this,
29679			group_id = item.data('group');
29680
29681		$.ajax({
29682			url: myDeco.urls.wishlistGroupRemove,
29683			type: 'POST',
29684			dataType: 'json',
29685			data: $.param({'group_id': group_id}),
29686			success: function (data) {
29687				if (data.success) {
29688					self.removeGroup(group_id);
29689					window.location = myDeco.addCacheMissParam(myDeco.urls.profilePublicWishlist + 'edit/');
29690				}
29691			}
29692		});
29693	},
29694
29695	initButtons: function() {
29696		myDeco.sharethis.init();
29697		myDeco.editable._inited = false;
29698		myDeco.editable.init();
29699		myDeco.basketUI.init();
29700		this.buttonHover();
29701		this.initWishlistPageHandlers();
29702		myDeco.infoPopups.init();
29703		myDeco.shortlist.initScroll($('.wishlist-products-list'));
29704	},
29705
29706	/**
29707	* Init lazy scroll for wishlist page
29708	*/
29709	initScroll: function (resultsContainer) {
29710		if (resultsContainer.length) {
29711			resultsContainer.lazyLoad({
29712				url: myDeco.urls.resultsUrl,
29713				step: 1,
29714				startFrom: 1,
29715				loadParam: 'page',
29716				scrollingOffset: 600,
29717				hasNext: resultsContainer.data('next'),
29718				appendMethod: function (container, data) {
29719					var wishlistData = $(data).children();
29720					this.hasNext = $(data).data('next');
29721					container.find('.wishlist-column').each(function (i, el) {
29722						$(el).append($(wishlistData[i]).children());
29723					});
29724				},
29725				callback: function() {
29726					myDeco.basketUI.buttonClick(resultsContainer);
29727					myDeco.shortlist.init(resultsContainer);
29728					myDeco.infoPopups.init(resultsContainer);
29729					myDeco.editable._inited = false;
29730					myDeco.editable.init();
29731				}
29732			});
29733		}
29734	}
29735};
29736
29737myDeco.storage.onready(myDeco.shortlist.init.bind(myDeco.shortlist));
29738
29739$(function() {
29740	myDeco.shortlist.initScroll($('.wishlist-products-list'));
29741});
29742/*globals $, window, myDeco, document, tmpl */
29743/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: true, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
29744/**
29745 * @fileOverview Component for replacing lightwindow with a little small functionality
29746 */
29747
29748if (typeof myDeco.popin === 'undefined') {
29749	myDeco.popin = {
29750		/**
29751		 * Default options
29752		 */
29753		options: {
29754			base: 'popin',
29755			width: 616,
29756			height: 200,
29757			url: '',
29758			afterOpen: null,
29759			beforeClose: null,
29760			closeIn: 0,
29761			fullscreen: false,
29762			fullscreenMargin: 30,
29763			autoResizeX: false,
29764			autoResizeY: true,
29765			ajaxLoaderClass: 'ajaxLoader',
29766			template: null,
29767			borders: 50,
29768			showClose: true,
29769			closeText: '',
29770			className: '',
29771			holderClassName: '',
29772			backgroundClassName: '',
29773			tracking: false,
29774			context: false
29775		},
29776		
29777		/**
29778		 * observers cache
29779		 */
29780		_cclose: null,
29781		_ccloseByKeyPress: null,
29782		_cresize: null,
29783		
29784		/**
29785		 * Current window options
29786		 */
29787		windowOptions: {},
29788		
29789		/**
29790		 * Visibility marker
29791		 */
29792		visible: false,
29793		
29794		/**
29795		 * Content holder variable
29796		 */
29797		holder: '',
29798		
29799		/**
29800		 * Content layer
29801		 */
29802		contentContainer: '',
29803		
29804		/**
29805		 * Background layer
29806		 */
29807		background: '',
29808		
29809		/**
29810		 * Background opacity level, will fetch from css 'opacity' property; default is 1
29811		 * @type {Number}
29812		 */
29813		backgroundOpacity: 1,
29814		
29815		/**
29816		 * Init function
29817		 * @param {Object} options Defailt options for initialisation
29818		 */
29819		init: function(options) {
29820			if (this._cclose) {
29821				return;
29822			}
29823			this.options = $.extend(true, this.options, options || {});
29824			this._cclose = this.close.bind(this);
29825			this._ccloseByKeyPress = this._closeByKeyPress.bind(this);
29826			this._cresize = this.resize.bind(this);
29827		},
29828		
29829		/**
29830		 * Open popin with specified parameters, or set a new parameters and content on already opened popin
29831		 * @param {Object} options Options for new window
29832		 */
29833		open: function(options) {
29834			// lazy load
29835			this.init();
29836			if (this.visible && $.isFunction(this.windowOptions.beforeClose)) {
29837				this.windowOptions.beforeClose();
29838			}
29839			
29840			// extend options to full size
29841			this.windowOptions = $.extend(true, {}, this.options, options || {});
29842			
29843			// return if url is empty
29844			if (this.windowOptions.url === '' && this.windowOptions.template === null) {
29845				return;
29846			}
29847	
29848			// content holder node
29849			var holderId = this.windowOptions.base + '-content-holder',
29850				// content node
29851				contentId = this.windowOptions.base + '-content',
29852				self = this,
29853				closeTitle;
29854			
29855			if (!this.visible) {
29856				this.holder = $('<div id="' + holderId + '" class="' + holderId + '" style="display:none;position:absolute"></div>')
29857					.appendTo(myDeco.body);
29858		
29859				this.contentContainer =  $('<div class="clearfix ' + contentId + '" id="' + contentId + '"></div>')
29860					.appendTo(this.holder);
29861		
29862				if (this.windowOptions.showClose) {
29863					this.holder.append('<a href="#" class="' + this.windowOptions.base +
29864						'-close-title">' + this.windowOptions.closeText + '</a>');
29865				}
29866				
29867				// push it into page
29868				this.contentContainer.hide();
29869	
29870				this.holder.css({
29871					display: 'block',
29872					left: parseInt(myDeco.window.scrollLeft() + myDeco.window.width() / 2, 10) + 'px',
29873					top: parseInt(myDeco.window.scrollTop() + myDeco.window.height() / 2, 10) + 'px',
29874					width: 0,
29875					height: 0
29876				});
29877				
29878				this._showBackground();
29879				
29880				// document wide listeners
29881				myDeco.window.keypress(this._ccloseByKeyPress).bind('resize', this._cresize);
29882				
29883				myDeco.body.bind('close.popin', this._cclose);
29884			} else {
29885				closeTitle = this.holder.find('>.' + this.windowOptions.base + '-close-title');
29886				if (this.windowOptions.showClose && closeTitle.size() === 0) {
29887					this.holder.append('<a href="#" class="' + this.windowOptions.base + '-close-title">' + this.windowOptions.closeText + '</a>');
29888				} else if (!this.windowOptions.showClose && closeTitle.size() !== 0) {
29889					closeTitle.remove();
29890				} else {
29891					closeTitle.text(this.windowOptions.closeText);
29892				}
29893				
29894				if ($.isFunction(this.windowOptions.beforeCreate)) {
29895					this.windowOptions.beforeCreate(this.contentContainer);
29896				}
29897				this.holder.find('.' + this.windowOptions.base + '-close-title').unbind('click', this._cclose);
29898				this.contentContainer.find('.' + this.windowOptions.base + '-close').unbind('click', this._cclose);
29899				this._hideContent();
29900			}
29901			
29902			// cleanup class names
29903			this.holder.attr('className', holderId);
29904			this.contentContainer.attr('className', contentId);
29905
29906			this.holder.toggleClass(this.windowOptions.base + '-fullscreen', this.windowOptions.fullscreen);
29907			
29908			if (this.windowOptions.className) {
29909				this.contentContainer.addClass(this.windowOptions.className);
29910			}
29911			
29912			if (this.windowOptions.holderClassName) {
29913				this.holder.addClass(this.windowOptions.holderClassName);
29914			}
29915
29916			// add custom class name for popup's background
29917			if (this.windowOptions.backgroundClassName) {
29918				this.background.addClass(this.windowOptions.backgroundClassName);
29919			}
29920
29921			// hide problem elements in IE
29922			this._hideProblemElements();
29923			
29924			// update content
29925			if (this.windowOptions.template || this.windowOptions.url.indexOf('#') === 0) {
29926				// inline content
29927				this.windowOptions.template = this.windowOptions.template || $(this.windowOptions.url).html();
29928				if (this.windowOptions.url.substring(0, 1) === '#') {
29929					self.contentContainer.addClass(this.windowOptions.url.substring(1));
29930				}
29931				this._processContent();
29932			} else {
29933				this.contentContainer.html('').addClass(this.windowOptions.ajaxLoaderClass);
29934				$.get(this.windowOptions.url, function(template) {
29935					self.windowOptions.template = template;
29936					self.contentContainer.removeClass(self.windowOptions.ajaxLoaderClass);
29937					self._processContent();
29938				});
29939			}
29940			
29941			return this;
29942		},
29943		
29944		_processContent: function() {
29945			var self = this, trackingLabel = this.windowOptions.tracking;
29946			
29947			this.contentContainer.html(this.windowOptions.context !== false ?
29948				tmpl(this.windowOptions.template, this.windowOptions.context || {}) :
29949				this.windowOptions.template);
29950				
29951			// show content
29952			this._showContent();
29953			
29954			// listeners
29955			this.holder.find('.' + this.windowOptions.base + '-close-title').bind('click', this._cclose);
29956			this.contentContainer.find('.' + this.windowOptions.base + '-close').bind('click', this._cclose);
29957			
29958			// after create callback
29959			if ($.isFunction(this.windowOptions.afterOpen)) {
29960				this.windowOptions.afterOpen(this.contentContainer);
29961			}
29962			
29963			// track this popin as a page view in google analytics
29964			if (trackingLabel) {
29965				if (typeof trackingLabel !== 'string') {
29966					trackingLabel = this.windowOptions.className || this.windowOptions.base;
29967				}
29968				myDeco.tracking.trackPageview(window.location.pathname + '#' + trackingLabel);
29969			}
29970	
29971			// resize area
29972			this.resize();
29973			
29974			this.visible = true;
29975	
29976			// set autoclose callback
29977			if (this.windowOptions.closeIn > 0) {
29978				// autoclose marker to content 
29979				this.contentContainer.timed = true;
29980				window.setTimeout(function() {
29981					if (self.contentContainer.timed) {
29982						self.close();
29983					}
29984				}, this.windowOptions.closeIn * 1000);
29985			}
29986		},
29987		
29988		/**
29989		 * Close active popin
29990		 * @param {Event} event Click event from elements or nothing in case of manual hitting
29991		 */
29992		close: function(e) {
29993			if (e) {
29994				e.stop();
29995			}
29996			
29997			// we do not want close an already closed popin
29998			if (!this.visible) {
29999				return false;
30000			}
30001			
30002			// run before close callback
30003			if ($.isFunction(this.windowOptions.beforeClose)) {
30004				this.windowOptions.beforeClose(this.contentContainer);
30005			}
30006			
30007			// stop our observers
30008			myDeco.window
30009				.unbind('keypress', this._ccloseByKeyPress)
30010				.unbind('resize', this._cresize);
30011			
30012			myDeco.body.unbind('close.popin', this._cclose);
30013			this.holder.find('.' + this.windowOptions.base + '-close-title').unbind('click', this._cclose);
30014			this.contentContainer.find('.' + this.windowOptions.base + '-close').unbind('click', this._cclose);
30015			
30016			// hide popin
30017			this.visible = false;
30018			this._hideHolder();
30019			this._hideBackground();
30020			// show problem elements
30021			this._showProblemElements();
30022		},
30023		
30024		/**
30025		 * Return content element, usable for manipulating with content elements
30026		 */
30027		content: function() {
30028			return this.contentContainer;
30029		},
30030		
30031		/**
30032		 * Return content holder element
30033		 */
30034		contentHolder: function() {
30035			return this.holder;
30036		},
30037		
30038		/**
30039		 * Return current state of window
30040		 */
30041		isOpen: function() {
30042			return this.visible;
30043		}, 
30044		
30045		/**
30046		 * Resize current popin to the new size
30047		 * @param {Number} width New popin width
30048		 * @param {Number} height New popin height
30049		 */
30050		resize: function(width, height) {
30051			if (typeof width === 'object'){
30052				width = undefined;
30053			}
30054			if (typeof height === 'object') {
30055				height = undefined;
30056			}
30057			var offsetLeft = myDeco.window.scrollLeft(),
30058				offsetTop = myDeco.window.scrollTop(),
30059				wWidth = myDeco.window.width(),
30060				wHeight = myDeco.window.height(),
30061				difference = 0,
30062				to = {
30063					width: width || (this.windowOptions.fullscreen ?
30064							wWidth - this.windowOptions.fullscreenMargin * 2 :
30065							this.windowOptions.width),
30066					height: height || (this.windowOptions.fullscreen ?
30067							wHeight - this.windowOptions.fullscreenMargin * 2 :
30068							this.windowOptions.height)
30069				},
30070				corner = {
30071					left: parseInt(offsetLeft + (wWidth - to.width) / 2, 10),
30072					top: parseInt(offsetTop + (wHeight - to.height) / 2, 10)
30073				};
30074			
30075			// move and resize
30076			this.holder.css({
30077				left: corner.left + 'px',
30078				top: corner.top + 'px',
30079				width: to.width + 'px',
30080				height: to.height + 'px'
30081			});
30082			
30083			if (!this.windowOptions.fullscreen) {
30084				if (this.windowOptions.autoResizeX) {
30085					to.width = this.contentContainer.innerWidth();
30086					difference = wWidth - this.contentContainer.outerWidth(true) - this.windowOptions.borders * 2;
30087					if (difference < 0) {
30088						to.width += difference;
30089					}
30090					corner.left = parseInt(offsetLeft + (wWidth - to.width) / 2, 10);
30091				}
30092				
30093				if (this.windowOptions.autoResizeY) { 
30094					to.height = this.contentContainer.innerHeight();
30095					difference = wHeight - this.contentContainer.outerHeight(true) - this.windowOptions.borders * 2;
30096					if (difference < 0) {
30097						to.height += difference;
30098					}
30099					corner.top = parseInt(offsetTop + (wHeight - to.height) / 2, 10);
30100				}
30101				
30102				if (this.windowOptions.autoResizeX || this.windowOptions.autoResizeY) {
30103					this.holder.css({
30104						left: corner.left + 'px',
30105						top: corner.top + 'px',
30106						width: to.width + 'px',
30107						height: to.height + 'px'
30108					});
30109				}
30110			}
30111			
30112			// finally fix background
30113			this._showBackground();
30114		},
30115		
30116		/**
30117		 * Show content layer, maybe smooth in feature
30118		 * 
30119		 * @private
30120		 */
30121		_showContent: function() {
30122			this.contentContainer.show();
30123		},
30124		
30125		/**
30126		 * Hide content layer, maybe smooth in feature
30127		 * 
30128		 * @private
30129		 */
30130		_hideContent: function() {
30131			this.contentContainer.hide();
30132		},
30133		
30134		/**
30135		 * Hide content holder, maybe smooth in feature
30136		 */
30137		_hideHolder: function() {
30138			this.holder.remove();
30139		},
30140		
30141		/**
30142		 * Show background overlay
30143		 * @param {Object} options New popin options
30144		 * 
30145		 * @private
30146		 */
30147		_showBackground: function() {
30148			var xScroll, yScroll, dimensions = {}, windowWidth, windowHeight,
30149				backgroundId = this.windowOptions.base + '-background';
30150			
30151			if (window.innerHeight && window.scrollMaxY) {
30152				xScroll = document.body.scrollWidth;
30153				yScroll = window.innerHeight + window.scrollMaxY;
30154			} else if (document.body.scrollHeight > document.body.offsetHeight) {
30155				xScroll = document.body.scrollWidth;
30156				yScroll = document.body.scrollHeight;
30157			} else {
30158				xScroll = document.body.offsetWidth;
30159				yScroll = document.body.offsetHeight;
30160			}
30161	
30162			if (document.documentElement && document.documentElement.clientHeight) {
30163				windowWidth = document.documentElement.clientWidth;
30164				windowHeight = document.documentElement.clientHeight;
30165			} else if (document.body) {
30166				windowWidth = document.body.clientWidth;
30167				windowHeight = document.body.clientHeight;
30168			}
30169			
30170			dimensions.height = Math.max(windowHeight, yScroll);
30171			dimensions.width = Math.max(windowWidth, xScroll);
30172	
30173			if (!this.background || this.background.size() === 0) {
30174				this.background = $('<div id="' + backgroundId + '" class="' + backgroundId + '" />').css({
30175					position: 'absolute',
30176					display: 'none',
30177					left: 0,
30178					top: 0
30179				});
30180				myDeco.body.append(this.background);
30181				
30182				// now try to find original opacity
30183				this.backgroundOpacity = (this.background[0].currentStyle ?
30184					this.background[0].currentStyle.opacity :
30185					                                 // Chromium returns opcity as a string with the
30186					                                 // comma as delimiter
30187					this.background.css('opacity')).toString().replace(',', '.');
30188				
30189				this.background
30190					.css({
30191						opacity: 0,
30192						display: 'block'
30193					})
30194					.show();
30195			} else {
30196				this.background
30197					.stop(true, false)
30198					.attr('className', backgroundId + ' ' + this.windowOptions.backgroundClassName)
30199					.show();
30200			}
30201			
30202			this.background
30203				.width(dimensions.width)
30204				.height(dimensions.height)
30205				.fadeTo('fast', this.backgroundOpacity);
30206		},
30207		
30208		/**
30209		 * Hide background
30210		 * 
30211		 * @private
30212		 */
30213		_hideBackground: function() {
30214			this.background.fadeTo('fast', 0, function() {
30215				$(this).hide();
30216			});
30217		},
30218		
30219		/**
30220		 * Hide problem elements in IE 6
30221		 * 
30222		 * @private
30223		 */
30224		_hideProblemElements: function() {
30225			if (myDeco.browser.isIE6 && !this.visible) {
30226				$.each($('select, object, embed, iframe'), function(i, element) {
30227					element = $(element);
30228					element
30229						.data('visibility', element.css('visibility'))
30230						.css('visibility', 'hidden');
30231				});
30232			}
30233		},
30234		
30235		/**
30236		 * Restore problem elements to its previous state
30237		 * 
30238		 * @private
30239		 */
30240		_showProblemElements: function() {
30241			if (myDeco.browser.isIE6) {
30242				$.each($('select, object, embed, iframe'), function(i, element) {
30243					element = $(element);
30244					var visibility = element.data('visibility');
30245					if (visibility) {
30246						element.css('visibility', visibility);
30247					}
30248				});
30249			}
30250		},
30251		
30252		/**
30253		 * Keypress observer, waits for ESC key and close popin
30254		 * @param {Event} e Keypress event
30255		 */
30256		_closeByKeyPress: function(e) {
30257			if (e.keyCode === 27) {
30258				this.close();
30259			}
30260		},
30261		
30262		/**
30263		 * Extend current object with plugin.
30264		 * Plugin will be available as myDeco.popin.name
30265		 * @param {String} name Plugin name
30266		 * @param {Object} defoptions Default options for plugin
30267		 * @param {Function} callback Callback which will be started before afterOpen callback 
30268		 */
30269		plugin: function(name, defoptions, callback) {
30270			this.init();
30271			var self = this;
30272			if (typeof this[name] !== 'undefined') {
30273				throw "Name is already occuped";
30274			}
30275			
30276			defoptions = defoptions || {};
30277			callback = callback || function() {};
30278			this[name] = function(options) {
30279				options = options || {};
30280				var afterOpen = options.afterOpen ? function(div) {
30281					(callback.bind(self))(div);
30282					options.afterOpen(div);
30283				} : callback.bind(self);
30284				this.open($.extend(true, {}, defoptions, options, {afterOpen: afterOpen}));
30285			};
30286		}
30287	};
30288	
30289	$(function() {
30290		var popin = myDeco.popin;
30291		popin.init.apply(popin);
30292		
30293		/**
30294		 * Replacement for standard alert() function
30295		 * @param {Object} options Additionally to default options are:
30296		 *   closeButtonText {String} default 'Close' text will replace with this value
30297		 *   title {String} replacement for title, will remove if no specified
30298		 *   message {String} replacement for message, will remove if no specified
30299		 */
30300		popin.plugin('alert', {
30301			template: '<h2 class="top-line title">Alert</h2><p class="message"></p>' +
30302				'<div class="toolbar"><a href="#" class="button green-button popin-close">' +
30303				'<span class="close-button-text">Close</span></a></div>'
30304		}, function(div) {
30305			if (this.windowOptions.closeButtonText) {
30306				div.find('.close-button-text').html(this.windowOptions.closeButtonText);
30307			}
30308			if (this.windowOptions.title) {
30309				div.find('.title').html(this.windowOptions.title);
30310			} else {
30311				div.find('.title').remove();
30312			}
30313			if (this.windowOptions.message) {
30314				div.find('.message').html(this.windowOptions.message);
30315			} else {
30316				div.find('.message').remove();
30317			}
30318		});
30319		
30320		/**
30321		 * Replacement for standard alert() function
30322		 * @param {Object} options Additionally to default options are:
30323		 *   yesButtonText {String} default 'Yes' text will replace with this value
30324		 *   noButtonText {String} default 'No' text will replace with this value
30325		 *   yesButton {Function} defaults to nothing
30326		 *   noButton {Function} defaults to close popin
30327		 *   title {String} replacement for title, will remove if no specified
30328		 *   message {String} replacement for message, will remove if no specified
30329		 */
30330		popin.plugin('confirm', {
30331			template: '<h2 class="top-line title">Confirm</h2><p class="message"></p>' +
30332				'<div class="toolbar"><a href="#" class="button green-button yes-button">' +
30333				'<span class="yes-button-text">' + gettext('Yes') + '</span></a>' +
30334				'<a href="#" class="button button-no-arrow no-button">' + 
30335				'<span class="no-button-text">' + gettext('No') + '</span>' +
30336				'</a></div>'
30337		}, function(div) {
30338			var self = this;
30339			if (this.windowOptions.yesButtonText) {
30340				div.find('.yes-button-text').html(this.windowOptions.yesButtonText);
30341			}
30342			if (this.windowOptions.noButtonText) {
30343				div.find('.no-button-text').html(this.windowOptions.noButtonText);
30344			}
30345			div.find('.no-button, .yes-button').click(function(e) {
30346				e.preventDefault();
30347				var callback = (self.windowOptions[
30348					$(this).hasClass('yes-button') ? 'yesButton' : 'noButton'
30349				]) || self._cclose;
30350				myDeco.popin.close();
30351				callback.apply(self, arguments);
30352			});
30353			if (this.windowOptions.title) {
30354				div.find('.title').html(this.windowOptions.title);
30355			} else {
30356				div.find('.title').remove();
30357			}
30358			if (this.windowOptions.message) {
30359				div.find('.message').html(this.windowOptions.message);
30360			} else {
30361				div.find('.message').remove();
30362			}
30363		});
30364		
30365		/**
30366		 * Replacement for lightwindow image show
30367		 * @param {Object} options Additionally to default options are:
30368		 *   title {String} replacement for title, will remove if no specified
30369		 */
30370		popin.plugin('image', {
30371			autoResizeX: true,
30372			template: '<h2 class="top-line title">Image</h2><div class="popin-image" style="position:relative" />'
30373		}, function(div) {
30374			var self = this;
30375			if (this.windowOptions.title) {
30376				div.find('.title').html(this.windowOptions.title);
30377			} else {
30378				div.find('.title').remove();
30379			}
30380			
30381			$('<img src="' + this.windowOptions.url + '" />').load(function(e) {
30382				var img = $(e.target),
30383					width, height, ccw, cch, chw, chh, wd, hd;
30384				div.find('.popin-image').css('height', 'auto').prepend(img);
30385				width = img.width();
30386				height = img.height();
30387				self.contentContainer.width(width);
30388				self.resize();
30389				
30390				// resize image to fit to content container
30391				ccw = self.contentContainer.outerWidth(true);
30392				cch = self.contentContainer.outerHeight(true);
30393				chw = self.holder.innerWidth();
30394				chh = self.holder.innerHeight();
30395				
30396				if (ccw > chw || cch > chh) {
30397					wd = ccw - chw;
30398					hd = cch - chh;
30399					if (wd > hd) {
30400						img.width(width - wd);
30401						self.contentContainer.width(width - wd);
30402					} else {
30403						img.height(height - hd);
30404						self.contentContainer.width(parseInt((height - hd) * width / height, 10));
30405					}
30406					
30407					self.resize();
30408				}
30409			});
30410		});
30411		
30412		popin.plugin('error', {
30413			template: '<div class="message"><div></div><div class="buttons"><a class="button popin-close">' +
30414						'<span class="close-button-text">Close</span></a></div></div>',
30415			showClose: false
30416		}, function(div) {
30417			if (this.windowOptions.closeButtonText) {
30418				div.find('.close-button-text').html(this.windowOptions.closeButtonText);
30419			}
30420			div.find('.message div:first').html(this.windowOptions.message || "Something goes wrong");
30421		});
30422	});
30423};
30424/**
30425 * @fileOverview Small wrapper for sharing
30426 */
30427/*globals myDeco, $, tmpl, window, screen */
30428/*jslint white: false, forin: true, fragment: true, undef: true, nomen: true, plusplus: true, bitwise: false, regexp: false, newcap: true, sloppy: true */
30429
30430myDeco.sharethis = {
30431	init: function (scope) {
30432		var self = this,
30433			url = window.location.href;
30434		scope = scope && !$.isFunction(scope) ? scope : $('.share-it');
30435
30436		//function that return an url depending of event target classname
30437		function sharedUrl(element) {
30438			return element.hasClass('url-source') ? element.attr('href') : url;
30439		}
30440
30441		scope.find('.facebook').click(function (e) {
30442			e.stop();
30443			var href = sharedUrl($(e.target));
30444			self.stats(e, href, 'share-on-facebook');
30445			self.openFacebook(href);
30446		});
30447
30448		scope.find('.twitter').click(function (e) {
30449			e.stop();
30450			var target = $(e.target),
30451				url = sharedUrl(target),
30452				ogp = $.ogp(),
30453				subj = target.attr('rel') ? target.attr('rel').split(':')[2] : window.document.title,
30454				message,
30455				container = $(this),
30456				dataMessage = container.data('message');
30457				
30458			if (!ogp.owner) ogp.owner = false;
30459			
30460			if (!target.attr('rel') && !$.isEmptyObject(ogp) && ogp.type) {
30461				// OGP driven message
30462				message = tmpl('I love this: "<%= title %>" <%= type %> <% if (owner) { %>by <%= owner %><% } %>', ogp);
30463			} else if (myDeco.user.inviteSuccessCallback) {
30464				// I&E message
30465				message = 'I just joined mydeco.com to get a £10 voucher. Click ' + myDeco.urls.main + 'inviteuser/' + myDeco.username + '/ to get your own £10 voucher';
30466			} else {
30467				message = 'I love this: ' + subj;
30468			}
30469
30470			if (dataMessage) {
30471				message = dataMessage + ' ' + subj;
30472			}
30473
30474			self.stats(e, url, 'share-on-twitter');
30475			self.openTwitter(url, {text: message});
30476		});
30477
30478		scope.find('.stumble').click(function (e) {
30479			e.stop();
30480			var href = sharedUrl($(e.target));
30481			self.stats(e, href, 'share-on-stumble');
30482			self.openStumble(href);
30483		});
30484
30485		scope.find('.pinterest').click(function (e) {
30486			e.stop();
30487			var href = sharedUrl($(e.target));
30488			self.stats(e, href, 'share-on-pinterest');
30489			self.openPinterest(href);
30490		});
30491
30492		scope.find('.email').click(function (e) {
30493			e.stop();
30494
30495			var element = $(e.target).closest('*[rel], *[href]'),
30496				meta = $('head meta[name="description"]'),
30497				params = $.extend(true, {
30498					subject: 'Check out ' + window.document.title.replace(/[\r\n\s]+/, ' '),
30499					body: (meta.size() > 0 ? meta.attr('content') + '\r\n\r\n' : '') + url
30500				}, (element.attr('rel') || element.attr('href')).toQueryParams()),
30501				values = [],
30502				name;
30503
30504			// can't use $.param here because returned result has %20 replaced with "+"
30505			for (name in params) {
30506				values.push(encodeURIComponent(name) + '=' + encodeURIComponent(params[name]));
30507			}
30508			self.stats(e, null, 'share-by-email');
30509			window.location = 'mailto:?' + values.join('&');
30510		});
30511
30512		scope.find('.room_embed').click(function(e) {
30513			e.stop();
30514			var rel = this.rel;
30515				getTitle = $('.for-embed-title').text();
30516
30517			$.ajax({
30518				url: myDeco.urls.blogRoomEmbedForm,
30519				dataType: 'json',
30520				type: 'get',
30521				data: {'id': rel},
30522				success: function (data) {
30523					self.getEmbedCode(data);
30524				}
30525			});
30526		});
30527
30528		scope.find('.folder_embed').click(function(e) {
30529			e.stop();
30530			var rel = this.rel;
30531				getTitle = $('.for-embed-title').text();
30532
30533			$.ajax({
30534				url: myDeco.urls.blogFolderEmbedForm,
30535				dataType: 'json',
30536				type: 'get',
30537				data: {'id': rel},
30538				success: function(data) {
30539					self.getEmbedCode(data);
30540					self.colorPicker();
30541				}
30542			});
30543
30544		});
30545	},
30546
30547	stats: function (e, url, action) {
30548		var target = $(e.target),
30549			params = {
30550				url: url || window.location.href,
30551				action: action,
30552				ctype: target.data('ctype') || 'unknown',
30553				object_id: target.data('id')
30554			},
30555			data = e ? target.closest('*[rel]').attr('rel') : false;
30556
30557		if (data) {
30558			data = data.split(':');
30559			params.ctype = data[0];
30560			params.object_id = data[1];
30561			params.title = data[2] || window.document.title;
30562		}
30563
30564		myDeco.tracking.trackEvent(params.action, params.ctype, params.object_id);
30565		$.post(myDeco.urls.saveSharedItem, params, null, 'json');
30566	},
30567
30568	openFacebook: function (url) {
30569		window.open('http://www.facebook.com/sharer.php?' + $.param({u: url}),
30570				'facebook_share', 'toolbar=0,status=0,width=626,height=436');
30571	},
30572
30573	openTwitter: function (url, params) {
30574		// code extracted from twitter's share bookmarklet
30575		var width = 550, height = 450, screenHeight = screen.height, screenWidth = screen.width,
30576			left = Math.round((screenWidth / 2) - (width / 2)),
30577			top = screenHeight > height ? Math.round((screenHeight / 2) - (height / 2)) : 0;
30578
30579		params = $.param($.extend({
30580			url: url,
30581			via: myDeco.settings.TWITTER_PARAMS.account,
30582			related: myDeco.settings.TWITTER_PARAMS.related
30583		}, params || {}));
30584
30585		window.open('http://twitter.com/share?' + params, '',
30586				['left=', left, ',top=', top, ',width=', width, ',height=', height, ',personalbar=0,toolbar=0,scrollbars=1,resizable=1'].join(''));
30587	},
30588
30589	openStumble: function (url) {
30590		window.open('http://www.stumbleupon.com/submit?' + $.param({url: url}));
30591	},
30592
30593	openPinterest: function (url) {
30594		window.open(url, 'Share on Pinterest', 'toolbar=0,status=0,width=600,height=550');
30595	},
30596
30597	socialURL: function (id, type) {
30598		if (type === 'fb') {
30599			myDeco.facebook.loadConnect(function() {
30600				FB.XFBML.parse($('.social-row')[0]);
30601			});
30602			return;
30603		}
30604		var urls = {
30605			tw: "//platform.twitter.com/widgets.js",
30606			pin: "http://assets.pinterest.com/js/pinit.js"
30607		}, js, fjs = document.getElementsByTagName('script')[0];
30608		if (document.getElementById(id)) {
30609			return;
30610		}
30611		js = document.createElement('script');
30612		js.id = id;
30613		js.src = urls[type];
30614		fjs.parentNode.insertBefore(js, fjs);
30615	},
30616
30617	/**
30618	* 	color picker function for Embed popin
30619	*/
30620
30621	colorPicker: function() {
30622		var colorList = [ '000000', '993300', '333300', '003300', '003366', '000066', '333399', '333333', 
30623						  '660000', 'FF6633', '666633', '336633', '336666', '0066FF', '666699', '666666', 
30624						  'CC3333', 'FF9933', '99CC33', '669966', '66CCCC', '3366FF', '663366', '999999',
30625						  'CC66FF', 'FFCC33', 'FFFF66', '99FF66', '99CCCC', '66CCFF', '993366', 'CCCCCC',
30626						  'FF99CC', 'FFCC99', 'FFFF99', 'CCffCC', 'CCFFff', '99CCFF', 'CC99FF', 'FFFFFF' ];
30627		var picker = $('#color-picker');
30628
30629		for (var i = 0; i < colorList.length; i++ ) {
30630			picker.append('<li class="color-item" data-hex="' + '#' + colorList[i] + '" style="background-color:' + '#' + colorList[i] + ';"></li>');
30631		}
30632
30633		$('#popin-content-holder').click(function() {
30634			picker.fadeOut(100);
30635		});
30636
30637		$('.call-picker').click(function(e) {
30638			e.stop();
30639			picker.fadeIn();
30640			picker.children('li').hover(function() {
30641				var codeHex = $(this).data('hex');
30642
30643				$('.color-holder').css('background-color', codeHex);
30644				$('#pickcolor').val(codeHex);
30645			});
30646		});
30647
30648	},
30649
30650	/**
30651	 * function that open an Embed popin in dependence of params
30652	 * grab and update embed code from template
30653	 * @param {JSON} data  - html of forms getting from ajax
30654	 * @param {Array} params - set of options for js template
30655	 **/
30656	getEmbedCode: function(data, params) {
30657		var self = this;
30658		myDeco.popin.open({
30659			//2 kinds of forms: form with options and embed code  form itself
30660			template: params ? data.code_form : data.template,
30661			tracking: 'embed',
30662			showClose: false,
30663			width: 480,
30664			className: 'embed-form-popin',
30665			afterOpen: function (div) {
30666				var setWishName = $('#wishname').val(getTitle);
30667				// competition embed or second form
30668				if (params) {
30669					// get code from template and update code field
30670					var html = $.minifyHtml(tmpl('embed-tmpl', params));
30671					div.find('textarea')
30672						.val(html)
30673						.select();
30674
30675					// copy to clipboard with zclip plugin
30676					$('#copy-button').zclip({
30677						path: "/media/ZeroClipboard.swf",
30678						copy: $('#copy-area').val(),
30679						afterCopy: function() {
30680							$('.copied').fadeIn(200).delay(400).fadeOut(200);
30681						}
30682					});
30683
30684				} else {
30685
30686					var prepareParams = function (o) {
30687						//update popin if it was Select options form before
30688						var qparams = $(o).closest('form').serialize().toQueryParams();
30689						var images = [];
30690						if (typeof(qparams.image) === 'string') { 
30691							qparams.image = [qparams.image]; 
30692						}
30693						for (var k = 0; k < qparams.image.length; k++) {
30694							var sp = qparams.image[k].split(';');
30695							images.push({image_url: sp[0],
30696							             title: sp[1],
30697							             url: sp[2]});
30698						}
30699						qparams.image = images;
30700						qparams.background = $.trim(qparams.custom_background);
30701						if (qparams.width == 'custom') {
30702							qparams.custom_width = $.trim(qparams.custom_width);
30703							if (isFinite(qparams.custom_width)) {
30704								qparams.width = qparams.custom_width <= 640 ? qparams.custom_width : 640;
30705							} else {
30706								qparams.width = 300;
30707							}
30708						}
30709						return qparams;
30710					}
30711					div.find('.continue').click(function (e) {
30712						e.stop();
30713						var d = {data: prepareParams(this)};
30714						self.getEmbedCode(data, d);
30715
30716					});
30717					div.find('#preview').click(function (e) {
30718						e.stop();
30719						var d = {data: prepareParams(this)};
30720						var html = tmpl($('#embed-tmpl').html(), d);
30721
30722						myDeco.popin.open({
30723							template: html,
30724							tracking: 'preview_embed',
30725							showClose: true,
30726							width: 710,
30727							height: 'auto',
30728							className: 'embed-form-popin'});
30729
30730						$('.popin-close-title').click(function (e) {
30731							e.stop();
30732							self.getEmbedCode(data, d);
30733							self.colorPicker();
30734						})
30735					});
30736					div.find('input[name=product_links]').change(function (e) {
30737						var pl = $('#product_links_style');
30738						if ($(this).is(':checked')) {
30739							pl.slideDown();
30740						} else {
30741							pl.slideUp();
30742						}
30743					});
30744					div.find('select[name=width]').change(function (e) {
30745						var cwb = $('#custom_width_block');
30746						if ($(this).val() == 'custom') {
30747							cwb.slideDown();
30748						} else {
30749							cwb.slideUp();
30750						}
30751					});
30752				}
30753			}
30754		});
30755	}
30756
30757
30758};
30759
30760
30761
30762$(myDeco.sharethis.init.bind(myDeco.sharethis));
30763myDeco.roomShare = {
30764	share: function(url) {
30765		$.get(url, function(template) {
30766			myDeco.popin.open({
30767				template: template,
30768				width: 760,
30769				className: 'room-share',
30770				tracking: true,
30771				afterOpen: function(content) {
30772					content.parent().addClass('rounded');
30773					
30774					if ($.fn.iscroll)
30775						content.parent().iscroll();
30776					
30777					var layouts = content.find('ul li'),
30778						textarea = content.find('textarea, input').click(function(e) {
30779							$(this).select();
30780						}).filter('textarea');
30781					
30782					layouts.click(function(e) {
30783						layouts.removeClass('selected');
30784						textarea
30785							.val($.minifyHtml(content.find('.layout-type-' + (layouts.index($(this).addClass('selected')) + 1) + '-container').get(0).innerHTML))
30786							.select();
30787					});
30788				}
30789			});
30790		});
30791	}
30792};
30793/*globals myDeco, $ */
30794/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: true, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
30795myDeco.buttons = {
30796	
30797	initSubmitButtons: function(elements) {
30798		var links = $();
30799		
30800		elements.each(function() {
30801			var button = $(this),
30802				link;
30803			
30804			if (!button.hasClass('no-decorate')) {
30805				button.val(button.val() || "Submit");
30806				
30807				if (!button.attr('disabled')) {
30808					link = $('<a>').attr({
30809							'href': '#',
30810							'class': button.attr('class')
30811						});
30812					
30813					links = links.add(
30814						link.addClass('mdtwo-button form-submit-button').html(
30815							$('<span>').html(button.val())
30816						)
30817					);
30818					
30819					button
30820						.after($('<div>').addClass('submit-button').html(link))
30821						.css({
30822							opacity: .01,
30823							position: 'absolute',
30824							left: '-9999em',
30825							top: '-9999em'
30826						});
30827				}
30828			}
30829		});
30830		
30831		links.click(function(e) {
30832			e.stop();
30833			var button = $(this);
30834			
30835			// if button or some holder are not disabled, submit form
30836			if (!button.hasClass('no-submit') &&
30837					button.closest('.disabled').size() === 0) {
30838				button.closest('form').submit();
30839			}
30840		});
30841		
30842		return elements;
30843	},
30844	
30845	enable: function(form) {
30846		return form.removeClass('disabled');
30847	},
30848	
30849	disable: function(form) {
30850		return form.addClass('disabled');
30851	},
30852	
30853	isDisabled: function(form) {
30854		return form.hasClass('disabled');
30855	}
30856	
30857};
30858
30859$(function(){
30860	myDeco.buttons.initSubmitButtons($('input:submit'));
30861});
30862/*globals $, myDeco, jQuery */
30863/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: false, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
30864/**
30865 * @fileOverview Support for draggable elements
30866 * Usage: <div class="position:absolute" class="draggable"><h1 class="handle">Drag me</h1></div>
30867 * Will start drag by key pressing on h1 element
30868 * If "restrict-bounds" class is added to draggable, element will check for c
30868ontainer bounds
30869 * Has three states: minimized, maximized and restored, each switch will fire event
30870 * maximized and minimized classNamed will be added to a element indicating state
30871 */
30872
30873(function($) {
30874	$.fn.draggable = function(handleClass) {
30875		handleClass = '.' + (handleClass || 'handle');
30876		
30877		var updateSettings = function(e, element) {
30878				var values = element.data('settings') || {},
30879					parent = element.offsetParent(),
30880					offset = element.offset(),
30881					offsetParent = parent.offset(),
30882					settings = $.extend(values, {
30883						element: element,
30884						offset: offset,
30885						parent: parent,
30886						offsetParent: offsetParent,
30887						restrict: element.hasClass('restrict-bounds'),
30888						offsetX: e ? e.pageX - offset.left + offsetParent.left : 0,
30889						offsetY: e ? e.pageY - offset.top  + offsetParent.top : 0,
30890						offsetW: element.outerWidth(),
30891						offsetH: element.outerHeight(),
30892						width: parent.width(),
30893						height: parent.height()
30894					});
30895				
30896				element.data('settings', settings);
30897				return settings;
30898			},
30899			getCoordinates = function(e, element, restrict) {
30900				var settings = element.data('settings'),
30901					x = e ? e.pageX - settings.offsetX : settings.offset.left,
30902					y = e ? e.pageY - settings.offsetY : settings.offset.top;
30903		
30904				if (restrict || settings.restrict) {
30905					x = Math.max(0, Math.min(settings.width  - settings.offsetW, x));
30906					y = Math.max(0, Math.min(settings.height - settings.offsetH, y));
30907				}
30908				
30909				return {left: x, top: y};
30910			},
30911			setPosition = function(element, status) {
30912				var position;
30913				
30914				myDeco.body.trigger('draggableGetPosition', [element, status, function(pos) {
30915					position = $.extend({
30916						left: 'auto',
30917						top: 'auto',
30918						bottom: 'auto',
30919						right: 'auto'
30920					}, pos);
30921				}]);
30922				
30923				if (typeof position !== 'object') {
30924					position = element.data('position');
30925				} else {
30926					if (status !== 'minimize') {
30927						position = $.extend(position, {
30928							right: 'auto',
30929							bottom: 'auto'
30930						});
30931					}
30932					element.css(position);
30933				}
30934				return element;
30935			},
30936			minimize = function(element) {
30937				element.data('position', {
30938					left: element.css('left'),
30939					top: element.css('top'),
30940					right: element.css('right'),
30941					botom: element.css('bottom')
30942				});
30943				
30944				setPosition(element
30945						.trigger('minimize')
30946						.removeClass('maximized')
30947						.addClass('minimized'), 'minimize'
30948					)
30949					.trigger('minimized');
30950			},
30951			restore = function(element) {
30952				setPosition(element
30953						.trigger('minimize')
30954						.removeClass('minimized maximized'), 'restore'
30955					)
30956					.trigger('restored');
30957			},
30958			maximize = function(element) {
30959				setPosition(element
30960						.trigger('maximize')
30961						.removeClass('minimized')
30962						.addClass('maximized'), 'maximize'
30963					)
30964					.trigger('maximized');
30965			},
30966			body = $('body'),
30967			self = this;
30968			
30969			
30970		$.each(this, function(i, element) {
30971			element = $(element);
30972			
30973			var linkTo = $(),
30974				offset = false,
30975				pos,
30976				linkedListener = function(e, ee) {
30977					var el = $(this);
30978					
30979					if (!ee) {
30980						return;
30981					}
30982					
30983					if (e.type === 'move') {
30984						offset = element.offset();
30985						offset.left = +ee.pageX - offset.left;
30986						offset.top = +ee.pageY - offset.top;
30987					} else if (e.type === 'moving') {
30988						element.css({
30989							left: +ee.pageX - offset.left + 'px',
30990							top: +ee.pageY - offset.top + 'px'
30991						});
30992					} else if (e.type === 'minimize' || e.type === 'maximize' || e.type === 'restore') {
30993						offset = el.offset();
30994						offset.left += el.width();
30995						offset.top += el.height();
30996						pos = element.offset();
30997						
30998						if (offset.left < pos.left) {
30999							offset.left = pos.left - offset.left;
31000							offset.top = 0;
31001						} else if (offset.top < pos.top) {
31002							offset.left = 0;
31003							offset.top = pos.top - offset.top;
31004						} else {
31005							offset = false;
31006						}
31007					} else if (offset !== false && (e.type === 'minimized' || e.type === 'restored' || e.type === 'maximized')) {
31008						if (offset.top !== 0) {
31009							element.css('top', el.offset().top + el.height() + offset.top + 'px');
31010						} else if (offset.left !== 0) {
31011							element.css('left', el.offset().left + el.width() + offset.left + 'px');
31012						}
31013						offset = false;
31014					} else {
31015						offset = false;
31016					}
31017				};
31018			
31019			element
31020				.bind('linkTo', function(e, elements) {
31021					linkTo.unbind('move moving moved minimize minimized maximize maximized restore restored', linkedListener);
31022					linkTo = elements;
31023					linkTo.bind('move moving moved minimize minimized maximize maximized restore restored', linkedListener);
31024				})
31025				.bind('checkRestrictions', function(e) {
31026					var element = $(this),
31027						coords;
31028					
31029					updateSettings(e, element);
31030					
31031					coords = getCoordinates(false, element, true);
31032					
31033					element.css($.extend({
31034						bottom: 'auto',
31035						right: 'auto'
31036					}, coords));
31037					
31038				})
31039				
31040				// rise window using zIndex on click
31041				.mousedown(function(e) {
31042					var element = $(this),
31043						elements, candidate = -1, cz = -1, ez;
31044					
31045					// don't want to deal with non-windowed layers
31046					if (!element.hasClass('windowed')) {
31047						return;
31048					}
31049					
31050					// find event owner zIndex
31051					ez = +element.css('zIndex') || 0;
31052					
31053					// find candidate for zIndex switching
31054					elements = self
31055						// only windows except event owner
31056						.filter(function(i) {
31057							return self.eq(i).hasClass('windowed') && self.get(i) !== element.get(0);
31058						})
31059						// with the max zIndex
31060						.each2(function(n, e) {
31061							var z = +e.css('zIndex') || 0;
31062							if (cz <= z) {
31063								cz = z;
31064								candidate = n;
31065							}
31066						});
31067					
31068					if (candidate !== -1) {
31069						// force zIndex increment in case of equality
31070						if (ez === cz) {
31071							ez++;
31072						}
31073						
31074						// switch windows
31075						elements.eq(candidate).css('zIndex', Math.min(ez, cz)).trigger('lowered');
31076						element.css('zIndex', Math.max(ez, cz)).trigger('raised');
31077					}
31078				})
31079				.find(handleClass)
31080					.mousedown(function(e) {
31081						e.preventDefault();
31082
31083						if (element.hasClass('locked')) {
31084							return;
31085						}
31086						
31087						element.removeData('position');
31088						
31089						// drop linking on self moving
31090						linkTo.unbind('move moving moved minimize minimized maximize maximized restore restored', linkedListener);
31091						element.trigger('move', [e]);
31092						
31093						updateSettings(e, element);
31094						
31095						var mousemove = function(e) {
31096								element.css($.extend({
31097									bottom: 'auto',
31098									right: 'auto'
31099								}, getCoordinates(e, element)));
31100								element.trigger('moving', [e]);
31101							},
31102							mouseup = function(e) {
31103								if (e.type !== 'mouseout' || e.type === 'mouseout' && e.target.tagName === 'BODY') {
31104									body
31105										.unbind('mousemove', mousemove)
31106										.unbind('mouseup', mouseup)
31107										.unbind('mouseout', mouseup);
31108									element.trigger('moved', [e]);
31109								}
31110							};
31111						
31112						body
31113							.mousemove(mousemove)
31114							.mouseout(mouseup)
31115							.mouseup(mouseup);
31116					
31117					})
31118					.dblclick(function(e) {
31119						if (element.hasClass('minimized')) {
31120							if (element.hasClass('maximized')) {
31121								maximize(element);
31122							} else {
31123								restore(element);
31124							}
31125						} else {
31126							minimize(element);
31127						}
31128					})
31129					.find('.control').mousedown(function(e) {
31130						e.stop();
31131						var button = $(e.target);
31132
31133						if (button.hasClass('minimize')) {
31134							minimize(element);
31135						} else if(button.hasClass('maximize')) {
31136							maximize(element);
31137						} else if(button.hasClass('restore')) {
31138							restore(element);
31139						}
31140					});
31141		});
31142	};
31143}(jQuery));
31144
31145jQuery(function($) { $('.draggable').draggable(); });
31146/**
31147 * @fileOverview Component for customized file-upload fields
31148 */
31149
31150myDeco.fileInputs = {
31151	/**
31152	 * jQuery set of  elements
31153	 */
31154	elements: [],
31155	/**
31156	 * Init fileinputs, hide input[type=file] and create overlap input[type=text] with browse button
31157	 * If class names on input[type=file] are specified, these class names will be added to browse button also
31158	 * @params {Element} scope DOM element for searching thru or empty in case of global replacing
31159	 */
31160	init : function(scope) {
31161		
31162		var self = this,
31163			elements = scope == undefined
31164				? $('.fileinputs:has(input:file)')
31165				: scope.find('.fileinputs:has(input:file)');
31166				
31167		elements.each(function() {
31168			var holder = $(this),
31169				file = holder.find(':file'),
31170				val = holder.data('buttontext') || 'Browse',
31171				// used for styling upload button, remove all validation-related classes
31172				classNames = $.grep(file.attr('className').split(' '), function(c) {
31173					return c != 'validate' && c.substring(0, 2) != 'v-';
31174				}).join(' '),
31175				overlay = holder.find('div.fakefile'),
31176				text = overlay.find('input:text');
31177			
31178			if (text.size() == 0)
31179				text = $('<input type="text" class="text" value="" />');
31180			
31181			if (overlay.size() == 0) {
31182				overlay = $('<div class="fakefile"></div>')
31183					.append($('<div class="field"></div>').append(text))
31184					.append('<a class="button ' + classNames + '"><span>' + val + '</span></a>');
31185				holder.prepend(overlay);
31186			}
31187			
31188			file.addClass('file').bind('change', function(e) {
31189				text.addClass('withtext').val(file.val().replace(/^.*?([^\/\\]+)$/, '$1'));
31190			});
31191		});
31192
31193		this.elements = elements;
31194	}
31195}
31196
31197$(function() { myDeco.fileInputs.init(); });
31198/*globals myDeco, $ */
31199/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: true, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
31200/**
31201 * @fileOverview Form field values validation tools
31202 */
31203myDeco.formValidation = {
31204	/**
31205	 * List of default messages for errors
31206	 * @type {Object}
31207	 */
31208	_messages: {
31209		'required': 'Please ensure each field is complete before you re-submit.',
31210		'number': 'Please enter a number.',
31211		'required-inbox': 'Oops! You forgot your message!',
31212		'email': 'We\'d love you to bend the rules in room design, but not in our forms! Sorry, our system doesn\'t recognise your email address. Please check and try again.',
31213		'password': 'Passwords must be 5-15 characters and may contain numbers, letters and underscores.',
31214		'passwordconfirm': 'Your passwords must match.',
31215		'date': 'Must be a valid date of the form dd/mm/yyyy. Eg: 19/09/2007',
31216		'url': 'Please enter a valid url that begins with http://',
31217		'no-whitespace-only': 'Sorry, we do not accept whitespace-only input.',
31218		'html': 'Should not contain any HTML-like tags',
31219		'word-length': 'Oops! Please make sure words in your comment do not exceed %1 characters in length.',
31220		'letters': 'May contain only letters, numbers, spaces and undercores',
31221		'postcode': 'Please enter valid postcode'
31222	},
31223	
31224	/**
31225	 * Initializes form validation by finding
31226	 * non popup/inline forms with "validate" class.
31227	 * @param {jQuery} scope Scope for form finding
31228	 */
31229	init: function(scope) {
31230		scope = typeof scope !== 'object' ? myDeco.body : scope;
31231		
31232		var self = this;
31233		scope.find('form.validate').submit(function(e) {
31234			var form = $(this);
31235			if (!self.validate(form, form.hasClass('inline'))) {
31236				e.stop();
31237			}
31238		});
31239	},
31240	
31241	/**
31242	 * Finds form elements to validate by "validate" class.
31243	 * Calls validation routine and applies bubble if it fails.
31244	 *
31245	 * @param {jQuery} form Form to validate
31246	 * @param {jQuery} inline use inline error messages, find them using .error.{input name} css selector
31247	 * @param {Object} messages Associative massive of custom error messages
31248	 * @returns {Boolean} Is form valid.
31249	 */
31250	validate: function(form, inline, messages) {
31251		var self = this,
31252			hasErrors = false;
31253		
31254		if (inline) {
31255			form.find('.error').remove();
31256		}
31257		
31258		form.find('.validate').each(function() {
31259			var element = $(this),
31260				parent = element.parent(), 
31261				message = self.isInvalid(element, messages);
31262			
31263			if (message) {
31264				self.showError(element, message, inline);
31265				hasErrors = true;
31266			}
31267			
31268			if (hasErrors) {
31269				parent.removeClass('valid')
31270				parent.addClass('invalid')
31271			} else {
31272				parent.addClass('valid');
31273				parent.removeClass('invalid')
31274			}
31275		});
31276		return !hasErrors;
31277	},
31278
31279	/**
31280	 * Shows error message for given element
31281	 * @param {jQuery Element} element
31282	 * @param {String} message
31283	 * @param {Boolean} inline, message type (true = 'inline' or false = 'bubble')
31284	 */
31285	showError: function(element, message, inline) {
31286		if (inline) {
31287			element.before($('<div class="error" />').addClass(element.attr('name')).text(message));
31288		} else {
31289			var form = element.parents('form'),
31290					useContainer = form.hasClass('bubble-container') || form.closest('#popin-content').size() > 0;
31291			// remove old bubble if exist
31292			this.clearError(element);
31293			if (message) {
31294				element.data('bubble', myDeco.bubbles.create(element, message, useContainer));
31295			}
31296		}
31297	},
31298
31299	/**
31300	 * Remove old error bubble if exist
31301	 * @param {jQuery Elements} Elements
31302	 */
31303	clearError: function(elements) {
31304		$.each(elements, function(){
31305			var element = $(this);
31306			if (element.data('bubble')) {
31307				element.data('bubble').remove();
31308				element.removeData('bubble');
31309			}
31310		});
31311	},
31312
31313	/**
31314	 * Performs validation of form element's value by class name.
31315	 * Available validation classes are:<br />
31316	 * <ul>
31317	 * <li>"v-required" - checks if field's value is not empty</li>
31318	 * <li>"v-number" - checks if field's value is a number</li>
31319	 * <li>"v-required-inbox" - checks if message field is not empty</li>
31320	 * <li>"v-email" - checks if email is correct</li>
31321	 * <li>"v-password" - checks if password is correct</li>
31322	 * <li>"v-passwordconfirm" - checks if both password fields match</li>
31323	 * <li>"v-date" - checks correctness of date input</li>
31324	 * <li>"v-url" - checks if field's value is valid URL</li>
31325	 * <li>"v-word-length-<b><maxLength></b>" - checks if words in the field
31326	 * don't exceed <b><maxLength></b></li>
31327	 * <li>"v-no-whitespace-only" - checks if field's value has visible symbols</li>
31328	 * <li>"v-no-html" - checks if field value hasn't contain HTML-like tags</li>
31329	 * <li>"v-letters" - checks if field value contains only letters and numbers</li>
31330	 * <li>"v-postcode" - checks if postcode valid. Avaliable for US, UK, FR</li>
31331	 * </ul>
31332	 *
31333	 * @param {jQuery} el Element to validate
31334	 * @param {Object} messages Custom error messages
31335	 * @returns {Boolean} Is element valid
31336	 */
31337	isInvalid : function(el, messages, countryCode) {
31338		if(!el.is(':visible')) {
31339			return false;
31340		}
31341
31342		countryCode = countryCode || 'United Kingdom';
31343		var value = el.val(),
31344			params = [], p,
31345			error = false;
31346
31347		if(el.hasClass('v-required') && !value) {
31348			error = 'required';
31349		} else if (el.hasClass('v-number') && value.match(/\D/)) {
31350			error = 'number';
31351		} else if(el.hasClass('v-required-inbox') && !value) {
31352			error = 'required-inbox';
31353		} else if(el.hasClass('v-email') && value && !this.validateEmail(value)) {
31354			error = 'email';
31355		} else if(el.hasClass('v-password') && !this.validatePassword(value)) {
31356			error = 'password';
31357		} else if(el.hasClass('v-passwordconfirm') && !this.validatePasswordConfirm(value)) {
31358			error = 'passwordconfirm';
31359		} else if(el.hasClass('v-date') && !this.validateDate(value)) {
31360			error = 'date';
31361		} else if(el.hasClass('v-url') && !this.validateUrl(value)) {
31362			error = 'url';
31363		} else if(el.hasClass('v-no-whitespace-only') && !/[^\s]/.test(value)) {
31364			el.val("");
31365			error = 'no-whitespace-only';
31366		} else if (el.hasClass('v-no-html') && !this.validateNoHTML(value)) {
31367			error = 'no-html';
31368		} else if ((params = el.attr('class').match(/v-word-length-(\d+)/)) && !this.validateWordLength(value, params[1])) {
31369			error = 'word-length';
31370			// remove global match
31371			params.shift();
31372		} else if (el.hasClass('v-letters') && !this.lettersOnly(value)) {
31373			error = 'letters';
31374		} else if (el.hasClass('v-postcode') && !this.validPostcode(value, countryCode)) {
31375			error = 'postcode';
31376		}
31377
31378		if (error !== false) {
31379			error = ($.extend({}, this._messages, messages || {})[error] || 'Error is occured');
31380			if (params && params.length !== 0) {
31381				error = error.replace(/%?%(\d+)/, function(str, num) {
31382					return str.indexOf('%%') === 0 ? str : params[num - 1];
31383				});
31384			}
31385		}
31386
31387		return error;
31388	},
31389
31390	/**
31391	 * Checks email
31392	 *
31393	 * @param {String} sVal Validation value
31394	 * @returns {Boolean} Is element valid
31395	 */
31396	validateEmail: function(sVal){
31397		// do the comparison, if we have a match write thank you or else the email is invalid
31398		// {2,6} - 2 usual geo domains, 6 - museum
31399		return (/^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$/i).test(sVal);
31400	},
31401
31402	/**
31403	 * Checks password
31404	 *
31405	 * @param {String} sVal Validation value
31406	 * @returns {Boolean} Is element valid
31407	 */
31408	validatePassword: function(sVal) {
31409		// Description: The password's first character must be a letter, it must contain at least 4 characters
31410		// and no more than 15 characters and no characters other than letters, numbers and the underscore may be used
31411		// Matches: [abcd], [aBc45DSD_sdf], [password] (http://www.regexlib.com)
31412		// do the comparison, if we have a match write thank you or else the email is invalid
31413		return (/^[a-zA-Z]\w{3,14}$/).test(sVal);
31414	},
31415
31416	/**
31417	 * Checks if passwords match
31418	 *
31419	 * @param {String} sVal Validation value
31420	 * @returns {Boolean} Is element valid
31421	 */
31422	validatePasswordConfirm: function(sVal) {
31423		return $('#sign-up-password1, #id_password1').val() === sVal;
31424	},
31425
31426	/**
31427	 * Checks date
31428	 *
31429	 * @param {String} sVal Validation value
31430	 * @returns {Boolean} Is element valid
31431	 */
31432	validateDate: function(sVal) {
31433		return (/(((0[13578]|10|12)([\-.\/])(0[1-9]|[12][0-9]|3[01])([\-.\/])(\d{4}))|((0[469]|11)([\-.\/])([0][1-9]|[12][0-9]|30)([\-.\/])(\d{4}))|((2)([\-.\/])(0[1-9]|1[0-9]|2[0-8])([\-.\/])(\d{4}))|((2)(\.|-|\/)(29)([\-.\/])([02468][048]00))|((2)([\-.\/])(29)([\-.\/])([13579][26]00))|((2)([\-.\/])(29)([\-.\/])([0-9][0-9][0][48]))|((2)([\-.\/])(29)([\-.\/])([0-9][0-9][2468][048]))|((2)([\-.\/])(29)([\-.\/])([0-9][0-9][13579][26])))/).test(sVal);
31434	},
31435
31436	/**
31437	 * Checks URL
31438	 *
31439	 * @param {String} sVal Validation value
31440	 * @returns {Boolean} Is element valid
31441	 */
31442	validateUrl: function(sVal) {
31443		return (/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/).test(sVal);
31444	},
31445
31446	/**
31447	 * Checks if words don't exceed <b>maxLength</b>
31448	 *
31449	 * @param {String} value Validation value
31450	 * @param {Integer} length Max length of single word in text
31451	 * @returns {Boolean} Is element valid
31452	 */
31453	validateWordLength: function(value, length) {
31454		var isUrl = this.validateUrl;
31455		return $.grep(value.split(/\s+/), function(value) {
31456			return value.length > length && !isUrl(value);
31457		}).length === 0;
31458	},
31459
31460	/**
31461	 * Check's that string hasn't contain any HTML-like symbols
31462	 *
31463	 *  @param {String} sVal Validation value
31464	 *  @returns {Boolean} Is element hasn't contain HTML-like tags
31465	 */
31466	validateNoHTML: function(sVal) {
31467		return ! /<[a-zA-Z]+(\s[^>]*)?>/.test(sVal);
31468	},
31469
31470	/**
31471	 * Check's that string contain only letters and numbers
31472	 *
31473	 *  @param {String} val Validation value
31474	 *  @returns {Boolean} contain or not
31475	 */
31476	lettersOnly: function(val) {
31477		return	(/^[a-zA-Z0-9 _]*$/).test(val)     
31478	},
31479
31480	validPostcode: function(val, countryCode) {
31481		if (val) {
31482			switch (countryCode) {
31483				case 'FR':
31484					return (/^FR?-\d{5}$/).test(val);
31485					break;
31486				case 'US':
31487					return (/^\d{5}(-\d{4})?$/).test(val);
31488					break;
31489				case 'UK':
31490					return (/(GIR 0AA)|(((A[BL]|B[ABDHLNRSTX]?|C[ABFHMORTVW]|D[ADEGHLNTY]|E[HNX]?|F[KY]|G[LUY]?|H[ADGPRSUX]|I[GMPV]|JE|K[ATWY]|L[ADELNSU]?|M[EKL]?|N[EGNPRW]?|O[LX]|P[AEHLOR]|R[GHM]|S[AEGKLMNOPRSTY]?|T[ADFNQRSW]|UB|W[ADFNRSV]|YO|ZE)[1-9]?[0-9]|((E|N|NW|SE|SW|W)1|EC[1-4]|WC[12])[A-HJKMNPR-Y]|(SW|W)([2-9]|[1-9][0-9])|EC[1-9][0-9]) [0-9][ABD-HJLNP-UW-Z]{2})/).test(val);
31491					break;
31492				default:
31493					return true;
31494					break;
31495			}
31496		} else {
31497			return 'empty';
31498		}
31499	}
31500
31501
31502};
31503
31504$(function() {
31505	myDeco.formValidation.init();
31506});
31507/*globals window, jQuery */
31508/*jslint forin: true, fragment: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, newcap: true, regexp: false, white: false, plusplus: false */
31509(function($) {
31510	$.fn.slider = function(options) {
31511		options = $.extend({
31512			min: 0,
31513			max: 100,
31514			value: null,
31515			borders: 0,
31516			direction: 'horizontal',
31517			formatData: function(value, label) {
31518				return $.isArray(value) ? value : value.toFixed(2);
31519			},
31520			// Scale correction functions for custom scaling, example of functions that can be overridden in custom files (e.c. minisearch.js)
31521			positionToValueScale: function(x) { return x; },
31522			valueToPositionScale: function(x) { return x; },
31523			timeout: null,
31524			onChange: null,
31525			onSlide: null,
31526			invert: false
31527		}, options);
31528		
31529		var sizeOp, posField, eventField, self = this;
31530		
31531		switch (options.direction) {
31532			case 'horizontal':
31533				sizeOp = 'width';
31534				posField = 'left';
31535				eventField = 'clientX';
31536			break;
31537			case 'vertical':
31538				sizeOp = 'height';
31539				posField = 'top';
31540				eventField = 'clientY';
31541				options.invert = !options.invert;
31542			break;
31543			default:
31544				throw "Wrong value for slider direction";
31545		}
31546		
31547		if (options.max <= options.min) {
31548			options.max = options.min + 1;
31549		}
31550		
31551		options.range = options.max - options.min;
31552		options.scaleRatio = options.max / (options.min ? options.min : 1);
31553		
31554		function boundValue(value) {
31555			return Math.min(options.max, 
31556				Math.max(options.min, value));
31557		}
31558		
31559		function updateClosestItem(value, array) {
31560			var delta, closest;
31561			$.each(array, function(i, val){
31562				var d = Math.abs(val - value);
31563				if (typeof delta === 'undefined' || d <= delta) {
31564					delta = d;
31565					closest = i;
31566				}
31567			});
31568			array[closest] = value;
31569			return array;
31570		}
31571		
31572		function cloneValue(value) {
31573			return $.isArray(value) ? $.merge([], value) : value;
31574		}
31575		
31576		// stop dragging if mouse is up outside of slider
31577		$('body').bind('mouseup mouseleave', function(){
31578			self.trigger('stop-dragging');
31579		});
31580		
31581		return this.each(function(i, element) {
31582			var slider = $(this),
31583				handlers = slider.find('.slider-handle'),
31584				ruler = slider.find('.slider-track'),
31585				rulerSize, handlerSize, rulerOffset,
31586				minValueLabel = slider.find('.min-value'),
31587				maxValueLabel = slider.find('.max-value'),
31588				currValueLabel = slider.find('.curr-value'),
31589				drag = false,
31590				sliderTimeout,
31591				oldSliderValue,
31592				sliderValue,
31593				setOffset = function() {
31594					rulerOffset = ruler.offset()[posField];
31595					rulerOffset += options.borders;
31596				},
31597				updateBounds = function() {
31598					minValueLabel.html(options.formatData(options.min));
31599					maxValueLabel.html(options.formatData(options.max));
31600				},
31601				setHandlers = function(handlers, value) {
31602					handlers.each2(function(i, handler) {
31603						var val = (typeof(value) !== 'object') ? value : value[i],
31604								// relative position [0...1]
31605								pos = (options.invert ? options.max - val : val - options.min) / options.range;
31606						// apply custom scaling
31607						pos = options.valueToPositionScale(pos);
31608						// calculate absolute hanler position
31609						pos = Math.round(pos * rulerSize - handlerSize / 2 + options.borders) + 'px';
31610						handler.css(posField, pos);
31611						currValueLabel
31612							.css(posField, pos);
31613					});
31614				},
31615				getHandlerPosition = function(handler) {
31616					return Math.round(handler.offset()[posField] + handlerSize / 2);
31617				},
31618				prepareValue = function(value) {
31619					if ($.isArray(value)) {
31620						// direct value set in multihandler mode
31621						value = $.map(value, boundValue);
31622					} else {
31623						value = $.isArray(sliderValue) ? 
31624							updateClosestItem(value, sliderValue) : 
31625							boundValue(value);
31626					}
31627					return value;
31628				},
31629				setValue = function(value) {
31630					sliderValue = prepareValue(value);
31631					setHandlers(handlers, sliderValue);
31632					currValueLabel
31633						.html(options.formatData(cloneValue(sliderValue)));
31634					
31635					if ($.isFunction(options.onSlide)) {
31636						options.onSlide(cloneValue(sliderValue));
31637					}
31638				},
31639				positionToValue = function(position) {
31640					var value;
31641					
31642					if (position - 1 <= rulerOffset) {
31643						value = (options.invert ? options.max : options.min);
31644					} else if (position >= rulerOffset + rulerSize) {
31645						value = (options.invert ? options.min : options.max);
31646					} else {
31647						// relative position
31648						value = (position - rulerOffset) / rulerSize;
31649						// custom scale correction
31650						value = options.positionToValueScale(value);
31651						// calculating absolute value
31652						value = options.invert ?
31653							options.max - value * options.range :
31654							options.min + value * options.range;
31655					}
31656					return value;
31657				};
31658			
31659			// no ruler — no slider
31660			if (ruler.size() === 0) {
31661				return false;
31662			}
31663			
31664			// set slider dimentions
31665			handlerSize = handlers[sizeOp]();
31666			rulerSize = ruler[sizeOp]() - options.borders * 2;
31667			setOffset();
31668			
31669			updateBounds();
31670			setValue($.isArray(options.value) || typeof options.value === 'number' ?
31671				options.value : [options.min, options.max]);
31672			
31673			ruler
31674				.mousedown(function(event) {
31675                    if (slider.hasClass('disabled')) return;
31676					setOffset();
31677					drag = true;
31678					window.clearTimeout(sliderTimeout);
31679					setValue(positionToValue(event[eventField]));
31680				})
31681				.mouseup(function(event) {
31682					var value;
31683					drag = false;
31684					if ($.isFunction(options.onChange) && oldSliderValue !== sliderValue.toString()) {
31685						value = cloneValue(sliderValue);
31686						if (options.timeout) {
31687							window.clearTimeout(sliderTimeout);
31688							sliderTimeout = window.setTimeout(function() {
31689								options.onChange(value);
31690							}, options.timeout);
31691						} else {
31692							options.onChange(value);
31693						}
31694					}
31695					oldSliderValue = sliderValue.toString();
31696					event.stop();
31697				})
31698				.mousemove(function(event) {
31699					if (drag) {
31700						setValue(positionToValue(event[eventField]));
31701					}
31702				});
31703			
31704			slider
31705				.bind('set-slider-value', function(event, data) {
31706					setValue(data.value);
31707				})
31708				.bind('update-slider-values', function(event) {
31709					updateBounds();
31710					currValueLabel
31711						.html(options.formatData(cloneValue(sliderValue)));
31712				})
31713				.bind('stop-dragging', function(){
31714					if (drag) {
31715						ruler.mouseup();
31716					}
31717				});
31718		});
31719	};
31720}(jQuery));
31721/**
31722 * Simple jQuery plugin for vertical finger scrolling on iPad
31723 */
31724
31725(function($) {
31726	
31727	var isMobile = navigator.platform.slice(0, 2) === "iP";
31728	
31729	$.fn.iscroll = function() {
31730		if (!isMobile)
31731			return this;
31732		
31733		this.each(function() {
31734			var y, move = false, element = $(this);
31735			
31736			element
31737				.bind('mousemove', function(e) {
31738					if (move) {
31739						element.scrollTop(element.scrollTop() + y - e.pageY);
31740						y = e.pageY;
31741					}
31742				})
31743				.bind('mouseup', function(e) {
31744					move = false;
31745				})
31746				.bind('mousedown', function(e) {
31747					// e.preventDefault();
31748					move = true;
31749					y = e.pageY;
31750				});
31751		});
31752		
31753		return this;
31754	};
31755})(jQuery);
31756/* jQuery Autocomplete
31757 * Version 1.0
31758 * Written by Yehuda Katz ([email protected]) and Rein Henrichs ([email protected])
31759 * @requires jQuery v1.2, jQuery dimensions plugin
31760 *
31761 * Copyright 2007 Yehuda Katz, Rein Henrichs
31762 * Dual licensed under the MIT and GPL licenses:
31763 *   http://www.opensource.org/licenses/mit-license.php
31764 *   http://www.gnu.org/licenses/gpl.html
31765 *
31766 */
31767
31768/*
31769 * @description Form autocomplete plugin using preloaded or Ajax JSON data source
31770 *
31771 * @example $('input#user-name').autocomplete({list: ["quentin", "adam", "admin"]})
31772 * @desc Simple autocomplete with basic JSON data source
31773 *
31774 * @example $('input#user-name').autocomplete({ajax: "/usernames.js"})
31775 * @desc Simple autocomplete with Ajax loaded JSON data source
31776 *
31777 * Modified by Stanislav Yadykin at 22/12/2010
31778 * Added popup position and relative option passing for autocomplete event
31779 * Relative option will insert dropdown into offset parent instead of body
31780 */
31781
31782
31783(function($) {
31784	$.ui = $.ui || {}; $.ui.autocomplete = $.ui.autocomplete || {}; var active; $.ui.autocomplete.ext = $.ui.autocomplete.ext || {};
31785
31786	//Initializing autocomplete plugin for search (ajax callback and UI); moved here because ui.init.js is absent in Austin
31787	$.ui.autocomplete.ext.ajax = function(opt) {
31788		var ajax = opt.ajax;
31789
31790		return { getList: function(input) {
31791			var val = input.val();
31792			if (!val)
31793				return false;
31794
31795			// uncomment for local testing search autocomplete
31796			// return input.trigger("updateList", [[{"match": "art"}, {"match": "arte"}, {"match": "arts"}, {"match": "artfulness"}, {"match": "armchair"}, {"match": "armchaire"}, {"match": "armchairs"}, {"match": "applicance"}, {"match": "application"}, {"match": "applicances"}]]);
31797			$.getJSON(ajax, opt.param + "=" + val, function(json) {
31798				input.trigger("updateList", [json.result || json]);
31799			});
31800		} };
31801	};
31802
31803	$.fn.autocompleteMode = function(container, input, size, opt) {
31804		var original = input.val(); var selected = -1; var self = this;
31805
31806		input.parent().find('input.search-submit').click(function(e){
31807			e.stopPropagation();
31808		});
31809
31810		$.data(document.body, "autocompleteMode", true);
31811
31812		$("body").one("cancelAutocomplete", function() {
31813			input.trigger("cancelAutocomplete"); $("body").trigger("offAutocomplete"); input.val(original);
31814		});
31815
31816		$("body").one("activateAutocomplete", function() {
31817			if (active)
31818				input.trigger("activateAutocomplete", [$.data(active[0], "originalObject")]); $("body").trigger("offAutocomplete");
31819		});
31820
31821		$("body").one("offAutocomplete", function(e, reset) {
31822			container.remove();
31823			$.data(document.body, "autocompleteMode", false);
31824			input.unbind("keydownAutocomplete");
31825			$("body").add(window).unbind("click.autocomplete").unbind("cancelAutocomplete").unbind("activateAutocomplete");
31826		});
31827
31828		// If a click bubbles all the way up to the window, close the autocomplete
31829		$(window).bind("click.autocomplete", function(e) {
31830			$("body").trigger("cancelAutocomplete");
31831		});
31832
31833		var select = function() {
31834		if (selected == -1)
31835			return false;
31836			active = $("ul > *", container).removeClass("selected").slice(selected, selected + 1).addClass("selected");
31837			input.trigger("itemSelected.autocomplete", [$.data(active[0], "originalObject")]);
31838			input.val(opt.insertText($.data(active[0], "originalObject")));
31839		};
31840
31841		container.mouseover(function(e) {
31842			// If you hover over the container, but not its children, return
31843			if(e.target == container[0]) return;
31844			// Set the selected item to the item hovered over and make it active
31845			selected = $("ul > *", container).index($(e.target).is('li') ? $(e.target)[0] : $(e.target).parents('li')[0]); select();
31846		}).bind("click.autocomplete", function(e) {
31847			$("body").trigger("activateAutocomplete"); $.data(document.body, "suppressKey", false);
31848			$('#query').focus();
31849		});
31850
31851		input
31852		.bind("keydownAutocomplete", function(e, trueEvent) {
31853			e.keyCode = e.keyCode || trueEvent.keyCode;
31854			code = e.keyCode || e.charCode || e.which;
31855			if (code == 27) {
31856				$("body").trigger("cancelAutocomplete");
31857			} else if (code == 40 || code == 9 || code == 38) {
31858				switch(code) {
31859					case 40:
31860					case 9:
31861					selected = selected >= size - 1 ? 0 : selected + 1; break;
31862					case 38:
31863					selected = selected <= 0 ? size - 1 : selected - 1; break;
31864					default: break;
31865				}
31866				select();
31867			} else if (code == 13) {
31868				$("body").trigger("offAutocomplete");
31869			} else {
31870				return true;
31871			}
31872
31873			$.data(document.body, "suppressKey", true);
31874		});
31875	};
31876
31877	$.fn.autocomplete = function(opt) {
31878
31879		opt = $.extend({}, {
31880			timeout: 1000,
31881			getList: function(input) { input.trigger("updateList", [opt.list]); },
31882			template: function(str) { return "<li>" + opt.insertText(str) + "</li>"; },
31883			insertText: function(str) { return str; },
31884			fixedTop: false, // custom option refMDTWO-1381
31885			wrapper: "<ul class='jq-ui-autocomplete'></ul>"
31886		}, opt);
31887
31888		if ($.ui.autocomplete.ext) {
31889			for (var ext in $.ui.autocomplete.ext) {
31890				if (opt[ext]) {
31891					opt = $.extend(opt, $.ui.autocomplete.ext[ext](opt));
31892					delete opt[ext];
31893				}
31894			}
31895		}
31896
31897		return this.each(function() {
31898			$(this)
31899				.keyup(function(e) {
31900					var typingTimeout = $.data(this, "typingTimeout");
31901
31902					$(this).trigger("keydownAutocomplete", e);
31903
31904					if (typingTimeout)
31905						window.clearInterval(typingTimeout);
31906
31907					if ($.data(document.body, "suppressKey")) {
31908						return $.data(document.body, "suppressKey", false);
31909					} else if ($.data(document.body, "autocompleteMode") && e.charCode < 32 && e.keyCode != 8 && e.keyCode != 46 && this.dataset && e.keyCode != this.dataset.customkeycode) {
31910						return false;
31911					} else {
31912						$.data(this, "typingTimeout", window.setTimeout(function() {
31913							$(e.target).trigger("autocomplete");
31914						}, opt.timeout));
31915					}
31916				})
31917				.bind("autocomplete", function(e, opts) {
31918					var self = $(this);
31919
31920					opts = $.extend({
31921						position: 'bottom',
31922						relative: false
31923					}, opts || {});
31924
31925					self.one("updateList", function(e, list) {
31926						list = $(list)
31927						.map(function() {
31928							var node = $(opt.template(this))[0];
31929							$.data(node, "originalObject", this);
31930							return node;
31931						});
31932
31933						$("body").trigger("offAutocomplete");
31934
31935						if (!list.length)
31936							return false;
31937
31938						var container = $('ul', opt.wrapper).html(list).end(),
31939							offset = opts.relative ? self.position() : self.offset(),
31940							css = {left: offset.left},
31941							parent = opts.relative ? self.offsetParent() : $(document.body);
31942
31943						if (opts.position === 'bottom') {
31944							if (!opt.fixedTop) {
31945								css.top = offset.top + self.outerHeight();
31946							}
31947						} else {
31948							css.bottom = parent.outerHeight() - offset.top - parseInt(self.css('marginTop') || 0);
31949						}
31950
31951						opt.container = container
31952							.css(css)
31953							.appendTo(parent)
31954							.mouseleave(function(){
31955								$(this).data('hideTimeout', setTimeout(function(){
31956									$("body").trigger("cancelAutocomplete");
31957								}, 1000));
31958							})
31959							.mouseenter(function(){
31960								clearTimeout($(this).data('hideTimeout'));
31961							});
31962
31963						$("body").autocompleteMode(container, self, list.length, opt);
31964					});
31965					opt.getList(self);
31966				});
31967		});
31968	};
31969})(jQuery);
31970
31971(function($) {
31972	$.fn.htmlDropdown = function(opts) {
31973		opts = opts || {};
31974		return this.each(function() {
31975			var select = $(this),
31976				id = select.attr('id'),
31977				input = $('<input>');
31978			select.after(
31979				input.attr({
31980					type: 'text',
31981					value: this.options[this.selectedIndex].text,
31982					readOnly: true,
31983					id: id + '-combo',
31984					className: 'combo'
31985				})
31986				.data('parentSelect', select[0]).autocomplete($.extend({}, {
31987					list: $.map(this.options, function(option){return option.text}),
31988					insertText: function(obj) { return obj },
31989					wrapper:  '<div id="' + id + '_options"><ul></ul></div>'
31990				}, opts))
31991				.click(function(e) {
31992					e.preventDefault();
31993					e.stopPropagation();
31994					$(this).trigger("autocomplete", [opts]).blur();
31995				})
31996				.bind("activateAutocomplete", function(e, d) {
31997					$.each($(e.target).data('parentSelect').options, function(i, option){
31998						if (option.text == d)
31999							option.selected = true;
32000					});
32001				})
32002			)
32003			.hide().change(function(e) {
32004				input.val(select.find('option:selected').text() || '');
32005			});
32006		});
32007	}
32008})(jQuery);
32009
32010/**
32011 * jQuery plugin for init and positioning bubbles
32012 * Usage:
32013 *    $('some-selector').rearrange(options)
32014 * Options are:
32015 *    anchor {jQ} Element positioning will be related to
32016 *    position {Array} Array of positioning elements
32017 *        Values are: left, right, top, bottom, center, digits (including negative)
32018 *        Default is to ['center', 'center']
32019 *    
32020 * If no anchor is specified positioning will be related to parent container
32021 */
32022
32023jQuery(function($) {
32024	
32025	$.fn.rearrange = function(options) {
32026		options = $.extend({
32027			anchor: $(),
32028			// relative to offset parent position
32029			position: [false, false]
32030		}, options || {});
32031		
32032		var anchor = options.anchor.size() > 0 ? options.anchor : false,
32033			anchorSize = anchor ? [anchor.width(), anchor.height()] : false,
32034			anchorPosition = anchor ? (function() {var o = anchor.offset(); return [o.left, o.top]})() : false;
32035		
32036		this.each(function() {
32037			var element = $(this), position = [], i, pos,
32038				elementSize = [element.outerWidth(true), element.outerHeight(true)],
32039				parent = element.offsetParent() || $(document.body),
32040				parentSize = [parent.width(), parent.height()];
32041			
32042			// calculate position
32043			for (i = 0; i < 2; i++)
32044				switch (options.position[i]) {
32045					case 'left':
32046					case 'top':
32047						position[i] = anchor ? anchorPosition[i] - elementSize[i] : 0;
32048						break;
32049						
32050					case 'right':
32051					case 'bottom':
32052						position[i] = anchor ? anchorPosition[i] + anchorSize[i] : parentSize[i] - elementSize[i];
32053						break;
32054						
32055					case 'center':
32056					case 'middle':
32057					case undefined:
32058					case false:
32059						position[i] = anchor ?
32060							Math.round(anchorPosition[i] + anchorSize[i] / 2 - elementSize[i] / 2) :
32061							Math.round(parentSize[i] / 2 - elementSize[i] / 2);
32062						break;
32063						
32064					default:
32065						pos = +options.position[i];
32066						position[i] = anchor ? (pos < 0 ?
32067							anchorPosition[i] - elementSize[i] + pos :
32068							anchorPosition[i] + anchorSize[i] + pos
32069						) : (pos < 0 ? parentSize[i] - elementSize[i] + pos : pos);
32070				}
32071			
32072			element.css({
32073				left: position[0] + 'px',
32074				top: position[1] + 'px'
32075			});
32076			
32077		});
32078		
32079		return this;
32080	};
32081});
32082/*globals jQuery, window */
32083/*jslint forin: true, fragment: true, onevar: true, undef: true, nomen: false, eqeqeq: true, plusplus: true, bitwise: false, newcap: true, immed: true, regexp: false, white: false, plusplus: false */
32084/**
32085 * Plugin to use ordinal input as a digit input with the inc/dec links
32086 */
32087 
32088jQuery(function($) {
32089	$.fn.numberInput = function(options) {
32090		// options are init-wide, so if you change postfix for one of
32091		// input initialized in set it will change for all
32092		options = $.extend(true, {
32093			// text to add after value
32094			postfix: '',
32095			// the fractional part length
32096			frac: 0,
32097			// incrementing value, can be function which receives true when
32098			// shift is pressed, uses increment multiplied by 10 in other case.
32099			increment: 1,
32100			// starting value
32101			value: false,
32102			// should value be only positive or not
32103			positive: false,
32104			// Add incremental controls or not
32105			addControls: true,
32106			// text for increment link
32107			incrementSign: '+',
32108			// text for decrement link
32109			decrementSign: '-',
32110			// custom function used for value formatting 
32111			formatFunction: false,
32112			// custom function used for parse manually entered value
32113			parseFunction: parseFloat,
32114			// custom function used for syntax check additionally to
32115			// embedded one, defaults is to check fractional point
32116			keyPress: function(e) {
32117				var input = $(this),
32118					value = input.val(),
32119					pos;
32120					
32121				switch (e.which) {
32122					// special keys
32123					case 0:
32124						switch (e.keyCode) {
32125							// up/down
32126							case 38: case 40:
32127								e.preventDefault();
32128								eventListener.call(this, e,
32129									e.keyCode === 38 ? 1 : -1);
32130								break;
32131						}
32132						break;
32133						
32134					// minus
32135					case 45:
32136						if (options.positive || value.indexOf('-') !== -1) {
32137							e.preventDefault();
32138						}
32139						break;
32140					case 46:
32141						pos = value.indexOf('.');
32142						if (e.which === 46 && options.frac !== 0 && pos !== -1) {
32143							e.preventDefault();
32144						}
32145						break;
32146						
32147					// digits
32148					case 48: case 49: case 50: case 51: case 52: case 53:
32149					case 54: case 55: case 56: case 57:
32150					
32151					// backspace
32152					case 8:
32153						break;
32154						
32155					// deny any changes by default
32156					default:
32157						e.preventDefault();
32158				}
32159			}
32160		}, options || {});
32161		
32162		if (!$.isFunction(options.formatFunction)) {
32163			options.formatFunction = function(value) {
32164				return value.toFixed(options.frac) + options.postfix;
32165			};
32166		}
32167		
32168		var setValue = function(e, value) {
32169				var input = $(this);
32170				
32171				value = typeof value === 'undefined' ? input.data('value') : value;
32172				
32173				if (options.positive && value < 0) {
32174					value = 0;
32175				}
32176				
32177				input.data('value', value).val(options.formatFunction(value));
32178			},
32179			
32180			setOptions = function(e, name, value) {
32181				var opt = {};
32182				
32183				if (typeof name === 'string' && typeof value !== 'undefined') {
32184					opt[name] = value;
32185				} else if (typeof name === 'object') {
32186					opt = name;
32187				}
32188				
32189				$.extend(true, options, opt);
32190				
32191				setValue.call(this);
32192			},
32193			
32194			eventListener = function(e, delta) {
32195				if (e.type === 'mousewheel') {
32196					e.stop();
32197				}
32198				var control = $(this),
32199					input = control.data('input') || control,
32200					increment = $.isFunction(options.increment) ?
32201						options.increment.call(window, e.shiftKey) :
32202						// increase ten times if shift is pressed
32203						(options.increment * (e.shiftKey ? 10 : 1));
32204				setValue.call(input.get(0), e, (input.data('value') || 0) +
32205						(delta || (control.hasClass('decrement') ? -1 : 1)) *
32206						increment);
32207				input.trigger('change');
32208			},
32209			
32210			onKeyUp = function(e) {
32211				var input = $(this),
32212					value;
32213				
32214				if (e.which === 13 || e.which === 27) {
32215					e.preventDefault();
32216					if (e.which === 27) {
32217						setValue.call(this, e);
32218					}
32219					
32220					// use direct fire there because jQ fire event twice in IE
32221					this.blur();
32222				// remove dash from any position except first
32223				} else if (!options.positive && (value = input.val()) &&
32224						value.indexOf('-', 1) !== -1) {
32225					
32226					input.val(value.substring(0, 1) +
32227						value.substring(1).replace(/-/, ''));
32228				}
32229			},
32230			
32231			onFocus = function(e) {
32232				this.select();
32233			},
32234			
32235			onChange = function(e) {
32236				setValue.call(this, e, options.parseFunction($(this).val()));
32237			}
32238		
32239		this.each(function(x, input) {
32240			if (input.tagName !== 'INPUT' || input.type.toUpperCase() !== 'TEXT') {
32241				return;
32242			}
32243			
32244			var value = options.value || options.parseFunction(input.value || "0") || 0;
32245			
32246			input = $(input)
32247				.attr('title', 'Use your mouse wheel to change value, hold down shift key to 
32247speedup the changes')
32248				.data('value', value)
32249				.val(options.formatFunction(value))
32250				.keyup(onKeyUp)
32251				.keypress(options.keyPress)
32252				.focus(onFocus)
32253				.bind('setValue', setValue)
32254				.bind('setOptions', setOptions)
32255				.bind('mousewheel', eventListener)
32256				.bind('change', onChange);
32257			
32258			if (options.addControls) {
32259				// decrement value
32260				$('<a href="" class="decrement" />').html(options.decrementSign).insertAfter(input)
32261					.data('input', input)
32262						.click(false)
32263						.mousedown(eventListener);
32264				
32265				// increment value
32266				$('<a href="" class="increment" />').html(options.incrementSign).insertAfter(input)
32267					.data('input', input)
32268						.click(false)
32269						.mousedown(eventListener);
32270			}
32271		});
32272		
32273		return this;
32274	};
32275});
32276/* jQuery SrollLoader plugin
32277 * Version 1.0
32278 * Written by Stanislav Yadykin ([email protected])
32279 * @requires jQuery v1.5
32280 */
32281
32282/*
32283 * @description Fires load event when scrolling is almost at the end 
32284 */
32285
32286(function($) {
32287	var rewindListener = function(e) {
32288			var holder = $(this);
32289			
32290			holder.data('scroller').animate({
32291				scrollTop: 0
32292			}, holder.data('options').rewindTime);
32293		},
32294		scrollListener = function(e) {
32295			var element = $(e.target),
32296				scrollTop = element.scrollTop(),
32297				holder = element.data('holder'),
32298				options = holder.data('options'),
32299				oldScroll = holder.data('scroll') || 0,
32300				shiftValue = e.target.scrollHeight - options.deviation,
32301				visible = holder.data('visible'),
32302				controls = holder.data('controls');
32303			
32304			if (scrollTop > 0 && !visible) {
32305				options.showControls(controls);
32306				visible = true;
32307			} else if (scrollTop === 0 && visible) {
32308				controls.fadeOut();
32309				visible = false;
32310			}
32311			
32312			holder
32313				.scrollTop(scrollTop)
32314				.data({
32315					visible: visible,
32316					scroll: scrollTop
32317				});
32318			
32319			if (element.height() + scrollTop > shiftValue &&
32320				element.height() + oldScroll < shiftValue) {
32321				holder.trigger(options.loadEventName, [holder, element]);
32322			}
32323		},
32324		clickListener = function(e) {
32325			e.preventDefault();
32326			
32327			$(this).data('holder').trigger('rewind');
32328		};
32329		
32330	
32331	$.fn.scrollLoader = function(options) {
32332		options = $.extend({
32333			deviation: 100,
32334			scrollerSelector: '.scroller',
32335			rewinderSelector: '.scroll-rewinder',
32336			controlsSelector: '.scrolling-item',
32337			loadEventName: 'load',
32338			showControls: function(controls) {
32339				controls.fadeIn();
32340			},
32341			rewindTime: 500
32342		}, options);
32343		
32344		
32345		$.each(this, function(i, holder) {
32346			holder = $(holder);
32347			var scroller = holder
32348					.find(options.scrollerSelector)
32349					.data({holder: holder})
32350					.bind('scroll', scrollListener),
32351				rewinder = holder
32352					.find(options.rewinderSelector)
32353					.data({holder: holder})
32354					.bind('click', clickListener),
32355				controls = holder.find(options.controlsSelector);
32356				
32357			holder
32358				.data({
32359					options: options,
32360					scroller: scroller,
32361					controls: controls,
32362					rewinder: rewinder
32363				})
32364				.bind('rewind', rewindListener);
32365		});
32366		
32367		return this;
32368	};
32369})(jQuery);
32370/*globals buildThumbnailLink, diffuse2RGB, $, myDeco */
32371/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32372          sloppy: true, white: true */
32373/**
32374 * @fileOverview Used for adding common method to all views that required it
32375 */
32376var generateElementForDragMixin = {
32377	/**
32378	 * Internal settings
32379	 * @type {Object}
32380	 */
32381	settings: {
32382		// thumbnail sizes for component and scrapbook items
32383		thumbnailSize: {
32384			small: {
32385				width: 70,
32386				height: 70
32387			},
32388			normal: {
32389				width: 110,
32390				height: 110
32391			}
32392		},
32393		// settings for material drag thumbnail
32394		dragThumbnail: {
32395			width: 50,
32396			height: 50,
32397			percent: 50
32398		}
32399	},
32400
32401	/**
32402	 * Default thumbnail sizes mode, will override later (added for consistency)
32403	 * @type {String}
32404	 */
32405	mode: 'normal',
32406
32407	/**
32408	 * Generate visual representation of given component for drag-n-drop
32409	 * @param {Object} component Component to render thumbnail for
32410	 * @param {String} type Item type
32411	 */
32412	generateElementForDrag: function(component, type) {
32413		type = type || this._model.type;
32414
32415		// Setting width and height explicitly to avoid situation when
32416		// image is not loaded yet and width and height would be miscalculated
32417		var holder = $('<div id="new-component" />'),
32418			inner;
32419		switch (type) {
32420			case 'component':
32421				holder
32422					.width(this.settings.thumbnailSize[this.mode].width)
32423					.height(this.settings.thumbnailSize[this.mode].height)
32424					.append($('<img class="original" />')
32425							.attr('src', component.thumbnail || component.componentData['3d'].render_perspective));
32426				break;
32427			case 'material':
32428				holder
32429					.width(this.settings.dragThumbnail.width)
32430					.height(this.settings.dragThumbnail.height)
32431					.addClass('no');
32432
32433				inner = $('<div />').append('<div class="helper" />').appendTo(holder);
32434
32435				if (typeof component.thumbnail !== 'undefined') {
32436					inner.css('background-image', 'url(' + component.thumbnail + ')');
32437				} else {
32438					inner.addClass('color').css('background-color', 'rgb(' + diffuse2RGB(component.diffuse) + ')');
32439				}
32440				break;
32441			case 'scrapbook':
32442				holder
32443					.addClass('photo no')
32444					.append($('<img src="' + component.thumbnail + '" style="border: 2px solid #ccc; border-radius: 5px;" />'))
32445					.append('<div class="helper" />');
32446				break;
32447			default:
32448				// For other items the width & height are not explicitly set because
32449				// they are not immediately known and may vary.
32450				holder.append('<img class="original" src="' + component.thumbnail + '"/>');
32451		}
32452		return holder;
32453	}
32454};
32455/*globals myDeco, $, hex2Diffuse */
32456/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32457          sloppy: true, white: true */
32458
32459myDeco.App.palette = {
32460	palettes: {},
32461
32462	settings: {
32463		palette: 'dulux'
32464	},
32465
32466	init: function() {
32467		if (!this.palettes[this.settings.palette]) {
32468			throw gettext("No palette found");
32469		}
32470
32471		$.extend(true, this, this.palettes[this.settings.palette].init());
32472	},
32473
32474	getIndex: function(color) {
32475		return this.colors.indexOf(color.toUpperCase().replace('#', ''));
32476	},
32477
32478	getMaterial: function(key) {
32479		key = key.replace('#', '').toUpperCase();
32480		var color = '#' + key,
32481			name = this.getName ? this.getName(key) : false;
32482
32483		return {
32484			'ID': 'color-' + key,
32485			'component_id': color,
32486			'name': name || 'Color ' + color,
32487			'ambient': [0, 0, 0],
32488			'diffuse': hex2Diffuse(key),
32489			'shader': 'combi_material',
32490			'shiny': 0,
32491			'use_diffuse_map': false,
32492			'class': 'color',
32493			'branded': myDeco.settings.DULUX_BRANDING && name,
32494			'url': this.getUrl ? this.getUrl(key) : false,
32495			'isColor': true
32496		};
32497	}
32498
32499};
32500
32501$(function() {
32502	myDeco.App.palette.init();
32503});
32504/*globals myDeco, $ */
32505/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32506          sloppy: true, white: true */
32507
32508myDeco.App.palette.palettes.dulux = {
32509	// list of color elements
32510	colors: '684157,6F4350,6F3B42,583B3F,8A383C,7A3A3D,703A3D,6F423F,644540,72453E,53433D,8B4C3C,764B3C,6B4C3D,674E3E,605149,65483D,6A4C3D,6A533D,6A543B,845F38,785E3A,66543D,9F7A38,4D4B3E,4A5C3F,395642,2D5C46,376144,1F6349,1A5B4E,285B4B,0A636C,305B69,3B4E53,055B71,355061,37536C,3F5169,3F486E,414966,4F4D60,4C4769,594669,644466,5D3F4F,4F4446,4D4143,4F413F,61514A,55463F,53483D,48433E,3D3D3C,854662,874C5E,7B4353,7B3B46,9A363B,A7363E,BB434A,943E3B,83473F,8D463B,884E3A,A84E39,B25938,95563B,8D5B3B,9D6139,9A6142,996237,A86D34,996C40,AF7630,B17E2D,9D7235,E6B81D,8D9340,758443,4C6C41,356E3C,2E7349,2D7A58,007F61,22665A,00767C,2A6B79,05687D,006F89,255C7A,00618F,33608F,4F66AB,3C4B73,524A74,64568C,6A4B75,785A7A,74466D,5A4548,5D4543,62453F,6E5248,705849,745C47,5B5044,5E5E5D,9A4466,A44E6B,A0415B,A24351,A64048,B2414B,B24848,B03F3A,B3463B,D05534,CE5D39,D36232,D86B3C,D46F37,C76F33,D9762F,CC773F,CF8129,DC8D1D,E2983E,DC9D39,F2AE1E,E4B017,F7C61D,B6A931,A2AC31,719A41,399146,308F56,00925B,008868,008674,328E94,007B90,0084A9,007498,006F9B,3C7AAE,2C6FAE,5A709E,4D5A8D,5E5A81,66587E,775C8E,895C8D,804466,7F6968,7E6660,82695F,8E766C,8E796B,9F8E82,AC9D8E,777777,A05174,AB5974,A64D61,AE596A,BF5661,C1616A,C05F5C,B5473A,CF5141,DB6335,D46946,E56F2F,DE7649,D97944,D3824A,E48E54,D88135,F19828,FFA520,FAAB1D,FFBF25,FFC300,FFC900,FBCB43,CFC142,C2B91B,87B65B,75B875,5BAA75,4CAB7F,29AE89,00A58E,0095A0,008CA3,0092B6,3086A8,3E89B3,4A89BC,5678A2,7384AD,6E74A0,78729E,81709A,886DA0,9B6D9F,8B5B80,AA9A99,B6A6A1,C1B3AD,B8A8A0,B9AAA0,C1B4AB,C2B6AA,A0A09F,BD7494,BA7086,C97182,C56679,D97980,DE858A,D57974,D17867,CC6B5B,D87452,DC7F5F,DD8251,E58A62,E18B5D,D88B56,DF8647,F49B43,F9AA3D,FFB448,FFC054,FFC43C,FFCD59,FFC924,FFD251,CFC057,B9BD5C,96C16C,7EB17E,75BA8A,69B991,6EB79D,4ABDA7,69B4B8,69A6B6,35A3C3,579AB9,689EC1,66A1CD,7690B5,808FB5,8488AF,827DA5,9485AA,967BAD,A580A6,AE7DA2,D5CECC,D5CECA,D9CEC9,D8CFC9,DFD1C7,D6CEC7,D6CEC5,CDCDCC,C98AA4,D293A4,D7949E,D38894,E39A9D,E19094,DD908A,E29D8E,DE9080,E6A188,E3977B,EAAA86,EA9E7C,E89F78,E09C70,EAA06F,F7A955,FBBB5C,FDCB7A,FFD181,FFD877,F9D787,FFD25F,F6CE6F,D9C96B,CACC77,B1D18A,A9D2A8,83C396,84C8A5,9AD0BC,6FCCB8,8BCED1,95C2CD,64B3CF,8FBDCF,8DB4D0,9EC0D7,93A8C7,8D9ABC,999DBE,9691B5,A79BBA,AF98C4,BD9FBD,
32510CBA7C1,ECE9E6,EBE9E6,ECE9E5,EDE9E5,EDE9E4,EDE9E4,EEE9E2,E9EAE7,C16B9E,BB597E,C86D94,C25C77,CF5F6F,D87082,E17161,E47C6C,E66E5B,E98762,EC926F,F1995A,EE8C4D,EB8830,F9B465,FFB453,FAB44D,FDC46B,FFC657,F9C833,FFCF58,FFC300,FDCB3A,EDCA33,B6BF3C,98B641,89B555,75B65D,68AF6B,57B176,33AD7B,00AA8F,2BA8A8,35AFBA,00A2BE,008ABD,0097C8,4C84B8,5873A7,6883B6,797FA8,7571A4,8A7AA2,A389B8,A77AAC,B977A9,E2DCDC,E9DDD3,E4DCD4,EADED0,ECE9E3,E4DECF,E0E1DC,D9DDDB,D188B8,D689AE,E197B2,D57C99,E18C9E,EC9CA4,EB9688,EEA498,F5967B,EF9F7E,F2AB8C,EDA97B,F5AD74,FFB26B,FBC783,FFBF6D,FAC377,FFD37F,FFCB6D,FFD46A,FFCF3F,FFD865,FECF51,F0D458,C6CC5A,B0CA5F,A3C979,94C97E,87C486,8ECFA1,64C195,5CCAB4,6ECBC7,68C3CC,54B7CE,2EA3D1,47AED8,689DCC,7290C0,809BC8,8E92B6,9A97C4,B2A5C2,BCA6CC,BC95C1,D6A5CC,ECE9EA,F0E8E2,F0E8DF,F0E8DC,F2EAD7,EEE9DB,E9EBDF,E5EAE7,E4B7D6,E2A6C5,EBBECF,E5A7BE,EDABBB,F2BEC3,F2B4AA,F5C4BB,F8A78B,F5B99D,F1BDA5,F1BA96,F8C394,FBBD73,FFCB96,FFD295,FCD38D,FCDDA4,FFD996,FADA93,FCD36E,FFE08A,FFDD79,F5DC7B,DADD88,BFD47C,BAD795,BCE2AB,A3D5A3,A8DFB7,8FD5B0,93DECE,8ED7D3,98D7DD,89CDDE,61B9DE,86C9E5,94B7DB,92ABD3,AABEDC,BDBED5,BDBBDA,C6BDD3,CAB9D7,D5BAD8,E6BDDA,ECE9EA,ECE8E5,ECE9E5,F0EADD,EFE9DE,EBEAE1,E9EAE4,E7EBEB,E9C6DE,EABFD6,EFCAD8,EABACB,EFCAD2,EDD1D0,EECEC6,F6D1C9,F8CEBE,F4D2C2,F4D2C0,F4D4BF,F8D1AA,FCD297,F8D7B4,FFE1B7,FBE1B2,FEE5B9,FBE0AE,FFDD8E,FEE6A3,FDE8AF,FFE19B,F9E49B,E7E7A4,D7E5A3,D3E7B6,C6E8B6,BDE2BE,B9E4C3,B5E7CB,B3E5D8,99DBD8,A1DADC,B5E0EB,92CDE7,A0D4E8,B7D5E7,B4C6E0,CFD3E3,D4D4E3,CECCE2,D0C8DA,D7C8E0,DDC6DF,E5D2DF,EDE9EA,ECEAE6,ECE9E3,EFEADF,F3EFDD,EDEAE2,EEEFEA,EAEAE7,ECD3E3,EFD5E1,F2DFE3,F0D6DE,F3D8DD,EEDDDB,F6DCD4,F0DBD2,F6DDD2,F4DCD0,F3DDCF,F6E2D3,F6E1C7,FADCAF,F8DCBD,F9E5C3,F8E4BC,F9E7C3,F7E7C5,FDE5B6,FBEBBC,F9EDC9,F6E5AC,F7E8B7,E7E5A9,E0EABA,E2EDCF,D2E9C5,CAE6C7,C9E8D0,C8EAD5,BFEADF,CCEBE6,BAE3E5,C2E4EC,B4DCEA,C4DCE9,C6DBE6,BECEE4,DCDDE3,DDDDE3,DBD9E7,DAD3E3,DED3E1,E5D2E3,E6DCE2,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,EFDDE6,F2DEE6,F1E7E8,F1E7E7,F1E1E2,F0E8E6,F6E4DD,F1E7E2,F3E9DD,F4E6DE,F4E7DE,F6E8DC,F5E9D5,F9E8CA,F7E5CD,F7ECD6,F9EACA,F5E9D4,F7EDD5,F8EFD4,FAE9C1,F6E9CC,F6E9BD,F6E9C4,F0EDC9,E7ECCE,E8EFD8,DCEFD1,D9EFD6,D9EFDC,D4EFDE,CFEEE4,DAEEEA,DDEEEC,CEE5EA,C5E4EC,D3E5EB,CADCE8,DDE1E6,E9EAEB,EAE9EB,E2E0E9,EAE9EA,ECE9EA,EBDEE8,EFE9EB,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,865873,864E66,954D65,994D5C,9F5C60,AC545C,A34847,9D5351,AA5452,8C5247,995039,BE5F37,AE663F,B36836,AD6A33,C4742F,C07D38,C58329,D39140,B37C39,C98734,D59421,C58926,DAA01D,877E43,7C8B4C,688841,507941,577549,3F6941,3C7C53,1D866A,1F6569,1E727D,2A7186,14647F,0B688E,235678,2F5472,455E7D,425578,5E658C,5A5679,675372,6F5670,775470,D8C1B9,D7C3B8,DBC5B6,E2C9B2,E4CAB0,E0CBB1,EAD2AF,E6D2AD,AD7C98,AA7189,AF6F81,C0707F,BF7579,CD787D,CA7873,C17772,B97772,BF7769,CE7354,C1704D,C47C56,CE8759,D48C53,DD8A43,DF9B59,DA9F5B,E1A869,F7AB52,DBA966,E3A956,F5C064,E5B04F,AEA46C,A2AE76,8FAD6B,87AD75,9CB98D,799D77,619C73,65A991,51898D,67A4AD,5791A6,46809B,3E799B,456C8E,5B7792,5E7697,5B6988,767B9F,7F7A9E,897593,967A97,A07896,E9DCD5,EDDCD2,EBDDD1,EEDECF,F0DFCD,EAD6BE,F0DEC4,F0E1C6,C39CB0,CDA0B1,CA9EA9,D0949C,D09798,DA989A,DA9C97,D49C97,DA9C8F,D29B90,DFA089,D79A7F,DAA689,DAA17D,E1A77B,EBAB79,E7AB74,E6B682,F0BB80,F5BC76,E5B981,EBB873,F3C47B,ECC077,CEC389,BBC496,ACC48C,A7C697,B2CBA6,A1C1A0,95C2A0,90C3B0,7DAEB2,A2CCD3,93BAC9,80A9BF,7299B4,6C92B4,859CB4,8A9BB5,808EAF,999BB9,9E9AB7,A695AE,B29BB2,B99AB1,EEE8E4,F1E8E0,F2E7DB,F6E7D9,F4E8D5,F3E0C9,F3E5CE,F2E7D3,D7BDCA,D9B8C3,DCC2C7,DEB6BB,DEBAB9,E1AFAF,E2B5B0,DDB4B0,E3B5AA,DEB4AB,E6B7A6,E5BAA7,E3BBA5,EAB897,E8B894,F0BC95,EDBC91,F5CA9E,EABE90,F4C58C,EBC89E,F1CB9B,EECA8D,F0CB91,DFD49E,D7DEB8,BFD3A3,C8E0BC,C9DEBE,BED8BC,B2D2B9,ACD6C5,9DCACB,B4D7DC,B1CED9,9CBFD1,A2BCCE,9FB7CF,A5B6C8,A7B4C9,A8B0C9,B0B1C9,C0BCD0,C7BAC
32510B,CBBBCB,C3ADBD,EEE8E6,EFE9E4,F0E9E1,F0E9DE,EFE9DE,F2E9DA,F4EAD8,F8EDD6,E1D2D8,E6CFD6,EADBDD,E7CFD1,EAD1D0,E7C6C5,ECCECA,E9CECA,EBCEC7,E7CDC7,EBD0C5,EDD1C4,EBD1C2,F0CAB3,E9CAB2,F3D5BE,F2CDAE,F6D8B9,F0CFAD,FFE0B7,EED0AC,F4D9B8,F8DFB3,EFD5AF,EFE4B2,E7ECD2,D2E1BA,D4E6CA,D6E4CC,CEE2CC,BFDBC5,C9E6D9,B3D5D6,C5E2E4,BFD8E2,AEC9D7,C4D5E0,C3D0DF,C5D0DC,C7D0DE,CFD2DC,C7C8D9,D6D3E1,D9D1DA,DCD2DB,E1D2DC,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,EEE8E8,E9DBDE,EFE8E7,EBDBDB,EFE8E5,EBDDDB,F1E7E4,ECDBD7,F1E7E3,ECDAD5,EEE7E3,EEDCD3,F2E8E0,F5DECE,F4DFCD,F5E7DB,F5DFCB,F7DFC7,F3DFCA,F7E9D5,F3E0C9,EED7BC,F7E8CF,F5DFC0,F2EACB,E8ECD5,E4ECD6,E2EEDA,E5ECDF,DFEEDC,DCEFDE,D9EFE4,CAE1E0,CDDFE1,CFDFE5,CCDFE8,D7DDE1,D8DCE0,D7DDE2,D8DCE3,DCDDE2,D2D3DE,DFDDE2,E1DDE1,EDE9EB,E5DCE1,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,6E5F66,876B75,83696C,8C6C6C,916865,7C6664,8A6760,95685E,986C5C,906F5E,90725E,886753,8B6F5A,A37959,AE7D57,A1754C,AC8355,9D7B57,988268,9A7E5A,8C785A,826E4F,A08965,98805A,7B755B,82825E,797F60,768760,697B5F,577056,5D7562,59776B,557E7C,53757A,5B7F89,567B88,5F7784,546F82,63747E,687386,747D94,727590,5A5869,756B82,807180,5C5460,A0989A,A39993,AA9A8F,AA9F8F,A09A8E,929898,92989D,939393,8E8288,A58E96,988184,A99090,AE8D89,9D8B88,A88C86,B28D85,B29083,AC9182,AD8B75,A38877,B3957F,BB997E,C49B7C,C09D7F,CAAA88,C0A58A,BDA78C,BDA07D,A99880,AA9372,BDA47E,B49E7F,928C73,A1A080,8E9477,8D9C77,7F9076,7E947C,839786,728D82,729694,68888E,839FA6,7E9BA6,8099A7,7E92A1,87959D,838C9D,999FB2,9395AA,737083,9D96A7,A094A1,827885,C4C0C0,C7C0BC,CBC1B8,D0C6B7,D3CFC6,CAD1D2,C9CFD4,BDBDBB,B2A9AD,C4B4B9,B7A6A7,C5B5B3,C9B2B0,B4A6A3,C5B3AE,CBB3AD,CCB4AB,C8B6AC,BFA290,BFAB9E,CCB8A8,C3A38C,D3B198,CFB298,D7BEA3,CFBAA5,C7B29A,D4BFA5,C7BBA8,C5B39A,D4BE97,CCBEA2,B3AE97,C1C0A4,A3A98E,A2B090,A5B39D,A8BAA6,ACB7AD,97B4A8,8EAEAC,8AA8AC,9DB5BB,99B1BB,9BB0BC,9BABB6,93A1A8,9CA7B7,B2B7C4,BABBCA,9F9DAB,BEBAC5,C1B8C0,A49CA7,DEDBD9,DFDBD8,E2DCD6,DDD4C2,DFDCD3,DDE2E2,D7DCE1,DADBD9,C9C2C4,D9CFD1,CDC1C2,D9CFCD,DBCDCA,CAC0BE,D8CDC9,DDCEC9,DCCEC8,DCD0C8,D7C5B8,D2C3B9,DED0C5,D3BAA8,E5D2C3,E3D2C3,DEC8B2,D8C6B4,D7C8B5,DCC9B3,DED3C3,D5C8B5,E0CFB0,DFD3BC,CBC6B2,D7D6BD,C6CBB5,CAD7C2,BEC9B7,C5D4C3,C8D1C9,B4CCC2,BCD6D3,B1CBCE,B9C7CC,B5C9D0,B6C8D1,BAC5CD,BAC4C9,BBC2CE,CED1DA,D1D1D9,B9B7C1,D4D1D8,D7D0D5,BCB6BD,ECE9E7,EBE9E6,EEE9E3,EBEAE6,ECEAE1,E8EBE8,E9EAEB,E9E9E6,D5CFD0,E3DBDC,D7CDCC,E4DCDA,E6DCD8,D8CECB,E3DAD7,E6DAD6,E7DCD7,E7DCD5,E3D1C5,DBCFC7,E9DDD3,E2D1C4,EFDDCF,EADDD1,E4D3C0,E0D1C2,DED2C3,E2D3C1,E2D8CA,DED7CD,EDE0CA,E9DEC8,D7D2BF,E2E1C7,DDE0CD,DBE3CD,CBD5C4,D3DFD1,D6DED6,D3DFD9,CEE0DE,C1D5D6,CCD9DD,C9DAE0,C4D2DA,C9D1D7,C8D1D5,C9CFD9,DCDDE1,DDDDE0,D2D0D7,DEDCDF,E0DCDF,D3D0D4,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,E0DCDB,EDE8E7,E3DBDA,ECE9E6,EEE9E6,E3DBD8,ECE8E5,EEE8E4,EDE9E5,EEE8E3,F0E8E1,E5DCD5,F2E9E1,F0E9E2,F2E8DE,F0E9E1,EBDDCF,E7DCD1,EFE9E1,E6DDD2,EAE4D9,E7E3DB,F1E9DB,F0E9DC,EEEAD9,EDEBD4,E8EBDA,E7EBDD,D8E0D1,E3ECE0,E5ECE4,E5EBE5,E2ECE9,D1DFDF,D3DEE0,D1DEE3,D1DEE4,D9DDDF,D5DDE0,D9DCE0,E9EAEA,EAE9E9,DEDCE0,EAE9E8,EDEAEA,DFDCDE,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF,FFFFFF',	
32511	names: 'Russian Velvet 1, Summer Surprise 1, Moroccan Velvet 1, Garnet Symphony 1, Ruby Fountain 1, Red Stallion 1, Fire Cracker 1, Volcanic Splash 1, Hot Paprika 1, African Adventure 1, Flame Frenzy 1, Auburn Falls 1, Mexican Mosaic 1, Ginger Glow 1, Roasted Pumpkin 1, Spicy Sandalwood 1, Nigerian Sands 1, Rich Havana 1, Golden Bark 1, Earth Glaze 1, Sunflower Symphony 1, Sulphur Springs 1, Delhi Bazaar 1, Sunny Savannah 1, Tarragon Glory 1, Indian Ivy 1, Woodland Fern 1, Dublin Bay 1, Paradise Green 1, Emerald Delight 1, Jade Cluster 1, Minted Glory 1, Blue Diamond 1, Hawaiian Blue 1, Azure Fusion 1, Celestial Blue 1, Venetian Crystal 1, Royal Regatta 1, Sapphire Springs 1, Martian Skies 1, Plush Velvet 1, Amethyst Falls 1, Purple Infusion 1, Velvet Ribbon 1, Pamplona Purple 1, Damson Dream 1, Maraschino Mocha 1, Cocoa Blush 1, Bitter Chocolate 1, Exotic Spice 1, Velvet Truffle 1, Rich Praline 1, Rum Caramel 1, Night Jewels 1, Russian Velvet 2, Summer Surprise 2, Moroccan Velvet 2, Garnet Symphony 2, Ruby Fountain 2, Red Stallion 2, Fire Cracker 2, Volcanic Splash 2, Hot Paprika 2, African Adventure 2, Flame Frenzy 2, Auburn Falls 2, Mexican Mosaic 2, Ginger Glow 2, Roasted Pumpkin 2, Spicy Sandalwood 2, Nigerian Sands 2, Rich Havana 2, Golden Bark 2, Earth Glaze 2, Sunflower Symphony 2, Sulphur Springs 2, Delhi Bazaar 2, Sunny Savannah 2, Tarragon Glory 2, Indian Ivy 2, Woodland Fern 2, Dublin Bay 2, Paradise Green 2, Emerald Delight 2, Jade Cluster 2, Minted Glory 2, Blue Diamond 2, Hawaiian Blue 2, Azure Fusion 2, Celestial Blue 2, Venetian Crystal 2, Royal Regatta 2, Sapphire Springs 2, Martian Skies 2, Plush Velvet 2, Amethyst Falls 2, Purple Infusion 2, Velvet Ribbon 2, Pamplona Purple 2, Damson Dream 2, Maraschino Mocha 2, Cocoa Blush 2, Bitter Chocolate 2, Exotic Spice 2, Velvet Truffle 2, Rich Praline 2, Rum Caramel 2, Night Jewels 2, Russian Velvet 3, Summer Surprise 3, Moroccan Velvet 3, Garnet Symphony 3, Ruby Fountain 3, Red Stallion 3, Fire Cracker 3, Volcanic Splash 3, Hot Paprika 3, African Adventure 3, Flame Frenzy 3, Auburn Falls 3, Mexican Mosaic 3, Ginger Glow 3, Roasted Pumpkin 3, Spicy Sandalwood 3, Nigerian Sands 3, Rich Havana 3, Golden Bark 3, Earth Glaze 3, Sunflower Symphony 3, Sulphur Springs 3, Delhi Bazaar 3, Sunny Savannah 3, Tarragon Glory 3, Indian Ivy 3, Woodland Fern 3, Dublin Bay 3, Paradise Green 3, Emerald Delight 3, Jade Cluster 3, Minted Glory 3, Blue Diamond 3, Hawaiian Blue 3, Azure Fusion 3, Celestial Blue 3, Venetian Crystal 3, Royal Regatta 3, Sapphire Springs 3, Martian Skies 3, Plush Velvet 3, Amethyst Falls 3, Purple Infusion 3, Velvet Ribbon 3, Pamplona Purple 3, Damson Dream 3, Maraschino Mocha 3, Cocoa Blush 3, Bitter Chocolate 3, Exotic Spice 3, Velvet Truffle 3, Rich Praline 3, Rum 
32511Caramel 3, Night Jewels 3, Russian Velvet 4, Summer Surprise 4, Moroccan Velvet 4, Garnet Symphony 4, Ruby Fountain 4, Red Stallion 4, Fire Cracker 4, Volcanic Splash 4, Hot Paprika 4, African Adventure 4, Flame Frenzy 4, Auburn Falls 4, Mexican Mosaic 4, Ginger Glow 4, Roasted Pumpkin 4, Spicy Sandalwood 5, Nigerian Sands 4, Rich Havana 4, Golden Bark 4, Earth Glaze 4, Sunflower Symphony 4, Sulphur Springs 4, Delhi Bazaar 4, Sunny Savannah 4, Tarragon Glory 4, Indian Ivy 4, Woodland Fern 4, Dublin Bay 4, Paradise Green 4, Emerald Delight 4, Jade Cluster 4, Minted Glory 4, Blue Diamond 4, Hawaiian Blue 4, Azure Fusion 4, Celestial Blue 4, Venetian Crystal 4, Royal Regatta 4, Sapphire Springs 4, Martian Skies 4, Plush Velvet 4, Amethyst Falls 4, Purple Infusion 4, Velvet Ribbon 4, Pamplona Purple 4, Damson Dream 4, Maraschino Mocha 4, Cocoa Blush 4, Bitter Chocolate 4, Exotic Spice 4, Velvet Truffle 4, Rich Praline 4, Rum Caramel 4, Night Jewels 4, Russian Velvet 5, Summer Surprise 5, Moroccan Velvet 5, Garnet Symphony 5, Ruby Fountain 5, Red Stallion 5, Fire Cracker 5, Volcanic Splash 5, Hot Paprika 5, African Adventure 5, Flame Frenzy 5, Auburn Falls 5, Mexican Mosaic 5, Ginger Glow 5, Roasted Pumpkin 5, Spicy Sandalwood 4, Nigerian Sands 5, Rich Havana 5, Golden Bark 5, Earth Glaze 5, Sunflower Symphony 5, Sulphur Springs 5, Delhi Bazaar 5, Sunny Savannah 5, Tarragon Glory 5, Indian Ivy 5, Woodland Fern 5, Dublin Bay 5, Paradise Green 5, Emerald Delight 5, Jade Cluster 5, Minted Glory 5, Blue Diamond 5, Hawaiian Blue 5, Azure Fusion 5, Celestial Blue 5, Venetian Crystal 5, Royal Regatta 5, Sapphire Springs 5, Martian Skies 5, Plush Velvet 5, Amethyst Falls 5, Purple Infusion 5, Velvet Ribbon 5, Pamplona Purple 5, Damson Dream 5, Maraschino Mocha 5, Cocoa Blush 5, Bitter Chocolate 5, Exotic Spice 5, Velvet Truffle 5, Rich Praline 5, Rum Caramel 5, Night Jewels 5, Russian Velvet 6, Summer Surprise 6, Moroccan Velvet 6, Garnet Symphony 6, Ruby Fountain 6, Red Stallion 6, Fire Cracker 6, Volcanic Splash 6, Hot Paprika 6, African Adventure 6, Flame Frenzy 6, Auburn Falls 6, Mexican Mosaic 6, Ginger Glow 6, Roasted Pumpkin 6, Spicy Sandalwood 6, Nigerian Sands 6, Rich Havana 6, Golden Bark 6, Earth Glaze 6, Sunflower Symphony 6, Sulphur Springs 6, Delhi Bazaar 6, Sunny Savannah 6, Tarragon Glory 6, Indian Ivy 6, Woodland Fern 6, Dublin Bay 6, Paradise Green 6, Emerald Delight 6, Jade Cluster 6, Minted Glory 6, Blue Diamond 6, Hawaiian Blue 6, Azure Fusion 6, Celestial Blue 6, Venetian Crystal 6, Royal Regatta 6, Sapphire Springs 6, Martian Skies 6, Plush Velvet 6, Amethyst Falls 6, Purple Infusion 6, Velvet Ribbon 6, Pamplona Purple 6, Damson Dream 6, Maraschino Mocha 6, Cocoa Blush 6, Bitter Chocolate 6, Exotic Spice 6, Velvet Truffle 6, Rich Praline 6, Rum Caramel 6, Night Jewels 6, Flamingo Fun 1, Rock Candy 1, Sweet Sundae 1, Fuchsia Falls 1, Rose Trellis 1, Party Surprise 1, Silken Sunrise 1, Caribbean Dawn 1, Bongo Jazz 1, Mango Melody 1, Coral Canyon 1, Amber Queen 1, Harvest Fruits 1, Moroccan Sands 1, Polar Flame 1, Golden Rambler 1, Californian Sands 1, Pharaohs Gold 1, Golden Fern 1, Buttercup Fool 1, Banana Dream 1, Lemon Chiffon 1, Easter Morn 1, Spring Breeze 1, Lime Zest 1, Kiwi Burst 1, Willow Creek 1, Green Parrot 1, Grecian Garland 1, Granada Green 1, Forest Falls 1, Grecian Spa 1, Crystal Surprise 1, Javan Dawn 1, Summer Medley 1, Azure Sky 1, Holiday Blues 1, Blue Seduction 1, Mystic Mauve 1, Fragrant Cloud 1, Lilac Heather 1, Amethyst Showers 1, Lavender Blossom 1, Lilac Spring 1, Violet Verona 1, Orchid Opera 1, Chalky White 1, Java Cream 1, Golden Jasmine 1, Subtle Ivory 1, Vanilla Mist 1, Cameo Silk 1, Chiffon White 1, Clouded Pearl 1, Flamingo Fun 2, Rock Candy 2, Sweet Sundae 2, Fuchsia Falls 2, Rose Trellis 2, Party Surprise 2, Silken Sunrise 2, Caribbean Dawn 2, Bongo Jazz 2, Mango Melody 2, Coral Canyon 2, Amber Queen 2, Harvest Fruits 2, Moroccan Sands 2, Polar Flame 2, Golden Rambler 2, Californian Sands 2, Pharaohs Gold 2, Golden Fern 2, Buttercup Fool 2, Banana Dream 2, Lemon Chiffon 2, Easter Morn 2, Spring Breeze 2, Lime Zest 2, Kiwi Burst 2, Willow Creek 2, Green Parrot 2, Grecian Garland 2, Granada Green 2, Forest Falls 2, Grecian Spa 2, Crystal Surprise 2, Javan Dawn 2, Summer Medley 2, Azure Sky 2, Holiday Blues 2, Blue Seduction 2, Mystic Mauve 2, Fragrant Cloud 2, Lilac Heather 2, Amethyst Showers 2, Lavender Blossom 2, Lilac Spring 2, Violet Verona 2, Orchid Opera 2, Chalky White 2, Java Cream 2, Golden Jasmine 2, Subtle Ivory 2, Vanilla Mist 2, Cameo Silk 2, Chiffon White 2, Clouded Pearl 2, Flamingo Fun 3, Rock Candy 3, Sweet Sundae 3, Fuchsia Falls 3, Rose Trellis 3, Party Surprise 3, Silken Sunrise 3, Caribbean Dawn 3, Bongo Jazz 3, Mango Melody 3, Coral Canyon 3, Amber Queen 3, Harvest Fruits 3, Moroccan Sands 3, Polar Flame 3, Golden Rambler 3, Californian Sands 3, Pharaohs Gold 3, Golden Fern 3, Buttercup Fool 3, Banana Dream 3, Lemon Chiffon 3, Easter Morn 3, Spring Breeze 3, Lime Zest 3, Kiwi Burst 3, Willow Creek 3, Green Parrot 3, Grecian Garland 3, Granada Green 3, Forest Falls 3, Grecian Spa 3, Crystal Surprise 3, Javan Dawn 3, Summer Medley 3, Azure Sky 3, Holiday Blues 3, Blue Seduction 3, Mystic Mauve 3, Fragrant Cloud 3, Lilac Heather 3, Amethyst Showers 3, Lavender Blossom 3, Lilac Spring 3, Violet Verona 3, Orchid Opera 3, Chalky White 3, Java Cream 3, Golden Jasmine 3, Subtle Ivory 3, Vanilla Mist 3, Cameo Silk 3, Chiffon White 3, Clouded Pearl 3, Flamingo Fun 4, Rock Candy 4, Sweet Sundae 4, Fuchsia Falls 4, Rose Trellis 4, Party Surprise 4, Silken Sunrise 4, Caribbean Dawn 4, Bongo Jazz 4, Mango Melody 4, Coral Canyon 4, Amber Queen 4, Harvest Fruits 4, Moroccan Sands 4, Polar Flame 4, Golden Rambler 4, Californian Sands 4, Pharaohs Gold 4, Golden Fern 4, Buttercup Fool 4, Banana Dream 4, Lemon Chiffon 4, Easter Morn 4, Spring Breeze 4, Lime Zest 4, Kiwi Burst 4, Willow Creek 4, Green Parrot 4, Grecian Garland 4, Granada Green 4, Forest Falls 4, Grecian Spa 4, Crystal Surprise 4, Javan Dawn 4, Summer Medley 4, Azure Sky 4, Holiday Blues 4, Blue Seduction 4, Mystic Mauve 4, Fragrant Cloud 4, Lilac Heather 4, Amethyst Showers 4, Lavender Blossom 4, Lilac Spring 4, Violet Verona 4, Orchid Opera 4, Chalky White 4, Java Cream 4, Golden Jasmine 4, Subtle Ivory 4, Vanilla Mist 4, C
32511ameo Silk 4, Chiffon White 4, Clouded Pearl 4, Flamingo Fun 5, Rock Candy 5, Sweet Sundae 5, Fuchsia Falls 5, Rose Trellis 5, Party Surprise 5, Silken Sunrise 5, Caribbean Dawn 5, Bongo Jazz 5, Mango Melody 5, Coral Canyon 5, Amber Queen 5, Harvest Fruits 5, Moroccan Sands 5, Polar Flame 5, Golden Rambler 5, Californian Sands 5, Pharaohs Gold 5, Golden Fern 5, Buttercup Fool 5, Banana Dream 5, Lemon Chiffon 5, Easter Morn 5, Spring Breeze 5, Lime Zest 5, Kiwi Burst 5, Willow Creek 5, Green Parrot 5, Grecian Garland 5, Granada Green 5, Forest Falls 5, Grecian Spa 5, Crystal Surprise 5, Javan Dawn 5, Summer Medley 5, Azure Sky 5, Holiday Blues 5, Blue Seduction 5, Mystic Mauve 5, Fragrant Cloud 5, Lilac Heather 5, Amethyst Showers 5, Lavender Blossom 5, Lilac Spring 5, Violet Verona 5, Orchid Opera 5, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Flamingo Fun 6, Rock Candy 6, Sweet Sundae 6, Fuchsia Falls 6, Rose Trellis 6, Party Surprise 6, Silken Sunrise 6, Caribbean Dawn 6, Bongo Jazz 6, Mango Melody 6, Coral Canyon 6, Amber Queen 6, Harvest Fruits 6, Moroccan Sands 6, Polar Flame 6, Golden Rambler 6, Californian Sands 6, Pharaohs Gold 6, Golden Fern 6, Buttercup Fool 6, Banana Dream 6, Lemon Chiffon 6, Easter Morn 6, Spring Breeze 6, Lime Zest 6, Kiwi Burst 6, Willow Creek 6, Green Parrot 6, Grecian Garland 6, Granada Green 6, Forest Falls 6, Grecian Spa 6, Crystal Surprise 6, Javan Dawn 6, Summer Medley 6, Azure Sky 6, Holiday Blues 6, Blue Seduction 6, Mystic Mauve 6, Fragrant Cloud 6, Lilac Heather 6, Amethyst Showers 6, Lavender Blossom 6, Lilac Spring 6, Violet Verona 6, Orchid Opera 6, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Emporium Rose 1, Waterlily Blush 1, Blush Rambler 1, Candy Love 1, Adobe Pink 1, Pink Nevada 1, Burnt Autumn 1, Gipsy Bloom 1, Thai Magic 1, Sicilian Summer 1, Jungle Ginger 1, Autumn Fern 1, Sumatran Melody 1, Nectar Jewels 1, Wholemeal Honey 1, Eastern Spice 1, Jamaican Bronze 1, Dutch Gold 1, Honey Drizzle 1, Tuscan Treasure 1, Golden Umber 1, Sundrenched Saffron 1, Desert Island 1, Sun Dust 1, Forest Lake 1, Soft Moss 1, Celtic Moor 1, Moorland Magic 1, Japanese Maze 1, Amazon Jungle 1, Crushed Pine 1, Peppermint Beach 1, Inky Pool 1, Velvet Touch 1, Liberty Blue 1, Luna Landscape 1, Amazon Beat 1, Atlantic Surf 1, Wild Water 1, Morning Jewel 1, Starlit Night 1, Marble Swirl 1, Purple Polka 1, Purple Sage 1, Himalayan Musk 1, Heather Bloom 1, Crushed Cotton 1, Cracked Clay 1, Frayed Hessian 1, Crumpled Linen 1, Earthen Cream 1, Gentle Gold 1, Quilted Calico 1, Twisted Bamboo 1, Emporium Rose 2, Waterlily Blush 2, Blush Rambler 2, Candy Love 2, Adobe Pink 2, Pink Nevada 2, Burnt Autumn 2, Gipsy Bloom 2, Thai Magic 2, Sicilian Summer 2, Jungle Ginger 2, Autumn Fern 2, Sumatran Melody 2, Nectar Jewels 2, Wholemeal Honey 2, Eastern Spice 2, Jamaican Bronze 2, Dutch Gold 2, Honey Drizzle 2, Tuscan Treasure 2, Golden Umber 2, Sundrenched Saffron 2, Desert Island 2, Sun Dust 2, Forest Lake 2, Soft Moss 2, Celtic Moor 2, Moorland Magic 2, Japanese Maze 2, Amazon Jungle 2, Crushed Pine 2, Peppermint Beach 2, Inky Pool 2, Velvet Touch 2, Liberty Blue 2, Luna Landscape 2, Amazon Beat 2, Atlantic Surf 2, Wild Water 2, Morning Jewel 2, Starlit Night 2, Marble Swirl 2, Purple Polka 2, Purple Sage 2, Himalayan Musk 2, Heather Bloom 2, Crushed Cotton 2, Cracked Clay 2, Frayed Hessian 2, Crumpled Linen 2, Earthen Cream 2, Gentle Gold 2, Quilted Calico 2, Twisted Bamboo 2, Emporium Rose 3, Waterlily Blush 3, Blush Rambler 3, Candy Love 3, Adobe Pink 3, Pink Nevada 3, Burnt Autumn 3, Gipsy Bloom 3, Thai Magic 3, Sicilian Summer 3, Jungle Ginger 3, Autumn Fern 3, Sumatran Melody 3, Nectar Jewels 3, Wholemeal Honey 3, Eastern Spice 3, Jamaican Bronze 3, Dutch Gold 3, Honey Drizzle 3, Tuscan Treasure 3, Golden Umber 3, Sundrenched Saffron 3, Desert Island 3, Sun Dust 3, Forest Lake 3, Soft Moss 3, Celtic Moor 3, Moorland Magic 3, Japanese Maze 3, Amazon Jungle 3, Crushed Pine 3, Peppermint Beach 3, Inky Pool 3, Velvet Touch 3, Liberty Blue 3, Luna Landscape 3, Amazon Beat 3, Atlantic Surf 3, Wild Water 3, Morning Jewel 3, Starlit Night 3, Marble Swirl 3, Purple Polka 3, Purple Sage 3, Himalayan Musk 3, Heather Bloom 3, Crushed Cotton 3, Cracked Clay 3, Frayed Hessian 3, Crumpled Linen 3, Earthen Cream 3, Gentle Gold 3, Quilted Calico 3, Twisted Bamboo 3, Emporium Rose 4, Waterlily Blush 4, Blush Rambler 4, Candy Love 4, Adobe Pink 4, Pink Nevada 4, Burnt Autumn 4, Gipsy Bloom 4, Thai Magic 4, Sicilian Summer 4, Jungle Ginger 4, Autumn Fern 4, Sumatran Melody 4, Nectar Jewels 4, Wholemeal Honey 4, Eastern Spice 4, Jamaican Bronze 4, Dutch Gold 4, Honey Drizzle 4, Tuscan Treasure 4, Golden Umber 4, Sundrenched S
32511affron 4, Desert Island 4, Sun Dust 4, Forest Lake 4, Soft Moss 4, Celtic Moor 4, Moorland Magic 4, Japanese Maze 4, Amazon Jungle 4, Crushed Pine 4, Peppermint Beach 4, Inky Pool 4, Velvet Touch 4, Liberty Blue 4, Luna Landscape 4, Amazon Beat 4, Atlantic Surf 4, Wild Water 4, Morning Jewel 4, Starlit Night 4, Marble Swirl 4, Purple Polka 4, Purple Sage 4, Himalayan Musk 4, Heather Bloom 4, Crushed Cotton 4, Cracked Clay 4, Frayed Hessian 4, Crumpled Linen 4, Earthen Cream 4, Gentle Gold 4, Quilted Calico 4, Twisted Bamboo 4, Emporium Rose 5, Waterlily Blush 5, Blush Rambler 5, Candy Love 5, Adobe Pink 5, Pink Nevada 5, Burnt Autumn 5, Gipsy Bloom 5, Thai Magic 5, Sicilian Summer 5, Jungle Ginger 5, Autumn Fern 5, Sumatran Melody 5, Nectar Jewels 5, Wholemeal Honey 5, Eastern Spice 5, Jamaican Bronze 5, Dutch Gold 5, Honey Drizzle 5, Tuscan Treasure 5, Golden Umber 5, Sundrenched Saffron 5, Desert Island 5, Sun Dust 5, Forest Lake 5, Soft Moss 5, Celtic Moor 5, Moorland Magic 5, Japanese Maze 5, Amazon Jungle 5, Crushed Pine 5, Peppermint Beach 5, Inky Pool 5, Velvet Touch 5, Liberty Blue 5, Luna Landscape 5, Amazon Beat 5, Atlantic Surf 5, Wild Water 5, Morning Jewel 5, Starlit Night 5, Marble Swirl 5, Purple Polka 5, Purple Sage 5, Himalayan Musk 5, Heather Bloom 5, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Emporium Rose 6, Waterlily Blush 6, Blush Rambler 6, Candy Love 6, Adobe Pink 6, Pink Nevada 6, Burnt Autumn 6, Gipsy Bloom 6, Thai Magic 6, Sicilian Summer 6, Jungle Ginger 6, Autumn Fern 6, Sumatran Melody 6, Nectar Jewels 6, Wholemeal Honey 6, Eastern Spice 6, Jamaican Bronze 6, Dutch Gold 6, Honey Drizzle 6, Tuscan Treasure 6, Golden Umber 6, Sundrenched Saffron 6, Desert Island 6, Sun Dust 6, Forest Lake 6, Soft Moss 6, Celtic Moor 6, Moorland Magic 6, Japanese Maze 6, Amazon Jungle 6, Crushed Pine 6, Peppermint Beach 6, Inky Pool 6, Velvet Touch 6, Liberty Blue 6, Luna Landscape 6, Amazon Beat 6, Atlantic Surf 6, Wild Water 6, Morning Jewel 6, Starlit Night 6, Marble Swirl 6, Purple Polka 6, Purple Sage 6, Himalayan Musk 6, Heather Bloom 6, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Mercury Shower 1, Soft Montelimar 1, Soft Cinnebar 1, Oyster Cove 1, Blush Noisette 1, Nordic Sails 1, Wild Mushroom 1, Nomadic Glow 1, Nutmeg Cluster 1, Sultana Spice 1, Cappuccino Candy 1, Expresso Delight 1, Roman Stone 1, Soft Almond 1, Treacle Tart 1, Summer Pecan 1, Caramel Blush 1, Soft Maplewood 1, Caramel Sand 1, Labrador Sands 1, Chalky Downs 1, Khaki Mists 1, Bracken Salts 1, Salisbury Stones 1, Gooseberry Fool 1, Celtic Forest 1, English Mist 1, Jungle Fever 1, Woodland Pearl 1, Tuscan Glade 1, Highland Falls 1, Soft Fauna 1, Sea Urchin 1, Jewelled Creek 1, Celestial Cloud 1, Winter Teal 1, Pebble Drift 1, Niagara Blues 1, Steel Symphony 1, Moon Waves 1, Imperial Mauve 1, Midnight Iris 1, Brooklyn Nights 1, Violet Surprise 1, Mellow Heather 1, Twilight Cinders 1, Potters Clay 1, Chalk Blush 1, Natural Taupe 1, Bleached Lichen 1, Dusted Moss 1, Quartz Flint 1, Mineral Haze 1, Grey Steel 1, Mercury Shower 2, Soft Montelimar 2, Soft Cinnebar 2, Oyster Cove 2, Blush Noisette 2, Nordic Sails 2, Wild Mushroom 2, Nomadic Glow 2, Nutmeg Cluster 2, Sultana Spice 2, Cappuccino Candy 2, Expresso Delight 2, Roman Stone 2, Soft Almond 2, Treacle Tart 2, Summer Pecan 2, Caramel Blush 2, Soft Maplewood 2, Caramel Sand 2, Labrador Sands 2, Chalky Downs 2, Khaki Mists 2, Bracken Salts 2, Salisbury Stones 2, Gooseberry Fool 2, Celtic Forest 2, English Mist 2, Jungle Fever 2, Woodland Pearl 2, Tuscan Glade 2, Highland Falls 2, Soft Fauna 2, Sea Urchin 2, Jewelled Creek 2, Celestial Cloud 2, Winter Teal 2, Pebble Drift 2, Niagara Blues 2, Steel Symphony 2, Moon Waves 2, Imperial Mauve 2, Midnight Iris 2, Brooklyn Nights 2, Violet Surprise 2, Mellow Heather 2, Twilight Cinders 2, Potters Clay 2, Chalk Blush 2, Natural Taupe 2, Bleached Lichen 2, Dusted Moss 2, Quartz Flint 2, Mineral Haze 2, Grey Steel 2, Mercury Shower 3, Soft Montelimar 3, Soft Cinnebar 3, Oyster Cove 3, Blush Noisette 3, Nordic Sails 3, Wild Mushroom 3, Nomadic Glow 3, Nutmeg Cluster 3, Sultana Spice 3, Cappuccino Candy 3, Expresso Delight 3, Roman Stone 3, Soft Almond 3, Treacle Tart 3, Summer Pecan 3, Caramel Blush 3, Soft Maplewood 3, Caramel Sand 3, Labrador Sands 3, Chalky Downs 3, Khaki Mists 3, Bracken Salts 3, Salisbury Stones 3, Gooseberry Fool 3, Celtic Forest 3, English Mist 3, Jungle Fever 3, Woodland Pearl 3, Tuscan Glade 3, Highland Falls 3, Soft Fauna 3, Sea Urchin 3, Jewelled Creek 3, Celestial Cloud 3, Winter Teal 3, Pebble Drift 3, Niagara Blues 3, Steel Symphony 3, Moon Waves 3, Imperial Mauve 3, Midnight Iris 3, Brooklyn Nights 3, Violet Surprise 3, Mellow Heather 3, Twilight Cinders 3, Potters Clay 3, Chalk Blush 3, Natural Taupe 3, Bleached Lichen 3, Dusted Moss 3, Quartz Flint 3, Mineral Haze 3, Grey Steel 3, Mercury Shower 4, Soft Montelimar 4, Soft Cinnebar 4, Oyster Cove 4, Blush Noisette 4, Nordic Sails 4, Wild Mushroom 4, Nomadic Glow 4, Nutmeg Cluster 4, Sultana Spice 4, Cappuccino Candy 4, Expresso Delight 4, Roman Stone 4, Soft Almond 4, Treacle Tart 4, Summer Pecan 4, Caramel Blush 4, Soft Maplewood 4, Caramel Sand 4, Labrador Sands 4, Chalky Downs 4, Khaki Mists 4, Bracken Salts 4, Salisbury Stones 4, Gooseberry Fool 4, Celtic Forest 4, English Mist 4, Jungle Fever 4, Woodland Pearl 4, Tuscan Glade 4, Highland Falls 4, Soft Fauna 4, Sea Urchin 4, Jewelled Creek 4, Celestial Cloud 4, Winter Teal 4, Pebble Drift 4, Niagara Blues 4, Steel Symphony 4, Moon Waves 4, Imperial Mauve 4, Midnight Iris 4, Brooklyn Nights 4, Violet Surprise 4, Mellow Heather 4, Twilight Cinders 4, Potters Clay 4, Chalk Blush 4, Natural Taupe 4, Bleached Lichen 4, Dusted Moss 4, Quartz Flint 4, Mineral Haze 4, Grey Steel 4, Mercury Shower 5, Soft Montelimar 5, Soft Cinnebar 5, Oyster Cove 5, Blush Noisette 5, Nordic Sails 5, Wild Mushroom 5, Nomadic Glow 5, Nutmeg Cluster 5, Sultana Spice 5, Cappuccino Candy 5, Expresso Delight 5, Roman Stone 5, Soft Almond 5, Treacle Tart 5, Summer Pecan 5, Caramel Blush 5, Soft Maplewood 5, Caramel Sand 5, Labrador Sands 5, Chalky Downs 5, Khaki Mists 5, Bracken Salts 5, Salisbury Stones 5, Gooseberry Fool 5, Celtic Forest 5, English Mist 5, Jungle Fever 5, Woodland Pearl 5, Tuscan Glade 5, Highland Falls 5, Soft Fauna 5, Sea Urchin 5, Jewelled Creek 5, Celestial Cloud 5, Winter Teal 5, Pebble Drift 5, Niagara Blues 5, Steel Symphony 5, Moon Waves 5, Imperial Mauve 5, Midnight Iris 5, Brooklyn Nights 5, Violet Surprise 5, Mellow Heather 5, Twilight Cinders 5, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Mercury Shower 6, Soft Montelimar 6, Soft Cinnebar 6, Oyster Cove 6, Blush Noisette 6, Nordic Sails 6, Wild Mushroom 6, Nomadic Glow 6, Nutmeg Cluster 6, Sultana Spice 6, Cappuccino Candy 6, Expresso Delight 6, Roman Stone 6, Soft Almond 6, Treacle Tart 6, Summer Pecan 6, Caramel Blush 6, Soft Maplewood 6, Caramel Sand 6, Labrador Sands 6, Chalky Downs 6, Khaki Mists 6, Bracken Salts 6, Salisbury Stones 6, Gooseberry Fool 6, Celtic Forest 6, English Mist 6, Jungle Fever 6, Woodland Pearl 6, Tuscan Glade 6, Highland Falls 6, Soft Fauna 6, Sea Urchin 6, Jewelled Creek 6, Celestial Cloud 6, Winter Teal 6, Pebble Drift 6, Niagara Blues 6, Steel Symphony 6, Moon Waves 6, Imperial Mauve 6, Midnight Iris 6, Brooklyn Nights 6, Violet Surprise 6, Mellow Heather 6, Twilight Cinders 6, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White, Pure Brilliant White',
32512	
32513	// number of columns
32514	cols: 54,
32515	
32516	// number of rows
32517	rows: 24,
32518	
32519	// grid cell size including spacing
32520	size: 17,
32521	
32522	// cells spacing
32523	spacing: 1,
32524	
32525	// small image (scroller)
32526	sImage: {
32527		width: 260,
32528		height: 116,
32529		url: '/assets/austin/dulux-small.png'
32530	},
32531	
32532	// big image size (palette)
32533	bImage: {
32534		width: 916,
32535		height: 406,
32536		url: '/assets/austin/dulux-big.png'
32537	},
32538	
32539	init: function() {
32540		this.colors = this.colors.split(',');
32541		this.names = this.names.split(',');
32542		return this;
32543	},
32544	
32545	getName: function(color) {
32546		// at this moment colors is massive
32547		var index = this.getIndex(color);
32548		
32549		if (myDeco.settings.DULUX_BRANDING && index !== -1) {
32550			color = this.names[index];
32551            if (myDeco.settings.WL && myDeco.settings.WL.WL_PARTNER_NAME.toLowerCase() == 'dulux') {
32552                color = color +  '™';
32553            }
32554		} else {
32555			color = '#' + color;
32556		}
32557		return color;
32558	},
32559	
32560	getUrl: function(color) {
32561		var idx = this.getIndex(color);
32562		if (idx !== -1) {
32563			return 'http://ad-emea.doubleclick.net/clk;242773234;65674453;n?' +
32564				'http://www.dulux.co.uk/servlet/ShoppingBasketHandler?' +
32565				'ItemType=sku&ns_campaign=content&ns_mchannel=content&' +
32566				'ns_source=mydeco&ns_linkname=basket&action=add&ItemID=' +
32567				$.trim(this.names[idx]).toLowerCase().replace(/\s/g, '_');
32568		}
32569		return false;
32570	}
32571};
32572/*globals Class, $ */
32573/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32574          sloppy: true, white: true */
32575/**
32576 * @class SearchModelFacet
32577 * @classDescription Abstract class for plain facets like supplier or color
32578 */
32579var SearchModelFacet = Class.extend({
32580	/**
32581	 * List of values
32582	 * @type {Object}
32583	 * @private
32584	 */
32585	_values: null,
32586	
32587	/**
32588	 * Initial fill
32589	 * @type {Boolean}
32590	 * @private
32591	 */
32592	_initial: false,
32593	
32594	/**
32595	 * Default list of values, filled with append if initial is set to true
32596	 * Useful for facets with default values like colour facet
32597	 * @type {Object}
32598	 * @private
32599	 */
32600	_default: {},
32601	
32602	/**
32603	 * List of selected values, will be glued with facet=...& on refine
32604	 * @type {Array}
32605	 * @public
32606	 */
32607	selected: null,
32608	
32609	/**
32610	 * List of refiners which should updated with the current one
32611	 * @type {Array}
32612	 * @private
32613	 */
32614	_dependant: [],
32615	
32616	/**
32617	 * Will update self from returned values on self refine
32618	 * @type {Boolean}
32619	 * @private
32620	 */
32621	_updateOnRefine: true,
32622	
32623	/**
32624	 * Name of parameter for request
32625	 * @type {String}
32626	 * @public
32627	 */
32628	paramName: 'facet',
32629	
32630	/**
32631	 * Result sorting values
32632	 * @type {Array}
32633	 * @public
32634	 */
32635	sort: [],
32636	
32637	/**
32638	 * Skip request on facet value
32639	 * @type {Array}
32640	 * @private
32641	 */
32642	_skip: [],
32643	
32644	/**
32645	 * Init function
32646	 * @param {Object} values Default values list
32647	 * @public
32648	 */
32649	init: function(values) {
32650		this.selected = {};
32651		this._values = values || {};
32652	},
32653	
32654	/**
32655	 * Function for parsing values list fetched from WS
32656	 * @param {Array} values List of facets
32657	 * @param {Object} appendTo Object to set facet values to, defaults to this._values
32658	 * @return this
32659	 * @public
32660	 */
32661	append: function(values, appendTo) {
32662		appendTo = appendTo || this._values;
32663		
32664		var self = this,
32665			p;
32666		
32667		// clear values on given level
32668		for (p in appendTo) {
32669			delete appendTo[p];
32670		}
32671		
32672		// process with structure like [name, count, [facet]]
32673		// where facet is "facetname,id"
32674		// TODO: check for correct count value
32675		$.each(values, function(idx) {
32676			var facet = this[2][0],
32677				id = facet.split(',').pop();
32678			appendTo[id] = {
32679				id: id,
32680				value: facet,
32681				count: this[1],
32682				name: this[0],
32683				values: {},
32684                order_val: idx
32685			};
32686			
32687			// fill defaults if needed
32688			if (self._initial) {
32689				self._default[id] = $.extend({}, appendTo[id]);
32690			}
32691			
32692		});
32693		
32694		this._initial = false;
32695		
32696		return this;
32697	},
32698
32699	/**
32700	 * Refine current facet with the new value
32701	 * Clear current refiners if no value given
32702	 * @param {String} value Refiner value
32703	 * @return this
32704	 * @public
32705	 */
32706	refine: function(values) {
32707		
32708		if (values) {
32709			var id = values.shift();
32710			
32711			// toggle value
32712			if (typeof this.selected[id] !== 'undefined') {
32713				delete this.selected[id];
32714			} else {
32715				this.selected[id] = values.join(':');
32716			}
32717		} else {
32718			this.reset();
32719		}
32720		
32721		return this;
32722	},
32723
32724	/**
32725	 * Reset refiner to its original state
32726	 * @public
32727	 */
32728	reset: function() {
32729		this._values = $.extend(true, {}, this._default);
32730		this.selected = {};
32731	},
32732	
32733	/**
32734	 * Return selected facets as list of refiners
32735	 * @public
32736	 */
32737	getSelected: function() {
32738		var selected = {};
32739		selected[this.paramName] = $.values(this.selected);
32740		if (selected[this.paramName].length > 0 && this.sort.length > 0) {
32741			selected.sort = this.sort;
32742		}
32743		return selected;
32744	},
32745	
32746	/**
32747	 * Determine is current facet have some selected values
32748	 * @public
32749	 */
32750	isSelected: function() {
32751		return $.keys(this.selected).length !== 0;
32752	},
32753	
32754	/**
32755	 * Check is facet need to request for given value
32756	 * @param {Any} value Value to check
32757	 * @public
32758	 */
32759	skip: function(value) {
32760		if (typeof value === 'undefined') {
32761			value = $.keys(this.selected || {}).shift();
32762		}
32763		return value && $.inArray(value, this._skip) !== -1;
32764	}
32765});
32766
32767/**
32768 * List of facets by its name, should extend {SearchModelFacet}
32769 */
32770var SearchModelFacets = {};
32771/*globals SearchModelFacets, SearchModelFacet, $ */
32772/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32773          sloppy: true, white: true */
32774/**
32775 * Category refiner facet class
32776 */
32777SearchModelFacets.category = SearchModelFacet.extend({
32778	/**
32779	 * Facet name
32780	 * @type {String}
32781	 * @public
32782	 * @static
32783	 */
32784	$name: 'category',
32785	
32786	/**
32787	 * Current selected path
32788	 * @type {Array}
32789	 * @public
32790	 */
32791	path: [],
32792	
32793	/**
32794	 * Facets which depend from this
32795	 * @type {Array}
32796	 * @private
32797	 */
32798	_dependant: ['image_colour'],
32799
32800	/**
32801	 * Depth limit
32802	 * @type {Number}
32803	 * @public
32804	 */
32805	limit: 4,
32806	
32807	/**
32808	 * Append new values from WS
32809	 * @param {Array} values Array of given values from WS
32810	 * @public
32811	 */
32812	append: function(values) {
32813		if (this.path.length < this.limit) {
32814			var appendTo = this._values,
32815				pathId;
32816			
32817			// find last element in tree relative to the last element in path
32818			$.each(this.path, function() {
32819				appendTo = appendTo[this.id].values;
32820			});
32821			
32822			// protect of circular references
32823			pathId = $.map(this.path, function(item) { return item.id; });
32824			
32825			values = $.grep(values, function(item) {
32826				return $.inArray(item[2][0].split(',').pop(), pathId) < 0;
32827			});
32828			
32829			this._super(values, appendTo);
32830		}
32831		return this;
32832	},
32833	
32834	/**
32835	 * Refine search by category, only one category selected
32836	 * @param {String} value Refiner value
32837	 * @public
32838	 */
32839	refine: function(values) {
32840		// reset facet
32841		if (!values || values[0] === false) {
32842			this.reset();
32843		}
32844		
32845		if (values && values[0] === false) {
32846			values.shift();
32847		}
32848		
32849		if (values) {
32850			var depth = values.pop(),
32851				
32852				// name of element in path
32853				id = values[0],
32854				v;
32855			
32856			// fill selected values
32857			this.selected = {};
32858			
32859			// check for refining with previous selection
32860			if (depth < this.path.length) {
32861				// store path to appendTo
32862				// cut path
32863				this.path.splice(depth + 1, this.path.length - depth);
32864				
32865				// set selected to previous to this one
32866				this.selected[id] = this.path[depth].facet;
32867				
32868			} else {
32869				v = values.pop();
32870				
32871				this.path.push({id: id, facet: v});
32872				this.selected[id] = v;
32873			}
32874		}
32875		
32876		return this;
32877	},
32878	
32879	/**
32880	 * Reset refiner to its original state
32881	 * @public
32882	 */
32883	reset: function() {
32884		this.path = [];
32885		this.selected = {};
32886	}
32887});
32888/*globals SearchModelFacets, SearchModelFacet */
32889/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32890          sloppy: true, white: true */
32891/**
32892 * Image colour facet
32893 */
32894SearchModelFacets.image_colour = SearchModelFacet.extend({
32895	/**
32896	 * Facet name
32897	 * @type {String}
32898	 * @public
32899	 * @static
32900	 */
32901	$name: 'image_colour',
32902	
32903	/**
32904	 * Do not update self on self refine
32905	 * @type {Boolean}
32906	 * @private 
32907	 */
32908	_updateOnRefine: false,
32909	
32910	/**
32911	 * Init, use initial fill
32912	 * @param {Object} values
32913	 * @public
32914	 */
32915	init: function(values) {
32916		this._initial = true;
32917		this._super(values);
32918	},
32919
32920    /* A HACK TO HAVE JUST ONE COLOR (OR NONE) SELECTED */
32921    refine: function(values) {
32922        this.selected = {};
32923        return this._super(values);
32924    }
32925});
32926/*globals SearchModelFacets, SearchModelFacet, $ */
32927/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
32928          sloppy: true, white: true */
32929/**
32930 * Item dimensions facet, simulated
32931 */
32932SearchModelFacets.dimension = SearchModelFacet.extend({
32933	/**
32934	 * Facet name
32935	 * @type {String}
32936	 * @public
32937	 * @static
32938	 */
32939	$name: 'dimension',
32940	
32941	/**
32942	 * Shorthand to facet name converting matrix
32943	 * @type {Object}
32944	 * @private
32945	 */
32946	_types: {
32947		w: '3dwidth',
32948		h: '3dheight',
32949		d: '3ddepth'
32950	},
32951	
32952	/**
32953	 * Set param name for 'ranges'
32954	 * @type {String}
32955	 * @public
32956	 */
32957	paramName: 'ranges',
32958	
32959	/**
32960	 * Sort by area size desc
32961	 * @type {Array}
32962	 */
32963	/* sort: ['-3darea'], */
32964	
32965	/**
32966	 * Refine facet method
32967	 * @type {String} value Value refine with
32968	 * @public
32969	 */
32970	refine: function(values) {
32971		this.selected = {};
32972		
32973		if (!values) {
32974			return this;
32975		}
32976		
32977		var self = this,
32978			name;
32979		
32980		for (name in values) {
32981			if (values[name]) {
32982				self.selected[name] = self._types[name] + ',0,' + values[name];
32983			}
32984		}
32985		
32986		return this;
32987	},
32988	
32989	/**
32990	 * Return all currently selected values
32991	 */
32992	getSelected: function() {
32993		var selected = this._super();
32994		// Hide all on-wall products from search by dimension
32995		if (selected[this.paramName].length > 0) {
32996			selected.filter = ['wall_mounted,0'];
32997		}
32998		return selected;
32999	}
33000});
33001
33002/*globals Class, $ */
33003/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
33004          sloppy: true, white: true */
33005/**
33006 * @class ItemsListModel
33007 * @classDescription Abstract class used as model in misc compnent list based models
33008 */
33009var ItemsListModel = Class.extend({
33010	/**
33011	 * Model type
33012	 * @type {String}
33013	 * @public
33014	 * @static
33015	 */
33016	$type: 'items-list',
33017	
33018	/**
33019	 * Fetched items list
33020	 * @type {Array}
33021	 * @public
33022	 */
33023	items: [],
33024	
33025	/**
33026	 * Index of fetched items
33027	 * @type {Object}
33028	 * @public
33029	 */
33030	itemsIndex: {
33031		'scrapbook': {},
33032		'component': {},
33033		'material': {},
33034		'product': {}
33035	},
33036	
33037	/**
33038	 * Callbacks list which will be run after initial loading is done
33039	 * @type {Array}
33040	 * @private
33041	 */
33042	_onInit: [],
33043
33044	/**
33045	 * Loaded flag
33046	 * @type {Boolean}
33047	 * @private
33048	 */
33049	loaded: false,
33050
33051	/**
33052	 * Estimate number of search result
33053	 * @type {Number}
33054	 * @public
33055	 */
33056	found: 0,
33057	
33058	/**
33059	 * Items type, component, material etc
33060	 * @type {String}
33061	 */
33062	type: 'component',
33063	
33064	/**
33065	 * Main init function, no parameters yet
33066	 * @public
33067	 */
33068	init: function() {
33069		var self = this;
33070		
33071		// set loaded flag
33072		this.loaded = true;
33073		
33074		// run loading callbacks
33075		$.each(this._onInit, function() {
33076			this.call(null, self);
33077		});
33078
33079	},	
33080	/**
33081	 * Append loading callback which will run on init
33082	 * @type {Function} callback
33083	 * @public
33084	 */
33085	onInit: function(callback) {
33086		if (!this.loaded) {
33087			this._onInit.push(callback);
33088		} else {
33089			callback(this);
33090		}
33091	},
33092	
33093	/**
33094	 * Reset all data
33095	 */
33096	reset: function() {
33097		var key;
33098		// update items list
33099		for (key in this.itemsIndex) {
33100			this.itemsIndex[key] = {};
33101		}
33102		this.items = [];
33103		
33104		// possible items count
33105		this.found = 0;
33106	},
33107	
33108	/**
33109	 * Fetch additional items from WS, stub
33110	 * @param {Number} offset Items result starting from
33111	 * @param {Function} callback Callback run after successful request
33112	 */
33113	fetchMore: function() {
33114	},
33115	
33116	/**
33117	 * Fetch additional component info from WS with syncronous request by list element mousedown event
33118	 * @param {String} id Item ID
33119	 * @param {Function} callback Function called when componen information arrived
33120	 * @public
33121	 */
33122	requestComponentInfo: function(id, callback) {
33123		if ($.isFunction(callback)) {
33124			callback(this.itemsIndex[this.type][id]);
33125		}
33126		return this.itemsIndex[this.type][id];
33127	}
33128});
33129/*globals ItemsListModel, $, SearchModelFacets, myDeco */
33130/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
33131          sloppy: true, white: true */
33132/**
33133 * @class SearchModel
33134 * @classDescription Class used as model in MVC search representation
33135 */
33136var SearchModel = ItemsListModel.extend({
33137	/**
33138	 * Model type
33139	 * @type {String}
33140	 * @public
33141	 * @static
33142	 */
33143	$type: 'search',
33144	
33145	/**
33146	 * Items type
33147	 * @type {String}
33148	 */
33149	type: 'component',
33150	
33151	/**
33152	 * Facets available
33153	 * @type {Object}
33154	 * @public
33155	 */
33156	facets: {},
33157	
33158	/**
33159	 * List of used filters
33160	 * @type {Object}
33161	 * @public
33162	 */
33163	filters: {},
33164	
33165	/**
33166	 * Query value
33167	 * @type {String}
33168	 * @public
33169	 */
33170	query: '',
33171
33172	/**
33173	 * Facets used in search
33174	 * @type {Array}
33175	 * @private
33176	 */
33177	_facetsUsed: ['category', 'manual_colour', 'dimension'],
33178	
33179	/**
33180	 * Flag for request status
33181	 * @type {Boolean}
33182	 * @private
33183	 */
33184	_requestActive: false,
33185	
33186	/**
33187	 * Default facet used for refine if no facet specified
33188	 * @type {String}
33189	 * @public
33190	 */
33191	defaultFacet: '',
33192	
33193	/**
33194	 * Default WS request options
33195	 * @private
33196	 */
33197	_default: {
33198		// database to request
33199		db: 'component',
33200		
33201		// items count
33202		count: 30,
33203		
33204		// list of facets
33205		requirefacet: ['category', 'manual_colour'],
33206		
33207		api_version: 2,
33208
33209		locale: 'en-gb' /*myDeco.settings.LOCALE*/
33210	},
33211	
33212	/**
33213	 * number of items fetched for the initial request
33214	 * @type {Number}
33215	 */
33216	_initialCount: 0,
33217	
33218	/**
33219	 * Additional options passed to backed
33220	 * @type {Object}
33221	 */
33222	additionalOptions: {},
33223	
33224	/**
33225	 * Options excluded from general and additional options
33226	 * @type {Object}
33227	 */
33228	excludedOptions: {},
33229
33230	/**
33231	 * Main init function, no parameters yet
33232	 * @param {Object} options Misc init options
33233	 * @param {Object} additionalOptions which can be passed at init state
33234	 * @public
33235	 */
33236	init: function(options, additionalOptions) {
33237		options = options || {};
33238		
33239		this.defaultFacet = options.defaultFacet || this._facetsUsed[0];
33240		
33241		var self = this,
33242			i, ilen, facetName, ClassName;
33243		
33244		// extend options
33245		$.each(additionalOptions || {}, function(key, value) {
33246			if ($.isArray(value)) {
33247				self.additionalOptions[key] = $.merge(
33248					self.additionalOptions[key] || [],
33249					value
33250				);
33251			} else {
33252				self.additionalOptions[key] = $.extend(
33253					true,
33254					self.additionalOptions[key] || {},
33255					value
33256				);
33257			}
33258		});
33259		
33260		// exclude options
33261		$.each(this.excludedOptions, function(key, value) {
33262			if (self.additionalOptions[key]) {
33263				if ($.isArray(value)) {
33264					$.each(value, function() {
33265						var param = this;
33266						self.additionalOptions[key] = $.grep(self.additionalOptions[key],
33267							function(param1) {
33268								return param1.indexOf(param) !== 0;
33269							});
33270					});
33271				}
33272			}
33273		});
33274		
33275		// init facets list
33276		for (i = 0, ilen = this._facetsUsed.length; i < ilen; i++) {
33277			facetName = this._facetsUsed[i];
33278			ClassName = SearchModelFacets[facetName];
33279			
33280			// use class if it available
33281			if (ClassName) {
33282				this.facets[facetName] = new ClassName();
33283			}
33284		}
33285
33286        var init_facets = $.proxy(function() {
33287		    // initial fill of default facets
33288		    this._request({count: this._initialCount}, function() {
33289			    // we can't execute parent constructior in the delayed mode
33290			    // so copy init code from parent
33291			    self.loaded = true;
33292			    
33293			    // run loading callbacks
33294			    $.each(self._onInit, function() {
33295				    this.call(null, self);
33296			    });
33297		    });
33298        }, this);
33299
33300        if (this.$type == 'search') {
33301            myDeco.new_search.get_used_categories( function(keys) {
33302                var result = [];
33303                var copy_used = function(source, dest) {
33304                    source.forEach( function(category) {
33305                        if (keys.indexOf(category.id) > -1) {
33306                            clone = {name: category.name, id: category.id, children: []};
33307                            dest.push(clone);
33308                            copy_used(category.children, clone.children);
33309                        } } )
33310                }
33311                copy_used(myDeco.settings.ITEM_CATEGORIES, result);
33312                myDeco.settings.ITEM_CATEGORIES = result;
33313                init_facets(); })
33314        } else {
33315            init_facets();
33316        }
33317	},
33318	
33319	/**
33320	 * Check for request availability
33321	 * @return {Boolean}
33322	 * @public
33323	 */
33324	searchAllowed: function() {
33325		var name;
33326		
33327		if (this._requestActive) {
33328			return false;
33329		}
33330		
33331		for (name in this.facets) {
33332			if (this.facets[name].skip()) {
33333				return false;
33334			}
33335		}
33336		
33337		return true;
33338	},
33339	
33340	/**
33341	 * Refine search with given facet
33342	 * @type {String} facetName Facet name to refine with
33343	 * @type {String} facetValue Passed value to facet refined
33344	 * @type {Function} callback Optional callback for running after refine is done (refine view for example)
33345	 * @public
33346	 */
33347	refine: function(facetName, facetValue, callback) {
33348		var name = facetName || this.defaultFacet,
33349			options = {},
33350			facet = this.facets[name],
33351		
33352			// list of facets which will refine after loading
33353			refineFacets = this.facets[name]._dependant;
33354
33355		if (facet._updateOnRefine) {
33356			refineFacets.push(name);
33357		}
33358		
33359		if (facetName) {
33360			facet.refine(facetValue);
33361		}
33362
33363		this.reset();
33364		this._request(options, callback, refineFacets);
33365	},
33366	
33367	/**
33368	 * Fetch additional items from WS
33369	 * @param {Number} offset Items result starting from
33370	 * @param {Function} callback Callback run after successful request
33371	 */
33372	fetchMore: function(offset, callback) {
33373		this._request({startrank: offset}, callback);
33374	},
33375	
33376	/**
33377	 * Append/remove filter to query
33378	 * @param {String|Object} name Name of filter or object with filters
33379	 * @param {String|Function} value Filter value or Callback in case of first
33380	 *                                first value is object. Will remove from
33381	 *                                list if omitted or undefined
33382	 * @param {Function} callback Callback run after successful request
33383	 */
33384	filter: function(name, value, callback) {
33385		if (typeof name === 'object') {
33386			this.filters = name;
33387			callback = value;
33388		} else {
33389			if (typeof value === 'undefined' && this.filters[name]) {
33390				delete this.filters[name];
33391			} else {
33392				this.filters[name] = value;
33393			}
33394		}
33395		
33396		this.reset();
33397		this._request({}, callback);
33398	},
33399	
33400	/**
33401	 * Request search result from backend
33402	 * @param {Object} options Options to request, will merge with default options and facets
33403	 * @param {Function} callback Optional callback run after request
33404	 * @private
33405	 */
33406	_request: function(options, callback, refineFacets) {
33407		var self = this;
33408		
33409		if (!this.searchAllowed()) {
33410			return callback({});
33411		}
33412
33413		refineFacets = refineFacets || this._facetsUsed;
33414		this._requestActive = true;
33415		
33416		myDeco.body.trigger('model-request-sent', [this.$type]);
33417		
33418		myDeco.App.ws.proxy.search(this._constructOptions(options), function(data, dictionary, list) {
33419
33420            var facet_data = data.results[self._default.db].facets || [];
33421
33422            // override categories for components
33423            if (self.$type == 'search') {
33424                var parent_cat = self.facets.category.getSelected().facet[0];
33425                var cats = myDeco.settings.ITEM_CATEGORIES;
33426
33427                var recursive_find = function(items, id) {
33428                    var found = null;
33429                    var idx;
33430                    for(idx = 0; idx < items.length; idx++) {
33431                        var item = items[idx];
33432                        if (item.id == id) {
33433                            found = item;
33434                            break;
33435                        } else {
33436                            found = recursive_find(item.children, id);
33437                            if (found) break;
33438                        } 
33439                    }
33440                    return found;
33441                };
33442
33443                if (parent_cat) {
33444                    var parent_id = parseInt(parent_cat.split(',').pop());
33445                    var cat = recursive_find(cats, parent_id);
33446                    cats = cat ? cat.children : [];
33447                }
33448
33449                var formatted_cats = $.map(cats, function(cat) {
33450                    return [[cat.name, 1, ["new," + cat.id.toString()]]];
33451                });
33452                facet_data = [["category", formatted_cats]];
33453            }
33454
33455            // override categories for materials
33456            if (self.$type == 'materials-search') {
33457                var parent_cat = self.facets["class"].getSelected().facet[0];
33458                var material_classes = (myDeco.settings.WL && myDeco.settings.WL.MATERIAL_CLASSES && JSON.parse(myDeco.settings.WL.MATERIAL_CLASSES)) ||
33459                    ["carpet", "paint", "stone", "tile", "wallpaper", "wood", "patio"];
33460
33461                var cats = parent_cat ? [] : $.map(material_classes, function(c, i) {
33462                    return [[c, i + 1, ["class," + c]]]; // for some reason, $.map flattens resulting array, so I have to nest it twice
33463                });
33464
33465                facet_data = [["class", cats]];
33466            }
33467
33468			// refine facets
33469			$.each(facet_data, function() {
33470				var facetName = this.shift();
33471
33472				if ($.inArray(facetName, refineFacets) >= 0) {
33473					self.facets[facetName].append(this.shift());
33474				}
33475			});
33476			
33477			// set possible number of items
33478			self.found = data.results[self._default.db].estmatches || 0;
33479			
33480			$.extend(true, self.itemsIndex[self.type], dictionary);
33481			
33482			$.each(list, function(i, id) {
33483				self.items.push(self.itemsIndex[self.type][id]);
33484			});
33485
33486            $.each(list, function(i, id) {
33487                var item = self.itemsIndex[self.type][id];
33488                // HACK for John Lewis. They wanted to show only their own wallpapers, but the search engine
33489                // doesn't support this filtering. So the filtering happens here...
33490                // NOTE it only filters the data that is coming in. If the data doesn't have John Lewis wallpapers
33491                // the sidebar will be empty...
33492/*                if(self.facets.class && self.facets.class.selected.wallpaper &&
33493                        myDeco.settings.WL && myDeco.settings.WL.WL_MYDECO_COM_REPLACEMENT === 'John Lewis') {
33494                    if(item.supplier === 'John Lewis') {
33495                        self.items.push(item);
33496                    }
33497                } else {
33498                    self.items.push(item);
33499                }
33500*/            });
33501
33502			self._requestActive = false;
33503			
33504			if ($.isFunction(callback)) {
33505				callback(self);
33506			}
33507		});
33508	},
33509
33510	/**
33511	 * Construct request parameters for search request
33512	 * @param {Object} options Options URL constructed from
33513	 * @private
33514	 * @returns {Object}
33515	 */
33516	_constructOptions: function(options) {
33517		var facetName, name, facet, selected, filters, i;
33518		
33519		// do not request any items if no facet selected and query is empty
33520		if (!this.query && typeof options.count === 'undefined' && $.keys(this.filters).length === 0 &&
33521				!$.any($.values(this.facets), function(facet) { return facet.isSelected(); })) {
33522			options.count = this._initialCount;
33523		}
33524		
33525		// extend with default options
33526		options = $.extend(true, {}, this._default, options);
33527		
33528		// list of selected facets
33529		for (facetName in this.facets) {
33530			
33531			facet = this.facets[facetName];
33532			selected = facet.getSelected();
33533			
33534			for (name in selected) {
33535				options[name] = $.merge(options[name] || [], selected[name]);
33536			}
33537		}
33538		
33539		// filters
33540		filters = $.extend({}, this.filters, options.filters || {});
33541		delete options[filters];
33542		
33543		if ($.keys(filters).length > 0) {
33544			options.filter = [];
33545			for (name in filters) {
33546				options.filter.push([name + ',' + filters[name]]);
33547			}
33548		}
33549		
33550		// store and add query only if it needed
33551		if (typeof options.query !== 'undefined') {
33552			this.query = options.query;
33553		}
33554		
33555		if (this.query) {
33556			options.query = this.query;
33557		} else {
33558			delete options.query;
33559		}
33560		
33561		if (options.sort && $.isArray(options.sort)) {
33562			options.sort = options.sort.join(',');
33563		}
33564		
33565		// integrate additional options
33566		for (name in this.additionalOptions) {
33567			options[name] = $.merge(options[name] || [], this.additionalOptions[name]);
33568		}
33569		
33570		// process ranges
33571		if ($.isArray(options.ranges)) {
33572			for (i = options.ranges.length - 1; i >= 0; i--) {
33573				if (options.ranges[i].indexOf('completed_date') === 0) {
33574					facet = options.ranges[i].split(',');
33575					// means that ranges provides only number of days from now
33576					if (facet.length === 2) {
33577						// now timestamp
33578						facet[2] = Math.round((new Date()).valueOf() / 1000);
33579						// now - N days timestamp
33580						facet[1] = facet[2] - (+facet[1]) * 24 * 60 * 60;
33581						options.ranges[i] = facet.join(',');
33582						// NOTE: remove this if you want to process another
33583						// ranges later
33584						break;
33585					}
33586				}
33587			}
33588		}
33589		return options;
33590	}
33591	
33592});
33593/*globals myDeco, AppController, $ */
33594/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
33595          sloppy: true, white: true */
33596/**
33597 * @class ItemsListController
33598 * @classDescription Abstract class for misc item list based controllers
33599 */
33600var ItemsListController = AppController.extend({
33601	/**
33602	 * Controller type
33603	 * @type {String}
33604	 * @public
33605	 * @static
33606	 */
33607	$type: 'items-list',
33608	
33609	/**
33610	 * Current position of fetchNext items
33611	 * @type {Number}
33612	 * @public
33613	 */
33614	position: -1,
33615	
33616	/**
33617	 * Flag indicated is drag operation executed
33618	 * @type {Boolean}
33619	 */
33620	hasOperation: false,
33621	
33622	/**
33623	 * Array of types of undraggable elements
33624	 * @type {Array}
33625	 * @public
33626	 * @static
33627	 */
33628	undraggableTypes: [],
33629	
33630	/**
33631	 * Register view for a given controller
33632	 * @param {FpeView} view
33633	 * @public
33634	 */
33635	registerView: function(view) {
33636		var self = this;
33637		
33638		this._super(view);
33639		
33640		this._view._canvas.mousedown(this.renderComponent.bind(this));
33641		
33642		// bind controllers to component drag start
33643		myDeco.body
33644			.bind('controller-fetch-next', this.fetchNext.bind(this))
33645			.bind('controller-fetch-prev', this.fetchPrev.bind(this))
33646			.bind('controller-component-info', this.fetchComponent.bind(this))
33647			.bind('roomCleared', function() {
33648				self._view._canvas.find('li').removeClass(self.settings.inRoomClass);
33649			})
33650			.bind('mouseup', this.operationReset.bind(this));
33651		// hide/display component info
33652		this._view._canvas.delegate('li', 'hover',
33653			$.proxy(this._view.displayComponentInfo, this._view));
33654		
33655		this.bindRelatedEvents();
33656
33657	},
33658	
33659	operationReset: function(e) {
33660		if (this.hasOperation) {
33661			this._super(e);
33662			this.hasOperation = false;
33663		}
33664	},
33665	
33666	/**
33667	 * Listener for fetch next event
33668	 * @param {Event} e
33669	 * @param {String} type
33670	 * @param {Function} callback
33671	 * @param {Boolean} first Force to reset position to a first item
33672	 */
33673	fetchNext: function(e, type, callback, first) {
33674		if (type !== this.$type) {
33675			return;
33676		}
33677		
33678		var self = this,
33679			ownCallback = function() {
33680				if (first) {
33681					self.position = -1;
33682				}
33683				if (self.position < self._model.found) {
33684					self.position = self.position + 1;
33685				}
33686				if ($.isFunction(callback)) {
33687					callback(self._model.items[self.position] ? self.fetchComponent(false, self.$type, self._model.items[self.position].component_id) : undefined,
33688							self.position, self._model.found);
33689				}
33690			};
33691		
33692		if (this.position < this._model.items.length - 1) {
33693			ownCallback();
33694		} else {
33695			this.fetchMore(ownCallback);
33696		}
33697	},
33698	
33699	/**
33700	 * Listener for fetch prev event
33701	 * @param {Event} e
33702	 * @param {String} type
33703	 * @param {Function} callback
33704	 */
33705	fetchPrev: function(e, type, callback) {
33706		if (type !== this.$type) {
33707			return;
33708		}
33709		
33710		if (this.position > 0) {
33711			this.position = this.position - 1;
33712		}
33713		if ($.isFunction(callback)) {
33714			callback(this.fetchComponent(false, this.$type, this._model.items[this.position].component_id), this.position, this._model.found);
33715		}
33716	},
33717	
33718	/**
33719	 * Listener for fetchComponent event
33720	 * Outline requested item if it exist
33721	 * @param {Event} e
33722	 * @param {String} type
33723	 * @param {Number} id
33724	 * @param {Function} callback
33725	 */
33726	fetchComponent: function(e, type, id, callback) {
33727		if (type !== this.$type || (!id && e.target.tagName !== 'LI')) {
33728			return false;
33729		}
33730		
33731		this.hasOperation = true;
33732		id = id || $(e.target).closest('li').data('id');
33733		this._view.selectComponent(id);
33734		
33735		return this._model.requestComponentInfo(id, callback);
33736	},
33737
33738	/**
33739	 * Event listener to initiate drag-n-drop from components panel
33740	 * @param {Event} e Mousedown event object
33741	 * @param {Number} id
33742	 * @public
33743	 */
33744	renderComponent: function(e, id) {
33745		var self = this, options,
33746			type = $(e.target).closest('.scroller-holder').data('type');
33747
33748		if (e.isDefaultPrevented() || e.target.tagName !== 'LI') {
33749			return;
33750		}
33751		e.stop();
33752
33753		// remove decor special items
33754		if ($(e.target).hasClass('no-decor')) {
33755			myDeco.App.model.removeDecor(type);
33756		} else {
33757		//all other items
33758			this.fetchComponent(e, this.$type, id, function (component) {
33759
33760				// remove component_id for paints
33761				if (component['class'] === 'color') {
33762					delete component.component_id;
33763				}
33764				
33765				// store component in cache
33766				if (myDeco.App.ws.proxy.has(self._model.type) &&
33767						self._model.itemsIndex[self._model.type][component.component_id]) {
33768					myDeco.App.ws.proxy.set(self._model.type, component.component_id,
33769						self._model.itemsIndex[self._model.type][component.component_id]);
33770				}
33771				
33772				options = {
33773					kind: 'furniture',
33774					entity: component,
33775					visual: self._view.generateElementForDrag(component)
33776				};
33777				
33778				switch (self._model.type) {
33779					case 'material':
33780					case 'scrapbook':
33781						options.kind = 'decor';
33782						options.ox = options.oy = -10;
33783						options.type = component['class'];
33784						break;
33785				}
33786				
33787				if ($.inArray(type, self.undraggableTypes) == -1) {
33788					myDeco.body.trigger('entity-drag-started', [options, e]);
33789				} else {
33790					myDeco.App.model.addImage(options.entity, 0, 0, type);
33791				}
33792				
33793			});
33794		}
33795	},
33796	
33797	/**
33798	 * Fetch one more set from backend
33799	 * @public
33800	 */
33801	fetchMore: function(callback) {
33802		var len = this._model.items.length,
33803			self = this;
33804		
33805		if (this._model.found - len > 0) {
33806			this._view.scrollerLoading(true);
33807			this._model.fetchMore(len, function() {
33808				self._view.appendItems(self._model.items.slice(len));
33809				self._view.scrollerLoading(false);
33810				if ($.isFunction(callback)) {
33811					callback();
33812				}
33813			});
33814		} else if ($.isFunction(callback)) {
33815			callback();
33816		}
33817	},
33818	
33819	/**
33820	 * Run event listeners related to the model
33821	 */
33822	bindRelatedEvents: function() {
33823		var self = this;
33824		
33825		myDeco.body.bind(this._model.type + 'Added ' + this._model.type +
33826				'Removed', function(e, component) {
33827			self._view._canvas.find('.' + self._model.type + '-' +
33828					(component.component_id || '').replace(/#/, '_'))
33829				.toggleClass(self._view.settings.inRoomClass,
33830					myDeco.App.model['is' + self._model.type.capitalize() +
33831					'InModel'].call(myDeco.App.model, component));
33832		});
33833		
33834	}
33835	
33836});
33837/*globals ItemsListController, $, myDeco */
33838/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
33839          sloppy: true, white: true */
33840/**
33841 * Search controller base class
33842 * 
33843 * Listen on body:
33844 * controller-search-facet-refine
33845 *    {String} facet name
33846 *    {Any} refine data
33847 * 
33848 * Triggers on body:
33849 * controller-search-facet-toggle:
33850 *    {String} facet name
33851 *    {Boolean} state
33852 * controller-search-view-registered:
33853 *    {jQset} canvas,
33854 *    {Function} component info fetch function
33855 */
33856var SearchController = ItemsListController.extend({
33857	/**
33858	 * Controller type
33859	 * @type {String}
33860	 * @public
33861	 * @static
33862	 */
33863	$type: 'search',
33864	
33865	/**
33866	 * Main search form
33867	 * @type {jQset}
33868	 */
33869	_form: false,
33870	
33871	/**
33872	 * Query field
33873	 * @type {jQset}
33874	 */
33875	_query: false,
33876	
33877	/**
33878	 * Register view for a given controller
33879	 * @param {FpeView} view
33880	 * @public
33881	 */
33882	registerView: function(view) {
33883		this._super(view);
33884		
33885		var self = this;
33886		this._form = this._view.canvasElement.find('form.search');
33887		this._query = this._form.find('input[name=query]');
33888
33889		// listen search bar for submit
33890		this._form.submit(function(e) {
33891			e.stop();
33892			if (self._view.facets.category) {
33893				self._view.facets.category.toggle(false);
33894				self.search(self._query.val(), 'category');
33895			}
33896		});
33897		
33898		this._view._queryHolder.find('a').click(function(e) {
33899			e.stop();
33900			self._query.val('');
33901			self._form.submit();
33902		});
33903		
33904		// bind controllers to component drag start
33905		myDeco.body
33906			.bind('controller-facet-toggle', function(e, type, facetName, state) {
33907				if ((type === false || self.$type === type) && self._view.facets[facetName]) {
33908					self._view.facets[facetName].toggle(state);
33909				}
33910			})
33911			.bind('controller-facet-refine', function(e, type, facetName, data, callback) {
33912				if (type === false || type === self.$type) {
33913					self.search(false, facetName, data, callback);
33914				}
33915			});
33916	},
33917	
33918	/**
33919	 * Register refiner from view
33920	 * @param {jQset} element Element on which listener will applied
33921	 * @param {Any} any Parameter additionally passed to the listener
33922	 * ...
33923	 * @public
33924	 */
33925	registerRefiner: function(element) {
33926		var parameters = Array.prototype.slice.call(arguments, [1]);
33927		
33928		parameters.unshift(this);
33929		
33930		if (element.get(0).tagName === 'A') {
33931			element.click(Function.prototype.bindAsEventListener.apply(this.refineByClick, parameters));
33932		} else if (element.get(0).tagName === 'FORM') {
33933			element.submit(Function.prototype.bindAsEventListener.apply(this.refineBySubmit, parameters));
33934		}
33935		return element;
33936	},
33937	
33938	/**
33939	 * Register model filter
33940	 * @param {jQset} elements
33941	 * @public
33942	 */
33943	registerFilter: function(elements) {
33944		var self = this;
33945		
33946		elements.change(function(e) {
33947			var element = $(e.target),
33948				name = element.attr('name'),
33949				value = element.val();
33950			
33951			if (e.target.tagName === 'SELECT') {
33952				element = element.children().eq(e.target.selectedIndex);
33953				name = element.attr('name') || name;
33954			}
33955			
33956			value = element.data('json') || value;
33957
33958			if ((e.target.tagName === 'INPUT' && !element.is(':checked')) ||
33959				(e.target.tagName === 'SELECT' && value === '')) {
33960				value = undefined;
33961			}
33962			
33963			self._view.loading(true);
33964			if (typeof value === 'object') {
33965				self._model.filter(value, self._view.redraw.bind(self._view));
33966			} else {
33967				self._model.filter(name, value, self._view.redraw.bind(self._view));
33968			}
33969		});
33970	},
33971	
33972	/**
33973	 * Register refiner invoker to show/hide main refiner area
33974	 * @param {jQset} element
33975	 * @param {String} facetName
33976	 * @public
33977	 */
33978	registerRefinerInvoker: function(element, facetName) {
33979		var self = this;
33980		
33981		if (!element.data('registered')){
33982			element.data('registered', 'true').click(function(e) {
33983				e.stop();
33984				myDeco.body.trigger('controller-facet-toggle',
33985						[self.$type, facetName, self._view.facets[facetName].toggle()]);
33986			});
33987		}
33988
33989		if(myDeco.settings.WL && myDeco.settings.WL.WL_DEFAULT_COMPONENT_CATEGORY) {
33990			if(this['$type'] === 'search') {
33991				this.search(false, "category", [myDeco.settings.WL.WL_DEFAULT_COMPONENT_CATEGORY, 
33992					"cat1," + myDeco.settings.WL.WL_DEFAULT_COMPONENT_CATEGORY, 0])
33993			}
33994		}
33995	},
33996
33997	/**
33998	 * Register resetter from view
33999	 * @param {jQset} element Link on which reset will bind
34000	 * @public
34001	 */
34002	registerResetter: function(element) {
34003		var self = this;
34004		
34005		element.click(function(e) {
34006			e.stop();
34007			
34008			var facetName = $(e.target).data('filter');
34009			
34010			self._view.facets[facetName].toggle(false);
34011			self.search(false, facetName);
34012		});
34013		
34014		return element;
34015	},
34016	
34017	/**
34018	 * Click-based refiner, get value from data storage
34019	 * @param {Event} e
34020	 * @public
34021	 */
34022	refineByClick: function(e) {
34023		e.stop();
34024		
34025		var target = $(e.target).closest('.filter-ref').data('filter'),
34026			facetName = target.shift();
34027
34028		this.search(false, facetName, target);
34029		
34030		this._view.facets[facetName].toggle(false);
34031	},
34032	
34033	/**
34034	 * Submit-based refiner, get value as form serialize
34035	 * @param {Event} e
34036	 * @param {String} facetName Facet name, should passed from registerRefiner as additional parameter
34037	 * @public
34038	 */
34039	refineBySubmit: function(e, facetName) {
34040		e.stop();
34041		
34042		if (!facetName) {
34043			throw "No facetName given";
34044		}
34045		
34046		this.search(false, facetName, $(e.target).serialize().toQueryParams());
34047	},
34048	
34049	/**
34050	 * Search method, pass updated values to model
34051	 * @param {String} query Query therm. pass false if you do not want to change search query
34052	 * @param {String} facetName
34053	 * @param {String} facetValue
34054	 * @public
34055	 */
34056	search: function(query, facetName, facetValue, callback) {
34057		var self = this;
34058
34059		if (query !== false) {
34060			this._model.query = query;
34061		}
34062		
34063		if (typeof facetName !== 'undefined') {
34064			myDeco.body.trigger('controller-facet-' + (facetValue ? 'refined' : 'cleared'),
34065					[this.$type, facetName, facetValue]);
34066		}
34067		
34068		this._view.loading(true);
34069		
34070		this.position = -1;
34071		this._model.refine(facetName, facetValue, function(data) {
34072			self._view.redraw(data);
34073			myDeco.body.trigger('controller-data-fetched', [self.$type]);
34074			if ($.isFunction(callback)) {
34075				callback(data);
34076			}
34077		});
34078		
34079		this._view.redrawFacets(facetName);
34080	}
34081});
34082
34083myDeco.App.controllers.push(SearchController);
34084/*globals Class, $ */
34085/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34086          sloppy: true, white: true */
34087/**
34088 * @class SearchViewFacet
34089 * @classDescription Abstract class for facet view classes
34090 */
34091var SearchViewFacet = Class.extend({
34092	/**
34093	 * Parent view
34094	 * @type {FpeView}
34095	 * @private
34096	 */
34097	_view: false,
34098	
34099	/**
34100	 * Facet model
34101	 * @type {SearchModelFacet}
34102	 * @private
34103	 */
34104	_model: false,
34105	
34106	/**
34107	 * HTML element for operating with
34108	 * @type {jQSet}
34109	 * @private
34110	 */
34111	_canvas: false,
34112	
34113	/**
34114	 * HTML element on which facet will be shown
34115	 * @type {jQset}
34116	 * @private
34117	 */
34118	_invoker: false,
34119	
34120	/**
34121	 * Name of facet
34122	 * @type {String}
34123	 * @public
34124	 * @static
34125	 */
34126	$name: 'facet',
34127	
34128	/**
34129	 * Facet constructor
34130	 * @param {FpeView} view
34131	 * @param {jQset} canvas HTML canvas for facet, defaults to #facet-$name
34132	 * @public
34133	 */
34134	init: function(view, canvas, invoker) {
34135		this._view = view;
34136		
34137		// fetch relevant model from model facets list
34138		this._model = this._view._model.facets[this.$name];
34139		
34140		// set up canvas
34141		this._canvas = canvas || this._view.canvasElement.find('.facet-' + this.$name);
34142		
34143		// set up invoker
34144		this._invoker = invoker || this._view.canvasElement.find('.facet.' + this.$name);
34145		// prevent double register
34146		if (this._invoker && this._invoker.size() > 0 && !this._invoker.data('registered')) {
34147			this._view._controller.registerRefinerInvoker(this._invoker, this.$name);
34148		}
34149	},
34150	
34151	/**
34152	 * Cleanup canvas
34153	 * @public
34154	 */
34155	reset: function() {
34156		this._canvas.text('');
34157	},
34158	
34159	/**
34160	 * Draw facet from scratch
34161	 * @public
34162	 */
34163	draw: function() {
34164		this.reset();
34165		this.redraw();
34166	},
34167	
34168	/**
34169	 * Redraw facet
34170	 * @public
34171	 */
34172	redraw: function() {
34173	},
34174	
34175	toggle: function(state) {
34176		state = typeof state !== 'undefined' ? state : !this._canvas.is(':visible');
34177		this._canvas.toggle(state);
34178		this._invoker.toggleClass('active', state);
34179		
34180		return state;
34181	},
34182	
34183	/**
34184	 * Make string representation for given arguments
34185	 * @param {Object} item Model item 
34186	 * @param {String} Additional link parameters
34187	 * ...
34188	 */
34189	_makeValueList: function(item) {
34190		return $.merge([this.$name, item.id, item.value], Array.prototype.slice.call(arguments, 1));
34191	},
34192	
34193	/**
34194	 * Make link for facet refiner
34195	 * @param {Object} item Model item
34196	 * @param {String} Additional link parameters
34197	 * @private
34198	 */
34199	_makeListItem: function(item) {
34200		var filter = this._makeValueList.apply(this, arguments);
34201
34202		return $('<li></li>')
34203			.toggleClass('selected', typeof this._model.selected[item.id] !== 'undefined')
34204			.append(this._view._controller.registerRefiner($('<a href="" class="filter-ref" id="' + item.value.replace(',', '-') + '"/>').data('filter', filter).html(
34205					'<span class="prev">&nbsp;</span>' + gettext(item.name.capitalize()) +
34206					// temporarily disabled 
34207//					'<span> (' + item.count + ')</span>' +
34208					(typeof this._model.selected[item.id] !== 'undefined' ? '<span class="expand">&nbsp;</span>' : '') + '</a>')));
34209	},
34210	
34211	_makeResetLink: function(text) {
34212		text = text || (myDeco.settings.WL && myDeco.settings.WL.WL_SEARCH_RESET_LINK ? 
34213			myDeco.settings.WL.WL_SEARCH_RESET_LINK : gettext('Back to any type'));
34214		
34215		return this._view._controller.registerResetter($('<a href="" class="back filter-ref" />').data('filter', this.$name).html(text));
34216	}
34217});
34218
34219/**
34220 * List of facet views used in search
34221 */
34222var SearchViewFacets = {};
34223/*globals SearchViewFacets, SearchViewFacet, $ */
34224/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34225          sloppy: true, white: true */
34226/**
34227 * Category facet refiner
34228 */
34229SearchViewFacets.category = SearchViewFacet.extend({
34230	/**
34231	 * Refiner name
34232	 * @type {String}
34233	 * @public
34234	 * @static
34235	 */
34236	$name: 'category',
34237	
34238	/**
34239	 * Redraw refiner
34240	 * @public
34241	 */
34242	redraw: function() {
34243		this.reset();
34244		
34245		if (this._model.path.length > 0) {
34246			this._canvas.append(this._makeResetLink());
34247		} else {
34248			this._canvas.append('<h1>' + 
34249				(myDeco.settings.WL && myDeco.settings.WL.WL_SEARCH_CATEGORY_HEADER ? 
34250					myDeco.settings.WL.WL_SEARCH_CATEGORY_HEADER : gettext('Browse category'))
34251				+ '</h1>');
34252		}
34253	
34254		var list = $('<ul></ul>').toggleClass('selected', this._model.path.length === 0), item, p, plen = this._model.path.length,
34255			listSource = this._model._values,
34256			root = list, ilen,
34257			ul, ids;
34258		
34259		for (p = 0; p < plen; p++) {
34260			item = listSource[this._model.path[p].id];
34261			ul = $('<ul></ul>').toggleClass('selected', typeof this._model.selected[item.id] !== 'undefined');
34262			root.append(this._makeListItem(item, p).append(ul));
34263			listSource = item.values;
34264			root = ul;
34265		}
34266		
34267        var values = $.values(listSource);
34268        plen = values.length;
34269        values.sort(function(a,b) { return a.order_val - b.order_val; });
34270        values.forEach(function(v) {
34271			root.append(this._makeListItem(v, plen));
34272		}, this);
34273		
34274		this._canvas.append(list);
34275	},
34276	
34277	/**
34278	 * Dummy function to prevent of facet hiding
34279	 * @public
34280	 */
34281	toggle: function() {
34282	}
34283	
34284});
34285/*globals SearchViewFacets, SearchViewFacet, $, myDeco */
34286/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34287          sloppy: true, white: true */
34288/**
34289 * Image colour refiner
34290 */
34291SearchViewFacets.image_colour = SearchViewFacet.extend({
34292	/**
34293	 * Refiner name
34294	 * @type {String}
34295	 * @public
34296	 * @static
34297	 */
34298	$name: 'image_colour',
34299	
34300	init: function(view, canvas, invoker) {
34301		this._super(view, canvas, invoker);
34302		
34303		this._content = this._canvas.find('.content');
34304		
34305		this._canvas.find('.clear').replaceWith(this._makeResetLink(gettext('Clear all')).addClass('clear'));
34306	},
34307	
34308	reset: function() {
34309		this._content.text('');
34310	},
34311	
34312	/**
34313	 * Redraw refiner
34314	 * @public
34315	 */
34316	redraw: function() {
34317		this.reset();
34318		
34319		var p = 0, p16, key, selected, hsl, rgb, hex_color,
34320			canvas = $('<ul />').appendTo(this._canvas.find('.content'));
34321
34322        // plus one row is grayscale
34323        var rows = myDeco.settings.PALETTE_ROWS;
34324        var cols = myDeco.settings.PALETTE_COLS;
34325
34326        for (p = 0; p < (rows + 1) * cols; p++) {
34327			p16 = p.toString(16);
34328            hsl = pal2hsl(p16, rows, cols);
34329            rgb = hsl2rgb(hsl[0], hsl[1], hsl[2]);
34330            hex_color = diffuse2Hex(rgb);
34331			canvas.append(this._makeListItem({id: p16, name: p16, value: "manual_colour," + p16}).
34332                          css('backgroundColor', '#' + hex_color));
34333        }
34334
34335		if (this._invoker && this._invoker.size() > 0) {
34336			selected = this._invoker.find('.facet-invoker .selected').text('')
34337				.toggleClass('no-colour', $.keys(this._model.selected).length === 0);
34338			for (key in this._model.selected) {
34339                hsl = pal2hsl(key, rows, cols);
34340                rgb = hsl2rgb(hsl[0], hsl[1], hsl[2]);
34341                hex_color = diffuse2Hex(rgb);
34342				selected.append($('<span>&nbsp;</span>').css('backgroundColor', '#' + hex_color));
34343			}
34344		}
34345		
34346	}
34347});
34348/*globals SearchViewFacets, SearchViewFacet, $, toUnits */
34349/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34350          sloppy: true, white: true */
34351SearchViewFacets.dimension = SearchViewFacet.extend({
34352	/**
34353	 * Refiner name
34354	 * @type {String}
34355	 * @public
34356	 * @static
34357	 */
34358	$name: 'dimension',
34359	
34360	/**
34361	 * Init dimension facet view
34362	 * @param {AppView} view
34363	 * @param {jQset} canvas
34364	 * @param {jQset} invoker
34365	 * @public
34366	 */
34367	init: function(view, canvas, invoker) {
34368		this._super(view, canvas, invoker);
34369		
34370		this._content = this._invoker.find('.facet-invoker .selected');
34371		
34372		this._canvas.find('.clear').replaceWith(this._makeResetLink(gettext('Clear all')).addClass('clear'));
34373	},
34374	
34375	/**
34376	 * Draw refiner
34377	 * @public
34378	 */
34379	draw: function() {
34380	},
34381	
34382	/**
34383	 * Reset facet view
34384	 * @public
34385	 */
34386	reset: function() {
34387		this._content.text('');
34388	},
34389	
34390	/**
34391	 * Redraw refiner
34392	 * @public
34393	 */
34394	redraw: function() {
34395		this._content.text('');
34396		
34397		if ($.keys(this._model.selected).length === 0) {
34398			return;
34399		}
34400		
34401		// display size
34402		this._content.html([this._model.selected.w.split(',').pop(), this._model.selected.d.split(',').pop()].toUnits());
34403		
34404		this.showClear(true);
34405	},
34406	
34407	/**
34408	 * Show clear link if data present
34409	 * @param {Boolean} state
34410	 */
34411	showClear: function(state) {
34412		this._canvas.toggle($.keys(this._view._model.facets[this.$name].selected).length > 0 ? state : false);
34413	},
34414	
34415	/**
34416	 * Toggle facet view
34417	 * @param {Boolean} state
34418	 * @public
34419	 */
34420	toggle: function(state) {
34421		state = typeof state !== 'undefined' ? state : !this._invoker.hasClass('active');
34422		
34423		// will show popup only if some selected values is present
34424		this.showClear(state);
34425		this._invoker.toggleClass('active', state);
34426		
34427		return state;
34428	}
34429});
34430/*globals AppView, $, generateElementForDragMixin, buildThumbnailLink, material2Style, tmpl, myDeco, window */
34431/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34432          sloppy: true, white: true */
34433/**
34434 * @class ItemsListView
34435 * @classDescription Abstract class for items list based views
34436 */
34437var ItemsListView = AppView.extend($.extend(true, {}, generateElementForDragMixin, {
34438	/**
34439	 * Class type
34440	 * @type {String}
34441	 * @public
34442	 * @static
34443	 */
34444	$type: 'items-list',
34445	
34446	/**
34447	 * Items holder
34448	 * @type {jQset}
34449	 * @private
34450	 */
34451	_canvas: null,
34452	
34453	/**
34454	 * Items counter
34455	 * @type {jQset}
34456	 * @private
34457	 */
34458	_counter: null,
34459	
34460	/**
34461	 * Empty results loopback
34462	 * @type {jQset}
34463	 * @private
34464	 */
34465	_empty: null,
34466	
34467	/**
34468	 * Current items count displayed on pad
34469	 * @type {Number}
34470	 * @public
34471	 */
34472	itemsCount: 0,
34473	
34474	/**
34475	 * Loading WoD for scroller
34476	 * @type {jQset}
34477	 * @private
34478	 */
34479	_scrollerLoading: null,
34480	
34481	/**
34482	 * Loading layer
34483	 * @type {jQset}
34484	 */
34485	_loading: null,
34486	
34487	/**
34488	 * Flag used to decide should we draw in-room corner or not
34489	 * @type {Boolean}
34490	 */
34491	_setRoomMarker: true,
34492
34493	/**
34494	 * View settings
34495	 * @type {Object}
34496	 * @public
34497	 */
34498	settings: {
34499		// thumbnail sizes for modes
34500		thumbnailSize: {
34501			small: {
34502				width: 110,
34503				height: 110
34504			},
34505			normal: {
34506				width: 110,
34507				height: 110
34508			}
34509		},
34510		selectedClass: 'selected',
34511		inRoomClass: 'in-room'
34512	},
34513	
34514	selected: false,
34515	
34516	/**
34517	 * List mode, normal of small
34518	 * @type {String}
34519	 * @public
34520	 */
34521	mode: 'normal',
34522	
34523	/**
34524	 * View constructor
34525	 * @param {Object} options
34526	 * @public
34527	 */
34528	init: function(options) {
34529		this._super(options);
34530		this._canvas = this.canvasElement.find('.items');
34531		
34532		var self = this;
34533		
34534		this._counter = this.canvasElement.find('.counter');
34535		this._loading = this.canvasElement.find('.wait');
34536		this._empty = this.canvasElement.find('.empty');
34537		
34538		this._scrollerLoading = self.canvasElement.find('.scroller-notify .loading');
34539		this._scrollerHolder = self.canvasElement.find('.scroller-holder')
34540			.scrollLoader({
34541				showControls: function(controls) {
34542					if (self._model.items.length !== 0) {
34543						controls.fadeIn();
34544					}
34545				}
34546			})
34547			.bind('load', function() {
34548				self._controller.fetchMore();
34549			});
34550		
34551		if (this._model.onInit) {
34552			this._model.onInit(this.draw.bind(this));
34553		}
34554	},
34555
34556	/**
34557	 * Redraw view, used for items drawing from scratch
34558	 * @public
34559	 */
34560	redraw: function() {
34561		this.drawItems();
34562		this.loading(false);
34563	},
34564	
34565	/**
34566	 * Draw view, used for partial items drawing
34567	 * @public
34568	 */
34569	draw: function() {
34570		this.drawItems();
34571		this.loading(false);
34572	},
34573	
34574	/**
34575	 * Draw items
34576	 * @public
34577	 */
34578	drawItems: function() {
34579		// direct type casting because toggleClass wants exactly Boolean type 
34580		this._canvas.text('').toggleClass('small', this.mode === 'small').scrollTop(0);
34581		this.itemsCount = this._model.items.length;
34582		this._empty.toggle(this._model.found === 0 && ($.isFunction(this._model.searchAllowed) ?
34583			this._model.searchAllowed() : true));
34584		this._counter.toggle(this._model.items.length !== 0).filter('.scrolling-item').hide();
34585		this._scrollerHolder.trigger('rewind');
34586		
34587		if (this.itemsCount !== 0) {
34588			this.appendItems(this._model.items);
34589		}
34590	},
34591	
34592	/**
34593	 * Append new set of items to already exist
34594	 * @param {Array} items Items to add
34595	 * @public
34596	 */
34597	appendItems: function(items) {
34598		var self = this,
34599			type = this._model.type,
34600			sizes = this.settings.thumbnailSize[this.mode],
34601			existent = [], element, holder;
34602		
34603		if ((myDeco.App.model.uniqueComponentIds && myDeco.App.model.uniqueMaterialIds) || myDeco.App.model.uniqueProductIds) {
34604			switch (type) {
34605				case 'component':
34606					existent = myDeco.App.model.uniqueComponentIds();
34607					break;
34608					
34609				case 'material':
34610					existent = myDeco.App.model.uniqueMaterialIds();
34611					break;
34612					
34613				case 'product':
34614					existent = myDeco.App.model.uniqueProductIds();
34615					break;
34616			}
34617		}
34618		
34619		this._counter.find('.items-counter').html('1 &ndash; ' + this._model.items.length + ' of ' + this._model.found);
34620		
34621		$.each(items, function() {
34622			var item = this;
34623			element = $('<li><b></b></li>')
34624				.attr('class', type + '-' + item.component_id.replace(/#/, '_'))
34625				.data('type', type)
34626				.data('id', item.component_id);
34627			switch (type) {
34628				case 'component':
34629					if (item.componentData) {
34630						if (item.componentData.labels.length !== 0 ||
34631								item.componentData.locale !==
34632									myDeco.settings.BASE_LOCALE) {
34633							holder = $('<div />');
34634							$.map(item.componentData.labels, function(label) {
34635								holder.append($('<label />').text(label)).append('<i />');
34636							});
34637							element.append(holder);
34638						}
34639					}
34640
34641					element.css('background-image', ['url(', (item.thumbnail || item.render_path)].join(''));
34642					break;
34643				case 'scrapbook':
34644					element.css('background-image', ['url(', (item.thumbnail || buildThumbnailLink(item.render_path, sizes)), ')'].join(''));
34645					break;
34646				case 'product':
34647					element.css('background-image', ['url(', item.thumbnail, ')'].join(''));
34648					break;
34649				case 'material':
34650					element.css(material2Style(item));
34651					break;
34652			}
34653			
34654			if (self._setRoomMarker && existent.indexOf(item.component_id) !== -1) {
34655				element.addClass(self.settings.inRoomClass);
34656			}
34657				
34658			self._canvas.append(element);
34659		});
34660	},
34661	
34662	/**
34663	 * Show/hide wait screen
34664	 * @param {Boolean} state hide/show flag, default show
34665	 * @public
34666	 */
34667	loading: function(state) {
34668		this._loading.toggle(typeof state !== 'undefined' ? state : true);
34669	},
34670	
34671	/**
34672	 * Show/hide bottom panel scroller WoD
34673	 * @param {Boolean} state
34674	 */
34675	scrollerLoading: function(state) {
34676		this._scrollerLoading.css('display', state ? 'inline-block' : 'none');
34677	},
34678	
34679	/**
34680	 * Display component info notify for search result
34681	 * @param {Event} e Mouseover/out event
34682	 */
34683	displayComponentInfo: function(e) {
34684		var self = this,
34685			target = $(e.target),
34686			component_id = target.data('id'),
34687			
34688			// get component from storage
34689			component = this._model.requestComponentInfo(component_id),
34690			
34691			// hide popup function
34692			hidePopup = function() {
34693				window.clearTimeout(ItemsListView.openTimeout);	
34694				ItemsListView.popup.animate({opacity: 0}, 0, function() {
34695					ItemsListView.content.html('');
34696					ItemsListView.popup.hide();
34697				});
34698			},
34699			
34700			// show popup function
34701			showPopup = function(data) {
34702				// shorthands
34703				var popup = ItemsListView.popup,
34704					content = ItemsListView.content,
34705					offset = target.offset(),
34706					
34707					// popup position
34708					top = offset.top + target.outerHeight() + 14,
34709					
34710					left = self._canvas.offset().left,
34711					height;
34712				
34713				// fill data to container
34714				content.html('').append(data);
34715				
34716				ItemsListView.arrow
34717					.removeClass('bottom')
34718					.toggleClass('right', offset.left - left > 50);
34719				
34720				// position popup
34721				popup
34722					.css({
34723						top: top,
34724						left: left,
34725						opacity: 0.01
34726					})
34727					.show();
34728				
34729				height = popup.outerHeight();
34730				if (top + height >
34731						self._scrollerHolder.offset().top +
34732						self._scrollerHolder.height()) {
34733					popup.css('top', offset.top - height - 14);
34734					ItemsListView.arrow.addClass('bottom');
34735				}
34736				
34737				// show it finally
34738				popup.animate({opacity: 1}, 100, function() {
34739					popup.css('opacity', 'auto');
34740				});
34741			},
34742			
34743			// data compose function
34744			messageCompose = function(wishlisted) {
34745				var data = component.componentData || component,
34746					content = $(tmpl('component-info-popup-template', {
34747						component: component,
34748						data: data,
34749						wishlisted: wishlisted
34750					})),
34751					links,
34752					label;
34753					
34754				content.find('.gts').data('gts', {
34755					guid: component.product_id || component.component_id,
34756					category: data.category,
34757					price: data.price
34758				});
34759				
34760				if (myDeco.username && data.available && typeof wishlisted !== 'undefined') {
34761					// wishlisting
34762					links = content.find('.wishlist');
34763					wishlisted = links.filter('.wishlisted');
34764					
34765					links.click(function(e) {
34766						e.stop();
34767						
34768						if (target.get(0) === wishlisted.get(0)) {
34769							return;
34770						}
34771						
34772						myDeco.body.trigger('component.wishlist', [component.component_id, function() {
34773							links.hide();
34774							wishlisted.show();
34775						}, wishlisted, $('#sidebar .shopping')]);
34776					});
34777				}
34778
34779				// show popup with a timeout
34780			//	ItemsListView.openTimeout = window.setTimeout(function() { 
34781					showPopup(content);
34782			//	}, 500);
34783			};
34784			
34785		// dont want to operate if any operation is in progress
34786		if (this._controller.hasOperation) {
34787			return;
34788		}
34789
34790		// create popup if not created already
34791		if (!ItemsListView.popup) {
34792			ItemsListView.popup = $('<div class="popup-info" style="display:block; opacity:0"><div class="ar" /><div class="close" /><div class="popup-content" /></div>').appendTo(myDeco.body);
34793			ItemsListView.content = ItemsListView.popup.find('.popup-content');
34794			ItemsListView.arrow = ItemsListView.popup.find('.ar');
34795			
34796			// left coordinate right where items start
34797			
34798			
34799			// click observer
34800			myDeco.body.mousedown(function(e) {
34801				if (e.target.tagName === 'A') {
34802					return;
34803				}
34804				hidePopup();
34805			});
34806			
34807			// mouseover observers
34808			ItemsListView.popup.hover(function() {
34809				window.clearTimeout(ItemsListView.closeTimeout);
34810			}, function() {
34811				ItemsListView.closeTimeout = window.setTimeout(hidePopup, 0);
34812			});
34813		}
34814		
34815		if (e.type === 'mouseenter') {
34816			window.clearTimeout(ItemsListView.closeTimeout);
34817			window.clearTimeout(ItemsListView.showTimeout);
34818			ItemsListView.showTimeout = window.setTimeout(function() {
34819				if (self._model.type !== 'scrapbook' && component_id) {
34820					// if component is furniture check for wishlisting
34821					if (component.componentData && component.componentData.available &&
34822							!myDeco.settings.WL) {
34823						myDeco.body.trigger('component.wishlisted', [component_id, messageCompose]);
34824					} else {
34825						messageCompose();
34826					}
34827				}
34828			}, /*100*/10);
34829		} else {
34830			window.clearTimeout(ItemsListView.showTimeout);
34831			ItemsListView.closeTimeout = window.setTimeout(hidePopup, 100);
34832		}
34833	},
34834	
34835	/**
34836	 * Add selected class to a clicked item
34837	 * @param {String} id Component ID
34838	 */
34839	selectComponent: function(id) {
34840		if (this.selected) {
34841			this.selected.removeClass(this.settings.selectedClass);
34842		}
34843		
34844		this.selected = this.canvasElement.find('.' + this._model.type + '-' + id.replace(/#/, '_')).addClass(this.settings.selectedClass);
34845	}
34846}));
34847/*globals ItemsListView, SearchViewFacets, $, myDeco */
34848/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34849          sloppy: true, white: true */
34850/**
34851 * @class SearchView
34852 * @classDescription Base class for search view
34853 */
34854var SearchView = ItemsListView.extend({
34855	/**
34856	 * Class type
34857	 * @type {String}
34858	 * @public
34859	 * @static
34860	 */
34861	$type: 'search',
34862	
34863	/**
34864	 * List of used refiners in search
34865	 * @type {Object}
34866	 * @public
34867	 */
34868	facets: {},
34869	
34870	_queryHolder: false,
34871	
34872	/**
34873	 * View constructor
34874	 * @param {Object} options
34875	 * @public
34876	 */
34877	init: function(options) {
34878		this._super(options);
34879		
34880		var self = this;
34881		
34882		this._queryHolder = this.canvasElement.find('.query-data');
34883		
34884		if (this._model.onInit) {
34885			// init view refiners on model loading
34886			this._model.onInit(function() {
34887				var name;
34888				for (name in self._model.facets) {
34889					self.facets[name] = new SearchViewFacets[name](self);
34890				}
34891			});
34892			
34893			this._model.onInit(this.draw.bind(this));
34894		}
34895		
34896		if (this._controller) {
34897			this._controller.registerFilter(this.canvasElement.find('input.filter, select.filter'));
34898			
34899		}
34900	},
34901
34902	/**
34903	 * Redraw view
34904	 * @public
34905	 */
34906	redraw: function() {
34907		this._scrollerHolder.trigger('rewind');
34908		this.redrawFacets();
34909		this._super();
34910		this._queryHolder
34911			.toggle(!!this._model.query && this._model.searchAllowed())
34912			.find('span').text(this._model.query || '');
34913	},
34914	
34915	/**
34916	 * Function for invoke redraw facets only
34917	 * @param {String} facetName Facet name to redraw; will redraw all registered facets if omitted
34918	 * @public
34919	 */
34920	redrawFacets: function(facetName) {
34921		if (facetName) {
34922			this.facets[facetName].redraw();
34923		} else {
34924			$.each(this.facets, function() {
34925				this.redraw();
34926			});
34927		}
34928	},
34929	
34930	/**
34931	 * Draw view
34932	 * @public
34933	 */
34934	draw: function() {
34935		$.each(this.facets, function() {
34936			this.draw();
34937		});
34938
34939		this._super();
34940	}
34941});
34942
34943myDeco.App.views.push(SearchView);
34944/*globals AppView, $, generateElementForDragMixin, myDeco, updateMaterialInfo */
34945/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
34946          sloppy: true, white: true */
34947/**
34948 * @fileOverview
34949 * Color picker view
34950 */
34951var ColorPickerView = AppView.extend($.extend(true, {}, generateElementForDragMixin, {
34952	
34953	/**
34954	 * View type
34955	 * @type {String}
34956	 * @public
34957	 * @static
34958	 */
34959	$type: 'color-picker',
34960	
34961	/**
34962	 * Reference to palette object
34963	 * @type {Object}
34964	 */
34965	_palette: {},
34966	
34967	/**
34968	 * Scroller element
34969	 * @type {jQset}
34970	 * @private
34971	 */
34972	_scroller: null,
34973	
34974	/**
34975	 * Scroller holder
34976	 * @type {jQset}
34977	 * @private
34978	 */
34979	_scrollerHolder: null,
34980	
34981	/**
34982	 * Scroller width, calculated from visible/full ratio of palette
34983	 * @type {Number}
34984	 * @private
34985	 */
34986	_scrollerWidth: 0,
34987	
34988	/**
34989	 * Palette container
34990	 * @type {jQset}
34991	 * @private
34992	 */
34993	_container: null,
34994	
34995	/**
34996	 * Palette container width
34997	 * @type {Number}
34998	 * @private
34999	 */
35000	_containerWidth: 0,
35001	
35002	/**
35003	 * Palette block
35004	 * @type {jQset}
35005	 * @private
35006	 */
35007	_paletteHolder: null,
35008	
35009	/**
35010	 * Palette block width, used for correct scrolling
35011	 * @type {Number}
35012	 * @private
35013	 */
35014	_paletteWidth: 0,
35015	
35016	/**
35017	 * Picked color selector
35018	 * @type {jQset}
35019	 * @private
35020	 */
35021	_selector: null,
35022	
35023	/**
35024	 * Internal variable for drag support
35025	 * @type {Number}
35026	 * @private 
35027	 */
35028	_offsetX: 0,
35029	
35030	/**
35031	 * Info pane
35032	 * @type {jQuery}
35033	 */
35034	_info: false,	
35035		
35036	/**
35037	 * Current colour
35038	 * @type {String}
35039	 */
35040	_currentColour: null,
35041	
35042	/**
35043	 * View init
35044	 * @param {Object} options
35045	 * @public
35046	 */
35047	init: function(options) {
35048		var self = this;
35049		
35050		this._super(options);
35051		this._palette = options.palette || myDeco.App.palette;
35052		
35053		// set parameters to scroller
35054		this.canvasElement.find('.small')
35055			.css('background-image', 'url(' + this._palette.sImage.url + ')')
35056			.width(this._palette.sImage.width)
35057			.height(this._palette.sImage.height);
35058		
35059		// define controls
35060		this._selector = this.canvasElement.find('.selector')
35061			.width(this._palette.size - this._palette.spacing)
35062			.height(this._palette.size - this._palette.spacing);
35063		
35064		// big palette container
35065		this._container = this.canvasElement.find('.palette-holder').height(this._palette.bImage.height);
35066		this._containerWidth = parseInt(this._container.css('width'), 10);
35067		
35068		// set parameters to big palette
35069		this._paletteHolder = this._container.find('.palette')
35070			.css('background-image', 'url(' + this._palette.bImage.url + ')')
35071			.width(this._palette.bImage.width)
35072			.height(this._palette.bImage.height);
35073		
35074		// scroller values
35075		this._scrollerWidth = Math.round(this._containerWidth * this._palette.sImage.height / this._palette.bImage.height);
35076		this._scroller = this.canvasElement.find('.scroller')
35077			// set calculated scroller width
35078			.width(this._scrollerWidth)
35079			.height(this._palette.sImage.height);
35080		
35081		// scroller holder values
35082		this._scrollerHolder = this.canvasElement.find('.scroller-holder')
35083			.width(this._palette.sImage.width +
35084				(parseInt(this._scroller.css('border-left-width'), 10) || 0) +
35085				(parseInt(this._scroller.css('border-right-width'), 10) || 0))
35086			.height(this._palette.sImage.height +
35087				(parseInt(this._scroller.css('border-top-width'), 10) || 0) +
35088				(parseInt(this._scroller.css('border-bottom-width'), 10) || 0))
35089			.mousedown(this.startPan.bind(this));
35090		
35091		// color info pane
35092		this._info = this.canvasElement.find('.palette-info').hide();
35093		// bind listener for color drag
35094		this._container.mousedown(this.renderComponent.bind(this));
35095		
35096		if (this.settings.mousemove) {
35097			this._container.mousemove(function(e) {
35098				self.settings.mousemove(self.getPaletteData(e));
35099			});
35100		}
35101		
35102		if (this.settings.mouseleave) {
35103			this._container.mouseleave(self.settings.mouseleave);
35104		}
35105		
35106		// shift palette to its starting position
35107		this.pan(false, 0);
35108		
35109		// system wide listener fo selecting color on palette
35110		myDeco.body.bind('selectColor', function(e, color) {
35111			var index = self._palette.getIndex(color);
35112			if (index !== -1) {
35113				self.getMaterial(false, index);
35114				self.pan(false, Math.round(
35115					((index % self._palette.cols) / self._palette.cols) *
35116						self._palette.sImage.width - self._scrollerWidth / 2));
35117			}
35118		});
35119	},
35120	
35121	/**
35122	 * Scroller pan start listener
35123	 * @param {Event} e
35124	 * @public
35125	 */
35126	startPan: function(e) {
35127		if (e.isDefaultPrevented()) {
35128			return;
35129		}
35130		e.preventDefault();
35131		
35132		// cache listener for correct unbind
35133		var move = this.pan.bind(this),
35134			offset = this._scroller.offset(),
35135			left = parseInt(this._scroller.css('left'), 10);
35136		
35137		// not moved to init due to some problems with detection of invisible DOM elements
35138		this._paletteWidth = this._paletteHolder.outerWidth();
35139		
35140		// check for bounds
35141		if (e.pageX < offset.left || e.pageX > offset.left + this._scrollerWidth) {
35142			left = Math.max(0, Math.min(this._palette.sImage.width - this._scrollerWidth,
35143					// value of left without bounds
35144					e.pageX - this._scrollerHolder.offset().left - Math.round(this._scrollerWidth / 2)));
35145			this._scroller.css('left', left + 'px');
35146		}
35147		
35148		// store initial offset
35149		this._offsetX = e.pageX - left;
35150		
35151		myDeco.body
35152			.bind('mousemove', move)
35153			.one('mouseup', function() {
35154				myDeco.body.unbind('mousemove', move);
35155			});
35156		// move palette in case of click somewhere out of scroller
35157		this.pan(e);
35158	},
35159	
35160	/**
35161	 * Pan mouse move listener
35162	 * @param {Event} e
35163	 * @public
35164	 */
35165	pan: function(e, offset) {
35166		offset = typeof offset !== 'undefined' ? offset : e.pageX - this._offsetX;
35167		
35168		offset = Math.round(Math.max(0, Math.min(offset,
35169				// offset without bound checking
35170				this._palette.sImage.width - this._scrollerWidth)));
35171		
35172		this._scroller.css('left', offset + 'px');
35173		
35174		offset = offset === this._palette.sImage.width - this._scrollerWidth ?
35175			this._paletteWidth - this._containerWidth :
35176			offset * this._paletteWidth / this._palette.sImage.width;
35177		
35178		// append('') is a fix for IE which doesn't refresh inner content shifted with scrollLeft without DOM modifications
35179		this._container.scrollLeft(offset).append('');
35180	},
35181	
35182	getPaletteData: function(e, index) {
35183		var offset, row = 0, column = 0;
35184		
35185		// calculate row/column for color
35186		if (typeof index !== 'undefined') {
35187			row = Math.floor(index / this._palette.cols);
35188			column = index % this._palette.cols;
35189		} else if (e) {
35190			offset = this._container.offset();
35191			column = Math.floor((e.pageX - offset.left +
35192				this._container.scrollLeft()) / this._palette.size);
35193			row = Math.floor((e.pageY - offset.top) / this._palette.size);
35194		}
35195		
35196		// set the current color from palette
35197		this._currentColour = this._palette.colors[row * this._palette.cols + column];
35198		
35199		return [column, row, this._currentColour];
35200	},
35201	
35202	/**
35203	 * update info pane, select color on palette and return material for event/index
35204	 * @param {Event} e Click event
35205	 * @param {Number} index Color index in palette
35206	 */
35207	getMaterial: function(e, index) {
35208		var data = this.getPaletteData(e, index),
35209			material,
35210			self = this;
35211		
35212		if (data[2]) {
35213			//update the palette holder and scroller width
35214			this._container.width(this._containerWidth - this._info.width());
35215			this._containerWidth = parseInt(this._container.css('width'), 10);
35216			this._scrollerWidth = Math.round(this._containerWidth *
35217				this._palette.sImage.height / this._palette.bImage.height);
35218			this._scroller.width(this._scrollerWidth);
35219			
35220			// retrieve material
35221			material = myDeco.App.palette.getMaterial('#' + data[2]);
35222			
35223			// update material info
35224			updateMaterialInfo(material, this._info).show();
35225			
35226			// bind listener for color drag from info panel
35227            if (!myDeco.browser.isMobileSafari) {
35228			    this._info.find('.texture').mousedown(function(e) {
35229				    self.renderComponent(e, self._currentColour);
35230			    });
35231            }
35232			
35233			// position selector
35234			this._selector
35235				.css({
35236					left: this._palette.size * data[0] + 'px',
35237					top: this._palette.size * data[1] + 'px'
35238				})
35239				.toggleClass('light', (Math.max.apply(null, material.diffuse) +
35240					Math.min.apply(null, material.diffuse)) / 2 < 0.5);
35241			
35242			// enable info pane and position selector
35243			this.canvasElement.addClass('selected');
35244		}
35245		
35246		return material;
35247	},
35248	
35249	/**
35250	 * Color palette mousedown listener
35251	 * @param {Event} e
35252	 * @param {Boolean} current - optional
35253	 * @public
35254	 */
35255	renderComponent: function(e, current) {
35256		if (e.isDefaultPrevented()) {
35257			return;
35258		}
35259		e.preventDefault();
35260		
35261		// container offset relative to document
35262		var material = current ? myDeco.App.palette.getMaterial("#" + current) : this.getMaterial(e);
35263
35264		// palette can have holes, so check for color there
35265		if (material) {
35266			myDeco.body.trigger('entity-drag-started', [{
35267				entity: material,
35268				kind: 'decor',
35269				type: 'color',
35270				ox: -10,
35271				oy: -10,
35272				visual: this.generateElementForDrag(material, 'material')
35273			}, e]);
35274		}
35275		
35276	}
35277}));
35278
35279myDeco.App.views.push(ColorPickerView);
35280/*globals SearchModel */
35281/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35282          sloppy: true, white: true */
35283/**
35284 * @fileOverview Materials search model
35285 */
35286var MaterialsSearchModel = SearchModel.extend({
35287	/**
35288	 * Model type
35289	 * @type {String}
35290	 * @public
35291	 * @static
35292	 */
35293	$type: 'materials-search',
35294	
35295	/**
35296	 * Items type
35297	 * @type {String}
35298	 */
35299	type: 'material',
35300	
35301	/**
35302	 * Facets used
35303	 * @type {Array}
35304	 * @private
35305	 */
35306	_facetsUsed: ['class'],//, 'colour'
35307	
35308	/**
35309	 * Default parameters
35310	 * @type {Object}
35311	 * @private
35312	 */
35313	_default: {
35314		// database to request
35315		db: 'materials',
35316		
35317		// number of result items by default
35318		count: 30,
35319	
35320		// list of facets
35321		requirefacet: ['class'],//'colour'
35322		
35323		// current api version
35324		api_version: 2,
35325		
35326		// the number of materials to look thru
35327		checkatleast: -1,
35328
35329		// locale
35330		locale: 'en-gb' /*myDeco.settings.LOCALE*/
35331	}
35332});
35333/*globals SearchModelFacets */
35334/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35335          sloppy: true, white: true */
35336/**
35337 * @fileOverview Class facet from models database
35338 * Fully inherit category facet
35339 */
35340SearchModelFacets['class'] = SearchModelFacets.category.extend({
35341	/**
35342	 * Facet name
35343	 * @type {String}
35344	 * @public
35345	 * @static
35346	 */
35347	$name: 'class',
35348	
35349	/**
35350	 * Skip facet request on paint value,
35351	 * will replace with custom color picker
35352	 * @type {Array}
35353	 * @private
35354	 */
35355	_skip: ['paint']
35356});
35357/*globals SearchModelFacets */
35358/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35359          sloppy: true, white: true */
35360/**
35361 * @fileOverview Colour facet, fully inherited from image_colour facet
35362 */
35363SearchModelFacets.colour = SearchModelFacets.image_colour.extend({
35364	/**
35365	 * Facet name
35366	 * @type {String}
35367	 * @public
35368	 * @static
35369	 */
35370	$name: 'colour'
35371});
35372/*globals SearchModelFacets, SearchModelFacet */
35373/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35374          sloppy: true, white: true */
35375/**
35376 * Image colour facet
35377 */
35378SearchModelFacets.manual_colour = SearchModelFacets.image_colour.extend({
35379	/**
35380	 * Facet name
35381	 * @type {String}
35382	 * @public
35383	 * @static
35384	 */
35385	$name: 'manual_colour'
35386});
35387/*globals SearchModel, myDeco */
35388/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35389          sloppy: true, white: true */
35390/**
35391 * @fileOverview Special search model
35392 */
35393var SpecialSearchModel = SearchModel.extend({
35394	/**
35395	 * Model type
35396	 * @type {String}
35397	 * @public
35398	 * @static
35399	 */
35400	$type: 'special-search',
35401	
35402	_facetsUsed: ['manual_colour', 'dimension'],
35403	
35404	_default: {
35405		// database to request
35406		db: 'component',
35407		
35408		// items count
35409		count: 30,
35410		
35411		// list of facets
35412		requirefacet: ['manual_colour'],
35413		
35414		api_version: 2
35415	},
35416	
35417	additionalOptions: $.extend(myDeco.settings.SPECIAL_SEARCH ?
35418		myDeco.settings.SPECIAL_SEARCH.options : {}, {skip_wl_filters: true}),
35419	
35420	_initialCount: 30
35421	
35422});
35423/*globals SearchModel, myDeco */
35424/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35425          sloppy: true, white: true */
35426/**
35427 * @fileOverview Architecture search model
35428 */
35429
35430var arch_id = (myDeco.settings.WL && myDeco.settings.WL.ARCH_CATEGORY_ID) || 2035;
35431
35432var ArchitectureSearchModel = SearchModel.extend({
35433	/**
35434	 * Model type
35435	 * @type {String}
35436	 * @public
35437	 * @static
35438	 */
35439	$type: 'architecture-search',
35440	
35441	_facetsUsed: [],
35442	
35443	_default: {
35444		// database to request
35445		db: 'component',
35446		
35447		// items count
35448		count: 30,
35449		
35450		// list of facets
35451		requirefacet: [],
35452		
35453		api_version: 2
35454	},
35455	
35456	_initialCount: 30,
35457	
35458	additionalOptions: {facet: ['new,' + arch_id.toString()], skip_wl_filters: true},
35459	
35460	excludedOptions: {facet: ['supplier_id']}
35461	
35462});
35463/*globals SearchModel, myDeco */
35464/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35465          sloppy: true, white: true */
35466/**
35467 * @fileOverview Garden search model
35468 */
35469var GardenSearchModel = SearchModel.extend({
35470	/**
35471	 * Model type
35472	 * @type {String}
35473	 * @public
35474	 * @static
35475	 */
35476	$type: 'garden-search',
35477	
35478	_facetsUsed: [],
35479	
35480	_default: {
35481		// database to request
35482		db: 'component',
35483		
35484		// items count
35485		count: 30,
35486		
35487		// list of facets
35488		requirefacet: [],
35489		
35490		api_version: 2
35491	},
35492	
35493	_initialCount: 30,
35494	
35495	additionalOptions: {facet: ['new,1376'], skip_wl_filters: true},
35496	
35497	excludedOptions: {facet: ['supplier_id']}
35498});
35499/*globals SearchController, myDeco */
35500/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35501          sloppy: true, white: true */
35502/**
35503 * @fileOverview Materials search controller
35504 */
35505var MaterialsSearchController = SearchController.extend({
35506	/**
35507	 * Controller type
35508	 * @type {String}
35509	 * @public
35510	 * @static
35511	 */
35512	$type: 'materials-search',
35513
35514    // add another form submit handler to make it possible to
35515    // name-search materials
35516	registerView: function(view) {
35517		this._super(view);
35518        var self = this;
35519
35520		// listen search bar for submit
35521		this._form.submit(function(e) {
35522			e.stop();
35523			self.search(self._query.val());
35524        });
35525    }
35526});
35527
35528myDeco.App.controllers.push(MaterialsSearchController);
35529/*globals SearchController, myDeco */
35530/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35531          sloppy: true, white: true */
35532/**
35533 * @fileOverview Special search controller
35534 */
35535var SpecialSearchController = SearchController.extend({
35536	/**
35537	 * Controller type
35538	 * @type {String}
35539	 * @public
35540	 * @static
35541	 */
35542	$type: 'special-search'
35543});
35544
35545if (myDeco.settings.SPECIAL_SEARCH) {
35546	myDeco.App.controllers.push(SpecialSearchController);
35547};
35548/*globals SearchController, myDeco */
35549/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35550          sloppy: true, white: true */
35551/**
35552 * @fileOverview Special search controller
35553 */
35554var ArchitectureSearchController = SearchController.extend({
35555	/**
35556	 * Controller type
35557	 * @type {String}
35558	 * @public
35559	 * @static
35560	 */
35561	$type: 'architecture-search'
35562});
35563
35564myDeco.App.controllers.push(ArchitectureSearchController);
35565
35566/*globals SearchController, myDeco */
35567/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35568          sloppy: true, white: true */
35569/**
35570 * @fileOverview Special search controller
35571 */
35572var GardenSearchController = SearchController.extend({
35573	/**
35574	 * Controller type
35575	 * @type {String}
35576	 * @public
35577	 * @static
35578	 */
35579	$type: 'garden-search'
35580});
35581
35582myDeco.App.controllers.push(GardenSearchController);
35583
35584/*globals SearchView, myDeco */
35585/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35586          sloppy: true, white: true */
35587/**
35588 * @fileOverview Materials search view
35589 */
35590var MaterialsSearchView = SearchView.extend({
35591	/**
35592	 * View type
35593	 * @type {String}
35594	 * @public
35595	 * @static
35596	 */
35597	$type: 'materials-search',
35598
35599	/**
35600	 * View constructor
35601	 * @param {Object} options
35602	 * @public
35603	 */
35604	init: function(options) {
35605		this._super(options);
35606		this.canvasElement.find('.controls').hide()
35607	}
35608
35609});
35610
35611
35612myDeco.App.views.push(MaterialsSearchView);
35613/*globals SearchViewFacets */
35614/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35615          sloppy: true, white: true */
35616/**
35617 * @fileOverview 'class' view facet 
35618 */
35619SearchViewFacets['class'] = SearchViewFacets.category.extend({
35620	/**
35621	 * Facet name
35622	 * @type {String}
35623	 * @public
35624	 * @static
35625	 */
35626	$name: 'class',
35627	
35628	/**
35629	 * Comparison function used for making Paint section at the top of the list
35630	 * @param {String} a
35631	 * @param {String} b
35632	 * @returns {Number}
35633	 */
35634	sort: function(a, b) {
35635		if (a === 'paint') {
35636			return -1;
35637		} else if (b === 'paint') {
35638			return 1;
35639		}
35640		return 0;
35641	}
35642});
35643/*globals SearchViewFacets */
35644/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35645          sloppy: true, white: true */
35646/**
35647 * @fileOverview Colour refiner
35648 */
35649SearchViewFacets.colour = SearchViewFacets.image_colour.extend({
35650	/**
35651	 * Refiner name
35652	 * @type {String}
35653	 * @public
35654	 * @static
35655	 */
35656	$name: 'colour'
35657});
35658/*globals SearchViewFacets, SearchViewFacet, $, myDeco */
35659/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35660          sloppy: true, white: true */
35661/**
35662 * Image colour refiner
35663 */
35664SearchViewFacets.manual_colour = SearchViewFacets.image_colour.extend({
35665	/**
35666	 * Refiner name
35667	 * @type {String}
35668	 * @public
35669	 * @static
35670	 */
35671	$name: 'manual_colour'
35672});
35673/*globals SearchView, myDeco */
35674/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35675          sloppy: true, white: true */
35676/**
35677 * @fileOverview Special search view
35678 */
35679var SpecialSearchView = SearchView.extend({
35680	/**
35681	 * View type
35682	 * @type {String}
35683	 * @public
35684	 * @static
35685	 */
35686	$type: 'special-search'
35687});
35688
35689if (myDeco.settings.SPECIAL_SEARCH) {
35690	myDeco.App.views.push(SpecialSearchView);
35691};
35692/*globals SearchView, myDeco */
35693/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35694          sloppy: true, white: true */
35695/**
35696 * @fileOverview Special search view
35697 */
35698var ArchitectureSearchView = SearchView.extend({
35699	/**
35700	 * View type
35701	 * @type {String}
35702	 * @public
35703	 * @static
35704	 */
35705	$type: 'architecture-search',
35706
35707	/**
35708	 * View constructor
35709	 * @param {Object} options
35710	 * @public
35711	 */
35712	init: function(options) {
35713		this._super(options);
35714		this.canvasElement.find('.controls').hide()
35715	}
35716});
35717
35718myDeco.App.views.push(ArchitectureSearchView);
35719/*globals SearchView, myDeco */
35720/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35721          sloppy: true, white: true */
35722/**
35723 * @fileOverview Special search view
35724 */
35725var GardenSearchView = SearchView.extend({
35726	/**
35727	 * View type
35728	 * @type {String}
35729	 * @public
35730	 * @static
35731	 */
35732	$type: 'garden-search',
35733
35734	/**
35735	 * View constructor
35736	 * @param {Object} options
35737	 * @public
35738	 */
35739	init: function(options) {
35740		this._super(options);
35741		this.canvasElement.find('.controls').hide()
35742	}
35743});
35744
35745myDeco.App.views.push(GardenSearchView);
35746/*globals myDeco, $, FpeController */
35747/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35748          sloppy: true, white: true */
35749/**
35750 * @fileOverview FPE room controller
35751 */
35752
35753var IsometricController = FpeController.extend({
35754	/**
35755	 * Controller type
35756	 * @type {String}
35757	 * @static
35758	 */
35759	$type: 'isometric',
35760	
35761	/**
35762	 * Register view
35763	 * @param {Object} view FPE view
35764	 */
35765	registerView: function(view) {
35766		var self = this;
35767		this._super(view);
35768		
35769		this._view.viewHolder.find('.rotating-arrows a').click(function(e) {
35770			e.stop();
35771			self._view.rotate($(e.target).hasClass('right'));
35772		});
35773	},
35774	
35775	/**
35776	 * Mouse move event listener
35777	 * @param {Event} e Mousemove event
35778	 */
35779	mouseMove: function(e) {
35780		if (this._operation.type === 'drag') {
35781			var self = this;
35782			
35783			$.each(this._model.selected(), function() {
35784				var p = self._view.toModelPosition(
35785						e.pageX - self._operation.data.d[this.ID].x,
35786						e.pageY - self._operation.data.d[this.ID].y,
35787						self._operation.data.d[this.ID].z,
35788						this.parent);
35789				
35790				// special cases when walls are perpendicular to view
35791				if (p) {
35792					self._model.move(
35793						p.x - this.position.x,
35794						p.y - this.position.y,
35795						this.parent ?
35796							p.z - this.position.z : undefined,
35797							// force restrictions for on-wall
35798							// components in 3d view
35799						this, this.parent);
35800				}
35801			});
35802			
35803			e.stop();
35804		} else {
35805			this._super(e);
35806			
35807			if (this._operation.type === 'pan') {
35808				this._view.updateShapes();
35809			}
35810		}
35811	},
35812	
35813	/**
35814	 * Visual element drag observer
35815	 * @param {Event} e Mousedown event
35816	 * @param {Object} entity operating on
35817	 */
35818	dragStart: function(e, visual) {
35819		var self = this,
35820			transform = this._view.transforms[this._view.angle],
35821			offset = this._view.getCanvasOffset(),
35822			sceneX = (e.pageX - offset.left - this._view._offset.x) /
35823				this._view.zoom,
35824			sceneY = (e.pageY - offset.top - this._view._offset.y) /
35825				this._view.zoom;
35826		
35827		this._super(e, visual);
35828		
35829		// Storing distance between point of drag and center of door/window 
35830		$.each(this._model.selected(), function() {
35831			var p = this.position
35832				.applyTransformation(transform, 'z' + self._view.angle);
35833			
35834			if (!self._operation.data.d[this.ID]) {
35835				self._operation.data.d[this.ID] = {
35836					x: (sceneX - p.x) * self._view.zoom,
35837					y: (sceneY - p.y) * self._view.zoom,
35838					z: this.position.z
35839				};
35840			}
35841		});
35842	},
35843	
35844	/**
35845	 * End operation handler, used for redrawing after some kind of changes
35846	 * @param {Array} data Custom event data
35847	 */
35848	operationEnd: function(e, data) {
35849		data = this._super(e, data);
35850		
35851		switch(data[0]) {
35852			case 'wallHeight':
35853				this._view.calculateTransforms();
35854				this._view._cloud.findByField('type', ['IsoWall'])
35855					.batch('draw');
35856				break;
35857			case 'drag':
35858				this._view.sortCloud();
35859				break;
35860		}
35861	},
35862	
35863	/**
35864	 * Move items by keypress
35865	 * @param {Event} e
35866	 */
35867	moveByKey: function() {
35868		// don't want to move in 3D
35869	}
35870
35871});
35872
35873myDeco.App.controllers.push(IsometricController);
35874/*globals myDeco, $, FpeView, T, Transform, IsoFloor, IsoWall, IsoWindow,
35875          IsoComponent, Point, getY, getIntersection */
35876/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
35877          sloppy: true, white: true */
35878/**
35879 * @fileOverview Isometric view
35880 */
35881var IsometricView = FpeView.extend({
35882	/**
35883	 * View type, should be static
35884	 * @type {String}
35885	 * @static
35886	 */
35887	$type: 'isometric',
35888	
35889	/**
35890	 * App DOM element
35891	 * @type {Object}
35892	 */
35893	app: false,
35894	
35895	/**
35896	 * Initial angle shift
35897	 * @type {Number}
35898	 */
35899	angleShift: 45,
35900
35901	/* 
35902	 * List of precalculated transformations for different angles of view
35903	 * @type {Hash of Transform}
35904	 */
35905	transforms: {},
35906	
35907	/*
35908	 * Current angle of view
35909	 * @type {Int}
35910	 */
35911	angle: 0,
35912
35913	/*
35914	 * Angle of isometry
35915	 * @type {Int}
35916	 */
35917	isoAngle: 27,
35918	
35919	/**
35920	 * Active shape for working on
35921	 * @type {Object}
35922	 */
35923	activeShape: false,
35924	
35925	/**
35926	 * Rotation step, degrees
35927	 * @type {Number}
35928	 */
35929	step: 45,
35930	
35931	/**
35932	 * Render revision for avoid of caching
35933	 * @type {Number}
35934	 */
35935	renderRevision: 1,
35936	
35937	/**
35938	 * Init method
35939	 */
35940	init: function(options) {
35941		this._isoCos = T.cos(this.isoAngle);
35942		this._isoSin = T.sin(this.isoAngle);
35943		this._updateAngle();
35944		
35945		this._super(options);
35946		
35947		this.calculateTransforms();
35948		
35949	},
35950	
35951	/**
35952	 * Pre-calculate transformation matrices for 4 viewing angles (floor and
35953	 * ceiling)
35954	 */	
35955	calculateTransforms: function() {
35956		var i, t1, t2;
35957		
35958		for (i = 0; i < parseInt(360 / this.step, 10); i++) {
35959			t1 = new Transform()
35960					.isometric(this.isoAngle, i * this.step);
35961			this.transforms[(i * this.step)] = t1;
35962			
35963			t2 = new Transform()
35964					.translate(0, 0, this._model.ceilingHeight)
35965					.isometric(this.isoAngle, i * this.step);
35966					
35967			this.transforms[(i * this.step) + 'c'] = t2;
35968			
35969			this.transforms[(i * this.step) + 'c'].foo = 'bar';
35970		}
35971	},
35972	
35973	/** 
35974	 * Update precalculated values for sin/cos of the view rotation angle
35975	 * @param {Number} angle
35976	 */
35977	_updateAngle: function(angle) {
35978		if (typeof angle !== 'undefined') {
35979			this.angle = angle;
35980		} else if(this._model) {
35981			this.angle = (180 + Math.round((-this._model.camera.getAngle() +
35982				360 + this.angleShift) / this.step) * this.step) % 360;
35983		}
35984		this._rotCos = parseFloat(T.cos(this.angle - this.angleShift)
35985			.toFixed(5));
35986		this._rotSin = parseFloat(T.sin(this.angle - this.angleShift)
35987			.toFixed(5));
35988	},
35989	
35990	/**
35991	 * Rotate view content CW and CCW.
35992	 * @param {Boolean} backward rotate CCW.
35993	 */
35994	rotate: function(backward) {
35995		var offset = this.getCanvasOffset(),
35996			x = offset.left + this.canvasElement.width() / 2,
35997			y = offset.top + this.canvasElement.height() / 2,
35998			// rotate around a half wall height
35999			p = this.toModelPosition(x, y, this._model.ceilingHeight / 2);
36000		
36001		this._updateAngle((this.angle +
36002			(backward ? -this.step : this.step) + 360) % 360);
36003		
36004		p = p.applyTransformation(this.transforms[this.angle], this.angle);
36005		x -= p.x * this.zoom + this._offset.x + offset.left;
36006		y -= p.y * this.zoom + this._offset.y + offset.top;
36007		this.pan(x, y);
36008		
36009		this.activeShape = false;
36010		this._cloud.batch('draw');
36011		this.sortCloud(true);
36012		if (this._model.shapes.length === 1) {
36013			this.updateShapes();
36014		} else {
36015			this.updateWalls();
36016		}
36017		this.updateMaterial();
36018	},
36019	
36020	/**
36021	 * Load from model and draw isometry representation of the room on canvas
36022	 * @param {Boolean} sameRoom Flag for calls for room load from undo/redo
36023	 * routines. Disables room auto positioning
36024	 */
36025	load: function(sameRoom) {
36026		this._super(sameRoom);
36027		this._updateAngle();
36028		this.calculateTransforms();
36029		
36030		this.activeShape = false;
36031		
36032		// init and push components to a cloud
36033		this.populateCloud([
36034			[this._model.shapes, IsoFloor, this._droppablesObserver],
36035			[this._model.walls, IsoWall, this._droppablesObserver],
36036			[this._model.windows, IsoWindow, this._draggablesObserver],
36037			[this._model.doors, IsoWindow, this._draggablesObserver],
36038			[this._model.components, IsoComponent, this._draggablesObserver]
36039		], $.proxy(function() {
36040			this.sortCloud(true);
36041			
36042			if (this._model.shapes.length === 1) {
36043				this.updateShapes();
36044			} else {
36045				this.updateWalls();
36046			}
36047			
36048			this.updateMaterial();
36049		}, this));
36050	},
36051	
36052	/**
36053	 * Sort cloud and reposition components by Z index on view
36054	 * @param {Boolean} dropCache Drop cached median points values 
36055	 */
36056	sortCloud: function(dropCache) {
36057		var transform = this.transforms[this.angle],
36058			angle = this.angle,
36059			self = this,
36060			stack = [],
36061			updateMiddlePoints = function(component) {
36062				var points, i, idx = 0;
36063				if (!component._mp || dropCache ||
36064					$.inArray(component._model, self._model.selected()) !== -1) {
36065					if (component.type === 'IsoComponent' ||
36066							component.type === 'IsoWindow') {
36067						points = $.map(component._model._bbox, function(point, i) {
36068							if (i % 2 === 0) {
36069								return point.applyTransformation(transform, angle);
36070							}
36071						});
36072						
36073						// normalize order of points
36074						points.push(points.splice(2, 1).shift());
36075						
36076						// find left and right points
36077						for (i = 0; i < 4; i++) {
36078							if (points[i].x < points[idx].x) {
36079								idx = i;
36080							}
36081						}
36082						
36083						// get opposite points
36084						component._mp = [points[idx], points[(idx + 2) % 4]];
36085					} else if (component.type === 'IsoWall') {
36086						component._mp = $.map(component._model, function(point) {
36087							var p = point.applyTransformation(transform, angle);
36088							p._model = point;
36089							return p;
36090						});
36091						
36092					}
36093					
36094					// reverse middle points if first x greater than second or
36095					// if x equals and y coordinaes are reversed
36096					if (component._mp &&
36097							(component._mp[0].x > component._mp[1].x ||
36098								(component._mp[0].x === component._mp[1].x &&
36099									component._mp[0].y < component._mp[1].y))) {
36100						component._mp.reverse();
36101					}
36102				}
36103				
36104				return component;
36105			},
36106			
36107			// sorting components and walls based on medians and distance between centers of intersection
36108			medianSort = function(a, b) {
36109				var median = getIntersection(a._mp, b._mp),
36110					ay = typeof median !== 'undefined' ? getY(a._mp, median) :
36111						(a._mp[0].y + a._mp[1].y) / 2,
36112					by = typeof median !== 'undefined' ? getY(b._mp, median) :
36113						(b._mp[0].y + b._mp[1].y) / 2;
36114				
36115				return ay > by ? 1 : -1;
36116			},
36117			onWallComponents = [],
36118			secondLevelComponents = [],
36119			zeroLevelComponents = [],
36120			shapes = [],
36121			toSort = $.grep(this._cloud, function(visual) {
36122				var parents;
36123				
36124				if (visual.type === 'IsoFloor') {
36125					shapes.push(visual);
36126				
36127				// on wall components detected by level ID
36128				} else if (visual._model.level === -1) {
36129					updateMiddlePoints(visual);
36130					
36131					onWallComponents.push(visual);
36132				
36133				// second level components which isn't over other components
36134				} else if (visual._model.level === 2) {
36135					updateMiddlePoints(visual);
36136					// find "parents" for these components
36137					parents = $.grep(self._model.components,
36138						function(component) {
36139							return (component.level === 1) &&
36140								component._surface[0].z -
36141								component._surface.zDiff <
36142								visual._model._surface[0].z &&
36143								visual._model.position
36144									.inPoly(component._surface);
36145						});
36146					
36147					// if no parents force it as ordinal first-level component
36148					if (parents.length === 0) {
36149						return true;
36150					}
36151					
36152					// sort parents by z coordinate and fetch first
36153					parents.sort(function(a, b) {
36154						return b.position.z - a.position.z;
36155					});
36156					
36157					visual.parent = parents[0];
36158					
36159					secondLevelComponents.push(visual);
36160				// carpets and other coverings
36161				} else if (visual._model.level === 0) {
36162					zeroLevelComponents.push(visual);
36163				} else {
36164					updateMiddlePoints(visual);
36165					return true;
36166				}
36167				return false;
36168			});
36169		
36170		// put floor to the bottom
36171		$.merge(stack, shapes);
36172		
36173		// sort carpets by height and put to stack
36174		zeroLevelComponents.sort(function(a, b) {
36175			return b._model.height - a._model.height;
36176		});
36177		$.merge(stack, zeroLevelComponents);
36178		
36179		// sort oll other elements
36180		toSort.sort(medianSort);
36181		
36182		
36183		
36184		
36185		// sort second level components respecting to each other
36186		secondLevelComponents.sort(function(a, b) {
36187			return -medianSort(a, b);
36188		});
36189		
36190		// sort second level components respecting to its parent and height
36191		// in accending order to satisfy height requirements
36192		secondLevelComponents.sort(function(a, b) {
36193			if (a.parent.ID === b.parent.ID) {
36194				return 0;
36195			} else {
36196				return a.parent._surface[0].z - b.parent._surface[0].z;
36197			}
36198		});
36199		
36200		// add onWall elements to before walls
36201		$.each(onWallComponents, function() {
36202			var idx = 0;
36203			while (toSort[idx]._model !== this._model.parent) {
36204				idx++;
36205			}
36206			
36207			toSort.splice(idx + 1, 0, this);
36208		});
36209		
36210		// insert all second level components under its parent
36211		$.each(secondLevelComponents, function() {
36212			var idx = 0;
36213			while (toSort[idx]._model !== this.parent) {
36214				idx++;
36215			}
36216			
36217			toSort.splice(idx + 1, 0, this);
36218			delete this.parent;
36219		});
36220		
36221		$.merge(stack, toSort);
36222		
36223		$.each(stack, function() {
36224			this.element.toFront();
36225			if ($.isFunction(this.sort)) {
36226				this.sort();
36227			}
36228		});
36229	},
36230	
36231	/**
36232	 * Auto scale/pan current room to fit view area
36233	 */
36234	showAll: function() {
36235		this._super();
36236		
36237		var settings = this.settings.offset || {},
36238			top = settings.top || 0,
36239			bottom = settings.bottom || 0,
36240			transform = this.transforms[this.angle],
36241			bb = this._model.getBBox(transform, this.angle),
36242			wzoom = (Math.abs(bb.p2.x - bb.p1.x) || 500) / this.canvasWidth,
36243			hzoom = (Math.abs(bb.p2.y - bb.p1.y) || 500) /
36244				(this.canvasHeight - top - bottom),
36245			
36246			// set some space around model
36247			zoom = 0.90 / Math.max(wzoom, hzoom),
36248			cx = this.canvasWidth / 2,
36249			cy = (this.canvasHeight + top - bottom) / 2;
36250		
36251		this.pan(cx - Math.abs(bb.p2.x + bb.p1.x) / 2 * zoom,
36252			cy + Math.abs(bb.p2.y + bb.p1.y) / 2 * zoom, true);
36253		this.scale(zoom, true);
36254	},
36255	
36256	/**
36257	 * Add component to cloud and canvas
36258	 * 
36259	 * @param {Event} e Event
36260	 * @param {Object} entity Canvas entity
36261	 * @param {String} type Component type
36262	 */
36263	addComponent: function(entity) {
36264		switch(entity.type) {
36265			case 'component':
36266				this._cloud.append([entity], IsoComponent,
36267					this._draggablesObserver);
36268			break;
36269			case 'door':
36270			case 'window':
36271				this._cloud.append([entity], IsoWindow,
36272					this._draggablesObserver);
36273			break;
36274		}
36275	},
36276	
36277	/**
36278	 * Add room
36279	 * @param {Object} shape
36280	 */
36281	addShape: function(shape) {
36282		this._cloud.append([shape], IsoFloor,
36283			this.droppablesObserver, '_model');
36284	},
36285	
36286	/**
36287	 * Add wall to view
36288	 * @param {Wall} wall
36289	 */
36290	addWall: function(wall) {
36291		this._cloud.append([wall], IsoWall, this.droppablesObserver, '_model');
36292	},
36293	
36294	/**
36295	 * Generate draggable HTML element for display while dragging
36296	 * 
36297	 * @param {Object} component Display component
36298	 * @param {String} kind Kind of entity: furniture, decor, build element
36299	 */
36300	generateElementForDrag: function(component, kind) {
36301		var result = {},
36302			self = this, bb;
36303			
36304		function getOnScreenBounds(bb, onWall) {
36305			var q1, q2, q3, xmin, ymin, point,
36306				i = 0,
36307				xs = [],
36308				ys = [];
36309			
36310			for (q1 = 0; q1 <= 1; q1++) {
36311				for (q2 = 0; q2 <= 1; q2++) { 
36312					for (q3 = 0; q3 <= 1; q3++) {
36313						point = new Point(  bb[q1][0], 
36314											onWall ? bb[q2][2] : bb[q2][1],
36315											onWall ? bb[q2][1] : bb[q2][2]);
36316						
36317						point.applyTransformation(self.transforms[self.angle]);
36318						i++;
36319						xs.push(point.x);
36320						ys.push(point.y);
36321					}
36322				}
36323			}
36324			
36325			xmin = Math.min.apply(Math, xs);
36326			ymin = Math.min.apply(Math, ys);
36327			
36328			return {
36329				xmin: Math.round(xmin * self.zoom),
36330				ymin: Math.round(ymin * self.zoom),
36331				w: Math.round((Math.max.apply(Math, xs) - xmin) * self.zoom),
36332				h: Math.round((Math.max.apply(Math, ys) - ymin) * self.zoom)
36333			};
36334		}
36335		
36336		switch (kind) {
36337			case 'furniture':
36338			case 'element':
36339				if (component.type !== 'room') {
36340					bb = getOnScreenBounds(component.bounding_box, component.level === -1);
36341					result = {	
36342						'ox': - bb.xmin,
36343						'oy': - bb.ymin,
36344						'visual':
36345							$('<img src="' + 
36346							// In some rare cases render_perspective can be empty (should be fixed by SRCH-1228)
36347							(component.componentData['3d'].render_perspective || '').replace(/^(http:\/\/([^\/]+)).*$/,
36348								'$1/cdb/render/' + component.component_id + '_' + (component.revision || '00') +
36349								'_vir' + ((360 - this.angle) % 360) + (component.level === -1 ? 'w1' : '') +  '.png') + 
36350								'" style="width:' + bb.w + 'px;height:' + bb.h + 'px; display: none" class="isometric" />')
36351						};
36352				} else {
36353					bb = getOnScreenBounds([[- component.width / 2, - component.depth / 2, 0],
36354											[component.width / 2, component.depth / 2, this._model.ceilingHeight]]);
36355					
36356					result = {	
36357						'ox': - bb.xmin,
36358						'oy': - bb.ymin,
36359						'visual': $('<img class="isometric" style="width:' + bb.w + 'px;height:' + (bb.h * 1.15) + 'px;display:none" src="assets/austin/room3d.png"/>')
36360					};
36361				}
36362				break;
36363			case 'decor':
36364				result = {
36365					'ox': -10,
36366					'oy': -10,
36367					'visual': false
36368					};
36369				break;
36370		}
36371		
36372		return result;
36373	},
36374
36375	/**
36376	 * Calculate real model coordinates from given screen coordinates
36377	 * @param {Float} screenX X screen coord
36378	 * @param {Float} screenY Y screen coord
36379	 * @param {Float} sceneZ height of the plane, parallel to the floor, on which coords should be projected (used for isometry)
36380	 * @param {Wall} wall Parent wall to which screen coordinates should be projected (used for isometry)
36381	 * @returns {Object} in format {x: ..., y: ...}
36382	 */
36383	toModelPosition: function(screenX, screenY, sceneZ, wall) {
36384		var offset = this.getCanvasOffset(),
36385			sceneX = (screenX - offset.left - this._offset.x) / this.zoom,
36386			sceneY = (screenY - offset.top - this._offset.y) / this.zoom,
36387			x, y, z = sceneZ || 0, l, t;
36388		
36389		if (typeof wall === 'undefined') {
36390			// special case when angle is 90 or 270 deg
36391			if (this._rotCos === 0) {
36392				y = sceneX / this._rotSin;
36393				x = sceneY * this._rotSin / this._isoSin + (z * this._isoCos / this._isoSin) * this._rotSin;
36394			} else {
36395				y = - (sceneY * this._rotCos - this._isoSin * this._rotSin * sceneX + this._isoCos * this._isoCos * z * this._rotCos) /
36396					(this._isoSin * (this._rotSin * this._rotSin + this._rotCos * this._rotCos));
36397				x = (sceneX - this._rotSin * y) / this._rotCos;
36398			}
36399		} else {
36400			l = wall.getCoeffs();
36401			t = this._rotSin * l.a - l.b * this._rotCos;
36402			if (t === 0) {
36403				// wall is perpendicular to current view
36404				return false;
36405			} else {
36406				y = (l.c * this._rotCos + sceneX * l.a) / t;
36407				if (this._rotCos === 0) {
36408					x = (wall[0].y - y) * l.b / l.a + wall[0].x;
36409				} else {
36410					x = sceneX / this._rotCos - this._rotSin / this._rotCos * y;
36411				}
36412				z = (this._isoSin * this._rotSin * x - this._isoSin * this._rotCos * y - sceneY) / 
36413					(this._isoCos * this._isoCos);
36414			}
36415		}
36416		return new Point(x, y, z);
36417	},
36418	
36419	/**
36420	 * Do not change cursor for isometric view at all
36421	 */
36422	cursor: function() {
36423	},
36424	
36425	/**
36426	 * Convert center of viewport to a model coordinate
36427	 * @param {Number} height Height for which calculation will be
36428	 * @returns {Point}
36429	 */
36430	viewCenterToModel: function(height) {
36431		height = height || 0;
36432		
36433		var offset = this.getCanvasOffset();
36434		return this.toModelPosition(offset.left + this.canvasElement.width() / 2,
36435					offset.top + this.canvasElement.height() / 2, height);
36436	},
36437	
36438	/**
36439	 * Calculate new active shape
36440	 */
36441	updateShapes: function() {
36442		// return;
36443		var p, shape = false;
36444		// detect main shape
36445		if (this._model.shapes.length === 1) {
36446			shape = this._model.shapes[0];
36447		} else if (this._model.shapes.length > 1) {
36448			p = this.viewCenterToModel(150);
36449			shape = $.first(this._model.shapes, function(s) {
36450				return p.inPoly(s);
36451			});
36452			shape = shape || false;
36453		}
36454		
36455		if ((shape && shape !== this.activeShape) || (!shape && this.activeShape)) {
36456			this.activeShape = shape;
36457			this.updateWalls();
36458		}
36459	},
36460	
36461	/**
36462	 * Update walls visibility based on current activeShape
36463	 */
36464	updateWalls: function() {
36465		var self = this,
36466			angle = this.angle,
36467			transform = this.transforms[angle],
36468			// grep walls for further actions, active shape or all
36469			walls = this._cloud.grep(function(visual) {
36470				return visual.type === 'IsoWall' &&
36471					((!self.activeShape && visual.toggle()) ||
36472						(self.activeShape && !visual.toggle(visual._model._shapes.indexOf(self.activeShape) === -1)));
36473			}),
36474			mpoints, vpoints, wall, modelWalls,
36475			point, lP, rP, lW;
36476		
36477		if (this.activeShape) {
36478			modelWalls = this.activeShape.getWalls();
36479			lP = rP = walls[0]._mp[0];
36480			
36481			// get left and right points from iso projection
36482			$.each(walls, function() {
36483				if (this._mp[0].x < lP.x || (this._mp[0].x === lP.x && this._mp[0].y > lP.y)) {
36484					lP = this._mp[0];
36485				}
36486				
36487				if (this._mp[1].x > rP.x || (this._mp[1].x === rP.x && this._mp[1].y > rP.y)) {
36488					rP = this._mp[1];
36489				}
36490			});
36491			
36492			lP = lP._model;
36493			rP = rP._model;
36494			mpoints = this.activeShape.getNeighbors(lP);
36495			vpoints = $.map(mpoints, function(point) {
36496				return point.applyTransformation(transform, angle);
36497			});
36498			
36499			point = lP.applyTransformation(transform, angle);
36500			
36501			lW = point.angleTo(vpoints[0]) < point.angleTo(vpoints[1]) ?
36502				[lP, mpoints[0]] : [lP, mpoints[1]];
36503			
36504			// walk thru shape points until max right point isn't reached
36505			for(;;) {
36506				wall = this._model._findWall(lW[0], lW[1], modelWalls);
36507				if (wall) {
36508					walls.findByField('_model', [wall]).batch('toggle', true);
36509				}
36510				
36511				if (lP === rP) {
36512					break;
36513				}
36514				
36515				lP = this.activeShape.getOpposite(lW[1], lW[0]);
36516				lW.push(lP); lW.shift();
36517			}
36518			
36519			// enable wall joints
36520			// find hidden walls
36521			walls = this._cloud.grep(function(c) {
36522				return c.type === 'IsoWall' && c.state === false;
36523			});
36524			
36525			// find wall joints which should be enabled
36526			$.each(walls, function(i, wall) {
36527				// find walls which connected to disabled wall
36528				for (i = 0; i < 2; i++) {
36529					walls = self._cloud
36530						.findByField('_model',
36531							wall._model[i].getWalls(wall._model))
36532						.findByField('state', [true])
36533						.batch('showJoint', i, wall._model);
36534				}
36535			});
36536		}
36537	},
36538	
36539	/**
36540	 * Update materials on all decorating items
36541	 */
36542	updateMaterial: function() {
36543		this._cloud.findByField('type', ['IsoWall', 'IsoFloor']).batch('updateMaterial');
36544		this.activeShape = false;
36545	}
36546});
36547
36548// register view in dispatched view list
36549myDeco.App.views.push(IsometricView);
36550/*globals myDeco, $, Drawable, window, diffuse2Hex, textureFill */
36551/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
36552          sloppy: true, white: true */
36553/**
36554 * @fileOverview Isometric representation of the floor
36555 */
36556var IsoFloor = Drawable.extend({
36557	/**
36558	 * Intsance type
36559	 * @type {String}
36560	 */
36561	type: 'IsoFloor',
36562	
36563	/**
36564	 * Init function
36565	 * @param {Object} view FPE view
36566	 * @param {Object} model Model entity
36567	 */
36568	init: function(view, model) {
36569		this._super(view, model);
36570		
36571		var self = this;
36572		
36573		this._model.attachEvent('onMaterial', function() {
36574			self.updateMaterial();
36575		}, this._view.$type);
36576	},
36577
36578	/**
36579	 * Floor drawing function
36580	 */
36581	draw: function() {
36582		var self = this,
36583			points = $.map(this._model, function(point) {
36584				return point.applyTransformation(
36585					self._view.transforms[self._view.angle], self._view.angle);
36586			});
36587		
36588		if (!this.element) {
36589			this.element = this._view.canvas.path({
36590				'stroke-opacity': 0,
36591				'stroke-width': 0
36592			}).hold().poly(points, true).render();
36593		} else {
36594			this.element.hold().update(points, true).render();
36595		}
36596		
36597		this._super();
36598	},
36599	
36600	/**
36601	 * Update floor material
36602	 */
36603	updateMaterial: function() {
36604		this.element.attr({
36605			fill: textureFill(this._model.floorMaterial, $.extend(true, {
36606				'filled_rotation':
36607					(this._view.angleShift - this._view.angle) % 360,
36608				'y_scale': Math.round(this._view._isoSin * 100)
36609			}, this._settings.options))
36610		});
36611	}
36612});
36613/*globals myDeco, $, Drawable, window, diffuse2Hex, textureFill,
36614          getIntersection, getY */
36615/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
36616          sloppy: true, white: true */
36617/**
36618 * @fileOverview Isometric representation of the wall
36619 */
36620var IsoWall = Drawable.extend({
36621	/**
36622	 * Intsance type
36623	 * @type {String}
36624	 */
36625	type: 'IsoWall',
36626	
36627	/**
36628	 * Texture shear angle
36629	 * @type {Number}
36630	 */
36631	_shearAngle: 0,
36632	
36633	_top: false,
36634	
36635	_order: [],
36636	
36637	/**
36638	 * Init function
36639	 * @param {Object} view FPE view
36640	 * @param {Object} model Model entity
36641	 */
36642	init: function(view, model) {
36643		this._super(view, model);
36644		
36645		var self = this;
36646		this._model
36647			.attachEvent('onMaterial', function() {
36648				self.updateMaterial();
36649			}, this._view.$type)
36650			.attachEvent('onThickness', function() {
36651				// update attached points on the attached walls
36652				self._view._cloud.findByField('_model', self._model.getWalls())
36653					.batch('draw');
36654			}, this._view.$type);
36655	},
36656
36657	/**
36658	 * Floor drawing function
36659	 */
36660	draw: function() {
36661		var i, j,
36662			settings = this._settings.visual,
36663			floor = [], ceiling = [], walls = [], points,
36664			yCoords = [],
36665			joints = this._model.getJoints(),
36666			tFloor = this._view.transforms[this._view.angle],
36667			tCeiling = this._view.transforms[this._view.angle + 'c'],
36668			noStroke = $.extend({}, settings, {
36669				'stroke-width': 0,
36670				'stroke-opacity': 0,
36671				'stroke': settings.fill
36672			});
36673		
36674		this._order = [0, 1, 2, 3, 4, 5];
36675		
36676		for (i = 0; i < 6; i++) {
36677			floor.push(joints[i]
36678				.applyTransformation(tFloor, this._view.angle));
36679			ceiling.push(joints[i]
36680				.applyTransformation(tCeiling, this._view.angle + 'c', true));
36681		}
36682		
36683		for (i = 0; i < 6; i++) {
36684			walls.push([floor[i], floor[(i + 1) % 6]]);
36685			yCoords.push((walls[i][0].y + walls[i][1].y) / 2);
36686		}
36687		
36688		this._shearAngle = -Math.round(floor[1].angleTo(floor[4], true));
36689		
36690		if (!this.element) {
36691			this.element = this._view.canvas.set();
36692		}
36693		
36694		for (i = 0; i < 6; i++) {
36695			j = (i + 1) % 6;
36696			points = [floor[i], ceiling[i], ceiling[j], floor[j]];
36697			if (!this._top) {
36698				this.element.push(this._view.canvas.path(noStroke).hold()
36699					.poly(points, true).render());
36700			} else {
36701				this.element[i].hold().update(points, true).render();
36702			}
36703		}
36704		
36705		this._order.sort(function(a, b) {
36706			var x = getIntersection(walls[a], walls[b]),
36707				ay = typeof x !== 'undefined' ? getY(walls[a], x) : yCoords[a],
36708				by = typeof x !== 'undefined' ? getY(walls[b], x) : yCoords[b];
36709				
36710			return ay > by ? 1 : -1;
36711		});
36712		
36713		if (!this._top) {
36714			this._top = this._view.canvas.path(settings).hold()
36715				.poly(ceiling, true).render();
36716			this.element.push(this._top);
36717			this._super();
36718		} else {
36719			this._top.hold().update(ceiling, true).render();
36720		}
36721		
36722		// add top side to sorting array
36723		this._order.push(6);
36724		
36725		this.updateMaterial();
36726	},
36727	
36728	/**
36729	 * Sort wall after scene sorting
36730	 */
36731	sort: function() {
36732		var i;
36733		for (i = 1; i < 7; i++) {
36734			this.element[this._order[i]]
36735				.insertAfter(this.element[this._order[i - 1]]);
36736		}
36737	},
36738	
36739	/**
36740	 * Update wall material
36741	 */
36742	updateMaterial: function() {
36743		var attr = {
36744			fill: textureFill(this._model.material, $.extend(true, {
36745					'y_wrapped_shear': this._shearAngle
36746				}, this._settings.options))
36747			};
36748		this.element[2].attr(attr);
36749		this.element[5].attr(attr);
36750	},
36751	
36752	/**
36753	 * Toggle wall visibility with all components on it
36754	 * @param {Boolean} state
36755	 */
36756	toggle: function(state) {
36757		var ret, self = this,
36758			x = Math.min(this._order.indexOf(2), this._order.indexOf(5));
36759		
36760		this._view._cloud.findByField('_model', this._model.components)
36761			.batch('toggle', state);
36762			
36763		ret = this._super(state);
36764		$.each([0, 1, 3, 4], function(i, id) {
36765			self.element[id].hide();
36766		});
36767		
36768		this.element[this._order[x]].hide();
36769		
36770		return ret;
36771	},
36772	
36773	/**
36774	 * Show joint wall pieces for given wall and corner index in given wall
36775	 * @param {Number} i
36776	 * @param {Object} wall
36777	 */
36778	showJoint: function(i, wall) {
36779		// convert foreign wall point index to our model point index
36780		i = this._model.indexOf(wall[i]);
36781		
36782		var idx = 3 * i,
36783			joints = this._model[i].getJointWalls(this._model),
36784			ilen = joints.length;
36785		
36786		for (i = 0; i < ilen; i++) {
36787			if (joints[i] === wall) {
36788				this.element[i + idx].show();
36789			}
36790		}
36791	}
36792});
36793/*globals Draggable, IsoWindow, Point, T, buildThumbnailLink, myDeco */
36794/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
36795          sloppy: true, white: true */
36796/**
36797 * @fileOverview Isometric representation of the window
36798 */
36799var IsoWindow = Draggable.extend({
36800	/**
36801	 * Intsance type
36802	 * @type {String}
36803	 */
36804	type: 'IsoWindow',
36805
36806	/**
36807	 * Init function
36808	 * @param {Object} view FPE view
36809	 * @param {Object} model Model entity
36810	 */
36811	init: function(view, model) {
36812		var self = this;
36813		// Little trick to full parent's constructor
36814		this.move = this.draw;
36815		this._super(view, model);
36816		
36817		this._model.attachEvent('onMaterial', function() {
36818			self.updateMaterial();
36819		}, this._view.$type);
36820		
36821		this._model.attachEvent('onAttributeChange', this.draw.bind(this));
36822	},
36823	
36824	/**
36825	 * Shear angle for door/window texture
36826	 * @type {Number}
36827	 */
36828	_shearAngle: 0,
36829	
36830	/**
36831	 * Floor drawing function
36832	 */
36833	draw: function() {
36834		var p, x = [], y = [], x1, x2, y1, y2, c,
36835			tFloor = this._view.transforms[this._view.angle],
36836			points = [], angleFix = 90,
36837			position = this._model.position.clone()
36838				.translate(this._model.parent.thickness / 2, 0),
36839			indexes = [[0, 0], [0, 1], [1, 1], [1, 0]],
36840			doorway = '48d83ae6e14995f52feb252631f9eac1da8105da';
36841		
36842		y1 = this._model.position.clone().translate(100, 0)
36843			.rotate(this._model.position,
36844				this._model.parent.getAngle() + angleFix)
36845			.applyTransformation(tFloor, this._view.angle).y;
36846		
36847		y2 = this._model.position.clone().translate(100, 0)
36848			.rotate(this._model.position,
36849				this._model.parent.getAngle() - angleFix)
36850			.applyTransformation(tFloor, this._view.angle).y;
36851		
36852		if (y1 < y2) {
36853			angleFix = -90;
36854		}
36855		
36856		position.rotate(this._model.position,
36857			this._model.parent.getAngle() + angleFix);
36858		
36859		x1 = y1 = Number.MAX_VALUE;
36860		x2 = y2 = -Number.MAX_VALUE;
36861		
36862		for (c = 0; c < 4; c++) {
36863			p = new Point(this._model.bounding_box[indexes[c][0]].x,
36864					this._model.bounding_box[indexes[c][1]].y, 0)
36865				.translate(position.x, position.y, position.z)
36866				.rotate(position, -this._model.rotation[2],
36867					this._model.rotation[0], this._model.rotation[1])
36868				.applyTransformation(tFloor, this._view.angle);
36869			points.push(p);
36870			x.push(p.x);
36871			y.push(p.y);
36872		}
36873		
36874		if (this._model.component_id === doorway) {
36875			if (!this.element) {
36876				this.element = this._view.canvas.path({
36877					'fill': '#fff',
36878					'stroke-width': 0.5,
36879					'stroke': '#ccc'
36880				}).poly(points, true);
36881			} else {
36882				this.element.update(points, true);
36883			}
36884		} else {
36885			x1 = Math.min.apply(Math, x);
36886			x2 = Math.max.apply(Math, x);
36887			y1 = Math.min.apply(Math, y);
36888			y2 = Math.max.apply(Math, y);
36889			
36890			if (!this.element) {
36891				this.element = this._view.canvas.image(
36892					myDeco.settings.STATIC_URL +
36893						'blank.gif', x1, y1, x2 - x1, y2 - y1);
36894			} else {
36895				this.element.attr({
36896					src: myDeco.settings.STATIC_URL + 'blank.gif',
36897					x: x1, y: y1, width: x2 - x1, height: y2 - y1
36898				});
36899			}
36900
36901		}
36902		
36903		this._shearAngle = Math.round(T.atan(
36904			((x2 - x1) / ((points[1].y - points[0].y) * this._model.width /
36905				this._model.height)) * (points[1].y - points[2].y) /
36906					(points[2].x - points[1].x)));
36907		
36908		this._super();
36909	},
36910	
36911	/**
36912	 * Update component image
36913	 */
36914	updateMaterial: function() {
36915		if (!isNaN(this._shearAngle)) {
36916			this.element.attr({src: buildThumbnailLink(
36917				this._model.componentData['3d'].render_top, {
36918					mirror: +(this._model.open_side === 'l'),
36919					shear: this._shearAngle
36920				})});
36921		}
36922	}
36923});
36924/*globals myDeco, $, Draggable, extractRootUrl */
36925/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
36926          sloppy: true, white: true */
36927/**
36928 * @fileOverview Isometric representation of the component
36929 */
36930var IsoComponent = Draggable.extend({
36931	/**
36932	 * Intsance type
36933	 * @type {String}
36934	 */
36935	type: 'IsoComponent',
36936	
36937	/**
36938	 * Init function
36939	 * @param {Object} view FPE view
36940	 * @param {Object} model Model entity
36941	 */
36942	init: function(view, model) {
36943	// Little trick to fool parent's constructor
36944		this.move = this.draw;
36945		
36946		this._super(view, model);
36947		this._model.attachEvent('onRotate', this.draw.bind(this),
36948			this._view.$type);
36949	},
36950
36951	/**
36952	 * Component drawing function
36953	 */
36954	draw: function() {
36955		var p, q1, q2, q3, 
36956			bbox = {}, i = 0,
36957			tFloor = this._view.transforms[this._view.angle],
36958			x1 = Number.MAX_VALUE, y1 = Number.MAX_VALUE,
36959			x2 = -Number.MAX_VALUE, y2 = -Number.MAX_VALUE,
36960			angleFix = 90;
36961		
36962		if (this._model.parent) {
36963			q1 = this._model.position.clone().translate(100, 0)
36964				.rotate(this._model.position,
36965					this._model.parent.getAngle() + angleFix)
36966				.applyTransformation(tFloor, this._view.angle).y;
36967			
36968			q2 = this._model.position.clone().translate(100, 0)
36969				.rotate(this._model.position,
36970					this._model.parent.getAngle() - angleFix)
36971				.applyTransformation(tFloor, this._view.angle).y;
36972			if (q1 < q2) {
36973				angleFix = -90;
36974			}
36975		}
36976		
36977		for (q1 = 0; q1 <= 1; q1++) {
36978			for (q2 = 0; q2 <= 1; q2++) {
36979				for (q3 = 0; q3 <= 1; q3++) {
36980					p = this._model._bbox[i];
36981					
36982					if (this._model.parent) {
36983						p = p.clone()
36984							.translate(this._model.parent.thickness / 2, 0)
36985							.rotate(this._model._bbox[i],
36986								this._model.parent.getAngle() + angleFix);
36987					}
36988					
36989					p = bbox[String(q1) + String(q2) + String(q3)] = p
36990						.applyTransformation(tFloor, this._view.angle);
36991					
36992					x1 = Math.min(x1, p.x);
36993					y1 = Math.min(y1, p.y);
36994					x2 = Math.max(x2, p.x);
36995					y2 = Math.max(y2, p.y);
36996					i++;
36997				}
36998			}
36999		}
37000		
37001		if (typeof this._hover === 'undefined') {
37002			this._hover = this._view.canvas.path(
37003				this._view.settings.IsoComponent.visual.inactive).hold();
37004			this._base = this._view.canvas.image(
37005				myDeco.settings.STATIC_URL + 'blank.gif',
37006				x1, y1, x2 - x1, y2 - y1);
37007		} else {
37008			this._hover.hold().clear();
37009			// Point image src to small blank image to avoid skewing of
37010			// old image, while new is loading
37011			this._base.attr({src: myDeco.settings.STATIC_URL + 'blank.gif',
37012				width: x2 - x1, height: y2 - y1, x: x1, y: y1});
37013		}
37014		
37015		this.updateRender();
37016		
37017		this._hover
37018			.moveTo(bbox['000'].x, bbox['000'].y)
37019			.lineTo(bbox['100'].x, bbox['100'].y)
37020			.lineTo(bbox['110'].x, bbox['110'].y)
37021			.lineTo(bbox['010'].x, bbox['010'].y)
37022			.lineTo(bbox['000'].x, bbox['000'].y)
37023			.lineTo(bbox['001'].x, bbox['001'].y)
37024			.lineTo(bbox['101'].x, bbox['101'].y)
37025			.lineTo(bbox['111'].x, bbox['111'].y)
37026			.lineTo(bbox['011'].x, bbox['011'].y)
37027			.lineTo(bbox['001'].x, bbox['001'].y)
37028			.moveTo(bbox['100'].x, bbox['100'].y)
37029			.lineTo(bbox['101'].x, bbox['101'].y)
37030			.moveTo(bbox['110'].x, bbox['110'].y)
37031			.lineTo(bbox['111'].x, bbox['111'].y)
37032			.moveTo(bbox['010'].x, bbox['010'].y)
37033			.lineTo(bbox['011'].x, bbox['011'].y)
37034			.render();
37035		
37036		this.element = this._view.canvas.set([this._hover, this._base]);
37037
37038		this._super();
37039	},
37040
37041	/**
37042	 * Update render view on canvas
37043	 */
37044	updateRender: function() {
37045		if (this._model.componentData) {
37046			this._base
37047				// clear previous image
37048				.attr({'src': myDeco.settings.STATIC_URL + 'blank.gif'})
37049				// set new image
37050				.attr({'src': extractRootUrl(
37051					this._model.componentData['3d'].render_perspective) +
37052					'cdb/render/r' + this._view.renderRevision + '/' +
37053					this._model.component_id + '_' +
37054					(this._model.revision || '00') +
37055					'_vir' + ((this._view.angleShift - this._view.angle -
37056					this._model.rotation[2] + 360).toFixed() % 360) +
37057					(this._model.level === -1 ? 'w1' : '') + '.png'});
37058		}
37059	},
37060	
37061	/**
37062	 * Decorate component depend on event type
37063	 * @param {String} type Event type
37064	 * @returns {Object} Event settings
37065	 */
37066	_decorate: function(type) {
37067		switch(type) {
37068			case 'selected':
37069			case 'highlighted':
37070				this._hover
37071					.attr(this._view.settings.IsoComponent.visual.active);
37072				break;
37073			case 'normal':
37074				this._hover
37075					.attr(this._view.settings.IsoComponent.visual.inactive);
37076			break;
37077		}
37078		
37079		return {};
37080	}
37081});
37082/*globals myDeco, $, AppController, window, document, KEY */
37083/*jslint sloppy: true, white: true, nomen: true, plusplus: true */
37084/**
37085 * @fileOverview Common Layout controller
37086 */
37087
37088var LayoutController = AppController.extend({
37089	/**
37090	 * Controller type
37091	 * @type {String}
37092	 * @static
37093	 */
37094	$type: 'layout',
37095
37096	/**
37097	 * The list of elements which should not loose focus
37098	 * @type {String}
37099	 */
37100	tagNames: ' INPUT TEXTAREA SELECT OPTION EMBED ',
37101
37102	/**
37103	 * Active action name
37104	 * @type {String}
37105	 */
37106	action: false,
37107
37108	/**
37109	 * Status message element and data
37110	 * @type {Object}
37111	 */
37112	_status: {},
37113
37114	/**
37115	 * Layout controller init
37116	 * @param {Object} model FPE model
37117	 * @param {Object} settings Controller settings
37118	 */
37119	init: function (model, settings) {
37120		this._super(model, settings);
37121
37122		var self = this,
37123			sidebar = $('#sidebar'),
37124			leftLimit = parseInt(sidebar.css('left'), 10),
37125			sidebarTabs = sidebar.find('.sidebar-buttons ul.tabs'),
37126			fakeControl = $('#fakeControl input'),
37127			preventReset;
37128
37129		/* Montage tabs */
37130		if (sidebarTabs.size() === 0) {
37131			sidebarTabs = sidebar.find('ul.sidebar-buttons');
37132		}
37133
37134		sidebar.delegate('dt a', 'click', function (e) {
37135			e.stop();
37136			$(this).closest('dl').toggleClass('collapsed');
37137		});
37138
37139		myDeco.tabSwitcher.initTabSwitcher(sidebarTabs, {
37140			onShow: function (tab, callback) {
37141				callback(tab);
37142				sidebar.animate({left: 0}, 'fast', 'swing', function() {
37143					sidebar.removeClass('minimized').trigger('maximized');
37144				});
37145			},
37146
37147			onHide: function (tab, callback) {
37148				sidebar.animate({left: leftLimit}, 'fast', 'swing', function() {
37149					callback(tab);
37150					sidebar.addClass('minimized').trigger('minimized');
37151				});
37152			}
37153		});
37154
37155		myDeco.body
37156			.bind('preventOperationReset', function() {
37157				preventReset = true;
37158			})
37159			.mouseup(function() {
37160				if (preventReset) {
37161					preventReset = false;
37162				} else {
37163					myDeco.App.model.operationReset();
37164				}
37165			})
37166			.mousemove(this.globalMouseMove.bind(this))
37167			// To avoid uneccessary scrolling on iPad.
37168			.bind('touchmove', function (e) {
37169				if (e.changedTouches.length === 2) {
37170					e.preventDefault();
37171				}
37172			})
37173			.bind('toggleSidebar', function (e, tabClass) {
37174				if (tabClass) {
37175					sidebarTabs.trigger('show', [tabClass]);
37176				} else {
37177					sidebarTabs.trigger('hide');
37178				}
37179			})
37180			.bind('entity-drag-started', this.entityDrag.bind(this))
37181			.bind('status roomLoading roomLoaded roomLoadError roomSaving ' +
37182					'roomSaved roomSavedError',
37183				function (e, data1, data2, data3) {
37184					var text, timeout,
37185						roomName = (myDeco.settings.WL && myDeco.settings.WL.WL_ROOM_NAME) ? myDeco.settings.WL.WL_ROOM_NAME : 'room';
37186
37187					switch (e.type) {
37188						case 'status':
37189							text = data1 || '';
37190							timeout = data2;
37191							break;
37192
37193						case 'roomLoading':
37194							if (typeof data1 === 'string') {
37195								text = roomName.capitalize() + ' is loading…';
37196								timeout = 0;
37197							}
37198							break;
37199
37200						case 'roomLoaded':
37201							if (!data1 && self._model._ws_url) {
37202								text = roomName.capitalize() + ' loaded successfully';
37203							}
37204							break;
37205
37206						case 'roomLoadError':
37207							text = 'Error while '+ roomName +' loading, please try later';
37208							timeout = 30;
37209							break;
37210
37211						case 'roomSaving':
37212							text = 'Saving your '+ roomName +'…';
37213							timeout = 0;
37214							break;
37215
37216						case 'roomSaved':
37217							text = $('<span>Your ' + (roomName === 'room' ? 'design' : roomName) + ' has been saved successfully. </span>');
37218							timeout = 30;
37219							break;
37220
37221						case 'roomSavingError':
37222							text = 'Error while '+ roomName +' saving, please try later';
37223							timeout = 30;
37224							break;
37225					}
37226
37227					if (text !== undefined) {
37228						self.status(text, timeout);
37229					}
37230			});
37231
37232		if (!myDeco.browser.isMobileSafari) {
37233			myDeco.body.bind('mousedown selectstart dragstart',
37234				function (e) {
37235					if (self.tagNames.indexOf(' ' + e.target.tagName + ' ') === -1) {
37236						e.preventDefault();
37237						// undefine text cursor position in the document
37238                        fakeControl.focus().blur();
37239					}
37240				});
37241		} else {
37242			// longpress emulation for iPad (doubleclick replacement)
37243			myDeco.body.bind('mousedown', function (e) {
37244				var timeout = window.setTimeout(function() {
37245						var event = new $.Event(e);
37246						event.type = 'dblclick';
37247						$(e.target).trigger(event);
37248					}, 1000);
37249
37250				myDeco.body.one('mouseup', function() {
37251					window.clearTimeout(timeout);
37252				});
37253			});
37254		}
37255
37256
37257		this._status.element = $('#messages');
37258		this._status.id = 0;
37259		this._status.top = parseInt(this._status.element.css('top'), 10);
37260		this._status.height = this._status.element.outerHeight(true);
37261		this._status.element.click(function (e) {
37262			e.preventDefault();
37263			self.status();
37264		});
37265
37266		$(document)
37267			.keydown(this.keyDown.bind(this))
37268			.keypress(this.keyPress.bind(this))
37269			.keyup(this.keyUp.bind(this));
37270	},
37271
37272	/**
37273	 * Register view
37274	 * @param {Object} view View associated to controller
37275	 */
37276	registerView: function (view) {
37277		this._super(view);
37278		var self = this;
37279
37280		view.app
37281			.mouseout(function (e) {
37282				var offset = self._view.getAppOffset(),
37283					dimensions = self._view.getAppDimensions();
37284
37285				// reset operation if pointer is out of app
37286				if (e.pageX < offset.left || e.pageX > offset.left + dimensions.width ||
37287					e.pageY < offset.top || e.pageY > offset.top + dimensions.height) {
37288					self._model.operationReset();
37289				}
37290			});
37291
37292	},
37293
37294	/**
37295	 * Handler for custom entity-drag-started event. Creates a DOM wrapper for
37296	 * draggable element and does some visual machinery.
37297	 * @param {Event} e entity-drag-started event
37298	 * @param {Object} draggable Info about draggable entity and it's visual representation
37299	 * @param {Event} originalEvent Original event which caused start of drag-n-drop
37300	 */
37301	entityDrag: function (e, draggable, originalEvent) {
37302		this._view.app
37303			.append(draggable.visual);
37304
37305		this._operation =	{ type: 'add',
37306								data: {
37307									entity: draggable.entity,
37308									visual: draggable.visual,
37309									ox: draggable.ox || draggable.visual.width() / 2,
37310									oy: draggable.oy || draggable.visual.height() / 2
37311								}
37312							};
37313
37314		draggable.visual
37315			.css({
37316				left: (originalEvent.pageX || 0) - this._operation.data.ox + 'px',
37317				top: (originalEvent.pageY || 0) - this._operation.data.oy + 'px',
37318				position: 'absolute'
37319			});
37320	},
37321
37322	/**
37323	 * Global mouse move event handler for serving drag-n-drop between sidebar and canvases
37324	 * @param {Event} e Mousemove event
37325	 */
37326	globalMouseMove: function (e) {
37327		if (this._operation.type === 'add') {
37328			e.preventDefault();
37329			var position = this._operation.data.visual.data('position') ||
37330							{x: this._operation.data.ox, y: this._operation.data.oy};
37331
37332			this._operation.data.visual.css({
37333				left: e.pageX - position.x + 'px',
37334				top: e.pageY - position.y + 'px'
37335			});
37336		}
37337	},
37338
37339	/**
37340	 * Callback for resetting current operation state
37341	 */
37342	operationReset: function() {
37343		if (this._operation.type === 'add' && this._operation.data.visual) {
37344			this._operation.data.visual.remove();
37345		}
37346
37347		this._operation = {type: '', data: {}};
37348	},
37349
37350	/**
37351	 * Show status message in app
37352	 * @param {jQset|String} message Message to show, will close if omitted
37353	 * @param {Number} timeout Timeout to hide message, will show endless
37354	 *                         message if less or equals to zero and show WoD in
37355	 *                         case of equals to zero.
37356	 */
37357	status: function (message, timeout) {
37358		var self = this,
37359			type = typeof message;
37360
37361		if (message && type !== 'number') {
37362			if (type === 'string') {
37363				message = $(document.createTextNode(message));
37364			}
37365
37366			timeout = typeof timeout === 'undefined' ? 5 : timeout;
37367
37368			this._status.element
37369				.toggleClass('waiting', timeout === 0)
37370				.children('.message').html('').append(message).end()
37371				.animate({top: this._status.top + this._status.height});
37372
37373			if (timeout > 0) {
37374				window.setTimeout(function() {
37375					self.status(++self._status.id);
37376				}, timeout * 1000);
37377			}
37378		} else if (!message ||
37379				(type === 'number' && message === this._status.id)) {
37380			this._status.element.animate({top: this._status.top});
37381		}
37382	},
37383
37384	/**
37385	 * Global document observer, used to prevent default browser behavior on backspace.
37386	 * @param {Event} e
37387	 */
37388	keyDown: function (e) {
37389		// prevent to stop animation etc
37390		if (typeof KEY !== 'undefined' && e.keyCode === KEY.Esc) {
37391			e.preventDefault();
37392		}
37393
37394		// prevent to deletion if key pressed in input fields
37395		// pass through any special events with modifiers
37396		if (!e.ctrlKey && !e.altKey && !e.metaKey &&
37397				this.tagNames.indexOf(' ' + e.target.tagName + ' ') !== -1) {
37398			return;
37399		}
37400
37401		// under win repeated event is keydown
37402		var kdr = window.navigator.platform.indexOf('Win') !== -1 &&
37403				($.browser.msie || $.browser.mozilla),
37404			action;
37405
37406
37407		if (!this.action) {
37408			action = $.first((this.settings.shortcuts || []), function (cmd) {
37409				return (e.keyCode === cmd[1] && e.shiftKey === cmd[2] &&
37410						(e.ctrlKey === cmd[3] || e.metaKey === cmd[3]) && e.altKey === cmd[4]);
37411			});
37412		}
37413
37414		// prevent default behavior of some keys to
37415		// disable default browser reaction
37416		// eg. scrolling of focused layers or move back in browser history
37417		if (this.action || action ||
37418			(typeof KEY !== 'undefined' &&
37419				(e.keyCode === KEY.Up || e.keyCode === KEY.Down ||
37420				e.keyCode === KEY.BackSpace))) {
37421			e.preventDefault();
37422		}
37423
37424		if (action || this.action) {
37425			if (!this.action || (this.action[5] && kdr)) {
37426				myDeco.body.trigger('actionRequest', [this.action || action]);
37427			}
37428			this.action = this.action || action;
37429		}
37430	},
37431
37432	/**
37433	 * Keypress listener
37434	 * @param {Event} e
37435	 */
37436	keyPress: function() {
37437		if (this.action && this.action[5]) {
37438			myDeco.body.trigger('actionRequest', [this.action]);
37439		}
37440	},
37441
37442	/**
37443	 * Keyup listener
37444	 * @param {Event} e
37445	 */
37446	keyUp: function() {
37447		this.action = false;
37448	}
37449});
37450
37451(function() {
37452var _obj = {
37453		width: {
37454			"name":"width",
37455			"text":gettext("Width"),
37456			"type":"length",
37457			"detail":0,
37458			"default":60,
37459			"minimum":20,
37460			"maximum":500
37461			},
37462		height: {"name":"height",
37463			"text":gettext("Height"),
37464			"type":"length",
37465			"detail":0,
37466			"default":180,
37467			"minimum":50,
37468			"maximum":500
37469			},
37470		depth: {
37471			"name":"depth",
37472			"text":gettext("Depth"),
37473			"type":"length",
37474			"detail":99,
37475			"default":20,
37476			"minimum":0,
37477			"maximum":100
37478			},
37479		open_side: {
37480			"name":"open_side",
37481			"text":gettext("Open Side"),
37482			"type":"enumeration",
37483			"detail":0,
37484			"default":"l",
37485			"values":[["l", gettext("Left")], ["r", gettext("Right")]]
37486			},
37487		open_direction: {
37488			"name":"open_direction",
37489			"text":gettext("Open Direction"),
37490			"type":"enumeration",
37491			"detail":0,
37492			"default":"i",
37493			"values":[["i", gettext("In")], ["o", gettext("Out")]]
37494			},
37495		open: {
37496			"name":"open",
37497			"text":gettext("Open"),
37498			"type":"fraction",
37499			"detail":0,
37500			"default":"0"
37501			},
37502		finish: {
37503			"name":"finish",
37504			"text":gettext("Colour"),
37505			"type":"material",
37506			"detail":0,
37507			"default":"white"
37508			},
37509
37510		panel_h: {
37511				"name":"panels_h",
37512				"text":"Panels Across",
37513				"type":"count",
37514				"detail":0,
37515				"default":3,
37516				"minimum":1,
37517				"maximum":6
37518			},
37519		panel_v: {
37520				"name":"panels_v",
37521				"text":"Panels Down",
37522				"type":"count",
37523				"detail":0,
37524				"default":6,
37525				"minimum":1,
37526				"maximum":8
37527			}
37528	};
37529myDeco.App.ws.components = {
37530	//door
37531	"f75c8910592b2bd613dcd1d2b60d5f237bb27a08": {
37532		"name": gettext("Plain Door"),
37533		"level": -1,
37534		"renders": {
37535			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.back.png",
37536			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.bottom.png",
37537			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.front.png",
37538			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.left.png",
37539			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.perspective.png",
37540			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.right.png",
37541			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.top.png"
37542		},
37543		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_07.perspective.png"],
37544		"type": "Door",
37545		"aperture": "rectangle",
37546		"configuration": [
37547			_obj.width,
37548			_obj.height,
37549			_obj.depth,
37550			_obj.open_side,
37551			_obj.open_direction,
37552			_obj.open,
37553			_obj.finish
37554		]
37555	},
37556	//panel door
37557	"cf28e9e5b7659cbc88e3ed735bdfc827606628ff": {
37558		"name": gettext("Panel Door"),
37559		"level": -1,
37560		"renders": {
37561			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55
37561d9b3c7b9c138f5370aa5d4cd23fae7_07.back.png",
37562			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.bottom.png",
37563			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.front.png",
37564			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.png",
37565			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.perspective.png",
37566			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.right.png",
37567			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.top.png"
37568		},
37569		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/59/59e1639b55d9b3c7b9c138f5370aa5d4cd23fae7_07.perspective.png"],
37570		"type": "Door",
37571		"aperture": "rectangle",
37572		"configuration": [
37573			_obj.width,
37574			_obj.height,
37575			_obj.depth,
37576			_obj.open_side,
37577			_obj.open_direction,
37578			_obj.open,
37579			_obj.finish,
37580			$.extend({}, _obj.panel_h,
37581				{
37582					"default":2,
37583					"maximum":5
37584				}
37585			),
37586			$.extend({}, _obj.panel_v,
37587				{
37588					"default":2,
37589					"maximum":5
37590				}
37591			)
37592		]
37593	},
37594	//door
37595	"899f1d4c7b441166851b9e38eb28f0ade2837687": {
37596		"name": gettext("Sliding Door"),
37597		"level": -1,
37598		"renders": {
37599			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.back.png",
37600			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.bottom.png",
37601			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.front.png",
37602			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.left.png",
37603			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.perspective.png",
37604			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.right.png",
37605			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.top.png"
37606		},
37607		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/c0/c0f1132733e116371860ad3e8bf2e7005d0853b8_01.perspective.png"],
37608		"type": "Door",
37609		"aperture": "rectangle",
37610		"configuration": [
37611			_obj.width,
37612			_obj.height,
37613			_obj.depth,
37614			_obj.open_side,
37615			_obj.open,
37616			_obj.finish
37617		]
37618	},
37619	//Patio door
37620	"ed4398ca15141c396325b0903ac23fbacc960479": {
37621		"name": gettext("Patio Door"),
37622		"level": -1,
37623		"renders": {
37624			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.back.png",
37625			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.bottom.png",
37626			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.front.png",
37627			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.png",
37628			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.perspective.png",
37629			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.right.png",
37630			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.top.png"
37631		},
37632		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/77/7713d0f323bc68e2be45e287dcd8bf28d6568bec_04.perspective.png"],
37633		"type": "Door",
37634		"aperture": "rectangle",
37635		"configuration": [
37636			_obj.width,
37637			_obj.height,
37638			_obj.depth,
37639			_obj.open_side,
37640			_obj.open,
37641			_obj.finish
37642		]
37643        },
37644	"5dd92a4fd20cc337ef9cd009d620ef39ac051e54": {
37645		"name": gettext("Folding Door"),
37646		"level": -1,
37647		"renders": {
37648			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a6
3764873e4ad12f17a08f1f2304e5dc47e95ab2d6_04.back.png",
37649			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.bottom.png",
37650			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.front.png",
37651			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.png",
37652			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.perspective.png",
37653			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.right.png",
37654			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.top.png"
37655		},
37656		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/3e/3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6_04.perspective.png"],
37657		"type": "Door",
37658		"aperture": "rectangle",
37659		"configuration": [
37660			_obj.width,
37661			_obj.height,
37662			_obj.depth,
37663			_obj.open_side,
37664			_obj.open,
37665			_obj.finish,
37666			{
37667				"name":"folds",
37668				"text":gettext("Folds Across"),
37669				"type":"count",
37670				"detail":0,
37671				"default":2,
37672				"minimum":1,
37673				"maximum":6
37674			}
37675		]
37676        },
37677	//door
37678	"fa310873ea1f5a6816728bfc50d96dd0d1c619ec": {
37679		"name": gettext("Glazed Double French"),
37680		"level": -1,
37681		"renders": {
37682			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.back.png",
37683			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.bottom.png",
37684			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.front.png",
37685			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.png",
37686			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.perspective.png",
37687			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.right.png",
37688			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.top.png"
37689		},
37690		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/79/79cbd33997154d30c4d7057a73f403dbbf673a89_06.perspective.png"],
37691		"type": "Door",
37692		"aperture": "rectangle",
37693		"configuration": [
37694			_obj.width,
37695			_obj.height,
37696			_obj.depth,
37697			_obj.open_side,
37698			_obj.open,
37699			_obj.finish
37700		]
37701        },
37702	//door
37703	"1d07f9d6ba47a92d3bdb428f656ead081851f22f": {
37704		"name": gettext("Glazed Panel Door"),
37705		"level": -1,
37706		"renders": {
37707			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.back.png",
37708			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.bottom.png",
37709			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.front.png",
37710			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.png",
37711			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.perspective.png",
37712			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.right.png",
37713			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.top.png"
37714		},
37715		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.perspective.png"],
37716		"type": "Door",
37717		"aperture": "rectangle",
37718		"configuration": [
37719			_obj.width,
37720			_obj.height,
37721			_obj.depth,
37722			_obj.open_side,
37723			_obj.open_direction,
37724			_obj.open,
37725			_obj.finish,
37726			_obj.panel_h,
37727			_obj.panel_v
37728		]
37729        },
37730	//door
37731	"0143d8e0e3c2de799c8661d5d97130bd248af342": {
37732		"name": gettext("Glazed Door"),
37733		"level": -1,
37734		"renders": {
37735			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.back.png",
37736			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.bottom.png",
37737			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.front.png",
37738			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.png",
37739			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.perspective.png",
37740			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.right.png",
37741			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.top.png"
37742		},
37743		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/93/93c95ea3540158b52ae6ec3c5af682d1ccb57867_05.perspective.png"],
37744		"type": "Door",
37745		"aperture": "rectangle",
37746		"configuration": [
37747			_obj.width,
37748			_obj.height,
37749			_obj.depth,
37750			_obj.open_side,
37751			_obj.open_direction,
37752			_obj.open,
37753			_obj.finish,
37754			$.extend({}, _obj.panel_h,
37755				{
37756					"default":2,
37757					"maximum":5
37758				}
37759			),
37760			$.extend({}, _obj.panel_v,
37761				{
37762					"default":2,
37763					"maximum":5
37764				}
37765			)
37766		]
37767        },
37768	//door:FIXME: have no renders becuase of error on qa/prod
37769	"56e44065a5645a008497882a7901c06811fe6298": {
37770		"name": gettext("Opening"),
37771		"level": -1,
37772		"renders": {
37773			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c766
377735538bf922e83a3_04.back.png",
37774			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.bottom.png",
37775            "front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37776			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.left.png",
37777			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.perspective.png",
37778			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.right.png",
37779			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.top.png"
37780		},
37781		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/83/83282e79a02ebfa3c4aa50c7665538bf922e83a3_04.perspective.png"],
37782		"type": "Door",
37783		"aperture": "rectangle",
37784		"configuration": [
37785			_obj.width,
37786			_obj.height,
37787			_obj.depth,
37788			_obj.finish
37789		]
37790	},
37791    // rectangular opening
37792    // NB! using renders and render_path of a different item
37793    "482e0a08d4a4666fdad20c0bc8194bbebf223077": {
37794        name: gettext("Rectangular Opening"),
37795        level: -1,
37796        type: "Door",
37797        aperture: "rectangle",
37798		"renders": {
37799            "front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37800            "top": "https://fp-media-img-cdn.floorplanner.com/cdb/scaled/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.top.png"
37801		},
37802		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png"],
37803		"configuration": [
37804			_obj.width,
37805		    {"name": "height",
37806			 "text": gettext("Height"),
37807			 "type": "length",
37808			 "detail": 0,
37809			 "default": 180,
37810			 "minimum": 20,
37811			 "maximum": 500
37812			},
37813			_obj.depth,
37814			_obj.finish
37815		]
37816    },
37817
37818    // circular opening
37819    // NB! using renders and render_path of a different item
37820    "22b0042cf9b5710f2fc20a407af244e3e8a98147": {
37821        name: gettext("Round Opening"),
37822        level: -1,
37823        type: "Door",
37824        aperture: "round",
37825		"renders": {
37826            "front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37827            "top": "https://fp-media-img-cdn.floorplanner.com/cdb/scaled/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.top.png"
37828		},
37829		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png"],
37830		"configuration": [
37831			_obj.width,
37832			_obj.height,
37833			_obj.depth,
37834			_obj.finish
37835		]
37836    },
37837
37838    // arched opening
37839    // NB! using renders and render_path of a different item
37840    "1b4768a1cb1ffcacdc3a37de6eb3678a125ae34f": {
37841        name: gettext("Arched Opening"),
37842        level: -1,
37843        type: "Door",
37844        aperture: "arched",
37845		"renders": {
37846            "front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37847			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/scaled/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.top.png"
37848		},
37849		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png"],
37850		"configuration": [
37851			_obj.width,
37852			_obj.height,
37853			_obj.depth,
37854			_obj.finish
37855		]
37856    },
37857
37858    // half-round opening
37859    // NB! using renders and render_path of a different item
37860    "14f0695b5b699d2cde9ac56b72d064c714e440a9": {
37861        name: gettext("Half Round Opening"),
37862        level: -1,
37863        type: "Door",
37864        aperture: "half-round",
37865		"renders": {
37866            "front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37867			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/scaled/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.top.png"
37868		},
37869		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png"],
37870		"configuration": [
37871			_obj.width,
37872			_obj.height,
37873			_obj.depth,
37874			_obj.finish
37875		]
37876    },
37877
37878	//Sash window
37879	"3dfe203d609c4e61ed5d5ed34144421a176cfeed": {
37880		"name": gettext("Sash Window"),
37881		"level": -1,
37882		"renders": {
37883			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.back.png",
37884			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.bottom.png",
37885			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.front.png",
37886			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.left.png",
37887			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.perspective.png",
37888			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_034.right.png",
37889			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.top.png"
37890		},
37891		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/95/9569fbcfae9444412504bbebfc2cc988f031ea99_03.perspective.png"],
37892		"type": "Window",
37893		"aperture": "rectangle",
37894		"configuration": [
37895			_obj.width,
37896			_obj.height,
37897			_obj.depth,
37898			_obj.open,
37899			_obj.finish
37900		]
37901	},
37902	//Picture window
37903	"23f3f34205d4709811052a6709914697600302c6": {
37904		"name": gettext("Picture Window"),
37905		"level": -1,
37906		"renders": {
37907			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.back.png",
37908			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.bottom.png",
37909			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.front.png",
37910			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_034.left.png",
37911			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.perspective.png",
37912			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.right.png",
37913			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.top.png"
37914		},
37915		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/29/2968716ba07d5794311e0058b68e7c7f4ec6bfe9_03.perspective.png"],
37916		"type": "Window",
37917		"aperture": "rectangle",
37918		"configuration": [
37919			_obj.width,
37920			_obj.height,
37921			_obj.depth,
37922			_obj.finish
37923		]
37924	},
37925	//Case window
37926	"6d48b971d09f830198aad21b2b9fcb2c5b9d36af": {
37927		"name": gettext("Casement Window"),
37928		"level": -1,
37929		"renders": {
37930			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.back.png",
37931			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.bottom.png",
37932			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.front.png",
37933			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.left.png",
37934			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.perspective.png",
37935			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.right.png",
37936			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.top.png"
37937		},
37938		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/9f/9ff3d15ce477fa98bb0f64d29df5042648ebfc0a_03.perspective.png"],
37939		"type": "Window",
37940		"aperture": "rectangle",
37941		"configuration": [
37942			_obj.width,
37943			_obj.height,
37944			_obj.depth,
37945			_obj.open_side,
37946			_obj.open_direction,
37947			_obj.open,
37948			_obj.finish
37949		]
37950	},
37951	//Arched window
37952	"6c705d137661ddf3de8ce3e159cc2446562912cf": {
37953		"name": gettext("Arched Window"),
37954		"level": -1,
37955		"renders": {
37956			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.back.png",
37957			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.bottom.png",
37958			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.front.png",
37959			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.left.png",
37960			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.perspective.png",
37961			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.right.png",
37962			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.top.png"
37963		},
37964		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/91/91ebc7872da6ff77157f26b77a4bbdace2c93bb4_00.perspective.png"],
37965		"type": "Window",
37966		"aperture": "arched",
37967		"configuration": [
37968			_obj.width,
37969			_obj.height,
37970			_obj.depth,
37971			_obj.open_side,
37972			_obj.open_direction,
37973			_obj.open,
37974			_obj.finish
37975		]
37976        },
37977	//Half-round window
37978	"b83b277ce9f79359fe2fef82c8ab7e1f7f83495b": {
37979		"name": gettext("Half Round Window"),
37980		"level": -1,
37981		"renders": {
37982			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.back.png",
37983			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.bottom.png",
37984			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.front.png",
37985			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.left.png",
37986			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.perspective.png",
37987			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.right.png",
37988			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.top.png"
37989		},
37990		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/42/42919437e4daa8c94e51acdf9bfbd76080d323bd_00.perspective.png"],
37991		"type": "Window",
37992		"aperture": "half-round",
37993		"configuration": [
37994			_obj.width,
37995			_obj.height,
37996			_obj.depth,
37997			_obj.finish
37998		]
37999        },
38000	//Round window
38001	"2bf5ae66583be1729caabb74f64f44fe5057c0df": {
38002		"name": gettext("Round Window"),
38003		"level": -1,
38004		"renders": {
38005			"back": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e764
380056a74fcfc81695a682db7a60c2577a6e8_00.back.png",
38006			"bottom": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.bottom.png",
38007			"front": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.front.png",
38008			"left": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.left.png",
38009			"perspective": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.perspective.png",
38010			"right": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.right.png",
38011			"top": "https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.top.png"
38012		},
38013		"render_path": ["https://fp-media-img-cdn.floorplanner.com/cdb/renders/5f/5ff1e7646a74fcfc81695a682db7a60c2577a6e8_00.perspective.png"],
38014		"type": "Window",
38015		"aperture": "round",
38016		"configuration": [
38017			_obj.width,
38018			_obj.height,
38019			_obj.depth,
38020			_obj.finish
38021		]
38022        },
38023	//molding
38024	"c903ee5c4d0d7c194964a77e8412bcbdb2fba8e6": {
38025		"name": "Molding",
38026		"level": -1,
38027		"renders": {
38028			"back": "",
38029			"bottom": "",
38030			"front": "",
38031			"left": "",
38032			"perspective": "",
38033			"right": "",
38034			"top": ""
38035		},
38036		"render_path": [],
38037		"type": "Window",
38038		"aperture": "rectangle",
38039		"configuration": [
38040			{
38041				"name":"width",
38042				"text":"Width",
38043				"type":"length",
38044				"detail":0,
38045				"default":100,
38046				"minimum":0,
38047				"maximum":10000
38048			},
38049			{
38050				"name":"cut_angle_l",
38051				"text":"Left cut angle",
38052				"type":"angle",
38053				"detail":99,
38054				"default":90,
38055				"minimum":0,
38056				"maximum":180
38057			},
38058			{
38059				"name":"cut_angle_r",
38060				"text":"Right cut angle",
38061				"type":"angle",
38062				"detail":99,
38063				"default":90,
38064				"minimum":0,
38065				"maximum":180
38066			},
38067			{
38068				"name":"profile",
38069				"text":"Profile",
38070				"type":"enumeration",
38071				"detail":0,
38072				"default":"simple",
38073				"values":[["cove","Cove"], ["ogee", "Ogee"]]
38074			},
38075			{
38076				"name":"finish",
38077				"text":"Colour",
38078				"type":"material",
38079				"detail":0,
38080				"default":"white"
38081			}
38082		]
38083        },
38084	//molding
38085	"86403415a0c5756ba1f07997805256a1a88b8268": {
38086		"name": "Picture Rail",
38087		"level": -1,
38088		"renders": {
38089			"back": "",
38090			"bottom": "",
38091			"front": "",
38092			"left": "",
38093			"perspective": "",
38094			"right": "",
38095			"top": ""
38096		},
38097		"render_path": [],
38098		"type": "Molding",
38099		"aperture": "rectangle",
38100		"configuration": [
38101			{
38102				"name":"width",
38103				"text":"Width",
38104				"type":"length",
38105				"detail":0,
38106				"default":100,
38107				"minimum":0,
38108				"maximum":10000
38109			},
38110			{
38111				"name":"cut_angle_l",
38112				"text":"Left cut angle",
38113				"type":"angle",
38114				"detail":99,
38115				"default":90,
38116				"minimum":0,
38117				"maximum":180
38118			},
38119			{
38120				"name":"cut_angle_r",
38121				"text":"Right cut angle",
38122				"type":"angle",
38123				"detail":99,
38124				"default":90,
38125				"minimum":0,
38126				"maximum":180
38127			},
38128			{
38129				"name":"finish",
38130				"text":"Colour",
38131				"type":"material",
38132				"detail":0,
38133				"default":"white"
38134			}
38135		]
38136        },
38137	//molding
38138	"9da8ad612b335b4a8dbcb8c2e4b58bd68b2eb4b2": {
38139		"name": "Dado Rail",
38140		"level": -1,
38141		"renders": {
38142			"back": "",
38143			"bottom": "",
38144			"front": "",
38145			"left": "",
38146			"perspective": "",
38147			"right": "",
38148			"top": ""
38149		},
38150		"render_path": [],
38151		"type": "MoldingS",
38152		"aperture": "rectangle",
38153		"configuration": [
38154			{
38155				"name":"width",
38156				"text":"Width",
38157				"type":"length",
38158				"detail":0,
38159				"default":100,
38160				"minimum":0,
38161				"maximum":10000
38162			},
38163			{
38164				"name":"cut_angle_l",
38165				"text":"Left cut angle",
38166				"type":"angle",
38167				"detail":99,
38168				"default":90,
38169				"minimum":0,
38170				"maximum":180
38171			},
38172			{
38173				"name":"cut_angle_r",
38174				"text":"Right cut angle",
38175				"type":"angle",
38176				"detail":99,
38177				"default":90,
38178				"minimum":0,
38179				"maximum":180
38180			},
38181			{
38182				"name":"finish",
38183				"text":"Colour",
38184				"type":"material",
38185				"detail":0,
38186				"default":"white"
38187			}
38188		]
38189        },
38190	//molding
38191	"b8aa8b9573ed536e0bbbc820255f46ea07b67933": {
38192		"name": "Skirting Board",
38193		"level": -1,
38194		"renders": {
38195			"back": "",
38196			"bottom": "",
38197			"front": "",
38198			"left": "",
38199			"perspective": "",
38200			"right": "",
38201			"top": ""
38202		},
38203		"render_path": [],
38204		"type": "Molding",
38205		"aperture": "rectangle",
38206		"configuration": [
38207			{
38208				"name":"width",
38209				"text":"Width",
38210				"type":"length",
38211				"detail":0,
38212				"default":100,
38213				"minimum":0,
38214				"maximum":10000
38215			},
38216			{
38217				"name":"cut_angle_l",
38218				"text":"Left cut angle",
38219				"type":"angle",
38220				"detail":99,
38221				"default":90,
38222				"minimum":0,
38223				"maximum":180
38224			},
38225			{
38226				"name":"cut_angle_r",
38227				"text":"Right cut angle",
38228				"type":"angle",
38229				"detail":99,
38230				"default":90,
38231				"minimum":0,
38232				"maximum":180
38233			},
38234			{
38235				"name":"profile",
38236				"text":"Profile",
38237				"type":"enumeration",
38238				"detail":0,
38239				"default":"square",
38240				"values":[["square","Square Edge"], ["bevelled", "Bevelled"], ["bullnose", "Bullnose"], ["victorian", "Victorian"]]
38241			},
38242			{
38243				"name":"finish",
38244				"text":"Colour",
38245				"type":"material",
38246				"detail":0,
38247				"default":"white"
38248			}
38249		]
38250        }
38251};
38252})();
38253/*globals */
38254/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
38255          sloppy: true, white: true */
38256// http://mydeco.com/rooms/planner/
38257var demoRoom1 =
38258{
38259    "category": "",
38260    "view_count": 0,
38261    "original_image": null,
38262    "last_updated": "2009-07-23 16:22:27",
38263    "name": "A Room",
38264    "author": "mydeco",
38265    "rating": "0",
38266    "image": "http://cdn.mydeco.com/media/data/rooms/images/408759.jpeg",
38267    "children": [],
38268    "background_image": null,
38269    "original_author": null,
38270    // original value is 408759
38271    "id": 0,
38272    "thumbnail": "http://cdn.mydeco.com/media/data/rooms/images/_thumbs/408759.jpeg_169x127_q85.jpg",
38273    "comment_count": 0,
38274    "products": ["0108bfd5b4a7971afc69ad6d97c75e3ff0fbb5a8", "000c3ff0e44a2ace51095c7084bc6f7243e5d35e", "2c87ac734f537b095acb386a264a0f16238994f8", "4571adbcf5fbe5cd8261b0f0025407b3c4e46cd0", "5b0eed690b9d285448591c6becd8e2a52cf69aeb", "743129b2201d36c493818831460f561b44237e33", "752ff2646297f0c52fdf3925b6e5ba93e0724085", "c1b1f9d7ee04fe16e4c786a0180b8e63f6caa4a8", "cf2e4cf4cc979fc09e88b6842f28867de707f91a", "cf5abb1ea271b0ac4c7789f19a853eb13072470a"],
38275    "model": {
38276        "geometry": {
38277            "points": [[-245.02500000000001, 255.75, 0], [335.6875, 255.77500000000001, 0], [335.6875, 255.77500000000001, 310], [-245.02500000000001, 255.75, 310], [-117.98674984868759, 255.75546906785999, 77.087000000000003], [-30.550749929712353, 255.75923323687303, 77.087000000000003], [-30.550749929712353, 255.75923323687303, 232.91300000000001], [-117.98674984868759, 255.75546906785999, 232.91300000000001], [104.81324994451236, 255.76506073349293, 77.087000000000003], [192.24924986348762, 255.76882490250597, 77.087000000000003], [192.24924986348762, 255.76882490250597, 232.91300000000001], [104.81324994451236, 255.76506073349293, 232.91300000000001], [335.6875, 255.77500000000001, 0], [335.71249999999998, -277.27499999999998, 0], [335.71249999999998, -277.27499999999998, 310], [335.6875, 255.77500000000001, 310], [335.71249999999998, -277.27499999999998, 0], [-245, -277.30000000000001, 0], [-245, -277.30000000000001, 310], [335.71249999999998, -277.27499999999998, 310], [-245, -277.30000000000001, 0], [-245.02500000000001, 255.75, 0], [-245.02500000000001, 255.75, 310], [-245, -277.30000000000001, 310], [-244.99793630991635, 141.79800004839342, 1], [-245.00206369008257, 229.80199995160655, 1], [-245.00206369008257, 229.80199995160655, 207], [-244.99793630991635, 141.79800004839342, 207], [-244.99794962949289, -55.617999951918897, 77.087000000000003], [-245.00205037050708, 31.817999951918907, 77.087000000000003], [-245.00205037050708, 31.817999951918907, 232.91300000000001], [-244.99794962949289, -55.617999951918897, 232.91300000000001], [-245.02500000000001, 255.75, 0], [-245, -277.30000000000001, 0], [335.71249999999998, -277.27499999999998, 0], [335.6875, 255.77500000000001, 0], [-245.02500000000001, 255.75, 0], [-245.02500000000001, 255.75, 310], [335.6875, 255.77500000000001, 310], [335.71249999999998, -277.27499999999998, 310], [-245, -277.30000000000001, 310], [-245.02500000000001, 255.75, 310]],
38278            "polygons": [{
38279                "indices": [0, 1, 2, 3, 0, 4, 5, 6, 7, 4, 8, 9, 10, 11],
38280                "material": "materialp578",
38281                "ID": "wall0"
38282            },
38283            {
38284                "indices": [12, 13, 14, 15],
38285                "material": "materialp233",
38286                "ID": "wall1"
38287            },
38288            {
38289                "indices": [16, 17, 18, 19],
38290                "material": "materialp578",
38291                "ID": "wall2"
38292            },
38293            {
38294                "indices": [20, 21, 22, 23, 20, 24, 25, 26, 27, 24, 28, 29, 30, 31],
38295                "material": "materialp578",
38296                "ID": "wall3"
38297            },
38298            {
38299                "indices": [32, 33, 34, 35, 36],
38300                "material": "material105",
38301                "ID": "Floor"
38302            },
38303            {
38304                "indices": [37, 38, 39, 40, 41],
38305                "material": "materialpsdpfbf7ee",
38306                "ID": "Ceiling"
38307            }]
38308        },
38309        "camera": {
38310            "position": [190.44864468037053, -70.11369249303513, 112.833061446],
38311            "focal_point": [-88.03980515482976, 85.10937790691267, 112.833061446]
38312        },
38313        "lights": [{
38314            "position": [10, 0, 500],
38315            "direction": [0, 0, -500],
38316            "type": "sun"
38317        }],
38318        "materials": [{
38319            "shader": "combi_material",
38320            "diffuse": [1, 0.97999999999999998, 0.96799999999999997],
38321            "use_diffuse_map": false,
38322            "ambient": [0, 0, 0],
38323            "shiny": 0,
38324            "ID": "materialp578"
38325        },
38326        {
38327            "shader": "combi_material",
38328            "diffuse": [0.98399999999999999, 0.84699999999999998, 0.439],
38329            "use_diffuse_map": false,
38330            "ambient": [0, 0, 0],
38331            "shiny": 0,
38332            "ID": "materialp233"
38333        },
38334        {
38335            "use_ambientmap": "on",
38336            "diffuse_texture": "floor_and_wall/original/T_al_Ukr0008WideBoardAsClas.jpg",
38337            "ambient_texture": "floor_and_wall/original/T_al_Ukr0008WideBoardAsClas.jpg",
38338            "shader": "combi_material",
38339            "diffuse": [1, 1, 1],
38340            "texture_width": 150,
38341            "ambient": [0.050000000000000003, 0.050000000000000003, 0.050000000000000003],
38342            "use_diffusemap": "on",
38343            "shiny": 0.20000000000000001,
38344            "ID": "material105"
38345        },
38346        {
38347            "shader": "combi_material",
38348            "diffuse": [0.98399999999999999, 0.96899999999999997, 0.93300000000000005],
38349            "use_diffuse_map": false,
38350            "ambient": [0, 0, 0],
38351            "shiny": 0,
38352            "ID": "materialpsdpfbf7ee"
38353        }],
38354        "room_id": 0,
38355        "components": [{
38356            "category": null,
38357            "component_id": "000c3ff0e44a2ace51095c7084bc6f7243e5d35e",
38358            "hingeSide": null,
38359            "ID": "4",
38360            "bounding_box": [
38361                [-73.650000000000006, - 99.25, 0.0],
38362                [73.650000000000006, 99.25, 119.0]
38363            ],
38364			"parentID": null,
38365            "position": [209.09999999999999, -25.550000000000001, 0],
38366            "model": "models/00/000c3ff0e44a2ace51095c7084bc6f7243e5d35e_03.mi",
38367            "type": "Component",
38368            "rotation": [0, 0, 89.908747671100002],
38369            "openDirection": null
38370        },
38371        {
38372            "category": null,
38373            "component_id": "2c87ac734f537b095acb386a264a0f16238994f8",
38374            "hingeSide": null,
38375            "ID": "5",
38376            "bounding_box": [[-74.703000000000003, 25.449000000000002, 0], [74.703000000000003, -25.449000000000002, 62.792000000000002]],
38377            "parentID": null,
38378            "position": [303.25, 156.44999999999999, 0],
38379            "model": "models/2c/2c87ac734f537b095acb386a264a0f16238994f8_01.mi",
38380            "type": "Component",
38381            "rotation": [0, 0, 89.908747671100002],
38382            "openDirection": null
38383        },
38384        {
38385            "category": null,
38386            "component_id": "4571adbcf5fbe5cd8261b0f0025407b3c4e46cd0",
38387            "hingeSide": null,
38388            "ID": "6",
38389            "bounding_box": [[-35, 35, 0], [35, -35, 70]],
38390            "parentID": null,
38391            "position": [293.69999999999999, 185.30000000000001, 63],
38392            "model": "models/45/4571adbcf5fbe5cd8261b0f0025407b3c4e46cd0_02.mi",
38393            "type": "Component",
38394            "rotation": [0, 0, 89.908747671078501],
38395            "openDirection": null
38396        },
38397        {
38398            "category": "Door",
38399            "component_id": "e79b4c034a30d52dac5d8b2cd460a2dddf711874",
38400            "hingeSide": "right",
38401            "ID": "8",
38402            "bounding_box": [[-45.009, 103.952, -14.529], [44.994999999999997, -104.048, 1.2330000000000001]],
38403            "parentID": "3",
38404            "position": [-245, 185.80000000000001, 104],
38405            "model": "models/e7/e79b4c034a30d52dac5d8b2cd460a2dddf711874_02.mi",
38406            "type": "ArchitecturalComponent",
38407            "rotation": [-89.954373835539243, 0, 269.99731283300002],
38408            "openDirection": "in"
38409        },
38410        {
38411            "category": null,
38412            "component_id": "5b0eed690b9d285448591c6becd8e2a52cf69aeb",
38413            "hingeSide": null,
38414            "ID": "9",
38415            "bounding_box": [[-17, 17, 0], [17, -17, 5]],
38416            "parentID": null,
38417            "position": [306.77692307699999, 119.97307692299999, 63],
38418            "model": "models/5b/5b0eed690b9d285448591c6becd8e2a52cf69aeb_02.mi",
38419            "type": "Component",
38420            "rotation": [0, 0, 0],
38421            "openDirection": null
38422        },
38423        {
38424            "category": null,
38425            "component_id": "cf2e4cf4cc979fc09e88b6842f28867de707f91a",
38426            "hingeSide": null,
38427            "ID": "10",
38428            "bounding_box": [[-11.089, 11.109, 0.050000000000000003], [11.31, -11.291, 33.049999999999997]],
38429            "parentID": null,
38430            "position": [306.89999999999998, 119.7, 63],
38431            "model": "models/cf/cf2e4cf4cc979fc09e88b6842f28867de707f91a_02.mi",
38432            "type": "Component",
38433            "rotation": [0, 0, 0],
38434            "openDirection": null
38435        },
38436        {
38437            "category": "Window",
38438            "component_id": "97e2f2b80d14b085d626c9a3c1b77a8a86f557b7",
38439            "hingeSide": null,
38440            "ID": "12",
38441            "bounding_box": [[-44.865000000000002, 78.853999999999999, -6.2859999999999996], [44.570999999999998, -78.971999999999994, 0.84099999999999997]],
38442            "parentID": "3",
38443            "position": [-245, -11.9, 155],
38444            "model": "models/97/97e2f2b80d14b085d626c9a3c1b77a8a86f557b7_02.mi",
38445            "type": "ArchitecturalComponent",
38446            "rotation": [-89.954373835539243, 0, 269.99731283300002],
38447            "openDirection": null
38448        },
38449        {
38450            "category": "Window",
38451            "component_id": "97e2f2b80d14b085d626c9a3c1b77a8a86f557b7",
38452            "hingeSide": null,
38453            "ID": "13",
38454            "bounding_box": [[-44.865000000000002, 78.853999999999999, -6.2859999999999996], [44.570999999999998, -78.971999999999994, 0.84099999999999997]],
38455            "parentID": "0",
38456            "position": [-74.268749889199995, 255.757351153, 155],
38457            "model": "models/97/97e2f2b80d14b085d626c9a3c1b77a8a86f557b7_02.mi",
38458            "type": "ArchitecturalComponent",
38459            "rotation": [-89.954373835539243, 0, 359.99753338400001],
38460            "openDirection": null
38461        },
38462        {
38463            "category": "Window",
38464            "component_id": "97e2f2b80d14b085d626c9a3c1b77a8a86f557b7",
38465            "hingeSide": null,
38466            "ID": "14",
38467            "bounding_box": [[-44.865000000000002, 78.853999999999999, -6.2859999999999996], [44.570999999999998, -78.971999999999994, 0.84099999999999997]],
38468            "parentID": "0",
38469            "position": [148.53124990399999, 255.76694281799999, 155],
38470            "model": "models/97/97e2f2b80d14b085d626c9a3c1b77a8a86f557b7_02.mi",
38471            "type": "ArchitecturalComponent",
38472            "rotation": [-89.954373835539243, 0, 359.99753338400001],
38473            "openDirection": null
38474        },
38475        {
38476            "category": null,
38477            "component_id": "752ff2646297f0c52fdf3925b6e5ba93e0724085",
38478            "hingeSide": null,
38479            "ID": "15",
38480            "bounding_box": [[-35, 35, 0], [35, -35, 0.20000000000000001]],
38481            "parentID": "1",
38482            "position": [335.69999999999999, -19.300000000000001, 155],
38483            "model": "models/75/752ff2646297f0c52fdf3925b6e5ba93e0724085_01.mi",
38484            "type": "Component",
38485            "rotation": [-89.954373835539243, 0, 89.997312832800006],
38486            "openDirection": null
38487        },
38488        {
38489            "category": null,
38490            "component_id": "c1b1f9d7ee04fe16e4c786a0180b8e63f6caa4a8",
38491            "hingeSide": null,
38492            "ID": "16",
38493            "bounding_box": [[-37.5, 82.5, 0], [37.5, -82.5, 1.3999999999999999]],
38494            "parentID": "2",
38495            "position": [42.8812500023, -277.28760655000002, 155],
38496            "model": "models/c1/c1b1f9d7ee04fe16e4c786a0180b8e63f6caa4a8_03.mi",
38497            "type": "Component",
38498            "rotation": [-89.954373835539243, 0, 179.99753338400001],
38499            "openDirection": null
38500        },
38501        {
38502            "category": null,
38503            "component_id": "cf5abb1ea271b0ac4c7789f19a853eb13072470a",
38504            "hingeSide": null,
38505            "ID": "17",
38506            "bounding_box": [[-35.317999999999998, 10.946, -0.126], [35.432000000000002, -9.1509999999999998, 59.695]],
38507            "parentID": null,
38508            "position": [41.299999999999997, -11.5, 36],
38509            "model": "models/cf/cf5abb1ea271b0ac4c7789f19a853eb13072470a_02.mi",
38510            "type": "Component",
38511            "rotation": [0, 0, 269.81749534199997],
38512            "openDirection": null
38513        },
38514        {
38515            "category": null,
38516            "component_id": "743129b2201d36c493818831460f561b44237e33",
38517            "hingeSide": null,
38518            "ID": "18",
38519            "bounding_box": [[-90, 28, 0], [90, -28, 36]],
38520            "parentID": null,
38521            "position": [41.299999999999997, -18.449999999999999, 0],
38522            "model": "models/74/743129b2201d36c493818831460f561b44237e33_03.mi",
38523            "type": "Component",
38524            "rotation": [0, 0, -90],
38525            "openDirection": null
38526        },
38527        {
38528            "category": null,
38529            "component_id": "0108bfd5b4a7971afc69ad6d97c75e3ff0fbb5a8",
38530            "hingeSide": null,
38531            "ID": "19",
38532            "bounding_box": [[-69, 31.305, 0], [69, -31.305, 220]],
38533            "parentID": null,
38534            "position": [230.75, -236.90000000000001, 0],
38535            "model": "models/01/0108bfd5b4a7971afc69ad6d97c75e3ff0fbb5a8_05.mi",
38536            "type": "Component",
38537            "rotation": [0, 0, 179.86312150699999],
38538            "openDirection": null
38539        }]
38540    },
38541    "type": "3d",
38542    "public": 0,
38543    "description": ""
38544};
38545
38546// This is the default room model used when
38547// opening roomstyler.com/3dplanner (used in _view_room_management.erb)
38548var emptyRoom =
38549{
38550	"category": "",
38551	"original_image": null,
38552	"name": "",
38553	"rating": "0",
38554	"children": [],
38555	"background_image": null,
38556	"author": null,
38557	"original_author": null,
38558	"comment_count": 0,
38559	"products": [],
38560    "image": "",
38561    "thumbnail": "",
38562	"model": {
38563		"camera": {
38564			"position": [0, 0, 112.833061446],
38565			"focal_point": [-278.48980515482975, 155.21937790691265, 112.833061446]
38566		},
38567		"lights": [{
38568			"position": [10, 0, 500],
38569			"direction": [0, 0, - 500],
38570			"type": "sun"
38571		}],
38572		"materials": [{
38573			"shader": "combi_material",
38574			"diffuse": [1, 0.98, 0.968],
38575			"use_diffuse_map": false,
38576			"ambient": [0, 0, 0],
38577			"shiny": 0,
38578			"ID": "default"
38579		}],
38580		"room_id": 0,
38581		"components": [],
38582		"plan": {
38583			"height": 310,
38584			"points": [],
38585			"areas": {},
38586			"walls": {}
38587		},
38588		"version": 2
38589	},
38590	"type": "3d",
38591	"public": 0,
38592	"description": ""
38593};
38594/*globals myDeco, jQuery, window */
38595/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
38596          sloppy: true, white: true */
38597
38598/**
38599 * @fileOverview
38600 * Units selector jquery plugin, special for austin, uses mydeco namespace a lot
38601 */
38602jQuery(function() {
38603	'use strict';
38604	
38605	var $ = jQuery,
38606		units = $.cookie('units'),
38607		updateUnits = function(holder) {
38608			holder
38609				.find('.unit').text(units).end()
38610				.find('.button')
38611					.removeClass('active')
38612					.filter('.' + units)
38613					.addClass('active');
38614		};
38615		
38616	// if valid units set in cookie then override default setting
38617	// otherwise use default
38618	if (units && myDeco.settings.UNITS_LIST[units]) {
38619		myDeco.settings.UNITS = units;
38620	} else {
38621		units = myDeco.settings.UNITS;
38622	}
38623
38624	$.fn.unitsSelector = function() {
38625		this.each(function() {
38626			var unit,
38627				holder = $(this).append($('<span class="unit">').text(units)),
38628				options = $('<span class="holder" />').appendTo(holder);
38629			
38630			for (unit in myDeco.settings.UNITS_LIST) {
38631				var name = myDeco.settings.UNITS_LIST[unit][1] === 'Feet' ? 'ft' : 'mtr';
38632
38633					options.append($('<a class="button" href="" />')
38634						.addClass(unit)
38635						.toggleClass('active', unit === units)
38636						.append($('<span class="u" />')
38637							.data('unit', unit)
38638							.text(holder.hasClass('fullname') ?
38639								name : unit)));
38640			}
38641			
38642			myDeco.body.bind('unitsChanged', function() {
38643				updateUnits(holder);
38644			});
38645		})
38646		.click(function(e) {
38647			e.stop();
38648			
38649			var unit = $(e.target),
38650				holder;
38651			
38652			if (unit.hasClass('u')) {
38653				
38654				holder = unit.closest('.units');
38655				units = myDeco.settings.UNITS = unit.data('unit');
38656				
38657				$.cookie('units', units, {
38658					expires: 365,
38659					domain: window.location.hostname,
38660					path: window.location.pathname
38661				});
38662				
38663				myDeco.body.trigger('unitsChanged', [units]);
38664			}
38665		});
38666		return this;
38667	};
38668});
38669/*globals $, myDeco, AppView, window, buildThumbnailLink, tmpl */
38670/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
38671          sloppy: true, white: true */
38672/**
38673 * @fileOverview
38674 * Build view
38675 */
38676var BuildView = AppView.extend({
38677	
38678	/**
38679	 * View type
38680	 * @type {String}
38681	 * @public
38682	 * @static
38683	 */
38684	$type: 'build',
38685	
38686	/**
38687	 * Settings
38688	 * @type {Object}
38689	 */
38690	settings: {
38691		// available
38692		types: ['doors', 'windows'],
38693		
38694		// prefix for images
38695		prefix: '/assets/austin/',
38696
38697		listingThumbnail: {width: 70, height: 70, maxWidth: 110, maxHeight: 110}
38698	},
38699	
38700	/**
38701	 * View init
38702	 * @param {Object} options
38703	 * @public
38704	 */
38705	init: function(options) {
38706		this._super(options);
38707		
38708		var self = this, ids = [], t, tlen, type,
38709			typesCreation = function(mtype, m) {
38710				ids.push(mtype.component_id);
38711
38712				return {
38713					type: mtype.type,
38714					name: mtype.name,
38715					sizes: $.map(mtype.sizes, function(size) {
38716						return {
38717							width: size[0],
38718							height: size[1],
38719							subtype: size[2]
38720						};
38721					})
38722				};
38723			},
38724			masking = this.canvasElement.find('.setscale a')
38725				.click(function(e) {
38726					e.stop();
38727					myDeco.body.trigger('setscaleStart');
38728				})
38729				.closest('.masking');
38730				
38731		this.canvasElement
38732			.find('.setscale a').click(function(e) {
38733				e.stop();
38734				myDeco.body.trigger('setscaleStart');
38735			}).end()
38736			.find('.adjusting').delegate('li', 'click', function(e) {
38737				e.stop();
38738				myDeco.body.trigger('backgroundChange',
38739					[$(this).attr('className'), e.ctrlKey ? 10 : e.shiftKey ? 0.1 : 1]);
38740			}).end()
38741			.find('.build-walls-container').html(
38742				tmpl('view-build-af-template', {
38743					type: 'shapes',
38744					types: $.keys(this._model.settings.shapes)
38745				})
38746			)
38747			.find('.own-walls a').click(function(e) {
38748				e.stop();
38749				myDeco.body.trigger('drawStart');
38750			});
38751		
38752		$('#view-units').find('.draw-walls').click(function(e) {
38753			e.stop();
38754			myDeco.body.trigger('drawStart');			
38755		});
38756		
38757		// windows and doors
38758		for (t = 0, tlen = this.settings.types.length; t < tlen; t++) {
38759			type = this.settings.types[t];
38760            var items = this._model.settings[type], custom_list;
38761            if (myDeco.settings.WL && (custom_list = myDeco.settings.WL[type.toUpperCase() + '_LIST'])) {
38762                items = $.grep(items, function(item) { return custom_list.indexOf(item.component_id) > -1 });
38763                this._model.settings[type] = items;
38764            }
38765			this.canvasElement.find('.build-' + type + '-container').html(
38766				tmpl('view-build-af-template', {
38767					type: type,
38768					types: $.map(items, typesCreation)
38769				})
38770			);
38771		}
38772
38773		// force loading components to local cache
38774		myDeco.App.ws.proxy.fetch('component', ids, $.noop, true);
38775		
38776		this.canvasElement.find('.build-variants').delegate('a', 'click',
38777			function(e) {
38778				e.preventDefault();
38779				
38780				var element = $(this),
38781					root = element.closest('.build-variants'),
38782					sections = root.children().removeClass('selected'),
38783					section = element.closest('li').addClass('selected'),
38784					i = sections.index(section.get(0));
38785				
38786				if (i === 0) {
38787					root.siblings('.dimensions').show().children('ul').show();
38788				} else {
38789					root.siblings('.dimensions').show().children('ul').hide()
38790						.eq(i - 1).show();
38791				}
38792			}
38793		);
38794		
38795		this.canvasElement.delegate('.dimensions a', {
38796			click: function(e) {
38797				e.stop();
38798			},
38799			mousedown: function(e) {
38800				e.preventDefault();
38801				
38802				var path = $(this).data('path').split(':'),
38803					fireEvent = function(data) {
38804						myDeco.body.trigger('entity-drag-started', [{
38805							entity: data,
38806							kind: 'element',
38807							visual: self.generateElementForDrag(data)
38808						}, e]);
38809					};
38810				
38811				switch (path[0]) {
38812					case 'doors':
38813					case 'windows':
38814						myDeco.App.ws.proxy.fetch('component',
38815							self._model.settings[path[0]][path[1]].component_id,
38816							function(data) {
38817								var size = self._model.settings[path[0]][path[1]].sizes[path[2]];
38818								data = $.values(data).shift();
38819								data.configuration.width = size[0];
38820								data.configuration.height = size[1];
38821								fireEvent(data);
38822							});
38823						break;
38824						
38825					default:
38826						fireEvent({
38827							type: 'room',
38828							subtype: path[1],
38829							width: self._model.settings.shapes[path[1]].width,
38830							depth: self._model.settings.shapes[path[1]].height
38831						});
38832				}
38833			}
38834		});
38835		
38836		myDeco.body
38837			.bind('view-' + this.$type + '-upload-response',
38838				this.uploadResponse.bind(this))
38839			.bind('view-' + this.$type + '-upload',
38840				this.showFloorUpload.bind(this))
38841			.bind('unitsChanged', function() {
38842				self.canvasElement.find('.component-dimensions')
38843					.each(function() {
38844						var element = $(this);
38845						element.html(element.data('dimensions')
38846							.split(':').toUnits());
38847					});
38848			})
38849			.bind('roomLoaded', function() {
38850				masking.toggle(self._model._masking);
38851			});
38852		
38853		this.canvasElement.find('.build-floor-container .button.upload')
38854			.click(function(e) {
38855				e.stop();
38856				myDeco.body.trigger('view-' + self.$type + '-upload');
38857			});
38858	},
38859
38860	/**
38861	 * Shows upload popin
38862	 * @param {Event} e
38863	 */
38864	showFloorUpload: function(e) {
38865		if (e) {
38866			e.stop();
38867		}
38868		
38869		var self = this;
38870		
38871		myDeco.user.loginRequired(function() {
38872			self._model.checkFloorPermissions(true, function() {
38873				myDeco.popin.open({
38874					url: '#floor-plan-popin',
38875					width: 595,
38876					afterOpen: function (content) {
38877						myDeco.fileInputs.init(content);
38878						myDeco.buttons.initSubmitButtons(
38879								content.find('input:submit'));
38880						content.find('form').submit(function (e) {
38881							myDeco.buttons.disable($(e.target));
38882						}).find('.form-submit-button').click(function (e) {
38883							e.stop();
38884							$(e.target).closest('form').submit();
38885						});
38886					}
38887				})
38888			})
38889		});
38890	},
38891
38892	/**
38893	 * Generate visual representation of given build entity for drag-n-drop
38894	 * @param {Material} component Floor shape/window/door to render
38895	 * @public
38896	 */
38897	generateElementForDrag: function(component) {
38898		var result;
38899		
38900		if (component.type === 'ArchitecturalComponent') {
38901			result = $('<div id="new-component">' +
38902				'<img src="' + buildThumbnailLink(component.componentData['3d'].render_top, 
38903				(component.model === 'doorway_door' ? null : this.settings.listingThumbnail)) + 
38904				'" class="original" /></div>');
38905		} else {
38906			result = $('<div id="new-room"><div class="original ' +
38907				component.subtype + ' room-icon"></div></div>');
38908		}
38909		
38910		return result;
38911	},
38912	
38913	/**
38914	 * Listener for iframe response from WS
38915	 * @param {Event} e
38916	 * @param {Object} object Response from WS
38917	 */
38918	uploadResponse: function(e, object) {
38919		var form = myDeco.buttons.enable(myDeco.popin.content().find('form')),
38920			name, message, field;
38921		
38922		if (object && object.error) {
38923			form.find('.error').hide();
38924			
38925			for (name in object.error.message) {
38926				message = object.error.message[name][0];
38927				field = form.find('.error.' + (name === '__all__' ? 'image' : name));
38928				if (message && !field.is(':visible')) {
38929					field.text(message).show();
38930				}
38931			}
38932			
38933			myDeco.popin.resize();
38934		} else {
38935			// assign back to me
38936			if (form.find('input:checked').size() !== 0) {
38937				myDeco.popin.close();
38938				myDeco.App.model.loadRoom(object.location);
38939			} else {
38940				myDeco.popin.open({
38941					url: '#floor-plan-popin-success',
38942					width: 595,
38943					closeIn: 30,
38944					afterOpen: function(div) {
38945						div.find('.note a').one('click', function(e) {
38946							e.preventDefault();
38947							myDeco.body.trigger('roomOpenRequest');
38948						});
38949					}
38950				});
38951			}
38952		}
38953	}
38954});
38955
38956/**
38957 * Callback for WS response from iframe proxy
38958 * @param {Object} object
38959 */
38960function FPEfloorUploadResponse(object) {
38961	myDeco.body.trigger('view-build-upload-response', [object]);
38962}
38963
38964myDeco.App.views.push(BuildView);
38965/*globals myDeco, $, AppView, window, location, document, emptyRoom, tmpl */
38966/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
38967          sloppy: true, white: true */
38968/**
38969 * @fileOverview
38970 * Menu view
38971 */
38972var MenuView = AppView.extend({
38973
38974	/**
38975	 * View type
38976	 * @type {String}
38977	 * @public
38978	 * @static
38979	 */
38980	$type: 'menu',
38981
38982	/**
38983	 * Used controls cache
38984	 * @type {Object}
38985	 */
38986	_controls: {},
38987
38988	/**
38989	 * Help page
38990	 * @type {jQset}
38991	 */
38992	_helpPage: $(),
38993
38994	/**
38995	 * View init
38996	 * @param {Object} options
38997	 * @public
38998	 */
38999	init: function(options) {
39000		this._super(options);
39001
39002		var self = this,
39003			form = this.canvasElement.find('#search'),
39004			query = form.find('input[name=query]'),
39005			clear = form.find('.clear').hide().click(function(e) {
39006				e.stop();
39007				clear.hide();
39008				query.val('');
39009			});
39010
39011		this._controls.save = this.canvasElement.find('.save');
39012		this._controls.hd = this.canvasElement.find('.save-hd');
39013		this._controls.undo = this.canvasElement.find('.undo');
39014		this._controls.redo = this.canvasElement.find('.redo');
39015		this._controls.extra = this.canvasElement.find('.extra-buttons');
39016
39017		this.canvasElement.find('#rm-invoker')
39018			.click(function(e) {
39019				e.stop();
39020				myDeco.body.trigger('roomOpenRequest');
39021			});
39022
39023		// change/loading/saving
39024		myDeco.body
39025			.bind('roomLoaded roomSaved operationEnd roomLoadError',
39026				function(e, sameRoom) {
39027					switch (e.type) {
39028						case 'operationEnd':
39029							self._controls.undo.removeClass('disabled');
39030							self._controls.redo.addClass('disabled');
39031							break;
39032
39033						case 'roomLoaded':
39034							if (!sameRoom && self._model._ws_url) {
39035								self._controls.undo.addClass('disabled');
39036								self._controls.redo.addClass('disabled');
39037							}
39038							self._controls.extra.toggleClass('masking',
39039								self._model._masking);
39040							break;
39041					}
39042
39043					self._updateSaveButton();
39044				}
39045			)
39046			.bind('roomSaving', function() {
39047				self._controls.save.addClass('disabled');
39048			})
39049			.bind('roomSavingError', function() {
39050				self._updateSaveButton();
39051			})
39052			.bind('roomNewRequest', function() {
39053				self.showNewAction();
39054			})
39055			.bind('undo redo', function(e) {
39056				self._controls.undo.toggleClass('disabled', e.type === 'undo');
39057				self._controls.redo.toggleClass('disabled', e.type !== 'undo');
39058				self._updateSaveButton();
39059			})
39060			.bind('mydeco.login', this.updateUsername.bind(this))
39061			.bind('actionRequest', function(e, action) {
39062				if (action[0] === 'help') {
39063					self.toggleShortcuts();
39064				}
39065			});
39066
39067		this._controls.undo.click(function(e) {
39068			e.stop();
39069			if (self._controls.undo.hasClass('disabled')) {
39070				return;
39071			}
39072
39073			self._model.undo();
39074		});
39075
39076		this._controls.redo.click(function(e) {
39077			e.stop();
39078			if (self._controls.redo.hasClass('disabled')) {
39079				return;
39080			}
39081
39082			self._model.redo();
39083		});
39084
39085		this.canvasElement.find('.menu-item').click(function(e) {
39086				e.preventDefault();
39087			})
39088			.filter('.file').mouseup(this.fileItem.bind(this)).end()
39089			.filter('.help').mouseup(this.helpItem.bind(this)).end()
39090			.filter('.profile, .newsfeed').mouseup(this.profileItem.bind(this)).end()
39091			.filter('.share').mouseup(function() {
39092				if (!self._model.isNew) {
39093					if (self._model._metadata['public']) {
39094						var url = myDeco.urls.ws.roomShare.replace("/0/", "/" + self._model._room_id + "/");
39095						if (myDeco.settings.WL_PROFILE) {
39096							url += '?profile=' + myDeco.settings.WL_PROFILE;
39097						}
39098						myDeco.roomShare.share(url);
39099					} else {
39100						myDeco.popin.alert({
39101							title: gettext('Room sharing'),
39102							message: gettext('Your design should be marked public.')
39103						});
39104					}
39105				} else {
39106					myDeco.popin.alert({
39107						title: gettext('Room sharing'),
39108						message: gettext('Please save your design before sharing.')
39109					});
39110				}
39111			});
39112
39113		// show new splash on startup if no parent action is given
39114		if (!myDeco.App.roomUrl && window.location.hash.indexOf('nosplash') === -1) {
39115			this.showNewAction();
39116		}
39117
39118		form.submit(function(e) {
39119			e.stop();
39120
39121            var tabs = $('#sidebar').find('.sidebar-buttons .tabs');
39122
39123            if (tabs.find('.tab.decorate').hasClass('active')) {
39124				$('.sidebar-decorate-container form')
39125				    .find('input').val(query.blur().val()).end()
39126				    .submit();
39127            } else {
39128			    tabs.trigger('show', ['furnish']).end()
39129				    .find('.sidebar-furnish-container form')
39130				    .find('input').val(query.blur().val()).end()
39131				    .submit();
39132            }
39133
39134			// simulate mouseover to show clear button
39135			form.mouseover();
39136		}).find('.submit').click(function(e) {
39137			e.stop();
39138			form.submit();
39139		}).end();
39140
39141		form.hover(function(e) {
39142			clear.toggle(e.type === 'mouseenter' && query.val() !== '');
39143		});
39144
39145		var that = this;
39146		this._helpPage = $('#view-help').find('.close').click(function(){
39147			that.toggleShortcuts();
39148		}).end();
39149
39150		this.updateUsername();
39151	},
39152
39153	/**
39154	 * Update save button state method
39155	 * @private
39156	 */
39157	_updateSaveButton: function() {
39158		this._controls.save.toggleClass('disabled', this._model.history.counter === 0);
39159	},
39160
39161	/**
39162	 * Process of file submenu items
39163	 * @param {Event} e Click event
39164	 */
39165	fileItem: function(e) {
39166		var menu = $(e.target),
39167			saveCallback = function() {
39168				myDeco.body.trigger('roomSaveRequest', [{
39169					forceNew: menu.hasClass('save-as')
39170				}
39170]);
39171			};
39172		if (menu.closest('.disabled').size() === 0) {
39173			if (menu.hasClass('new')) {
39174				window.location.href = location.protocol + '//' + location.host + location.pathname;
39175				//this.showNewAction();
39176			} else if (menu.hasClass('open')) {
39177				myDeco.body.trigger('roomOpenRequest');
39178			} else if (menu.hasClass('save') || menu.hasClass('save-as')) {
39179				if (this._model._masking) {
39180					myDeco.popin.confirm({
39181						message: gettext('Are you sure you want to send this floor-plan to the customer?'),
39182						yesButtonText: gettext('Confirm'),
39183						noButtonText: gettext('Not yet!'),
39184						yesButton: function(e) {
39185							e.stop();
39186							saveCallback();
39187						}
39188					});
39189				} else {
39190					saveCallback();
39191				}
39192			} else if (this._model._masking && menu.hasClass('reject')) {
39193				this.showRejectAction();
39194			}
39195		}
39196	},
39197
39198	/**
39199	 * Support for help menu item
39200	 * @param {Event} e Click event
39201	 */
39202	helpItem: function(e) {
39203		e.preventDefault();
39204		switch ($.trim($(e.target).attr('class')).split(/\s+/).pop()) {
39205			case 'manual':
39206              var locale = myDeco.settings.I18N_LOCALE,
39207                wls = myDeco.settings.WL,
39208                manual_url = wls && (wls['MANUAL_URL_' + locale.toUpperCase()] || wls.MANUAL_URL);
39209
39210                // FIXME if we use a convention naming for manuals, we can make this logic a bit simpler
39211                if (!manual_url) {
39212                    if (locale === 'de') {
39213		                manual_url = "https://s3-eu-west-1.amazonaws.com/media.eu.mydeco3d.com/media/manual/manual-de-2014-v1.pdf";
39214                    } else {
39215				  	    manual_url = "https://s3-eu-west-1.amazonaws.com/media.eu.mydeco3d.com/media/manual/manual-en-2013-v1.pdf";
39216   				    }
39217                }
39218                window.open(manual_url);
39219  		        break;
39220			case 'users':
39221				window.open("https://roomstyler.com/forum/discussions/5083");
39222				break;
39223			case 'forum':
39224				window.open("https://roomstyler.com/forum/");
39225				break;
39226			case 'tutorial':
39227				window.open(myDeco.settings.WL && myDeco.settings.WL.WL_VIDEO_TUTORIAL_LINK ?
39228					myDeco.settings.WL.WL_VIDEO_TUTORIAL_LINK : myDeco.settings.AUSTIN_TUTORIAL_URL);
39229				break;
39230			case 'shortcuts':
39231				this.toggleShortcuts();
39232				break;
39233			case 'forum':
39234				window.open(myDeco.settings.AUSTIN_FORUM_TOPIC_URL);
39235				break;
39236			case 'furniture':
39237				window.open(myDeco.settings.AUSTIN_REQUEST_FURNITURE_URL);
39238				break;
39239			case 'tips':
39240				this.showTips();
39241				break;
39242		}
39243	},
39244
39245	/**
39246	 * Support for profile submenu
39247	 * @param {Event} e Click event
39248	 */
39249	profileItem: function(e) {
39250		var elem = $(e.target);
39251		var isRoomstyler = location.host.indexOf('roomstyler') != -1;
39252		if(isRoomstyler) {
39253			if (elem.hasClass('profile')) {
39254				window.open('/users/' + myDeco.userid)
39255			} else if (elem.hasClass('newsfeed')) {
39256				window.open('/newsfeed');
39257			}
39258		} else {
39259			if (elem.hasClass('profile') || elem.hasClass('newsfeed')) {
39260				window.open(elem.data('href') + (myDeco.settings.WL ? "?profile=" + myDeco.settings.WL_PROFILE : ""));
39261			}
39262		}
39263	},
39264
39265	toggleShortcuts: function() {
39266		this._helpPage.toggle();
39267	},
39268
39269	/**
39270	 * Show new splash
39271	 */
39272	showNewAction: function() {
39273		var self = this;
39274
39275		if (myDeco.settings.WL && myDeco.settings.WL.NOSPLASH) {
39276            // sidebar event handlers are not yet attached, so this new 'scratch' action should be called asyncronously,
39277            // after all syncronous stuff finishes running
39278            setTimeout(function() {
39279			    self._model.operationStart(['startingchoice', 'scratch']);
39280			    self.newAction('scratch');
39281            }, 0);
39282			return;
39283		}
39284
39285		myDeco.popin.open({
39286			template: tmpl('view-' + this.$type + '-new'),
39287			width: 694,
39288			className: 'view-' + this.$type + '-new',
39289			afterOpen: function(content) {
39290				content.find('li a:not(.youtube)').click(function(e) {
39291					e.stop();
39292
39293					var type = $(e.target).closest('li').attr('className');
39294
39295					// No upload for mobile safari.
39296					if (type === 'upload' && myDeco.browser.isMobileSafari) {
39297						myDeco.popin.alert({
39298							width: '650',
39299							title: gettext('Oops, we cannot get images from this device'),
39300							message: interpolate(
39301									gettext('To upload a floor plan, please visit %s from a computer where the image is stored'),
39302									[location.hostname]
39303								)
39304						});
39305						return;
39306					}
39307
39308					self._model.operationStart(['startingchoice', type]);
39309					self.newAction(type);
39310				});
39311			}
39312		});
39313	},
39314
39315	/**
39316	 * Action invoker for new splash
39317	 * @param {String} type Action type
39318	 */
39319	newAction: function(type) {
39320		var self = this;
39321
39322		myDeco.popin.close();
39323		switch(type) {
39324			case 'saved':
39325				myDeco.body.trigger('roomOpenRequest');
39326				break;
39327			case 'scratch':
39328				if (!$.cookie('hide-hints') && !(myDeco.settings.WL && myDeco.settings.WL.NOHINTS)) {
39329					// show hints after timeout
39330					myDeco.body.one('roomLoaded', function() {
39331						window.setTimeout(function() {
39332							self.showTips();
39333						}, 1000);
39334					});
39335
39336					// set session cookie
39337					$.cookie('hide-hints', '1', {path: document.location.pathname});
39338				}
39339
39340				myDeco.body
39341					.trigger('toggleSidebar', ['build'])
39342					.find('#view-build .build-tabs').trigger('show', ['walls']).end()
39343					.trigger('viewToggle', ['floor'])
39344					.trigger('roomLoadRequest', [emptyRoom]);
39345				break;
39346			case 'wizard':
39347				myDeco.body.trigger('showWizard');
39348				break;
39349			case 'furnished':
39350				myDeco.body.one('roomLoaded', function() { myDeco.body.trigger('roomMetadata', [{name: ''}]); });
39351				myDeco.body.trigger('roomLoadRequest', [myDeco.settings.DEFAULT_ROOM_URL]);
39352				break;
39353			case 'upload':
39354				myDeco.body.trigger('view-build-upload');
39355				break;
39356		}
39357	},
39358
39359	showTips: function() {
39360		var bubbles = $('.bubble.init').filter('.help').rearrange({
39361				anchor: $('#menu .help'),
39362				position: [0, 'bottom']
39363			}).end()
39364			.filter('.camera-angle').rearrange({
39365				anchor: $('#view-render'),
39366				position: ['right', 10]
39367			}).end()
39368			.filter('.camera').rearrange().end()
39369			.filter('.drag').rearrange({
39370				position: [280, 620]
39371			}).end()
39372			.fadeIn(500),
39373			cancelBubbles = function() {
39374				bubbles.fadeOut(500);
39375			};
39376		myDeco.body.one('mousedown', cancelBubbles);
39377	},
39378
39379	/**
39380	 * Update welcome text when user log in/out
39381	 * @param {Event} e
39382	 */
39383	updateUsername: function() {
39384		this.canvasElement.find('#username-container')
39385			.html(myDeco.settings.T.welcome.replace('%s', myDeco.username || ''));
39386	},
39387
39388	/**
39389	 * Show popup for floorplan rejecting
39390	 */
39391	showRejectAction: function() {
39392		var self = this,
39393			callback = function(e) {
39394				if (e.type === 'roomDeleted') {
39395					myDeco.popin.close();
39396				} else {
39397					myDeco.popin.alert({
39398						message: gettext('Error in deletion!')
39399					});
39400				}
39401				myDeco.body.unbind('roomDeleted roomDeletionError', callback);
39402			};
39403
39404		myDeco.popin.open({
39405			url: '#view-menu-reject',
39406			afterOpen: function(div) {
39407				myDeco.buttons.initSubmitButtons(div.find('form').submit(function(e) {
39408					e.stop();
39409					myDeco.body.one('roomDeleted roomDeletionError', callback);
39410					self._model.deleteRoom({reason: $(this).find('textarea').val()});
39411				}
39411).find('input:submit'));
39412			}
39413		});
39414	}
39415
39416});
39417
39418myDeco.App.views.push(MenuView);
39419/*globals myDeco, $, AppView, window, location, document, emptyRoom,
39420          numberInputKeyPress, tmpl, WizardColorPicker, diffuse2RGB, Point */
39421/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
39422          sloppy: true, white: true */
39423/**
39424 * @fileOverview
39425 * Wizard view
39426 */
39427var WizardView = AppView.extend({
39428	
39429	/**
39430	 * View type
39431	 * @type {String}
39432	 * @public
39433	 * @static
39434	 */
39435	$type: 'wizard',
39436	
39437	/**
39438	 * Used controls cache
39439	 * @type {jQset}
39440	 */
39441	_controls: false,
39442	
39443	/**
39444	 * Colour controls
39445	 * @type {jQuery}
39446	 */
39447	_colours: false,
39448	
39449	/**
39450	 * New room settings
39451	 * @type {Object}
39452	 */
39453	defaultRoomSettings: {
39454		width: 450,
39455		length: 450,
39456		height: 310,
39457		style: false,
39458		shape: false,
39459		colours: []
39460	},
39461	
39462	/**
39463	 * Current room settings
39464	 * @type {Object}
39465	 */
39466	roomSettings: {},
39467	
39468	/**
39469	 * Order of panes
39470	 * @type {Array}
39471	 */
39472	_order: ['type', 'shape', 'color', 'style', 'last'],
39473	
39474	/**
39475	 * Current step of the wizard
39476	 * @type {Number}
39477	 */
39478	_step: 'type',
39479	
39480	/**
39481	 * Default architecture elements
39482	 * @type {Array}
39483	 */
39484	_components: [
39485		'23f3f34205d4709811052a6709914697600302c6',
39486		'e79b4c034a30d52dac5d8b2cd460a2dddf711874'
39487	],
39488	
39489	/**
39490	 * Current colour to select
39491	 * @type {Number}
39492	 */
39493	_currentColour: false,
39494	
39495	/**
39496	 * Component added instead of style selection
39497	 * @type {String}
39498	 */
39499	component_id: undefined,
39500	
39501	/**
39502	 * View init
39503	 * @param {Object} options
39504	 * @public
39505	 */
39506	init: function(options) {
39507		this._super(options);
39508		
39509		var self = this;
39510		
39511		myDeco.body
39512			.bind('showWizard', function(e, component_id) {
39513				myDeco.body.trigger('roomLoadRequest',
39514					[emptyRoom, function() {
39515						self.showWizard(component_id);
39516					}]);
39517			});
39518	},
39519		
39520	/**
39521	 * Show wizard popin
39522	 * @param {String} component_id Component added instead of hardcoded styles
39523	 * @public
39524	 */
39525	showWizard: function(component_id) {
39526		var self = this;
39527		
39528		this.component_id = component_id;
39529		
39530		myDeco.popin.open({
39531			url: '#view-' + this.$type,
39532			width: 694,
39533			height: 386,
39534			className: 'view-' + this.$type,
39535			afterOpen: function(content) {
39536				// reset settings to default
39537				self.roomSettings = $.cloneObject(self.defaultRoomSettings);
39538				
39539				// room dimensions
39540				var dimensions = content.find('.number-input input')
39541					.numberInput({
39542						formatFunction: function() {
39543							return Array.prototype.slice.call(arguments).toUnits();
39544						},
39545						parseFunction: function(v) {
39546							var value = v.fromUnits().shift() || 0;
39547							if (value < 10) {
39548								value = 10;
39549							}
39550							return value;
39551						},
39552						positive: true,
39553						frac: 2,
39554						increment: function(shift) {
39555							return myDeco.settings
39556								.UNITS_LIST[myDeco.settings.UNITS][+shift + 2]
39557									.fromViewUnits();
39558						},
39559						keyPress: numberInputKeyPress
39560					})
39561					.each(function() {
39562						var input = $(this);
39563						input.trigger('setValue', self.roomSettings[input.attr('name')]);
39564					})
39565					.change(function() {
39566						var input = $(this);
39567						self.roomSettings[input.attr('name')] = input.data('value');
39568					}),
39569					palette = content.find('.color .palette-holder'),
39570					picker;
39571				
39572				myDeco.body.bind('unitsChanged', function() {
39573					dimensions.each(function() {
39574						$(this).trigger('setValue');
39575					});
39576				});
39577				
39578				self._step = self._order[0];
39579				self.steps = content.find('.wizard-step');
39580				
39581				// wizard types
39582				content.find('.types').html(tmpl('view-wizard-types-template', {
39583					types: $.keys(myDeco.settings.WIZARD_BUNDLES)
39584				}));
39585				
39586				// wizard shapes
39587				content.find('.shapes').html(tmpl('view-wizard-shapes-template', {
39588					types: $.keys(self._model.settings.shapes)
39589				}));
39590				
39591				picker = new WizardColorPicker({
39592					canvas: self.steps.filter('.color'),
39593					settings: {
39594						mousemove: function(data) {
39595							if (self._currentColour !== false && data[2]) {
39596								self._colours.eq(self._currentColour)
39597									.css('background-color', '#' + data[2]);
39598							}
39599						},
39600						mouseleave: function() {
39601							if (self._currentColour !== false) {
39602								self._colours.eq(self._currentColour)
39603									.css('background-color', 'transparent');
39604							}
39605						},
39606						select: function(material) {
39607							if (self._currentColour !== false) {
39608								self.roomSettings.colours[self._currentColour] =
39609									material;
39610								
39611								self._colours.eq(self._currentColour)
39612									.css('background-color', 'rgb(' +
39613										diffuse2RGB(material.diffuse) + ')')
39614									.parent().removeClass('selected');
39615								
39616								palette.hide();
39617								
39618								self._currentColour = false;
39619							}
39620
39621							if (self.roomSettings.colours[0] && !self.roomSettings.colours[1]){
39622								self._colours.eq(1).click();
39623							}
39624							
39625							if (self.roomSettings.colours[0] ||
39626									self.roomSettings.colours[1]) {
39627								self._controls.filter('.next').removeClass('disabled');
39628							}
39629						}
39630					},
39631					palette: $.extend(true, {}, myDeco.App.palette, {
39632						size: 9,
39633						spacing: 1,
39634						bImage: {
39635							width: 484,
39636							height: 214,
39637							url: '/assets/austin/wizard.png'
39638						}
39639					})
39640				});
39641				
39642				// controls
39643				self._controls = content.find('.button.next, .button.prev')
39644					.click(function(e) {
39645						e.stop();
39646						
39647						var button = $(this);
39648						if (!button.hasClass('disabled')) {
39649							self._switchStep(button.hasClass('next') ? 1 : -1);
39650						}
39651					});
39652					
39653				self._colours = content.find('.colour-block .colour')
39654					.click(function(e) {
39655						e.stop();
39656						
39657						self._colours.parent().removeClass('selected');
39658						$(this).parent().addClass('selected');
39659						palette.show();
39660						self._currentColour = self._colours.index(this);
39661					});
39662				
39663				content.find('.units').unitsSelector();
39664				
39665				content.delegate('li.room-icon a', 'click', function(e) {
39666					e.preventDefault();
39667					
39668					var button = $(this);
39669					
39670					button.closest('.wizard-step').find('.room-icon a')
39671						.removeClass('active');
39672					
39673					self.roomSettings[self._step] = button.addClass('active')
39674						.data(self._step);
39675						
39676					self._controls.filter('.next').removeClass('disabled');
39677					
39678					if (self._step === 'type' || self._step === 'style') {
39679						self._switchStep(1);
39680					}
39681				});
39682				
39683				self._switchStep(self._order[0]);
39684			}
39685		});
39686	},
39687	
39688	/**
39689	 * Switch slides in the wizard
39690	 * @param {String} step Step to switch
39691	 * @private
39692	 */
39693	_switchStep: function(step) {
39694		var next = this._controls.filter('.next'),
39695			prev = this._controls.filter('.prev'),
39696			nextText = next.children(),
39697			pane, bubbles,
39698			cancelBubbles = function() {
39699				bubbles.fadeOut(500);
39700			};
39701		
39702		if (typeof step === 'number') {
39703			step = this._order[
39704				Math.max(0, Math.min(this._order.length,
39705					this._order.indexOf(this._step) + step))
39706			];
39707		}
39708		
39709		//  that means that we have all steps completed
39710		if (!step) {
39711			myDeco.popin.close();
39712			if (this.roomSettings.style !== 'user') {
39713				bubbles = $('.bubble.drag-wizard').rearrange()
39714					.fadeIn(500);
39715				myDeco.body.one('mousedown', cancelBubbles);
39716			}
39717			return;
39718		}
39719		
39720		// time to update controls and switch to next pane	
39721		this._step = step;
39722		
39723		pane = this.steps.filter('.' + this._step);
39724		
39725		next.toggleClass('disabled', this._step !==
39726			this._order[this._order.length - 1]).hide();
39727		prev.toggle(this._step !== this._order[0]);
39728
39729		switch (step) {
39730			//if component_id skip this step
39731			case 'style':
39732				if (!this.component_id) {
39733					pane.find('.styles').html(tmpl('view-wizard-styles-template', {
39734						types: myDeco.settings.WIZARD_BUNDLES[this.roomSettings.type]
39735					}));
39736				} else {
39737					this._switchStep(1);
39738					return;
39739				}
39740				break;
39741				
39742			case 'last':
39743				this.buildRoom();
39744				next.show();
39745				nextText.text(gettext('See my room now'));
39746				//if component_id - prev button should switch 3 steps behind
39747				if (this.component_id) {
39748					self = this;
39749					prev.unbind().bind('click',function(e) {
39750						e.stop();
39751						self._switchStep(-3);
39752					});
39753				}
39754				break;
39755			
39756			case 'color':
39757				// if component_id - skip this step and the colors are white by default
39758				if (!this.component_id) {
39759					this._colours.eq(0).click();
39760					pane.find('.roomtype').html(this.roomSettings.type);
39761					next.show();
39762					nextText.text(gettext('Next'));
39763				} else {
39764					this.roomSettings.colours[0] = myDeco.App.palette.getMaterial('#FFFFFF');
39765					this._switchStep(2);
39766					return;
39767				}
39768				break;
39769				
39770			case 'shape':
39771				pane.find('.roomtype').html(this.roomSettings.type);
39772				next.show();
39773				nextText.text(gettext('Next'));
39774				break;
39775				
39776			default:
39777				next.show();
39778				nextText.text(gettext('Next'));
39779		}
39780		
39781		this.steps.hide();
39782		pane.show();
39783	},
39784	
39785	/**
39786	 * get 5 products for chosen room type and furniture style
39787	 * @public
39788	 */
39789	buildRoom: function() {
39790		var self = this,
39791			wallsCount, data, walls,
39792			metadata = {
39793				category: this.roomSettings.type
39794			},
39795			lastStep = function() {
39796				// show window tab
39797				myDeco.body
39798					.trigger('toggleSidebar', ['build'])
39799					.find('#view-build .build-tabs').trigger('show', ['windows']);
39800					
39801				// update room metadata with selected category and style
39802				self._model.metadata(false, metadata);
39803				myDeco.body.trigger('roomLoaded', [true]);
39804				
39805			};
39806		
39807		// add room
39808		this._model.loadRoom(emptyRoom);
39809		
39810		this._model.setCeilingHeight(this.roomSettings.height);
39811		this._model.addRoom(this.roomSettings.shape, 0, 0,
39812			this.roomSettings.width, this.roomSettings.length);
39813		
39814		wallsCount = this._model.walls.length;
39815		
39816		// get walls and sort them by length
39817		walls = this._model.walls.slice();
39818		walls.sort(function(a, b) {
39819			return a.getAttribute('width') - b.getAttribute('width');
39820		});
39821
39822		$.each(this._model.walls, function(i, wall) {
39823			var material = self.roomSettings.colours[Math.round(i / wallsCount)] ||
39824				self.roomSettings.colours[0] || self.roomSettings.colours[1];
39825				
39826			self._model.assignMaterial(wall, material, 'wall');
39827		});
39828		
39829		// fetch products
39830		if (this.roomSettings.style && this.roomSettings.style !== 'user') {
39831			metadata.style = self.roomSettings.style;
39832			data = {
39833				category: self.roomSettings.type,
39834				style: self.roomSettings.style
39835			};
39836			if (myDeco.settings.WL_PROFILE) {
39837				data.profile = myDeco.settings.WL_PROFILE;
39838			}
39839			$.ajax({
39840				url: myDeco.urls.wizardComponents,
39841				data: data,
39842				type: 'get',
39843				success: function(data) {
39844					self._model.dropComponents(self._components.concat(data), lastStep);
39845				}
39846			});
39847		} else {
39848			data = this._components;
39849			if (this.component_id) {
39850				data = data.concat([this.component_id]);
39851			}
39852			this._model.dropComponents(data, lastStep);
39853		}
39854	}
39855	
39856});
39857
39858myDeco.App.views.push(WizardView);
39859/*globals ColorPickerView */
39860/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
39861          sloppy: true, white: true */
39862
39863var WizardColorPicker = ColorPickerView.extend({
39864	
39865	$type: 'wizard-color-picker',
39866	
39867	renderComponent: function(e) {
39868		if (e.isDefaultPrevented()) {
39869			return;
39870		}
39871		e.preventDefault();
39872		this.settings.select(this.getMaterial(e));
39873	}
39874});
39875/*globals myDeco, $, AppView, Raphael, T, window */
39876/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
39877          sloppy: true, white: true */
39878/**
39879 * @fileOverview Render view
39880 */
39881var RenderView = AppView.extend({
39882	
39883	/**
39884	 * View type
39885	 * @type {String}
39886	 * @public
39887	 * @static
39888	 */
39889	$type: 'render',
39890	
39891	/**
39892	 * Render timeout holder
39893	 * @type {Number}
39894	 * @private
39895	 */
39896	_renderTimeout: 0,
39897	
39898	/**
39899	 * Active render process
39900	 * @type {Boolean}
39901	 * @private
39902	 */
39903	_renderInProgress: false,
39904	
39905	/**
39906	 * Was any render request while render is working
39907	 * @type {Boolean}
39908	 * @private
39909	 */
39910	_renderRequested: false,
39911	
39912	/**
39913	 * render() method with bind to current object cache
39914	 * @type {Function}
39915	 * @private
39916	 */
39917	_cacheRender: false,
39918	
39919	/**
39920	 * Render info overlay
39921	 * @type {jQset}
39922	 * @private
39923	 */
39924	_overlay: false,
39925	
39926	/**
39927	 * Control buttons
39928	 * @type {Array}
39929	 * @private
39930	 */
39931	_buttons: {},
39932	
39933	/**
39934	 * Flag for availability HQ render
39935	 * @type {Boolean}
39936	 */
39937	_hqRenderAvailable: false,
39938	
39939	/**
39940	 * HQ render request callback
39941	 * @type {Function}
39942	 */
39943	_hqRenderCallback: $.noop,
39944	
39945	/**
39946	 * Default view settings
39947	 * @type {Object}
39948	 * @public
39949	 */
39950	settings: {
39951		enlargeClassName: 'big-render',
39952		
39953		timeout: 1000,
39954		
39955		camera: {
39956			// max camera top position in cm
39957			maxHeight: 700,
39958			
39959			// rotating point X shift
39960			rotateX: 55,
39961			
39962			// distance between rotating point and focal point on image
39963			rotateD: 70,
39964			
39965			// max stroke of rotating point
39966			rotateH: 175,
39967			
39968			// drag square coordinates (relative to rotateX and vertical center of canvas) and sizes : [x, y, w, h]
39969			drag: [-4, -19, 48, 38],
39970			
39971			// arrow space:
39972			dragPaddingTop: 105,
39973			
39974			// rotate circle (relative to rotateX - rotateD and vertical center of canvas) and radius: [x, y, r]
39975			rotate: [-9, 0, 7],
39976			
39977			image: ['/vertical-camera.png', -4, -21, 79, 42],
39978			
39979			// main drag path image:
39980			dragPath: '/vertical-camera-path.png'
39981		},
39982		// render timeout before next request after error in seconds
39983		renderErrorTimeout: 15,
39984		
39985		// max render requests after error response
39986		renderErrorCount: 3,
39987		
39988		// big render size
39989		bigRender: [1024, 576, 'normal']
39990	},
39991	
39992	/**
39993	 * View init
39994	 * @param {Object} options View options
39995	 * @public
39996	 */
39997	init: function(options) {
39998		if (window.location.hash.indexOf('norender') !== -1) {
39999			return;
40000		}
40001		
40002		var self = this,
40003			setDimensions = function() {
40004				self.canvasWidth = self.canvas.innerWidth();
40005				self.canvasHeight = self.canvas.innerHeight();
40006			};
40007		
40008		this._super(options);
40009		this.canvas = this.canvasElement.find('.render');
40010		this._overlay = this.canvasElement.find('.overlay');
40011		this.fileMenuItem = $('#view-menu').find('.save-hd');
40012		
40013		if (this.canvas.size() === 0) {
40014			this.canvas = this.canvasElement;
40015		} else {
40016			setDimensions();
40017		}
40018		
40019		this._cacheRender = this.render.bind(this);
40020		this._attachCamera($("#view-component-info").find('.verticalcamera'));
40021
40022        $('.camera-fov input').click(function() {
40023            var value = parseInt($('.camera-fov input:checked').val());
40024            self._model.camera.setFov(value);
40025		    self.processChanges();
40026        });
40027		
40028		this._hqRenderCallback = function(e) {
40029			e.stop();
40030			if (self._hqRenderAvailable) {
40031				myDeco.body.trigger('roomSaveRequest', [{
40032					showProperties: true,
40033					hqRender: true
40034				}]);
40035			}
40036		};
40037		
40038		this.canvasElement
40039			.bind('restored maximized minimized', function(e) {
40040				setDimensions();
40041				
40042				// don't want to re-render in a worse quality
40043				if (e.type === 'maximized') {
40044					self.render();
40045				}
40046			});
40047		
40048		this._buttons.preview = this.canvasElement
40049			.find('.preview').click(this.bigRender.bind(this));
40050			
40051		this._buttons.hd = $('#view-menu')
40052			.find('.hd').click(this._hqRenderCallback);
40053		
40054		this.fileMenuItem.click(this._hqRenderCallback);
40055		
40056		myDeco.body
40057			.bind('operationEnd roomLoaded roomSaved', this.processChanges.bind(this))
40058			.bind('renderState', function(state) {
40059				if (state === 'session') {
40060					self.updateMessage(gettext('still rendering...'));
40061				}
40062			})
40063			.bind('actionRequest', function(e, action) {
40064				if (action[0] === 'enlarge') {
40065					self._buttons.preview.click();
40066				}
40067			});
40068		
40069		this.processChanges();
40070	},
40071	
40072	/**
40073	 * Camera drawing and support
40074	 * @param {jQset} holder Camera holder
40075	 */
40076	_attachCamera: function(holder) {
40077		if (!holder || holder.size() === 0) {
40078			return;
40079		}
40080		
40081		var self = this,
40082			// settings shortcut
40083			s = this.settings.camera,
40084			
40085			// default attributes for rotating handlers
40086			attr = {opacity: 0.01, fill: '#fff', 'stroke-width': 0},
40087			
40088			// camera shortcut
40089			model = self._model.camera,
40090			
40091			// canvas size
40092			canvasW = holder.innerWidth(),
40093			canvasH = holder.innerHeight(),
40094			
40095			// rotating center X coordinate
40096			rotateX = s.rotateX,
40097			
40098			// rotating center Y coordinate
40099			rotateY = Math.round(canvasH),
40100			
40101			// distance between rotating center and rotating handler
40102			rotateD = s.rotateD,
40103			
40104			// rotation angle
40105			rotateA = 0,
40106			
40107			// calculating coefficient between px and cm
40108			coeff = s.rotateH / s.maxHeight,
40109			
40110			// drag operations offset storage
40111			offsetY = 0,
40112			offsetX = 0,
40113			
40114			// drag operation active entity
40115			activeEntity = null,
40116			
40117			// canvas
40118			canvas = new Raphael(holder.get(0), canvasW, canvasH),
40119			
40120			// camera entity
40121			camera = canvas.set(),
40122			
40123			// Y axis movement holder entity
40124			drag = canvas.rect(rotateX + s.drag[0], rotateY + s.drag[1], s.drag[2], s.drag[3]).attr(attr),
40125			
40126			// rotating holder entity
40127			rotate = canvas.set(),
40128			
40129			// top offset of camera base
40130			offsetH = Math.round((canvasH - s.rotateH) / 2),
40131			
40132			// mouse move listener
40133			mousemove = function(e) {
40134				var rA = 0, rY = 0, rpY, offset, result;
40135				
40136				if (e) {
40137					// for dragging
40138					if (activeEntity === drag) {
40139						offset = (offsetY + rotateY - e.pageY) / coeff;
40140						result = model.position.z + offset;
40141						
40142						// limit with 0 and max top
40143						if (result < 10 || result > (s.maxHeight - s.dragPaddingTop)) {
40144							return;
40145						}
40146
40147						model.move(0, 0, offset);
40148						
40149					// for rotating
40150					} else {
40151						rA = T.atan2(offsetY - e.pageY, e.pageX - offsetX);
40152						
40153						// limit angle with +90 / -90 degrees
40154						if (rA > 90 || rA < -90) {
40155							return;
40156						}
40157					}
40158				}
40159				
40160				if (!e || activeEntity === drag) {
40161					rY = Math.round(s.rotateH + offsetH - coeff * model.position.z);
40162				}
40163				
40164				if (!e) {
40165					rA = model.getVAngle();
40166				}
40167				
40168				// check for position of rotating circle
40169				rpY = (rY || rotateY) - rotateD * T.sin(rA || rotateA);
40170				if (rpY < s.rotate[2] || rpY > canvasH - s.rotate[2]) {
40171					// if rotating mode, do not rotate anything
40172					if (rA !== 0) {
40173						rA = 0;
40174					// else calcualte shifting angle
40175					} else {
40176						rA = rotateA + T.asin((rpY - (rpY < s.rotate[2] ? s.rotate[2] : canvasH - s.rotate[2])) / rotateD);
40177					}
40178				}
40179				
40180				// Y axis movement if needed
40181				if (rY) {
40182					camera.translate(0, rY - rotateY);
40183					rotateY = rY;
40184				}
40185				
40186				// rotating
40187				if (rA) {
40188					
40189					// rotate camera relative to Z axis
40190					model.rotate(undefined, rA);
40191					
40192					camera.rotate(-rA, rotateX, rotateY);
40193					rotateA = rA;
40194				}
40195				
40196			},
40197			
40198			// mouse up listener
40199			mouseup = function() {
40200				myDeco.body
40201					.unbind('mouseup', mouseup)
40202					.unbind('mousemove', mousemove);
40203				self.processChanges();
40204			},
40205			
40206			// mouse down listener
40207			mousedown = function(e, entity) {
40208				if (e.isDefaultPrevented()) {
40209					return;
40210				}
40211				e.preventDefault();
40212				
40213				if (entity === drag) {
40214					offsetY = e.pageY - rotateY;
40215				} else {
40216					var offset = holder.offset();
40217					offsetX = offset.left + rotateX;
40218					offsetY = offset.top  + rotateY;
40219				}
40220				activeEntity = entity;
40221				
40222				// bind handlers
40223				myDeco.body
40224					.bind('mouseup', mouseup)
40225					.bind('mousemove', mousemove);
40226			},
40227			
40228			// mouse over listener
40229			mouseover = function(e, entity) {
40230				self.cursor(entity === drag ? 'move' : 'crosshair');
40231			},
40232			
40233			// mouse out listener
40234			mouseout = function() {
40235				self.cursor('');
40236			};
40237
40238		rotate.push(canvas.circle(rotateX + s.rotate[0] + rotateD, rotateY + s.rotate[1], s.rotate[2]).attr(attr),
40239					// Hidden rect for easier rotation on ipad.
40240					canvas.rect(rotateX + s.rotate[0] + rotateD - s.rotate[2] * 1.5, 
40241								rotateY + s.rotate[1] - s.rotate[2] * 3, 
40242								s.rotate[2] * 3, 
40243								s.rotate[2] * 6)
40244							.attr({fill: 'white', opacity: 0}));
40245		
40246		// draw vertical line
40247		canvas.image(myDeco.settings.STATIC_URL + '..' + s.dragPath, 46, 0, 20, s.rotateH);
40248		
40249		camera
40250			.push(canvas.image(myDeco.settings.STATIC_URL + '..' + s.image[0], rotateX + s.image[1], rotateY + s.image[2], s.image[3], s.image[4])
40251					// prevent image drag if cursor leave  control box
40252					.mousedown(function(e) { e.preventDefault(); }, true))
40253			.push(drag.toFront()
40254					.mousedown(mousedown.bindAsEventListener(this, drag), true)
40255					.mouseover(mouseover.bindAsEventListener(this, drag))
40256					.mouseout(mouseout.bindAsEventListener(this, drag)))
40257			.push(rotate.toFront()
40258					.mousedown(mousedown.bindAsEventListener(this, rotate), true)
40259					.mouseover(mouseover.bindAsEventListener(this, rotate))
40260					.mouseout(mouseout.bindAsEventListener(this, rotate)));
40261		
40262		// bind respositioning to room load
40263		myDeco.body.bind('roomLoaded', function() {
40264			model = self._model.camera;
40265			mousemove();
40266            $('.camera-fov input[value=' + model.fieldofview + ']').attr('checked', true);
40267		});
40268		
40269		// redraw camera
40270		mousemove();
40271	},
40272	
40273	/**
40274	 * Schedule render for view update and enable/disable buttons
40275	 * @public
40276	 * @param {Event} e
40277	 * @param {String} type
40278	 * @param {Number} id
40279	 * @param {Object} options Custom options
40280	 */
40281	processChanges: function(e, type, id, options) {
40282		options = options || {};
40283		if (e && e.type === 'roomLoaded') {
40284			this._renderInProgress = false;
40285		}
40286		
40287		this._hqRenderAvailable = this._model.history.position !== -1 &&
40288			!options.hqRender && !options.forceNew;
40289		
40290		this._buttons.hd.toggleClass('disabled', !this._hqRenderAvailable);
40291		this.fileMenuItem.toggleClass('disabled', !this._hqRenderAvailable);
40292		
40293		if (e && e.type === 'roomSaved') {
40294			return;
40295		}
40296		
40297		if (this._renderTimeout) {
40298			window.clearTimeout(this._renderTimeout);
40299		}
40300		if (!this.canvasElement.hasClass('minimized')) {
40301			this._renderTimeout = window.setTimeout(this._cacheRender, this.settings.timeout);
40302		}
40303	},
40304	
40305	/**
40306	 * Render room
40307	 * @public
40308	 */
40309	render: function() {
40310        RoomstylerLive.render(this._model.getModel());
40311        
40312		if (this._renderTimeout) {
40313			window.clearTimeout(this._renderTimeout);
40314		}
40315		
40316		// schedule render if it already in progress
40317		if (this._renderInProgress) {
40318			this._renderRequested = true;
40319			return;
40320		}
40321		
40322		this._renderInProgress = true;
40323		
40324		var self = this,
40325			errors = 0,
40326			finishRender = function() {
40327				self._renderInProgress = false;
40328				if (self._renderRequested) {
40329					self._renderRequested = false;
40330					self.render();
40331				}
40332			},
40333			processRender,
40334			renderCountdown = function(timeLeft) {
40335				self.updateMessage(
40336						interpolate(
40337							ngettext('Next camera preview in %s second', 'Next camera preview in %s seconds', timeLeft),
40338							[timeLeft]
40339						)
40340				);
40341				if (timeLeft > 0) {
40342					window.setTimeout(function() {
40343						renderCountdown(--timeLeft);
40344					}, 1000);
40345				} else {
40346					processRender();
40347				}
40348			},
40349			renderRequest = function() {
40350				self._model.render(processRender, undefined, self.canvasWidth, self.canvasHeight);
40351			};
40352			
40353			processRender = function(img) {
40354				// initial state
40355				if (typeof img === 'undefined') {
40356					self.updateMessage(gettext('render...')).show();
40357					renderRequest();
40358				} else if (img) {
40359					self.canvas.find('img').remove();
40360					self.canvas.append(img);
40361					self.updateMessage().hide();
40362					finishRender();
40363				} else if (typeof img !== 'undefined') {
40364					if (errors < self.settings.renderErrorCount) {
40365						renderCountdown(self.settings.renderErrorTimeout * (++errors));
40366					} else {
40367						// remove message at all
40368						self.updateMessage('');
40369						finishRender();
40370					}
40371				}
40372			};
40373		processRender();
40374	},
40375	
40376	/**
40377	 * Update render message
40378	 * @param {String} text
40379	 * @return {jQset}
40380	 */
40381	updateMessage: function(text) {
40382		text = text || '';
40383		return this._overlay.html(text);
40384	},
40385	
40386	/**
40387	 * Show big render with good quality
40388	 * @param {Event} e Click event
40389	 */
40390	bigRender: function(e) {
40391		if (e) {
40392			e.stop();
40393			if ($(e.target).closest('.button').hasClass('disabled')) {
40394				return;
40395			}
40396		}
40397		
40398		var self = this,
40399			info = self.settings.bigRender,
40400			addDisabled = true, // !!(myDeco.browser.isMobileSafari || myDeco.settings.WL_PROFILE || myDeco.settings.FACEBOOK_CANVAS),
40401			params = {};
40402
40403		// Additional params for popin window with advertise
40404		if (addDisabled) {
40405			params = {
40406				width: info[0] + 20,
40407				holderClassName: 'add-disabled'
40408			};
40409		}
40410
40411		myDeco.popin.open($.extend({
40412			url: '#view-render-template',
40413			borders: 10,
40414			width: info[0] + 170,
40415			tracking: 'big-render',
40416			afterOpen: function(div) {
40417				var renderImage,
40418					holder = div.find('.render-image').css({
40419						width: info[0],
40420						height: info[1]
40421					});
40422
40423				myDeco.popin.contentHolder().addClass('big-render');
40424
40425				div.find('.hd')
40426					.toggleClass('disabled', !self._hqRenderAvailable)
40427					.click(self._hqRenderCallback);
40428
40429				self._model.render(function(img) {
40430					renderImage = img.attr('src');
40431					holder.append(img);
40432				}, undefined, info[0], info[1], info[2]);
40433
40434				if (!addDisabled) {
40435					div.find('.block-render-add')
40436						.append($('<iframe src="' + myDeco.urls.promotion_adhost + '" scrolling="no"'
40437							+ 'marginwidth="0" marginheight="0" width="120" height="600">'));
40438				}
40439			}
40440		}, params));
40441	}
40442});
40443
40444myDeco.App.views.push(RenderView);
40445/*globals $, AppController, myDeco, numberInputKeyPress */
40446
40447/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
40448          sloppy: true, white: true */
40449var ComponentInfoController = AppController.extend({
40450	
40451	/**
40452	 * Component type
40453	 * @type {String}
40454	 * @public
40455	 * @static
40456	 */
40457	$type: 'component-info',
40458	
40459	/**
40460	 * Register view in controller
40461	 * @param {FPEView} view
40462	 */
40463	registerView: function(view) {
40464		this._super(view);
40465		
40466		var self = this,
40467			dimensions = view.pages.find('.number-input input'),
40468			model = myDeco.App.model,
40469			parseFunction = function(v) {
40470				return v.fromUnits().shift();
40471			},
40472			incrementFunction = function(shift) {
40473				var a = myDeco.settings
40474					.UNITS_LIST[myDeco.settings.UNITS][+shift + 2]
40475						.fromViewUnits();
40476				return a;
40477			},
40478			formatFunction = function() {
40479				return Array.prototype.slice.call(arguments).toUnits();
40480			};
40481		
40482		myDeco.body
40483			.bind('roomLoaded roomCleared roomSaved materialAdded materialRemoved roomMetadata', function(e, sameRoom) {
40484				view.updateScenePage();
40485				// for real rooms, not empty one
40486				if (e.type === 'roomLoaded' && !sameRoom && myDeco.App.model.walls.length !== 0) {
40487					myDeco.body.trigger('showInfo');
40488				}
40489			})
40490			.bind('unselectEnd operationEnd showInfo unitsChanged', function(e, entity) {
40491				var entities = model.selected(),
40492					destination = 'scene',
40493					page = destination;
40494				// skip on add operation
40495				if ($.isArray(entity) && (entity[0] === 'add' ||
40496						(entity[0] === 'material' && entity[1] !== true))) {
40497					return;
40498				}
40499				
40500				entity = $.isPlainObject(entity) ? entity : (entities.length === 1 ? entities[0] : false);
40501
40502				if (entity && entity.type) {
40503					switch(entity.type) {
40504						case 'wall':
40505						case 'shape':
40506						case 'camera':
40507							destination = entity.type;
40508							break;
40509						case 'door':
40510						case 'window':
40511						case 'component':
40512							destination = 'component';
40513							break;
40514					}
40515					page = entity.type;
40516				}
40517
40518				self._view.pages.hide();
40519				
40520				if (destination) {					
40521					self._view['update' + destination.capitalize() + 'Page'].call(self._view, entity);
40522					self._view.pages.filter('.' + page).show();
40523				}
40524
40525                if (entity && (
40526                    (entity.type == 'wall' && !(myDeco.settings.WL && myDeco.settings.WL.NO_WALL_PROPERTIES)) ||
40527                    ($.inArray(entity.type, ['window', 'door', 'component', 'shape', 'camera']) !== -1))) {
40528					    myDeco.body.trigger('toggleSidebar', ['info']);
40529                }
40530			});
40531		
40532		// scenery
40533		view.pages.filter('.scene').find('.button').click(function(e) {
40534			e.stop();
40535			
40536			model.removeEnvironment('image');
40537			model.operationEnd(['material']);
40538
40539			self._view.updateScenePage();
40540		});
40541		
40542		view.pages.filter('.shape')
40543			.find('.ceiling .change, .ceiling .delete, .floor .change, .floor .delete').click(function(e) {
40544				e.stop();
40545				
40546				var button = $(this),
40547					type = button.closest('.floor').size() !== 0 ? 'floor' : 'ceiling';
40548				
40549				if (button.hasClass('delete')) {
40550					model.assignMaterial(model.selected(true), model.settings.defaultMaterials[type], type);
40551					model.operationEnd(['material']);
40552				} else {
40553					myDeco.body.trigger('selectColor', [model.selected(true)[type + 'Material'].component_id]);
40554				}
40555			});
40556		
40557		// doors
40558		view.pages.filter('.door').find('input:radio').change(function() {
40559			var input = $(this);
40560			
40561			if (!input.attr('checked')) {
40562				return;
40563			}
40564			
40565			model.selected(true).setAttribute(input.attr('name'), input.val());
40566		});
40567		
40568		// walls
40569		
40570		function updateArtCoordinates(selector) {
40571			var entity = model.selected(true),
40572				wallWidth = entity.getAttribute('width'),
40573
40574
40575				holder = selector.closest('.wall'),
40576				coeff = wallWidth / holder.width();
40577				
40578			model.assignPhoto(entity, false, {
40579				x: (parseInt(selector.css('left'), 10) + parseInt(selector.css('border-left-width'), 10) || 0) * coeff,
40580				y: (parseInt(selector.css('top'), 10) + parseInt(selector.css('border-top-width'), 10) || 0) * coeff,
40581				width: selector.width() * coeff,
40582				height: selector.height() * coeff
40583			});
40584			model.operationEnd(['material']);
40585		}
40586		
40587		view.pages.filter('.wall')
40588			// remove material from wall
40589			.find('.material .change, .material .delete').click(function(e) {
40590				e.stop();
40591				
40592				if ($(this).hasClass('delete')) {
40593					model.assignMaterial(model.selected(true), model.settings.defaultMaterials.wall);
40594					model.operationEnd(['material']);
40595				} else {
40596					myDeco.body.trigger('selectColor', [model.selected(true).material.component_id]);
40597				}
40598			}).end()
40599			.find('.thickness').delegate('input', 'change', function() {
40600				view.updateWallPage(model.selected(true)
40601					.setAttribute('thickness', +$(this).val()));
40602			}).end()
40603			.find('.art')
40604				// remove art from wall
40605				.find('.button').click(function(e) {
40606					e.stop();
40607					
40608					model.removePhoto(model.selected(true));
40609					model.operationEnd(['material']);
40610					
40611					view.updateWallPage(model.selected(true));
40612				}).end()
40613				.find('.selector')
40614					.mousedown(function(e) {
40615						if (e.isDefaultPrevented()) {
40616							return;
40617						}
40618						e.preventDefault();
40619						
40620						var selector = $(this),
40621							cx = parseInt(selector.css('left'), 10),
40622							cy = parseInt(selector.css('top'), 10),
40623							x = e.pageX, y = e.pageY,
40624							mousemove = function(e) {
40625								cx += e.pageX - x;
40626								cy += e.pageY - y;
40627								
40628								x = e.pageX;
40629								y = e.pageY;
40630								selector.css({
40631									'left': cx + 'px',
40632									'top': cy + 'px'
40633								});
40634							},
40635							holder = selector.closest('.wall'),
40636							endaction = function(e) {
40637								e.preventDefault();
40638								myDeco.body
40639									.unbind('mousemove', mousemove)
40640									.unbind('mouseup', endaction);
40641								holder.unbind('mouseleave', endaction);
40642								updateArtCoordinates(selector);
40643							};
40644							
40645						holder.bind('mouseleave', endaction);
40646						myDeco.body
40647							.bind('mouseup', endaction)
40648							.bind('mousemove', mousemove);
40649					})
40650					.find('.drag')
40651						.mousedown(function(e) {
40652							if (e.isDefaultPrevented()) {
40653								return;
40654							}
40655							e.preventDefault();
40656							
40657							var selector = $(this).parent(),
40658								w = selector.width(), h = selector.height(),
40659								x = e.pageX, y = e.pageY,
40660								mousemove = function(e) {
40661									w += e.pageX - x;
40662									h += e.pageY - y;
40663									x = e.pageX;
40664									y = e.pageY;
40665									selector.width(w).height(h);
40666								};
40667							
40668							myDeco.body
40669								.one('mouseup', function() {
40670									myDeco.body.unbind('mousemove', mousemove);
40671									updateArtCoordinates(selector);
40672								})
40673								.bind('mousemove', mousemove);
40674						})
40675					.end()
40676				.end()
40677				.find('input[type=checkbox]').change(function() {
40678					var input = $(this),
40679						options = {};
40680					options[input.attr('name')] = input.is(':checked');
40681					myDeco.App.model.assignPhoto(model.selected(true), false, options);
40682					myDeco.App.model.operationEnd(['material']);
40683				});
40684		
40685		myDeco.body.bind('unitsChanged', function() {
40686			view.pages.filter('.wall').find('.tickness .dimension')
40687				.each(function() {
40688					var element = $(this);
40689					element.html([element.data('dimension')].toUnits());
40690				});
40691		});
40692		
40693		view.pages.filter('.component')
40694			.find('.wishlist').click(function(e) {
40695				e.stop();
40696				
40697				var link = $(this);
40698				
40699				myDeco.body.trigger('component.wishlist', [link.attr('rel'), function() {
40700					link.closest('.component').find('.wishlist, .wishlisted').hide().filter('.wishlisted').show();
40701				}]);
40702			});
40703		
40704		view.pages.filter('.door, .window').find('.colours')
40705			.delegate('a', 'click', function(e) {
40706				e.stop();
40707
40708				var element = $(this),
40709					id = element.data('id');
40710				
40711				element
40712					.closest('ul')
40713						.find('a').removeClass('current')
40714						.end()
40715					.end()
40716					.addClass('current');
40717
40718				myDeco.App.ws.proxy.get('material', id, function(result) {
40719					if (result) {
40720						self._model.assignMaterial(self._model.selected(true), result);
40721						self._model.operationEnd(['material', true]);
40722					}
40723				});
40724			});
40725		
40726		// wall dimensions
40727		dimensions.filter('*[name=width], *[name=height]').numberInput({
40728			formatFunction: formatFunction,
40729			parseFunction: parseFunction,
40730			positive: true,
40731			increment: incrementFunction,
40732			keyPress: numberInputKeyPress
40733		}).change(function(e) {
40734			var input = $(this),
40735				value = input.data('value'),
40736				type = input.attr('name'),
40737				entity = model.selected(true),
40738				newValue = false;
40739			
40740			newValue = entity.setAttribute(type, value).getAttribute(type);
40741			
40742			if (newValue !== false && value !== newValue) {
40743				input.trigger('setValue', newValue);
40744			}
40745		});
40746		
40747		// window position
40748		dimensions.filter('*[name=distance]').numberInput({
40749			formatFunction: formatFunction,
40750			parseFunction: parseFunction,
40751			positive: true,
40752			increment: incrementFunction,
40753			keyPress: numberInputKeyPress
40754		}).change(function(e) {
40755			var input = $(this),
40756				value = input.data('value'),
40757				entity = model.selected(true);
40758				
40759			entity.setFloorDistance(value);
40760		});
40761
40762        $('#drop-the-floor').click(function() {
40763            myDeco.body.trigger('actionRequest', [["remove", 46, true]]);
40764        });
40765
40766        $('#drop-the-room').click(function() {
40767            myDeco.body.trigger('actionRequest', [["remove"]]);
40768        });
40769	}
40770});
40771
40772myDeco.App.controllers.push(ComponentInfoController);
40773/*globals $, AppView, material2Style, myDeco, appImage2Thumbnail, toSquareUnits,
40774          window, buildThumbnailLink, tmpl,
40775          toDimensionsString, updateMaterialInfo */
40776/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
40777          sloppy: true, white: true */
40778
40779var ComponentInfoView = AppView.extend({
40780	
40781	/**
40782	 * Component type
40783	 * @type {String}
40784	 * @public
40785	 * @static
40786	 */
40787	$type: 'component-info',
40788	
40789	/**
40790	 * Init view
40791	 * @param {Object} options
40792	 */
40793	init: function(options) {
40794		var self = this;
40795
40796		this._super(options);
40797
40798		// cache door/window predefined materials
40799		myDeco.App.ws.proxy.fetch('material', this._model.settings.finishes, function(result) {
40800			self._model.settings.finishes = $.map(self._model.settings.finishes,
40801				function(id) {
40802					return result[id] || null;
40803				});
40804		}, true);
40805
40806		this.pages = this.canvasElement.children();
40807		
40808		if ($('su\\:badge').size()) {
40809			$.ajax({dataType: "script", cache: true,
40810					url: '//platform.stumbleupon.com/1/widgets.js'});
40811		}
40812
40813/*		if (!myDeco.browser.isIE) {		
40814			this._appendFacebookLikeButton("#recommend-mydeco-facebook-button");
40815			this._appendFacebookLikeButton("div.dulux-facebook-button");
40816		}
40817*/	},
40818	
40819	/**
40820	 * Add social facebook like button to the info panel
40821	 * 
40822	 * @param {String} container selector
40823	 */	
40824	_appendFacebookLikeButton: function(container) {
40825		var facebookContainer = $(container),
40826			facebookPath,
40827			facebookLike;	
40828			
40829		if (facebookContainer.size()) {
40830			facebookLike = $(document.createElement("iframe"));
40831				
40832			facebookPath = '//www.facebook.com/plugins/like.php?href='
40833				+ facebookContainer.data("likeurl") + '&layout=box_count&send_button=false'; 
40834
40835			facebookLike
40836				.attr({'src' : facebookPath, 'scrolling' : 'no', 'frameborder' : 0})
40837				.addClass("facebook-button")
40838				.appendTo(facebookContainer);			
40839		}	
40840	},
40841	
40842	/**
40843	 * Update pane with the scene information
40844	 */
40845	updateScenePage: function() {
40846		var page = this.pages.filter('.scene'),
40847			model = myDeco.App.model,
40848			materialsCount = $.map(model.materials, function(material) {
40849				var id = material.ID.substring(material.ID.indexOf(':') + 1);
40850				if (id.match(/Default$/)) {
40851					return null;
40852				}
40853				return id;
40854			}).unique().length,
40855			scrapbooks = $.map(model.materials, function(material) { return material.decal_texture || null; }),
40856			scenery = page.find('.scenery').toggle(!!model.environment.image),
40857			original = page.find('.original').toggle(!!model._metadata.original_author);
40858		
40859		if (model.environment.image) {
40860			scenery
40861				.find('.name').text(model.environment.image.replace(/^.*\/(.*)$/, '$1')).end()
40862				.find('.thumb').css('background-image', 'url(' + appImage2Thumbnail(model.environment.image, 70, 70) + ')');
40863			scrapbooks.push(model.environment.image);
40864		}
40865		
40866		if (model._metadata.original_author) {
40867			original.find('dd a').text(model._metadata.original_author).attr('href', myDeco.urls.publicUserPageStub.replace('username', model._metadata.original_author));
40868		}
40869		
40870		scrapbooks = scrapbooks.unique();
40871		
40872		// update stats
40873		page.find('.name').text(model._metadata.name).parent().toggle(!!model._metadata.name).end().end()
40874			.find('.description').text(model._metadata.description).parent().toggle(!!model._metadata.description).end().end()
40875			.find('.wallsCount').text(model.walls.length).end()
40876			.find('.doorsCount').text(model.doors.length).end()
40877			.find('.windowsCount').text(model.windows.length).end()
40878			.find('.componentsCount').text(model.components.length).end()
40879			.find('.productsCount').text(model.components.length).end()
40880			.find('.materialsCount').text(materialsCount).end()
40881			.find('.scrapbookCount').text(scrapbooks.length).end();
40882	},
40883	
40884	/**
40885	 * Update pane with the room information
40886	 * @param {Object} shape Room shape entity
40887	 */
40888	updateShapePage: function(shape) {
40889		var page = this.pages.filter('.shape'),
40890			wallsCount = shape._walls.length,
40891			doorsCount = 0, windowsCount = 0, productsCount = 0,
40892			materials = [], scrapbook = [], x, y, pos, id, wall;
40893		
40894		updateMaterialInfo(shape.ceilingMaterial, page.find('.ceiling')).toggle(shape.ceilingMaterial.ID.indexOf('Default') === -1);
40895		updateMaterialInfo(shape.floorMaterial, page.find('.floor')).toggle(shape.floorMaterial.ID.indexOf('Default') === -1);
40896		
40897		for (x = wallsCount - 1; x >= 0; x--) {
40898			wall = shape._walls[x];
40899			
40900			for (y = wall.components.length - 1; y >= 0; y--) {
40901				switch (wall.components[y].category) {
40902					case 'Door':
40903						doorsCount++;
40904						break;
40905					case 'Window':
40906						windowsCount++;
40907						break;
40908					default:
40909						productsCount++;
40910				}
40911			}
40912			
40913			pos = wall.material.ID.indexOf(':');
40914			
40915			if (pos === -1) {
40916				id = wall.material.ID;
40917			} else {
40918				id = wall.material.ID.substring(pos + 1);
40919				scrapbook.push(wall.material.ID.substring(0, pos));
40920			}
40921			
40922			if (!id.match(/Default$/)) {
40923				materials.push(id);
40924			}
40925		}
40926		
40927		for (x = myDeco.App.model.components.length - 1; x >=0; x--) {
40928			if (!myDeco.App.model.components[x].parent && myDeco.App.model.components[x].position.inPoly(shape)) {
40929				productsCount++;
40930			}
40931		}
40932		
40933		page.find('.data')
40934			.find('.area').text(Math.abs(shape.getArea()).toSquareUnits()).end()
40935			.find('.wallsCount').text(wallsCount).end()
40936			.find('.doorsCount').text(doorsCount).end()
40937			.find('.windowsCount').text(windowsCount).end()
40938			.find('.productsCount').text(productsCount).end()
40939			.find('.materialsCount').text(materials.unique().length).end()
40940			.find('.scrapbookCount').text(scrapbook.unique().length);
40941	},
40942	
40943	/**
40944	 * Update pane with the wall info
40945	 * @param {Object} wall Wall entity
40946	 */
40947	updateWallPage: function(wall) {
40948		var page = this.pages.filter('.wall'),
40949			thickness = myDeco.App.model.settings.wallThickness,
40950			materials = page.find('.materials'),
40951			material = materials.find('.material'),
40952			art = materials.find('.art'),
40953			materialCss = material2Style(wall.material),
40954			wallWidth = wall.getAttribute('width'),
40955			wallHeight = wall.getAttribute('height'),
40956			wallBox, selector, width, height, coeff, repeatX, repeatY, i, ilen;
40957		
40958		// dimensions
40959		page.find('input[name=width]').trigger('setValue', [wallWidth]).end()
40960			.find('input[name=height]').trigger('setValue', [wallHeight]);
40961		
40962		selector = page.find('.thickness').html('');
40963		for (i = 0, ilen = thickness.length; i < ilen; i++) {
40964			$('<label />').append(
40965				$('<input type="radio" name="thickness" />')
40966					.val(thickness[i].value)
40967					.toggleAttr('checked', 'checked',
40968						thickness[i].value === wall.thickness)
40969				)
40970				.append(thickness[i].name + ' (')
40971				.append($('<span class="dimension">' +
40972					[thickness[i].value].toUnits() + '</span>')
40973						.data('dimensions', thickness[i].value))
40974				.append(')')
40975				.appendTo(selector);
40976		}
40977		
40978		// material
40979		if (wall.material.ID.indexOf('materialWallDefault') === -1 || typeof wall.material.photo !== 'undefined') {
40980			// material
40981			updateMaterialInfo(wall.material, material).toggle(wall.material.ID.indexOf('materialWallDefault') === -1);
40982			
40983			// wall art
40984			if (typeof wall.material.photo !== 'undefined') {
40985				wallBox = art.find('.wall').css(materialCss);
40986				selector = wallBox.find('.selector');
40987				// it's hardcoded because width of hidden element can't be
40988				// properly calculated
40989				width = 235;
40990				height = width * wallHeight / wallWidth;
40991				coeff = width / wallWidth;
40992				
40993				repeatX = art.find('input[name=repeatX]');
40994				repeatY = art.find('input[name=repeatY]');
40995				
40996				if (wall.material.photo.repeatX) {
40997					repeatX.attr('checked', 'checked');
40998				} else {
40999					repeatX.removeAttr('checked');
41000				}
41001				
41002				if (wall.material.photo.repeatY) {
41003					repeatY.attr('checked', 'checked');
41004				} else {
41005					repeatY.removeAttr('checked');
41006				}
41007				
41008				wallBox.width(width).height(height);
41009				selector
41010					.width(wall.material.photo.width * coeff)
41011					.height(wall.material.photo.height * coeff)
41012					.css({
41013						left: wall.material.photo.x * coeff - parseInt(selector.css('border-left-width'), 10),
41014						top: wall.material.photo.y * coeff - parseInt(selector.css('border-top-width'), 10)
41015					})
41016					.find('img').attr('src', appImage2Thumbnail(wall.material.decal_texture));
41017				
41018				art.show();
41019			} else {
41020				art.hide();
41021			}
41022			
41023			materials.show();
41024		} else {
41025			materials.hide();
41026		}
41027	},
41028
41029	/**
41030	 * Update component pane with its information
41031	 * @param {Object} component Door/Window/Component entity
41032	 * @param {String} ctype type of the component: 'door', 'window' or 'component'
41033	 */
41034	updateComponentPage: function(component) {
41035		var ctype = component.type,
41036			page = this.pages.hide().filter('.' + ctype),
41037			materialsHolder = page.find('.colours'),
41038			hinge = page.find('.hinge'),
41039			elevation = page.find('.elevation'),
41040			direction = page.find('.direction'),
41041			data, supplier, wishlist, gtsData;
41042		
41043		if (ctype === 'door' || ctype === 'window') {
41044			page.find('.type span').text(component.componentData.name).end()
41045				.find('input[name=width]').trigger('setValue', [component.getAttribute('width')]).end()
41046				.find('input[name=height]').trigger('setValue', [component.getAttribute('height')]);
41047
41048			// check for finiahes have plain objects instead of Number or String
41049			// it's possible when initial loading of materials isn't done yet
41050			if ($.isPlainObject(this._model.settings.finishes[0])) {
41051				page.find('.colours ul').html(tmpl('view-component-info-colours', {
41052					finishes: this._model.settings.finishes,
41053					component: component
41054				}));
41055			}
41056		}
41057		
41058		switch (ctype) {
41059			case 'window':
41060				//distance of the window
41061				page.find('input[name=distance]')
41062					.trigger('setValue', [component.getFloorDistance()]);
41063				break;
41064			case 'door':
41065				page.find('input[name=distance]')
41066					.trigger('setValue', [component.getFloorDistance()]);
41067				// hinge & opening of the door
41068				if (component.open_side === 'l' || component.open_side === 'r') {
41069					hinge.find('input').removeAttr('checked')
41070						.filter('[value=' + component.open_side + ']')
41071						.attr('checked', 'checked');
41072					hinge.show();
41073				} else {
41074					hinge.hide();
41075				}
41076				
41077				if (component.open_direction === 'i' || component.open_direction === 'o') {
41078					direction.find('input').removeAttr('checked')
41079						.filter('[value=' + component.open_direction + ']')
41080						.attr('checked', 'checked');
41081					direction.show();
41082				} else {
41083					direction.hide();
41084				}
41085				break;
41086			default:
41087				if(component.level === -1) {
41088					// set the elevation
41089					page.find('input[name=distance]')
41090						.trigger('setValue', [component.getFloorDistance()]);
41091					elevation.show();
41092				} else {
41093					elevation.hide();
41094				}
41095
41096				data = component.componentData;
41097				gtsData = {
41098					guid: component.product_id || component.component_id,
41099					category: data.category,
41100					price: data.price
41101				};
41102
41103				page.toggleClass('unavailable', !data.name).toggleClass('empty', !data.available);
41104				page
41105					.find('.thumbnail').css('background-image', 'url(' + component.render_path + ')').end()
41106					.find('.name').html(data.name).end()
41107					.find('.dimensions').html(toDimensionsString(component));
41108				
41109				supplier = page.find('.supplier').toggle(!!data.supplier).find('span, a').text(data.supplier).hide();
41110				if (data.supplier_url) {
41111					supplier.filter('a').attr('href', data.supplier_url).show();
41112				} else {
41113					supplier.filter('span').show();
41114				}
41115
41116                if (myDeco.settings.component_page_hook) {
41117                    myDeco.settings.component_page_hook(data, page);
41118                }
41119
41120				if(data.price && data.price > 0 && (!myDeco.settings.WL || data.supplier_id == myDeco.settings.WL.WL_SUPPLIERS)) {
41121					page.find('.price').text(myDeco.formatPrice(data.price || 0, data.locale)).end()
41122				} else {
41123					page.find('.price').html("&nbsp;").end();
41124				}
41125
41126				if(data.url && (!myDeco.settings.WL || data.supplier_id == myDeco.settings.WL.WL_SUPPLIERS)) {
41127					page.find('.gts').data('gts', gtsData).attr('href', data.url).end()
41128					page.find('.gts').css('height', '34px');
41129				} else {
41130					// a hack to hide the Buy button
41131					page.find('.gts').css('height', '0px');
41132				}
41133
41134				if(data.out_of_stock) {
41135					page.find('.price').html("&nbsp;").end();
41136					page.find('.gts').css('height', '0px');
41137				}
41138
41139                if (myDeco.settings.WL && data.supplier_id == myDeco.settings.WL.WL_SUPPLIERS) {
41140				    page.find('.description').toggle(!!data.description).html(data.description || '');
41141                }
41142
41143				if (data.available) {
41144					page
41145						.find('.price').text(myDeco.formatPrice(data.price || 0, data.locale)).end()
41146						.find('.details').toggle(!!data.url).attr('href', data.url || '').toggleClass('gts', !!myDeco.settings.WL).end()
41147						.find('.gts').data('gts', gtsData)
41148							.not('.details')
41149								.attr('href', data.url).end()
41150							// don't show button and details link if WL_SUPPLIERS_BRANDED is set and current supplier isn't there
41151							.toggle(!myDeco.settings.WL || (!!myDeco.settings.WL.WL_SUPPLIERS_BRANDED === false || 
41152								(!!myDeco.settings.WL.WL_SUPPLIERS_BRANDED && $.inArray(data.supplier_id, myDeco.settings.WL.WL_SUPPLIERS_BRANDED.split(',')) !== -1)))
41153						.end()
41154						.find('.description').toggle(!!data.description).html(data.description || '');
41155
41156					wishlist = page.find('.wishlist, .wishlisted').hide().filter('.wishlist').attr('rel', component.component_id).end();
41157
41158					myDeco.body.trigger('component.wishlisted', [component.component_id, function(result) {
41159						if (typeof result !== 'undefined') {
41160							wishlist.filter(result ? '.wishlisted' : '.wishlist').show();
41161						}
41162					}]);
41163				}
41164
41165		}
41166	},
41167	
41168	/**
41169	 * Update pane with the Camera info
41170	 * @param {Object} win Camera entity
41171	 */	
41172	updateCameraPage: function(entity) {
41173		// It's empty because we have no information to observe
41174	}	
41175});
41176
41177myDeco.App.views.push(ComponentInfoView);
41178/*globals ItemsListModel, $, myDeco, appImage2Thumbnail */
41179/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
41180          sloppy: true, white: true */
41181/**
41182 * @class ComponentListModel
41183 * @classDescription Model class for wishlisted and current room component fetching
41184 */
41185var ComponentListModel = ItemsListModel.extend({
41186	
41187	/**
41188	 * Model type
41189	 * @type {String}
41190	 * @public
41191	 * @static
41192	 */
41193	$type: 'component-list',
41194	
41195	/**
41196	 * Model source, can be set to 'room' and 'wishlist'
41197	 * @type {String}
41198	 * @public
41199	 */
41200	src: 'component',
41201	
41202	/**
41203	 * List of wishlisted _components_, not products
41204	 * @type {Array}
41205	 */
41206	_wishlist: [],
41207	
41208	/**
41209	 * List of scrapbook images of owner
41210	 * @type {Array}
41211	 */
41212	_scrapbook: [],
41213	
41214	/**
41215	 * Flag indicated wishlist fetching status
41216	 * @type {Boolean}
41217	 */
41218	_wishlistLoaded: false,
41219	
41220	/**
41221	 * Flag indicated scrapbook fetching status
41222	 * @type {Boolean}
41223	 */
41224	_scrapbookLoaded: false,
41225	
41226	/**
41227	 * Number of items fetched per request
41228	 */
41229	_scrapbookLimit: 60,
41230	
41231	_scrapbookLoadingAvailable: true,
41232	
41233	/**
41234	 * Fetch scrapbook list
41235	 * @param {Function} callback
41236	 */
41237	fetchScrapbook: function(callback, append) {
41238		var self = this;
41239
41240		if (!append) {
41241			this._scrapbookStart = 0;
41242			this._scrapbookLoaded = false;
41243			this._scrapbookLoadingAvailable = true;
41244			this._scrapbook = [];
41245			this.itemsIndex.scrapbook = {};
41246		}
41247		
41248		if (myDeco.username && this._scrapbookLoadingAvailable) {
41249			$.ajax({
41250				url: myDeco.urls.ws.userRooms,
41251				data: {
41252						type: 'photo',
41253						api: 2,
41254						startrank: this._scrapbookStart,
41255						count: this._scrapbookLimit
41256				},
41257				cache: false,
41258				success: function(data) {
41259					self._scrapbookLoadingAvailable =
41260						data.length === self._scrapbookLimit;
41261					self._scrapbookLoaded = true;
41262					self._scrapbookStart += data.length;
41263					
41264					data = $.map(data, function(item) {
41265						var id = String(item[1].id);
41266						$.extend(item[1], {
41267							'component_id': id,
41268							'class': 'photo',
41269							'texture': item[1].image
41270								.replace(/^.*\/(rooms\/images\/.*)$/, '$1')
41271								.replace(/^.*\/(scrapbook_images\/.*)$/, '$1'),
41272							'thumbnail': item[1].thumbnail && item[1].thumbnail.replace('169x127', '70x70') || appImage2Thumbnail(item[1].image, 70, 70)
41273						});
41274						self.itemsIndex.scrapbook[id] = item[1];
41275						return item[1];
41276					});
41277					
41278					self._scrapbook.push.apply(self._scrapbook, data);
41279					self.found = self._scrapbook.length;
41280					if (self._scrapbookLoadingAvailable) {
41281						self.found += '+';
41282					}
41283					if ($.isFunction(callback)) {
41284						callback(data);
41285					}
41286				}
41287			});
41288		} else {
41289			callback([]);
41290		}
41291	},
41292	
41293	/**
41294	 * Fetch wishlist components
41295	 * @param {Function} callback
41296	 */
41297	fetchWishlist: function(callback) {
41298		var self = this;
41299		if (myDeco.username) {
41300			this._wishlistLoaded = false;
41301			$.getJSON(myDeco.addCacheMissParam(myDeco.urls.ws.userShortlistedProducts), function(data) {
41302				// filter wishlist by components with 3d info
41303				self._wishlist = [];
41304				$.each($.values(data), function() {
41305					if (this && this['3d_component_id'] && self._wishlist.indexOf(this['3d_component_id']) === -1) {
41306						self._wishlist.push(this['3d_component_id']);
41307					}
41308				});
41309				// request 3D info for specified components
41310				myDeco.App.ws.proxy.fetch('component', self._wishlist, function(data) {
41311					self._wishlistLoaded = true;
41312					$.extend(true, self.itemsIndex.component, data);
41313					if ($.isFunction(callback)) {
41314						callback();
41315					}
41316				});
41317			});
41318		}
41319	},
41320	
41321	/**
41322	 * Fetching wishlist from a backend for current user
41323	 * @param {Function} callback Callback called after fetching
41324	 */
41325	fetchLists: function(callback) {
41326		var self = this,
41327			internalCallback = function() {
41328				if (self._wishlistLoaded && self._scrapbookLoaded && $.isFunction(callback)) {
41329					callback();
41330				}
41331			};
41332		
41333		this.reset();
41334		
41335		if (myDeco.username) {
41336			this.fetchWishlist(internalCallback);
41337			this.fetchScrapbook(internalCallback);
41338		} else if ($.isFunction(callback)) {
41339			callback();
41340		}
41341	},
41342	
41343	/**
41344	 * Clear all internal owner-related storages
41345	 */
41346	reset: function() {
41347		this._wishlistLoaded = false;
41348		this._scrapbookLoaded = false;
41349		this._wishlist = [];
41350		this._scrapbook = [];
41351		this._super();
41352	},
41353	
41354	/**
41355	 * Set internal representation to a wishlisted products
41356	 * @param {Function} callback Callback executed when done
41357	 */
41358	setWishlist: function(callback) {
41359		this.src = 'wishlist';
41360		this.type = 'component';
41361		this._setItems(this._wishlist, callback);
41362	},
41363	
41364	/**
41365	 * Set internal representation to a current room components
41366	 * @param {Function} callback Callback executed when done
41367	 */
41368	setComponent: function(callback) {
41369		this.type = this.src = 'component';
41370		this._setItems(myDeco.App.model.uniqueComponentIds(), callback);
41371	},
41372	
41373	/**
41374	 * Set internal representation to a scrapbook list
41375	 * @param {Function} callback Callback executed when done
41376	 */
41377	setScrapbook: function(callback) {
41378		this.type = this.src = 'scrapbook';
41379		this._setItems(this._scrapbook, callback);
41380	},
41381	
41382	/**
41383	 * Set internal representation to a materials list
41384	 * @param {Function} callback Callback executed when done
41385	 */
41386	setMaterial: function(callback) {
41387		this.type = this.src = 'material';
41388		this._setItems(myDeco.App.model.uniqueMaterialIds(), callback);
41389	},
41390	
41391	/**
41392	 * Fill internal structures and prepare variables with the given items list
41393	 * @param {Array} items List of component ID
41394	 * @param {Function} callback Callback executed when done
41395	 * @private
41396	 */
41397	_setItems: function(items, callback) {
41398		callback = callback || function() {};
41399		var self = this;
41400		
41401		if (items.length > 0) {
41402			switch (this.type) {
41403				case 'scrapbook':
41404					this.items = this._scrapbook;
41405					this.found = this._scrapbook.length;
41406					if (this._scrapbookLoadingAvailable) {
41407						this.found += '+';
41408					}
41409					callback();
41410					break;
41411				case 'material':
41412					myDeco.App.ws.proxy.fetch(this.type, items, function(data) {
41413						$.extend(self.itemsIndex[self.type], data);
41414						self.items = $.map(items, function(e) { return self.itemsIndex[self.type][e]; });
41415						self.found = self.items.length;
41416						callback();
41417					});
41418					break;
41419				// wihlist and room components
41420				case 'component':
41421					myDeco.App.ws.proxy.fetch(this.type, items, function(data) {
41422						$.extend(self.itemsIndex[self.type], data);
41423						self.items = $.map(items, function(e) { return self.itemsIndex[self.type][e]; });
41424						self.found = self.items.length;
41425						callback();
41426					});
41427					break;
41428			}
41429		} else {
41430			this.items = [];
41431			this.found = 0;
41432			callback();
41433		}
41434	},
41435	
41436	/**
41437	 * Wishlist given component
41438	 * @param {String} or {Array} guid Component GUID
41439	 * @param {Function} callback Callback executed when done
41440	 */
41441	wishlist: function(guid, callback) {
41442		var self = this,
41443			guids = $.isArray(guid) ? guid : [guid],
41444			products = [];
41445		$.each(guids, function(i, guid) {
41446			if ($.inArray(guid, self._wishlist) === -1) {
41447				self._wishlist.unshift(guid);
41448			}
41449
41450			myDeco.App.ws.proxy.fetch('component', guid, function(data) {
41451				products.push(data[guid].product_id);
41452			});
41453		});
41454
41455		myDeco.shortlist.sendOne($(), JSON.stringify(products), callback);
41456	}
41457});
41458/*globals ItemsListController, myDeco, $,  */
41459/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
41460          sloppy: true, white: true */
41461/**
41462 * @class ComponentListController
41463 * @classDescription Component list controller manage current room component list and wishlisted components
41464 */
41465var ComponentListController = ItemsListController.extend({
41466	
41467	/**
41468	 * Controller type
41469	 * @type {String}
41470	 * @public
41471	 * @static
41472	 */
41473	$type: 'component-list',
41474	
41475	/**
41476	 * Register given view
41477	 * @param {Object} view
41478	 */
41479	registerView: function(view) {
41480		var self = this;
41481		
41482		this._super(view);
41483		
41484		// set loaded to view
41485		this._view.loading(true);
41486		
41487		// bind wishlist component checking
41488		myDeco.body
41489			.bind('component.wishlisted', function(e, guid, callback) {
41490				var guids = $.isArray(guid) ? guid : [guid],
41491					results = $.intersect(self._model._wishlist, guids);
41492				callback(self._model._wishlistLoaded ? ($.isArray(guid) ? results : results.length !== 0) : undefined);
41493			})
41494			.bind('scrapbookReload', function() {
41495				self._view.loading(true);
41496				self._model.fetchScrapbook(function() {
41497					self._view.loading(false);
41498					self.setBackend('scrapbook');
41499				});
41500			});
41501		
41502		this._view.canvasElement.find('.button.scrapbook').click(function(e) {
41503			e.stop();
41504			
41505			myDeco.user.loginRequired(self._view.showUpload.bind(self));
41506		});
41507		
41508		// fetch lists
41509		this._model.fetchLists(function() {
41510			self._view.canvasElement.find('.sources a').click(self.switchSource.bind(self));
41511			myDeco.body
41512				// update room components list on room change
41513				.bind('componentAdded componentRemoved materialAdded materialRemoved roomLoaded roomCleared loadRoom', function(e) {
41514					switch (self._model.src) {
41515						case 'component':
41516						case 'material':
41517							self.setBackend(self._model.src);
41518							break;
41519					}
41520				})
41521				// update lists on login/logout
41522				.bind('mydeco.login', function(e, data) {
41523					function updateLists() {
41524						switch (self._model.src) {
41525							case 'wishlist':
41526								self.setBackend('wishlist');
41527								break;
41528							case 'scrapbook':
41529								self.setBackend('scrapbook');
41530								break;
41531						}
41532					}
41533					if ($.keys(data).length > 0) {
41534						self._view.loading(true);
41535						self._model.fetchLists(function() {
41536							self._view.loading(false);
41537							updateLists();
41538						});
41539					} else {
41540						self._model.reset();
41541						updateLists();
41542					}
41543				})
41544				// wishlist component listener; guid can be either an array or single guid
41545				.bind('component.wishlist', function(e, guid, callback, start, finish) {
41546						self._model.wishlist(guid, function() {
41547						// avoid of switching if wishlisting has been made from room items tab
41548						if (self._model.src !== 'component') {
41549							self.setBackend('wishlist');
41550						}
41551						if (callback) {
41552							callback();
41553						}
41554					}, start, finish);
41555				});
41556			
41557			if (myDeco.App.model.loaded) {
41558				self.setBackend('component');
41559			}
41560			
41561		});
41562	},
41563	
41564	fetchMore: function() {
41565		var self = this;
41566		if (this._model.type === 'scrapbook') {
41567			this._view.scrollerLoading(true);
41568			this._model.fetchScrapbook(function(data) {
41569				self._view.appendItems(data);
41570				self._view.scrollerLoading(false);
41571			}, true);
41572		}
41573	},
41574	
41575	/**
41576	 * Switch list source
41577	 * @param {Event} e
41578	 */
41579	switchSource: function(e) {
41580		e.stop();
41581		
41582		var link = $(e.target),
41583			element = link.closest('.source'),
41584			elements = element.siblings('.source'),
41585			type = link.attr('class');
41586		
41587		elements.removeClass('selected');
41588		element.addClass('selected');
41589		this.setBackend(type);
41590	},
41591	
41592	/**
41593	 * Set backend type depending on storage
41594	 * @type {String} type backend type
41595	 */
41596	setBackend: function(type) {
41597		// call model
41598		var self = this;
41599
41600		this._view._setRoomMarker = type === 'wishlist';
41601		
41602		this._model['set' + type.capitalize()].call(this._model, function() {
41603			self._view.draw();
41604			self._view._scrollerHolder.removeClass('scrapbook wishlist component')
41605				.addClass(type);
41606		});
41607	},
41608	
41609	/**
41610	 * Bind to model events to reflect the changes on the pane
41611	 */
41612	bindRelatedEvents: function() {
41613		var self = this;
41614		// will update only the whishlist page if it already shown
41615		myDeco.body.bind('componentAdded componentRemoved', function(e, component) {
41616			var components = myDeco.App.model.components,
41617				totalCost = 0,
41618				i=0;
41619			for (i=0; i < components.length; i++) {
41620				totalCost += components[i].componentData.price;
41621			}
41622			self._view.canvasElement.find('.total-cost').text(myDeco.formatPrice(totalCost || 0));
41623			
41624			if (self._model.src === 'wishlist' && self._view._setRoomMarker) {
41625				self._view._canvas.find('.' + self._model.type + '-' + component.component_id)
41626					.toggleClass(self._view.settings.inRoomClass, myDeco.App.model.isComponentInModel(component));
41627			}			
41628		});
41629	}
41630});
41631
41632/**
41633 * Scrapbook upload response
41634 * @param {String} response HTML response
41635 */
41636function FPEscrapbookUploadResponse(response) {
41637	if (typeof response.error !== 'undefined') {
41638		var form = myDeco.popin.content().find('form'),
41639			errors = form.find('div.error').hide(),
41640			name;
41641		
41642		myDeco.buttons.enable(form);
41643		
41644		for (name in response.error.message) {
41645			errors.filter('.' + name).text(response.error.message[name].shift()).show();
41646		}
41647		
41648		myDeco.popin.resize();
41649	} else {
41650		myDeco.body.trigger('scrapbookReload');
41651		myDeco.popin.close();
41652	}
41653}
41654
41655myDeco.App.controllers.push(ComponentListController);
41656/*globals ItemsListView, myDeco */
41657/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
41658          sloppy: true, white: true */
41659/**
41660 * @class ComponentListView
41661 * @classDescription Component list view class, used for displaying wishlisted and components in a current room 
41662 */
41663var ComponentListView = ItemsListView.extend({
41664	
41665	/**
41666	 * View type
41667	 * @type {String}
41668	 * @public
41669	 * @static
41670	 */
41671	$type: 'component-list',
41672	
41673	/**
41674	 * Icons size
41675	 * @type {String}
41676	 */
41677	mode: 'small',
41678
41679	draw: function() {
41680        if (!window._roomLoading) {
41681		    this._super();            
41682        }
41683	},
41684	
41685	/**
41686	 * Show upload popup
41687	 */
41688	showUpload: function() {
41689		myDeco.popin.open({
41690			url: '#scrapbook-popin',
41691			afterOpen: function(div) {
41692				var form = div.find('form');
41693				
41694				myDeco.buttons.initSubmitButtons(form.find('input:submit').removeClass('no-decorate'));
41695				myDeco.fileInputs.init(form);
41696				
41697				form.submit(function() {
41698					form.find('div.error').hide();
41699					myDeco.buttons.disable(form);
41700					myDeco.popin.resize();
41701				});
41702			}
41703		});
41704	}
41705	
41706});
41707
41708myDeco.App.views.push(ComponentListView);
41709/*globals myDeco, $, FpePlugin, window */
41710/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
41711          sloppy: true, white: true */
41712
41713var SnappingPlugin = FpePlugin.extend({
41714	/**
41715	 * Plugin type
41716	 * @type {String}
41717	 * @static
41718	 */
41719	$type: 'floor',
41720	
41721	/**
41722	 * Plugin name
41723	 * @type {String}
41724	 * @static
41725	 */
41726	$name: 'snapping',
41727	
41728	_settings: {
41729		autostart: true,
41730		
41731		/**
41732		 * Default visual attributes
41733		 */
41734		attributes: {
41735			'stroke': '#777',
41736			'stroke-width': 1
41737		}
41738	},
41739	
41740	/**
41741	 * Snapping lines
41742	 * @type {Object}
41743	 */
41744	_snaps: {
41745		x: false,
41746		y: false
41747	},
41748	
41749	/**
41750	 * Init function
41751	 * @param {FpeView} view FPE view
41752	 * @param {Object} options Plugin options
41753	 */
41754	init: function(view, options) {
41755		this._super(view, options);
41756		// bind to model events
41757		myDeco.body
41758			.bind('snappingOn', this.snapOn.bind(this))
41759			.bind('snappingOff', this.snapOff.bind(this));
41760	},
41761	
41762	/**
41763	 * Snap on listener
41764	 * @param {Event} e Snap event
41765	 * @param {String} type Snapping type
41766	 * @param {Point} p1 Snapping points #1
41767	 * @param {Point} p2 Snapping points #2 
41768	 */
41769	snapOn: function(e, axis, value) {
41770		var i;
41771		if (!this._snaps[axis]) {
41772			this._snaps[axis] = this._view.canvas
41773				.path(this._settings.attributes)
41774				.moveTo(-10000, -10000)
41775				.lineTo(10000, 10000)
41776				.hide();
41777		}
41778		
41779		if (axis === 'y') {
41780			value = -value;
41781		}
41782		
41783		this._snaps[axis].hold();
41784		for (i = 0; i < 2; i++) {
41785			this._snaps[axis].pathPoints[i].dropCache();
41786			this._snaps[axis].pathPoints[i][axis] = value;
41787		}
41788		this._snaps[axis].render().toFront().show();
41789	},
41790	
41791	/**
41792	 * Snap off listener
41793	 * @param {Event} e Snap event
41794	 * @param {String} axis Snapping type
41795	 */
41796	snapOff: function(e, axis) {
41797		var i, ilen,
41798			axes = axis ? [axis] : ['x', 'y'];
41799		
41800		for (i = 0, ilen = axes.length; i < ilen; i++) {
41801			if (this._snaps[axes[i]]) {
41802				this._snaps[axes[i]].hide();
41803			}
41804		}
41805	}
41806	
41807});
41808
41809myDeco.App.plugins.push(SnappingPlugin);
41810/*globals myDeco, $, FpePlugin, document, buildThumbnailLink */
41811/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
41812          sloppy: true, white: true */
41813/**
41814 * @fileOverview Dimension selector plugin with component scrolling
41815 */
41816var DimensionSelectorPlugin = FpePlugin.extend({
41817	/**
41818	 * View type
41819	 * @static
41820	 */
41821	$type: 'floor',
41822	
41823	/**
41824	 * Plugin name
41825	 * @type {String}
41826	 */
41827	$name: 'dimension-selector',
41828	
41829	/**
41830	 * Selector left coordinate
41831	 * @type {Number}
41832	 */
41833	x: undefined,
41834	
41835	/**
41836	 * Selector top coordinate
41837	 * @type {Number}
41838	 */
41839	y: undefined,
41840	
41841	/**
41842	 * Selector width
41843	 * @type {Number}
41844	 */
41845	width: undefined,
41846	
41847	/**
41848	 * Selector height
41849	 * @type {Number}
41850	 */
41851	height: undefined,
41852	
41853	/**
41854	 * Default settings
41855	 */
41856	_settings: {
41857		/**
41858		 * Value of width/height in px
41859		 * @type {Number}
41860		 */
41861		'default': 100
41862	},
41863	
41864	/**
41865	 * Parent view
41866	 * @type {FPEView}
41867	 */
41868	view: null,
41869	
41870	/**
41871	 * Selector element
41872	 * @type {jQset}
41873	 */
41874	selector: null,
41875	
41876	/**
41877	 * Flag for resize operation
41878	 * @type {Boolean}
41879	 */
41880	resize: false,
41881	
41882	/**
41883	 * Selector offset helper values
41884	 * @type {Number}
41885	 */
41886	offset: {x: 0, y: 0},
41887	
41888	/**
41889	 * Flag for dimension selection state
41890	 * @type {Number}
41891	 */
41892	set: false,
41893	
41894	/**
41895	 * Text info element
41896	 * @type {jQset}
41897	 */
41898	info: null,
41899	
41900	/**
41901	 * Resulting image element
41902	 * @type {jQset}
41903	 */
41904	image: null,
41905	
41906	/**
41907	 * Inner block of selector with loader etc
41908	 * @type {jQset}
41909	 */
41910	inner: null,
41911	
41912	/**
41913	 * List of controls
41914	 * @type {jQset}
41915	 */
41916	controls: null,
41917	
41918	/**
41919	 * Facet name events listen to
41920	 * @type {String}
41921	 */
41922	facetName: 'dimension',
41923	
41924	/**
41925	 * Clear listener
41926	 * @type {Function}
41927	 * @private
41928	 */
41929	_clear: null,
41930	
41931	/**
41932	 * Scale listener
41933	 * @type {Function}
41934	 * @private
41935	 */
41936	_scale: null,
41937	
41938	/**
41939	 * Cached callback for updateComponent method
41940	 * @type {Function}
41941	 */
41942	_updateComponent: null,
41943	
41944	/**
41945	 * Cached callback for updateByOtherFacet method
41946	 * @type {Function}
41947	 */
41948	_updateSelectorByOther: null,
41949	
41950	/**
41951	 * Show hint flag
41952	 * @type {Boolean}
41953	 */
41954	_showHint: true,
41955	
41956	/**
41957	 * Is help massage has been viewed or not
41958	 * @type {Boolean}
41959	 */
41960	_helpMessageViewed: false,
41961	
41962	/**
41963	 * Move bound function cache
41964	 * @type {Function}
41965	 */
41966	_moveCache: false,
41967	
41968	/**
41969	 * Type of controller/model which triggered opening
41970	 * @type {String}
41971	 */
41972	_invokerType: false,
41973	
41974	init: function(view, options) {
41975		this._super(view, options);
41976		var self = this;
41977		
41978		// cache event listeners for proper unbind
41979		this._scale = this.update.bind(this);
41980		this._clear = this.clear.bind(this);
41981		this._updateComponent = this.updateComponent.bind(this);
41982		this._updateSelectorByOther = this.updateSelectorByOther.bind(this);
41983		this._showHint = !$.cookie('hint-dimension');
41984		this._moveCache = this.move.bind(this);
41985		
41986		// main selector
41987		this.selector = $('<div class="dimension-facet" style="display:none"><div class="inner"><span>' +
41988				this.getText() + '</span><div class="loader" /><img src="/assets/austin/blank.gif" style="display:none" class="item" />' +
41989				'<div class="controls"><a class="prev" /><a class="drop" /><a class="next" /></div>' +
41990				'</div><div class="resizer" /><a class="close" /></div>')
41991			.mousedown(this.start.bind(this))
41992			.mouseup(this.stop.bind(this))
41993			.appendTo(this._view.canvasElement);
41994		// and nested elements
41995		this.inner = this.selector.find('.inner');
41996		this.info = this.inner.find('span');
41997		this.image = this.inner.find('img.item');
41998		this.controls = this.inner.find('.controls a')
41999			.mousedown(function(e) { e.preventDefault(); })
42000			// bind events to listeners
42001			.filter('.drop').mouseup(this.dropItem.bind(this)).end()
42002			.filter('.next, .prev').mouseup(this.switchItem.bind(this)).end();
42003		
42004		this.selector.find('.close').mouseup(function(e) {
42005				e.preventDefault();
42006				myDeco.body.trigger('controller-facet-toggle', [false, self.facetName, false]);
42007			})
42008			.mousedown(function(e) { e.preventDefault(); });
42009		myDeco.body
42010			.bind('controller-facet-toggle', this.toggle.bind(this))
42011			.bind('roomCleared', function() {
42012				myDeco.body.trigger('controller-facet-toggle', [false, self.facetName, false]);
42013			})
42014			.bind('unitsChanged', function() {
42015				self.info.html(self.getText());
42016			});
42017	},
42018	
42019	/**
42020	 * Toggle selector visibility
42021	 * @param {Event} e Event from search controller
42022	 * @param {String} type Invoker type
42023	 * @param {String} facetName
42024	 * @param {Boolean} state Selector visibility
42025	 */
42026	toggle: function(e, type, facetName, state) {
42027		
42028		facetName = facetName || this.facetName;
42029		state = state || false;
42030		
42031		// do not service other facets
42032		if (facetName !== this.facetName) {
42033			return;
42034		}
42035		
42036		var bubble;
42037		
42038		myDeco.body
42039			.unbind('controller-facet-cleared', this._clear)
42040			.unbind('view-' + this._view.$type + '-scaled', this._scale)
42041			.unbind('controller-data-fetched', this._updateSelectorByOther);
42042		
42043		// show selector
42044		if (state) {
42045			this._invokerType = type;
42046			this.x = this.y = undefined;
42047			if (!this.set) {
42048				this.width = this.height = undefined;
42049			}
42050			
42051			this.update();
42052			
42053			// hide not necessary items
42054			this.state(this.set ? 'empty' : 'default');
42055			
42056			if (this.set) {
42057				myDeco.body.trigger('controller-fetch-next', [this._invokerType, this._updateComponent, true]);
42058			}
42059			
42060			// listen to events
42061			myDeco.body
42062				.bind('controller-facet-cleared', this._clear)
42063				.bind('view-' + this._view.$type + '-scaled', this._scale)
42064				.bind('controller-data-fetched', this._updateSelectorByOther);
42065			
42066			this.selector.fadeIn('fast');
42067			
42068			if (this._showHint) {
42069				bubble = $('.bubble.drag-dimension').rearrange({
42070					anchor: this.selector.find('.resizer'),
42071					position: ['right', 'bottom']
42072				}).fadeIn('fast');
42073				$.cookie('hint-dimension', '1', {path: document.location.pathname});
42074				this._showHint = false;
42075				myDeco.body.one('mousedown', function() {
42076					bubble.fadeOut('fast');
42077				});
42078			}
42079		} else if (type === false || type === this._invokerType) {
42080			this.selector.fadeOut('fast');
42081		}
42082	},
42083	
42084	/**
42085	 * Mouse move event listener
42086	 * @param {Event} e Mousemove event
42087	 */
42088	move: function(e) {
42089		// calculate actual values
42090		var x = e.pageX - this.offset.x,
42091			y = e.pageY - this.offset.y;
42092		
42093		if (this.resize) {
42094			this.width = x / this._view.zoom;
42095			this.height = y / this._view.zoom;
42096			this.info.html(this.getText());
42097			this.set = true;
42098		} else {
42099			this.x = x;
42100			this.y = y;
42101		}
42102		this.update();
42103	},
42104	
42105	/**
42106	 * Update size and position of selector
42107	 * @public
42108	 */
42109	update: function() {
42110		var zoom = this._view.zoom,
42111			width, height;
42112		
42113		this.width = this.width || Math.round(this._settings['default'] / zoom);
42114		this.height = this.height || Math.round(this._settings['default'] / zoom);
42115		
42116		width = this.width * zoom;
42117		height = this.height * zoom;
42118		
42119		this.x = this.x || Math.ceil((this._view.canvasWidth - width) / 2);
42120		this.y = this.y || Math.ceil((this._view.canvasHeight - height) / 2);
42121		
42122		if (this.selector) {
42123			this.selector.css({
42124				left  : this.x + 'px',
42125				top   : this.y + 'px',
42126				width : width  + 'px',
42127				height: height + 'px'
42128			});
42129		}
42130	},
42131	
42132	/**
42133	 * Start operation listener
42134	 * @param {Event} e Mousedown event
42135	 */
42136	start: function(e) {
42137		if ($(e.target).closest('.controls').size() !== 0) {
42138			return;
42139		}
42140		
42141		if (e.isDefaultPrevented()) {
42142			return;
42143		}
42144		e.preventDefault();
42145		
42146		// select operation type by event starter
42147		this.resize = $(e.target).hasClass('resizer');
42148		
42149		var o = this.selector.offset(),
42150			p = this.selector.offsetParent().offset();
42151		this.offset = {
42152			x: e.pageX - o.left + p.left,
42153			y: e.pageY - o.top + p.top
42154		};
42155		
42156		if (this.resize) {
42157			this.state('resize');
42158			p = this.selector.position();
42159			this.offset = {
42160				x: this.offset.x - Math.round(this.width * this._view.zoom) + p.left,
42161				y: this.offset.y - (this.height * this._view.zoom) + p.top
42162			};
42163		}
42164		this._view.canvasElement.bind('mousemove', this._moveCache);
42165	},
42166	
42167	/**
42168	 * End of operation
42169	 * @param {Event} e
42170	 * @public
42171	 */
42172	stop: function(e) {
42173		e.preventDefault();
42174		
42175		this._view.canvasElement.unbind('mousemove', this._moveCache);
42176		
42177		// fire event if dimension is selected
42178		if (this.set && this.resize) {
42179			this.state('loading');
42180			myDeco.body.trigger('controller-facet-refine',
42181					[this._invokerType, this.facetName, {w: this.width, d: this.height}]);
42182			this.resize = false;
42183		}
42184	},
42185	
42186	/**
42187	 * Prepare text for info block
42188	 * @param {Number} width Selector width, defaults to this.width
42189	 * @param {Number} width Selector height, defaults to this.height
42190	 */
42191	getText: function() {
42192		return this.set ? [this.width || 0, this.height || 0].toUnits() : gettext('Dimension<br />cleared');
42193	},
42194	
42195	/**
42196	 * Clear current selection and hide selector
42197	 * @param {Event} e
42198	 * @param {String} facetName
42199	 */
42200	clear: function(e, type, facetName) {
42201		// do not service another facets
42202		if ((e && facetName !== this.facetName) || type !== this._invokerType) {
42203			return;
42204		}
42205		
42206		this.x = undefined;
42207		this.y = undefined;
42208		this.width = undefined;
42209		this.height = undefined;
42210		this.set = false;
42211		this.info.html(this.getText());
42212		this.state('default');
42213		this.toggle(false, type);
42214	},
42215	
42216	/**
42217	 * Method for updating selector block with result component
42218	 * @param {Object} item Component object
42219	 * @param {Number} position Current component position in stack
42220	 * @param {Number} found Number of totally found items
42221	 */
42222	updateComponent: function(item, position, found) {
42223		var self = this,
42224			width, height;
42225		if (item) {
42226			// update controls according to item ID and item position in stack
42227			this.controls
42228				.filter('.prev').toggleClass('disabled', position <= 0).end()
42229				.filter('.next').toggleClass('disabled', position >= found - 1).end()
42230				.filter('.drop').data('component', item.component_id);
42231			
42232			// update image attributes
42233			width = Math.round(item.width * this._view.zoom);
42234			height = Math.round(item.depth * this._view.zoom);
42235			this.image.css({
42236				width: width + 'px',
42237				height: height + 'px',
42238				left: Math.round((this.width * this._view.zoom - width) / 2) + 'px',
42239				top: Math.round((this.height * this._view.zoom - height) / 2) + 'px'
42240			}).one('load', function() {
42241				self.state('active');
42242				if (!self._helpMessageViewed) {
42243					myDeco.body.trigger('status',
42244						[gettext('Click arrows to preview furniture. Press «✓» to select.')]);
42245					self._helpMessageViewed = true;
42246				}
42247			}).attr({
42248				src: buildThumbnailLink(item.componentData['3d'].render_top),
42249				alt: item.componentData.name
42250			});
42251			
42252			// hack for already cached images (chrome)
42253			if (myDeco.browser.isChrome && this.image.get(0).complete) {
42254				this.image.trigger('load');
42255			}
42256		} else {
42257			self.state('empty');
42258		}
42259	},
42260	
42261	/**
42262	 * Listener for item dropping
42263	 * @param {Event} e
42264	 */
42265	dropItem: function(e) {
42266		e.preventDefault();
42267		
42268		var self = this;
42269		
42270		this.state('loading');
42271		// fetch component info and drop component to canvas
42272		myDeco.body.trigger('controller-component-info', [this._invokerType, $(e.target).data('component'), function(component) {
42273			var offset = self.selector.offset(),
42274				pos = self._view.toModelPosition(
42275					offset.left + parseInt(self.selector.css('border-left-width'), 10) + (self.width / 2) * self._view.zoom,
42276					offset.top + parseInt(self.selector.css('border-top-width'), 10) + (self.height / 2) * self._view.zoom),
42277				entity = $.cloneObject(component);
42278				
42279			delete entity.ID;
42280			
42281			self._view._model.addComponent(entity, pos.x, pos.y);
42282			self._view._model.operationEnd(['add']);
42283			
42284			// hide selector
42285			myDeco.body.trigger('controller-facet-toggle', [self._invokerType, self.facetName, false]);
42286		}]);
42287	},
42288	
42289	/**
42290	 * Listener for component switching in the list
42291	 * @param {Event} e
42292	 */
42293	switchItem: function(e) {
42294		e.preventDefault();
42295		
42296		var element = $(e.target),
42297			action = element.hasClass('prev') ? 'prev' : 'next';
42298		
42299		// do not serve disabled buttons 
42300		if (element.hasClass('disabled')) {
42301			return;
42302		}
42303		
42304		this.state('loading');
42305		myDeco.body.trigger('controller-fetch-' + action,
42306				[this._invokerType, this._updateComponent]);
42307	},
42308	
42309	/**
42310	 * Update selector with new set of items by events from search
42311	 * @param {Event} e
42312	 */
42313	updateSelectorByOther: function(e, type) {
42314		if (this.set && type && type === this._invokerType) {
42315			this.state('loading');
42316			// fetch first item
42317			myDeco.body.trigger('controller-fetch-next', [this._invokerType, this._updateComponent, true]);
42318		}
42319	},
42320	
42321	/**
42322	 * Hide/show layers by setting combinations of classNames to selector element
42323	 * @param {Boolean} state
42324	 */
42325	state: function(state) {
42326		switch (state) {
42327			case 'loading':
42328				this.selector.addClass('active loading').removeClass('resize empty');
42329				this.image.hide();
42330				break;
42331			case 'resize':
42332				this.selector.addClass('active resize').removeClass('empty loading');
42333				this.image.hide();
42334				break;
42335			case 'empty':
42336				this.selector.addClass('active empty').removeClass('resize loading');
42337				this.image.hide();
42338				break;
42339			case 'active':
42340				this.selector.addClass('active').removeClass('empty resize loading');
42341				this.image.show();
42342				break;
42343			default:
42344				this.image.hide();
42345				this.selector.removeClass('active resize loading empty');
42346		}
42347	}
42348});
42349
42350myDeco.App.plugins.push(DimensionSelectorPlugin);
42351/*globals myDeco, $, KEY */
42352/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
42353          sloppy: true, white: true */
42354myDeco.App.settings = $.extend(true, myDeco.App.settings, {
42355	model: {
42356		minHeight: 200,
42357		maxHeight: 500,
42358		shapes: {
42359			square: {
42360				name: 'Square',
42361				shape: [[1, 1], [1, 0], [0, 0], [0, 1]],
42362				width: 450,
42363				height: 450
42364			},
42365			lshaped1: {
42366				name: 'L Shaped 1',
42367				shape: [[1, 1], [1, 0.5], [0.5, 0.5], [0.5, 0], [0, 0], [0, 1]],
42368				width: 450,
42369				height: 450
42370			},
42371			lshaped2: {
42372				name: 'L Shaped 2',
42373				shape: [[1, 0.5], [1, 0], [0, 0], [0, 1], [0.5, 1], [0.5, 0.5]],
42374				width: 450,
42375				height: 450
42376			},
42377			lshaped3: {
42378				name: 'L Shaped 3',
42379				shape: [[1, 1], [1, 0], [0.5, 0], [0.5, 0.5], [0, 0.5], [0, 1]],
42380				width: 450,
42381				height: 450
42382			},
42383			lshaped4: {
42384				name: 'L Shaped 4',
42385				shape: [[0.5, 1], [1, 1], [1, 0], [0, 0], [0, 0.5], [0.5, 0.5]],
42386				width: 450,
42387				height: 450
42388			},
42389			rect1: {
42390				name: 'Rect 1',
42391				shape: [[1, 1], [1, 0], [0, 0], [0, 1]],
42392				width: 250,
42393				height: 450
42394			},
42395			rect2: {
42396				name: 'Rect 2',
42397				shape: [[1, 1], [1, 0], [0, 0], [0, 1]],
42398				width: 450,
42399				height: 250
42400			},
42401			alcove1: {
42402				name: 'Alcove 1',
42403				shape: [[1, 0.8], [1, 0], [0, 0], [0, 0.8], [0.25, 0.8], [0.25, 1], [0.75, 1], [0.75, 0.8]],
42404				width: 450,
42405				height: 450
42406			},
42407			alcove2: {
42408				name: 'Alcove 2',
42409				shape: [[0.8, 1], [0.8, 0.75], [1, 0.75], [1, 0.25], [0.8, 0.25], [0.8, 0], [0, 0], [0, 1]],
42410				width: 450,
42411				height: 450
42412			},
42413			alcove3: {
42414				name: 'Alcove 3',
42415				shape: [[1, 1], [1, 0.2], [0.75, 0.2], [0.75, 0], [0.25, 0], [0.25, 0.2], [0, 0.2], [0, 1]],
42416				width: 450,
42417				height: 450
42418			},
42419			alcove4: {
42420				name: 'Alcove 4',
42421				shape: [[1, 1], [1, 0], [0.2, 0], [0.2, 0.25], [0, 0.25], [0, 0.75], [0.2, 0.75], [0.2, 1]],
42422				width: 450,
42423				height: 450
42424			}
42425		},
42426		wallThickness: [
42427			{name: gettext('Thick wall'), value: 30},
42428			{name: gettext('Middle wall'), value: 17},
42429			{name: gettext('Thin wall'), value: 10}
42430		],
42431		// index from array above
42432		wallThicknessDefault: 1,
42433		finishes: [2649, 2652, 2653, 2657, 2658, 2660, 2661, 2662],
42434		doors: [
42435			{
42436				type: "flush-plain",
42437				name: gettext("Plain Door"),
42438				component_id: "f75c8910592b2bd613dcd1d2b60d5f237bb27a08",
42439				sizes: [[82, 208]]
42440			},
42441			{
42442				type: "moulded",
42443				name: gettext("Panel Door"),
42444				component_id: "cf28e9e5b7659cbc88e3ed735bdfc827606628ff",
42445				sizes: [[82, 208]]
42446			},
42447			{
42448				type: "patio",
42449				name: gettext("Patio Door"),
42450				component_id: "ed4398ca15141c396325b0903ac23fbacc960479",
42451				sizes: [[173, 208], [365, 220]]
42452			},
42453			{
42454				type: "folding",
42455				name: gettext("Folding Door"),
42456				component_id: "5dd92a4fd20cc337ef9cd009d620ef39ac051e54",
42457				sizes: [[240, 310]]
42458			},
42459			{
42460				type: "double-french",
42461				name: gettext("Glazed Double French"),
42462				component_id: "fa310873ea1f5a6816728bfc50d96dd0d1c619ec",
42463				sizes: [[171, 208]]
42464			},
42465			{
42466				type: "glazed",
42467				name: gettext("Glazed Door"),
42468				component_id: "0143d8e0e3c2de799c8661d5d97130bd248af342",
42469				sizes: [[82, 208]]
42470			},
42471			{
42472				type: "patio",
42473				name: gettext("Sliding Door"),
42474				component_id: "899f1d4c7b441166851b9e38eb28f0ade2837687",
42475				sizes: [[400, 300]]
42476			},
42477			{
42478				type: "doorway",
42479				name: gettext("Opening"),
42480				component_id: "56e44065a5645a008497882a7901c06811fe6298",
42481				sizes: [[82, 208]]
42482			},
42483			{
42484				type: "paned",
42485				name: gettext("Glazed Panel Door"),
42486				component_id: "1d07f9d6ba47a92d3bdb428f656ead081851f22f",
42487				sizes: [[82, 208]]
42488			},
42489			{
42490				type: "rectangular-opening",
42491				name: gettext("Rectangular Opening"),
42492				component_id: "482e0a08d4a4666fdad20c0bc8194bbebf223077",
42493				sizes: [[82, 208]]
42494			},
42495			{
42496				type: "round-opening",
42497				name: gettext("Round Opening"),
42498				component_id: "22b0042cf9b5710f2fc20a407af244e3e8a98147",
42499				sizes: [[90, 90]]
42500			},
42501			{
42502				type: "arched-opening",
42503				name: gettext("Arched Opening"),
42504				component_id: "1b4768a1cb1ffcacdc3a37de6eb3678a125ae34f",
42505				sizes: [[90, 160]]
42506			},
42507			{
42508				type: "half-round-opening",
42509				name: gettext("Half Round Opening"),
42510				component_id: "14f0695b5b699d2cde9ac56b72d064c714e440a9",
42511				sizes: [[120, 60]]
42512			}
42513		],
42514		windows: [
42515			{
42516				type: "sash",
42517				name: gettext("Sash"),
42518				component_id: "3dfe203d609c4e61ed5d5ed34144421a176cfeed",
42519				sizes: [[92, 102]]
42520			},
42521			{
42522				type: "picture",
42523				name: gettext("Picture"),
42524				component_id: "23f3f34205d4709811052a6709914697600302c6",
42525				sizes: [[90, 102]]
42526			},
42527			{
42528				type: "standard",
42529				name: gettext("Casement"),
42530				component_id: "6d48b971d09f830198aad21b2b9fcb2c5b9d36af",
42531				sizes: [[92, 102]]
42532			},
42533			{
42534				type: "arched",
42535				name: gettext("Arched"),
42536				component_id: "6c705d137661ddf3de8ce3e159cc2446562912cf",
42537				sizes: [[90, 160]]
42538			},
42539			{
42540				type: "round",
42541				name: gettext("Half Round"),
42542				component_id: "b83b277ce9f79359fe2fef82c8ab7e1f7f83495b",
42543				sizes: [[120,  70]]
42544			},
42545			{
42546				type: "circle",
42547				name: gettext("Round"),
42548				component_id: "2bf5ae66583be1729caabb74f64f44fe5057c0df",
42549				sizes: [[90,  90]]
42550			}
42551		],
42552		replacements: {
42553			'3aa1d582ac225cb89ffccd90cd9a133226c40a25': '3e6a673e4ad12f17a08f1f2304e5dc47e95ab2d6',
42554			'2c515ee55bd217b573e05663f7ad0cf4f6458fa5': '98a6f80c8ac25cc7e03c5e301646463f50a8f973',
42555			'b303e1c2e108af3eed81f5d871ce51441d9ee728': 'f23b87d056907b297d89d9b3b70e19051d23b0d7',
42556			'f3e206bc66af47e2ef393aca58ca2422d6418bd1': '61b03b1e217f4164b27376f22068af2037307143'
42557		}
42558	},
42559	views: {
42560		floor: {
42561			offset: {
42562				top: 56,
42563				bottom: 36
42564			},
42565			library: { 
42566				camera: {
42567					scaleFactor: 0.2
42568				}
42569			},
42570			floor: {
42571				fillOpacity: 1
42572			},
42573			measurer: {
42574				// distance between outer wall side and ruler
42575				distance: 20,
42576				text: {
42577					'font-size': 10,
42578					'color': '#f00'
42579				},
42580				rulers: {
42581					'stroke-width': 1,
42582					'stroke': '#999'
42583				}
42584			},
42585			keyboardShiftValue: 10,
42586			keyboardRotateValue: 15
42587		},
42588		isometric: {
42589			linked: ['floor'],
42590			offset: {
42591				top: 56,
42592				bottom: 36
42593			},
42594			IsoWall: {
42595				normal: {
42596					stroke: '#000000'
42597				},
42598				highlighted: {
42599					stroke: '#67afdf'
42600				}
42601			}
42602		},
42603		render: {
42604			timeout: myDeco.settings.AUSTIN_RENDER_TIMEOUT * 1000
42605		}
42606	},
42607	controllers: {
42608		layout: {
42609			shortcuts: [
42610				// action name, key code, shift, ctrl or meta, alt, repeated
42611				['remove',		KEY.Delete,			false, false, false, false],
42612				['remove',		KEY.BackSpace,		false, false, false, false],
42613				['move',		KEY.Left,			false, false, false, true ],
42614				['move',		KEY.Left,			true,  false, false, true ],
42615				['move',		KEY.Right,			false, false, false, true ],
42616				['move',		KEY.Right,			true,  false, false, true ],
42617				['move',		KEY.Up,				false, false, false, true ],
42618				['move',		KEY.Up,				true,  false, false, true ],
42619				['move',		KEY.Down,			false, false, false, true ],
42620				['move',		KEY.Down,			true,  false, false, true ],
42621				['rotate',		KEY.L,				false, false, false, true ],
42622				['rotate',		KEY.L,				true,  false, false, true ],
42623				['rotate',		KEY.R,				false, false, false, true ],
42624				['rotate',		KEY.R,				true,  false, false, true ],
42625				['redo',		KEY.Y,				false, true,  false, false],
42626				['undo',		KEY.Z,				false, true,  false, false],
42627				['duplicate',	KEY.D,				false, false, false, false],
42628				['split',		KEY.S,				false, false, false, false],
42629				['info',		KEY.I,				false, false, false, false],
42630				['help',		KEY.H,				false, false, false, false],
42631				['open',		KEY.O,				false, true,  false, false],
42632				['save',		KEY.S,				false, true,  false, false],
42633				['saveas',		KEY.S,				true,  true,  false, false],
42634				['enlarge',		KEY.E,				false, false, false, false],
42635				['zoomin',		KEY.PlusEq,			true,  false, false, false],
42636				['zoomin',		KEY.Plus,			false, false, false, false],
42637				['zoomout',		KEY.Minus,			false, false, false, false],
42638				['draw',		KEY.P,				false, false, false, false],
42639				['reset',		KEY.Esc,			false, false, false, false],
42640				['find',		KEY.F,				false, true,  false, false],
42641				['setscale',	KEY.D,				false, true,  false, false],
42642				['debug',   	KEY.X,				false, false, false, false]
42643			]
42644		},
42645		floor: {
42646			linked: ['isometric']
42647		},
42648		isometric: {
42649			linked: ['floor']
42650		}
42651	}
42652});
42653/*globals myDeco, $ */
42654/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
42655          sloppy: true, white: true */
42656if (myDeco.browser.isMobileSafari) {
42657	myDeco.App.settings = $.extend(true, myDeco.App.settings, {
42658		views: {
42659			'color-picker': {
42660				dragThumbnail: {
42661					width: 150,
42662					height: 150
42663				}
42664			},
42665			'materials-search': {
42666				dragThumbnail: {
42667					width: 150,
42668					height: 150,
42669					postfix: '_50.jpg'
42670				}
42671			},
42672			floor: {
42673				rotateArrow: {
42674					distance: 60,
42675					scale: 0.4
42676				},
42677				library: {
42678					camera: {
42679						scaleFactor: 0.3,
42680						arrowDistance: 360
42681					},
42682					splitButton: {
42683						size: 32,
42684						img: '/split-button.png'
42685					},
42686					drawButton: {
42687						size: 32,
42688						img: '/draw-button.png'
42689					},
42690					closeButton: {
42691						size: 32,
42692						img: '/delete-button.png'
42693					},
42694					infoButton: {
42695						size: 32,
42696						img: '/default.png'
42697					},
42698					duplicateButton: {
42699						size: 32,
42700						img: '/duplicate-button.png'
42701					}
42702				},
42703
42704				Corner: {
42705					radius: 15
42706				},
42707
42708				Wall: {
42709					thickness: 20
42710				},
42711
42712				Door: {
42713					thickness: 20
42714				},
42715
42716				Window: {
42717					thickness: 20
42718				},
42719
42720				Drawable: {
42721					controlPanel: {
42722						distance: 50,
42723						padding: 1
42724					}
42725				},
42726
42727				Component: {
42728					minsize: 8,
42729					hideOutline: true
42730				}
42731			}
42732		}
42733	});
42734};
42735/*globals myDeco, $ */
42736/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
42737          sloppy: true, white: true */
42738/**
42739 * Tracking module for 3D tool
42740 */
42741
42742myDeco.App.tracking = $.extend({}, myDeco.tracking, {
42743	/**
42744	 * Events namespace prefix
42745	 * @type {String}
42746	 */
42747	prefix: 'austin.',
42748	
42749	/**
42750	 * Init tracking
42751	 */
42752	watch: function() {
42753		var self = this;
42754		
42755		if (!myDeco.settings.AUSTIN_GA_ACCOUNT) {
42756			return;
42757		}
42758		
42759		this.setVar('Account', myDeco.settings.AUSTIN_GA_ACCOUNT);
42760		$.each(myDeco.settings.AUSTIN_GA_OPTIONS, this.setVar.bind(this));
42761		
42762		myDeco.body
42763			.bind('roomSaving', function(e, roomType, roomId, options) {
42764				self.trackEvent('austin-save', 'request', options.isNew ? 'new' : 'update');
42765			})
42766			.bind('roomSaved', function(e, roomType, roomId, options) {
42767				self.trackEvent('austin-save', 'success', options.isNew ? 'new' : 'update');
42768			})
42769			.bind('roomSavingError', function(e, code, status, message) {
42770				self.trackEvent('austin-save', 'error ' + code, message.toString());
42771			})
42772			.bind('roomLoadRequest', function() {
42773				self.trackEvent('app-openRoom', '3d');
42774			})
42775			.bind('operationStart', function(e, data) {
42776				switch (data[0]) {
42777					case 'draw':
42778						self.trackEvent('austin-actions', '3d', 'draw_walls');
42779						break;
42780						
42781					case 'startingchoice':
42782						self.trackEvent('startingchoice', '3d', data[1]);
42783						break;
42784					
42785					case 'showdetails':
42786						self.trackEvent('GTS', data[1], data[2], data[3]);
42787						break;
42788				}
42789			})
42790			.bind('entity-drag-started', function(e, data) {
42791				if (data.kind === 'decor' && data.type === 'color') {
42792					self.trackEvent('austin-actions', '3d', 'drag_paint', 1);
42793				}
42794			})
42795			.bind('viewToggle', function(e, type) {
42796				self.trackEvent('austin-view-selection', '3d', type);
42797			})
42798			.bind('controller-facet-refined', function(e, type, facetName, facetValue) {
42799				if (type === 'materials-search' && facetName === 'class') {
42800					if (facetValue && facetValue[0] === 'paint') {
42801						self.trackEvent('austin-section-selection', '3d',
42802							'decorate_paint', 1);
42803						if (myDeco.settings.DULUX_TRACKING) {
42804							// track DULUX code
42805							self.trackingImage(myDeco.settings.DULUX_BRANDING.tracking_image);
42806						}
42807					}
42808				}
42809			})
42810			.bind('materialsError', function(materials) {
42811				self.trackEvent('austin-error-materials', '3d', materials);
42812			})
42813			.bind('saveFormFriendInvite', function(e, prefix) {
42814				self.trackEvent('opinion', '3d', prefix + '_friend_invite');
42815			})
42816			.bind('selectedShareSubject', function(e, data) {
42817				var _val, i, trackingMap = {
42818					facebook: 'FbFriendsOpinionChoice',
42819					living: 'HomeFriendsOpinionChoice',
42820					email: 'EmailFriendsOpinionChoice',
42821					color: 'ColourOpinionChoice',
42822					furniture: 'FurnitureOpinionChoice',
42823					decoration: 'DecorationOpinionChoice'
42824				}
42825				function send(value) {
42826					self.trackEvent('opinion', '3d', trackingMap[value]);
42827				}
42828
42829				for (key in data) {
42830					_val = data[key];
42831					if ($.isArray(_val)) {
42832						i = _val.length;
42833						for (i > -1; i--;) {
42834							send(_val[i]);
42835						}
42836					} else {
42837						send(_val);
42838					}
42839				}
42840			});
42841		// global error tracking
42842		window.onerror = function(message, url, line) {
42843			self.trackEvent('austin-error', 'window', url + ':' + line +
42844				':' + encodeURIComponent(message.substring(0, 100)));
42845		};
42846		
42847		// global ajax error tracking
42848		$(document).ajaxError(function(e, xhr, options, message) {
42849			self.trackEvent('austin-error', 'ajax', options.type + '=' +
42850				options.url + ':' + message.substring(0, 100));
42851		});
42852	}
42853});
42854/*globals myDeco, $, SearchModel, MaterialsSearchModel, SpecialSearchModel,
42855          RoomManagementModel, ComponentListModel, emptyRoom, IsometricView,
42856          window, ArchitectureSearchModel, GardenSearchModel */
42857/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
42858          sloppy: true, white: true */
42859/**
42860 * @fileOverview Main FPE init function
42861 */
42862myDeco.App.init = function() {
42863	var searchOptions = myDeco.settings.WL ? {
42864			facet: $.map(myDeco.settings.WL.WL_SUPPLIERS.split(','), function(supplier) {
42865				return supplier ? 'supplier_id,' + supplier : null;
42866			}), 
42867			filter: myDeco.settings.WL.WL_SEARCH_EXTRA_FILTER ? [myDeco.settings.WL.WL_SEARCH_EXTRA_FILTER] : ''
42868		} : {},
42869		search = new SearchModel(false, searchOptions),
42870		materialsSearch = new MaterialsSearchModel(),
42871		specialSearch = myDeco.settings.SPECIAL_SEARCH ? (new SpecialSearchModel(false, searchOptions)) : false,
42872		roomManagement = new RoomManagementModel('3d'),
42873		componentList = new ComponentListModel(),
42874		colorPicker = $('#view-color-picker'),
42875		settings = {
42876			model: emptyRoom,
42877			views: {
42878				search: {
42879					model: search
42880				},
42881				'materials-search': {
42882					model: materialsSearch
42883				},
42884				'special-search': {
42885					model: specialSearch
42886				},
42887				'architecture-search': {
42888					model: new ArchitectureSearchModel(false, searchOptions)
42889				},
42890				'garden-search': {
42891					model: new GardenSearchModel(false, searchOptions)
42892				},
42893				'room-management': {
42894					model: roomManagement
42895				},
42896				'component-list': {
42897					model: componentList
42898				},
42899				floor: {
42900					canvasSelector: '#view-floor .view .canvas'
42901				},
42902				layout: {},
42903				isometric: {
42904					canvasSelector: '#view-isometric .view .canvas'
42905				}
42906			}
42907		},
42908		action;
42909	
42910	$('.scroller, .iscroll, #view-build, #view-component-info').iscroll();
42911	
42912	myDeco.App.tracking.watch();
42913	
42914	try {
42915		myDeco.App.roomUrl = myDeco.settings.OPEN_ROOM_URL; // window.location.search.getURIParam('room_url').pop() || *
42916		// uncomment one of the lines below to force loading a room (for debugging)
42917		// myDeco.App.roomUrl = '/ws/people/creo_forma/rooms/140298/265694/'; // version 0
42918		// myDeco.App.roomUrl = '/ws/people/timknip/rooms/4537186/'; // version 2
42919		// myDeco.App.roomUrl = '/Users/timk/Documents/floorplanner/floorpy/test/out/TestPlanExport_test_mydeco.json';
42920		action = myDeco.App.roomUrl ? 'openRoom' : (window.opener ? window.opener.loadAction : '');
42921		myDeco.App.roomUrl = myDeco.App.roomUrl || (window.opener ? window.opener.loadValue : '');
42922	} catch(e) { }
42923	
42924	myDeco.App.dispatcher.init(settings);
42925	
42926	myDeco.body
42927		.bind('controller-facet-cleared', function(e, type, facetName) {
42928			if (type === 'materials-search' && facetName === 'class') {
42929				$('#view-color-picker').toggle(false);
42930			}
42931		})
42932		.bind('controller-facet-refined', function(e, type, facetName, facetValue) {
42933			if (type === 'materials-search' && facetName === 'class') {
42934				colorPicker.toggle(facetValue && ((facetValue[0] === false &&
42935					facetValue[1] === 'paint') || facetValue[0] === 'paint'));
42936			}
42937		})
42938		.bind('model-request-sent', function(e, type) {
42939			if (type === 'materials-search') {
42940				colorPicker.hide();
42941			}
42942		})
42943		.trigger('viewToggle', ['floor'])
42944		.find('#view-layout').trigger('linkTo', [myDeco.body.find('#view-render')]);
42945
42946	// called by the parent page to load data, e.g. a room
42947	window.switchState = function(e, action, value) {
42948		if (action === 'openRoom') {
42949			if (value) {
42950				myDeco.body.trigger('roomLoadRequest', [value]);
42951			} else {
42952				myDeco.body.trigger('roomNewRequest');
42953			}
42954		} else if (action === 'addComponent' && value) {
42955			if (myDeco.App.model._ws_url) {
42956				myDeco.App.model.dropComponents([value]);
42957			} else {
42958				myDeco.body.trigger('showWizard', [value]);
42959			}
42960		}
42961	};
42962	
42963	// load room or add component
42964	if (action && myDeco.App.roomUrl) {
42965		window.switchState(undefined, action, myDeco.App.roomUrl);
42966	}
42967
42968	// jquery has buggy support of this event in IE
42969	window.onbeforeunload = function() {
42970		if (myDeco.App.model.history.counter > 0) {
42971			return gettext('Have you saved your project? You may lose all your work!');
42972		}
42973	};
42974	
42975	myDeco.popin.plugin('message', {
42976		template: '<div class="content"><h1 class="title">' + gettext('Alert') + '</h1><p class="message"></p></div></div>'
42977	}, function(div) {
42978		if (this.windowOptions.closeButtonText) {
42979			div.find('.close-button-text').html(this.windowOptions.closeButtonText);
42980		}
42981		if (this.windowOptions.title) {
42982			div.find('.title').html(this.windowOptions.title);
42983		} else {
42984			div.find('.title').remove();
42985		}
42986		if (this.windowOptions.message) {
42987			div.find('.message').html(this.windowOptions.message);
42988		} else {
42989			div.find('.message').remove();
42990		}
42991	});
42992};
42993
42994$(function() {
42995	myDeco.App.init();
42996});
42997myDeco.COUNTRIES = [
42998	["AB", "Abkhazia"],
42999	["AF", "Afghanistan"],
43000	["AL", "Albania"],
43001	["DZ", "Algeria"],
43002	["AS", "American Samoa"],
43003	["AD", "Andorra"],
43004	["AO", "Angola"],
43005	["AI", "Anguilla"],
43006	["AQ", "Antarctica"],
43007	["AG", "Antigua and Barbuda"],
43008	["AR", "Argentina"],
43009	["AM", "Armenia"],
43010	["AW", "Aruba"],
43011	["AU", "Australia"],
43012	["AT", "Austria"],
43013	["AZ", "Azerbaijan"],
43014	["BS", "Bahamas"],
43015	["BH", "Bahrain"],
43016	["BD", "Bangladesh"],
43017	["BB", "Barbados"],
43018	["BY", "Belarus"],
43019	["BE", "Belgium"],
43020	["BZ", "Belize"],
43021	["BJ", "Benin"],
43022	["BM", "Bermuda"],
43023	["BT", "Bhutan"],
43024	["BO", "Bolivia, plurinational state of"],
43025	["BQ", "Bonaire, Sint Eustatius and Saba"],
43026	["BA", "Bosnia and Herzegovina"],
43027	["BW", "Botswana"],
43028	["BV", "Bouvet Island"],
43029	["BR", "Brazil"],
43030	["IO", "British Indian Ocean Territory"],
43031	["BN", "Brunei Darussalam"],
43032	["BG", "Bulgaria"],
43033	["BF", "Burkina Faso"],
43034	["MM", "Burma"],
43035	["BI", "Burundi"],
43036	["KH", "Cambodia"],
43037	["CM", "Cameroon"],
43038	["CA", "Canada"],
43039	["CV", "Cape Verde"],
43040	["KY", "Cayman Islands"],
43041	["CF", "Central African Republic"],
43042	["TD", "Chad"],
43043	["CL", "Chile"],
43044	["CN", "China"],
43045	["CX", "Christmas Island"],
43046	["CC", "Cocos (Keeling) Islands"],
43047	["CO", "Colombia"],
43048	["KM", "Comoros"],
43049	["CG", "Congo"],
43050	["CD", "Congo, Democratic Republic of the"],
43051	["CK", "Cook Islands"],
43052	["CR", "Costa Rica"],
43053	["CI", "Cote d'Ivoire"],
43054	["HR", "Croatia"],
43055	["CU", "Cuba"],
43056	["CW", "Curaçao"],
43057	["CY", "Cyprus"],
43058	["CZ", "Czech Republic"],
43059	["DK", "Denmark"],
43060	["DJ", "Djibouti"],
43061	["DM", "Dominica"],
43062	["DO", "Dominican Republic"],
43063	["EC", "Ecuador"],
43064	["EG", "Egypt"],
43065	["SV", "El Salvador"],
43066	["GQ", "Equatorial Guinea"],
43067	["ER", "Eritrea"],
43068	["EE", "Estonia"],
43069	["ET", "Ethiopia"],
43070	["FK", "Falkland Islands (Malvinas)"],
43071	["FO", "Faroe Islands"],
43072	["FJ", "Fiji"],
43073	["FI", "Finland"],
43074	["FR", "France"],
43075	["GF", "French Guiana"],
43076	["PF", "French Polynesia"],
43077	["TF", "French Southern Territories"],
43078	["GA", "Gabon"],
43079	["GM", "Gambia"],
43080	["GE", "Georgia"],
43081	["DE", "Germany"],
43082	["GH", "Ghana"],
43083	["GI", "Gibraltar"],
43084	["GR", "Greece"],
43085	["GL", "Greenland"],
43086	["GD", "Grenada"],
43087	["GP", "Guadeloupe"],
43088	["GU", "Guam"],
43089	["GT", "Guatemala"],
43090	["GG", "Guernsey"],
43091	["GN", "Guinea"],
43092	["GW", "Guinea-Bissau"],
43093	["GY", "Guyana"],
43094	["HT", "Haiti"],
43095	["HM", "Heard Island and McDonald Islands"],
43096	["VA", "Holy See (Vatican City State)"],
43097	["HN", "Honduras"],
43098	["HK", "Hong Kong"],
43099	["HU", "Hungary"],
43100	["IS", "Iceland"],
43101	["IN", "India"],
43102	["ID", "Indonesia"],
43103	["IR", "Iran, Islamic Republic of"],
43104	["IQ", "Iraq"],
43105	["IE", "Ireland"],
43106	["IM", "Isle of Man"],
43107	["IL", "Israel"],
43108	["IT", "Italy"],
43109	["JM", "Jamaica"],
43110	["JP", "Japan"],
43111	["JE", "Jersey"],
43112	["JO", "Jordan"],
43113	["KZ", "Kazakhstan"],
43114	["KE", "Kenya"],
43115	["KI", "Kiribati"],
43116	["KP", "Korea, Democratic People's republic of"],
43117	["KR", "Korea, Republic of"],
43118	["KW", "Kuwait"],
43119	["KG", "Kyrgyzstan"],
43120	["LA", "Lao People's Democratic Republic"],
43121	["LV", "Latvia"],
43122	["LB", "Lebanon"],
43123	["LS", "Lesotho"],
43124	["LR", "Liberia"],
43125	["LY", "Libyan Arab Jamahiriya"],
43126	["LI", "Liechtenstein"],
43127	["LT", "Lithuania"],
43128	["LU", "Luxembourg"],
43129	["MO", "Macao"],
43130	["MK", "Macedonia, The Former Yugoslab Republic Of"],
43131	["MG", "Madagascar"],
43132	["MW", "Malawi"],
43133	["MY", "Malaysia"],
43134	["MV", "Maldives"],
43135	["ML", "Mali"],
43136	["MT", "Malta"],
43137	["MH", "Marshall Islands"],
43138	["MQ", "Martinique"],
43139	["MR", "Mauritania"],
43140	["MU", "Mauritius"],
43141	["YT", "Mayotte"],
43142	["MX", "Mexico"],
43143	["FM", "Micronesia, Federated States of"],
43144	["MD", "Moldova"],
43145	["MC", "Monaco"],
43146	["MN", "Mongolia"],
43147	["ME", "Montenegro"],
43148	["MS", "Montserrat"],
43149	["MA", "Morocco"],
43150	["MZ", "Mozambique"],
43151	["NA", "Namibia"],
43152	["NR", "Nauru"],
43153	["NP", "Nepal"],
43154	["NL", "Netherlands"],
43155	["NC", "New Caledonia"],
43156	["NZ", "New Zealand"],
43157	["NI", "Nicaragua"],
43158	["NE", "Niger"],
43159	["NG", "Nigeria"],
43160	["NU", "Niue"],
43161	["NF", "Norfolk Island"],
43162	["MP", "Northern Mariana Islands"],
43163	["NO", "Norway"],
43164	["OM", "Oman"],
43165	["PK", "Pakistan"],
43166	["PW", "Palau"],
43167	["PS", "Palestinian Territory, Occupied"],
43168	["PA", "Panama"],
43169	["PG", "Papua New Guinea"],
43170	["PY", "Paraguay"],
43171	["PE", "Peru"],
43172	["PH", "Philippines"],
43173	["PN", "Pitcairn"],
43174	["PL", "Poland"],
43175	["PT", "Portugal"],
43176	["PR", "Puerto Rico"],
43177	["QA", "Qatar"],
43178	["RE", "Reunion"],
43179	["RO", "Romania"],
43180	["RU", "Russian Federation"],
43181	["RW", "Rwanda"],
43182	["BL", "Saint Barthélemy"],
43183	["SH", "Saint Helena, Ascension And Tristan Da Cunha"],
43184	["KN", "Saint Kitts and Nevis"],
43185	["LC", "Saint Lucia"],
43186	["MF", "Saint Martin (French Part)"],
43187	["PM", "Saint Pierre and Miquelon"],
43188	["VC", "Saint Vincent and the Grenadines"],
43189	["WS", "Samoa"],
43190	["SM", "San Marino"],
43191	["ST", "Sao Tome and Principe"],
43192	["SA", "Saudi Arabia"],
43193	["SN", "Senegal"],
43194	["RS", "Serbia"],
43195	["SC", "Seychelles"],
43196	["SL", "Sierra Leone"],
43197	["SG", "Singapore"],
43198	["SX", "Sint Maarten"],
43199	["SK", "Slovakia"],
43200	["SI", "Slovenia"],
43201	["SB", "Solomon Islands"],
43202	["SO", "Somalia"],
43203	["ZA", "South Africa"],
43204	["GS", "South Georgia and the South Sandwich Islands"],
43205	["OS", "South Ossetia"],
43206	["SS", "South Sudan"],
43207	["ES", "Spain"],
43208	["LK", "Sri Lanka"],
43209	["SD", "Sudan"],
43210	["SR", "Suriname"],
43211	["SJ", "Svalbard and Jan Mayen"],
43212	["SZ", "Swaziland"],
43213	["SE", "Sweden"],
43214	["CH", "Switzerland"],
43215	["SY", "Syrian Arab Republic"],
43216	["TW", "Taiwan, Province of China"],
43217	["TJ", "Tajikistan"],
43218	["TZ", "Tanzania, United Republic Of"],
43219	["TH", "Thailand"],
43220	["TL", "Timor-Leste"],
43221	["TG", "Togo"],
43222	["TK", "Tokelau"],
43223	["TO", "Tonga"],
43224	["TT", "Trinidad and Tobago"],
43225	["TN", "Tunisia"],
43226	["TR", "Turkey"],
43227	["TM", "Turkmenistan"],
43228	["TC", "Turks and Caicos Islands"],
43229	["TV", "Tuvalu"],
43230	["UG", "Uganda"],
43231	["UA", "Ukraine"],
43232	["AE", "United Arab Emirates"],
43233	["GB", "United Kingdom"],
43234	["US", "United States"],
43235	["UM", "United States Minor Outlying Islands"],
43236	["UY", "Uruguay"],
43237	["UZ", "Uzbekistan"],
43238	["VU", "Vanuatu"],
43239	["VE", "Venezuela"],
43240	["VN", "Vietnam"],
43241	["VG", "Virgin Islands, British"],
43242	["VI", "Virgin Islands, U.S."],
43243	["WF", "Wallis and Futuna"],
43244	["EH", "Western Sahara"],
43245	["YE", "Yemen"],
43246	["ZM", "Zambia"],
43247	["ZW", "Zimbabwe"],
43248	["AX", "Åland Islands"]
43249];
43250/*globals myDeco, $, LayoutController, window, document, KEY */
43251/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
43252          sloppy: true, white: true */
43253/**
43254 * @fileOverview FPE Layout controller
43255 */
43256
43257var FpeLayoutController = LayoutController.extend({
43258	/**
43259	 * Controller type
43260	 * @type {String}
43261	 * @static
43262	 */
43263	$type: 'layout',
43264	
43265	/**
43266	 * Layout controller init
43267	 * @param {Object} model FPE model
43268	 * @param {Object} settings Controller settings
43269	 */
43270	init: function(model, settings) {
43271		this._super(model, settings);
43272		var self = this,
43273			saveTimestamp = 0;
43274		
43275		if (this._model.checkFloorPermissions) {
43276			this._model.checkFloorPermissions(false);
43277		}
43278		
43279		myDeco.body
43280			.bind('roomSaveRequest', function(e, options) {
43281				var time = $.now(),
43282					model = myDeco.App.model;
43283			})
43284			.bind('selectColor', function() {
43285				$('#sidebar').find('.sidebar-buttons ul.tabs').trigger('show', ['decorate']);
43286				// false means reset facet before apply
43287				myDeco.body.trigger('controller-facet-refine',
43288					['materials-search',
43289					 'class',
43290					 [false, "paint", "class,paint", 0]
43291				]);
43292			})
43293			.delegate('.gts', 'click', function() {
43294				var src = myDeco.settings.AUSTIN_GTS_SRC,
43295					element = $(this),
43296					data = element.data('gts');
43297				
43298				if (typeof data !== 'undefined') {
43299					if (myDeco.settings.WL) {
43300						src = myDeco.settings.WL.WL_MYDECO_COM_REPLACEMENT + src;
43301					}
43302					
43303					if (element.hasClass('details')) {
43304						self._model.operationStart(['showdetails', data.category, src, data.price]);
43305					} else {
43306						myDeco.buynow.buynowClick(this, data.guid, data.price, data.category, src);
43307					}
43308				}
43309			})
43310			.bind('status roomLoading roomLoaded roomLoadError roomSaving ' +
43311					'roomSaved roomSavingError',
43312				function(e, data1, data2, data3) {
43313					var text, timeout;
43314
43315					switch (e.type) {
43316						case 'status':
43317							text = data1 || '';
43318							timeout = data2;
43319							break;
43320							
43321						case 'roomLoading':
43322							if (typeof data1 === 'string') {
43323								text = gettext('Room is loading...');
43324								timeout = 0;
43325                                window._roomLoading = true;
43326							}
43327							break;
43328							
43329						case 'roomLoaded':
43330							if (!data1 && self._model._ws_url) {
43331								text = gettext('Room loaded successfully');
43332								if (self._model._masking) {
43333									myDeco.body
43334										.trigger('toggleSidebar', ['build'])
43335										.find('#view-build .build-tabs')
43336											.trigger('show', ['floor']);
43337								}
43338                                window._roomLoading = false;
43339							}
43340							break;
43341							
43342						case 'roomLoadError':
43343							text = gettext('Error while room loading, please try later');
43344							timeout = 30;
43345							break;
43346						
43347						case 'roomSaving':
43348							text = gettext('Saving your room...');
43349							timeout = 0;
43350							break;
43351							
43352						case 'roomSaved':
43353							text = $('<span>' + gettext('Your design has been saved successfully.') +  '</span>');
43354							if (!data3.hqRender) {
43355								text.append($('<a href="" />')
43356									.text(gettext('Get a photo-quality image of your room.'))
43357									.click(function(e) {
43358										e.stop();
43359										
43360										self.status();
43361										
43362										myDeco.body.trigger('roomSaveRequest', [{
43363											showProperties: true,
43364											hqRender: true
43365										}]);
43366									})
43367								);
43368							}
43369							timeout = 30;
43370							break;
43371							
43372						case 'roomSavingError':
43373							text = gettext('Error while room saving, please try later');
43374							timeout = 30;
43375							break;
43376					}
43377					
43378					if (text !== undefined) {
43379						self.status(text, timeout);
43380					}
43381			});
43382			
43383		$(document)
43384			.keydown(this.keyDown.bind(this))
43385			.keypress(this.keyPress.bind(this))
43386			.keyup(this.keyUp.bind(this));
43387		
43388		if (!myDeco.browser.isMobileSafari) {
43389			myDeco.body.bind('mousedown selectstart dragstart',
43390				function(e) {
43391					if (self.tagNames.indexOf(' ' + e.target.tagName + ' ') === -1) {
43392						e.preventDefault();
43393					}
43394				});
43395		} else {
43396			// longpress emulation for iPad (doubleclick replacement)
43397			myDeco.body.bind('mousedown', function(e) {
43398				var timeout = window.setTimeout(function() {
43399						var event = new $.Event(e);
43400						event.type = 'dblclick';
43401						$(e.target).trigger(event);
43402					}, 1000);
43403					
43404				myDeco.body.one('mouseup', function() {
43405					window.clearTimeout(timeout);
43406				});
43407			});
43408		}
43409		
43410
43411		
43412		this._status.element = $('#messages');
43413		this._status.id = 0;
43414		this._status.top = parseInt(this._status.element.css('top'), 10);
43415		this._status.height = this._status.element.outerHeight(true);
43416		this._status.element.click(function(e) {
43417			e.preventDefault();
43418			self.status();
43419		});
43420
43421	},
43422	
43423	/**
43424	 * Register view
43425	 * @param {Object} view View associated to controller
43426	 */
43427	registerView: function(view) {
43428		this._super(view);
43429		var self = this,
43430			wallHint = $.cookie('wall-hint'),
43431			wallHintFunction = function(e, event, visual) {
43432				if (visual._model.type === 'wall') {
43433					var bubble = $('.bubble.drag-wall').rearrange({position: [event.pageX, event.pageY]}).fadeIn(500);
43434					myDeco.body.one('mousedown', function() {
43435						bubble.fadeOut(500);
43436					});
43437					myDeco.body.unbind('highlightOn', wallHintFunction);
43438					$.cookie('wall-hint', '1', {path: window.location.pathname});
43439				}
43440			},
43441			typeButtons = view.canvasElement.find('.type-buttons .button'),
43442			switchView = function(type) {
43443				type = type || (typeButtons.filter('.active')
43444					.hasClass('floor') ? 'isometric' : 'floor');
43445				
43446				myDeco.body.trigger('viewToggle', [type]);
43447			},
43448			zoom = function(enlarge) {
43449				myDeco.body.trigger(enlarge ? 'zoomIn' : 'zoomOut');
43450			},
43451			lockButton = view.app.find('#view-units .lock-plan').click(function(e) {
43452				e.stop();
43453				
43454				var button = $(this);
43455				button.toggleClass('active');
43456				
43457				self._model.toggleLock(button.hasClass('active'));
43458			}),
43459			lockTimeout,
43460			controlsPane = view.app.find('#view-units');
43461			
43462		myDeco.body
43463			.bind('entity-drag-started', this.entityDrag.bind(this))
43464			.bind('loadRoom', function() {
43465				myDeco.body.trigger('viewToggle', ['floor']);
43466				typeButtons.removeClass('active').filter('.floor').addClass('active');
43467			})
43468			.bind('actionRequest', function(e, action) {
43469				if (action[0] === 'switchview') {
43470					switchView();
43471				} else if (action[0] === 'zoomin') {
43472					zoom(true);
43473				} else if (action[0] === 'zoomout') {
43474					zoom(false);
43475				}
43476			}).bind('locked-move', function() {
43477				var n = 6,
43478					toggleFunc;
43479					
43480				if (lockTimeout) {
43481					window.clearTimeout(lockTimeout);
43482					lockButton.removeClass('attention');
43483				}
43484					
43485				toggleFunc = function() {
43486					lockButton.toggleClass('attention');
43487					n = n - 1;
43488					if (n > 0 && lockButton.hasClass('active')) {
43489						lockTimeout = window.setTimeout(toggleFunc, 200);
43490					} else {
43491						lockButton.removeClass('attention');
43492					}
43493				};
43494				
43495				toggleFunc();
43496			})
43497			.bind('view-toggle', function(e, view, state) {
43498				if (state) {
43499					typeButtons
43500						.removeClass('active')
43501						.filter('.' + view.$type)
43502						.addClass('active');
43503				}
43504					
43505				if (view.$type === 'floor') {
43506					controlsPane.toggle(state);
43507				}
43508			});
43509		
43510		if (!wallHint) {
43511			myDeco.body.bind('highlightOn', wallHintFunction);
43512		}
43513			
43514		view.app
43515			.mouseout(function(e) {
43516				var offset = self._view.getAppOffset(),
43517					dimensions = self._view.getAppDimensions();
43518				
43519				// reset operation if pointer is out of app
43520				if (e.pageX < offset.left || e.pageX > offset.left + dimensions.width ||
43521					e.pageY < offset.top || e.pageY > offset.top + dimensions.height) {
43522					self._model.operationReset();
43523				}
43524			});
43525			
43526		view.canvasElement.find('.zoom-in, .zoom-out').bind('click', function(e) {
43527			e.stop();
43528			zoom($(this).hasClass('zoom-in'));
43529		});
43530		
43531		typeButtons.click(function(e) {
43532			e.stop();
43533			switchView($(this).hasClass('isometric') ? 'isometric' : 'floor');
43534		});
43535	},
43536	
43537	/**
43538	 * Global mouse move event handler for serving drag-n-drop between sidebar and canvases
43539	 * @param {Event} e Mousemove event
43540	 */
43541	globalMouseMove: function(e) {
43542		if (this._operation.type === 'add') {
43543			e.preventDefault();
43544			var position = this._operation.data.visual.data('position') || 
43545							{x: this._operation.data.ox, y: this._operation.data.oy};
43546							
43547			this._operation.data.visual.css({
43548				left: e.pageX - position.x + 'px',
43549				top: e.pageY - position.y + 'px'
43550			});
43551		}
43552	},
43553	
43554	/**
43555	 * Callback for resetting current operation state
43556	 */
43557	operationReset: function() {
43558		if (this._operation.type === 'add' && this._operation.data.visual) {
43559			this._operation.data.visual.remove();
43560		}
43561		
43562		this._operation = {type: '', data: {}};
43563	},
43564	
43565	/**
43566	 * Handler for custom entity-drag-started event. Creates a DOM wrapper for
43567	 * draggable element and does some visual machinery.
43568	 * @param {Event} e entity-drag-started event
43569	 * @param {Object} draggable Info about draggable entity and it's visual representation
43570	 * @param {Event} originalEvent Original event which caused start of drag-n-drop
43571	 */
43572	entityDrag: function(e, draggable, originalEvent) {
43573		this._view.app
43574			.append(draggable.visual);
43575
43576		this._operation =	{ type: 'add',
43577								data: { 
43578									entity: draggable.entity,
43579									visual: draggable.visual,
43580									ox: draggable.ox || draggable.visual.width() / 2,
43581									oy: draggable.oy || draggable.visual.height() / 2
43582								}
43583							};
43584
43585		draggable.visual
43586			.css({
43587				left: (originalEvent.pageX || 0) - this._operation.data.ox + 'px',
43588				top: (originalEvent.pageY || 0) - this._operation.data.oy + 'px',
43589				position: 'absolute'
43590			});
43591	},
43592	
43593	/**
43594	 * Send feedback to backend, assume all data is correct (validated with formValidation)
43595	 * @param {Object} data Feedback data
43596	 */
43597	sendFeedback: function(data) {
43598		var feedbackData = {
43599				message: data.message +
43600					'\n\nOverall rating: ' + (data.rating || 'Not rated') +
43601					'\n\nAustin referrer: ' + window.document.referrer +
43602					'\n\nRoom stats' +
43603					'\nProducts (unique/total): ' + myDeco.App.model.uniqueComponentIds().length + ' / ' +
43604													myDeco.App.model.components.length +
43605					'\nMaterials (unique/total): ' + myDeco.App.model.uniqueMaterialIds().length + ' / ' +
43606													myDeco.App.model.materials.length +
43607					'\nRooms: ' + myDeco.App.model.shapes.length +
43608					'\nWalls: ' + myDeco.App.model.walls.length,
43609				context_data: { 
43610					username: myDeco.username
43611				}
43612			},
43613			inexistent;
43614		
43615		if (data.email) {
43616			feedbackData.email = data.email;
43617		}
43618		
43619		if (myDeco.settings.AUSTIN_FEEDBACK_CATEGORY) {
43620			feedbackData.category = myDeco.settings.AUSTIN_FEEDBACK_CATEGORY;
43621		}
43622		
43623		feedbackData.message = feedbackData.message + 
43624			'\nRoom url: ' + (myDeco.App.model._ws_url || 'room is not saved yet');
43625		
43626		inexistent = $.keys(myDeco.App.model._componentsFailed);
43627		if (inexistent.length > 20) {
43628			inexistent = inexistent.slice(0, 20);
43629		}
43630		$.extend(feedbackData.context_data, {
43631			model: myDeco.App.model.getModel(2),
43632			history: myDeco.App.model.changeLog(10),
43633			inexistentGUIDs: inexistent
43634		});
43635		
43636		feedbackData.context_data = JSON.stringify(feedbackData.context_data);
43637		
43638		$.ajax({
43639			url: myDeco.urls.feedback_post_ajax,
43640			type: 'post',
43641			dataType: 'json',
43642			data: feedbackData,
43643			success: this._view.feedbackSuccess.bind(this._view),
43644			error: function() {
43645				myDeco.popin.close();
43646			}
43647		});
43648	},
43649	
43650	/**
43651	 * Global document observer, used to prevent default browser behavior on backspace.
43652	 * @param {Event} e
43653	 */
43654	keyDown: function(e) {
43655		// prevent to stop animation etc
43656		if (e.keyCode === KEY.Esc) {
43657			e.preventDefault();
43658		}
43659
43660		// prevent to deletion if key pressed in input fields
43661		// pass through any special events with modifiers
43662		if (!e.ctrlKey && !e.altKey && !e.metaKey &&
43663				this.tagNames.indexOf(' ' + e.target.tagName + ' ') !== -1) {
43664			return;
43665		}
43666		
43667		// under win repeated event is keydown
43668		var kdr = window.navigator.platform.indexOf('Win') !== -1 &&
43669				($.browser.msie || $.browser.mozilla),
43670			action;
43671		
43672		
43673		if (!this.action) {
43674			action = $.first((this.settings.shortcuts || []), function(cmd) {
43675				return (e.keyCode === cmd[1] && e.shiftKey === cmd[2] &&
43676						(e.ctrlKey === cmd[3] || e.metaKey === cmd[3]) && e.altKey === cmd[4]);
43677			});
43678
43679            if (action && myDeco.settings.WL && myDeco.settings.WL.WL_DISABLED_SHORTCUTS && 
43680                JSON.parse(myDeco.settings.WL.WL_DISABLED_SHORTCUTS).indexOf(action[0]) > -1) {
43681                action = undefined;
43682            }
43683		}
43684
43685		// prevent default behavior of some keys to
43686		// disable default browser reaction
43687		// eg. scrolling of focused layers or move back in browser history
43688		if (this.action || action ||
43689				e.keyCode === KEY.Up || e.keyCode === KEY.Down ||
43690				e.keyCode === KEY.BackSpace) {
43691			e.preventDefault();
43692		}
43693		
43694		if (action || this.action) {
43695			if (!this.action || (this.action[5] && kdr)) {
43696				myDeco.body.trigger('actionRequest', [this.action || action]);
43697			}
43698			this.action = this.action || action;
43699		}
43700	},
43701	
43702	/**
43703	 * Keypress listener
43704	 * @param {Event} e
43705	 */
43706	keyPress: function() {
43707		if (this.action && this.action[5]) {
43708			myDeco.body.trigger('actionRequest', [this.action]);
43709		}
43710	},
43711	
43712	/**
43713	 * Keyup listener
43714	 * @param {Event} e
43715	 */
43716	keyUp: function() {
43717		this.action = false;
43718	},
43719	
43720	/**
43721	 * Show status message in app
43722	 * @param {jQset|String} message Message to show, will close if omitted
43723	 * @param {Number} timeout Timeout to hide message, will show endless
43724	 *                         message if less or equals to zero and show WoD in
43725	 *                         case of equals to zero. 
43726	 */
43727	status: function(message, timeout) {
43728		var self = this,
43729			type = typeof message;
43730		
43731		if (message && type !== 'number') {
43732			if (type === 'string') {
43733				message = $(document.createTextNode(message));
43734			}
43735			
43736			timeout = typeof timeout === 'undefined' ? 5 : timeout;
43737			
43738			this._status.element
43739				.toggleClass('waiting', timeout === 0)
43740				.children('.message').html('').append(message).end()
43741				.animate({top: this._status.top + this._status.height});
43742			
43743			if (timeout > 0) {
43744				window.setTimeout(function() {
43745					self.status(++self._status.id);
43746				}, timeout * 1000);
43747			}
43748		} else if (!message ||
43749				(type === 'number' && message === this._status.id)) {
43750			this._status.element.animate({top: this._status.top});
43751		}
43752	}
43753});
43754
43755myDeco.App.controllers.push(FpeLayoutController);
43756/*globals myDeco, $, AppView */
43757/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
43758          sloppy: true, white: true */
43759/**
43760 * @fileOverview Layout View for FPE app
43761 */
43762var LayoutView = AppView.extend({
43763	/**
43764	 * View type
43765	 * @type {String}
43766	 * @public
43767	 * @static
43768	 */
43769	$type: 'layout',
43770
43771	/**
43772	 * Init view
43773	 * @param {Object} options View options
43774	 */
43775	init: function(options) {
43776		this._super(options);
43777		if($.fn.unitsSelector) {
43778			this.app.find('.units').unitsSelector();
43779		}
43780
43781		// $('#app > .feedback').click(this.showFeedback.bind(this));
43782	},
43783
43784	showFeedback: function(e) {
43785		e.stop();
43786		var self = this;
43787		myDeco.popin.open({
43788			width: 760,
43789			url: '#view-' + this.$type + '-feedback',
43790			// enable template
43791			context: {},
43792			afterOpen: function(content) {
43793				myDeco.formValidation.init(content);
43794				myDeco.buttons.initSubmitButtons(content.find('form').submit(function(e) {
43795					if (!e.isPropagationStopped()) {
43796						e.stop();
43797						self._controller.sendFeedback({
43798								rating: content.find('input[checked]').val() || '',
43799								email: content.find('[name=email]').val() || '',
43800								message: $.trim(content.find('textarea').val())
43801							});
43802						content.find('.loading').hide().show();
43803					}
43804				}).find('input:submit'));
43805			}
43806		});
43807	},
43808
43809	feedbackSuccess: function(data) {
43810		myDeco.popin.content().find('.loading').hide();
43811		myDeco.popin.close();
43812
43813		if (data.success) {
43814			myDeco.popin.message({
43815				title: getttext('Thank you'),
43816				message: gettext('We appreciate your feedback.'),
43817				closeIn: 5
43818			});
43819		}
43820	},
43821
43822	feedbackError: function(text) {
43823		myDeco.popin.content().find('.loading').hide();
43824		myDeco.popin.message({
43825			title: 'Error',
43826			message: text
43827		});
43828	}
43829});
43830
43831myDeco.App.views.push(LayoutView);
43832/*globals Class, window, $, myDeco */
43833/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
43834          sloppy: true, white: true */
43835/**
43836 * @fileOverview Model class for Room management
43837 * Extendable with various set of backends
43838 */
43839var RoomManagementModel = Class.extend({
43840	/**
43841	 * Component type
43842	 * @type {String}
43843	 */
43844	$type: 'room-management',
43845
43846	/**
43847	 * Type of rooms, defaults to '3d'
43848	 * @type {String}
43849	 */
43850	_type: '3d',
43851
43852	/**
43853	 * Storage for loaded rooms
43854	 * @type {Array}
43855	 */
43856	rooms: [],
43857
43858	/**
43859	 * List of room snbapshots, used for snapshots mode
43860	 * @type {Array}
43861	 */
43862	snapshots: [],
43863
43864	/**
43865	 * Start rank
43866	 * @type {Number}
43867	 */
43868	start: 0,
43869
43870	/**
43871	 * Default limit on loading
43872	 * @type {Number}
43873	 */
43874	limit: 0,
43875
43876	/**
43877	 * Flag for room loading availability
43878	 * @type {Boolean}
43879	 */
43880	isLoadingAvailable: true,
43881
43882	/**
43883	 * @constructor
43884	 * @base {Class}
43885	 * @param {String} type Room type
43886	 * @public
43887	 */
43888	init: function (type) {
43889		if (type) {
43890			this._type = type;
43891		}
43892	},
43893
43894	/**
43895	 * Default errback for ajax operations
43896	 * @type {Function}
43897	 */
43898	errback: function() {
43899		window.alert(gettext('Seems that something is happened with our service. Please try again later, sorry.'));
43900	},
43901
43902	/**
43903	 * Get rooms list from backend
43904	 * @param {Function} callback
43905	 * @param {Boolean} append Flag indicated append loading
43906	 */
43907	load: function(callback, append) {
43908		var self = this;
43909
43910		append = !!append;
43911
43912		if (!append) {
43913			this.reset();
43914		}
43915
43916		if (!append || this.isLoadingAvailable) {
43917			$.ajax({
43918				type: 'get',
43919				url: myDeco.urls.ws.userRooms,
43920				data: {
43921					api: 2,
43922					type: this._type,
43923					startrank: this.start,
43924					count: this.limit
43925				},
43926				dataType: 'json',
43927				success: function(data) {
43928					var rooms = self.parse(data);
43929					self.rooms.push.apply(self.rooms, rooms);
43930					self.isLoadingAvailable = rooms.length === self.limit;
43931					self.start = self.rooms.length;
43932					callback.call(null, rooms);
43933				},
43934				error: this.errback
43935			});
43936		} else {
43937			callback.call(null, []);
43938		}
43939	},
43940
43941	/**
43942	 * Parse WS response (replace html entites etc) and return array of rooms
43943	 * @param {Array} data Response from WS
43944	 * @returns {Array}
43945	 */
43946	parse: function(data) {
43947		return $.map(data, function(room) {
43948			room[1].url = room[0];
43949			room[1].name = room[1].name.fromHtml().replace(/^\s+/, ' ');
43950			room[1].category = room[1].category.replace(/&#39;/g, "'");
43951
43952			/**
43953			 * Rooms and floor plans improvements it works only for 1 user @mask_monkey@
43954			 */
43955			if (myDeco.username && myDeco.username === 'mask_monkey' && room[1].locale && room[1].locale != 'en-gb-x-retail') {
43956				room[1].name = room[1].locale.split('-').pop().toUpperCase() + ' - ' + room[1].name;
43957				room[1].isWL = true;
43958			}
43959
43960			return room[1];
43961		});
43962	},
43963
43964	/**
43965	 * Model reset to defaults
43966	 */
43967	reset: function() {
43968		this.isLoadingAvailable = true;
43969		this.rooms = [];
43970		this.start = 0;
43971	},
43972
43973	/**
43974	 * Fetch room snapshots
43975	 * @param {Object} room
43976	 * @param {Function} callback
43977	 */
43978	loadSnapshots: function(room, callback) {
43979		var self = this;
43980
43981		$.ajax({
43982			type: 'get',
43983			url: myDeco.urls.ws.userRooms,
43984			data: {history: room.id, api: 2},
43985			dataType: 'json',
43986			success: function(data) {
43987				self.snapshots = self.parse(data);
43988				callback(self.snapshots);
43989			}
43990		});
43991	},
43992
43993	/**
43994	 * Save data for room
43995	 * @param {Object} room
43996	 * @param {Function} callback
43997	 */
43998	save: function(room, callback) {
43999		var url = room.url + 'attributes/',
44000			data = {};
44001
44002		// denylisted attributes
44003		$.each(['category', 'style', 'name', 'public'], function(i, name) {
44004			data[name] = room[name];
44005		});
44006
44007		$.ajax({
44008			type: 'put',
44009			contentType: 'application/json',
44010			data: JSON.stringify(data),
44011			dataType: 'json',
44012			url: url,
44013			success: function() {
44014				myDeco.body.trigger('roomMetadata', function(metadata, id) {
44015					if (room.id === id) {
44016						myDeco.body.trigger('roomMetadata', data);
44017					}
44018				});
44019				callback.call();
44020			},
44021			error: this.errback
44022		});
44023	},
44024
44025	/**
44026	 * Remove specified room by ID
44027	 * @param {Object} room
44028	 * @param {Function} callback
44029	 * @param {Boolean} snapshot Snapshot mode flag
44030	 */
44031	remove: function(room, callback, snapshot) {
44032		var self = this,
44033			source = this[snapshot ? 'snapshots' : 'rooms'],
44034			index = source.indexOf(room);
44035
44036		$.ajax({
44037			type: 'delete',
44038			data: '{}',
44039			dataType: 'json',
44040			url: room.url,
44041			success: function() {
44042				if (!snapshot) {
44043					self.start -= 1;
44044				}
44045				callback.call(null, index);
44046				source.splice(index, 1);
44047			},
44048			error: this.errback
44049		});
44050	}
44051});
44052/*globals AppController, myDeco, $, window */
44053/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
44054          sloppy: true, white: true */
44055/**
44056 * @fileOverview Controller class for room management
44057 * Will bind to a 'roomOpenBrowser' event
44058 *
44059 * Triggers:
44060 * loadRoom - force ME model to load new room
44061 *      {String} room url]
44062 *
44063 * Listens:
44064 * roomSaved - on ME model room loaded
44065 * roomOpenRequest - open room browser
44066 * roomSaveRequest - save currently opened room
44067 */
44068var RoomManagementController = AppController.extend({
44069
44070	/**
44071	 * Component type
44072	 * @type {String}
44073	 * @public
44074	 * @static
44075	 */
44076	$type: 'room-management',
44077
44078	/**
44079	 * List type, default to normal
44080	 * @type {String}
44081	 */
44082	type: 'normal',
44083
44084	/**
44085	 * Operation mode, default or 'snapshots'
44086	 * @type {String}
44087	 */
44088	mode: '',
44089
44090	/**
44091	 * Options passed to save dispatcher
44092	 * @type {Object}
44093	 */
44094	_options: {},
44095
44096	/**
44097	 * Storage for delayed action executed after successful save
44098	 * @type {Function}
44099	 */
44100	_saveAction: false,
44101
44102	/**
44103	 * Register view in controller
44104	 * @param {AppView} view
44105	 */
44106	registerView: function(view) {
44107		var self = this;
44108
44109		this._super(view);
44110
44111		if (myDeco.settings.WL) {
44112			this.type = 'extended';
44113		}
44114
44115		myDeco.body
44116			.bind('roomOpenRequest', function() {
44117				myDeco.user.loginRequired(function() {
44118					self._view.openRooms();
44119					self.registerControls();
44120					self.mode = '';
44121
44122					// we need to use timeout here because right after show()
44123					// content is still invisible (fadeIn)
44124					window.setTimeout(function() {
44125						self.loadRooms();
44126					}, 200);
44127				});
44128			})
44129			.bind('roomLoadRequest', function(e, url, callback, noClosePo
44129pup) {
44130				var loadRoom = function() {
44131						myDeco.body.trigger('loadRoom', [url]);
44132						if (callback) {
44133							callback.call();
44134						}
44135					};
44136
44137					if (myDeco.App.model.history.counter === 0 || noClosePopup) {
44138						loadRoom();
44139					} else {
44140						// ask user about unsaved changes and run run loading
44141						myDeco.popin.confirm({
44142							title: gettext('Unsaved project'),
44143							message: gettext('You have an unsaved project. Do you want to save it now?'),
44144							yesButton: function() {
44145								myDeco.body.trigger('roomSaveRequest', {callback: loadRoom});
44146							},
44147							noButton: loadRoom
44148						});
44149					}
44150			})
44151			.bind('roomSaveRequest', function(e, options) {
44152                var user_options = options.user_options,
44153                    m = myDeco.App.model;
44154
44155                if (m._metadata.type != 'montage' && !m._masking && m.walls.length == 0 && m.components.length == 0) {
44156                    myDeco.body.trigger('status', 'Cannot save an empty room', 3);
44157                    var msg = {name: "error", code: 1, message: "room is empty"};
44158                    window.parent.postMessage(msg, "*");
44159                    window.parent.postMessage(JSON.stringify(msg), "*"); // IE9
44160                    return;
44161                }
44162
44163				self._options = options || {};
44164				self._options.forceNew = self._options.forceNew || myDeco.App.model.isNew;
44165				self._options.hqRender = self._options.hqRender || (user_options && user_options.hq_render) || false;
44166				self._saveAction = $.isFunction(self._options.callback) ? self._options.callback : false;
44167
44168				myDeco.user.loginRequired(function() {
44169					myDeco.body.trigger('roomMetadata', [function(room) {
44170                        if (!room.category && myDeco.settings.WL && myDeco.settings.WL.DEFAULT_ROOM_CATEGORY) {
44171                            room.category = myDeco.settings.WL.DEFAULT_ROOM_CATEGORY;
44172                        }
44173                        if (user_options && user_options.room_attributes) {
44174                            $.extend(room, user_options.room_attributes);
44175                            if (myDeco.settings.ROOM_CATEGORY_LIST.indexOf(room.category) == -1) {
44176                                room.category = 'Other';
44177                            }
44178                        }
44179						if ((self._options.forceNew || self._options.showProperties) && !(user_options && user_options.hide_dialog)) {
44180							self._view.save(room, [], self._options.forceNew);
44181						} else {
44182							self.saveRoom(room);
44183						}
44184					}]);
44185				}, 'register-mydeco');
44186			})
44187			.bind('actionRequest', function(e, action) {
44188				switch (action) {
44189					case 'open':
44190						myDeco.body.trigger('roomOpenRequest');
44191						break;
44192					case 'save':
44193					case 'saveas':
44194						myDeco.body.trigger('roomSaveRequest',
44195							[{forceNew: action === 'saveas'}]);
44196						break;
44197				}
44198			})
44199			.bind('roomSaving', function() {
44200				myDeco.App.model.getModel();
44201			});
44202	},
44203
44204	/**
44205	 * Register controls in open popup
44206	 */
44207	registerControls: function() {
44208		var self = this,
44209			getRoom = function(e) {
44210				var element = $(e.target).closest('li, .item'),
44211					index, source = 'rooms';
44212
44213				if (element.get(0).tagName === 'LI') {
44214					source = self.mode ? 'snapshots' : 'rooms';
44215					index = self._view[source].children().index(element);
44216				} else {
44217					index = parseInt(element.data('index'), 10);
44218				}
44219				return self._model[source][index];
44220			};
44221
44222		// back button
44223		this._view._canvas.find('.back').click(function(e) {
44224			e.stop();
44225
44226			self._view._canvas.removeClass(self.mode);
44227			self.mode = '';
44228			self._view.render();
44229		});
44230
44231		this._view._canvas.find('.list-type')
44232			.removeClass('selected')
44233			.click(function(e) {
44234				e.stop();
44235
44236				var button = $(this),
44237					type = button.data('type');
44238
44239				if (!button.hasClass('selected')) {
44240					self.type = type;
44241					if (self.mode) {
44242						self._view.render();
44243					} else {
44244						self.loadRooms();
44245					}
44246					button
44247						.addClass('selected')
44248						.siblings().removeClass('selected');
44249				}
44250			})
44251			.filter('.' + this.type)
44252				.addClass('selected');
44253
44254		this._view._holder.bind('load', function() {
44255			self.loadRooms(true);
44256		});
44257
44258		// load room click handler
44259		this._view._canvas
44260			.delegate('.load', 'click', function(e) {
44261				e.stop();
44262				var room = getRoom(e),
44263					url = room.latest_snapshot || room.url;
44264
44265				// do not move close after trigger it will cause
44266				// problems with next popup opening
44267				self._view.close();
44268				myDeco.body.trigger('roomLoadRequest', [url]);
44269			})
44270			.delegate('.image', 'mouseenter mouseleave', function(e) {
44271				$(this).children('.hover').toggle(e.type === 'mouseenter');
44272			})
44273			.delegate('.snapshot', 'click', function(e) {
44274				e.stop();
44275
44276				var room = getRoom(e);
44277
44278				self.mode = 'snapshots';
44279				self._view.openSnapshots(room);
44280				self._view.loading(true);
44281				self._model.loadSnapshots(room, function() {
44282					self._view.render();
44283					self._view.loading(false);
44284				});
44285			})
44286			.delegate('.yoostyler', 'click', function(e) {
44287				e.stop();
44288				var room = getRoom(e);
44289				//var socket = new WebSocket("ws://192.168.178.54:3000/ipad");
44290				var socket = new WebSocket("ws://10.0.0.147:3000/ipad");
44291				socket.onerror = function(e) {
44292					console.log("WebSocket error", e)
44293				}
44294				socket.onclose = function(e) {
44295					console.log("WebSocket closed", e)
44296				}
44297				socket.onopen = function(e) {
44298					socket.send(
44299          				JSON.stringify( {
44300            				kind: 'command',
44301            				name: 'load room',
44302        	    			extra: { room_id: room.id, user_id: myDeco.userid }
44303    	      			} )
44304	        		);
44305					self._view.close();
44306				}
44307			})
44308			.delegate('.delete', 'click', function(e) {
44309				e.stop();
44310				var room = getRoom(e),
44311					source = self.mode ? 'snapshots' : 'rooms';
44312
44313				if (window.confirm('Do you really want to remove room «' +
44314						room.name.toHtml() + '»?')) {
44315
44316					self._view.loading(true);
44317					self._model.remove(room, function(index) {
44318						self._view[source].children().eq(index).remove();
44319						if (self.mode) {
44320							self._view.loading(false);
44321						} else {
44322							self.loadRooms(true, 1);
44323						}
44324					}, self.mode);
44325				}
44326			})
44327			.delegate('.visibility', 'click', function(e) {
44328				e.stop();
44329				var link = $(this),
44330					room = getRoom(e);
44331
44332				room['public'] = link.hasClass('public') ? 0 : 1;
44333				self._model.save(room, function() {
44334					link.siblings('.visibility').andSelf().toggle();
44335					if (self.mode) {
44336						self._view.replaceRoom(room);
44337					}
44338				});
44339			})
44340			.delegate('select.category, select.style', 'change', function(e) {
44341				var room = getRoom(e),
44342					selector = $(this).attr('disabled', 'disabled'),
44343					type = selector.attr('class');
44344
44345				room[type] = selector.val();
44346				self._model.save(room, function() {
44347					selector.removeAttr('disabled');
44348					if (self.mode) {
44349						self._view.replaceRoom(room);
44350					}
44351				});
44352			});
44353	},
44354
44355	/**
44356	 * Load rooms list method
44357	 * @param {Boolean} append Append flag
44358	 * @param {Number} limit Number of items to fetch
44359	 */
44360	loadRooms: function(append, limit) {
44361		var self = this;
44362
44363		append = !!append;
44364
44365		this._model.limit = limit ||
44366			this._view.getLimit() * (2 - +append);
44367
44368		this._view.loading(true);
44369		this._model.load(function(data) {
44370			self._view.render(data, append);
44371			self._view.loading(false);
44372		}, append);
44373	},
44374
44375	/**
44376	 * Save room
44377	 * @param {Object} data Room metadata
44378	 * @param {Array} share List of social networks room share to
44379	 */
44380	saveRoom: function(room, share) {
44381		room = room || {};
44382		share = share || [];
44383
44384		var self = this,
44385			errors = $.map(['name', 'category'], function(field) {
44386				return room[field] ? null : field;
44387			}),
44388			saveSuccess,
44389			saveError = function(e, code, status, message) {
44390				myDeco.body.unbind('roomSaved', saveSuccess);
44391				self._view.showError(gettext('Please try again'), message);
44392			};
44393			saveSuccess = function() {
44394				var i;
44395
44396				myDeco.body.unbind('roomSavingError', saveError);
44397
44398				// lets share something
44399				for (i = share.length - 1; i >= 0; i--) {
44400					myDeco.sharethis['open' + share[i].capitalize()]
44401						.call(myDeco.sharethis, myDeco.App.model.weburl);
44402				}
44403
44404				if ((self._options.forceNew || self._options.showProperties) &&
44405                    !(myDeco.settings.WL && myDeco.settings.WL.NO_SAVE_SUCCESS_POPUP)) {
44406					self._view.success(!room['public']);
44407				}
44408
44409				// execute planned action after saving
44410				if (self._saveAction) {
44411					self._saveAction();
44412					self._saveAction = false;
44413				}
44414			};
44415
44416		if (errors.length === 0) {
44417			this._view.close();
44418			myDeco.body
44419				.trigger('roomMetadata', room)
44420				.one('roomSaved', saveSuccess)
44421				.one('roomSavingError', saveError)
44422				.trigger('saveRoom', {
44423					forceNew: this._options.forceNew || false,
44424					hqRender: this._options.hqRender || false
44425				});
44426		} else {
44427			this._view.save(room, errors, this._options.forceNew);
44428		}
44429	}
44430});
44431
44432myDeco.App.controllers.push(RoomManagementController);
44433/*globals AppView, $, myDeco, tmpl */
44434/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
44435          sloppy: true, white: true */
44436/**
44437 * @fileOverview View class for room management app component
44438 */
44439var RoomManagementView = AppView.extend({
44440
44441	/**
44442	 * Component type
44443	 * @type {String}
44444	 */
44445	$type: 'room-management',
44446
44447	/**
44448	 * Active canvas holder
44449	 * @type {jQuery}
44450	 */
44451	_canvas: null,
44452
44453	/**
44454	 * Items holder
44455	 * @type {jQuery}
44456	 */
44457	rooms: null,
44458
44459	/**
44460	 * Loader holder (WoD)
44461	 * @type {jQuery}
44462	 */
44463	_loader: null,
44464
44465	/**
44466	 * Rooms list holder
44467	 * @type {jQuery}
44468	 */
44469	_holder: null,
44470
44471	/**
44472	 * Snapshots list
44473	 * @type {jQuery}
44474	 */
44475	snapshots: null,
44476
44477	/**
44478	 * flag do we need first empty option for new-created select
44479	 * @type {Boolean}
44480	 */
44481	noEmpty: false,
44482
44483	/**
44484	 * Make link as string for room visibility
44485	 * @param {Object} room
44486	 * @returns {String}
44487	 */
44488	_makeVisibilityLink: function(room) {
44489		var invisible = ' style="display:none"';
44490		return  '<a href="" class="visibility private"' + (room['public'] ? invisible : '') + '>' + gettext('Make public') + '</a>' +
44491				'<a href="" class="visibility public"' + (!room['public'] ? invisible : '') + '>' + gettext('Make private') + '</a>';
44492	},
44493
44494	/**
44495	 * Returns jQset of constructed selector
44496	 * @param {String} name Type of element
44497	 * @param {Array} options
44498	 * @param {String} value Value of the selected element
44499	 * @returns {String}
44500	 */
44501	_makeSelector: function(name, options, value, id) {
44502		var type = name.toHtml(),
44503			text = '<select name="' + type + '" class="' + type + '"'
44504				+ (id ? ' id="' + id + '"' : '')
44505				+ '>' + (this.noEmpty ? '' : '<option></option>'),
44506			i, ilen, option;
44507
44508		for (i = 0, ilen = options.length; i < ilen; i++) {
44509			option = options[i].toHtml();
44510			text += '<option value="' + option + '"' + (options[i] === value ?
44511				' selected="selected"' : '') + '>' + gettext(option) + '</option>';
44512		}
44513
44514		text += '</select>';
44515		return text;
44516	},
44517
44518	/**
44519	 * Returns jQset of constructed selector
44520	 * @param {String} name Type of element
44521	 * @param {Array} options
44522	 * @param {String} value Value of the selected element
44523	 * @returns {String}
44524	 */
44525	_makeSelector2: function(name, options, value, id) {
44526		var type = name.toHtml(), opt,
44527			text = '<select name="' + type + '" class="' + type + '"'
44528				+ (id ? ' id="' + id + '"' : '')
44529				+ '>' + (this.noEmpty ? '' : '<option></option>'),
44530			i, ilen, _value, _text;
44531
44532		for (i = 0, ilen = options.length; i < ilen; i++) {
44533			opt = options[i];
44534
44535			if ($.isArray(opt)) {
44536				_value = opt[0];
44537				_text = opt[1];
44538			} else {
44539				_value = options[i].toHtml();
44540				_text = _value;
44541			}
44542
44543			text += '<option value="' + _value + '"' + (_value === value ?
44544				' selected="selected"' : '') + '>' + _text + '</option>';
44545		}
44546
44547		text += '</select>';
44548		return text;
44549	},
44550
44551	/**
44552	 * Returns jQset of constructed selector
44553	 * @param {String} name Type of element
44554	 * @param {Array} options
44555	 * @param {String} value Value of the selected element
44556	 * @returns {String}
44557	 */
44558	_makeList: function(name, options, value) {
44559		var type = name.toHtml(), text = '',
44560			i, ilen, option, classname, value, selected;
44561
44562		for (i = 0, ilen = options.length; i < ilen; i++) {
44563			option = options[i];
44564			_value = option.toHtml();
44565			selected = (option === value || !value && option === 'Other');
44566			classname = 'category-thumb ' + option.toLowerCase().split(' ')[0] + '-link'
44567			       	+ (selected ? ' selected' : '');
44568
44569			text += '<label class="' + classname + '">' + gettext(_value)
44570				+ '<input type="radio" name="category" value="' + _value
44571			      	+ (selected ? '" checked="checked' : '') + '" /></label>';
44572		}
44573
44574		return text;
44575	},
44576
44577	/**
44578	 * Main show function, opens popup and fill internal variables with nodes
44579	 */
44580	openRooms: function() {
44581		var self = this;
44582		myDeco.popin.open({
44583			url: '#view-' + this.$type + '-rooms',
44584			fullscreen: true,
44585			className: 'vrm-rooms',
44586			afterOpen: function(content) {
44587				self._canvas = content.children();
44588				self._loader = self._canvas.find('.loading');
44589				self._holder = self._canvas.find('.items-holder')
44590					.scrollLoader()
44591					.iscroll();
44592				self.rooms = self._holder.find('.items');
44593				self.snapshots = self._canvas.find('.snapshots-holder')
44594					.iscroll()
44595					.find('.items');
44596				self._holder.find('.scroller').scrollTop(0);
44597			},
44598			tracking: 'room-management'
44599		});
44600	},
44601
44602	/**
44603	 * Function for calculation of limit of loaded items per request
44604	 * @returns {Number}
44605	 */
44606	getLimit: function() {
44607		var result = 100;
44608		switch (this._controller.type) {
44609			case 'normal':
44610				result = Math.floor(this._holder.width() / 211) *
44611					Math.ceil(this._holder.height() / 250);
44612				break;
44613			case 'extended':
44614				result = Math.ceil(this._holder.height() / 446) * 4;
44615				break;
44616		}
44617		return result;
44618	},
44619
44620	/**
44621	 * Rendering list
44622	 * @param {Array} rooms
44623	 * @param {Boolean} append Append to list
44624	 */
44625	render: function(rooms, append) {
44626		var self = this,
44627			source = this._controller.mode ? 'snapshots' : 'rooms';
44628
44629		rooms = rooms || self._model[source];
44630
44631		if (!append || this._controller.mode) {
44632			this[source]
44633				.html('')
44634				.removeClass('normal extended')
44635				.addClass(this._controller.type);
44636		}
44637
44638		$.each(rooms, function() {
44639			self[source].append(self.room(this, self._controller.mode));
44640		});
44641	},
44642
44643	/**
44644	 * Render and return room representation
44645	 * @param {Object} room
44646	 * @param {Boolean} snapshots Snapshot mode
44647	 * @returns {String}
44648	 */
44649	room: function(room, snapshots) {
44650		return tmpl('view-' + this.$type + '-room-' + this._controller.type, {
44651			room: room,
44652			category: this._makeSelector('category',
44653				myDeco.settings.ROOM_CATEGORY_LIST, room.category),
44654			style: this._makeSelector('style',
44655				myDeco.settings.ROOM_STYLE_LIST, room.style),
44656			visibility: this._makeVisibilityLink(room),
44657			snapshots: snapshots
44658		});
44659	},
44660
44661	/**
44662	 * Replace room view inplace in rooms holder
44663	 * @param {Object} room
44664	 */
44665	replaceRoom: function(room) {
44666		var index = this._model.rooms.indexOf(room);
44667		this.rooms.children().eq(index)
44668			.replaceWith(this.room(room));
44669	},
44670
44671	/**
44672	 * Siwitch open popup to snapshots mode
44673	 * @param {Object} room
44674	 */
44675	openSnapshots: function(room) {
44676		this._canvas
44677			.addClass('snapshots')
44678			.find('h1.snapshots span').html(room.name).end()
44679			.find('.item')
44680				.data({index: this._model.rooms.indexOf(room)})
44681				.html(tmpl('view-' + this.$type + '-room-info', {
44682					room: room,
44683					category: this._makeSelector('category',
44684						myDeco.settings.ROOM_CATEGORY_LIST, room.category),
44685					style: this._makeSelector('style',
44686						myDeco.settings.ROOM_STYLE_LIST, room.style),
44687					visibility: this._makeVisibilityLink(room)
44688				}));
44689	},
44690
44691	/**
44692	 * small method wich select checked input's label
44693	 * @param {Object} container with inputs
44694	 */
44695	selectLabels: function(container, disableParent) {
44696		container
44697			.find('input[type="checkbox"], input[type="radio"]')
44698			.change(function() {
44699				var box = $(this), text = box.data('text'), daddy = box.parent();
44700
44701				//checkboxes can have multiple selection
44702				if (this.type !== 'checkbox') {
44703					box.closest('div.input-container')
44704						.find('label').removeClass('selected');
44705				}
44706
44707				daddy.toggleClass('selected');
44708
44709				if (text) {
44710					text = text.split(';')
44711					daddy.find('i').text(daddy.hasClass('selected') ? text[1] : text[0]);
44712				}
44713
44714				if (disableParent) {
44715					container.find('form').toggleClass('disabled', !container.find('input:checked').length)
44716				}
44717			});
44718	},
44719
44720	/**
44721	 * Show save popup with errors if exists
44722	 * @param {Object} room
44723	 * @param {Array} errors Array of error names
44724	 * @param {Boolean} forceNew Full save dialogue flag
44725	 */
44726	save: function(room, errors, forceNew) {
44727		errors = errors || [];
44728		var self = this,
44729			type = forceNew || errors.length !== 0 ? 'save-as' : 'save';
44730
44731        // set public to false by default, if we are saving a room, created by other user
44732        if ((type == 'save-as') && (room.author) && (room.author !== myDeco.userData.username) && !myDeco.settings.WL) {
44733            room["public"] = false;
44734        }
44735
44736		myDeco.popin.open({
44737			url: '#view-' + this.$type + '-' + type,
44738			width: 775,
44739			holderClassName: 'vrm-popin',
44740			context: {
44741				room: room,
44742				category: this._makeList('category', myDeco.settings.ROOM_CATEGORY_LIST, room.category),
44743				style: this._makeSelector('style', myDeco.settings.ROOM_STYLE_LIST, room.style || gettext('Other'), 'vrm-category')
44744			},
44745			afterOpen: function(content) {
44746				var exposure = room.exposure || 0,
44747					form = content.find('form');
44748
44749				function showPostcode (value) {
44750					var show_postcode = ['France', 'United Kingdom', 'United States'].indexOf(value) !== -1;
44751					form.find('.postcode-container').toggleClass('inactive', !show_postcode);
44752				}
44753
44754				content.parent().iscroll();
44755
44756				$.each(errors, function(i, name) {
44757					var input;
44758					form.find('.error.' + name)
44759						.text(gettext('This field is required.'))
44760						.show();
44761					if (i === 0) {
44762						input = form.find('[name=' + name + ']').focus();
44763						if (input.get(0).select) {
44764							input.get(0).select();
44765						}
44766					}
44767				});
44768
44769				self.selectLabels(form);	//common for fpe and montage
44770
44771				if (myDeco.App.model._metadata.type.indexOf('3d') > -1) { //fpe
44772
44773					$.ajax({url:'https://vault.floorplanner.com/geo/ip2country.js', dataType: '
44773script', cache: true}).done(function() {
44774						content.find('label.country-anchor').after(
44775							self._makeSelector2('country', myDeco.COUNTRIES, room.country || geoip_country_code(), 'vrm-country')
44776						);
44777						content.find('select.country, select.style').htmlDropdown({
44778							template: function(obj) { return '<li>' + this.insertText(obj) + '</li>'},
44779							relative: true
44780						})
44781						showPostcode(room.country || geoip_country_name());
44782					});
44783
44784					self.addProductsToWishlist(content);
44785					myDeco.popin.resize();
44786
44787					form
44788						.find('input#vrm-country-combo.combo').bind('activateAutocomplete', function() {
44789							showPostcode(this.value);
44790						}).end()
44791						.find('input[name=name], input[name=postcode]').blur(self.validateInputs.bind(this)).end()
44792						.find('input[name=prop]').change(function() {
44793							var operand = (+this.value === 1) ? 1 : -1;
44794							content
44795								.find('.country-container').toggleClass('opened')
44796								.end()
44797								.parent().css({height: content.parent().outerHeight() + (70 * operand)});
44798						}).end()
44799						.find('.exposure').slider({
44800							min: -4,
44801							max: 4,
44802							value: exposure,
44803							onChange: function(value) {
44804								exposure = value;
44805							}
44806						}).end()
44807						.find('.button.save').click(function(e) {
44808							e.stop();
44809							if (!form.hasClass('disabled')) {
44810								form.submit();
44811							}
44812						}).end()
44813						.submit(function(e) {
44814							e.stop();
44815
44816							var publicRadio = content.find('input[name=public]:checked'), address,
44817								_showCountry = content.find('input[name=public]:checked'),
44818								showCountry = _showCountry.length && +_showCountry.val() === 1;
44819							if (publicRadio.size() !== 0) {
44820								room['public'] = !!parseInt(publicRadio.val(), 10);
44821							}
44822
44823							$.extend(room, {
44824								hq_renderer: content.find('input[name=hq_renderer]:checked').val() ||
44825									'realityserver',
44826								exposure: exposure
44827							});
44828
44829                            room.force_thickness = form.find('#with_thickness:checked').size() > 0;
44830
44831							if (showCountry) {
44832								address = {
44833										'country': form.find('select.country').val(),
44834										'post_code': form.find('input[name="postcode"]').val()
44835									}
44836							} else {
44837								address = {};
44838							}
44839
44840							if (type === 'save-as') {
44841								$.extend(room, {
44842									'style': form.find('select[name=style]').val(),
44843									'name': form.find('input[name=name]').val(),
44844									'category': form.find('input.category, select.category, input[name=category]:checked').val(),
44845									'address': address,
44846									'prop': showCountry
44847								});
44848							}
44849
44850							self._controller.saveRoom(room);
44851					});
44852				} else { //montage
44853					form
44854						.find('input[name=name]').blur(self.validateInputs.bind(this)).end()
44855						.find('.button.save').click(function(e) {
44856						e.stop();
44857						if (!form.hasClass('disabled')) {
44858							form.submit();
44859						}
44860					}).end()
44861					.submit(function(e) {
44862						var publicRadio = content.find('input[name=public]:checked');
44863						e.stop();
44864						if (publicRadio.size() !== 0) {
44865							room['public'] = !!parseInt(publicRadio.val(), 10);
44866						}
44867
44868						if (type === 'save-as') {
44869							$.extend(room, {
44870								'style': form.find('select[name=style]').val(),
44871								'name': form.find('input[name=name]').val(),
44872								'category': form.find('input.category, select.category, input[name=category]:checked').val()
44873							});
44874						}
44875						self._controller.saveRoom(room);
44876					});
44877				}
44878			}
44879		});
44880
44881	},
44882
44883	/**
44884	 * Close opened room management popin
44885	 */
44886	close: function() {
44887		myDeco.popin.close();
44888		this._model.reset();
44889	},
44890
44891	validateInputs: function(e) {
44892		var element = $(e.target),
44893			parent = element.parent(),
44894			form = element.closest('form'),
44895			message = myDeco.formValidation.isInvalid(element, {
44896					required: gettext('This field is required'),
44897					email: gettext('Please enter address in correct format')
44898				},
44899				form.find('#vrm-country').val()) || '';
44900
44901		parent.find('div.error').addClass(element.attr('name')).text(message).toggle(!!message);
44902		myDeco.popin.resize();
44903
44904		if (!!message) {
44905				parent.removeClass('valid');
44906				parent.addClass('invalid');
44907		} else {
44908				parent.addClass('valid');
44909				parent.removeClass('invalid');
44910		}
44911
44912		if (element.hasClass('can-be-empty') && !e.target.value) {
44913			parent.removeClass('valid');
44914		}
44915
44916		form.toggleClass('disabled', form.find('.invalid').length > 0);
44917	},
44918
44919	addProductsToWishlist: function(content) {
44920		var comp_filtered = [],
44921			self = this,
44922			ids = myDeco.App.model.uniqueComponentIds(),
44923			excludedCategories = $.map(myDeco.settings.ROOM_IMAGEMAP_EXCLUDED_CATEGORIES, function(item) {
44924				if (item[0] === myDeco.settings.LOCALE) {
44925					return item[1];
44926				}
44927			});
44928
44929		//filter ids of wishlisted components
44930		myDeco.body.trigger('component.wishlisted', [ids, function(results) {
44931			if (typeof results !== 'undefined') {
44932				comp_filtered = $.grep(ids, function(guid) {
44933					return $.inArray(guid, results) === -1;
44934				});
44935			}
44936		}]);
44937
44938		//get components by ids
44939		comp_filtered = $.grep(
44940			$.map(comp_filtered, function(id) {
44941				return $.first(myDeco.App.model.components, function(c) {
44942					return c.component_id === id;
44943				});
44944			}), function(c) {
44945			//check if it's available and has proper category
44946			return c.componentData.available &&
44947				$.inArray(c.componentData.category_id, excludedCategories) === -1;
44948			});
44949
44950		if (comp_filtered.length > 0) {
44951			content.find('.room-wishlist-button').show().click(function(e) {
44952				e.stop();
44953				self.wishlistButton = $(this);
44954
44955				self.wishlistButton.toggleClass('active', !self.wishlistButton.hasClass('active'));
44956
44957				myDeco.body.one('roomSaving', function() {
44958					if (self.wishlistButton.hasClass('active')) {
44959						myDeco.body.trigger('component.wishlist',
44960							[$.map(comp_filtered, function(el) {return el.component_id;})]
44961						);
44962					}
44963				});
44964			});
44965		}
44966	},
44967
44968	/**
44969	 * Show/hide loading indicator
44970	 * @param {Boolean} state indicator state
44971	 */
44972	loading: function(state) {
44973		state = typeof state !== 'undefined' ? state : true;
44974		this._loader.toggle(state);
44975		this._canvas.find('h1 .room-count')
44976			.text('(' + this._model.start +
44977				(this._model.isLoadingAvailable ? '+' : '') + ')')
44978			.toggle(!state && this._model.start > 0);
44979	},
44980
44981	/**
44982	 * Show error popin with title and message
44983	 * @param {String} title
44984	 * @param {String} message
44985	 */
44986	showError: function(message, code) {
44987		myDeco.popin.error({
44988			className: 'vrm-rooms vrm-message',
44989			message: (code ? '<span class="code">' + code + '</span>' : '') + message,
44990			width: 400,
44991			height: 180
44992		});
44993	},
44994
44995	/**
44996	 * Show success message
44997	 */
44998	success: function(is_private) {
44999		var self = this, classname="", closeIn = false;
45000		is_private = is_private || myDeco.settings.WL;
45001		if (is_private) {
45002			classname = ' private-room';
45003		}
45004		// pre-load facebook cookies
45005		myDeco.facebook.getLoginStatus();
45006
45007		myDeco.popin.open({
45008			url: '#view-' + this.$type + '-save-success',
45009			className: 'vrm-success' + classname,
45010			width: 775,
45011			closeIn: closeIn,
45012			holderClassName: 'vrm-popin',
45013            context: {},
45014			afterOpen: function(content) {
45015				var link = content.find('a.rooms-list');
45016				if (/*myDeco.settings.WL || */myDeco.App.model._metadata.type === 'montage') {
45017					link.click(function(e) {
45018						e.stop();
45019						myDeco.body.trigger('roomOpenRequest');
45020					});
45021				} else {
45022					if(myDeco.settings.WL) {
45023						link.attr('href', 'https://' + myDeco.settings.WL.WL_PARTNER_NAME + '.roomstyler.com/rooms/' + myDeco.App.model._room_id);
45024					} else {
45025						link.attr('href', 'https://roomstyler.com/rooms/' + myDeco.App.model._room_id);
45026					}
45027				}
45028			}
45029		});
45030	},
45031
45032	/*
45033	 * automatically follow mydeco users
45034	 */
45035	follow: function(action, data) {
45036		if (data && data.length) {
45037			if (typeof data !== 'string') {
45038				data = $.param({id: data});
45039			}
45040			$.ajax({
45041				url: myDeco.urls.follow,
45042				data: 'profile_type=user&' + data + '&action=' + action,
45043				dataType: 'json',
45044				type: 'post',
45045				error: function(error) {
45046					console.log(error)
45047				}
45048			});
45049		}
45050	},
45051
45052	observeEmails: function(content, data) {
45053		var form = content.find('form'), self = this;
45054		console.log(content.find('input[name$="name"], input[name$="email"]'))
45055		content.find('input[name$="name"], input[name$="email"]')
45056			.blur(self.validateInputs.bind(this)).end()
45057			.find('a.invite-friends').click(function(e) {
45058				e.stop();
45059				if (form.hasClass('disabled')) {
45060					return;
45061				}
45062
45063				var names = form.find('input[name=name]'),
45064					emails = form.find('input[name=email]'),
45065					len = emails.length, arr = [], i, email, prefix, concat;
45066
45067				for (i=0; i < len; i++) {
45068					email = emails[i].value;
45069					if (names.length) {
45070						if (names[i].value && !email) {
45071							$(emails[i]).parent()
45072								.addClass('invalid').find('div.error')
45073								.text('This field is required');
45074						} else {
45075							arr[i] = [];
45076							arr[i].push(names[i].value);
45077							arr[i].push(email);
45078						}
45079						prefix = 'home';
45080					} else if (!!email) {
45081						arr.push(email);
45082						prefix = 'email';
45083					}
45084				}
45085
45086				$.ajax({
45087					url: '/rooms/ask-advice/ajax/',
45088					data: form.serialize(),
45089					type: 'post',
45090					success: function() {
45091							myDeco.body.trigger('saveFormFriendInvite', [prefix]);
45092							self.inviteConfirmation.call(self);
45093						}
45094					});
45095
45096			});
45097
45098	},
45099
45100	collectCheckboxValues: function(form, name, selector) {
45101		selector = selector || '';
45102		var inp = form.find('input[name=' + name + ']' + selector), len = inp.length, i, values = [];
45103		for (i = len; i--;) {
45104			values.push(inp[i].value);
45105		}
45106		return values;
45107	},
45108
45109	observeFB: function(content, dummy, context) {
45110		this.selectLabels(content, true);
45111		var form = content.find('form'), el, userId, len = context.friends_not_on_mydeco.length,
45112		    fb_inputs = content.find('input[name=fb_friends]'), li, ul = content.find('ul.vrm-fb-friends'),
45113		    count = 100, li_height = $('ul.vrm-fb-friends li').outerHeight();
45114
45115		function buildFriend(obj) {
45116			return '<li class="li' + obj.id + '"><span class="image" style="background-image: url(http://graph.facebook.com/'
45117				+ obj.id + '/picture/)"></span>'
45118				+ '<span class="face-name">' + obj.name + '</span>'
45119				+ '<label data-value="' + obj.id + '" class="radio">Invite</label></li>';
45120		}
45121
45122		function scrolltoFriend(num) {
45123			content.find('div.scrolled').animate({scrollTop: num * li_height});
45124		};
45125
45126		content.find('ul.vrm-fb-friends').delegate('label.radio', 'click', function(e) {
45127			el = $(this);
45128			if (!el.hasClass('selected')) {
45129				userId = el.data('value');
45130				FB.ui({
45131					to: userId,
45132					method: 'send',
45133					link: myDeco.App.model.weburl
45134				}, function (data) {
45135					if (!!data) {
45136						myDeco.body.trigger('saveFormFriendInvite', ['fb']);
45137						el.addClass('selected').text('Invited');
45138					}
45139				});
45140			}
45141		}).end()
45142		.find('a.move-fb-list').toggleClass('invisible', len < 100).click(function(e) {
45143			e.stop();
45144			count += 100;
45145			var _array = context.friends_not_on_mydeco.splice(100, count), i = 100;
45146			for ( i > -1; i--;) {
45147				if (array[i]) {
45148					$(buildFriend(a[i])).appendTo(ul)
45149				}
45150			}
45151			scrolltoFriend(count);
45152			if (len - count < 101) {
45153				$(this).addClass('invisible');
45154			}
45155
45156		}).end()
45157		.find('input.fb-autocomplete')
45158			.bind('activateAutocomplete', function(e, originValue) {
45159				if (originValue) {
45160					li = ul.find('li.li' + originValue.id);
45161					if (!li.length) {
45162						li = $(buildFriend(originValue)).prependTo(ul);
45163					}
45164					scrolltoFriend(li.index());
45165					li.find('label.radio').trigger('click');
45166				}
45167			})
45168			.autocomplete({
45169				param: 'q',
45170				ajax: myDeco.urls.facebookFriendsAutocomplete,
45171				list: $.map(context.friends_not_on_mydeco, function (name) {return name; }),
45172				insertText: function (obj) {return obj.name; },
45173				template: buildFriend,
45174				wrapper: '<div class="autocomplete super-top"><ul class="share-list"></ul></div>'
45175			}).end()
45176		.find('form').submit(function(e) {e.stop()});
45177
45178	},
45179
45180	inviteConfirmation: function() {
45181		myDeco.popin.open({
45182			url: '#view-' + this.$type + '-invite-confirm',
45183			className: 'vrm-confirm',
45184			width: 775,
45185			holderClassName: 'vrm-popin',
45186			closeIn: 30,
45187			afterOpen: function(content) {
45188				content.find('a.save').click(function(e) {
45189					e.stop();
45190					window.open(this.href);
45191				})
45192			}
45193		});
45194	},
45195
45196	invite: function(callback, context, data, beforeClose) {
45197		var self = this, _content;
45198		myDeco.popin.open({
45199			url: '#view-' + self.$type + '-save-invite',
45200			className: 'vrm-invite',
45201			width: 775,
45202			context: context,
45203			holderClassName: 'vrm-popin',
45204			afterOpen: function(content) {
45205				_content = content;
45206				callback.call(self, content, data, context)
45207			},
45208			beforeClose: function(content) {
45209				if ($.isFunction(beforeClose)) {
45210					beforeClose.call(self, _content);
45211				}
45212			}
45213		});
45214	}
45215});
45216
45217myDeco.App.views.push(RoomManagementView);
45218myDeco.moveTracker = {
45219	
45220	items: $(),
45221	
45222	properties: [],
45223	
45224	active: false,
45225	
45226	init: function(elements) {
45227		var self = this;
45228		
45229		this.items = elements.bind('moved restored minimized maximized raised lowered', function(e) {
45230			self.setProperties($(this));
45231		});
45232		
45233		myDeco.window.bind('resize', this.setAllProperties.bind(this));
45234		
45235		myDeco.body.mousemove(this.findActive.bind(this)).mousedown(this.findActive.bind(this));
45236		
45237		this.setAllProperties();
45238	},
45239	
45240	setAllProperties: function() {
45241		var self = this;
45242		this.items.each2(function(i, item) {
45243			self.setProperties(item);
45244		});
45245	},
45246	
45247	setProperties: function(item) {
45248		var params = {
45249				enabled: item.is(':visible'),
45250				x1: 0, x2: 0, y1: 0, y2: 0, z: 0
45251		}, offset;
45252		
45253		if (params.enabled) {
45254			offset = item.offset();
45255			$.extend(params, {
45256				x1: offset.left,
45257				x2: offset.left + item.width(),
45258				y1: offset.top,
45259				y2: offset.top + item.height(),
45260				z: +item.css('zIndex') || 0
45261			});
45262		}
45263		this.properties[this.items.index(item.get(0))] = params;
45264	},
45265	
45266	inArea: function(e, area) {
45267		return e.pageX >= area.x1 && e.pageX <= area.x2 &&
45268				e.pageY >= area.y1 && e.pageY <= area.y2;
45269	},
45270	
45271	findActive: function(e) {
45272		var zIndex = -1,
45273			self = this,
45274			active = -1,
45275			offset, item, area, z;
45276		
45277		$.each(this.properties, function(i, area) {
45278			if (area.enabled && self.inArea(e, area) && area.z > zIndex) {
45279				zIndex = area.z;
45280				active = i;
45281			}
45282		});
45283		
45284		if (this.active && (active == -1 || this.active.get(0) !== this.items.get(active))) {
45285			this.active.trigger('canvasOut', [e]);
45286			this.active = false;
45287		}
45288		
45289		if (active !== -1 && !this.active) {
45290			this.active = this.items.eq(active);
45291			this.active.trigger('canvasIn', [e]);
45292		}
45293	}
45294	
45295};
45296
45297$(function(e) {
45298	myDeco.moveTracker.init($('.move-tracker'));
45299});
45300$(function() {
45301    $(window).bind('message', function(e) {
45302        var data = e.originalEvent.data;
45303
45304        if ((typeof data === 'string') && data[0] === '{') {
45305            data = JSON.parse(data);
45306        }
45307
45308        var name = typeof data === 'object' ? data.name : data;
45309        delete data.name;
45310
45311        switch (name) {
45312        case 'refresh_userinfo':
45313            myDeco.setUserData();
45314            myDeco.user.updateLoginArea();
45315            break;
45316        case 'logout':
45317            myDeco.user.ajaxLogoutClickHandler();
45318            break;
45319        case 'save':
45320            var opts = {forceNew: false};
45321            if (typeof data === 'object') {
45322                opts.forceNew = data.forceNew || false;
45323                data = _.pick(data, 'room_attributes', 'render_hq', 'hide_dialog');
45324                if (data.room_attributes) {
45325                    data.room_attributes = _.pick(data.room_attributes, 'name', 'public', 'category');
45326                }
45327                opts.user_options = data;
45328            }
45329			myDeco.body.trigger('roomSaveRequest', [opts]);
45330            break;
45331        case 'save_and_render':
45332            myDeco.body.trigger('roomSaveRequest', [{showProperties: true, hqRender: true}]);
45333            break;
45334        }
45335    });
45336});
45337//     Underscore.js 1.8.2
45338//     http://underscorejs.org
45339//     (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
45340//     Underscore may be freely distributed under the MIT license.
45341(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=d(e,i,4);var o=!w(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=b(r,e);for(var u=null!=t&&t.length,i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t){var r=S.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||o,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=S[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var e=this,u=e._,i=Array.prototype,o=Object.prototype,a=Function.prototype,c=i.push,l=i.slice,f=o.toString,s=o.hasOwnProperty,p=Array.isArray,h=Object.keys,v=a.bind,g=Object.create,y=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):e._=m,m.VERSION="1.8.2";var d=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},b=function(n,t,r){return null==n?m.identity:m.isFunction(n)?d(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return b(n,t,1/0)};var x=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var l=o[c];t&&r[l]!==void 0||(r[l]=i[l])}return r}},_=function(n){if(!m.isObject(n))return{};if(g)return g(n);y.prototype=n;var t=new y;return y.prototype=null,t},j=Math.pow(2,53)-1,w=function(n){var t=n&&n.length;return"number"==typeof t&&t>=0&&j>=t};m.each=m.forEach=function(n,t,r){t=d(t,r);var e,u;if(w(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=b(t,r);for(var e=!w(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=w(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=b(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(b(t)),r)},m.every=m.all=function(n,t,r){t=b(t,r);for(var e=!w(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=b(t,r);for(var e=!w(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r){return w(n)||(n=m.values(n)),m.indexOf(n,t,"number"==typeof r&&r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;
45341if(null==t&&null!=n){n=w(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=b(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=w(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=b(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=w(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(w(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=b(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var A=function(n){return function(t,r,e){var u={};return r=b(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=A(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=A(function(n,t,r){n[r]=t}),m.countBy=A(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):w(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:w(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=b(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var k=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=n&&n.length;a>o;o++){var c=n[o];if(w(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=k(c,t,r));var l=0,f=c.length;for(u.length+=f;f>l;)u[i++]=c[l++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return k(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){if(null==n)return[];m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=b(r,e));for(var u=[],i=[],o=0,a=n.length;a>o;o++){var c=n[o],l=r?r(c,o,n):c;t?(o&&i===l||u.push(c),i=l):r?m.contains(i,l)||(i.push(l),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(k(arguments,!0,!0))},m.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=k(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,"length").length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=n&&n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.indexOf=function(n,t,r){var e=0,u=n&&n.length;if("number"==typeof r)e=0>r?Math.max(0,u+r):r;else if(r&&u)return e=m.sortedIndex(n,t),n[e]===t?e:-1;if(t!==t)return m.findIndex(l.call(n,e),m.isNaN);for(;u>e;e++)if(n[e]===t)return e;return-1},m.lastIndexOf=function(n,t,r){var e=n?n.length:0;if("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1)),t!==t)return m.findLastIndex(l.call(n,0,e),m.isNaN);for(;--e>=0;)if(n[e]===t)return e;return-1},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=b(r,e,1);for(var u=r(t),i=0,o=n.length;o>i;){var a=Math.floor((i+o)/2);r(n[a])<u?i=a+1:o=a}return i},m.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var O=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=_(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(v&&n.bind===v)return v.apply(n,l.call(arguments,1
vendor: 9,077 bytes, lines 45341-45343
45341));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return O(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e<arguments.length;)i.push(arguments[e++]);return O(n,r,this,this,i)};return r},m.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var l=m.now();a||r.leading!==!1||(a=l);var f=t-(l-a);return e=this,u=arguments,0>=f||f>t?(o&&(clearTimeout(o),o=null),a=l,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,f)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var l=m.now()-o;t>l&&l>=0?e=setTimeout(c,t-l):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var l=r&&!e;return e||(e=setTimeout(c,t)),l&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var F=!{toString:null}.propertyIsEnumerable("toString"),S=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(h)return h(n);var t=[];for(var e in n)m.has(n,e)&&t.push(e);return F&&r(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var e in n)t.push(e);return F&&r(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=b(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=x(m.allKeys),m.extendOwn=m.assign=x(m.keys),m.findKey=function(n,t,r){t=b(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=d(t,r)):(u=k(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var l=u[a],f=o[l];e(f,l,o)&&(i[l]=f)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(k(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=x(m.allKeys,!0),m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var E=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=f.call(n);if(u!==f.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!E(n[c],t[c],r,e))return!1}else{var l,s=m.keys(n);if(c=s.length,m.keys(t).length!==c)return!1;for(;c--;)if(l=s[c],!m.has(t,l)||!E(n[l],t[l],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return E(n,t)},m.isEmpty=function(n){return null==n?!0:w(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=p||function(n){return"[object Array]"===f.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return f.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===f.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&s.call(n,t)},m.noConflict=function(){return e._=u,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=function(n){return function(t){return null==t?void 0:t[n]}},m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=d(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var M={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},N=m.invert(M),I=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=I(M),m.unescape=I(N),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var B=0;m.uniqueId=function(n){var t=++B+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,R={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},q=/\\|'|\r|\n|\u2028|\u2029/g,K=function(n){return"\\"+R[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||T).source,(t.interpolate||T).source,(t.evaluate||T).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(q,K),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},l=t.variable||"obj";return c.source="function("+l+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var z=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return c.apply(n,arguments),z(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=i[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],z(this,r)}}),m.each(["concat","join","slice"],function(n){var t=i[n];m.prototype[n]=function(){return z(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
45342
45343(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}}
45343)(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h<u;h++){t=o[h];if(a=this._events[t]){this._events[t]=s=[];if(e||r){for(l=0,f=a.length;l<f;l++){n=a[l];if(e&&e!==n.callback&&e!==n.callback._callback||r&&r!==n.context){s.push(n)}}}if(!s.length)delete this._events[t]}}return this},trigger:function(t){if(!this._events)return this;var e=o.call(arguments,1);if(!c(this,"trigger",t,e))return this;var i=this._events[t];var r=this._events.all;if(i)f(i,e);if(r)f(r,arguments);return this},stopListening:function(t,e,r){var s=this._listeningTo;if(!s)return this;var n=!e&&!r;if(!r&&typeof e==="object")r=this;if(t)(s={})[t._listenId]=t;for(var a in s){t=s[a];t.off(e,r,this);if(n||i.isEmpty(t._events))delete this._listeningTo[a]}return this}};var l=/\s+/;var c=function(t,e,i,r){if(!i)return true;if(typeof i==="object"){for(var s in i){t[e].apply(t,[s,i[s]].concat(r))}return false}if(l.test(i)){var n=i.split(l);for(var a=0,o=n.length;a<o;a++){t[e].apply(t,[n[a]].concat(r))}return false}return true};var f=function(t,e){var i,r=-1,s=t.length,n=e[0],a=e[1],o=e[2];switch(e.length){case 0:while(++r<s)(i=t[r]).callback.call(i.ctx);return;case 1:while(++r<s)(i=t[r]).callback.call(i.ctx,n);return;case 2:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a);return;case 3:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a,o);return;default:while(++r<s)(i=t[r]).callback.apply(i.ctx,e);return}};var d={listenTo:"on",listenToOnce:"once"};i.each(d,function(t,e){u[e]=function(e,r,s){var n=this._listeningTo||(this._listeningTo={});var a=e._listenId||(e._listenId=i.uniqueId("l"));n[a]=e;if(!s&&typeof r==="object")s=this;e[t](r,s,this);return this}});u.bind=u.on;u.unbind=u.off;i.extend(e,u);var p=e.Model=function(t,e){var r=t||{};e||(e={});this.cid=i.uniqueId("c");this.attributes={};if(e.collection)this.collection=e.collection;if(e.parse)r=this.parse(r,e)||{};r=i.defaults({},r,i.result(this,"defaults"));this.set(r,e);this.changed={};this.initialize.apply(this,arguments)};i.extend(p.prototype,u,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(t){return i.clone(this.attributes)},sync:function(){return e.sync.apply(this,arguments)},get:function(t){return this.attributes[t]},escape:function(t){return i.escape(this.get(t))},has:function(t){return this.get(t)!=null},set:function(t,e,r){var s,n,a,o,h,u,l,c;if(t==null)return this;if(typeof t==="object"){n=t;r=e}else{(n={})[t]=e}r||(r={});if(!this._validate(n,r))return false;a=r.unset;h=r.silent;o=[];u=this._changing;this._changing=true;if(!u){this._previousAttributes=i.clone(this.attributes);this.changed={}}c=this.attributes,l=this._previousAttributes;if(this.idAttribute in n)this.id=n[this.idAttribute];for(s in n){e=n[s];if(!i.isEqual(c[s],e))o.push(s);if(!i.isEqual(l[s],e)){this.changed[s]=e}else{delete this.changed[s]}a?delete c[s]:c[s]=e}if(!h){if(o.length)this._pending=r;for(var f=0,d=o.length;f<d;f++){this.trigger("change:"+o[f],this,c[o[f]],r)}}if(u)return this;if(!h){while(this._pending){r=this._pending;this._pending=false;this.trigger("change",this,r)}}this._pending=false;this._changing=false;return this},unset:function(t,e){return this.set(t,void 0,i.extend({},e,{unset:true}))},clear:function(t){var e={};for(var r in this.attributes)e[r]=void 0;return this.set(e,i.extend({},t,{unset:true}))},hasChanged:function(t){if(t==null)return!i.isEmpty(this.changed);return i.has(this.changed,t)},changedAttributes:function(t){if(!t)return this.hasChanged()?i.clone(this.changed):false;var e,r=false;var s=this._changing?this._previousAttributes:this.attributes;for(var n in t){if(i.isEqual(s[n],e=t[n]))continue;(r||(r={}))[n]=e}return r},previous:function(t){if(t==null||!this._previousAttributes)return null;return this._previousAttributes[t]},previousAttributes:function(){return i.clone(this._previousAttributes)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=this;var r=t.success;t.success=function(i){if(!e.set(e.parse(i,t),t))return false;if(r)r(e,i,t);e.trigger("sync",e,i,t)};q(this,t);return this.sync("read",this,t)},save:function(t,e,r){var s,n,a,o=this.attributes;if(t==null||typeof t==="object"){s=t;r=e}else{(s={})[t]=e}r=i.extend({validate:true},r);if(s&&!r.wait){if(!this.set(s,r))return false}
45343else{if(!this._validate(s,r))return false}if(s&&r.wait){this.attributes=i.extend({},o,s)}if(r.parse===void 0)r.parse=true;var h=this;var u=r.success;r.success=function(t){h.attributes=o;var e=h.parse(t,r);if(r.wait)e=i.extend(s||{},e);if(i.isObject(e)&&!h.set(e,r)){return false}if(u)u(h,t,r);h.trigger("sync",h,t,r)};q(this,r);n=this.isNew()?"create":r.patch?"patch":"update";if(n==="patch")r.attrs=s;a=this.sync(n,this,r);if(s&&r.wait)this.attributes=o;return a},destroy:function(t){t=t?i.clone(t):{};var e=this;var r=t.success;var s=function(){e.trigger("destroy",e,e.collection,t)};t.success=function(i){if(t.wait||e.isNew())s();if(r)r(e,i,t);if(!e.isNew())e.trigger("sync",e,i,t)};if(this.isNew()){t.success();return false}q(this,t);var n=this.sync("delete",this,t);if(!t.wait)s();return n},url:function(){var t=i.result(this,"urlRoot")||i.result(this.collection,"url")||M();if(this.isNew())return t;return t.replace(/([^\/])$/,"$1/")+encodeURIComponent(this.id)},parse:function(t,e){return t},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return!this.has(this.idAttribute)},isValid:function(t){return this._validate({},i.extend(t||{},{validate:true}))},_validate:function(t,e){if(!e.validate||!this.validate)return true;t=i.extend({},this.attributes,t);var r=this.validationError=this.validate(t,e)||null;if(!r)return true;this.trigger("invalid",this,r,i.extend(e,{validationError:r}));return false}});var v=["keys","values","pairs","invert","pick","omit"];i.each(v,function(t){p.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.attributes);return i[t].apply(i,e)}});var g=e.Collection=function(t,e){e||(e={});if(e.model)this.model=e.model;if(e.comparator!==void 0)this.comparator=e.comparator;this._reset();this.initialize.apply(this,arguments);if(t)this.reset(t,i.extend({silent:true},e))};var m={add:true,remove:true,merge:true};var y={add:true,remove:false};i.extend(g.prototype,u,{model:p,initialize:function(){},toJSON:function(t){return this.map(function(e){return e.toJSON(t)})},sync:function(){return e.sync.apply(this,arguments)},add:function(t,e){return this.set(t,i.extend({merge:false},e,y))},remove:function(t,e){var r=!i.isArray(t);t=r?[t]:i.clone(t);e||(e={});var s,n,a,o;for(s=0,n=t.length;s<n;s++){o=t[s]=this.get(t[s]);if(!o)continue;delete this._byId[o.id];delete this._byId[o.cid];a=this.indexOf(o);this.models.splice(a,1);this.length--;if(!e.silent){e.index=a;o.trigger("remove",o,this,e)}this._removeReference(o,e)}return r?t[0]:t},set:function(t,e){e=i.defaults({},e,m);if(e.parse)t=this.parse(t,e);var r=!i.isArray(t);t=r?t?[t]:[]:i.clone(t);var s,n,a,o,h,u,l;var c=e.at;var f=this.model;var d=this.comparator&&c==null&&e.sort!==false;var v=i.isString(this.comparator)?this.comparator:null;var g=[],y=[],_={};var b=e.add,w=e.merge,x=e.remove;var E=!d&&b&&x?[]:false;for(s=0,n=t.length;s<n;s++){h=t[s]||{};if(h instanceof p){a=o=h}else{a=h[f.prototype.idAttribute||"id"]}if(u=this.get(a)){if(x)_[u.cid]=true;if(w){h=h===o?o.attributes:h;if(e.parse)h=u.parse(h,e);u.set(h,e);if(d&&!l&&u.hasChanged(v))l=true}t[s]=u}else if(b){o=t[s]=this._prepareModel(h,e);if(!o)continue;g.push(o);this._addReference(o,e)}o=u||o;if(E&&(o.isNew()||!_[o.id]))E.push(o);_[o.id]=true}if(x){for(s=0,n=this.length;s<n;++s){if(!_[(o=this.models[s]).cid])y.push(o)}if(y.length)this.remove(y,e)}if(g.length||E&&E.length){if(d)l=true;this.length+=g.length;if(c!=null){for(s=0,n=g.length;s<n;s++){this.models.splice(c+s,0,g[s])}}else{if(E)this.models.length=0;var k=E||g;for(s=0,n=k.length;s<n;s++){this.models.push(k[s])}}}if(l)this.sort({silent:true});if(!e.silent){for(s=0,n=g.length;s<n;s++){(o=g[s]).trigger("add",o,this,e)}if(l||E&&E.length)this.trigger("sort",this,e)}return r?t[0]:t},reset:function(t,e){e||(e={});for(var r=0,s=this.models.length;r<s;r++){this._removeReference(this.models[r],e)}e.previousModels=this.models;this._reset();t=this.add(t,i.extend({silent:true},e));if(!e.silent)this.trigger("reset",this,e);return t},push:function(t,e){return this.add(t,i.extend({at:this.length},e))},pop:function(t){var e=this.at(this.length-1);this.remove(e,t);return e},unshift:function(t,e){return this.add(t,i.extend({at:0},e))},shift:function(t){var e=this.at(0);this.remove(e,t);return e},slice:function(){return o.apply(this.models,arguments)},get:function(t){if(t==null)return void 0;return this._byId[t]||this._byId[t.id]||this._byId[t.cid]},at:function(t){return this.models[t]},where:function(t,e){if(i.isEmpty(t))return e?void 0:[];return this[e?"find":"filter"](function(e){for(var i in t){if(t[i]!==e.get(i))return false}return true})},findWhere:function(t){return this.where(t,true)},sort:function(t){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");t||(t={});if(i.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(i.bind(this.comparator,this))}if(!t.silent)this.trigger("sort",this,t);return this},pluck:function(t){return i.invoke(this.models,"get",t)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=t.success;var r=this;t.success=function(i){var s=t.reset?"reset":"set";r[s](i,t);if(e)e(r,i,t);r.trigger("sync",r,i,t)};q(this,t);return this.sync("read",this,t)},create:function(t,e){e=e?i.clone(e):{};if(!(t=this._prepareModel(t,e)))return false;if(!e.wait)this.add(t,e);var r=this;var s=e.success;e.success=function(t,i){if(e.wait)r.add(t,e);if(s)s(t,i,e)};t.save(null,e);return t},parse:function(t,e){return t},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(t,e){if(t instanceof p)return t;e=e?i.clone(e):{};e.collection=this;var r=new this.model(t,e);if(!r.validationError)return r;this.trigger("invalid",this,r.validationError,e);return false},_addReference:function(t,e){this._byId[t.cid]=t;if(t.id!=null)this._byId[t.id]=t;if(!t.collection)t.collection=this;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)}
45343,_onModelEvent:function(t,e,i,r){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(e&&t==="change:"+e.idAttribute){delete this._byId[e.previous(e.idAttribute)];if(e.id!=null)this._byId[e.id]=e}this.trigger.apply(this,arguments)}});var _=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","difference","indexOf","shuffle","lastIndexOf","isEmpty","chain","sample"];i.each(_,function(t){g.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.models);return i[t].apply(i,e)}});var b=["groupBy","countBy","sortBy","indexBy"];i.each(b,function(t){g.prototype[t]=function(e,r){var s=i.isFunction(e)?e:function(t){return t.get(e)};return i[t](this.models,s,r)}});var w=e.View=function(t){this.cid=i.uniqueId("view");t||(t={});i.extend(this,i.pick(t,E));this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var x=/^(\S+)\s*(.*)$/;var E=["model","collection","el","id","attributes","className","tagName","events"];i.extend(w.prototype,u,{tagName:"div",$:function(t){return this.$el.find(t)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(t,i){if(this.$el)this.undelegateEvents();this.$el=t instanceof e.$?t:e.$(t);this.el=this.$el[0];if(i!==false)this.delegateEvents();return this},delegateEvents:function(t){if(!(t||(t=i.result(this,"events"))))return this;this.undelegateEvents();for(var e in t){var r=t[e];if(!i.isFunction(r))r=this[t[e]];if(!r)continue;var s=e.match(x);var n=s[1],a=s[2];r=i.bind(r,this);n+=".delegateEvents"+this.cid;if(a===""){this.$el.on(n,r)}else{this.$el.on(n,a,r)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_ensureElement:function(){if(!this.el){var t=i.extend({},i.result(this,"attributes"));if(this.id)t.id=i.result(this,"id");if(this.className)t["class"]=i.result(this,"className");var r=e.$("<"+i.result(this,"tagName")+">").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$('<iframe src="javascript:0" tabindex="-1">');this.iframe=a.hide().appendTo("body")[0].contentWindow;this.navigate(r)}if(this._hasPushState){e.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!n){e.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=r;var o=this.location;if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){this.fragment=this.getFragment(null,true);this.location.replace(this.root+"#"+this.fragment);return true}else if(this._hasPushState&&this.atRoot()&&o.hash){this.fragment=this.getHash().replace(R,"");this.history.replaceState({},document.title,this.root+this.fragment)}}if(!this.options.silent)return this.loadUrl()},stop:function(){e.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);N.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){t=this.fragment=this.getFragment(t);return i.any(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!N.started)return false;if(!e||e===true)e={trigger:!!e};var i=this.root+(t=this.getFragment(t||""));t=t.replace(j,"");if(this.fragment===t)return;this.fragment=t;if(t===""&&i!=="/")i=i.slice(0,-1);if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if
45343(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new N;var U=function(t,e){var r=this;var s;if(t&&i.has(t,"constructor")){s=t.constructor}else{s=function(){return r.apply(this,arguments)}}i.extend(s,r,e);var n=function(){this.constructor=s};n.prototype=r.prototype;s.prototype=new n;if(t)i.extend(s.prototype,t);s.__super__=r.prototype;return s};p.extend=g.extend=$.extend=w.extend=N.extend=U;var M=function(){throw new Error('A "url" property or function must be specified')};var q=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}};return e});
45344
45345/*
45346colpick Color Picker
45347Copyright 2013 Jose Vargas. Licensed under GPL license. Based on Stefan Petre's Color Picker www.eyecon.ro, dual licensed under the MIT and GPL licenses
45348
45349For usage and examples: colpick.com/plugin
45350 */
45351
45352(function ($) {
45353	var colpick = function() {
45354		var
45355			tpl = '<div class="colpick"><div class="colpick_color"><div class="colpick_color_overlay1"><div class="colpick_color_overlay2"><div class="colpick_selector_outer"><div class="colpick_selector_inner"></div></div></div></div></div><div class="colpick_hue"><div class="colpick_hue_arrs"><div class="colpick_hue_larr"></div><div class="colpick_hue_rarr"></div></div></div><div class="colpick_new_color"></div><div class="colpick_current_color"></div><div class="colpick_hex_field"><div class="colpick_field_letter">#</div><input type="text" maxlength="6" size="6" /></div><div class="colpick_rgb_r colpick_field"><div class="colpick_field_letter">R</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_rgb_g colpick_field"><div class="colpick_field_letter">G</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_rgb_b colpick_field"><div class="colpick_field_letter">B</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_h colpick_field"><div class="colpick_field_letter">H</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_s colpick_field"><div class="colpick_field_letter">S</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_b colpick_field"><div class="colpick_field_letter">B</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_submit"></div></div>',
45356			defaults = {
45357				showEvent: 'click',
45358				onShow: function() {},
45359				onBeforeShow: function(){},
45360				onHide: function() {},
45361				onChange: function() {},
45362				onSubmit: function() {},
45363				colorScheme: 'light',
45364				color: '3289c7',
45365				livePreview: true,
45366				flat: false,
45367				layout: 'full',
45368				submit: 1,
45369				submitText: 'OK',
45370				height: 156
45371			},
45372			//Fill the inputs of the plugin
45373			fillRGBFields = function  (hsb, cal) {
45374				var rgb = hsbToRgb(hsb);
45375				$(cal).data('colpick').fields
45376					.eq(1).val(rgb.r).end()
45377					.eq(2).val(rgb.g).end()
45378					.eq(3).val(rgb.b).end();
45379			},
45380			fillHSBFields = function  (hsb, cal) {
45381				$(cal).data('colpick').fields
45382					.eq(4).val(Math.round(hsb.h)).end()
45383					.eq(5).val(Math.round(hsb.s)).end()
45384					.eq(6).val(Math.round(hsb.b)).end();
45385			},
45386			fillHexFields = function (hsb, cal) {
45387				$(cal).data('colpick').fields.eq(0).val(hsbToHex(hsb));
45388			},
45389			//Set the round selector position
45390			setSelector = function (hsb, cal) {
45391				$(cal).data('colpick').selector.css('backgroundColor', '#' + hsbToHex({h: hsb.h, s: 100, b: 100}));
45392				$(cal).data('colpick').selectorIndic.css({
45393					left: parseInt($(cal).data('colpick').height * hsb.s/100, 10),
45394					top: parseInt($(cal).data('colpick').height * (100-hsb.b)/100, 10)
45395				});
45396			},
45397			//Set the hue selector position
45398			setHue = function (hsb, cal) {
45399				$(cal).data('colpick').hue.css('top', parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h/360, 10));
45400			},
45401			//Set current and new colors
45402			setCurrentColor = function (hsb, cal) {
45403				$(cal).data('colpick').currentColor.css('backgroundColor', '#' + hsbToHex(hsb));
45404			},
45405			setNewColor = function (hsb, cal) {
45406				$(cal).data('colpick').newColor.css('backgroundColor', '#' + hsbToHex(hsb));
45407			},
45408			//Called when the new color is changed
45409			change = function (ev) {
45410				var cal = $(this).parent().parent(), col;
45411				if (this.parentNode.className.indexOf('_hex') > 0) {
45412					cal.data('colpick').color = col = hexToHsb(fixHex(this.value));
45413					fillRGBFields(col, cal.get(0));
45414					fillHSBFields(col, cal.get(0));
45415				} else if (this.parentNode.className.indexOf('_hsb') > 0) {
45416					cal.data('colpick').color = col = fixHSB({
45417						h: parseInt(cal.data('colpick').fields.eq(4).val(), 10),
45418						s: parseInt(cal.data('colpick').fields.eq(5).val(), 10),
45419						b: parseInt(cal.data('colpick').fields.eq(6).val(), 10)
45420					});
45421					fillRGBFields(col, cal.get(0));
45422					fillHexFields(col, cal.get(0));
45423				} else {
45424					cal.data('colpick').color = col = rgbToHsb(fixRGB({
45425						r: parseInt(cal.data('colpick').fields.eq(1).val(), 10),
45426						g: parseInt(cal.data('colpick').fields.eq(2).val(), 10),
45427						b: parseInt(cal.data('colpick').fields.eq(3).val(), 10)
45428					}));
45429					fillHexFields(col, cal.get(0));
45430					fillHSBFields(col, cal.get(0));
45431				}
45432				setSelector(col, cal.get(0));
45433				setHue(col, cal.get(0));
45434				setNewColor(col, cal.get(0));
45435				cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]);
45436			},
45437			//Change style on blur and on focus of inputs
45438			blur = function (ev) {
45439				$(this).parent().removeClass('colpick_focus');
45440			},
45441			focus = function() {
45442				$(this).parent().parent().data('colpick').fields.parent().removeClass('colpick_focus');
45443				$(this).parent().addClass('colpick_focus');
45444			},
45445			//Increment/decrement arrows functions
45446			downIncrement = function (ev) {
45447				ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
45448				var field = $(this).parent().find('input').focus();
45449				var current = {
45450					el: $(this).parent().addClass('colpick_slider'),
45451					max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255),
45452					y: ev.pageY,
45453					field: field,
45454					val: parseInt(field.val(), 10),
45455					preview: $(this).parent().parent().data('colpick').livePreview
45456				};
45457				$(document).mouseup(current, upIncrement);
45458				$(document).mousemove(current, moveIncrement);
45459			},
45460			moveIncrement = function (ev) {
45461				ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val - ev.pageY + ev.data.y, 10))));
45462				if (ev.data.preview) {
45463					change.apply(ev.data.field.get(0), [true]);
45464				}
45465				return false;
45466			},
45467			upIncrement = function (ev) {
45468				change.apply(ev.data.field.get(0), [true]);
45469				ev.data.el.removeClass('colpick_slider').find('input').focus();
45470				$(document).unbind('mouseup', upIncrement);
45471				$(document).unbind('mousemove', moveIncrement);
45472				return false;
45473			},
45474			//Hue slider functions
45475			downHue = function (ev) {
45476				ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
45477				var current = {
45478					cal: $(this).parent(),
45479					y: $(this).offset().top
45480				};
45481				$(document).bind('mouseup touchend',current,upHue);
45482				$(document).bind('mousemove touchmove',current,moveHue);
45483				
45484				var pageY = ((ev.type == 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY );
45485				change.apply(
45486					current.cal.data('colpick')
45487					.fields.eq(4).val(parseInt(360*(current.cal.data('colpick').height - (pageY - current.y))/current.cal.data('colpick').height, 10))
45488						.get(0),
45489					[current.cal.data('colpick').livePreview]
45490				);
45491				return false;
45492			},
45493			moveHue = function (ev) {
45494				var pageY = ((ev.type == 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY );
45495				change.apply(
45496					ev.data.cal.data('colpick')
45497					.fields.eq(4).val(parseInt(360*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.y))))/ev.data.cal.data('colpick').height, 10))
45498						.get(0),
45499					[ev.data.preview]
45500				);
45501				return false;
45502			},
45503			upHue = function (ev) {
45504				fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
45505				fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
45506				$(document).unbind('mouseup touchend',upHue);
45507				$(document).unbind('mousemove touchmove',moveHue);
45508				return false;
45509			},
45510			//Color selector functions
45511			downSelector = function (ev) {
45512				ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
45513				var current = {
45514					cal: $(this).parent(),
45515					pos: $(this).offset()
45516				};
45517				current.preview = current.cal.data('colpick').livePreview;
45518				
45519				$(document).bind('mouseup touchend',current,upSelector);
45520				$(document).bind('mousemove touchmove',current,moveSelector);
45521
45522				var payeX,pageY;
45523				if(ev.type == 'touchstart') {
45524					pageX = ev.originalEvent.changedTouches[0].pageX,
45525					pageY = ev.originalEvent.changedTouches[0].pageY;
45526				} else {
45527					pageX = ev.pageX;
45528					pageY = ev.pageY;
45529				}
45530
45531				change.apply(
45532					current.cal.data('colpick').fields
45533					.eq(6).val(parseInt(100*(current.cal.data('colpick').height - (pageY - current.pos.top))/current.cal.data('colpick').height, 10)).end()
45534					.eq(5).val(parseInt(100*(pageX - current.pos.left)/current.cal.data('colpick').height, 10))
45535					.get(0),
45536					[current.preview]
45537				);
45538				return false;
45539			},
45540			moveSelector = function (ev) {
45541				var payeX,pageY;
45542				if(ev.type == 'touchmove') {
45543					pageX = ev.originalEvent.changedTouches[0].pageX,
45544					pageY = ev.originalEvent.changedTouches[0].pageY;
45545				} else {
45546					pageX = ev.pageX;
45547					pageY = ev.pageY;
45548				}
45549
45550				change.apply(
45551					ev.data.cal.data('colpick').fields
45552					.eq(6).val(parseInt(100*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.pos.top))))/ev.data.cal.data('colpick').height, 10)).end()
45553					.eq(5).val(parseInt(100*(Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageX - ev.data.pos.left))))/ev.data.cal.data('colpick').height, 10))
45554					.get(0),
45555					[ev.data.preview]
45556				);
45557				return false;
45558			},
45559			upSelector = function (ev) {
45560				fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
45561				fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
45562				$(document).unbind('mouseup touchend',upSelector);
45563				$(document).unbind('mousemove touchmove',moveSelector);
45564				return false;
45565			},
45566			//Submit button
45567			clickSubmit = function (ev) {
45568				var cal = $(this).parent();
45569				var col = cal.data('colpick').color;
45570				cal.data('colpick').origColor = col;
45571				setCurrentColor(col, cal.get(0));
45572				cal.data('colpick').onSubmit(col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el);
45573			},
45574			//Show/hide the color picker
45575			show = function (ev) {
45576				// Prevent the trigger of any direct parent
45577				ev.stopPropagation();
45578				var cal = $('#' + $(this).data('colpickId'));
45579				cal.data('colpick').onBeforeShow.apply(this, [cal.get(0)]);
45580				var pos = $(this).offset();
45581				var top = pos.top + this.offsetHeight;
45582				var left = pos.left;
45583				var viewPort = getViewport();
45584				var calW = cal.width();
45585				if (left + calW > viewPort.l + viewPort.w) {
45586					left -= calW;
45587				}
45588				cal.css({left: left + 'px', top: top + 'px'});
45589				if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) != false) {
45590					cal.show();
45591				}
45592				//Hide when user clicks outside
45593				$('html').mousedown({cal:cal}, hide);
45594				cal.mousedown(function(ev){ev.stopPropagation();})
45595			},
45596			hide = function (ev) {
45597				if (ev.data.cal.data('colpick').onHide.apply(this, [ev.data.cal.get(0)]) != false) {
45598					ev.data.cal.hide();
45599				}
45600				$('html').unbind('mousedown', hide);
45601			},
45602			getViewport = function() {
45603				var m = document.compatMode == 'CSS1Compat';
45604				return {
45605					l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft),
45606					w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth)
45607				};
45608			},
45609			//Fix the values if the user enters a negative or high value
45610			fixHSB = function (hsb) {
45611				return {
45612					h: Math.min(360, Math.max(0, hsb.h)),
45613					s: Math.min(100, Math.max(0, hsb.s)),
45614					b: Math.min(100, Math.max(0, hsb.b))
45615				};
45616			}, 
45617			fixRGB = function (rgb) {
45618				return {
45619					r: Math.min(255, Math.max(0, rgb.r)),
45620					g: Math.min(255, Math.max(0, rgb.g)),
45621					b: Math.min(255, Math.max(0, rgb.b))
45622				};
45623			},
45624			fixHex = function (hex) {
45625				var len = 6 - hex.length;
45626				if (len > 0) {
45627					var o = [];
45628					for (var i=0; i<len; i++) {
45629						o.push('0');
45630					}
45631					o.push(hex);
45632					hex = o.join('');
45633				}
45634				return hex;
45635			},
45636			restoreOriginal = function() {
45637				var cal = $(this).parent();
45638				var col = cal.data('colpick').origColor;
45639				cal.data('colpick').color = col;
45640				fillRGBFields(col, cal.get(0));
45641				fillHexFields(col, cal.get(0));
45642				fillHSBFields(col, cal.get(0));
45643				setSelector(col, cal.get(0));
45644				setHue(col, cal.get(0));
45645				setNewColor(col, cal.get(0));
45646			};
45647		return {
45648			init: function (opt) {
45649				opt = $.extend({}, defaults, opt||{});
45650				//Set color
45651				if (typeof opt.color == 'string') {
45652					opt.color = hexToHsb(opt.color);
45653				} else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) {
45654					opt.color = rgbToHsb(opt.color);
45655				} else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) {
45656					opt.color = fixHSB(opt.color);
45657				} else {
45658					return this;
45659				}
45660				
45661				//For each selected DOM element
45662				return this.each(function() {
45663					//If the element does not have an ID
45664					if (!$(this).data('colpickId')) {
45665						var options = $.extend({}, opt);
45666						options.origColor = opt.color;
45667						//Generate and assign a random ID
45668						var id = 'collorpicker_' + parseInt(Math.random() * 1000);
45669						$(this).data('colpickId', id);
45670						//Set the tpl's ID and get the HTML
45671						var cal = $(tpl).attr('id', id);
45672						//Add class according to layout
45673						cal.addClass('colpick_'+options.layout+(options.submit?'':' colpick_'+options.layout+'_ns'));
45674						//Add class if the color scheme is not default
45675						if(options.colorScheme != 'light') {
45676							cal.addClass('colpick_'+options.colorScheme);
45677						}
45678						//Setup submit button
45679						cal.find('div.colpick_submit').html(options.submitText).click(clickSubmit);
45680						//Setup input fields
45681						options.fields = cal.find('input').change(change).blur(blur).focus(focus);
45682						cal.find('div.colpick_field_arrs').mousedown(downIncrement).end().find('div.colpick_current_color').click(restoreOriginal);
45683						//Setup hue selector
45684						options.selector = cal.find('div.colpick_color').bind('mousedown touchstart',downSelector);
45685						options.selectorIndic = options.selector.find('div.colpick_selector_outer');
45686						//Store parts of the plugin
45687						options.el = this;
45688						options.hue = cal.find('div.colpick_hue_arrs');
45689						huebar = options.hue.parent();
45690						//Paint the hue bar
45691						var UA = navigator.userAgent.toLowerCase();
45692						var isIE = navigator.appName === 'Microsoft Internet Explorer';
45693						var IEver = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0;
45694						var ngIE = ( isIE && IEver < 10 );
45695						var stops = ['#ff0000','#ff0080','#ff00ff','#8000ff','#0000ff','#0080ff','#00ffff','#00ff80','#00ff00','#80ff00','#ffff00','#ff8000','#ff0000'];
45696						if(ngIE) {
45697							var i, div;
45698							for(i=0; i<=11; i++) {
45699								div = $('<div></div>').attr('style','height:8.333333%; filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+')";');
45700								huebar.append(div);
45701							}
45702						} else {
45703							stopList = stops.join(',');
45704							huebar.attr('style','background:-webkit-linear-gradient(top,'+stopList+'); background: -o-linear-gradient(top,'+stopList+'); background: -ms-linear-gradient(top,'+stopList+'); background:-moz-linear-gradient(top,'+stopList+'); -webkit-linear-gradient(top,'+stopList+'); background:linear-gradient(to bottom,'+stopList+'); ');
45705						}
45706						cal.find('div.colpick_hue').bind('mousedown touchstart',downHue);
45707						options.newColor = cal.find('div.colpick_new_color');
45708						options.currentColor = cal.find('div.colpick_current_color');
45709						//Store options and fill with default color
45710						cal.data('colpick', options);
45711						fillRGBFields(options.color, cal.get(0));
45712						fillHSBFields(options.color, cal.get(0));
45713						fillHexFields(options.color, cal.get(0));
45714						setHue(options.color, cal.get(0));
45715						setSelector(options.color, cal.get(0));
45716						setCurrentColor(options.color, cal.get(0));
45717						setNewColor(options.color, cal.get(0));
45718						//Append to body if flat=false, else show in place
45719						if (options.flat) {
45720							cal.appendTo(this).show();
45721							cal.css({
45722								position: 'relative',
45723								display: 'block'
45724							});
45725						} else {
45726							cal.appendTo(document.body);
45727							$(this).bind(options.showEvent, show);
45728							cal.css({
45729								position:'absolute'
45730							});
45731						}
45732					}
45733				});
45734			},
45735			//Shows the picker
45736			showPicker: function() {
45737				return this.each( function() {
45738					if ($(this).data('colpickId')) {
45739						show.apply(this);
45740					}
45741				});
45742			},
45743			//Hides the picker
45744			hidePicker: function() {
45745				return this.each( function() {
45746					if ($(this).data('colpickId')) {
45747						$('#' + $(this).data('colpickId')).hide();
45748				        $('html').unbind('mousedown', hide);
45749					}
45750				});
45751			},
45752			//Sets a color as new and current (default)
45753			setColor: function(col, setCurrent) {
45754				setCurrent = (typeof setCurrent === "undefined") ? 1 : setCurrent;
45755				if (typeof col == 'string') {
45756					col = hexToHsb(col);
45757				} else if (col.r != undefined && col.g != undefined && col.b != undefined) {
45758					col = rgbToHsb(col);
45759				} else if (col.h != undefined && col.s != undefined && col.b != undefined) {
45760					col = fixHSB(col);
45761				} else {
45762					return this;
45763				}
45764				return this.each(function(){
45765					if ($(this).data('colpickId')) {
45766						var cal = $('#' + $(this).data('colpickId'));
45767						cal.data('colpick').color = col;
45768						cal.data('colpick').origColor = col;
45769						fillRGBFields(col, cal.get(0));
45770						fillHSBFields(col, cal.get(0));
45771						fillHexFields(col, cal.get(0));
45772						setHue(col, cal.get(0));
45773						setSelector(col, cal.get(0));
45774						
45775						setNewColor(col, cal.get(0));
45776						cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]);
45777						if(setCurrent) {
45778							setCurrentColor(col, cal.get(0));
45779						}
45780					}
45781				});
45782			}
45783		};
45784	}();
45785	//Color space convertions
45786	var hexToRgb = function (hex) {
45787		var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
45788		return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)};
45789	};
45790	var hexToHsb = function (hex) {
45791		return rgbToHsb(hexToRgb(hex));
45792	};
45793	var rgbToHsb = function (rgb) {
45794		var hsb = {h: 0, s: 0, b: 0};
45795		var min = Math.min(rgb.r, rgb.g, rgb.b);
45796		var max = Math.max(rgb.r, rgb.g, rgb.b);
45797		var delta = max - min;
45798		hsb.b = max;
45799		hsb.s = max != 0 ? 255 * delta / max : 0;
45800		if (hsb.s != 0) {
45801			if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta;
45802			else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta;
45803			else hsb.h = 4 + (rgb.r - rgb.g) / delta;
45804		} else hsb.h = -1;
45805		hsb.h *= 60;
45806		if (hsb.h < 0) hsb.h += 360;
45807		hsb.s *= 100/255;
45808		hsb.b *= 100/255;
45809		return hsb;
45810	};
45811	var hsbToRgb = function (hsb) {
45812		var rgb = {};
45813		var h = hsb.h;
45814		var s = hsb.s*255/100;
45815		var v = hsb.b*255/100;
45816		if(s == 0) {
45817			rgb.r = rgb.g = rgb.b = v;
45818		} else {
45819			var t1 = v;
45820			var t2 = (255-s)*v/255;
45821			var t3 = (t1-t2)*(h%60)/60;
45822			if(h==360) h = 0;
45823			if(h<60) {rgb.r=t1;	rgb.b=t2; rgb.g=t2+t3}
45824			else if(h<120) {rgb.g=t1; rgb.b=t2;	rgb.r=t1-t3}
45825			else if(h<180) {rgb.g=t1; rgb.r=t2;	rgb.b=t2+t3}
45826			else if(h<240) {rgb.b=t1; rgb.r=t2;	rgb.g=t1-t3}
45827			else if(h<300) {rgb.b=t1; rgb.g=t2;	rgb.r=t2+t3}
45828			else if(h<360) {rgb.r=t1; rgb.g=t2;	rgb.b=t1-t3}
45829			else {rgb.r=0; rgb.g=0;	rgb.b=0}
45830		}
45831		return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)};
45832	};
45833	var rgbToHex = function (rgb) {
45834		var hex = [
45835			rgb.r.toString(16),
45836			rgb.g.toString(16),
45837			rgb.b.toString(16)
45838		];
45839		$.each(hex, function (nr, val) {
45840			if (val.length == 1) {
45841				hex[nr] = '0' + val;
45842			}
45843		});
45844		return hex.join('');
45845	};
45846	var hsbToHex = function (hsb) {
45847		return rgbToHex(hsbToRgb(hsb));
45848	};
45849	$.fn.extend({
45850		colpick: colpick.init,
45851		colpickHide: colpick.hidePicker,
45852		colpickShow: colpick.showPicker,
45853		colpickSetColor: colpick.setColor
45854	});
45855	$.extend({
45856		colpick:{ 
45857			rgbToHex: rgbToHex,
45858			rgbToHsb: rgbToHsb,
45859			hsbToHex: hsbToHex,
45860			hsbToRgb: hsbToRgb,
45861			hexToHsb: hexToHsb,
45862			hexToRgb: hexToRgb
45863		}
45864	});
45865})(jQuery);
45866(function() {
45867  this.LightsUI = {};
45868
45869  $(function() {
45870    var $component_info, $lights_container, add_light, info_view, light_env, light_env_view, lights, remove_info_view, setup_info_view, store_lights, trigger_change;
45871    $lights_container = $('#lights-list');
45872    lights = new LightsUI.ItemLightCollection;
45873    light_env = new LightsUI.RoomEnvironment({
45874      setting: 'day',
45875      intensity: 30,
45876      azimuth: 45,
45877      altitude: 60
45878    });
45879    light_env_view = new LightsUI.RoomEnvironmentView({
45880      model: light_env
45881    });
45882    $('#lights-env').append(light_env_view.render().el);
45883    light_env_view.init_slider();
45884    add_light = function(attrs) {
45885      var light, view;
45886      light = new LightsUI.ItemLight(attrs);
45887      lights.add(light);
45888      view = new LightsUI.ItemLightView({
45889        model: light
45890      });
45891      $lights_container.append(view.render().el);
45892      return view.init_slider();
45893    };
45894    store_lights = function(room) {
45895      var model;
45896      model = room.model;
45897      model.light_env = light_env.attributes;
45898      return _.each(model.components, function(c) {
45899        var light;
45900        if (light = lights.get(c.ID)) {
45901          return c.light = _.pick(light.attributes, 'on', 'wattage', 'color');
45902        }
45903      });
45904    };
45905    trigger_change = function() {
45906      return myDeco.App.model.operationEnd(['lights']);
45907    };
45908    light_env.bind('change', function(model, options) {
45909      if (options != null ? options.source : void 0) {
45910        return trigger_change();
45911      }
45912    });
45913    lights.bind('change', trigger_change);
45914    $component_info = $('#view-component-info');
45915    info_view = null;
45916    remove_info_view = function() {
45917      info_view.remove_with_picker();
45918      return info_view = null;
45919    };
45920    setup_info_view = function(light) {
45921      info_view = new LightsUI.ItemLightView({
45922        model: light
45923      });
45924      $component_info.append(info_view.render().el);
45925      return info_view.init_slider();
45926    };
45927    $('body').bind('componentAdded', function(e, c) {
45928      if (c.light) {
45929        return add_light(_.extend({
45930          id: c.ID,
45931          name: c.componentData.name,
45932          render_path: c.render_path
45933        }, c.light));
45934      }
45935    }).bind('componentRemoved', function(e, c) {
45936      return lights.remove(c.ID);
45937    }).bind('roomCleared', function() {
45938      lights.reset();
45939      return $lights_container.html('');
45940    }).bind('roomLoaded', function() {
45941      return light_env.set(myDeco.App.model.light_env);
45942    }).bind('showInfo', function(event, obj) {
45943      var light;
45944      if (info_view) {
45945        remove_info_view();
45946      }
45947      if (obj) {
45948        light = lights.get(obj.ID);
45949        if (light) {
45950          return setup_info_view(light);
45951        }
45952      }
45953    });
45954    return myDeco.settings.SAVE_CALLBACKS.push(store_lights);
45955  });
45956
45957}).call(this);
45958(function() {
45959  LightsUI.ItemLightCollection = Backbone.Collection.extend({
45960    model: LightsUI.ItemLight
45961  });
45962
45963}).call(this);
45964(function() {
45965  LightsUI.ItemLight = Backbone.Model.extend({
45966    initialize: function() {
45967      return this.set('thumb', this.get('render_path').replace('/cdb/renders/', '/cdb/scaled/renders/') + '_w110h110.png');
45968    },
45969    toggle: function() {
45970      return this.set('on', !this.get('on'));
45971    }
45972  });
45973
45974}).call(this);
45975(function() {
45976  LightsUI.RoomEnvironment = Backbone.Model.extend({
45977    toggle_setting: function(options) {
45978      var new_value;
45979      new_value = this.get('setting') === 'day' ? 'night' : 'day';
45980      return this.set('setting', new_value, options);
45981    }
45982  });
45983
45984}).call(this);
45985(function() { this.JST || (this.JST = {}); this.JST["lights/templates/item-light"] = function template(locals) {
45986  var buf = [];
45987  var jade_mixins = {};
45988  var jade_interp;
45989  ;var locals_for_with = (locals || {});(function (gettext, name, on, thumb, wattage, wattage_fixed) {
45990  buf.push("<img" + (jade.attr("src", thumb, true, false)) + " class=\"image\"/><div class=\"title\">" + (jade.escape(null == (jade_interp = name) ? "" : jade_interp)) + "</div><div class=\"toggle on-off-toggle\"><div data-value=\"on\"" + (jade.cls(['option',(on ? 'active' : '')], [null,true])) + ">" + (jade.escape(null == (jade_interp = gettext("On")) ? "" : jade_interp)) + "</div><div data-value=\"off\"" + (jade.cls(['option',(!on ? 'active' : '')], [null,true])) + ">" + (jade.escape(null == (jade_interp = gettext("Off")) ? "" : jade_interp)) + "</div></div><div" + (jade.cls(['slider','wattage',(wattage_fixed ? 'disabled' : '')], [null,null,true])) + "><div class=\"slider-track\"><div" + (jade.attr("title", wattage, true, false)) + " class=\"slider-handle\"></div></div></div>");}.call(this,"gettext" in locals_for_with?locals_for_with.gettext:typeof gettext!=="undefined"?gettext:undefined,"name" in locals_for_with?locals_for_with.name:typeof name!=="undefined"?name:undefined,"on" in locals_for_with?locals_for_with.on:typeof on!=="undefined"?on:undefined,"thumb" in locals_for_with?locals_for_with.thumb:typeof thumb!=="undefined"?thumb:undefined,"wattage" in locals_for_with?locals_for_with.wattage:typeof wattage!=="undefined"?wattage:undefined,"wattage_fixed" in locals_for_with?locals_for_with.wattage_fixed:typeof wattage_fixed!=="undefined"?wattage_fixed:undefined));;return buf.join("");
45991  };
45992}).call(this);
45993(function() { this.JST || (this.JST = {}); this.JST["lights/templates/room-light-env"] = function template(locals) {
45994  var buf = [];
45995  var jade_mixins = {};
45996  var jade_interp;
45997  ;var locals_for_with = (locals || {});(function (gettext, intensity, setting) {
45998  buf.push("<div class=\"general\"><div class=\"title\">" + (jade.escape(null == (jade_interp = gettext('General lighting settings')) ? "" : jade_interp)) + "</div><div class=\"toggle day-night-toggle\"><div data-value=\"day\"" + (jade.cls(['option',(setting == 'day' ? "active" : '')], [null,true])) + ">" + (jade.escape(null == (jade_interp = gettext('Day')) ? "" : jade_interp)) + "</div><div data-value=\"night\"" + (jade.cls(['option',(setting == 'night' ? "active" : '')], [null,true])) + ">" + (jade.escape(null == (jade_interp = gettext('Night')) ? "" : jade_interp)) + "</div></div><div class=\"slider intensity\"><div class=\"slider-track\"><div" + (jade.attr("title", intensity, true, false)) + " class=\"slider-handle\"></div></div></div></div><div class=\"sun-position\"><div class=\"title\">" + (jade.escape(null == (jade_interp = gettext('Position of the Sun')) ? "" : jade_interp)) + "</div><label>" + (jade.escape(null == (jade_interp = gettext('Direction')) ? "" : jade_interp)) + "</label><select class=\"azimuth\"><option value=\"0\">" + (jade.escape(null == (jade_interp = gettext('North')) ? "" : jade_interp)) + "</option><option value=\"45\">" + (jade.escape(null == (jade_interp = gettext('Northeast')) ? "" : jade_interp)) + "</option><option value=\"90\">" + (jade.escape(null == (jade_interp = gettext('East')) ? "" : jade_interp)) + "</option><option value=\"135\">" + (jade.escape(null == (jade_interp = gettext('Southeast')) ? "" : jade_interp)) + "</option><option value=\"180\">" + (jade.escape(null == (jade_interp = gettext('South')) ? "" : jade_interp)) + "</option><option value=\"225\">" + (jade.escape(null == (jade_interp = gettext('Southwest')) ? "" : jade_interp)) + "</option><option value=\"270\">" + (jade.escape(null == (jade_interp = gettext('West')) ? "" : jade_interp)) + "</option><option value=\"315\">" + (jade.escape(null == (jade_interp = gettext('Northwest')) ? "" : jade_interp)) + "</option></select><br/><label>" + (jade.escape(null == (jade_interp = gettext('Altitude')) ? "" : jade_interp)) + "</label><select class=\"altitude\"><option value=\"15\">" + (jade.escape(null == (jade_interp = gettext('low')) ? "" : jade_interp)) + "</option><option value=\"40\">" + (jade.escape(null == (jade_interp = gettext('medium')) ? "" : jade_interp)) + "</option><option value=\"60\">" + (jade.escape(null == (jade_interp = gettext('high')) ? "" : jade_interp)) + "</option></select></div>");}.call(this,"gettext" in locals_for_with?locals_for_with.gettext:typeof gettext!=="undefined"?gettext:undefined,"intensity" in locals_for_with?locals_for_with.intensity:typeof intensity!=="undefined"?intensity:undefined,"setting" in locals_for_with?locals_for_with.setting:typeof setting!=="undefined"?setting:undefined));;return buf.join("");
45999  };
46000}).call(this);
46001(function() {
46002  LightsUI.ItemLightView = Backbone.View.extend({
46003    className: 'light-view',
46004    template: JST["lights/templates/item-light"],
46005    initialize: function() {
46006      _.bindAll(this, 'change_wattage', 'change_color', 'toggle_light', 'remove_with_picker', 'wattage_changed', 'color_changed', 'state_changed');
46007      this.listenTo(this.model, 'remove', this.remove_with_picker);
46008      this.listenTo(this.model, 'change:wattage', this.wattage_changed);
46009      this.listenTo(this.model, 'change:color', this.color_changed);
46010      return this.listenTo(this.model, 'change:on', this.state_changed);
46011    },
46012    render: function() {
46013      var acolor, hex, picker, rgb;
46014      this.$el.html(this.template(this.model.attributes));
46015      acolor = this.model.attributes.color;
46016      rgb = {
46017        r: acolor[0],
46018        g: acolor[1],
46019        b: acolor[2]
46020      };
46021      hex = $.colpick.rgbToHex(rgb);
46022      picker = this.$('.picker');
46023      picker.css('background-color', '#' + hex);
46024      if (!this.model.get('color_fixed')) {
46025        picker.colpick({
46026          layout: 'rgbhex',
46027          color: rgb,
46028          onSubmit: this.change_color
46029        });
46030      }
46031      this.$('.on-off-toggle .option').click(this.toggle_light);
46032      return this;
46033    },
46034    init_slider: function() {
46035      var wattage;
46036      wattage = this.model.attributes.wattage;
46037      this.$('.wattage').slider({
46038        value: wattage,
46039        onChange: this.change_wattage,
46040        max: Math.max(wattage, 100)
46041      });
46042      return this.$(".wattage").toggle(this.model.attributes.on);
46043    },
46044    remove_with_picker: function() {
46045      var picker_id;
46046      if (!this.model.get('color_fixed')) {
46047        picker_id = this.$('.picker').data('colpickId');
46048        $('#' + picker_id).remove();
46049      }
46050      return this.remove();
46051    },
46052    change_wattage: function(value) {
46053      return this.model.set("wattage", Math.round(value), {
46054        source: this
46055      });
46056    },
46057    change_color: function(hsb, hex, rgb, el, not_from_ui) {
46058      if (!not_from_ui) {
46059        this.$('.picker').colpickHide();
46060        return this.model.set("color", [rgb.r, rgb.g, rgb.b]);
46061      }
46062    },
46063    toggle_light: function(e) {
46064      var value;
46065      value = $(e.target).data('value') === 'on';
46066      return this.model.set("on", value);
46067    },
46068    state_changed: function(model, value) {
46069      var str_value;
46070      this.$('.on-off-toggle .option').removeClass('active');
46071      str_value = value ? 'on' : 'off';
46072      this.$(".on-off-toggle .option[data-value=" + str_value + "]").addClass('active');
46073      return this.$(".wattage").toggle(value);
46074    },
46075    wattage_changed: function(model, value, options) {
46076      this.$('.wattage .slider-handle').attr('title', value);
46077      if (options.source !== this) {
46078        return this.$('.wattage').trigger('set-slider-value', {
46079          value: value
46080        });
46081      }
46082    },
46083    color_changed: function(model, value, options) {
46084      var hex, rgb;
46085      rgb = {
46086        r: value[0],
46087        g: value[1],
46088        b: value[2]
46089      };
46090      hex = $.colpick.rgbToHex(rgb);
46091      return this.$('.picker').css('background-color', "#" + hex).colpickSetColor(hex, true);
46092    }
46093  });
46094
46095}).call(this);
46096(function() {
46097  LightsUI.RoomEnvironmentView = Backbone.View.extend({
46098    className: 'room-light-env-view',
46099    template: JST["lights/templates/room-light-env"],
46100    initialize: function() {
46101      _.bindAll(this, 'change_intensity', 'toggle_setting', 'setting_changed', 'intensity_changed', 'change_azimuth', 'change_altitude', 'azimuth_changed', 'altitude_changed');
46102      this.listenTo(this.model, 'change:setting', this.setting_changed);
46103      this.listenTo(this.model, 'change:intensity', this.intensity_changed);
46104      this.listenTo(this.model, 'change:azimuth', this.azimuth_changed);
46105      return this.listenTo(this.model, 'change:altitude', this.altitude_changed);
46106    },
46107    render: function() {
46108      this.$el.html(this.template(this.model.attributes));
46109      this.$('.day-night-toggle').click(this.toggle_setting);
46110      this.$('.azimuth').val(this.model.attributes.azimuth).change(this.change_azimuth);
46111      this.$('.altitude').val(this.model.attributes.altitude).change(this.change_altitude);
46112      return this;
46113    },
46114    init_slider: function() {
46115      this.$('.intensity').slider({
46116        value: this.model.attributes.intensity,
46117        onChange: this.change_intensity
46118      });
46119      return this.$(".intensity").toggle(this.model.attributes.setting === 'day');
46120    },
46121    change_intensity: function(value) {
46122      return this.model.set("intensity", Math.round(value), {
46123        source: this
46124      });
46125    },
46126    change_azimuth: function(e) {
46127      var value;
46128      value = parseInt($(e.target).val());
46129      return this.model.set("azimuth", value, {
46130        source: this
46131      });
46132    },
46133    change_altitude: function(e) {
46134      var value;
46135      value = parseInt($(e.target).val());
46136      return this.model.set("altitude", value, {
46137        source: this
46138      });
46139    },
46140    toggle_setting: function(e) {
46141      var value;
46142      value = $(e.target).data('value');
46143      return this.model.set("setting", value, {
46144        source: this
46145      });
46146    },
46147    setting_changed: function(model, value) {
46148      this.$('.day-night-toggle .option').removeClass('active');
46149      this.$(".day-night-toggle .option[data-value=" + value + "]").addClass('active');
46150      return this.$(".intensity").toggle(value === 'day');
46151    },
46152    intensity_changed: function(model, value, options) {
46153      this.$('.intensity .slider-handle').attr('title', value);
46154      if (options.source !== this) {
46155        return this.$('.intensity').trigger('set-slider-value', {
46156          value: value
46157        });
46158      }
46159    },
46160    azimuth_changed: function(model, value, options) {
46161      if (options.source !== this) {
46162        return this.$('.azimuth').val(value);
46163      }
46164    },
46165    altitude_changed: function(model, value, options) {
46166      if (options.source !== this) {
46167        return this.$('.altitude').val(value);
46168      }
46169    }
46170  });
46171
46172}).call(this);
46173var RoomstylerLive = {
46174    render: function(model) {
46175        var render_url = myDeco.urls.live;
46176        if (model && render_url) {
46177            
46178            // let plugins add data to the room before saving it            
46179            var roomData = {model: model};
46180            for (var i=0; i < myDeco.settings.SAVE_CALLBACKS.length; i++) {
46181                myDeco.settings.SAVE_CALLBACKS[i](roomData);
46182            }
46183            
46184            $.ajax({
46185            	type: 'POST',
46186			    url: render_url,
46187			    contentType: "application/json",
46188			    dataType: "json",
46189			    data: JSON.stringify(roomData.model)
46190            });
46191        }
46192    }
46193};
46194/*globals myDeco, $ */
46195/*jslint  browser: true, forin: true, nomen: true, plusplus: true, regexp: true,
46196          sloppy: true, white: true */
46197$(function() {
46198  if(myDeco.browser.isIOSWebView) {
46199    document.getElementById('user_section').style.display = 'none';
46200    document.getElementById('guest_section').style.display = 'none';
46201    document.querySelector('#menu .help.menu').style.display = 'none';
46202  }
46203});
46204
46205
46206
46207
46208
46209
46210
46211
46212
46213
46214
46215
46216
46217
46218
46219
46220
46221
46222
46223
46224
46225
46226
46227
46228
46229
46230
46231
46232
46233
46234
46235
46236
46237
46238
46239
46240
46241
46242
46243
46244
46245
46246
46247
46248
46249
46250
46251
46252
46253
46254
46255
46256
46257
46258
46259
46260
46261
46262
46263
46264
46265
46266
46267
46268
46269
46270
46271
46272
46273
46274
46275
46276
46277
46278
46279
46280
46281
46282
46283
46284
46285
46286
46287
46288
46289
46290
46291
46292
46293
46294
46295
46296
46297
46298
46299
46300
46301
46302
46303
46304
46305
46306
46307
46308
46309
46310
46311
46312
46313
46314
46315
46316
46317
46318
46319
46320
46321
46322
46323
46324
46325
46326
46327
46328
46329
46330
46331
46332
46333
46334
46335
46336
46337
46338
46339
46340
46341
46342
46343
46344
46345
46346
46347
46348
46349
46350
46351
46352
46353
46354
46355
46356

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.